[Clipboard API] Add support for Clipboard.write()
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 14 Nov 2019 06:39:08 +0000 (06:39 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 14 Nov 2019 06:39:08 +0000 (06:39 +0000)
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.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@252450 268f45cc-cd09-0410-ab3c-d52691b4dbfc

17 files changed:
LayoutTests/ChangeLog
LayoutTests/editing/async-clipboard/clipboard-change-data-while-writing-expected.txt [new file with mode: 0644]
LayoutTests/editing/async-clipboard/clipboard-change-data-while-writing.html [new file with mode: 0644]
LayoutTests/editing/async-clipboard/clipboard-write-basic-expected.txt [new file with mode: 0644]
LayoutTests/editing/async-clipboard/clipboard-write-basic.html [new file with mode: 0644]
LayoutTests/editing/async-clipboard/clipboard-write-items-twice-expected.txt [new file with mode: 0644]
LayoutTests/editing/async-clipboard/clipboard-write-items-twice.html [new file with mode: 0644]
LayoutTests/editing/async-clipboard/resources/async-clipboard-helpers.js
LayoutTests/platform/win/TestExpectations
Source/WebCore/ChangeLog
Source/WebCore/Modules/async-clipboard/Clipboard.cpp
Source/WebCore/Modules/async-clipboard/Clipboard.h
Source/WebCore/platform/ios/PlatformPasteboardIOS.mm
Source/WebCore/platform/mac/PlatformPasteboardMac.mm
Tools/ChangeLog
Tools/DumpRenderTree/mac/DumpRenderTreePasteboard.mm
Tools/WebKitTestRunner/mac/WebKitTestRunnerPasteboard.mm

index 09751b3..d777195 100644 (file)
@@ -1,3 +1,40 @@
+2019-11-13  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [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  <sabouhallawa@apple.com>
 
         [SVG2] Add the 'orient' property of the interface SVGMarkerElement
diff --git a/LayoutTests/editing/async-clipboard/clipboard-change-data-while-writing-expected.txt b/LayoutTests/editing/async-clipboard/clipboard-change-data-while-writing-expected.txt
new file mode 100644 (file)
index 0000000..9072760
--- /dev/null
@@ -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
+
diff --git a/LayoutTests/editing/async-clipboard/clipboard-change-data-while-writing.html b/LayoutTests/editing/async-clipboard/clipboard-change-data-while-writing.html
new file mode 100644 (file)
index 0000000..e3fec52
--- /dev/null
@@ -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="../../resources/js-test.js"></script>
+        <script src="../../resources/ui-helper.js"></script>
+        <script src="./resources/async-clipboard-helpers.js"></script>
+        <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>
diff --git a/LayoutTests/editing/async-clipboard/clipboard-write-basic-expected.txt b/LayoutTests/editing/async-clipboard/clipboard-write-basic-expected.txt
new file mode 100644 (file)
index 0000000..f9c4477
--- /dev/null
@@ -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
+
diff --git a/LayoutTests/editing/async-clipboard/clipboard-write-basic.html b/LayoutTests/editing/async-clipboard/clipboard-write-basic.html
new file mode 100644 (file)
index 0000000..81a1ee6
--- /dev/null
@@ -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="../../resources/js-test.js"></script>
+        <script src="../../resources/ui-helper.js"></script>
+        <script src="./resources/async-clipboard-helpers.js"></script>
+        <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='https://webkit.org/'>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>
diff --git a/LayoutTests/editing/async-clipboard/clipboard-write-items-twice-expected.txt b/LayoutTests/editing/async-clipboard/clipboard-write-items-twice-expected.txt
new file mode 100644 (file)
index 0000000..b7a5c61
--- /dev/null
@@ -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
+
diff --git a/LayoutTests/editing/async-clipboard/clipboard-write-items-twice.html b/LayoutTests/editing/async-clipboard/clipboard-write-items-twice.html
new file mode 100644 (file)
index 0000000..7e62118
--- /dev/null
@@ -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="../../resources/js-test.js"></script>
+        <script src="../../resources/ui-helper.js"></script>
+        <script src="./resources/async-clipboard-helpers.js"></script>
+        <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>
index 57647c9..61bc6a1 100644 (file)
@@ -84,3 +84,12 @@ async function triggerProgrammaticPaste(locationOrElement, options = []) {
             })()`, 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}"`);
+}
index 08e297a..4e1fa03 100644 (file)
@@ -1190,6 +1190,9 @@ webkit.org/b/203100 editing/async-clipboard/clipboard-change-data-while-reading.
 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
index f2822a5..fe9cf67 100644 (file)
@@ -1,3 +1,52 @@
+2019-11-13  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [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  <sabouhallawa@apple.com>
 
         [SVG2] Add the 'orient' property of the interface SVGMarkerElement
index 4b03438..a033dba 100644 (file)
 #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(Navigator& navigator)
 {
 }
 
-Clipboard::~Clipboard() = default;
+Clipboard::~Clipboard()
+{
+    if (auto writer = WTFMove(m_activeItemWriter))
+        writer->invalidate();
+}
 
 Navigator* Clipboard::navigator()
 {
@@ -179,10 +186,43 @@ void Clipboard::getType(ClipboardItem& item, const String& type, Ref<DeferredPro
         promise->reject(NotAllowedError);
 }
 
+static bool shouldProceedWithClipboardWrite(const Frame& frame)
+{
+    auto& settings = frame.settings();
+    if (settings.javaScriptCanAccessClipboard())
+        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
@@ -197,4 +237,91 @@ Pasteboard& Clipboard::activePasteboard()
     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);
+}
+
 }
index d59751b..40a6b46 100644 (file)
@@ -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 DeferredPromise;
 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 @@ private:
 
     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
index 0885ea3..014a47b 100644 (file)
@@ -599,10 +599,10 @@ static RetainPtr<WebItemProviderRegistrationInfoList> createItemProviderRegistra
     }
 
     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]];
index 5ec59d2..31a2f7c 100644 (file)
@@ -226,7 +226,9 @@ int64_t PlatformPasteboard::write(const PasteboardCustomData& data)
 {
     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();
index 18aabb2..319a86c 100644 (file)
@@ -1,3 +1,63 @@
+2019-11-13  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [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  <megan_gardner@apple.com>
 
         Cleanup UIKitSPI for Testing
index e87df8b..119c3db 100644 (file)
@@ -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 @@ static NSMutableDictionary *localPasteboards;
 
 - (NSInteger)declareTypes:(NSArray *)newTypes owner:(id)newOwner
 {
-    _types.clear();
-    _data.clear();
+    [self _clearContentsWithoutUpdatingChangeCount];
 
     [self _addTypesWithoutUpdatingChangeCount:newTypes owner:newOwner];
     return ++_changeCount;
@@ -142,6 +142,19 @@ static RetainPtr<CFStringRef> toUTI(NSString *type)
     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 @@ static RetainPtr<CFStringRef> toUTI(NSString *type)
 
 - (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 @@ static RetainPtr<CFStringRef> toUTI(NSString *type)
 
 - (NSArray<NSPasteboardItem *> *)pasteboardItems
 {
+    if (_writtenPasteboardItems)
+        return _writtenPasteboardItems.get();
+
     auto item = adoptNS([[NSPasteboardItem alloc] init]);
     for (const auto& typeAndData : _data) {
         NSData *data = (__bridge NSData *)typeAndData.value.get();
index 6a42bca..1b6286c 100644 (file)
 
 @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 @@ static NSMutableDictionary *localPasteboards;
     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,22 +118,23 @@ static NSMutableDictionary *localPasteboards;
 
 - (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
 {
     [self _clearContentsWithoutUpdatingChangeCount];
     [self _addTypesWithoutUpdatingChangeCount:newTypes owner:newOwner];
-    return ++changeCount;
+    return ++_changeCount;
 }
 
 - (NSInteger)addTypes:(NSArray<NSPasteboardType> *)newTypes owner:(id)newOwner
@@ -149,7 +142,7 @@ static NSMutableDictionary *localPasteboards;
     [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 @@ static NSMutableDictionary *localPasteboards;
     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 @@ static NSMutableDictionary *localPasteboards;
 
 - (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 @@ static NSMutableDictionary *localPasteboards;
 
 - (BOOL)setData:(NSData *)data forType:(NSString *)dataType
 {
-    if (![typesSet containsObject:dataType])
+    if (![_typesSet containsObject:dataType])
         return NO;
     if (!data)
         data = [NSData 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 @@ static NSMutableDictionary *localPasteboards;
 
 - (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