From 4b94b0b561d233327931028c9d23fdf9ec781776 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Fri, 10 Nov 2023 13:29:20 -0500 Subject: [PATCH] LibWeb: Begin implementing the Clipboard API https://w3c.github.io/clipboard-apis/ This implements enough for navigator.clipboard.writeText(String). --- .../LibWeb/BindingsGenerator/Namespaces.h | 1 + Tests/LibWeb/Text/expected/clipboard.txt | 2 + Tests/LibWeb/Text/input/clipboard.html | 26 +++ Userland/Libraries/LibWeb/CMakeLists.txt | 1 + .../Libraries/LibWeb/Clipboard/Clipboard.cpp | 198 ++++++++++++++++++ .../Libraries/LibWeb/Clipboard/Clipboard.h | 33 +++ .../Libraries/LibWeb/Clipboard/Clipboard.idl | 12 ++ Userland/Libraries/LibWeb/Forward.h | 4 + .../Libraries/LibWeb/HTML/EventLoop/Task.h | 6 + Userland/Libraries/LibWeb/HTML/Navigator.cpp | 9 + Userland/Libraries/LibWeb/HTML/Navigator.h | 4 + Userland/Libraries/LibWeb/HTML/Navigator.idl | 4 + Userland/Libraries/LibWeb/Page/Page.h | 2 + Userland/Libraries/LibWeb/idl_files.cmake | 1 + 14 files changed, 303 insertions(+) create mode 100644 Tests/LibWeb/Text/expected/clipboard.txt create mode 100644 Tests/LibWeb/Text/input/clipboard.html create mode 100644 Userland/Libraries/LibWeb/Clipboard/Clipboard.cpp create mode 100644 Userland/Libraries/LibWeb/Clipboard/Clipboard.h create mode 100644 Userland/Libraries/LibWeb/Clipboard/Clipboard.idl diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h index f615e9a603..9c17220c29 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/Namespaces.h @@ -14,6 +14,7 @@ namespace IDL { static constexpr Array libweb_interface_namespaces = { "CSS"sv, + "Clipboard"sv, "Crypto"sv, "DOM"sv, "DOMParsing"sv, diff --git a/Tests/LibWeb/Text/expected/clipboard.txt b/Tests/LibWeb/Text/expected/clipboard.txt new file mode 100644 index 0000000000..9c380cba5b --- /dev/null +++ b/Tests/LibWeb/Text/expected/clipboard.txt @@ -0,0 +1,2 @@ + Failure +Success diff --git a/Tests/LibWeb/Text/input/clipboard.html b/Tests/LibWeb/Text/input/clipboard.html new file mode 100644 index 0000000000..dfc6d391db --- /dev/null +++ b/Tests/LibWeb/Text/input/clipboard.html @@ -0,0 +1,26 @@ + + + diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index aa16ed53ee..18bc55b29c 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -20,6 +20,7 @@ set(SOURCES Bindings/MainThreadVM.cpp Bindings/OptionConstructor.cpp Bindings/PlatformObject.cpp + Clipboard/Clipboard.cpp Crypto/Crypto.cpp Crypto/SubtleCrypto.cpp CSS/Angle.cpp diff --git a/Userland/Libraries/LibWeb/Clipboard/Clipboard.cpp b/Userland/Libraries/LibWeb/Clipboard/Clipboard.cpp new file mode 100644 index 0000000000..8a07fbeb4f --- /dev/null +++ b/Userland/Libraries/LibWeb/Clipboard/Clipboard.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::Clipboard { + +WebIDL::ExceptionOr> Clipboard::construct_impl(JS::Realm& realm) +{ + return realm.heap().allocate(realm, realm); +} + +Clipboard::Clipboard(JS::Realm& realm) + : DOM::EventTarget(realm) +{ +} + +Clipboard::~Clipboard() = default; + +void Clipboard::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + set_prototype(&Bindings::ensure_web_prototype(realm, "Clipboard")); +} + +// https://w3c.github.io/clipboard-apis/#os-specific-well-known-format +static String os_specific_well_known_format(StringView mime_type_string) +{ + // NOTE: Here we always takes the Linux case, and defer to the chrome layer to handle OS specific implementations. + auto mime_type = MUST(MimeSniff::MimeType::parse(mime_type_string)); + + // 1. Let wellKnownFormat be an empty string. + String well_known_format {}; + + // 2. If mimeType’s essence is "text/plain", then + if (mime_type->essence() == "text/plain"sv) { + // On Windows, follow the convention described below: + // Assign CF_UNICODETEXT to wellKnownFormat. + // On MacOS, follow the convention described below: + // Assign NSPasteboardTypeString to wellKnownFormat. + // On Linux, ChromeOS, and Android, follow the convention described below: + // Assign "text/plain" to wellKnownFormat. + well_known_format = "text/plain"_string; + } + // 3. Else, if mimeType’s essence is "text/html", then + if (mime_type->essence() == "text/html"sv) { + // On Windows, follow the convention described below: + // Assign CF_HTML to wellKnownFormat. + // On MacOS, follow the convention described below: + // Assign NSHTMLPboardType to wellKnownFormat. + // On Linux, ChromeOS, and Android, follow the convention described below: + // Assign "text/html" to wellKnownFormat. + well_known_format = "text/html"_string; + } + // 4. Else, if mimeType’s essence is "image/png", then + if (mime_type->essence() == "image/png"sv) { + // On Windows, follow the convention described below: + // Assign "PNG" to wellKnownFormat. + // On MacOS, follow the convention described below: + // Assign NSPasteboardTypePNG to wellKnownFormat. + // On Linux, ChromeOS, and Android, follow the convention described below: + // Assign "image/png" to wellKnownFormat. + well_known_format = "image/png"_string; + } + + // 5. Return wellKnownFormat. + return well_known_format; +} + +// https://w3c.github.io/clipboard-apis/#write-blobs-and-option-to-the-clipboard +static void write_blobs_and_option_to_clipboard(JS::Realm& realm, ReadonlySpan> items, String presentation_style) +{ + auto& window = verify_cast(realm.global_object()); + + // FIXME: 1. Let webCustomFormats be a sequence. + + // 2. For each item in items: + for (auto const& item : items) { + // 1. Let formatString be the result of running os specific well-known format given item’s type. + auto format_string = os_specific_well_known_format(item->type()); + + // 2. If formatString is empty then follow the below steps: + if (format_string.is_empty()) { + // FIXME: 1. Let webCustomFormatString be the item’s type. + // FIXME: 2. Let webCustomFormat be an empty type. + // FIXME: 3. If webCustomFormatString starts with `"web "` prefix, then remove the `"web "` prefix and store the + // FIXME: remaining string in webMimeTypeString. + // FIXME: 4. Let webMimeType be the result of parsing a MIME type given webMimeTypeString. + // FIXME: 5. If webMimeType is failure, then abort all steps. + // FIXME: 6. Let webCustomFormat’s type's essence equal to webMimeType. + // FIXME: 7. Set item’s type to webCustomFormat. + // FIXME: 8. Append webCustomFormat to webCustomFormats. + } + + // 3. Let payload be the result of UTF-8 decoding item’s underlying byte sequence. + auto decoder = TextCodec::decoder_for("UTF-8"sv); + auto payload = MUST(TextCodec::convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(*decoder, item->bytes())); + + // 4. Insert payload and presentationStyle into the system clipboard using formatString as the native clipboard format. + if (auto* page = window.page()) + page->client().page_did_insert_clipboard_entry(move(payload), move(presentation_style), move(format_string)); + } + + // FIXME: 3. Write web custom formats given webCustomFormats. +} + +// https://w3c.github.io/clipboard-apis/#check-clipboard-write-permission +static bool check_clipboard_write_permission(JS::Realm& realm) +{ + // NOTE: The clipboard permission is undergoing a refactor because the clipboard-write permission was removed from + // the Permissions spec. So this partially implements the proposed update: + // https://pr-preview.s3.amazonaws.com/w3c/clipboard-apis/pull/164.html#write-permission + + // 1. Let hasGesture be true if the relevant global object of this has transient activation, false otherwise. + auto has_gesture = verify_cast(realm.global_object()).has_transient_activation(); + + // 2. If hasGesture then, + if (has_gesture) { + // FIXME: 1. Return true if the current script is running as a result of user interaction with a "cut" or "copy" + // element created by the user agent or operating system. + return true; + } + + // 3. Otherwise, return false. + return false; +} + +// https://w3c.github.io/clipboard-apis/#dom-clipboard-writetext +JS::NonnullGCPtr Clipboard::write_text(String data) +{ + // 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. Run the following steps in parallel: + Platform::EventLoopPlugin::the().deferred_invoke([&realm, promise, data = move(data)]() mutable { + // 1. Let r be the result of running check clipboard write permission. + auto result = check_clipboard_write_permission(realm); + + // 2. If r is false, then: + if (!result) { + // 1. Queue a global task on the permission task source, given realm’s global object, to reject p with + // "NotAllowedError" DOMException in realm. + queue_global_task(HTML::Task::Source::Permissions, realm.global_object(), [&realm, promise]() mutable { + HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm) }; + WebIDL::reject_promise(realm, promise, WebIDL::NotAllowedError::create(realm, "Clipboard writing is only allowed through user activation"_fly_string)); + }); + + // 2. Abort these steps. + return; + } + + // 1. Queue a global task on the clipboard task source, given realm’s global object, to perform the below steps: + queue_global_task(HTML::Task::Source::Clipboard, realm.global_object(), [&realm, promise, data = move(data)]() mutable { + // 1. Let itemList be an empty sequence. + Vector> item_list; + + // 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. + // Note: On Windows replace `\n` characters with `\r\n` in data before creating textBlob. + auto text_blob = FileAPI::Blob::create(realm, MUST(ByteBuffer::copy(data.bytes())), "text/plain;charset=utf-8"_string); + + // 3. Add textBlob to itemList. + item_list.append(text_blob); + + // 4. Let option be set to "unspecified". + auto option = "unspecified"_string; + + // 5. Write blobs and option to the clipboard with itemList and option. + write_blobs_and_option_to_clipboard(realm, item_list, move(option)); + + // 6. Resolve p. + HTML::TemporaryExecutionContext execution_context { Bindings::host_defined_environment_settings_object(realm) }; + WebIDL::resolve_promise(realm, promise, JS::js_undefined()); + }); + }); + + // 4. Return p. + return JS::NonnullGCPtr { verify_cast(*promise->promise()) }; +} + +} diff --git a/Userland/Libraries/LibWeb/Clipboard/Clipboard.h b/Userland/Libraries/LibWeb/Clipboard/Clipboard.h new file mode 100644 index 0000000000..21553d61b3 --- /dev/null +++ b/Userland/Libraries/LibWeb/Clipboard/Clipboard.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Web::Clipboard { + +class Clipboard final : public DOM::EventTarget { + WEB_PLATFORM_OBJECT(Clipboard, DOM::EventTarget); + +public: + static WebIDL::ExceptionOr> construct_impl(JS::Realm&); + virtual ~Clipboard() override; + + JS::NonnullGCPtr write_text(String); + +private: + Clipboard(JS::Realm&); + + virtual void initialize(JS::Realm&) override; +}; + +} diff --git a/Userland/Libraries/LibWeb/Clipboard/Clipboard.idl b/Userland/Libraries/LibWeb/Clipboard/Clipboard.idl new file mode 100644 index 0000000000..f902736c13 --- /dev/null +++ b/Userland/Libraries/LibWeb/Clipboard/Clipboard.idl @@ -0,0 +1,12 @@ +#import + +// FIXME: typedef sequence ClipboardItems; + +// https://w3c.github.io/clipboard-apis/#clipboard +[SecureContext, Exposed=Window] +interface Clipboard : EventTarget { + // FIXME: Promise read(); + // FIXME: Promise readText(); + // FIXME: Promise write(ClipboardItems data); + Promise writeText(DOMString data); +}; diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 42b33c64e7..6cfb0ddf5e 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -65,6 +65,10 @@ enum class ResponseType; enum class XMLHttpRequestResponseType; } +namespace Web::Clipboard { +class Clipboard; +} + namespace Web::Cookie { struct Cookie; struct ParsedCookie; diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h b/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h index 2bfb7f4fdc..5a7e88264c 100644 --- a/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h +++ b/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h @@ -47,6 +47,12 @@ public: // https://html.spec.whatwg.org/multipage/canvas.html#canvas-blob-serialisation-task-source CanvasBlobSerializationTask, + // https://w3c.github.io/clipboard-apis/#clipboard-task-source + Clipboard, + + // https://w3c.github.io/permissions/#permissions-task-source + Permissions, + // !!! IMPORTANT: Keep this field last! // This serves as the base value of all unique task sources. // Some elements, such as the HTMLMediaElement, must have a unique task source per instance. diff --git a/Userland/Libraries/LibWeb/HTML/Navigator.cpp b/Userland/Libraries/LibWeb/HTML/Navigator.cpp index f24b3dfb32..5c5029a854 100644 --- a/Userland/Libraries/LibWeb/HTML/Navigator.cpp +++ b/Userland/Libraries/LibWeb/HTML/Navigator.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -57,6 +58,7 @@ void Navigator::visit_edges(Cell::Visitor& visitor) Base::visit_edges(visitor); visitor.visit(m_mime_type_array); visitor.visit(m_plugin_array); + visitor.visit(m_clipboard); } JS::NonnullGCPtr Navigator::mime_types() @@ -73,4 +75,11 @@ JS::NonnullGCPtr Navigator::plugins() return *m_plugin_array; } +JS::NonnullGCPtr Navigator::clipboard() +{ + if (!m_clipboard) + m_clipboard = heap().allocate(realm(), realm()); + return *m_clipboard; +} + } diff --git a/Userland/Libraries/LibWeb/HTML/Navigator.h b/Userland/Libraries/LibWeb/HTML/Navigator.h index 4d2722ae0d..a89539d068 100644 --- a/Userland/Libraries/LibWeb/HTML/Navigator.h +++ b/Userland/Libraries/LibWeb/HTML/Navigator.h @@ -45,6 +45,7 @@ public: [[nodiscard]] JS::NonnullGCPtr mime_types(); [[nodiscard]] JS::NonnullGCPtr plugins(); + [[nodiscard]] JS::NonnullGCPtr clipboard(); virtual ~Navigator() override; @@ -58,6 +59,9 @@ private: JS::GCPtr m_plugin_array; JS::GCPtr m_mime_type_array; + + // https://w3c.github.io/clipboard-apis/#dom-navigator-clipboard + JS::GCPtr m_clipboard; }; } diff --git a/Userland/Libraries/LibWeb/HTML/Navigator.idl b/Userland/Libraries/LibWeb/HTML/Navigator.idl index 6ea3dd23a1..fcf5f3ab2a 100644 --- a/Userland/Libraries/LibWeb/HTML/Navigator.idl +++ b/Userland/Libraries/LibWeb/HTML/Navigator.idl @@ -1,3 +1,4 @@ +#import #import #import #import @@ -10,6 +11,9 @@ [Exposed=Window] interface Navigator { // objects implementing this interface also implement the interfaces given below + + // https://w3c.github.io/clipboard-apis/#navigator-interface + [SecureContext, SameObject] readonly attribute Clipboard clipboard; }; // NOTE: As NavigatorContentUtils, NavigatorCookies, NavigatorPlugins, and NavigatorAutomationInformation diff --git a/Userland/Libraries/LibWeb/Page/Page.h b/Userland/Libraries/LibWeb/Page/Page.h index 701c3493f1..a68c7a5c77 100644 --- a/Userland/Libraries/LibWeb/Page/Page.h +++ b/Userland/Libraries/LibWeb/Page/Page.h @@ -258,6 +258,8 @@ public: virtual void page_did_change_theme_color(Gfx::Color) { } + virtual void page_did_insert_clipboard_entry([[maybe_unused]] String data, [[maybe_unused]] String presentation_style, [[maybe_unused]] String mime_type) { } + protected: virtual ~PageClient() = default; }; diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index c3293ec239..2be955df56 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -5,6 +5,7 @@ libweb_js_bindings(Animations/Animation) libweb_js_bindings(Animations/AnimationEffect) libweb_js_bindings(Animations/AnimationTimeline) libweb_js_bindings(Animations/DocumentTimeline) +libweb_js_bindings(Clipboard/Clipboard) libweb_js_bindings(Crypto/Crypto) libweb_js_bindings(Crypto/SubtleCrypto) libweb_js_bindings(CSS/CSSConditionRule)