[Clipboard API] Support navigator.clipboard.read()
https://bugs.webkit.org/show_bug.cgi?id=203021

Reviewed by Ryosuke Niwa.

LayoutTests/imported/w3c:

Rebaseline a web platform test, now that Clipboard.read() no longer immediately rejects.

* web-platform-tests/clipboard-apis/async-navigator-clipboard-basics.https-expected.txt:

Source/WebCore:

Add support for navigator.clipboard.read(), which returns a promise that resolves to a list of ClipboardItems.
See below for more details.

Tests: editing/async-clipboard/clipboard-change-data-while-reading.html
       editing/async-clipboard/clipboard-read-basic.html

* Modules/async-clipboard/Clipboard.cpp:
(WebCore::Clipboard::read):

Implement read(). This makes two calls to the platform pasteboard: the first to get the current change count,
and if the change count is different from the changeCount used for the last read() call (or there are no
existing clipboard items being tracked), then we request pasteboard item information for all items on the
pasteboard, and use this information to create new clipboard items. Otherwise, if the changeCount is still valid
for the current list of clipboard items, simply return these clipboard items.

If the changeCount ends up being different in between the initial changeCount request and when the pasteboard
item information is received, we immediately bail with a NotAllowedError. The new layout test
clipboard-change-data-while-reading.html exercises this scenario.

(WebCore::Clipboard::getType):
(WebCore::Clipboard::frame const):
* Modules/async-clipboard/Clipboard.h:
* Modules/async-clipboard/ClipboardItem.cpp:
(WebCore::ClipboardItem::blobFromString):
(WebCore::ClipboardItem::ClipboardItem):
(WebCore::ClipboardItem::create):
(WebCore::ClipboardItem::navigator):

Refactor this so that each clipboard item itself has a WeakPtr to its Navigator. This avoids having to follow
the weak pointer to the Clipboard to get to the Clipboard's navigator during garbage collection when computing
reachability from opaque roots, since this may happen on a background (GC) thread.

(WebCore::ClipboardItem::clipboard):
* Modules/async-clipboard/ClipboardItem.h:
* Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp:
(WebCore::ClipboardItemBindingsDataSource::getType):
(WebCore::blobFromString): Deleted.

Move this to ClipboardItem, and make it a static method.

* Modules/async-clipboard/ClipboardItemPasteboardDataSource.cpp:
(WebCore::ClipboardItemPasteboardDataSource::ClipboardItemPasteboardDataSource):
(WebCore::ClipboardItemPasteboardDataSource::getType):
* Modules/async-clipboard/ClipboardItemPasteboardDataSource.h:

Move a couple of member variables (index and changeCount) out of ClipboardItem. Instead of having each
ClipboardItem keep track of this information, have the Clipboard that owns the ClipboardItem keep this
information. This means that reading data from ClipboardItem will (in a future patch) work by having the item
ask its Clipboard object to read data on its behalf.

* platform/Pasteboard.cpp:
(WebCore::Pasteboard::allPasteboardItemInfo const):
(WebCore::Pasteboard::pasteboardItemInfo const):
(WebCore::Pasteboard::readString):
(WebCore::Pasteboard::readBuffer):
(WebCore::Pasteboard::readURL):

Add some null checks to handle the case where there is no pasteboard strategy.

Tools:

Make adjustments to WebKitTestRunner and DumpRenderTree to support the new layout tests. See below for more
details.

* DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj:
* DumpRenderTree/mac/DumpRenderTreePasteboard.mm:
(-[LocalPasteboard pasteboardItems]):

Fixes an existing issue with the mock NSPasteboard used for layout tests. Currently, our logic for converting
the contents of the platform pasteboard to NSPasteboardItem simply writes the pasteboard data as-is to
NSPasteboardItems. However, these pasteboard types may be legacy pasteboard types, in which case
NSPasteboardItem will simply handle the call to `-setData:forType:` as a no-op. AppKit has logic in this
scenario to canonicalize these legacy pasteboard types to their modern counterparts, but this is absent in
DumpRenderTreePasteboard and WebKitTestRunnerPasteboard.

Address this by teaching the mock pasteboards to convert legacy types to modern types when generating platform
pasteboard items.

* TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
* TestRunnerShared/UIScriptContext/UIScriptController.h:
(WTR::UIScriptController::copyText):

Add a new UIScriptController method to write a string to the platform pasteboard. This patch adds support for
this new testing hook on macOS and iOS, in WebKit2 (WebKitTestRunner).

* TestRunnerShared/mac/NSPasteboardAdditions.h: Copied from Tools/WebKitTestRunner/mac/UIScriptControllerMac.h.
* TestRunnerShared/mac/NSPasteboardAdditions.mm: Added.
(+[NSPasteboard _modernPasteboardType:]):

Add a helper to convert legacy pasteboard types (and dynamic UTIs that map to legacy pasteboard types) to
modern pasteboard types, suitable for writing to NSPasteboardItems on macOS.

* WebKitTestRunner/WebKitTestRunner.xcodeproj/project.pbxproj:
* WebKitTestRunner/ios/UIScriptControllerIOS.h:
* WebKitTestRunner/ios/UIScriptControllerIOS.mm:
(WTR::UIScriptControllerIOS::copyText):
* WebKitTestRunner/mac/UIScriptControllerMac.h:
* WebKitTestRunner/mac/UIScriptControllerMac.mm:
(WTR::UIScriptControllerMac::copyText):
* WebKitTestRunner/mac/WebKitTestRunnerPasteboard.mm:

Apply the same fix for WebKitTestRunner's mock NSPasteboard.

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

Make -clearContents clear out all the contents on the mock pasteboard, instead of crashing in AppKit.

(-[LocalPasteboard declareTypes:owner:]):
(-[LocalPasteboard pasteboardItems]):

LayoutTests:

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

Add a new layout test to verify that if the platform pasteboard changes in the middle of a DOM paste access
request, the promise returned by Clipboard.read() should reject, and the page should not receive any clipboard
items.

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

Add a new layout test to exercise Clipboard.read(). Since we don't support reading data from clipboard items
yet, this only checks the types of each pasteboard item. This test additionally ensures that the ClipboardItems
returned from the API are the same between calls to Clipboard.read() if the data hasn't changed.

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

Add a new helper to synchronously write data to the clipboard using execCommand and DataTransfer API.

(async.triggerProgrammaticPaste):

Add a new helper to trigger programmatic paste by activating the given element or location. Also receives an
array of options (which, for now, just supports a single option to change the pasteboard when granting DOM paste
access).

* platform/ios-wk1/TestExpectations: Skip clipboard-change-data-while-reading.html for now in WebKit1.
* platform/mac-wk1/TestExpectations: Skip clipboard-change-data-while-reading.html for now in WebKit1.
* platform/win/TestExpectations: Skip the new layout tests on Windows for now.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251279 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 7e95287..4dbc4c1 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,39 @@
+2019-10-17  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Clipboard API] Support navigator.clipboard.read()
+        https://bugs.webkit.org/show_bug.cgi?id=203021
+
+        Reviewed by Ryosuke Niwa.
+
+        * editing/async-clipboard/clipboard-change-data-while-reading-expected.txt: Added.
+        * editing/async-clipboard/clipboard-change-data-while-reading.html: Added.
+
+        Add a new layout test to verify that if the platform pasteboard changes in the middle of a DOM paste access
+        request, the promise returned by Clipboard.read() should reject, and the page should not receive any clipboard
+        items.
+
+        * editing/async-clipboard/clipboard-read-basic-expected.txt: Added.
+        * editing/async-clipboard/clipboard-read-basic.html: Added.
+
+        Add a new layout test to exercise Clipboard.read(). Since we don't support reading data from clipboard items
+        yet, this only checks the types of each pasteboard item. This test additionally ensures that the ClipboardItems
+        returned from the API are the same between calls to Clipboard.read() if the data hasn't changed.
+
+        * editing/async-clipboard/resources/async-clipboard-helpers.js:
+        (writeToClipboardUsingDataTransfer):
+
+        Add a new helper to synchronously write data to the clipboard using execCommand and DataTransfer API.
+
+        (async.triggerProgrammaticPaste):
+
+        Add a new helper to trigger programmatic paste by activating the given element or location. Also receives an
+        array of options (which, for now, just supports a single option to change the pasteboard when granting DOM paste
+        access).
+
+        * platform/ios-wk1/TestExpectations: Skip clipboard-change-data-while-reading.html for now in WebKit1.
+        * platform/mac-wk1/TestExpectations: Skip clipboard-change-data-while-reading.html for now in WebKit1.
+        * platform/win/TestExpectations: Skip the new layout tests on Windows for now.
+
 2019-10-17  Chris Dumez  <cdumez@apple.com>
 
         Don't put pages that have not reached the non-visually empty layout milestone in the back/forward cache
diff --git a/LayoutTests/editing/async-clipboard/clipboard-change-data-while-reading-expected.txt b/LayoutTests/editing/async-clipboard/clipboard-change-data-while-reading-expected.txt
new file mode 100644
index 0000000..80020db
--- /dev/null
+++ b/LayoutTests/editing/async-clipboard/clipboard-change-data-while-reading-expected.txt
@@ -0,0 +1,10 @@
+This test verifies that if platform pasteboard contents are changed immediately after granting programmatic clipboard access during Clipboard.read(), 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 read clipboard items with NotAllowedError.
+PASS finishedReading became true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/editing/async-clipboard/clipboard-change-data-while-reading.html b/LayoutTests/editing/async-clipboard/clipboard-change-data-while-reading.html
new file mode 100644
index 0000000..031517b
--- /dev/null
+++ b/LayoutTests/editing/async-clipboard/clipboard-change-data-while-reading.html
@@ -0,0 +1,64 @@
+<!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;
+        finishedReading = false;
+
+        async function runTest() {
+            description("This test verifies that if platform pasteboard contents are changed immediately after granting programmatic clipboard access during Clipboard.read(), the promise should be rejected. This test needs to be run in WebKitTestRunner or DumpRenderTree.");
+
+            const copyElement = document.getElementById("copy");
+            const pasteElement = document.getElementById("paste");
+            pasteElement.addEventListener("click", async event => {
+                try {
+                    testFailed(`Did not expect to read ${(await navigator.clipboard.read()).length} item(s).`);
+                } catch (exception) {
+                    testPassed(`Failed to read clipboard items with ${exception.name}.`);
+                } finally {
+                    finishedReading = true;
+                }
+            });
+
+            await UIHelper.activateElement(copyElement);
+            await triggerProgrammaticPaste(pasteElement, ["ChangePasteboardWhenGrantingAccess"]);
+            await new Promise(resolve => shouldBecomeEqual("finishedReading", "true", resolve));
+
+            [pasteElement, copyElement].map(element => element.remove());
+            finishJSTest();
+        }
+
+        addEventListener("load", runTest);
+    </script>
+    <body>
+        <iframe id="copy" src="data:text/html,<button id='copy' style='text-align: center; font-size: 24px; width: 100%; height: 100%; line-height: 80px;'>Click here to copy</button>
+        <p id='text' style='text-align: center; font-size: 24px; width: 100%; height: 100%; line-height: 80px;'>This is some text to copy.</p>
+        <script>
+        copy.addEventListener('click', () => {
+            getSelection().selectAllChildren(text);
+            document.execCommand('Copy');
+            getSelection().removeAllRanges();
+        });
+        </script>"></iframe>
+        <div><button id="paste">Paste</button></div>
+        <div id="description"></div>
+        <div id="console"></div>
+    </body>
+</html>
diff --git a/LayoutTests/editing/async-clipboard/clipboard-read-basic-expected.txt b/LayoutTests/editing/async-clipboard/clipboard-read-basic-expected.txt
new file mode 100644
index 0000000..e03e5ae
--- /dev/null
+++ b/LayoutTests/editing/async-clipboard/clipboard-read-basic-expected.txt
@@ -0,0 +1,21 @@
+This test verifies that clipboard items can be read with their expected types, and that the wrapper for clipboard items is the same between calls to Clipboard.read. This test requires WebKitTestRunner or DumpRenderTree.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS sortedTypes.length is 3
+PASS sortedTypes[0] is "text/html"
+PASS sortedTypes[1] is "text/plain"
+PASS sortedTypes[2] is "text/uri-list"
+PASS item.foo is undefined
+PASS readCount became 1
+PASS sortedTypes.length is 3
+PASS sortedTypes[0] is "text/html"
+PASS sortedTypes[1] is "text/plain"
+PASS sortedTypes[2] is "text/uri-list"
+PASS item.foo is "bar"
+PASS readCount became 2
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/editing/async-clipboard/clipboard-read-basic.html b/LayoutTests/editing/async-clipboard/clipboard-read-basic.html
new file mode 100644
index 0000000..e63da70
--- /dev/null
+++ b/LayoutTests/editing/async-clipboard/clipboard-read-basic.html
@@ -0,0 +1,74 @@
+<!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;
+
+        description("This test verifies that clipboard items can be read with their expected types, and that the wrapper for clipboard items is the same between calls to Clipboard.read. This test requires WebKitTestRunner or DumpRenderTree.");
+
+        async function runTest() {
+            const copyButton = document.getElementById("copy");
+            const pasteButton = document.getElementById("paste");
+            copyButton.addEventListener("click", event => {
+                writeToClipboardUsingDataTransfer({
+                    "text/html": "<a href='https://www.apple.com/'>apple.com</a>",
+                    "text/plain": "apple.com",
+                    "text/uri-list": "https://apple.com/",
+                });
+                event.preventDefault();
+            });
+
+            pasteButton.addEventListener("click", async event => {
+                for (const item of await navigator.clipboard.read()) {
+                    window.item = item;
+                    window.sortedTypes = item.types.slice(0);
+                    sortedTypes.sort();
+                    shouldBe("sortedTypes.length", "3");
+                    shouldBeEqualToString("sortedTypes[0]", "text/html");
+                    shouldBeEqualToString("sortedTypes[1]", "text/plain");
+                    shouldBeEqualToString("sortedTypes[2]", "text/uri-list");
+                    if (isFirstTimeReading)
+                        shouldBe("item.foo", "undefined");
+                    else
+                        shouldBeEqualToString("item.foo", "bar");
+                    item.foo = "bar";
+                    window.item = window.sortedTypes = null;
+                }
+                ++readCount;
+            });
+
+            readCount = 0;
+            isFirstTimeReading = true;
+            await UIHelper.activateElement(copyButton);
+            await UIHelper.activateElement(pasteButton);
+            await new Promise(resolve => shouldBecomeEqual("readCount", "1", resolve));
+
+            GCController.collect();
+
+            isFirstTimeReading = false;
+            await UIHelper.activateElement(pasteButton);
+            await new Promise(resolve => shouldBecomeEqual("readCount", "2", resolve));
+
+            [pasteButton, copyButton].map(button => button.remove());
+            finishJSTest();
+        }
+
+        addEventListener("load", runTest);
+    </script>
+    <body>
+        <button id="copy">Copy</button>
+        <button id="paste">Paste</button>
+    </body>
+</html>
diff --git a/LayoutTests/editing/async-clipboard/resources/async-clipboard-helpers.js b/LayoutTests/editing/async-clipboard/resources/async-clipboard-helpers.js
index 3507289..9f48b60 100644
--- a/LayoutTests/editing/async-clipboard/resources/async-clipboard-helpers.js
+++ b/LayoutTests/editing/async-clipboard/resources/async-clipboard-helpers.js
@@ -29,3 +29,52 @@
         image.src = URL.createObjectURL(blob);
     });
 }
+
+function writeToClipboardUsingDataTransfer(data) {
+    const input = document.createElement("input");
+    document.body.appendChild(input);
+    input.value = "a";
+    input.setSelectionRange(0, 1);
+    input.addEventListener("copy", event => {
+        for (const type of Object.keys(data))
+            event.clipboardData.setData(type, data[type]);
+        event.preventDefault();
+    }, { once: true });
+    document.execCommand("copy");
+    input.remove();
+}
+
+
+async function triggerProgrammaticPaste(locationOrElement, options = []) {
+    let x, y;
+    if (locationOrElement instanceof Element)
+        [x, y] = [locationOrElement.offsetLeft + locationOrElement.offsetWidth / 2, locationOrElement.offsetTop + locationOrElement.offsetHeight / 2];
+    else
+        [x, y] = [locationOrElement.x, locationOrElement.y];
+
+    return new Promise(resolve => {
+        testRunner.runUIScript(`
+            (() => {
+                doneCount = 0;
+                function checkDone() {
+                    if (++doneCount === 3)
+                        uiController.uiScriptComplete();
+                }
+
+                uiController.didHideMenuCallback = checkDone;
+
+                function resolveDOMPasteRequest() {
+                    if (${options.includes("ChangePasteboardWhenGrantingAccess")})
+                        uiController.copyText("*** this text should never appear in a passing layout test ***");
+                    uiController.chooseMenuAction("Paste", checkDone);
+                }
+
+                if (uiController.isShowingMenu)
+                    resolveDOMPasteRequest();
+                else
+                    uiController.didShowMenuCallback = resolveDOMPasteRequest;
+
+                uiController.activateAtPoint(${x}, ${y}, checkDone);
+            })()`, resolve);
+    });
+}
diff --git a/LayoutTests/imported/w3c/ChangeLog b/LayoutTests/imported/w3c/ChangeLog
index 748fd30..af87797 100644
--- a/LayoutTests/imported/w3c/ChangeLog
+++ b/LayoutTests/imported/w3c/ChangeLog
@@ -1,3 +1,14 @@
+2019-10-17  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Clipboard API] Support navigator.clipboard.read()
+        https://bugs.webkit.org/show_bug.cgi?id=203021
+
+        Reviewed by Ryosuke Niwa.
+
+        Rebaseline a web platform test, now that Clipboard.read() no longer immediately rejects.
+
+        * web-platform-tests/clipboard-apis/async-navigator-clipboard-basics.https-expected.txt:
+
 2019-10-17  Rob Buis  <rbuis@igalia.com>
 
         Remove duplicate MathML tests
diff --git a/LayoutTests/imported/w3c/web-platform-tests/clipboard-apis/async-navigator-clipboard-basics.https-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/clipboard-apis/async-navigator-clipboard-basics.https-expected.txt
index 99aed2e..8ab41cf 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/clipboard-apis/async-navigator-clipboard-basics.https-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/clipboard-apis/async-navigator-clipboard-basics.https-expected.txt
@@ -6,6 +6,6 @@
 PASS navigator.clipboard.write(DOMString) fails (expect DataTransfer) 
 FAIL navigator.clipboard.writeText(DOMString) succeeds promise_test: Unhandled rejection with value: object "NotSupportedError: The operation is not supported."
 PASS navigator.clipboard.writeText() fails (expect DOMString) 
-FAIL navigator.clipboard.read() succeeds promise_test: Unhandled rejection with value: object "NotSupportedError: The operation is not supported."
+FAIL navigator.clipboard.read() succeeds assert_true: expected true got false
 FAIL navigator.clipboard.readText() succeeds promise_test: Unhandled rejection with value: object "NotSupportedError: The operation is not supported."
 
diff --git a/LayoutTests/platform/ios-wk1/TestExpectations b/LayoutTests/platform/ios-wk1/TestExpectations
index 3cbeffb..c309734 100644
--- a/LayoutTests/platform/ios-wk1/TestExpectations
+++ b/LayoutTests/platform/ios-wk1/TestExpectations
@@ -31,6 +31,9 @@
 fast/forms/datalist [ WontFix ]
 imported/w3c/web-platform-tests/html/semantics/forms/the-datalist-element [ WontFix ]
 
+# DOM paste access requests are not implemented in WebKit1.
+editing/async-clipboard/clipboard-change-data-while-reading.html [ Skip ]
+
 # testRunner.queueLoad() does not support loading data URLs in iOS WK1
 http/tests/security/contentSecurityPolicy/navigate-self-to-data-url.html [ Skip ]
 
diff --git a/LayoutTests/platform/mac-wk1/TestExpectations b/LayoutTests/platform/mac-wk1/TestExpectations
index 8678d6d..20da02b 100644
--- a/LayoutTests/platform/mac-wk1/TestExpectations
+++ b/LayoutTests/platform/mac-wk1/TestExpectations
@@ -65,6 +65,9 @@
 fast/forms/datalist [ WontFix ]
 imported/w3c/web-platform-tests/html/semantics/forms/the-datalist-element [ WontFix ]
 
+# DOM paste access requests are not implemented in WebKit1.
+editing/async-clipboard/clipboard-change-data-while-reading.html [ Skip ]
+
 imported/w3c/web-platform-tests/websockets/Close-1000-reason.any.html [ Pass Failure ]
 imported/w3c/web-platform-tests/websockets/Close-1000-reason.any.worker.html [ Pass Failure ]
 imported/w3c/web-platform-tests/websockets/binary/001.html [ Pass Failure ]
diff --git a/LayoutTests/platform/win/TestExpectations b/LayoutTests/platform/win/TestExpectations
index 4b58a8f..c9241de 100644
--- a/LayoutTests/platform/win/TestExpectations
+++ b/LayoutTests/platform/win/TestExpectations
@@ -1189,6 +1189,9 @@
 http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-in-same-origin.html [ Skip ]
 http/tests/security/clipboard/copy-paste-html-across-origin-strips-mso-list.html [ Skip ]
 
+webkit.org/b/203100 editing/async-clipboard/clipboard-read-basic.html [ Skip ]
+webkit.org/b/203100 editing/async-clipboard/clipboard-change-data-while-reading.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
 webkit.org/b/140786 editing/pasteboard/copy-backslash-with-euc.html [ Skip ] # Causes later tests to fail
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 05dcb20..b022774 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,69 @@
+2019-10-17  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Clipboard API] Support navigator.clipboard.read()
+        https://bugs.webkit.org/show_bug.cgi?id=203021
+
+        Reviewed by Ryosuke Niwa.
+
+        Add support for navigator.clipboard.read(), which returns a promise that resolves to a list of ClipboardItems.
+        See below for more details.
+
+        Tests: editing/async-clipboard/clipboard-change-data-while-reading.html
+               editing/async-clipboard/clipboard-read-basic.html
+
+        * Modules/async-clipboard/Clipboard.cpp:
+        (WebCore::Clipboard::read):
+
+        Implement read(). This makes two calls to the platform pasteboard: the first to get the current change count,
+        and if the change count is different from the changeCount used for the last read() call (or there are no
+        existing clipboard items being tracked), then we request pasteboard item information for all items on the
+        pasteboard, and use this information to create new clipboard items. Otherwise, if the changeCount is still valid
+        for the current list of clipboard items, simply return these clipboard items.
+
+        If the changeCount ends up being different in between the initial changeCount request and when the pasteboard
+        item information is received, we immediately bail with a NotAllowedError. The new layout test
+        clipboard-change-data-while-reading.html exercises this scenario.
+
+        (WebCore::Clipboard::getType):
+        (WebCore::Clipboard::frame const):
+        * Modules/async-clipboard/Clipboard.h:
+        * Modules/async-clipboard/ClipboardItem.cpp:
+        (WebCore::ClipboardItem::blobFromString):
+        (WebCore::ClipboardItem::ClipboardItem):
+        (WebCore::ClipboardItem::create):
+        (WebCore::ClipboardItem::navigator):
+
+        Refactor this so that each clipboard item itself has a WeakPtr to its Navigator. This avoids having to follow
+        the weak pointer to the Clipboard to get to the Clipboard's navigator during garbage collection when computing
+        reachability from opaque roots, since this may happen on a background (GC) thread.
+
+        (WebCore::ClipboardItem::clipboard):
+        * Modules/async-clipboard/ClipboardItem.h:
+        * Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp:
+        (WebCore::ClipboardItemBindingsDataSource::getType):
+        (WebCore::blobFromString): Deleted.
+
+        Move this to ClipboardItem, and make it a static method.
+
+        * Modules/async-clipboard/ClipboardItemPasteboardDataSource.cpp:
+        (WebCore::ClipboardItemPasteboardDataSource::ClipboardItemPasteboardDataSource):
+        (WebCore::ClipboardItemPasteboardDataSource::getType):
+        * Modules/async-clipboard/ClipboardItemPasteboardDataSource.h:
+
+        Move a couple of member variables (index and changeCount) out of ClipboardItem. Instead of having each
+        ClipboardItem keep track of this information, have the Clipboard that owns the ClipboardItem keep this
+        information. This means that reading data from ClipboardItem will (in a future patch) work by having the item
+        ask its Clipboard object to read data on its behalf.
+
+        * platform/Pasteboard.cpp:
+        (WebCore::Pasteboard::allPasteboardItemInfo const):
+        (WebCore::Pasteboard::pasteboardItemInfo const):
+        (WebCore::Pasteboard::readString):
+        (WebCore::Pasteboard::readBuffer):
+        (WebCore::Pasteboard::readURL):
+
+        Add some null checks to handle the case where there is no pasteboard strategy.
+
 2019-10-17  Chris Dumez  <cdumez@apple.com>
 
         Don't put pages that have not reached the non-visually empty layout milestone in the back/forward cache
diff --git a/Source/WebCore/Modules/async-clipboard/Clipboard.cpp b/Source/WebCore/Modules/async-clipboard/Clipboard.cpp
index 693b14b..acc5814 100644
--- a/Source/WebCore/Modules/async-clipboard/Clipboard.cpp
+++ b/Source/WebCore/Modules/async-clipboard/Clipboard.cpp
@@ -26,10 +26,14 @@
 #include "config.h"
 #include "Clipboard.h"
 
+#include "Blob.h"
 #include "ClipboardItem.h"
+#include "Frame.h"
+#include "JSClipboardItem.h"
 #include "JSDOMPromise.h"
 #include "JSDOMPromiseDeferred.h"
 #include "Navigator.h"
+#include "Pasteboard.h"
 #include <wtf/IsoMallocInlines.h>
 
 namespace WebCore {
@@ -46,6 +50,8 @@
 {
 }
 
+Clipboard::~Clipboard() = default;
+
 Navigator* Clipboard::navigator()
 {
     return m_navigator.get();
@@ -74,6 +80,54 @@
 
 void Clipboard::read(Ref<DeferredPromise>&& promise)
 {
+    auto rejectPromiseAndClearActiveSession = [&] {
+        m_activeSession = WTF::nullopt;
+        promise->reject(NotAllowedError);
+    };
+
+    auto frame = makeRefPtr(this->frame());
+    if (!frame) {
+        rejectPromiseAndClearActiveSession();
+        return;
+    }
+
+    auto pasteboard = Pasteboard::createForCopyAndPaste();
+    int changeCountAtStart = pasteboard->changeCount();
+
+    if (!frame->requestDOMPasteAccess()) {
+        rejectPromiseAndClearActiveSession();
+        return;
+    }
+
+    if (!m_activeSession || m_activeSession->changeCount != changeCountAtStart) {
+        auto allInfo = pasteboard->allPasteboardItemInfo();
+        if (allInfo.isEmpty()) {
+            rejectPromiseAndClearActiveSession();
+            return;
+        }
+
+        Vector<Ref<ClipboardItem>> clipboardItems;
+        clipboardItems.reserveInitialCapacity(allInfo.size());
+        for (auto& itemInfo : allInfo) {
+            // FIXME: This should be refactored such that the initial changeCount is delivered to the client, where it is then checked
+            // against the current changeCount of the platform pasteboard. For instance, in WebKit2, this would relocate the changeCount
+            // check to the UI process instead of the web content process.
+            if (itemInfo.changeCount != changeCountAtStart) {
+                rejectPromiseAndClearActiveSession();
+                return;
+            }
+            clipboardItems.uncheckedAppend(ClipboardItem::create(*this, itemInfo));
+        }
+        m_activeSession = {{ WTFMove(pasteboard), WTFMove(clipboardItems), changeCountAtStart }};
+    }
+
+    promise->resolve<IDLSequence<IDLInterface<ClipboardItem>>>(m_activeSession->items);
+}
+
+void Clipboard::getType(ClipboardItem& item, const String& type, Ref<DeferredPromise>&& promise)
+{
+    UNUSED_PARAM(item);
+    UNUSED_PARAM(type);
     promise->reject(NotSupportedError);
 }
 
@@ -83,4 +137,9 @@
     promise->reject(NotSupportedError);
 }
 
+Frame* Clipboard::frame() const
+{
+    return m_navigator ? m_navigator->frame() : nullptr;
+}
+
 }
diff --git a/Source/WebCore/Modules/async-clipboard/Clipboard.h b/Source/WebCore/Modules/async-clipboard/Clipboard.h
index 1373f4d..217314b 100644
--- a/Source/WebCore/Modules/async-clipboard/Clipboard.h
+++ b/Source/WebCore/Modules/async-clipboard/Clipboard.h
@@ -34,16 +34,20 @@
 
 class ClipboardItem;
 class DeferredPromise;
+class Frame;
 class Navigator;
+class Pasteboard;
 
 class Clipboard final : public RefCounted<Clipboard>, public EventTargetWithInlineData, public CanMakeWeakPtr<Clipboard> {
     WTF_MAKE_ISO_ALLOCATED(Clipboard);
 public:
     static Ref<Clipboard> create(Navigator&);
+    ~Clipboard();
 
     EventTargetInterface eventTargetInterface() const final;
     ScriptExecutionContext* scriptExecutionContext() const final;
 
+    Frame* frame() const;
     Navigator* navigator();
 
     using RefCounted::ref;
@@ -55,12 +59,21 @@
     void read(Ref<DeferredPromise>&&);
     void write(const Vector<RefPtr<ClipboardItem>>& data, Ref<DeferredPromise>&&);
 
+    void getType(ClipboardItem&, const String& type, Ref<DeferredPromise>&&);
+
 private:
     Clipboard(Navigator&);
 
+    struct Session {
+        std::unique_ptr<Pasteboard> pasteboard;
+        Vector<Ref<ClipboardItem>> items;
+        int changeCount;
+    };
+
     void refEventTarget() final { ref(); }
     void derefEventTarget() final { deref(); }
 
+    Optional<Session> m_activeSession;
     WeakPtr<Navigator> m_navigator;
 };
 
diff --git a/Source/WebCore/Modules/async-clipboard/ClipboardItem.cpp b/Source/WebCore/Modules/async-clipboard/ClipboardItem.cpp
index 9c7a2e3..bdcb9a0 100644
--- a/Source/WebCore/Modules/async-clipboard/ClipboardItem.cpp
+++ b/Source/WebCore/Modules/async-clipboard/ClipboardItem.cpp
@@ -26,15 +26,23 @@
 #include "config.h"
 #include "ClipboardItem.h"
 
+#include "Blob.h"
 #include "ClipboardItemBindingsDataSource.h"
 #include "ClipboardItemPasteboardDataSource.h"
 #include "Navigator.h"
 #include "PasteboardItemInfo.h"
+#include "SharedBuffer.h"
 
 namespace WebCore {
 
 ClipboardItem::~ClipboardItem() = default;
 
+Ref<Blob> ClipboardItem::blobFromString(const String& stringData, const String& type)
+{
+    auto utf8 = stringData.utf8();
+    return Blob::create(SharedBuffer::create(utf8.data(), utf8.length()), Blob::normalizedContentType(type));
+}
+
 static ClipboardItem::PresentationStyle clipboardItemPresentationStyle(const PasteboardItemInfo& info)
 {
     switch (info.preferredPresentationStyle) {
@@ -55,9 +63,10 @@
 {
 }
 
-ClipboardItem::ClipboardItem(Clipboard& clipboard, const PasteboardItemInfo& info, size_t index)
+ClipboardItem::ClipboardItem(Clipboard& clipboard, const PasteboardItemInfo& info)
     : m_clipboard(makeWeakPtr(clipboard))
-    , m_dataSource(makeUnique<ClipboardItemPasteboardDataSource>(*this, info, index))
+    , m_navigator(makeWeakPtr(clipboard.navigator()))
+    , m_dataSource(makeUnique<ClipboardItemPasteboardDataSource>(*this, info))
     , m_presentationStyle(clipboardItemPresentationStyle(info))
 {
 }
@@ -67,9 +76,9 @@
     return adoptRef(*new ClipboardItem(WTFMove(data), options));
 }
 
-Ref<ClipboardItem> ClipboardItem::create(Clipboard& clipboard, const PasteboardItemInfo& info, size_t index)
+Ref<ClipboardItem> ClipboardItem::create(Clipboard& clipboard, const PasteboardItemInfo& info)
 {
-    return adoptRef(*new ClipboardItem(clipboard, info, index));
+    return adoptRef(*new ClipboardItem(clipboard, info));
 }
 
 Vector<String> ClipboardItem::types() const
@@ -84,7 +93,12 @@
 
 Navigator* ClipboardItem::navigator()
 {
-    return m_clipboard ? m_clipboard->navigator() : nullptr;
+    return m_navigator.get();
+}
+
+Clipboard* ClipboardItem::clipboard()
+{
+    return m_clipboard.get();
 }
 
 } // namespace WebCore
diff --git a/Source/WebCore/Modules/async-clipboard/ClipboardItem.h b/Source/WebCore/Modules/async-clipboard/ClipboardItem.h
index 98e96ff..49d2547 100644
--- a/Source/WebCore/Modules/async-clipboard/ClipboardItem.h
+++ b/Source/WebCore/Modules/async-clipboard/ClipboardItem.h
@@ -53,21 +53,22 @@
     };
 
     static Ref<ClipboardItem> create(Vector<KeyValuePair<String, RefPtr<DOMPromise>>>&&, const Options&);
-    static Ref<ClipboardItem> create(Clipboard&, const PasteboardItemInfo&, size_t index);
+    static Ref<ClipboardItem> create(Clipboard&, const PasteboardItemInfo&);
+    static Ref<Blob> blobFromString(const String& stringData, const String& type);
 
     Vector<String> types() const;
     void getType(const String&, Ref<DeferredPromise>&&);
 
     PresentationStyle presentationStyle() const { return m_presentationStyle; };
     Navigator* navigator();
-
-protected:
-    WeakPtr<Clipboard> m_clipboard;
+    Clipboard* clipboard();
 
 private:
     ClipboardItem(Vector<KeyValuePair<String, RefPtr<DOMPromise>>>&&, const Options&);
-    ClipboardItem(Clipboard&, const PasteboardItemInfo&, size_t index);
+    ClipboardItem(Clipboard&, const PasteboardItemInfo&);
 
+    WeakPtr<Clipboard> m_clipboard;
+    WeakPtr<Navigator> m_navigator;
     std::unique_ptr<ClipboardItemDataSource> m_dataSource;
     PresentationStyle m_presentationStyle { PresentationStyle::Unspecified };
 };
diff --git a/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp b/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp
index 5038141..a45fcb0 100644
--- a/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp
+++ b/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp
@@ -35,12 +35,6 @@
 
 namespace WebCore {
 
-static Ref<Blob> blobFromString(const String& stringData, const String& type)
-{
-    auto utf8 = stringData.utf8();
-    return Blob::create(SharedBuffer::create(utf8.data(), utf8.length()), Blob::normalizedContentType(type));
-}
-
 ClipboardItemBindingsDataSource::ClipboardItemBindingsDataSource(ClipboardItem& item, Vector<KeyValuePair<String, RefPtr<DOMPromise>>>&& itemPromises)
     : ClipboardItemDataSource(item)
     , m_itemPromises(WTFMove(itemPromises))
@@ -83,7 +77,7 @@
         String string;
         result.getString(itemPromise->globalObject()->globalExec(), string);
         if (!string.isNull()) {
-            promise->resolve<IDLInterface<Blob>>(blobFromString(string, type));
+            promise->resolve<IDLInterface<Blob>>(ClipboardItem::blobFromString(string, type));
             return;
         }
 
diff --git a/Source/WebCore/Modules/async-clipboard/ClipboardItemPasteboardDataSource.cpp b/Source/WebCore/Modules/async-clipboard/ClipboardItemPasteboardDataSource.cpp
index 7b9ead1..5bbae75 100644
--- a/Source/WebCore/Modules/async-clipboard/ClipboardItemPasteboardDataSource.cpp
+++ b/Source/WebCore/Modules/async-clipboard/ClipboardItemPasteboardDataSource.cpp
@@ -30,11 +30,9 @@
 
 namespace WebCore {
 
-ClipboardItemPasteboardDataSource::ClipboardItemPasteboardDataSource(ClipboardItem& item, const PasteboardItemInfo& info, size_t itemIndex)
+ClipboardItemPasteboardDataSource::ClipboardItemPasteboardDataSource(ClipboardItem& item, const PasteboardItemInfo& info)
     : ClipboardItemDataSource(item)
     , m_types(info.webSafeTypesByFidelity)
-    , m_itemIndex(itemIndex)
-    , m_initialChangeCount(info.changeCount)
 {
 }
 
@@ -47,11 +45,10 @@
 
 void ClipboardItemPasteboardDataSource::getType(const String& type, Ref<DeferredPromise>&& promise)
 {
-    // FIXME: Not implemented.
-    UNUSED_PARAM(m_initialChangeCount);
-    UNUSED_PARAM(m_itemIndex);
-    UNUSED_PARAM(type);
-    promise->reject(NotSupportedError);
+    if (auto clipboard = makeRefPtr(m_item.clipboard()))
+        clipboard->getType(m_item, type, WTFMove(promise));
+    else
+        promise->reject(NotAllowedError);
 }
 
 } // namespace WebCore
diff --git a/Source/WebCore/Modules/async-clipboard/ClipboardItemPasteboardDataSource.h b/Source/WebCore/Modules/async-clipboard/ClipboardItemPasteboardDataSource.h
index a021530..b26328b 100644
--- a/Source/WebCore/Modules/async-clipboard/ClipboardItemPasteboardDataSource.h
+++ b/Source/WebCore/Modules/async-clipboard/ClipboardItemPasteboardDataSource.h
@@ -34,7 +34,7 @@
 class ClipboardItemPasteboardDataSource : public ClipboardItemDataSource {
     WTF_MAKE_FAST_ALLOCATED;
 public:
-    ClipboardItemPasteboardDataSource(ClipboardItem&, const PasteboardItemInfo&, size_t itemIndex);
+    ClipboardItemPasteboardDataSource(ClipboardItem&, const PasteboardItemInfo&);
     ~ClipboardItemPasteboardDataSource();
 
 private:
@@ -42,8 +42,6 @@
     void getType(const String&, Ref<DeferredPromise>&&) final;
 
     Vector<String> m_types;
-    size_t m_itemIndex { 0 };
-    int m_initialChangeCount { 0 };
 };
 
 } // namespace WebCore
diff --git a/Source/WebCore/platform/Pasteboard.cpp b/Source/WebCore/platform/Pasteboard.cpp
index e734b38..d07a5fa 100644
--- a/Source/WebCore/platform/Pasteboard.cpp
+++ b/Source/WebCore/platform/Pasteboard.cpp
@@ -64,27 +64,37 @@
 
 Vector<PasteboardItemInfo> Pasteboard::allPasteboardItemInfo() const
 {
-    return platformStrategies()->pasteboardStrategy()->allPasteboardItemInfo(name());
+    if (auto* strategy = platformStrategies()->pasteboardStrategy())
+        return strategy->allPasteboardItemInfo(name());
+    return { };
 }
 
 PasteboardItemInfo Pasteboard::pasteboardItemInfo(size_t index) const
 {
-    return platformStrategies()->pasteboardStrategy()->informationForItemAtIndex(index, name());
+    if (auto* strategy = platformStrategies()->pasteboardStrategy())
+        return strategy->informationForItemAtIndex(index, name());
+    return { };
 }
 
 String Pasteboard::readString(size_t index, const String& type)
 {
-    return platformStrategies()->pasteboardStrategy()->readStringFromPasteboard(index, type, name());
+    if (auto* strategy = platformStrategies()->pasteboardStrategy())
+        return strategy->readStringFromPasteboard(index, type, name());
+    return { };
 }
 
 RefPtr<WebCore::SharedBuffer> Pasteboard::readBuffer(size_t index, const String& type)
 {
-    return platformStrategies()->pasteboardStrategy()->readBufferFromPasteboard(index, type, name());
+    if (auto* strategy = platformStrategies()->pasteboardStrategy())
+        return strategy->readBufferFromPasteboard(index, type, name());
+    return nullptr;
 }
 
 URL Pasteboard::readURL(size_t index, String& title)
 {
-    return platformStrategies()->pasteboardStrategy()->readURLFromPasteboard(index, name(), title);
+    if (auto* strategy = platformStrategies()->pasteboardStrategy())
+        return strategy->readURLFromPasteboard(index, name(), title);
+    return { };
 }
 
 };
diff --git a/Tools/ChangeLog b/Tools/ChangeLog
index 1f29cd9..69ea349 100644
--- a/Tools/ChangeLog
+++ b/Tools/ChangeLog
@@ -1,3 +1,60 @@
+2019-10-17  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Clipboard API] Support navigator.clipboard.read()
+        https://bugs.webkit.org/show_bug.cgi?id=203021
+
+        Reviewed by Ryosuke Niwa.
+
+        Make adjustments to WebKitTestRunner and DumpRenderTree to support the new layout tests. See below for more
+        details.
+
+        * DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj:
+        * DumpRenderTree/mac/DumpRenderTreePasteboard.mm:
+        (-[LocalPasteboard pasteboardItems]):
+
+        Fixes an existing issue with the mock NSPasteboard used for layout tests. Currently, our logic for converting
+        the contents of the platform pasteboard to NSPasteboardItem simply writes the pasteboard data as-is to
+        NSPasteboardItems. However, these pasteboard types may be legacy pasteboard types, in which case
+        NSPasteboardItem will simply handle the call to `-setData:forType:` as a no-op. AppKit has logic in this
+        scenario to canonicalize these legacy pasteboard types to their modern counterparts, but this is absent in
+        DumpRenderTreePasteboard and WebKitTestRunnerPasteboard.
+
+        Address this by teaching the mock pasteboards to convert legacy types to modern types when generating platform
+        pasteboard items.
+
+        * TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl:
+        * TestRunnerShared/UIScriptContext/UIScriptController.h:
+        (WTR::UIScriptController::copyText):
+
+        Add a new UIScriptController method to write a string to the platform pasteboard. This patch adds support for
+        this new testing hook on macOS and iOS, in WebKit2 (WebKitTestRunner).
+
+        * TestRunnerShared/mac/NSPasteboardAdditions.h: Copied from Tools/WebKitTestRunner/mac/UIScriptControllerMac.h.
+        * TestRunnerShared/mac/NSPasteboardAdditions.mm: Added.
+        (+[NSPasteboard _modernPasteboardType:]):
+
+        Add a helper to convert legacy pasteboard types (and dynamic UTIs that map to legacy pasteboard types) to
+        modern pasteboard types, suitable for writing to NSPasteboardItems on macOS.
+
+        * WebKitTestRunner/WebKitTestRunner.xcodeproj/project.pbxproj:
+        * WebKitTestRunner/ios/UIScriptControllerIOS.h:
+        * WebKitTestRunner/ios/UIScriptControllerIOS.mm:
+        (WTR::UIScriptControllerIOS::copyText):
+        * WebKitTestRunner/mac/UIScriptControllerMac.h:
+        * WebKitTestRunner/mac/UIScriptControllerMac.mm:
+        (WTR::UIScriptControllerMac::copyText):
+        * WebKitTestRunner/mac/WebKitTestRunnerPasteboard.mm:
+
+        Apply the same fix for WebKitTestRunner's mock NSPasteboard.
+
+        (-[LocalPasteboard _clearContentsWithoutUpdatingChangeCount]):
+        (-[LocalPasteboard clearContents]):
+
+        Make -clearContents clear out all the contents on the mock pasteboard, instead of crashing in AppKit.
+
+        (-[LocalPasteboard declareTypes:owner:]):
+        (-[LocalPasteboard pasteboardItems]):
+
 2019-10-17  Mark Lam  <mark.lam@apple.com>
 
         Use constexpr in more places and remove some unnecessary external linkage.
diff --git a/Tools/DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj b/Tools/DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj
index 4d8e239..d90c4ef 100644
--- a/Tools/DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj
+++ b/Tools/DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj
@@ -161,6 +161,7 @@
 		F44A531E21B89A5000DBB99C /* InstanceMethodSwizzler.mm in Sources */ = {isa = PBXBuildFile; fileRef = F44A531C21B89A4500DBB99C /* InstanceMethodSwizzler.mm */; };
 		F4C3578D20E8444E00FA0748 /* LayoutTestSpellChecker.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4C3578820E8442700FA0748 /* LayoutTestSpellChecker.mm */; };
 		F4D423611DD5048200678290 /* TextInputControllerIOS.m in Sources */ = {isa = PBXBuildFile; fileRef = F4D4235F1DD5045300678290 /* TextInputControllerIOS.m */; };
+		F4FED32023582158003C139C /* NSPasteboardAdditions.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4FED31F23582158003C139C /* NSPasteboardAdditions.mm */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -433,6 +434,8 @@
 		F4C3578920E8442700FA0748 /* LayoutTestSpellChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LayoutTestSpellChecker.h; sourceTree = "<group>"; };
 		F4D4235F1DD5045300678290 /* TextInputControllerIOS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TextInputControllerIOS.m; path = ios/TextInputControllerIOS.m; sourceTree = "<group>"; };
 		F4D423601DD5046900678290 /* TextInputController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextInputController.h; sourceTree = "<group>"; };
+		F4FED31E23582158003C139C /* NSPasteboardAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSPasteboardAdditions.h; path = mac/NSPasteboardAdditions.h; sourceTree = "<group>"; };
+		F4FED31F23582158003C139C /* NSPasteboardAdditions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = NSPasteboardAdditions.mm; path = mac/NSPasteboardAdditions.mm; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -706,6 +709,7 @@
 			isa = PBXGroup;
 			children = (
 				F4B6C31820E84382008AC225 /* cocoa */,
+				F4FED31623581EF3003C139C /* mac */,
 				A17A5A2B22A880D80065C5F0 /* spi */,
 				3148A0551E6F90F400D3B316 /* IOSLayoutTestCommunication.cpp */,
 				3148A0561E6F90F400D3B316 /* IOSLayoutTestCommunication.h */,
@@ -875,6 +879,15 @@
 			name = ios;
 			sourceTree = "<group>";
 		};
+		F4FED31623581EF3003C139C /* mac */ = {
+			isa = PBXGroup;
+			children = (
+				F4FED31E23582158003C139C /* NSPasteboardAdditions.h */,
+				F4FED31F23582158003C139C /* NSPasteboardAdditions.mm */,
+			);
+			name = mac;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXHeadersBuildPhase section */
@@ -1185,6 +1198,7 @@
 				E1B7816511AF31B7007E1BC2 /* MockGeolocationProvider.mm in Sources */,
 				31117B3D15D9A56A00163BC8 /* MockWebNotificationProvider.mm in Sources */,
 				BCA18B720C9B08DB00114369 /* NavigationController.m in Sources */,
+				F4FED32023582158003C139C /* NSPasteboardAdditions.mm in Sources */,
 				BCA18B320C9B01B400114369 /* ObjCController.m in Sources */,
 				BCA18B7E0C9B08F100114369 /* ObjCPlugin.m in Sources */,
 				BCA18B800C9B08F100114369 /* ObjCPluginFunction.m in Sources */,
diff --git a/Tools/DumpRenderTree/mac/DumpRenderTreePasteboard.mm b/Tools/DumpRenderTree/mac/DumpRenderTreePasteboard.mm
index 1d4a64b..fe6c426 100644
--- a/Tools/DumpRenderTree/mac/DumpRenderTreePasteboard.mm
+++ b/Tools/DumpRenderTree/mac/DumpRenderTreePasteboard.mm
@@ -34,6 +34,8 @@
 #if PLATFORM(MAC)
 
 #import "DumpRenderTreeMac.h"
+#import "NSPasteboardAdditions.h"
+#import <WebCore/LegacyNSPasteboardTypes.h>
 #import <WebKit/WebTypesInternal.h>
 #import <objc/runtime.h>
 #import <wtf/Assertions.h>
@@ -243,7 +245,7 @@
     for (const auto& typeAndData : _data) {
         NSData *data = (__bridge NSData *)typeAndData.value.get();
         NSString *type = (__bridge NSString *)typeAndData.key.get();
-        [item setData:data forType:type];
+        [item setData:data forType:[NSPasteboard _modernPasteboardType:type]];
     }
     return @[ item.get() ];
 }
diff --git a/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl b/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl
index c0a9238..8893f81 100644
--- a/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl
+++ b/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl
@@ -262,6 +262,8 @@
     void resignFirstResponder();
     readonly attribute boolean isPresentingModally;
 
+    void copyText(DOMString text);
+
     readonly attribute double contentOffsetX;
     readonly attribute double contentOffsetY;
 
diff --git a/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h b/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h
index b060db5..a08ff8e 100644
--- a/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h
+++ b/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h
@@ -105,6 +105,8 @@
     virtual void becomeFirstResponder() { notImplemented(); }
     virtual void resignFirstResponder() { notImplemented(); }
 
+    virtual void copyText(JSStringRef) { notImplemented(); }
+
     virtual void chooseMenuAction(JSStringRef, JSValueRef);
     virtual void dismissMenu();
 
diff --git a/Tools/TestRunnerShared/mac/NSPasteboardAdditions.h b/Tools/TestRunnerShared/mac/NSPasteboardAdditions.h
new file mode 100644
index 0000000..b1a4e0e
--- /dev/null
+++ b/Tools/TestRunnerShared/mac/NSPasteboardAdditions.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#if PLATFORM(MAC)
+
+#import <AppKit/AppKit.h>
+
+@interface NSPasteboard (TestRunnerAdditions)
+
++ (NSPasteboardType)_modernPasteboardType:(NSString *)type;
+
+@end
+
+#endif // PLATFORM(MAC)
diff --git a/Tools/TestRunnerShared/mac/NSPasteboardAdditions.mm b/Tools/TestRunnerShared/mac/NSPasteboardAdditions.mm
new file mode 100644
index 0000000..693aa58
--- /dev/null
+++ b/Tools/TestRunnerShared/mac/NSPasteboardAdditions.mm
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "config.h"
+#import "NSPasteboardAdditions.h"
+
+#if PLATFORM(MAC)
+
+#import <CoreServices/CoreServices.h>
+#import <WebCore/LegacyNSPasteboardTypes.h>
+#import <wtf/RetainPtr.h>
+
+@implementation NSPasteboard (TestRunnerAdditions)
+
++ (NSPasteboardType)_modernPasteboardType:(NSString *)type
+{
+    if (UTTypeIsDynamic((__bridge CFStringRef)type)) {
+        if (auto legacyType = adoptNS((__bridge NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)type, kUTTagClassNSPboardType)))
+            type = legacyType.autorelease();
+    }
+
+    if ([type isEqualToString:WebCore::legacyStringPasteboardType()])
+        return NSPasteboardTypeString;
+
+    if ([type isEqualToString:WebCore::legacyHTMLPasteboardType()])
+        return NSPasteboardTypeHTML;
+
+    if ([type isEqualToString:WebCore::legacyTIFFPasteboardType()])
+        return NSPasteboardTypeTIFF;
+
+    if ([type isEqualToString:WebCore::legacyURLPasteboardType()])
+        return NSPasteboardTypeURL;
+
+    if ([type isEqualToString:WebCore::legacyPDFPasteboardType()])
+        return NSPasteboardTypePDF;
+
+    if ([type isEqualToString:WebCore::legacyRTFDPasteboardType()])
+        return NSPasteboardTypeRTFD;
+
+    if ([type isEqualToString:WebCore::legacyRTFPasteboardType()])
+        return NSPasteboardTypeRTF;
+
+    if ([type isEqualToString:WebCore::legacyColorPasteboardType()])
+        return NSPasteboardTypeColor;
+
+    if ([type isEqualToString:WebCore::legacyFontPasteboardType()])
+        return NSPasteboardTypeFont;
+
+    return type;
+}
+
+@end
+
+#endif // PLATFORM(MAC)
diff --git a/Tools/WebKitTestRunner/WebKitTestRunner.xcodeproj/project.pbxproj b/Tools/WebKitTestRunner/WebKitTestRunner.xcodeproj/project.pbxproj
index 1643d40..aac7cb7 100644
--- a/Tools/WebKitTestRunner/WebKitTestRunner.xcodeproj/project.pbxproj
+++ b/Tools/WebKitTestRunner/WebKitTestRunner.xcodeproj/project.pbxproj
@@ -155,6 +155,7 @@
 		F44A531821B899E500DBB99C /* InstanceMethodSwizzler.mm in Sources */ = {isa = PBXBuildFile; fileRef = F44A531621B899DA00DBB99C /* InstanceMethodSwizzler.mm */; };
 		F46240B1217013E500917B16 /* UIScriptControllerCocoa.mm in Sources */ = {isa = PBXBuildFile; fileRef = F46240AF2170128300917B16 /* UIScriptControllerCocoa.mm */; };
 		F4C3578C20E8444600FA0748 /* LayoutTestSpellChecker.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4C3578A20E8444000FA0748 /* LayoutTestSpellChecker.mm */; };
+		F4FED324235823A3003C139C /* NSPasteboardAdditions.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4FED3222358215E003C139C /* NSPasteboardAdditions.mm */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -412,6 +413,8 @@
 		F46240AF2170128300917B16 /* UIScriptControllerCocoa.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = UIScriptControllerCocoa.mm; sourceTree = "<group>"; };
 		F4C3578A20E8444000FA0748 /* LayoutTestSpellChecker.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = LayoutTestSpellChecker.mm; path = ../TestRunnerShared/cocoa/LayoutTestSpellChecker.mm; sourceTree = "<group>"; };
 		F4C3578B20E8444000FA0748 /* LayoutTestSpellChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LayoutTestSpellChecker.h; path = ../TestRunnerShared/cocoa/LayoutTestSpellChecker.h; sourceTree = "<group>"; };
+		F4FED3212358215E003C139C /* NSPasteboardAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSPasteboardAdditions.h; path = ../TestRunnerShared/mac/NSPasteboardAdditions.h; sourceTree = "<group>"; };
+		F4FED3222358215E003C139C /* NSPasteboardAdditions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = NSPasteboardAdditions.mm; path = ../TestRunnerShared/mac/NSPasteboardAdditions.mm; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -522,6 +525,7 @@
 			children = (
 				0F18E71B1D6BC4E60027E547 /* Bindings */,
 				F4B6C31620E84369008AC225 /* cocoa */,
+				F4FED31D23582120003C139C /* mac */,
 				0F73B5471BA782FE004B3EF4 /* UIScriptContext */,
 				3148A0531E6F85B600D3B316 /* IOSLayoutTestCommunication.cpp */,
 				3148A0541E6F85B600D3B316 /* IOSLayoutTestCommunication.h */,
@@ -890,6 +894,15 @@
 			name = cocoa;
 			sourceTree = "<group>";
 		};
+		F4FED31D23582120003C139C /* mac */ = {
+			isa = PBXGroup;
+			children = (
+				F4FED3212358215E003C139C /* NSPasteboardAdditions.h */,
+				F4FED3222358215E003C139C /* NSPasteboardAdditions.mm */,
+			);
+			name = mac;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXHeadersBuildPhase section */
@@ -1140,6 +1153,7 @@
 			files = (
 				5670B8281386FCA5002EB355 /* EventSenderProxy.mm in Sources */,
 				BC793400118F7C84005EA8E2 /* main.mm in Sources */,
+				F4FED324235823A3003C139C /* NSPasteboardAdditions.mm in Sources */,
 				BC7934E811906846005EA8E2 /* PlatformWebViewMac.mm in Sources */,
 				E1C642C317CBCC7300D66A3C /* PoseAsClass.mm in Sources */,
 				BC8C795C11D2785D004535A1 /* TestControllerMac.mm in Sources */,
diff --git a/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.h b/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.h
index 6ba262a..3182c8d 100644
--- a/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.h
+++ b/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.h
@@ -134,6 +134,7 @@
     JSObjectRef calendarType() const override;
     void setHardwareKeyboardAttached(bool) override;
     void setAllowsViewportShrinkToFit(bool) override;
+    void copyText(JSStringRef) override;
 
     void setDidStartFormControlInteractionCallback(JSValueRef) override;
     void setDidEndFormControlInteractionCallback(JSValueRef) override;
diff --git a/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm b/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm
index 6e61224..716fc696 100644
--- a/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm
+++ b/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm
@@ -1208,6 +1208,11 @@
     });
 }
 
+void UIScriptControllerIOS::copyText(JSStringRef text)
+{
+    UIPasteboard.generalPasteboard.string = text->string();
+}
+
 }
 
 #endif // PLATFORM(IOS_FAMILY)
diff --git a/Tools/WebKitTestRunner/mac/UIScriptControllerMac.h b/Tools/WebKitTestRunner/mac/UIScriptControllerMac.h
index 2550f78..808c05c 100644
--- a/Tools/WebKitTestRunner/mac/UIScriptControllerMac.h
+++ b/Tools/WebKitTestRunner/mac/UIScriptControllerMac.h
@@ -52,6 +52,7 @@
     void toggleCapsLock(JSValueRef) override;
     NSView *platformContentView() const override;
     void clearAllCallbacks() override;
+    void copyText(JSStringRef) override;
 
     void chooseMenuAction(JSStringRef, JSValueRef) override;
 
diff --git a/Tools/WebKitTestRunner/mac/UIScriptControllerMac.mm b/Tools/WebKitTestRunner/mac/UIScriptControllerMac.mm
index 08c6683..c20efd9 100644
--- a/Tools/WebKitTestRunner/mac/UIScriptControllerMac.mm
+++ b/Tools/WebKitTestRunner/mac/UIScriptControllerMac.mm
@@ -225,4 +225,11 @@
     });
 }
 
+void UIScriptControllerMac::copyText(JSStringRef text)
+{
+    NSPasteboard *pasteboard = NSPasteboard.generalPasteboard;
+    [pasteboard declareTypes:[NSArray arrayWithObject:NSPasteboardTypeString] owner:nil];
+    [pasteboard setString:text->string() forType:NSPasteboardTypeString];
+}
+
 } // namespace WTR
diff --git a/Tools/WebKitTestRunner/mac/WebKitTestRunnerPasteboard.mm b/Tools/WebKitTestRunner/mac/WebKitTestRunnerPasteboard.mm
index 5e5cac4..9dbf6dd 100644
--- a/Tools/WebKitTestRunner/mac/WebKitTestRunnerPasteboard.mm
+++ b/Tools/WebKitTestRunner/mac/WebKitTestRunnerPasteboard.mm
@@ -25,11 +25,12 @@
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#include "config.h"
-#include "WebKitTestRunnerPasteboard.h"
+#import "config.h"
+#import "WebKitTestRunnerPasteboard.h"
 
-#include <objc/runtime.h>
-#include <wtf/RetainPtr.h>
+#import "NSPasteboardAdditions.h"
+#import <objc/runtime.h>
+#import <wtf/RetainPtr.h>
 
 @interface LocalPasteboard : NSPasteboard
 {
@@ -123,11 +124,22 @@
 {
 }
 
-- (NSInteger)declareTypes:(NSArray *)newTypes owner:(id)newOwner
+- (void)_clearContentsWithoutUpdatingChangeCount
 {
     [typesArray removeAllObjects];
     [typesSet removeAllObjects];
     [dataByType removeAllObjects];
+}
+
+- (NSInteger)clearContents
+{
+    [self _clearContentsWithoutUpdatingChangeCount];
+    return ++changeCount;
+}
+
+- (NSInteger)declareTypes:(NSArray *)newTypes owner:(id)newOwner
+{
+    [self _clearContentsWithoutUpdatingChangeCount];
     return [self addTypes:newTypes owner:newOwner];
 }
 
@@ -202,7 +214,7 @@
 {
     auto item = adoptNS([[NSPasteboardItem alloc] init]);
     for (NSString *type in dataByType)
-        [item setData:dataByType[type] forType:type];
+        [item setData:dataByType[type] forType:[NSPasteboard _modernPasteboardType:type]];
     return @[ item.get() ];
 }