Bug 1858627 - Add nsIClipboard API to return nsIAsyncSetClipboardData synchrously; r=ipc-reviewers,nika

This is a sync version of `nsIClipboard.asyncGetData`, which can be used for
synchronous clipboard APIs, e.g. DataTransfer, to support the clipboard seqence
number concept, see bug 1879401.

Differential Revision: https://phabricator.services.mozilla.com/D201364
This commit is contained in:
Edgar Chen 2024-03-13 20:44:06 +00:00
Родитель 671c3b21b5
Коммит 239696f93b
14 изменённых файлов: 441 добавлений и 49 удалений

Просмотреть файл

@ -3647,6 +3647,48 @@ mozilla::ipc::IPCResult ContentParent::RecvGetClipboardAsync(
return IPC_OK();
}
mozilla::ipc::IPCResult ContentParent::RecvGetClipboardDataSnapshotSync(
nsTArray<nsCString>&& aTypes, const int32_t& aWhichClipboard,
const MaybeDiscarded<WindowContext>& aRequestingWindowContext,
ClipboardReadRequestOrError* aRequestOrError) {
// If the requesting context has been discarded, cancel the paste.
if (aRequestingWindowContext.IsDiscarded()) {
*aRequestOrError = NS_ERROR_FAILURE;
return IPC_OK();
}
RefPtr<WindowGlobalParent> requestingWindow =
aRequestingWindowContext.get_canonical();
if (requestingWindow && requestingWindow->GetContentParent() != this) {
return IPC_FAIL(
this, "attempt to paste into WindowContext loaded in another process");
}
nsCOMPtr<nsIClipboard> clipboard(do_GetService(kCClipboardCID));
if (!clipboard) {
*aRequestOrError = NS_ERROR_FAILURE;
return IPC_OK();
}
nsCOMPtr<nsIAsyncGetClipboardData> asyncGetClipboardData;
nsresult rv =
clipboard->GetDataSnapshotSync(aTypes, aWhichClipboard, requestingWindow,
getter_AddRefs(asyncGetClipboardData));
if (NS_FAILED(rv)) {
*aRequestOrError = rv;
return IPC_OK();
}
auto result = CreateClipboardReadRequest(*this, *asyncGetClipboardData);
if (result.isErr()) {
*aRequestOrError = result.unwrapErr();
return IPC_OK();
}
*aRequestOrError = result.unwrap();
return IPC_OK();
}
already_AddRefed<PClipboardWriteRequestParent>
ContentParent::AllocPClipboardWriteRequestParent(
const int32_t& aClipboardType) {

Просмотреть файл

@ -974,6 +974,11 @@ class ContentParent final : public PContentParent,
mozilla::NotNull<nsIPrincipal*> aRequestingPrincipal,
GetClipboardAsyncResolver&& aResolver);
mozilla::ipc::IPCResult RecvGetClipboardDataSnapshotSync(
nsTArray<nsCString>&& aTypes, const int32_t& aWhichClipboard,
const MaybeDiscarded<WindowContext>& aRequestingWindowContext,
ClipboardReadRequestOrError* aRequestOrError);
already_AddRefed<PClipboardWriteRequestParent>
AllocPClipboardWriteRequestParent(const int32_t& aClipboardType);

Просмотреть файл

@ -1238,6 +1238,11 @@ parent:
nsIPrincipal aRequestingPrincipal)
returns (ClipboardReadRequestOrError aClipboardReadRequestOrError);
// Requests getting data from clipboard.
sync GetClipboardDataSnapshotSync(nsCString[] aTypes, int32_t aWhichClipboard,
MaybeDiscardedWindowContext aRequestingWindowContext)
returns (ClipboardReadRequestOrError aClipboardReadRequestOrError);
// Clears the clipboard.
async EmptyClipboard(int32_t aWhichClipboard);

Просмотреть файл

@ -95,6 +95,16 @@ description = Only used by gtests
[PTestUrgency::Reply2]
description = Only used by gtests
# Clipboard
[PContent::GetClipboard]
description = Legacy synchronous clipboard API
[PContent::ClipboardHasType]
description = Legacy synchronous clipboard API
[PContent::GetExternalClipboardFormats]
description = Retrieve supported clipboard formats synchronously
[PContent::GetClipboardDataSnapshotSync]
description = Legacy synchronous clipboard API
# The rest
[PHeapSnapshotTempFileHelper::OpenHeapSnapshotTempFile]
description = legacy sync IPC - please add detailed description
@ -132,12 +142,6 @@ description = JS MessageManager implementation
description = legacy sync IPC - please add detailed description
[PContent::PURLClassifier]
description = legacy sync IPC - please add detailed description
[PContent::GetClipboard]
description = Legacy synchronous clipboard API
[PContent::ClipboardHasType]
description = Legacy synchronous clipboard API
[PContent::GetExternalClipboardFormats]
description = Retrieve supported clipboard formats synchronously
[PContent::GetIconForExtension]
description = legacy sync IPC - please add detailed description
[PContent::BeginDriverCrashGuard]

Просмотреть файл

@ -812,56 +812,124 @@ NS_IMETHODIMP nsBaseClipboard::AsyncGetData(
return NS_OK;
}
already_AddRefed<nsIAsyncGetClipboardData>
nsBaseClipboard::MaybeCreateGetRequestFromClipboardCache(
const nsTArray<nsCString>& aFlavorList, int32_t aClipboardType,
mozilla::dom::WindowContext* aRequestingWindowContext) {
MOZ_DIAGNOSTIC_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType));
if (!mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()) {
return nullptr;
}
// If we were the last ones to put something on the native clipboard, then
// just use the cached transferable. Otherwise clear it because it isn't
// relevant any more.
ClipboardCache* clipboardCache = GetClipboardCacheIfValid(aClipboardType);
if (!clipboardCache) {
return nullptr;
}
nsITransferable* cachedTransferable = clipboardCache->GetTransferable();
MOZ_ASSERT(cachedTransferable);
nsTArray<nsCString> transferableFlavors;
if (NS_FAILED(cachedTransferable->FlavorsTransferableCanExport(
transferableFlavors))) {
return nullptr;
}
nsTArray<nsCString> results;
for (const auto& transferableFlavor : transferableFlavors) {
for (const auto& flavor : aFlavorList) {
// XXX We need special check for image as we always put the
// image as "native" on the clipboard.
if (transferableFlavor.Equals(flavor) ||
(transferableFlavor.Equals(kNativeImageMime) &&
nsContentUtils::IsFlavorImage(flavor))) {
MOZ_CLIPBOARD_LOG(" has %s", flavor.get());
results.AppendElement(flavor);
}
}
}
// XXX Do we need to check system clipboard for the flavors that cannot
// be found in cache?
return mozilla::MakeAndAddRef<AsyncGetClipboardData>(
aClipboardType, clipboardCache->GetSequenceNumber(), std::move(results),
true /* aFromCache */, this, aRequestingWindowContext);
}
void nsBaseClipboard::AsyncGetDataInternal(
const nsTArray<nsCString>& aFlavorList, int32_t aClipboardType,
mozilla::dom::WindowContext* aRequestingWindowContext,
nsIAsyncClipboardGetCallback* aCallback) {
MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType));
if (mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()) {
// If we were the last ones to put something on the native clipboard, then
// just use the cached transferable. Otherwise clear it because it isn't
// relevant any more.
if (auto* clipboardCache = GetClipboardCacheIfValid(aClipboardType)) {
nsITransferable* cachedTransferable = clipboardCache->GetTransferable();
MOZ_ASSERT(cachedTransferable);
nsTArray<nsCString> transferableFlavors;
if (NS_SUCCEEDED(cachedTransferable->FlavorsTransferableCanExport(
transferableFlavors))) {
nsTArray<nsCString> results;
for (const auto& transferableFlavor : transferableFlavors) {
for (const auto& flavor : aFlavorList) {
// XXX We need special check for image as we always put the
// image as "native" on the clipboard.
if (transferableFlavor.Equals(flavor) ||
(transferableFlavor.Equals(kNativeImageMime) &&
nsContentUtils::IsFlavorImage(flavor))) {
MOZ_CLIPBOARD_LOG(" has %s", flavor.get());
results.AppendElement(flavor);
}
}
}
// XXX Do we need to check system clipboard for the flavors that cannot
// be found in cache?
auto asyncGetClipboardData = mozilla::MakeRefPtr<AsyncGetClipboardData>(
aClipboardType, clipboardCache->GetSequenceNumber(),
std::move(results), true, this, aRequestingWindowContext);
aCallback->OnSuccess(asyncGetClipboardData);
return;
}
}
// At this point we can't satisfy the request from cache data so let's look
// for things other people put on the system clipboard.
if (nsCOMPtr<nsIAsyncGetClipboardData> asyncGetClipboardData =
MaybeCreateGetRequestFromClipboardCache(aFlavorList, aClipboardType,
aRequestingWindowContext)) {
aCallback->OnSuccess(asyncGetClipboardData);
return;
}
// At this point we can't satisfy the request from cache data so let's
// look for things other people put on the system clipboard.
MaybeRetryGetAvailableFlavors(aFlavorList, aClipboardType, aCallback,
kGetAvailableFlavorsRetryCount,
aRequestingWindowContext);
}
NS_IMETHODIMP nsBaseClipboard::GetDataSnapshotSync(
const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
mozilla::dom::WindowContext* aRequestingWindowContext,
nsIAsyncGetClipboardData** _retval) {
MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard);
*_retval = nullptr;
if (aFlavorList.IsEmpty()) {
return NS_ERROR_INVALID_ARG;
}
if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) {
MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__,
aWhichClipboard);
return NS_ERROR_FAILURE;
}
if (nsCOMPtr<nsIAsyncGetClipboardData> asyncGetClipboardData =
MaybeCreateGetRequestFromClipboardCache(aFlavorList, aWhichClipboard,
aRequestingWindowContext)) {
asyncGetClipboardData.forget(_retval);
return NS_OK;
}
auto sequenceNumberOrError =
GetNativeClipboardSequenceNumber(aWhichClipboard);
if (sequenceNumberOrError.isErr()) {
MOZ_CLIPBOARD_LOG("%s: unable to get sequence number for clipboard %d.",
__FUNCTION__, aWhichClipboard);
return sequenceNumberOrError.unwrapErr();
}
nsTArray<nsCString> results;
for (const auto& flavor : aFlavorList) {
auto resultOrError = HasNativeClipboardDataMatchingFlavors(
AutoTArray<nsCString, 1>{flavor}, aWhichClipboard);
if (resultOrError.isOk() && resultOrError.unwrap()) {
results.AppendElement(flavor);
}
}
*_retval =
mozilla::MakeAndAddRef<AsyncGetClipboardData>(
aWhichClipboard, sequenceNumberOrError.unwrap(), std::move(results),
false /* aFromCache */, this, aRequestingWindowContext)
.take();
return NS_OK;
}
NS_IMETHODIMP nsBaseClipboard::EmptyClipboard(int32_t aWhichClipboard) {
MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard);

Просмотреть файл

@ -55,6 +55,10 @@ class nsBaseClipboard : public nsIClipboard {
mozilla::dom::WindowContext* aRequestingWindowContext,
nsIPrincipal* aRequestingPrincipal,
nsIAsyncClipboardGetCallback* aCallback) override final;
NS_IMETHOD GetDataSnapshotSync(
const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
mozilla::dom::WindowContext* aRequestingWindowContext,
nsIAsyncGetClipboardData** _retval) override final;
NS_IMETHOD EmptyClipboard(int32_t aWhichClipboard) override final;
NS_IMETHOD HasDataMatchingFlavors(const nsTArray<nsCString>& aFlavorList,
int32_t aWhichClipboard,
@ -206,6 +210,11 @@ class nsBaseClipboard : public nsIClipboard {
nsIPrincipal* aRequestingPrincipal,
nsIAsyncClipboardGetCallback* aCallback);
already_AddRefed<nsIAsyncGetClipboardData>
MaybeCreateGetRequestFromClipboardCache(
const nsTArray<nsCString>& aFlavorList, int32_t aClipboardType,
mozilla::dom::WindowContext* aRequestingWindowContext);
// Track the pending request for each clipboard type separately. And only need
// to track the latest request for each clipboard type as the prior pending
// request will be canceled when a new request is made.

Просмотреть файл

@ -239,6 +239,35 @@ NS_IMETHODIMP nsClipboardProxy::AsyncGetData(
return NS_OK;
}
NS_IMETHODIMP nsClipboardProxy::GetDataSnapshotSync(
const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
mozilla::dom::WindowContext* aRequestingWindowContext,
nsIAsyncGetClipboardData** _retval) {
*_retval = nullptr;
if (aFlavorList.IsEmpty()) {
return NS_ERROR_INVALID_ARG;
}
if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) {
MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__,
aWhichClipboard);
return NS_ERROR_FAILURE;
}
ContentChild* contentChild = ContentChild::GetSingleton();
ClipboardReadRequestOrError requestOrError;
contentChild->SendGetClipboardDataSnapshotSync(
aFlavorList, aWhichClipboard, aRequestingWindowContext, &requestOrError);
auto result = CreateAsyncGetClipboardDataProxy(std::move(requestOrError));
if (result.isErr()) {
return result.unwrapErr();
}
result.unwrap().forget(_retval);
return NS_OK;
}
NS_IMETHODIMP
nsClipboardProxy::EmptyClipboard(int32_t aWhichClipboard) {
ContentChild::GetSingleton()->SendEmptyClipboard(aWhichClipboard);

Просмотреть файл

@ -178,6 +178,31 @@ interface nsIClipboard : nsISupports
in nsIPrincipal aRequestingPrincipal,
in nsIAsyncClipboardGetCallback aCallback);
/**
* Requests getting data from the native clipboard. This does not actually
* retreive the data, but returns a nsIAsyncGetClipboardData contains
* current avaiable data formats. If the native clipboard is updated, either
* by us or other application, the existing nsIAsyncGetClipboardData becomes
* invalid.
*
* @param aFlavorList
* Specific data formats ('flavors') that can be retrieved from the
* clipboard.
* @param aWhichClipboard
* Specifies the clipboard to which this operation applies.
* @param aRequestingWindowContext [optional]
* The window context window that is requesting the clipboard, which is
* used for content analysis. Passing null means that the content is
* exempt from content analysis. (for example, scripted clipboard read by
* system code) This parameter should not be null when calling this from a
* content process.
* @return nsIAsyncSetClipboardData if successful.
* @throws if the request can not be made.
*/
nsIAsyncGetClipboardData getDataSnapshotSync(in Array<ACString> aFlavorList,
in long aWhichClipboard,
[optional] in WindowContext aRequestingWindowContext);
/**
* This empties the clipboard and notifies the clipboard owner.
* This empties the "logical" clipboard. It does not clear the native clipboard.

Просмотреть файл

@ -9,9 +9,6 @@ support-files = [
# Privacy relevant
["test_bug1123480.xhtml"]
skip-if = ["win11_2009 && bits == 32"]
["test_bug343416.xhtml"]
skip-if = ["debug"]
@ -66,8 +63,8 @@ run-if = ["os == 'mac'"] # Cocoa widget test
["test_bug760802.xhtml"]
["test_clipboard_chrome.html"]
support-files = "file_test_clipboard.js"
["test_bug1123480.xhtml"]
skip-if = ["win11_2009 && bits == 32"]
["test_clipboard_asyncGetData_chrome.html"]
support-files = "file_test_clipboard_asyncGetData.js"
@ -77,6 +74,12 @@ support-files = "file_test_clipboard_asyncSetData.js"
["test_clipboard_cache_chrome.html"]
["test_clipboard_chrome.html"]
support-files = "file_test_clipboard.js"
["test_clipboard_getDataSnapshotSync_chrome.html"]
support-files = "file_test_clipboard_getDataSnapshotSync.js"
["test_clipboard_owner_chrome.html"]
["test_composition_text_querycontent.xhtml"]

Просмотреть файл

@ -157,6 +157,13 @@ function getClipboardData(aFlavor, aClipboardType) {
}
}
function getClipboardDataSnapshotSync(aClipboardType) {
return clipboard.getDataSnapshotSync(
["text/plain", "text/html", "image/png"],
aClipboardType
);
}
function asyncGetClipboardData(aClipboardType) {
return new Promise((resolve, reject) => {
try {

Просмотреть файл

@ -0,0 +1,153 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from clipboard_helper.js */
"use strict";
clipboardTypes.forEach(function (type) {
if (!clipboard.isClipboardTypeSupported(type)) {
add_task(async function test_clipboard_requestGetData_not_support() {
info(`Test getDataSnapshotSync request throwing on ${type}`);
SimpleTest.doesThrow(
() => clipboard.getDataSnapshotSync(["text/plain"], type),
"Passing unsupported clipboard type should throw"
);
});
return;
}
add_task(async function test_clipboard_getDataSnapshotSync_throw() {
info(`Test getDataSnapshotSync request throwing on ${type}`);
SimpleTest.doesThrow(
() => clipboard.getDataSnapshotSync([], type),
"Passing empty flavor list should throw"
);
});
add_task(
async function test_clipboard_getDataSnapshotSync_no_matched_flavor() {
info(`Test getDataSnapshotSync have no matched flavor on ${type}`);
cleanupAllClipboard();
is(
getClipboardData("text/plain", type),
null,
"ensure clipboard is empty"
);
writeRandomStringToClipboard("text/plain", type);
let request = clipboard.getDataSnapshotSync(["text/html"], type);
isDeeply(request.flavorList, [], "Check flavorList");
}
);
add_task(async function test_empty_data() {
info(`Test getDataSnapshotSync request with empty data on ${type}`);
cleanupAllClipboard();
is(getClipboardData("text/plain", type), null, "ensure clipboard is empty");
let request = getClipboardDataSnapshotSync(type);
isDeeply(request.flavorList, [], "Check flavorList");
await asyncClipboardRequestGetData(request, "text/plain", true).catch(
() => {}
);
});
add_task(async function test_clipboard_getDataSnapshotSync_after_write() {
info(`Test getDataSnapshotSync request after write on ${type}`);
let str = writeRandomStringToClipboard("text/plain", type);
let request = getClipboardDataSnapshotSync(type);
isDeeply(request.flavorList, ["text/plain"], "Check flavorList");
is(
await asyncClipboardRequestGetData(request, "text/plain"),
str,
"Check data"
);
ok(request.valid, "request should still be valid");
// Requesting a flavor that is not in the list should throw error.
await asyncClipboardRequestGetData(request, "text/html", true).catch(
() => {}
);
ok(request.valid, "request should still be valid");
// Writing a new data should invalid existing get request.
str = writeRandomStringToClipboard("text/plain", type);
await asyncClipboardRequestGetData(request, "text/plain").then(
() => {
ok(false, "asyncClipboardRequestGetData should not success");
},
() => {
ok(true, "asyncClipboardRequestGetData should reject");
}
);
ok(!request.valid, "request should no longer be valid");
info(`check clipboard data again`);
request = getClipboardDataSnapshotSync(type);
isDeeply(request.flavorList, ["text/plain"], "Check flavorList");
is(
await asyncClipboardRequestGetData(request, "text/plain"),
str,
"Check data"
);
cleanupAllClipboard();
});
add_task(async function test_clipboard_getDataSnapshotSync_after_empty() {
info(`Test getDataSnapshotSync request after empty on ${type}`);
let str = writeRandomStringToClipboard("text/plain", type);
let request = getClipboardDataSnapshotSync(type);
isDeeply(request.flavorList, ["text/plain"], "Check flavorList");
is(
await asyncClipboardRequestGetData(request, "text/plain"),
str,
"Check data"
);
ok(request.valid, "request should still be valid");
// Empty clipboard data
emptyClipboardData(type);
is(getClipboardData("text/plain", type), null, "ensure clipboard is empty");
await asyncClipboardRequestGetData(request, "text/plain").then(
() => {
ok(false, "asyncClipboardRequestGetData should not success");
},
() => {
ok(true, "asyncClipboardRequestGetData should reject");
}
);
ok(!request.valid, "request should no longer be valid");
info(`check clipboard data again`);
request = getClipboardDataSnapshotSync(type);
isDeeply(request.flavorList, [], "Check flavorList");
cleanupAllClipboard();
});
});
add_task(async function test_clipboard_getDataSnapshotSync_html_data() {
info(`Test getDataSnapshotSync request with html data`);
const html_str = `<img src="https://example.com/oops">`;
writeStringToClipboard(html_str, "text/html", clipboard.kGlobalClipboard);
let request = getClipboardDataSnapshotSync(clipboard.kGlobalClipboard);
isDeeply(request.flavorList, ["text/html"], "Check flavorList");
is(
await asyncClipboardRequestGetData(request, "text/html"),
// On Windows, widget adds extra data into HTML clipboard.
navigator.platform.includes("Win")
? `<html><body>\n<!--StartFragment-->${html_str}<!--EndFragment-->\n</body>\n</html>`
: html_str,
"Check data"
);
// Requesting a flavor that is not in the list should throw error.
await asyncClipboardRequestGetData(request, "text/plain", true).catch(
() => {}
);
});

Просмотреть файл

@ -25,12 +25,16 @@ skip-if = [
support-files = ["file_test_clipboard.js"]
["test_clipboard_asyncGetData.html"]
skip-if = ["display == 'wayland'"] # Bug 1864211
skip-if = ["display == 'wayland'"] # Bug 1879835
support-files = ["file_test_clipboard_asyncGetData.js"]
["test_clipboard_asyncSetData.html"]
support-files = ["file_test_clipboard_asyncSetData.js"]
["test_clipboard_getDataSnapshotSync.html"]
skip-if = ["display == 'wayland'"] # Bug 1879835
support-files = "file_test_clipboard_getDataSnapshotSync.js"
["test_contextmenu_by_mouse_on_unix.html"]
run-if = ["os == 'linux'"]
skip-if = ["headless"] # headless widget doesn't dispatch contextmenu event by mouse event.

Просмотреть файл

@ -0,0 +1,19 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1858627
-->
<head>
<title>Test for Bug 1858627</title>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="clipboard_helper.js"></script>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
<!-- Tests are in file_test_clipboard_getDataSnapshotSync.js -->
<script src="file_test_clipboard_getDataSnapshotSync.js"></script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,19 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1858627
-->
<head>
<title>Test for Bug 1858627</title>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="clipboard_helper.js"></script>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
<!-- Tests are in file_test_clipboard_getDataSnapshotSync.js -->
<script src="file_test_clipboard_getDataSnapshotSync.js"></script>
</body>
</html>