Title: [252450] trunk
Revision
252450
Author
[email protected]
Date
2019-11-13 22:39:08 -0800 (Wed, 13 Nov 2019)

Log Message

[Clipboard API] Add support for Clipboard.write()
https://bugs.webkit.org/show_bug.cgi?id=204078
<rdar://problem/57087756>

Reviewed by Ryosuke Niwa.

Source/WebCore:

This patch adds support for the write() method on Clipboard, forgoing sanitization for now (this will be added
in the next patch). See below for more details.

Tests: editing/async-clipboard/clipboard-change-data-while-writing.html
       editing/async-clipboard/clipboard-write-basic.html
       editing/async-clipboard/clipboard-write-items-twice.html

* Modules/async-clipboard/Clipboard.cpp:
(WebCore::Clipboard::~Clipboard):
(WebCore::shouldProceedWithClipboardWrite):
(WebCore::Clipboard::write):

Implement this method by creating a new ItemWriter and loading data from all the ClipboardItems that are being
written. If the previous writer is still in progress, make sure that we invalidate it first (rejecting the
promise) before proceeding.

(WebCore::Clipboard::didResolveOrReject):
(WebCore::Clipboard::ItemWriter::ItemWriter):
(WebCore::Clipboard::ItemWriter::write):
(WebCore::Clipboard::ItemWriter::invalidate):
(WebCore::Clipboard::ItemWriter::setData):
(WebCore::Clipboard::ItemWriter::didSetAllData):
(WebCore::Clipboard::ItemWriter::reject):

Introduce a private helper class to collect clipboard data for writing from a list of ClipboardItems, and
resolve or reject the given promise when finished.

* Modules/async-clipboard/Clipboard.h:
* platform/ios/PlatformPasteboardIOS.mm:
(WebCore::createItemProviderRegistrationList):

Fix a stray bug where the empty string could not be read back as plain text or URLs from the platform pasteboard
on iOS. This is exercised by the new layout test clipboard-write-basic.html.

* platform/mac/PlatformPasteboardMac.mm:
(WebCore::PlatformPasteboard::write):

Address another issue where we would sometimes try and declare the empty string as a pasteboard type when
writing to the platform pasteboard. While benign in a real NSPasteboard, there's no reason to include it in this
list of declared pasteboard types.

Tools:

Make the LocalPasteboard in WebKitTestRunner compatible with calls to -writeObjects: with a list of pasteboard
items. Currently, attempts to -writeObjects: result in a crash, since NSPasteboard code will attempt to
communicate with pasted and fail. We fix this by implementing -writeObjects: and storing the array of
NSPasteboardItems in LocalPasteboard, the same way we do in DumpRenderTree's LocalPasteboard implementation.

* DumpRenderTree/mac/DumpRenderTreePasteboard.mm:
(-[LocalPasteboard declareTypes:owner:]):
(-[LocalPasteboard _clearContentsWithoutUpdatingChangeCount]):

Factor out logic to clear the pasteboard's content into a separate helper, and clear out the list of saved
pasteboard items here as well.

(-[LocalPasteboard clearContents]):

Implement -clearContents in DumpRenderTree's LocalPasteboard, so that we can test Clipboard.write() in WebKit1.

(-[LocalPasteboard writeObjects:]):

Also make it so that we save any NSPasteboardItems we write to the local pasteboard, so that we can return them
later in -pasteboardItems.

(-[LocalPasteboard pasteboardItems]):
* WebKitTestRunner/mac/WebKitTestRunnerPasteboard.mm:
(-[LocalPasteboard initWithName:]):

Clean up this code a bit by replacing manual reference counting for `typesArray` and its neighboring data
structures with `RetainPtr`. Additionally, underscore-prefix the instance variables on LocalPasteboard to match
most of the other Objective-C objects in WebKit.

(-[LocalPasteboard name]):
(-[LocalPasteboard _clearContentsWithoutUpdatingChangeCount]):

Clear out the NSPasteboardItem list here too.

(-[LocalPasteboard clearContents]):
(-[LocalPasteboard declareTypes:owner:]):
(-[LocalPasteboard addTypes:owner:]):
(-[LocalPasteboard _addTypesWithoutUpdatingChangeCount:owner:]):
(-[LocalPasteboard changeCount]):
(-[LocalPasteboard types]):
(-[LocalPasteboard availableTypeFromArray:]):
(-[LocalPasteboard setData:forType:]):
(-[LocalPasteboard dataForType:]):
(-[LocalPasteboard pasteboardItems]):
(-[LocalPasteboard writeObjects:]):

Implement this by porting over the implementation that currently exists in DumpRenderTree. Like in
DumpRenderTree, we want to also save the NSPasteboardItem array we're given here, so that we can return it in
-pasteboardItems.

(-[LocalPasteboard dealloc]): Deleted.

LayoutTests:

Adds several new layout tests to exercise the write method on Clipboard.

* editing/async-clipboard/clipboard-change-data-while-writing-expected.txt: Added.
* editing/async-clipboard/clipboard-change-data-while-writing.html: Added.

Verify that if the platform pasteboard contents change while the page attempts to write to the clipboard, we
will reject the promise for writing.

* editing/async-clipboard/clipboard-write-basic-expected.txt: Added.
* editing/async-clipboard/clipboard-write-basic.html: Added.

Verify that writing multiple ClipboardItems to the clipboard using write() works. Among these items, one of them
contains no types, and another only contains types that resolve to empty strings. The page should be able to
read all four items back using Clipboard.read().

* editing/async-clipboard/clipboard-write-items-twice-expected.txt: Added.
* editing/async-clipboard/clipboard-write-items-twice.html: Added.

Verify that attempting to write a clipboard item that resolves on a long delay, and then attempting to write
another item that resolves on a short delay before the previous clipboard item has finished writing does not
cause the latter call to Clipboard.write() to fail. Additionally, the clipboard should contain the contents of
the second set of clipboard items, rather than the first.

* editing/async-clipboard/resources/async-clipboard-helpers.js:
(async.checkClipboardItemString):

Add a helper method to read a string for the given type, out of the given clipboard item, and compare it against
an expected result.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (252449 => 252450)


--- trunk/LayoutTests/ChangeLog	2019-11-14 04:40:23 UTC (rev 252449)
+++ trunk/LayoutTests/ChangeLog	2019-11-14 06:39:08 UTC (rev 252450)
@@ -1,3 +1,40 @@
+2019-11-13  Wenson Hsieh  <[email protected]>
+
+        [Clipboard API] Add support for Clipboard.write()
+        https://bugs.webkit.org/show_bug.cgi?id=204078
+        <rdar://problem/57087756>
+
+        Reviewed by Ryosuke Niwa.
+
+        Adds several new layout tests to exercise the write method on Clipboard.
+
+        * editing/async-clipboard/clipboard-change-data-while-writing-expected.txt: Added.
+        * editing/async-clipboard/clipboard-change-data-while-writing.html: Added.
+
+        Verify that if the platform pasteboard contents change while the page attempts to write to the clipboard, we
+        will reject the promise for writing.
+
+        * editing/async-clipboard/clipboard-write-basic-expected.txt: Added.
+        * editing/async-clipboard/clipboard-write-basic.html: Added.
+
+        Verify that writing multiple ClipboardItems to the clipboard using write() works. Among these items, one of them
+        contains no types, and another only contains types that resolve to empty strings. The page should be able to
+        read all four items back using Clipboard.read().
+
+        * editing/async-clipboard/clipboard-write-items-twice-expected.txt: Added.
+        * editing/async-clipboard/clipboard-write-items-twice.html: Added.
+
+        Verify that attempting to write a clipboard item that resolves on a long delay, and then attempting to write
+        another item that resolves on a short delay before the previous clipboard item has finished writing does not
+        cause the latter call to Clipboard.write() to fail. Additionally, the clipboard should contain the contents of
+        the second set of clipboard items, rather than the first.
+
+        * editing/async-clipboard/resources/async-clipboard-helpers.js:
+        (async.checkClipboardItemString):
+
+        Add a helper method to read a string for the given type, out of the given clipboard item, and compare it against
+        an expected result.
+
 2019-11-13  Said Abou-Hallawa  <[email protected]>
 
         [SVG2] Add the 'orient' property of the interface SVGMarkerElement

Added: trunk/LayoutTests/editing/async-clipboard/clipboard-change-data-while-writing-expected.txt (0 => 252450)


--- trunk/LayoutTests/editing/async-clipboard/clipboard-change-data-while-writing-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/async-clipboard/clipboard-change-data-while-writing-expected.txt	2019-11-14 06:39:08 UTC (rev 252450)
@@ -0,0 +1,10 @@
+This test verifies that if platform pasteboard contents are changed immediately after calling Clipboard.write(), the promise should be rejected. This test needs to be run in WebKitTestRunner or DumpRenderTree.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+PASS Failed to write clipboard item with NotAllowedError.
+PASS finishedWriting became true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/editing/async-clipboard/clipboard-change-data-while-writing.html (0 => 252450)


--- trunk/LayoutTests/editing/async-clipboard/clipboard-change-data-while-writing.html	                        (rev 0)
+++ trunk/LayoutTests/editing/async-clipboard/clipboard-change-data-while-writing.html	2019-11-14 06:39:08 UTC (rev 252450)
@@ -0,0 +1,61 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ domPasteAllowed=false useFlexibleViewport=true experimental:AsyncClipboardAPIEnabled=true ] -->
+<html>
+    <meta charset="utf8">
+    <head>
+        <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+        <script src=""
+        <script src=""
+        <script src=""
+        <style>
+            button {
+                width: 100%;
+                height: 100px;
+            }
+
+            iframe {
+                width: 100%;
+                height: 100px;
+            }
+        </style>
+    </head>
+    <script>
+        jsTestIsAsync = true;
+        finishedWriting = false;
+
+        async function runTest() {
+            description("This test verifies that if platform pasteboard contents are changed immediately after calling Clipboard.write(), the promise should be rejected. This test needs to be run in WebKitTestRunner or DumpRenderTree.");
+
+            const copyButton = document.getElementById("copy");
+            copyButton.addEventListener("click", async event => {
+                const item = new ClipboardItem({
+                    "text/plain" : new Promise(async function(resolve) {
+                        await UIHelper.delayFor(0);
+                        await UIHelper.copyText("foo");
+                        resolve("bar");
+                    })
+                });
+                try {
+                    await navigator.clipboard.write([item]);
+                    testFailed(`Did not expect to write the item.`);
+                } catch (exception) {
+                    testPassed(`Failed to write clipboard item with ${exception.name}.`);
+                } finally {
+                    finishedWriting = true;
+                }
+            });
+
+            await UIHelper.activateElement(copyButton);
+            await new Promise(resolve => shouldBecomeEqual("finishedWriting", "true", resolve));
+
+            copyButton.remove();
+            finishJSTest();
+        }
+
+        addEventListener("load", runTest);
+    </script>
+    <body>
+        <div><button id="copy">Copy</button></div>
+        <div id="description"></div>
+        <div id="console"></div>
+    </body>
+</html>

Added: trunk/LayoutTests/editing/async-clipboard/clipboard-write-basic-expected.txt (0 => 252450)


--- trunk/LayoutTests/editing/async-clipboard/clipboard-write-basic-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/async-clipboard/clipboard-write-basic-expected.txt	2019-11-14 06:39:08 UTC (rev 252450)
@@ -0,0 +1,24 @@
+This test verifies that multiple clipboard items, each with multiple types, can be written to and read from the clipboard. To run the test manually, click the Copy button and then click the Paste button.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS doneWritingItems became true
+Testing firstItem:
+PASS firstItem.types is ['text/plain', 'text/uri-list']
+PASS getType("text/plain") resolved to "The quick brown fox jumped over the lazy dog."
+PASS getType("text/uri-list") resolved to "https://www.apple.com/"
+Testing secondItem:
+PASS secondItem.types is ['text/uri-list', 'text/html']
+PASS getType("text/uri-list") resolved to "https://webkit.org/"
+PASS fragment.querySelector('a').href is "https://webkit.org/"
+Testing thirdItem:
+PASS thirdItem.types is ['text/uri-list', 'text/plain']
+PASS getType("text/uri-list") resolved to ""
+PASS getType("text/plain") resolved to ""
+Testing fourthItem:
+PASS fourthItem.types is []
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/editing/async-clipboard/clipboard-write-basic.html (0 => 252450)


--- trunk/LayoutTests/editing/async-clipboard/clipboard-write-basic.html	                        (rev 0)
+++ trunk/LayoutTests/editing/async-clipboard/clipboard-write-basic.html	2019-11-14 06:39:08 UTC (rev 252450)
@@ -0,0 +1,81 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true experimental:AsyncClipboardAPIEnabled=true ] -->
+<html>
+    <meta charset="utf8">
+    <head>
+        <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+        <script src=""
+        <script src=""
+        <script src=""
+        <style>
+            button {
+                width: 100px;
+                padding: 1em;
+            }
+        </style>
+    </head>
+    <script>
+        jsTestIsAsync = true;
+        doneWritingItems = false;
+
+        description("This test verifies that multiple clipboard items, each with multiple types, can be written to and read from the clipboard. To run the test manually, click the Copy button and then click the Paste button.");
+
+        addEventListener("load", async function() {
+            const copyButton = document.getElementById("copy");
+            const pasteButton = document.getElementById("paste");
+
+            copyButton.addEventListener("click", async () => {
+                await navigator.clipboard.write([
+                    new ClipboardItem({
+                        "text/plain" : Promise.resolve(textBlob("The quick brown fox jumped over the lazy dog.")),
+                        "text/uri-list" : Promise.resolve("https://www.apple.com/")
+                    }), new ClipboardItem({
+                        "text/uri-list" : Promise.resolve(textBlob("https://webkit.org/")),
+                        "text/html" : Promise.resolve(textBlob("<a href=''>WebKit</a>", "text/html"))
+                    }), new ClipboardItem({
+                        "text/uri-list" : Promise.resolve(""),
+                        "text/plain" : Promise.resolve(textBlob(""))
+                    }), new ClipboardItem({ })
+                ]);
+                doneWritingItems = true;
+            });
+
+            pasteButton.addEventListener("click", async () => {
+                [firstItem, secondItem, thirdItem, fourthItem] = await navigator.clipboard.read();
+
+                debug("Testing firstItem:");
+                shouldBe("firstItem.types", "['text/plain', 'text/uri-list']");
+                await checkClipboardItemString(firstItem, "text/plain", "The quick brown fox jumped over the lazy dog.");
+                await checkClipboardItemString(firstItem, "text/uri-list", "https://www.apple.com/");
+
+                debug("Testing secondItem:");
+                shouldBe("secondItem.types", "['text/uri-list', 'text/html']");
+                await checkClipboardItemString(secondItem, "text/uri-list", "https://webkit.org/");
+                fragment = await loadDocument(await secondItem.getType("text/html"));
+                shouldBeEqualToString("fragment.querySelector('a').href", "https://webkit.org/");
+
+                debug("Testing thirdItem:");
+                shouldBe("thirdItem.types", "['text/uri-list', 'text/plain']");
+                await checkClipboardItemString(thirdItem, "text/uri-list", "");
+                await checkClipboardItemString(thirdItem, "text/plain", "");
+
+                debug("Testing fourthItem:");
+                shouldBe("fourthItem.types", "[]");
+
+                copyButton.remove();
+                pasteButton.remove();
+                finishJSTest();
+            });
+
+            if (!window.testRunner)
+                return;
+
+            await UIHelper.activateElement(copyButton);
+            await new Promise(resolve => shouldBecomeEqual("doneWritingItems", "true", resolve));
+            await UIHelper.activateElement(pasteButton);
+        });
+    </script>
+    <body>
+        <button id="copy">Copy</button>
+        <button id="paste">Paste</button>
+    </body>
+</html>

Added: trunk/LayoutTests/editing/async-clipboard/clipboard-write-items-twice-expected.txt (0 => 252450)


--- trunk/LayoutTests/editing/async-clipboard/clipboard-write-items-twice-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/async-clipboard/clipboard-write-items-twice-expected.txt	2019-11-14 06:39:08 UTC (rev 252450)
@@ -0,0 +1,13 @@
+This test verifies that when writing items to the clipboard while a previous call to write items is ongoing, we will end up writing the latter set of items to the clipboard. To manually test, click the 'Copy' button, followed by the 'Copy Again' button, followed by 'Paste'
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS doneWritingSecondItem became true
+PASS item.types is ['text/plain', 'text/uri-list']
+PASS getType("text/plain") resolved to "This is the second item."
+PASS getType("text/uri-list") resolved to "https://www.example.com/"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/editing/async-clipboard/clipboard-write-items-twice.html (0 => 252450)


--- trunk/LayoutTests/editing/async-clipboard/clipboard-write-items-twice.html	                        (rev 0)
+++ trunk/LayoutTests/editing/async-clipboard/clipboard-write-items-twice.html	2019-11-14 06:39:08 UTC (rev 252450)
@@ -0,0 +1,72 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true experimental:AsyncClipboardAPIEnabled=true ] -->
+<html>
+    <meta charset="utf8">
+    <head>
+        <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+        <script src=""
+        <script src=""
+        <script src=""
+        <style>
+            button {
+                width: 100px;
+                display: block;
+                padding: 1em;
+                margin: 50px auto;
+            }
+        </style>
+    </head>
+    <script>
+        jsTestIsAsync = true;
+        doneWritingSecondItem = false;
+
+        description("This test verifies that when writing items to the clipboard while a previous call to write items is ongoing, we will end up writing the latter set of items to the clipboard. To manually test, click the 'Copy' button, followed by the 'Copy Again' button, followed by 'Paste'");
+
+        addEventListener("load", async function() {
+            const copyButton = document.getElementById("copy");
+            const copyAgainButton = document.getElementById("copy-again");
+            const pasteButton = document.getElementById("paste");
+
+            copyButton.addEventListener("click", async () => {
+                const item = new ClipboardItem({
+                    "text/plain" : new Promise(resolve => setTimeout(() => resolve("This is the first item."), 100)),
+                    "text/html" : new Promise(resolve => setTimeout(() => resolve("<strong>This is the first item.</strong>"), 100))
+                });
+                try {
+                    await navigator.clipboard.write([item]);
+                } catch (e) { }
+            });
+
+            copyAgainButton.addEventListener("click", async () => {
+                const item = new ClipboardItem({
+                    "text/plain" : Promise.resolve("This is the second item."),
+                    "text/uri-list" : Promise.resolve(textBlob("https://www.example.com/"))
+                });
+                await navigator.clipboard.write([item]);
+                doneWritingSecondItem = true;
+            });
+
+            pasteButton.addEventListener("click", async () => {
+                item = (await navigator.clipboard.read())[0]
+                shouldBe("item.types", "['text/plain', 'text/uri-list']");
+                await checkClipboardItemString(item, "text/plain", "This is the second item.");
+                await checkClipboardItemString(item, "text/uri-list", "https://www.example.com/");
+                [copyButton, copyAgainButton, pasteButton].map(e => e.remove());
+                finishJSTest();
+            });
+
+            if (!window.testRunner)
+                return;
+
+            await UIHelper.activateElement(copyButton);
+            await UIHelper.activateElement(copyAgainButton);
+            await new Promise(resolve => shouldBecomeEqual("doneWritingSecondItem", "true", resolve));
+            await UIHelper.delayFor(100);
+            await UIHelper.activateElement(pasteButton);
+        });
+    </script>
+    <body>
+        <button id="copy">Copy</button>
+        <button id="copy-again">Copy Again</button>
+        <button id="paste">Paste</button>
+    </body>
+</html>

Modified: trunk/LayoutTests/editing/async-clipboard/resources/async-clipboard-helpers.js (252449 => 252450)


--- trunk/LayoutTests/editing/async-clipboard/resources/async-clipboard-helpers.js	2019-11-14 04:40:23 UTC (rev 252449)
+++ trunk/LayoutTests/editing/async-clipboard/resources/async-clipboard-helpers.js	2019-11-14 06:39:08 UTC (rev 252450)
@@ -84,3 +84,12 @@
             })()`, resolve);
     });
 }
+
+async function checkClipboardItemString(item, type, expectedString)
+{
+    const observedString = await loadText(await item.getType(type));
+    if (observedString === expectedString)
+        testPassed(`getType("${type}") resolved to "${expectedString}"`);
+    else
+        testFailed(`getType("${type}") resolved to "${observedString}; expected "${expectedString}"`);
+}

Modified: trunk/LayoutTests/platform/win/TestExpectations (252449 => 252450)


--- trunk/LayoutTests/platform/win/TestExpectations	2019-11-14 04:40:23 UTC (rev 252449)
+++ trunk/LayoutTests/platform/win/TestExpectations	2019-11-14 06:39:08 UTC (rev 252450)
@@ -1190,6 +1190,9 @@
 webkit.org/b/203100 editing/async-clipboard/clipboard-change-data-while-getting-type.html [ Skip ]
 webkit.org/b/203100 editing/async-clipboard/clipboard-item-get-type-basic.html [ Skip ]
 webkit.org/b/203100 editing/async-clipboard/clipboard-get-type-with-old-items.html [ Skip ]
+webkit.org/b/203100 editing/async-clipboard/clipboard-change-data-while-writing.html [ Skip ]
+webkit.org/b/203100 editing/async-clipboard/clipboard-write-basic.html [ Skip ]
+webkit.org/b/203100 editing/async-clipboard/clipboard-write-items-twice.html [ Skip ]
 
 webkit.org/b/140783 [ Release ] editing/pasteboard/copy-standalone-image.html [ Failure ImageOnlyFailure ]
 webkit.org/b/140783 [ Debug ] editing/pasteboard/copy-standalone-image.html [ Skip ] # Debug Assertion

Modified: trunk/Source/WebCore/ChangeLog (252449 => 252450)


--- trunk/Source/WebCore/ChangeLog	2019-11-14 04:40:23 UTC (rev 252449)
+++ trunk/Source/WebCore/ChangeLog	2019-11-14 06:39:08 UTC (rev 252450)
@@ -1,3 +1,52 @@
+2019-11-13  Wenson Hsieh  <[email protected]>
+
+        [Clipboard API] Add support for Clipboard.write()
+        https://bugs.webkit.org/show_bug.cgi?id=204078
+        <rdar://problem/57087756>
+
+        Reviewed by Ryosuke Niwa.
+
+        This patch adds support for the write() method on Clipboard, forgoing sanitization for now (this will be added
+        in the next patch). See below for more details.
+
+        Tests: editing/async-clipboard/clipboard-change-data-while-writing.html
+               editing/async-clipboard/clipboard-write-basic.html
+               editing/async-clipboard/clipboard-write-items-twice.html
+
+        * Modules/async-clipboard/Clipboard.cpp:
+        (WebCore::Clipboard::~Clipboard):
+        (WebCore::shouldProceedWithClipboardWrite):
+        (WebCore::Clipboard::write):
+
+        Implement this method by creating a new ItemWriter and loading data from all the ClipboardItems that are being
+        written. If the previous writer is still in progress, make sure that we invalidate it first (rejecting the
+        promise) before proceeding.
+
+        (WebCore::Clipboard::didResolveOrReject):
+        (WebCore::Clipboard::ItemWriter::ItemWriter):
+        (WebCore::Clipboard::ItemWriter::write):
+        (WebCore::Clipboard::ItemWriter::invalidate):
+        (WebCore::Clipboard::ItemWriter::setData):
+        (WebCore::Clipboard::ItemWriter::didSetAllData):
+        (WebCore::Clipboard::ItemWriter::reject):
+
+        Introduce a private helper class to collect clipboard data for writing from a list of ClipboardItems, and
+        resolve or reject the given promise when finished.
+
+        * Modules/async-clipboard/Clipboard.h:
+        * platform/ios/PlatformPasteboardIOS.mm:
+        (WebCore::createItemProviderRegistrationList):
+
+        Fix a stray bug where the empty string could not be read back as plain text or URLs from the platform pasteboard
+        on iOS. This is exercised by the new layout test clipboard-write-basic.html.
+
+        * platform/mac/PlatformPasteboardMac.mm:
+        (WebCore::PlatformPasteboard::write):
+
+        Address another issue where we would sometimes try and declare the empty string as a pasteboard type when
+        writing to the platform pasteboard. While benign in a real NSPasteboard, there's no reason to include it in this
+        list of declared pasteboard types.
+
 2019-11-13  Said Abou-Hallawa  <[email protected]>
 
         [SVG2] Add the 'orient' property of the interface SVGMarkerElement

Modified: trunk/Source/WebCore/Modules/async-clipboard/Clipboard.cpp (252449 => 252450)


--- trunk/Source/WebCore/Modules/async-clipboard/Clipboard.cpp	2019-11-14 04:40:23 UTC (rev 252449)
+++ trunk/Source/WebCore/Modules/async-clipboard/Clipboard.cpp	2019-11-14 06:39:08 UTC (rev 252450)
@@ -33,8 +33,11 @@
 #include "JSDOMPromiseDeferred.h"
 #include "Navigator.h"
 #include "Pasteboard.h"
+#include "Settings.h"
 #include "SharedBuffer.h"
+#include "UserGestureIndicator.h"
 #include "WebContentReader.h"
+#include <wtf/CompletionHandler.h>
 #include <wtf/IsoMallocInlines.h>
 
 namespace WebCore {
@@ -51,7 +54,11 @@
 {
 }
 
-Clipboard::~Clipboard() = default;
+Clipboard::~Clipboard()
+{
+    if (auto writer = WTFMove(m_activeItemWriter))
+        writer->invalidate();
+}
 
 Navigator* Clipboard::navigator()
 {
@@ -179,12 +186,45 @@
         promise->reject(NotAllowedError);
 }
 
+static bool shouldProceedWithClipboardWrite(const Frame& frame)
+{
+    auto& settings = frame.settings();
+    if (settings._javascript_CanAccessClipboard())
+        return true;
+
+    switch (settings.clipboardAccessPolicy()) {
+    case ClipboardAccessPolicy::Allow:
+        return true;
+    case ClipboardAccessPolicy::RequiresUserGesture:
+        return UserGestureIndicator::processingUserGesture();
+    case ClipboardAccessPolicy::Deny:
+        return false;
+    }
+
+    ASSERT_NOT_REACHED();
+    return false;
+}
+
 void Clipboard::write(const Vector<RefPtr<ClipboardItem>>& items, Ref<DeferredPromise>&& promise)
 {
-    UNUSED_PARAM(items);
-    promise->reject(NotSupportedError);
+    auto frame = makeRefPtr(this->frame());
+    if (!frame || !shouldProceedWithClipboardWrite(*frame)) {
+        promise->reject(NotAllowedError);
+        return;
+    }
+
+    if (auto existingWriter = std::exchange(m_activeItemWriter, ItemWriter::create(*this, WTFMove(promise))))
+        existingWriter->invalidate();
+
+    m_activeItemWriter->write(items);
 }
 
+void Clipboard::didResolveOrReject(Clipboard::ItemWriter& writer)
+{
+    if (m_activeItemWriter == &writer)
+        m_activeItemWriter = nullptr;
+}
+
 Frame* Clipboard::frame() const
 {
     return m_navigator ? m_navigator->frame() : nullptr;
@@ -197,4 +237,91 @@
     return *m_activeSession->pasteboard;
 }
 
+Clipboard::ItemWriter::ItemWriter(Clipboard& clipboard, Ref<DeferredPromise>&& promise)
+    : m_clipboard(makeWeakPtr(clipboard))
+    , m_promise(WTFMove(promise))
+    , m_pasteboard(Pasteboard::createForCopyAndPaste())
+{
 }
+
+Clipboard::ItemWriter::~ItemWriter() = default;
+
+void Clipboard::ItemWriter::write(const Vector<RefPtr<ClipboardItem>>& items)
+{
+    ASSERT(m_promise);
+    ASSERT(m_clipboard);
+#if PLATFORM(COCOA)
+    m_changeCountAtStart = m_pasteboard->changeCount();
+#endif
+    m_dataToWrite.fill(WTF::nullopt, items.size());
+    m_pendingItemCount = items.size();
+    for (size_t index = 0; index < items.size(); ++index) {
+        items[index]->collectDataForWriting(*m_clipboard, [this, protectedThis = makeRef(*this), index] (auto data) {
+            protectedThis->setData(WTFMove(data), index);
+            if (!--m_pendingItemCount)
+                didSetAllData();
+        });
+    }
+    if (items.isEmpty())
+        didSetAllData();
+}
+
+void Clipboard::ItemWriter::invalidate()
+{
+    if (m_promise)
+        reject();
+}
+
+void Clipboard::ItemWriter::setData(Optional<PasteboardCustomData>&& data, size_t index)
+{
+    if (index >= m_dataToWrite.size()) {
+        ASSERT_NOT_REACHED();
+        return;
+    }
+
+    m_dataToWrite[index] = WTFMove(data);
+}
+
+void Clipboard::ItemWriter::didSetAllData()
+{
+    if (!m_promise)
+        return;
+
+#if PLATFORM(COCOA)
+    auto newChangeCount = m_pasteboard->changeCount();
+    if (m_changeCountAtStart != newChangeCount) {
+        // FIXME: Instead of checking the changeCount here, send it over to the client (e.g. the UI process
+        // in WebKit2) and perform it there.
+        reject();
+        return;
+    }
+#endif // PLATFORM(COCOA)
+    auto dataToWrite = std::exchange(m_dataToWrite, { });
+    Vector<PasteboardCustomData> customData;
+    customData.reserveInitialCapacity(dataToWrite.size());
+    for (auto data : dataToWrite) {
+        if (!data) {
+            reject();
+            return;
+        }
+        customData.append(*data);
+    }
+
+    m_pasteboard->writeCustomData(WTFMove(customData));
+    m_promise->resolve();
+    m_promise = nullptr;
+
+    if (auto clipboard = std::exchange(m_clipboard, nullptr))
+        clipboard->didResolveOrReject(*this);
+}
+
+void Clipboard::ItemWriter::reject()
+{
+    if (auto promise = std::exchange(m_promise, nullptr))
+        promise->reject(NotAllowedError);
+
+    if (auto clipboard = std::exchange(m_clipboard, nullptr))
+        clipboard->didResolveOrReject(*this);
+}
+
+}

Modified: trunk/Source/WebCore/Modules/async-clipboard/Clipboard.h (252449 => 252450)


--- trunk/Source/WebCore/Modules/async-clipboard/Clipboard.h	2019-11-14 04:40:23 UTC (rev 252449)
+++ trunk/Source/WebCore/Modules/async-clipboard/Clipboard.h	2019-11-14 06:39:08 UTC (rev 252450)
@@ -27,6 +27,7 @@
 
 #include "EventTarget.h"
 #include <wtf/IsoMalloc.h>
+#include <wtf/Optional.h>
 #include <wtf/Vector.h>
 #include <wtf/WeakPtr.h>
 
@@ -37,6 +38,7 @@
 class Frame;
 class Navigator;
 class Pasteboard;
+class PasteboardCustomData;
 
 class Clipboard final : public RefCounted<Clipboard>, public EventTargetWithInlineData, public CanMakeWeakPtr<Clipboard> {
     WTF_MAKE_ISO_ALLOCATED(Clipboard);
@@ -75,8 +77,41 @@
 
     Pasteboard& activePasteboard();
 
+    class ItemWriter : public RefCounted<ItemWriter> {
+    public:
+        static Ref<ItemWriter> create(Clipboard& clipboard, Ref<DeferredPromise>&& promise)
+        {
+            return adoptRef(*new ItemWriter(clipboard, WTFMove(promise)));
+        }
+
+        ~ItemWriter();
+
+        void write(const Vector<RefPtr<ClipboardItem>>&);
+        void invalidate();
+
+    private:
+        ItemWriter(Clipboard&, Ref<DeferredPromise>&&);
+
+        void setData(Optional<PasteboardCustomData>&&, size_t index);
+        void didSetAllData();
+        void reject();
+
+        WeakPtr<Clipboard> m_clipboard;
+        Vector<Optional<PasteboardCustomData>> m_dataToWrite;
+        RefPtr<DeferredPromise> m_promise;
+        unsigned m_pendingItemCount;
+        std::unique_ptr<Pasteboard> m_pasteboard;
+#if PLATFORM(COCOA)
+        int64_t m_changeCountAtStart { 0 };
+#endif
+    };
+
+    void didResolveOrReject(ItemWriter&);
+
     Optional<Session> m_activeSession;
     WeakPtr<Navigator> m_navigator;
+    Vector<Optional<PasteboardCustomData>> m_dataToWrite;
+    RefPtr<ItemWriter> m_activeItemWriter;
 };
 
 } // namespace WebCore

Modified: trunk/Source/WebCore/platform/ios/PlatformPasteboardIOS.mm (252449 => 252450)


--- trunk/Source/WebCore/platform/ios/PlatformPasteboardIOS.mm	2019-11-14 04:40:23 UTC (rev 252449)
+++ trunk/Source/WebCore/platform/ios/PlatformPasteboardIOS.mm	2019-11-14 06:39:08 UTC (rev 252450)
@@ -599,10 +599,10 @@
     }
 
     data.forEachPlatformString([&] (auto& type, auto& value) {
-        NSString *stringValue = value;
-        if (!stringValue.length)
+        if (!value)
             return;
 
+        NSString *stringValue = value;
         auto cocoaType = PlatformPasteboard::platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(type).createCFString();
         if (UTTypeConformsTo(cocoaType.get(), kUTTypeURL))
             [representationsToRegister addRepresentingObject:[NSURL URLWithString:stringValue]];

Modified: trunk/Source/WebCore/platform/mac/PlatformPasteboardMac.mm (252449 => 252450)


--- trunk/Source/WebCore/platform/mac/PlatformPasteboardMac.mm	2019-11-14 04:40:23 UTC (rev 252449)
+++ trunk/Source/WebCore/platform/mac/PlatformPasteboardMac.mm	2019-11-14 06:39:08 UTC (rev 252450)
@@ -226,7 +226,9 @@
 {
     NSMutableArray *types = [NSMutableArray array];
     data.forEachType([&] (auto& type) {
-        [types addObject:platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(type)];
+        NSString *platformType = platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(type);
+        if (platformType.length)
+            [types addObject:platformType];
     });
 
     bool hasSameOriginCustomData = data.hasSameOriginCustomData();

Modified: trunk/Tools/ChangeLog (252449 => 252450)


--- trunk/Tools/ChangeLog	2019-11-14 04:40:23 UTC (rev 252449)
+++ trunk/Tools/ChangeLog	2019-11-14 06:39:08 UTC (rev 252450)
@@ -1,3 +1,63 @@
+2019-11-13  Wenson Hsieh  <[email protected]>
+
+        [Clipboard API] Add support for Clipboard.write()
+        https://bugs.webkit.org/show_bug.cgi?id=204078
+        <rdar://problem/57087756>
+
+        Reviewed by Ryosuke Niwa.
+
+        Make the LocalPasteboard in WebKitTestRunner compatible with calls to -writeObjects: with a list of pasteboard
+        items. Currently, attempts to -writeObjects: result in a crash, since NSPasteboard code will attempt to
+        communicate with pasted and fail. We fix this by implementing -writeObjects: and storing the array of
+        NSPasteboardItems in LocalPasteboard, the same way we do in DumpRenderTree's LocalPasteboard implementation.
+
+        * DumpRenderTree/mac/DumpRenderTreePasteboard.mm:
+        (-[LocalPasteboard declareTypes:owner:]):
+        (-[LocalPasteboard _clearContentsWithoutUpdatingChangeCount]):
+
+        Factor out logic to clear the pasteboard's content into a separate helper, and clear out the list of saved
+        pasteboard items here as well.
+
+        (-[LocalPasteboard clearContents]):
+
+        Implement -clearContents in DumpRenderTree's LocalPasteboard, so that we can test Clipboard.write() in WebKit1.
+
+        (-[LocalPasteboard writeObjects:]):
+
+        Also make it so that we save any NSPasteboardItems we write to the local pasteboard, so that we can return them
+        later in -pasteboardItems.
+
+        (-[LocalPasteboard pasteboardItems]):
+        * WebKitTestRunner/mac/WebKitTestRunnerPasteboard.mm:
+        (-[LocalPasteboard initWithName:]):
+
+        Clean up this code a bit by replacing manual reference counting for `typesArray` and its neighboring data
+        structures with `RetainPtr`. Additionally, underscore-prefix the instance variables on LocalPasteboard to match
+        most of the other Objective-C objects in WebKit.
+
+        (-[LocalPasteboard name]):
+        (-[LocalPasteboard _clearContentsWithoutUpdatingChangeCount]):
+
+        Clear out the NSPasteboardItem list here too.
+
+        (-[LocalPasteboard clearContents]):
+        (-[LocalPasteboard declareTypes:owner:]):
+        (-[LocalPasteboard addTypes:owner:]):
+        (-[LocalPasteboard _addTypesWithoutUpdatingChangeCount:owner:]):
+        (-[LocalPasteboard changeCount]):
+        (-[LocalPasteboard types]):
+        (-[LocalPasteboard availableTypeFromArray:]):
+        (-[LocalPasteboard setData:forType:]):
+        (-[LocalPasteboard dataForType:]):
+        (-[LocalPasteboard pasteboardItems]):
+        (-[LocalPasteboard writeObjects:]):
+
+        Implement this by porting over the implementation that currently exists in DumpRenderTree. Like in
+        DumpRenderTree, we want to also save the NSPasteboardItem array we're given here, so that we can return it in
+        -pasteboardItems.
+
+        (-[LocalPasteboard dealloc]): Deleted.
+
 2019-11-13  Megan Gardner  <[email protected]>
 
         Cleanup UIKitSPI for Testing

Modified: trunk/Tools/DumpRenderTree/mac/DumpRenderTreePasteboard.mm (252449 => 252450)


--- trunk/Tools/DumpRenderTree/mac/DumpRenderTreePasteboard.mm	2019-11-14 04:40:23 UTC (rev 252449)
+++ trunk/Tools/DumpRenderTree/mac/DumpRenderTreePasteboard.mm	2019-11-14 06:39:08 UTC (rev 252450)
@@ -45,6 +45,7 @@
 @interface LocalPasteboard : NSPasteboard {
     RetainPtr<id> _owner;
     RetainPtr<NSString> _pasteboardName;
+    RetainPtr<NSMutableArray<NSPasteboardItem *>> _writtenPasteboardItems;
     NSInteger _changeCount;
 
     ListHashSet<RetainPtr<CFStringRef>, WTF::RetainPtrObjectHash<CFStringRef>> _types;
@@ -120,8 +121,7 @@
 
 - (NSInteger)declareTypes:(NSArray *)newTypes owner:(id)newOwner
 {
-    _types.clear();
-    _data.clear();
+    [self _clearContentsWithoutUpdatingChangeCount];
 
     [self _addTypesWithoutUpdatingChangeCount:newTypes owner:newOwner];
     return ++_changeCount;
@@ -142,6 +142,19 @@
     return adoptCF(UTTypeCreatePreferredIdentifierForTag(kUTTagClassNSPboardType, (__bridge CFStringRef)type, nullptr));
 }
 
+- (void)_clearContentsWithoutUpdatingChangeCount
+{
+    _writtenPasteboardItems = nil;
+    _types.clear();
+    _data.clear();
+}
+
+- (NSInteger)clearContents
+{
+    [self _clearContentsWithoutUpdatingChangeCount];
+    return ++_changeCount;
+}
+
 - (NSInteger)addTypes:(NSArray<NSPasteboardType> *)newTypes owner:(id)newOwner
 {
     auto previousOwner = _owner;
@@ -226,10 +239,11 @@
 
 - (BOOL)writeObjects:(NSArray<id <NSPasteboardWriting>> *)objects
 {
+    _writtenPasteboardItems = adoptNS([[NSMutableArray<NSPasteboardItem *> alloc] initWithCapacity:objects.count]);
     for (id <NSPasteboardWriting> object in objects) {
+        ASSERT([object isKindOfClass:NSPasteboardItem.class]);
+        [_writtenPasteboardItems addObject:(NSPasteboardItem *)object];
         for (NSString *type in [object writableTypesForPasteboard:self]) {
-            ASSERT(UTTypeIsDeclared((__bridge CFStringRef)type) || UTTypeIsDynamic((__bridge CFStringRef)type));
-
             [self addTypes:@[ type ] owner:self];
 
             id propertyList = [object pasteboardPropertyListForType:type];
@@ -245,6 +259,9 @@
 
 - (NSArray<NSPasteboardItem *> *)pasteboardItems
 {
+    if (_writtenPasteboardItems)
+        return _writtenPasteboardItems.get();
+
     auto item = adoptNS([[NSPasteboardItem alloc] init]);
     for (const auto& typeAndData : _data) {
         NSData *data = "" NSData *)typeAndData.value.get();

Modified: trunk/Tools/WebKitTestRunner/mac/WebKitTestRunnerPasteboard.mm (252449 => 252450)


--- trunk/Tools/WebKitTestRunner/mac/WebKitTestRunnerPasteboard.mm	2019-11-14 04:40:23 UTC (rev 252449)
+++ trunk/Tools/WebKitTestRunner/mac/WebKitTestRunnerPasteboard.mm	2019-11-14 06:39:08 UTC (rev 252450)
@@ -34,11 +34,12 @@
 
 @interface LocalPasteboard : NSPasteboard
 {
-    NSMutableArray *typesArray;
-    NSMutableSet *typesSet;
-    NSMutableDictionary *dataByType;
-    NSInteger changeCount;
-    NSString *pasteboardName;
+    RetainPtr<NSMutableArray> _typesArray;
+    RetainPtr<NSMutableSet> _typesSet;
+    RetainPtr<NSMutableArray<NSPasteboardItem *>> _writtenPasteboardItems;
+    RetainPtr<NSMutableDictionary> _dataByType;
+    NSInteger _changeCount;
+    RetainPtr<NSString> _pasteboardName;
 }
 
 -(id)initWithName:(NSString *)name;
@@ -99,25 +100,16 @@
     self = [super init];
     if (!self)
         return nil;
-    typesArray = [[NSMutableArray alloc] init];
-    typesSet = [[NSMutableSet alloc] init];
-    dataByType = [[NSMutableDictionary alloc] init];
-    pasteboardName = [name copy];
+    _typesArray = adoptNS([[NSMutableArray alloc] init]);
+    _typesSet = adoptNS([[NSMutableSet alloc] init]);
+    _dataByType = adoptNS([[NSMutableDictionary alloc] init]);
+    _pasteboardName = adoptNS([name copy]);
     return self;
 }
 
-- (void)dealloc
-{
-    [typesArray release];
-    [typesSet release];
-    [dataByType release];
-    [pasteboardName release];
-    [super dealloc];
-}
-
 - (NSString *)name
 {
-    return pasteboardName;
+    return _pasteboardName.get();
 }
 
 - (void)releaseGlobally
@@ -126,15 +118,16 @@
 
 - (void)_clearContentsWithoutUpdatingChangeCount
 {
-    [typesArray removeAllObjects];
-    [typesSet removeAllObjects];
-    [dataByType removeAllObjects];
+    _writtenPasteboardItems = nil;
+    [_typesArray removeAllObjects];
+    [_typesSet removeAllObjects];
+    [_dataByType removeAllObjects];
 }
 
 - (NSInteger)clearContents
 {
     [self _clearContentsWithoutUpdatingChangeCount];
-    return ++changeCount;
+    return ++_changeCount;
 }
 
 - (NSInteger)declareTypes:(NSArray *)newTypes owner:(id)newOwner
@@ -141,7 +134,7 @@
 {
     [self _clearContentsWithoutUpdatingChangeCount];
     [self _addTypesWithoutUpdatingChangeCount:newTypes owner:newOwner];
-    return ++changeCount;
+    return ++_changeCount;
 }
 
 - (NSInteger)addTypes:(NSArray<NSPasteboardType> *)newTypes owner:(id)newOwner
@@ -149,7 +142,7 @@
     [self _addTypesWithoutUpdatingChangeCount:newTypes owner:newOwner];
     // FIXME: Ideally, we would keep track of the current owner and only bump the change
     // count if the new owner is different.
-    return ++changeCount;
+    return ++_changeCount;
 }
 
 - (void)_addTypesWithoutUpdatingChangeCount:(NSArray *)newTypes owner:(id)newOwner
@@ -158,11 +151,11 @@
     unsigned i;
     for (i = 0; i < count; ++i) {
         NSString *type = [newTypes objectAtIndex:i];
-        NSString *setType = [typesSet member:type];
+        NSString *setType = [_typesSet member:type];
         if (!setType) {
             setType = [type copy];
-            [typesArray addObject:setType];
-            [typesSet addObject:setType];
+            [_typesArray addObject:setType];
+            [_typesSet addObject:setType];
             [setType release];
         }
         if (newOwner && [newOwner respondsToSelector:@selector(pasteboard:provideDataForType:)])
@@ -172,18 +165,18 @@
 
 - (NSInteger)changeCount
 {
-    return changeCount;
+    return _changeCount;
 }
 
 - (NSArray *)types
 {
-    return typesArray;
+    return _typesArray.get();
 }
 
 - (NSString *)availableTypeFromArray:(NSArray *)types
 {
     for (NSString *type in types) {
-        if (NSString *setType = [typesSet member:type])
+        if (NSString *setType = [_typesSet member:type])
             return setType;
     }
     return nil;
@@ -191,18 +184,18 @@
 
 - (BOOL)setData:(NSData *)data forType:(NSString *)dataType
 {
-    if (![typesSet containsObject:dataType])
+    if (![_typesSet containsObject:dataType])
         return NO;
     if (!data)
         data = "" data];
-    [dataByType setObject:data forKey:dataType];
-    ++changeCount;
+    [_dataByType setObject:data forKey:dataType];
+    ++_changeCount;
     return YES;
 }
 
 - (NSData *)dataForType:(NSString *)dataType
 {
-    return [dataByType objectForKey:dataType];
+    return [_dataByType objectForKey:dataType];
 }
 
 - (BOOL)setPropertyList:(id)propertyList forType:(NSString *)dataType
@@ -220,10 +213,37 @@
 
 - (NSArray<NSPasteboardItem *> *)pasteboardItems
 {
+    if (_writtenPasteboardItems)
+        return _writtenPasteboardItems.get();
+
     auto item = adoptNS([[NSPasteboardItem alloc] init]);
-    for (NSString *type in dataByType)
-        [item setData:dataByType[type] forType:[NSPasteboard _modernPasteboardType:type]];
+    for (NSString *type in _typesArray.get()) {
+        NSPasteboardType modernPasteboardType = [NSPasteboard _modernPasteboardType:type];
+        if (NSData *dataForType = [_dataByType objectForKey:type] ?: [_dataByType objectForKey:modernPasteboardType])
+            [item setData:dataForType forType:modernPasteboardType];
+    }
     return @[ item.get() ];
 }
 
+- (BOOL)writeObjects:(NSArray<id <NSPasteboardWriting>> *)objects
+{
+    _writtenPasteboardItems = adoptNS([[NSMutableArray<NSPasteboardItem *> alloc] initWithCapacity:objects.count]);
+    for (id <NSPasteboardWriting> object in objects) {
+        ASSERT([object isKindOfClass:NSPasteboardItem.class]);
+        [_writtenPasteboardItems addObject:(NSPasteboardItem *)object];
+        NSArray<NSPasteboardType> *writableTypes = [object writableTypesForPasteboard:self];
+        for (NSString *type in writableTypes) {
+            [self addTypes:@[type] owner:self];
+
+            id propertyList = [object pasteboardPropertyListForType:type];
+            if ([propertyList isKindOfClass:NSData.class])
+                [self setData:propertyList forType:type];
+            else
+                ASSERT_NOT_REACHED();
+        }
+    }
+
+    return YES;
+}
+
 @end
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to