Bug 1866994 - Handle clipboard contextmenu in parent process; r=geckoview-reviewers,extension-reviewers,webidl,win-reviewers,saschanaz,robwu,nika,Gijs,m_kato,emilio

This patch makes the clipboard context menu trigger from the parent process rather
than the content process. A new method, `confirmUserPaste`, is added on `nsIPromptService`
to trigger frontend UI.

The behavior of handling multiple requests should remain unchanged, new tests are
added to ensure that.

Differential Revision: https://phabricator.services.mozilla.com/D190405
This commit is contained in:
Edgar Chen 2023-11-28 15:38:01 +00:00
parent 96c69d9504
commit fd9072b080
36 changed files with 1028 additions and 552 deletions

View file

@ -58,26 +58,6 @@ bool Clipboard::IsTestingPrefEnabledOrHasReadPermission(
nsGkAtoms::clipboardRead);
}
// @return true iff the event was dispatched successfully.
static bool MaybeCreateAndDispatchMozClipboardReadPasteEvent(
nsPIDOMWindowInner& aOwner) {
RefPtr<Document> document = aOwner.GetDoc();
if (!document) {
// Presumably, this shouldn't happen but to be safe, this case is handled.
MOZ_LOG(Clipboard::GetClipboardLog(), LogLevel::Debug,
("%s: no document.", __FUNCTION__));
return false;
}
// Conceptionally, `ClipboardReadPasteChild` is the target of the event.
// It ensures to receive the event by declaring the event in
// <BrowserGlue.sys.mjs>.
return !NS_WARN_IF(NS_FAILED(nsContentUtils::DispatchChromeEvent(
document, document, u"MozClipboardReadPaste"_ns, CanBubble::eNo,
Cancelable::eNo)));
}
namespace {
/**
@ -234,9 +214,11 @@ NS_IMPL_ISUPPORTS(ClipboardGetCallbackForReadText, nsIAsyncClipboardGetCallback,
} // namespace
void Clipboard::ReadRequest::Answer() {
RefPtr<Promise> p(std::move(mPromise));
RefPtr<nsPIDOMWindowInner> owner(std::move(mOwner));
void Clipboard::RequestRead(Promise* aPromise, ReadRequestType aType,
nsPIDOMWindowInner* aOwner,
nsIPrincipal& aPrincipal) {
RefPtr<Promise> p(aPromise);
nsCOMPtr<nsPIDOMWindowInner> owner(aOwner);
nsresult rv;
nsCOMPtr<nsIClipboard> clipboardService(
@ -247,7 +229,7 @@ void Clipboard::ReadRequest::Answer() {
}
RefPtr<ClipboardGetCallback> callback;
switch (mType) {
switch (aType) {
case ReadRequestType::eRead: {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(owner);
if (NS_WARN_IF(!global)) {
@ -262,14 +244,16 @@ void Clipboard::ReadRequest::Answer() {
AutoTArray<nsCString, 3>{nsDependentCString(kHTMLMime),
nsDependentCString(kTextMime),
nsDependentCString(kPNGImageMime)},
nsIClipboard::kGlobalClipboard, callback);
nsIClipboard::kGlobalClipboard, owner->GetWindowContext(),
&aPrincipal, callback);
break;
}
case ReadRequestType::eReadText: {
callback = MakeRefPtr<ClipboardGetCallbackForReadText>(std::move(p));
rv = clipboardService->AsyncGetData(
AutoTArray<nsCString, 1>{nsDependentCString(kTextMime)},
nsIClipboard::kGlobalClipboard, callback);
nsIClipboard::kGlobalClipboard, owner->GetWindowContext(),
&aPrincipal, callback);
break;
}
default: {
@ -289,151 +273,45 @@ static bool IsReadTextExposedToContent() {
return StaticPrefs::dom_events_asyncClipboard_readText_DoNotUseDirectly();
}
void Clipboard::CheckReadPermissionAndHandleRequest(
Promise& aPromise, nsIPrincipal& aSubjectPrincipal, ReadRequestType aType) {
if (IsTestingPrefEnabledOrHasReadPermission(aSubjectPrincipal)) {
MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
("%s: testing pref enabled or has read permission", __FUNCTION__));
nsPIDOMWindowInner* owner = GetOwner();
if (!owner) {
aPromise.MaybeRejectWithUndefined();
return;
}
ReadRequest{aPromise, aType, *owner}.Answer();
return;
}
if (aSubjectPrincipal.GetIsAddonOrExpandedAddonPrincipal()) {
// TODO: enable showing the "Paste" button in this case; see bug 1773681.
MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
("%s: Addon without read permssion.", __FUNCTION__));
aPromise.MaybeRejectWithUndefined();
return;
}
HandleReadRequestWhichRequiresPasteButton(aPromise, aType);
}
void Clipboard::HandleReadRequestWhichRequiresPasteButton(
Promise& aPromise, ReadRequestType aType) {
nsPIDOMWindowInner* owner = GetOwner();
WindowContext* windowContext = owner ? owner->GetWindowContext() : nullptr;
if (!windowContext) {
MOZ_ASSERT_UNREACHABLE("There should be a WindowContext.");
aPromise.MaybeRejectWithUndefined();
return;
}
// If no transient user activation, reject the promise and return.
if (!windowContext->HasValidTransientUserGestureActivation()) {
aPromise.MaybeRejectWithNotAllowedError(
"Clipboard read request was blocked due to lack of "
"user activation.");
return;
}
// TODO: when a user activation stems from a contextmenu event
// (https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event),
// forbid pasting (bug 1767941).
switch (mTransientUserPasteState.RefreshAndGet(*windowContext)) {
case TransientUserPasteState::Value::Initial: {
MOZ_ASSERT(mReadRequests.IsEmpty());
if (MaybeCreateAndDispatchMozClipboardReadPasteEvent(*owner)) {
mTransientUserPasteState.OnStartWaitingForUserReactionToPasteMenuPopup(
windowContext->GetUserGestureStart());
mReadRequests.AppendElement(
MakeUnique<ReadRequest>(aPromise, aType, *owner));
} else {
// This shouldn't happen but let's handle this case.
aPromise.MaybeRejectWithUndefined();
}
break;
}
case TransientUserPasteState::Value::
WaitingForUserReactionToPasteMenuPopup: {
MOZ_ASSERT(!mReadRequests.IsEmpty());
mReadRequests.AppendElement(
MakeUnique<ReadRequest>(aPromise, aType, *owner));
break;
}
case TransientUserPasteState::Value::TransientlyForbiddenByUser: {
aPromise.MaybeRejectWithNotAllowedError(
"`Clipboard read request was blocked due to the user "
"dismissing the 'Paste' button.");
break;
}
case TransientUserPasteState::Value::TransientlyAllowedByUser: {
ReadRequest{aPromise, aType, *owner}.Answer();
break;
}
}
}
already_AddRefed<Promise> Clipboard::ReadHelper(nsIPrincipal& aSubjectPrincipal,
ReadRequestType aType,
ErrorResult& aRv) {
// Create a new promise
RefPtr<Promise> p = dom::Promise::Create(GetOwnerGlobal(), aRv);
if (aRv.Failed()) {
if (aRv.Failed() || !p) {
return nullptr;
}
CheckReadPermissionAndHandleRequest(*p, aSubjectPrincipal, aType);
return p.forget();
}
nsPIDOMWindowInner* owner = GetOwner();
if (!owner) {
p->MaybeRejectWithUndefined();
return p.forget();
}
auto Clipboard::TransientUserPasteState::RefreshAndGet(
WindowContext& aWindowContext) -> Value {
MOZ_ASSERT(aWindowContext.HasValidTransientUserGestureActivation());
if (IsTestingPrefEnabledOrHasReadPermission(aSubjectPrincipal)) {
MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
("%s: testing pref enabled or has read permission", __FUNCTION__));
} else {
// Testing pref is not enabled and no read permission (for extension), so
// need to check user activation.
WindowContext* windowContext = owner->GetWindowContext();
if (!windowContext) {
MOZ_ASSERT_UNREACHABLE("There should be a WindowContext.");
p->MaybeRejectWithUndefined();
return p.forget();
}
switch (mValue) {
case Value::Initial: {
MOZ_ASSERT(mUserGestureStart.IsNull());
break;
}
case Value::WaitingForUserReactionToPasteMenuPopup: {
MOZ_ASSERT(!mUserGestureStart.IsNull());
MOZ_ASSERT(
mUserGestureStart == aWindowContext.GetUserGestureStart(),
"A new transient user gesture activation should be impossible while "
"there's no response to the 'Paste' button.");
// `OnUserReactedToPasteMenuPopup` will handle the reaction.
break;
}
case Value::TransientlyForbiddenByUser: {
[[fallthrough]];
}
case Value::TransientlyAllowedByUser: {
MOZ_ASSERT(!mUserGestureStart.IsNull());
if (mUserGestureStart != aWindowContext.GetUserGestureStart()) {
*this = {};
}
break;
// If no transient user activation, reject the promise and return.
if (!windowContext->HasValidTransientUserGestureActivation()) {
p->MaybeRejectWithNotAllowedError(
"Clipboard read request was blocked due to lack of "
"user activation.");
return p.forget();
}
}
return mValue;
}
void Clipboard::TransientUserPasteState::
OnStartWaitingForUserReactionToPasteMenuPopup(
const TimeStamp& aUserGestureStart) {
MOZ_ASSERT(mValue == Value::Initial);
MOZ_ASSERT(!aUserGestureStart.IsNull());
mValue = Value::WaitingForUserReactionToPasteMenuPopup;
mUserGestureStart = aUserGestureStart;
}
void Clipboard::TransientUserPasteState::OnUserReactedToPasteMenuPopup(
const bool aAllowed) {
mValue = aAllowed ? Value::TransientlyAllowedByUser
: Value::TransientlyForbiddenByUser;
RequestRead(p, aType, owner, aSubjectPrincipal);
return p.forget();
}
already_AddRefed<Promise> Clipboard::Read(nsIPrincipal& aSubjectPrincipal,
@ -856,31 +734,6 @@ already_AddRefed<Promise> Clipboard::WriteText(const nsAString& aData,
return Write(std::move(sequence), aSubjectPrincipal, aRv);
}
void Clipboard::ReadRequest::MaybeRejectWithNotAllowedError(
const nsACString& aMessage) {
RefPtr<Promise> p(std::move(mPromise));
p->MaybeRejectWithNotAllowedError(aMessage);
}
void Clipboard::OnUserReactedToPasteMenuPopup(const bool aAllowed) {
MOZ_LOG(GetClipboardLog(), LogLevel::Debug, ("%s", __FUNCTION__));
mTransientUserPasteState.OnUserReactedToPasteMenuPopup(aAllowed);
MOZ_ASSERT(!mReadRequests.IsEmpty());
for (UniquePtr<ReadRequest>& request : mReadRequests) {
if (aAllowed) {
request->Answer();
} else {
request->MaybeRejectWithNotAllowedError(
"The user dismissed the 'Paste' button."_ns);
}
}
mReadRequests.Clear();
}
JSObject* Clipboard::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return Clipboard_Binding::Wrap(aCx, this, aGivenProto);

View file

@ -41,9 +41,6 @@ class Clipboard : public DOMEventTargetHelper {
nsIPrincipal& aSubjectPrincipal,
ErrorResult& aRv);
// See documentation of the corresponding .webidl file.
void OnUserReactedToPasteMenuPopup(bool aAllowed);
static LogModule* GetClipboardLog();
// Check if the Clipboard.readText API should be enabled for this context.
@ -71,62 +68,13 @@ class Clipboard : public DOMEventTargetHelper {
static bool IsTestingPrefEnabledOrHasReadPermission(
nsIPrincipal& aSubjectPrincipal);
void CheckReadPermissionAndHandleRequest(Promise& aPromise,
nsIPrincipal& aSubjectPrincipal,
ReadRequestType aType);
void HandleReadRequestWhichRequiresPasteButton(Promise& aPromise,
ReadRequestType aType);
already_AddRefed<Promise> ReadHelper(nsIPrincipal& aSubjectPrincipal,
ReadRequestType aType, ErrorResult& aRv);
~Clipboard();
class ReadRequest final {
public:
ReadRequest(Promise& aPromise, ReadRequestType aType,
nsPIDOMWindowInner& aOwner)
: mType(aType), mPromise(&aPromise), mOwner(&aOwner) {}
// Clears the request too.
void Answer();
void MaybeRejectWithNotAllowedError(const nsACString& aMessage);
private:
ReadRequestType mType;
// Not cycle-collected, because it's nulled when the request is answered or
// destructed.
RefPtr<Promise> mPromise;
RefPtr<nsPIDOMWindowInner> mOwner;
};
AutoTArray<UniquePtr<ReadRequest>, 1> mReadRequests;
class TransientUserPasteState final {
public:
enum class Value {
Initial,
WaitingForUserReactionToPasteMenuPopup,
TransientlyForbiddenByUser,
TransientlyAllowedByUser,
};
// @param aWindowContext requires valid transient user gesture activation.
Value RefreshAndGet(WindowContext& aWindowContext);
void OnStartWaitingForUserReactionToPasteMenuPopup(
const TimeStamp& aUserGestureStart);
void OnUserReactedToPasteMenuPopup(bool aAllowed);
private:
TimeStamp mUserGestureStart;
Value mValue = Value::Initial;
};
TransientUserPasteState mTransientUserPasteState;
void RequestRead(Promise* aPromise, ReadRequestType aType,
nsPIDOMWindowInner* aOwner, nsIPrincipal& aPrincipal);
};
} // namespace mozilla::dom

View file

@ -20,5 +20,11 @@ fail-if = ["a11y_checks"] # Bug 1854502 clicked browser may not be accessible
support-files = ["simple_navigator_clipboard_readText.html"]
fail-if = ["a11y_checks"] # Bug 1854502 clicked browser may not be accessible
["browser_navigator_clipboard_readText_multiple.js"]
support-files = [
"file_toplevel.html",
"file_iframe.html",
]
["browser_navigator_clipboard_touch.js"]
support-files = ["simple_navigator_clipboard_readText.html"]

View file

@ -142,7 +142,7 @@ add_task(async function test_dismissing_paste_button() {
await mutatedReadResultFromContentElement.then(value => {
is(
value,
"Rejected: The user dismissed the 'Paste' button.",
"Rejected: Clipboard read operation is not allowed.",
"`navigator.clipboard.read()` rejected after dismissing the 'Paste' button"
);
});

View file

@ -0,0 +1,302 @@
/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const kBaseUrlForContent = getRootDirectory(gTestPath).replace(
"chrome://mochitests/content",
"https://example.com"
);
const kContentFileName = "file_toplevel.html";
const kContentFileUrl = kBaseUrlForContent + kContentFileName;
async function waitForPasteContextMenu() {
await waitForPasteMenuPopupEvent("shown");
const pasteButton = document.getElementById(kPasteMenuItemId);
info("Wait for paste button enabled");
await BrowserTestUtils.waitForMutationCondition(
pasteButton,
{ attributeFilter: ["disabled"] },
() => !pasteButton.disabled,
"Wait for paste button enabled"
);
}
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
["dom.events.asyncClipboard.readText", true],
["test.events.async.enabled", true],
// Avoid paste button delay enabling making test too long.
["security.dialog_enable_delay", 0],
],
});
});
add_task(async function test_multiple_readText_from_same_frame_allow() {
// Randomized text to avoid overlapping with other tests.
const clipboardText = await promiseWritingRandomTextToClipboard();
await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
const pasteButtonIsShown = waitForPasteContextMenu();
const readTextRequest1 = SpecialPowers.spawn(browser, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
});
await pasteButtonIsShown;
info("readText() from same frame again before interact with paste button");
const readTextRequest2 = SpecialPowers.spawn(browser, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
});
// Give some time for the second request to arrive parent process.
await SpecialPowers.spawn(browser, [], async () => {
return new Promise(resolve => {
content.setTimeout(resolve, 0);
});
});
info("Click paste button, both request should be resolved");
await promiseClickPasteButton();
is(
await readTextRequest1,
clipboardText,
"First request should be resolved"
);
is(
await readTextRequest2,
clipboardText,
"Second request should be resolved"
);
});
});
add_task(async function test_multiple_readText_from_same_frame_deny() {
// Randomized text to avoid overlapping with other tests.
const clipboardText = await promiseWritingRandomTextToClipboard();
await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
const pasteButtonIsShown = waitForPasteContextMenu();
const readTextRequest1 = SpecialPowers.spawn(browser, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
});
await pasteButtonIsShown;
info("readText() from same frame again before interact with paste button");
const readTextRequest2 = SpecialPowers.spawn(browser, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
});
// Give some time for the second request to arrive parent process.
await SpecialPowers.spawn(browser, [], async () => {
return new Promise(resolve => {
content.setTimeout(resolve, 0);
});
});
info("Dismiss paste button, both request should be rejected");
await promiseDismissPasteButton();
await Assert.rejects(
readTextRequest1,
/NotAllowedError/,
"First request should be rejected"
);
await Assert.rejects(
readTextRequest2,
/NotAllowedError/,
"Second request should be rejected"
);
});
});
add_task(async function test_multiple_readText_from_same_origin_frame() {
// Randomized text to avoid overlapping with other tests.
const clipboardText = await promiseWritingRandomTextToClipboard();
await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
const pasteButtonIsShown = waitForPasteContextMenu();
const readTextRequest1 = SpecialPowers.spawn(browser, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
});
await pasteButtonIsShown;
info(
"readText() from same origin child frame again before interacting with paste button"
);
const sameOriginFrame = browser.browsingContext.children[0];
const readTextRequest2 = SpecialPowers.spawn(
sameOriginFrame,
[],
async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
}
);
// Give some time for the second request to arrive parent process.
await SpecialPowers.spawn(sameOriginFrame, [], async () => {
return new Promise(resolve => {
content.setTimeout(resolve, 0);
});
});
info("Click paste button, both request should be resolved");
await promiseClickPasteButton();
is(
await readTextRequest1,
clipboardText,
"First request should be resolved"
);
is(
await readTextRequest2,
clipboardText,
"Second request should be resolved"
);
});
});
add_task(async function test_multiple_readText_from_cross_origin_frame() {
// Randomized text to avoid overlapping with other tests.
const clipboardText = await promiseWritingRandomTextToClipboard();
await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
const pasteButtonIsShown = waitForPasteContextMenu();
const readTextRequest1 = SpecialPowers.spawn(browser, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
});
await pasteButtonIsShown;
info(
"readText() from different origin child frame again before interacting with paste button"
);
const crossOriginFrame = browser.browsingContext.children[1];
const readTextRequest2 = SpecialPowers.spawn(
crossOriginFrame,
[],
async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
}
);
// Give some time for the second request to arrive parent process.
await SpecialPowers.spawn(crossOriginFrame, [], async () => {
return new Promise(resolve => {
content.setTimeout(resolve, 0);
});
});
info("Click paste button, both request should be resolved");
await promiseClickPasteButton();
is(
await readTextRequest1,
clipboardText,
"First request should be resolved"
);
is(
await readTextRequest2,
clipboardText,
"Second request should be resolved"
);
});
});
add_task(async function test_multiple_readText_from_background_frame() {
// Randomized text to avoid overlapping with other tests.
const clipboardText = await promiseWritingRandomTextToClipboard();
const backgroundTab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
kContentFileUrl
);
await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
const pasteButtonIsShown = waitForPasteContextMenu();
const readTextRequest1 = SpecialPowers.spawn(browser, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
});
await pasteButtonIsShown;
info(
"readText() from background tab again before interact with paste button"
);
const readTextRequest2 = SpecialPowers.spawn(
backgroundTab.linkedBrowser,
[],
async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
}
);
// Give some time for the second request to arrive parent process.
await SpecialPowers.spawn(backgroundTab.linkedBrowser, [], async () => {
return new Promise(resolve => {
content.setTimeout(resolve, 0);
});
});
info("Click paste button, both request should be resolved");
await promiseClickPasteButton();
is(
await readTextRequest1,
clipboardText,
"First request should be resolved"
);
is(
await readTextRequest2,
clipboardText,
"Second request should be resolved"
);
});
await BrowserTestUtils.removeTab(backgroundTab);
});
add_task(async function test_multiple_readText_from_background_window() {
// Randomized text to avoid overlapping with other tests.
const clipboardText = await promiseWritingRandomTextToClipboard();
await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
const newWin = await BrowserTestUtils.openNewBrowserWindow();
const backgroundTab = await BrowserTestUtils.openNewForegroundTab(
newWin.gBrowser,
kContentFileUrl
);
await SimpleTest.promiseFocus(browser);
const pasteButtonIsShown = waitForPasteContextMenu();
const readTextRequest1 = SpecialPowers.spawn(browser, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
});
await pasteButtonIsShown;
info(
"readText() from background window again before interact with paste button"
);
await Assert.rejects(
SpecialPowers.spawn(backgroundTab.linkedBrowser, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.readText();`);
}),
/NotAllowedError/,
"Second request should be rejected"
);
info("Click paste button, both request should be resolved");
await promiseClickPasteButton();
is(
await readTextRequest1,
clipboardText,
"First request should be resolved"
);
await BrowserTestUtils.closeWindow(newWin);
});
});

View file

@ -0,0 +1,2 @@
<!DOCTYPE html>
<body>Dummy page</body>

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<iframe src="file_iframe.html"></iframe>
<iframe src="https://example.org/browser/dom/events/test/clipboard/file_iframe.html"></iframe>
</body>
</html>

View file

@ -3609,7 +3609,28 @@ NS_IMPL_ISUPPORTS(ClipboardGetCallback, nsIAsyncClipboardGetCallback)
mozilla::ipc::IPCResult ContentParent::RecvGetClipboardAsync(
nsTArray<nsCString>&& aTypes, const int32_t& aWhichClipboard,
const MaybeDiscarded<WindowContext>& aRequestingWindowContext,
mozilla::NotNull<nsIPrincipal*> aRequestingPrincipal,
GetClipboardAsyncResolver&& aResolver) {
if (!ValidatePrincipal(aRequestingPrincipal,
{ValidatePrincipalOptions::AllowSystem,
ValidatePrincipalOptions::AllowExpanded})) {
LogAndAssertFailedPrincipalValidationInfo(aRequestingPrincipal, __func__);
}
// If the requesting context has been discarded, cancel the paste.
if (aRequestingWindowContext.IsDiscarded()) {
aResolver(NS_ERROR_NOT_AVAILABLE);
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");
}
nsresult rv;
// Retrieve clipboard
nsCOMPtr<nsIClipboard> clipboard(do_GetService(kCClipboardCID, &rv));
@ -3619,7 +3640,8 @@ mozilla::ipc::IPCResult ContentParent::RecvGetClipboardAsync(
}
auto callback = MakeRefPtr<ClipboardGetCallback>(this, std::move(aResolver));
rv = clipboard->AsyncGetData(aTypes, aWhichClipboard, callback);
rv = clipboard->AsyncGetData(aTypes, aWhichClipboard, requestingWindow,
aRequestingPrincipal, callback);
if (NS_FAILED(rv)) {
callback->OnError(rv);
return IPC_OK();

View file

@ -991,6 +991,8 @@ class ContentParent final : public PContentParent,
mozilla::ipc::IPCResult RecvGetClipboardAsync(
nsTArray<nsCString>&& aTypes, const int32_t& aWhichClipboard,
const MaybeDiscarded<WindowContext>& aRequestingWindowContext,
mozilla::NotNull<nsIPrincipal*> aRequestingPrincipal,
GetClipboardAsyncResolver&& aResolver);
already_AddRefed<PClipboardWriteRequestParent>

View file

@ -1224,7 +1224,9 @@ parent:
sync GetExternalClipboardFormats(int32_t aWhichClipboard, bool aPlainTextOnly) returns (nsCString[] aTypes);
// Requests getting data from clipboard.
async GetClipboardAsync(nsCString[] aTypes, int32_t aWhichClipboard)
async GetClipboardAsync(nsCString[] aTypes, int32_t aWhichClipboard,
MaybeDiscardedWindowContext aRequestingWindowContext,
nsIPrincipal aRequestingPrincipal)
returns (PClipboardReadRequestOrError aClipboardReadRequest);
// Clears the clipboard.

View file

@ -28,13 +28,6 @@ interface Clipboard : EventTarget {
Promise<undefined> writeText(DOMString data);
};
partial interface Clipboard {
// @param allowed true, if the user allowed (e.g. clicked) the "Paste" menuitem.
// false, when the menupopup was dismissed.
[ChromeOnly]
undefined onUserReactedToPasteMenuPopup(boolean allowed);
};
typedef (DOMString or Blob) ClipboardItemDataType;
typedef Promise<ClipboardItemDataType> ClipboardItemData;
// callback ClipboardItemDelayedCallback = ClipboardItemData ();

View file

@ -1,95 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { GeckoViewActorChild } from "resource://gre/modules/GeckoViewActorChild.sys.mjs";
export class GeckoViewClipboardPermissionChild extends GeckoViewActorChild {
constructor() {
super();
this._pendingPromise = null;
}
async promptPermissionForClipboardRead() {
const uri = this.contentWindow.location.href;
const { x, y } = await this.sendQuery(
"ClipboardReadTextPaste:GetLastPointerLocation"
);
const promise = this.eventDispatcher.sendRequestForResult({
type: "GeckoView:ClipboardPermissionRequest",
uri,
screenPoint: {
x,
y,
},
});
this._pendingPromise = promise;
try {
const allowOrDeny = await promise;
if (this._pendingPromise !== promise) {
// Current pending promise is newer. So it means that this promise
// is already resolved or rejected. Do nothing.
return;
}
this.contentWindow.navigator.clipboard.onUserReactedToPasteMenuPopup(
allowOrDeny
);
this._pendingPromise = null;
} catch (error) {
debug`Permission error: ${error}`;
if (this._pendingPromise !== promise) {
// Current pending promise is newer. So it means that this promise
// is already resolved or rejected. Do nothing.
return;
}
this.contentWindow.navigator.clipboard.onUserReactedToPasteMenuPopup(
false
);
this._pendingPromise = null;
}
}
handleEvent(aEvent) {
debug`handleEvent: ${aEvent.type}`;
switch (aEvent.type) {
case "MozClipboardReadPaste":
if (aEvent.isTrusted) {
this.promptPermissionForClipboardRead();
}
break;
// page hide or deactivate cancel clipboard permission.
case "pagehide":
// fallthrough for the next three events.
case "deactivate":
case "mousedown":
case "mozvisualscroll":
// Gecko desktop uses XUL popup to show clipboard permission prompt.
// So it will be closed automatically by scroll and other user
// activation. So GeckoView has to close permission prompt by some user
// activations, too.
this.eventDispatcher.sendRequest({
type: "GeckoView:DismissClipboardPermissionRequest",
});
if (this._pendingPromise) {
this.contentWindow.navigator.clipboard.onUserReactedToPasteMenuPopup(
false
);
this._pendingPromise = null;
}
break;
}
}
}
const { debug, warn } = GeckoViewClipboardPermissionChild.initLogging(
"GeckoViewClipboardPermissionChild"
);

View file

@ -1,44 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { GeckoViewActorParent } from "resource://gre/modules/GeckoViewActorParent.sys.mjs";
export class GeckoViewClipboardPermissionParent extends GeckoViewActorParent {
getLastOverWindowPointerLocation() {
const mouseXInCSSPixels = {};
const mouseYInCSSPixels = {};
const windowUtils = this.window.windowUtils;
windowUtils.getLastOverWindowPointerLocationInCSSPixels(
mouseXInCSSPixels,
mouseYInCSSPixels
);
const screenRect = windowUtils.toScreenRect(
mouseXInCSSPixels.value,
mouseYInCSSPixels.value,
0,
0
);
return {
x: screenRect.x,
y: screenRect.y,
};
}
receiveMessage(aMessage) {
debug`receiveMessage: ${aMessage.name}`;
switch (aMessage.name) {
case "ClipboardReadTextPaste:GetLastPointerLocation":
return this.getLastOverWindowPointerLocation();
default:
return super.receiveMessage(aMessage);
}
}
}
const { debug, warn } = GeckoViewClipboardPermissionParent.initLogging(
"GeckoViewClipboardPermissionParent"
);

View file

@ -10,8 +10,6 @@ FINAL_TARGET_FILES.actors += [
"ContentDelegateParent.sys.mjs",
"GeckoViewAutoFillChild.sys.mjs",
"GeckoViewAutoFillParent.sys.mjs",
"GeckoViewClipboardPermissionChild.sys.mjs",
"GeckoViewClipboardPermissionParent.sys.mjs",
"GeckoViewContentChild.sys.mjs",
"GeckoViewContentParent.sys.mjs",
"GeckoViewExperimentDelegateParent.sys.mjs",

View file

@ -9,6 +9,8 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs",
GeckoViewClipboardPermission:
"resource://gre/modules/GeckoViewClipboardPermission.sys.mjs",
});
const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompt");
@ -420,6 +422,9 @@ export class PromptFactory {
asyncPromptAuth() {
return this.callProxy("asyncPromptAuth", arguments);
}
confirmUserPaste() {
return lazy.GeckoViewClipboardPermission.confirmUserPaste(...arguments);
}
}
PromptFactory.prototype.classID = Components.ID(

View file

@ -82,24 +82,6 @@ const JSWINDOWACTORS = {
allFrames: true,
messageManagerGroups: ["browsers"],
},
GeckoViewClipboardPermission: {
parent: {
esModuleURI:
"resource:///actors/GeckoViewClipboardPermissionParent.sys.mjs",
},
child: {
esModuleURI:
"resource:///actors/GeckoViewClipboardPermissionChild.sys.mjs",
events: {
MozClipboardReadPaste: {},
deactivate: { mozSystemGroup: true },
mousedown: { capture: true, mozSystemGroup: true },
mozvisualscroll: { mozSystemGroup: true },
pagehide: { capture: true, mozSystemGroup: true },
},
},
allFrames: true,
},
GeckoViewPdfjs: {
parent: {
esModuleURI: "resource://pdf.js/GeckoViewPdfjsParent.sys.mjs",

View file

@ -394,7 +394,6 @@ class SelectionActionDelegateTest : BaseSessionTest() {
perm: ClipboardPermission,
):
GeckoResult<AllowOrDeny> {
assertThat("URI should match", perm.uri, startsWith(url))
assertThat(
"Type should match",
perm.type,
@ -440,7 +439,6 @@ class SelectionActionDelegateTest : BaseSessionTest() {
perm: ClipboardPermission,
):
GeckoResult<AllowOrDeny>? {
assertThat("URI should match", perm.uri, startsWith(url))
assertThat(
"Type should match",
perm.type,
@ -477,6 +475,7 @@ class SelectionActionDelegateTest : BaseSessionTest() {
mainSession.waitForPageStop()
val result = GeckoResult<Void>()
val permissionResult = GeckoResult<AllowOrDeny>()
mainSession.delegateDuringNextWait(object : SelectionActionDelegate {
@AssertCalled(count = 1)
override fun onShowClipboardPermissionRequest(
@ -490,7 +489,7 @@ class SelectionActionDelegateTest : BaseSessionTest() {
equalTo(SelectionActionDelegate.PERMISSION_CLIPBOARD_READ),
)
result.complete(null)
return GeckoResult()
return permissionResult
}
})
@ -500,10 +499,12 @@ class SelectionActionDelegateTest : BaseSessionTest() {
mainSession.delegateDuringNextWait(object : SelectionActionDelegate {
@AssertCalled
override fun onDismissClipboardPermissionRequest(session: GeckoSession) {
permissionResult.complete(AllowOrDeny.DENY)
}
})
mainSession.loadTestPath(HELLO_HTML_PATH)
sessionRule.waitForResult(permissionResult)
sessionRule.waitForPageStop()
}

View file

@ -567,6 +567,9 @@ public class GeckoSession {
} else if ("GeckoView:FocusRequest".equals(event)) {
delegate.onFocusRequest(GeckoSession.this);
} else if ("GeckoView:DOMWindowClose".equals(event)) {
if (getSelectionActionDelegate() != null) {
getSelectionActionDelegate().onDismissClipboardPermissionRequest(GeckoSession.this);
}
delegate.onCloseRequest(GeckoSession.this);
} else if ("GeckoView:FullScreenEnter".equals(event)) {
delegate.onFullScreen(GeckoSession.this, true);
@ -982,6 +985,9 @@ public class GeckoSession {
final EventCallback callback) {
Log.d(LOGTAG, "handleMessage " + event + " uri=" + message.getString("uri"));
if ("GeckoView:PageStart".equals(event)) {
if (getSelectionActionDelegate() != null) {
getSelectionActionDelegate().onDismissClipboardPermissionRequest(GeckoSession.this);
}
delegate.onPageStart(GeckoSession.this, message.getString("uri"));
} else if ("GeckoView:PageStop".equals(event)) {
delegate.onPageStop(GeckoSession.this, message.getBoolean("success"));
@ -2627,6 +2633,8 @@ public class GeckoSession {
*/
@AnyThread
public void setFocused(final boolean focused) {
mEventDispatcher.dispatch("GeckoView:DismissClipboardPermissionRequest", null);
final GeckoBundle msg = new GeckoBundle(1);
msg.putBoolean("focused", focused);
mEventDispatcher.dispatch("GeckoView:SetFocused", msg);

View file

@ -0,0 +1,83 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
});
import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs";
const { debug } = GeckoViewUtils.initLogging("GeckoViewClipboardPermission");
export var GeckoViewClipboardPermission = {
confirmUserPaste(aWindowContext) {
return new Promise((resolve, reject) => {
if (!aWindowContext) {
reject(
Components.Exception("Null window context.", Cr.NS_ERROR_INVALID_ARG)
);
return;
}
const { document } = aWindowContext.browsingContext.topChromeWindow;
if (!document) {
reject(
Components.Exception(
"Unable to get chrome document.",
Cr.NS_ERROR_FAILURE
)
);
return;
}
if (this._pendingRequest) {
reject(
Components.Exception(
"There is an ongoing request.",
Cr.NS_ERROR_FAILURE
)
);
return;
}
this._pendingRequest = { resolve, reject };
const mouseXInCSSPixels = {};
const mouseYInCSSPixels = {};
const windowUtils = document.ownerGlobal.windowUtils;
windowUtils.getLastOverWindowPointerLocationInCSSPixels(
mouseXInCSSPixels,
mouseYInCSSPixels
);
const screenRect = windowUtils.toScreenRect(
mouseXInCSSPixels.value,
mouseYInCSSPixels.value,
0,
0
);
debug`confirmUserPaste (${screenRect.x}, ${screenRect.y})`;
document.ownerGlobal.WindowEventDispatcher.sendRequestForResult({
type: "GeckoView:ClipboardPermissionRequest",
screenPoint: {
x: screenRect.x,
y: screenRect.y,
},
}).then(
allowOrDeny => {
const propBag = lazy.PromptUtils.objectToPropBag({ ok: allowOrDeny });
this._pendingRequest.resolve(propBag);
this._pendingRequest = null;
},
error => {
debug`Permission error: ${error}`;
this._pendingRequest.reject();
this._pendingRequest = null;
}
);
});
},
};

View file

@ -15,6 +15,7 @@ EXTRA_JS_MODULES += [
"GeckoViewAutocomplete.sys.mjs",
"GeckoViewAutofill.sys.mjs",
"GeckoViewChildModule.sys.mjs",
"GeckoViewClipboardPermission.sys.mjs",
"GeckoViewConsole.sys.mjs",
"GeckoViewContent.sys.mjs",
"GeckoViewContentBlocking.sys.mjs",

View file

@ -1,39 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Propagates "MozClipboardReadPaste" events from a content process to the
* chrome process.
* Receives messages from the chrome process.
*/
export class ClipboardReadPasteChild extends JSWindowActorChild {
constructor() {
super();
}
// EventListener interface.
handleEvent(aEvent) {
if (aEvent.type == "MozClipboardReadPaste" && aEvent.isTrusted) {
this.sendAsyncMessage("ClipboardReadPaste:ShowMenupopup", {});
}
}
// For JSWindowActorChild.
receiveMessage(value) {
switch (value.name) {
case "ClipboardReadPaste:PasteMenuItemClicked": {
this.contentWindow.navigator.clipboard.onUserReactedToPasteMenuPopup(
true
);
break;
}
case "ClipboardReadPaste:PasteMenuItemDismissed": {
this.contentWindow.navigator.clipboard.onUserReactedToPasteMenuPopup(
false
);
break;
}
}
}
}

View file

@ -46,8 +46,6 @@ FINAL_TARGET_FILES.actors += [
"BackgroundThumbnailsChild.sys.mjs",
"BrowserElementChild.sys.mjs",
"BrowserElementParent.sys.mjs",
"ClipboardReadPasteChild.sys.mjs",
"ClipboardReadPasteParent.sys.mjs",
"ContentMetaChild.sys.mjs",
"ContentMetaParent.sys.mjs",
"ControllersChild.sys.mjs",

View file

@ -70,7 +70,7 @@ add_task(async function test_background_async_clipboard_no_permissions() {
});
browser.test.assertRejects(
clipboardRead(),
(err) => err === undefined,
"Clipboard read request was blocked due to lack of user activation.",
"Read should be denied without permission"
);
browser.test.assertRejects(
@ -85,7 +85,7 @@ add_task(async function test_background_async_clipboard_no_permissions() {
);
browser.test.assertRejects(
clipboardReadText(),
(err) => err === undefined,
"Clipboard read request was blocked due to lack of user activation.",
"ReadText should be denied without permission"
);
browser.test.sendMessage("ready");
@ -107,7 +107,7 @@ add_task(async function test_contentscript_async_clipboard_no_permission() {
});
browser.test.assertRejects(
clipboardRead(),
(err) => err === undefined,
"Clipboard read request was blocked due to lack of user activation.",
"Read should be denied without permission"
);
browser.test.assertRejects(
@ -122,7 +122,7 @@ add_task(async function test_contentscript_async_clipboard_no_permission() {
);
browser.test.assertRejects(
clipboardReadText(),
(err) => err === undefined,
"Clipboard read request was blocked due to lack of user activation.",
"ReadText should be denied without permission"
);
browser.test.sendMessage("ready");

View file

@ -7,6 +7,11 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
// This is redefined below, for strange and unfortunate reasons.
import { PromptUtils } from "resource://gre/modules/PromptUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ClipboardContextMenu: "resource://gre/modules/ClipboardContextMenu.sys.mjs",
});
const {
MODAL_TYPE_TAB,
MODAL_TYPE_CONTENT,
@ -652,6 +657,19 @@ Prompter.prototype = {
let p = this.pickPrompter({ browsingContext, modalType, async: true });
return p.promptAuth(...promptArgs);
},
/**
* Displays a contextmenu to get user confirmation for clipboard read. Only
* one context menu can be opened at a time.
*
* @param {WindowContext} windowContext - The window context that initiates
* the clipboard operation.
* @returns {Promise<nsIPropertyBag<{ ok: Boolean }>>}
* A promise which resolves when the contextmenu is dismissed.
*/
confirmUserPaste() {
return lazy.ClipboardContextMenu.confirmUserPaste(...arguments);
},
};
// Common utils not specific to a particular prompter style.

View file

@ -12,6 +12,7 @@ interface nsICancelable;
interface nsIChannel;
webidl BrowsingContext;
webidl WindowGlobalParent;
/**
* This is the interface to the embeddable prompt service; the service that
@ -612,4 +613,17 @@ interface nsIPromptService : nsISupports
in nsIChannel aChannel,
in uint32_t level,
in nsIAuthInformation authInfo);
/**
* Displays a contextmenu to get user confirmation for clipboard read. Only
* one context menu can be opened at a time.
*
* @param aWindow
* The window context that initiates the clipboard operation.
*
* @return A promise which resolves when the contextmenu is dismissed.
*
* @resolves nsIPropertyBag { ok: boolean }
*/
Promise confirmUserPaste(in WindowGlobalParent aWindow);
};

View file

@ -630,22 +630,6 @@ if (!Services.prefs.getBoolPref("browser.pagedata.enabled", false)) {
}
if (AppConstants.platform != "android") {
// For GeckoView support see bug 1776829.
JSWINDOWACTORS.ClipboardReadPaste = {
parent: {
esModuleURI: "resource://gre/actors/ClipboardReadPasteParent.sys.mjs",
},
child: {
esModuleURI: "resource://gre/actors/ClipboardReadPasteChild.sys.mjs",
events: {
MozClipboardReadPaste: {},
},
},
allFrames: true,
};
// Note that GeckoView has another implementation in mobile/android/actors.
JSWINDOWACTORS.Select = {
parent: {

View file

@ -2,26 +2,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const kMenuPopupId = "clipboardReadPasteMenuPopup";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
});
// Exchanges messages with the child actor and handles events from the
// pasteMenuHandler.
export class ClipboardReadPasteParent extends JSWindowActorParent {
constructor() {
super();
this._menupopup = null;
this._menuitem = null;
this._delayTimer = null;
this._pasteMenuItemClicked = false;
this._lastBeepTime = 0;
}
didDestroy() {
if (this._menupopup) {
this._menupopup.hidePopup(true);
}
}
export var ClipboardContextMenu = {
MENU_POPUP_ID: "clipboardReadPasteMenuPopup",
// EventListener interface.
handleEvent(aEvent) {
@ -39,12 +26,15 @@ export class ClipboardReadPasteParent extends JSWindowActorParent {
break;
}
}
}
},
_pasteMenuItemClicked: false,
onCommand() {
// onPopupHiding is responsible for returning result by calling onComplete
// function.
this._pasteMenuItemClicked = true;
this.sendAsyncMessage("ClipboardReadPaste:PasteMenuItemClicked");
}
},
onPopupHiding() {
// Remove the listeners before potentially sending the async message
@ -53,14 +43,21 @@ export class ClipboardReadPasteParent extends JSWindowActorParent {
this._clearDelayTimer();
this._stopWatchingForSpammyActivation();
if (this._pasteMenuItemClicked) {
// A message was already sent. Reset the state to handle further
// click/dismiss events properly.
this._pasteMenuItemClicked = false;
} else {
this.sendAsyncMessage("ClipboardReadPaste:PasteMenuItemDismissed");
}
}
this._menupopup = null;
this._menuitem = null;
let propBag = lazy.PromptUtils.objectToPropBag({
ok: this._pasteMenuItemClicked,
});
this._pendingRequest.resolve(propBag);
// A result has already been responded to. Reset the state to properly
// handle further click or dismiss events.
this._pasteMenuItemClicked = false;
this._pendingRequest = null;
},
_lastBeepTime: 0,
onKeyDown(aEvent) {
if (!this._menuitem.disabled) {
@ -78,25 +75,50 @@ export class ClipboardReadPasteParent extends JSWindowActorParent {
}
this._refreshDelayTimer();
}
}
},
// For JSWindowActorParent.
receiveMessage(value) {
if (value.name == "ClipboardReadPaste:ShowMenupopup") {
if (!this._menupopup) {
this._menupopup = this._getMenupopup();
this._menuitem = this._menupopup.firstElementChild;
_menupopup: null,
_menuitem: null,
_pendingRequest: null,
confirmUserPaste(aWindowContext) {
return new Promise((resolve, reject) => {
if (!aWindowContext) {
reject(
Components.Exception("Null window context.", Cr.NS_ERROR_INVALID_ARG)
);
return;
}
this._addMenupopupEventListeners();
let { document } = aWindowContext.browsingContext.topChromeWindow;
if (!document) {
reject(
Components.Exception(
"Unable to get chrome document.",
Cr.NS_ERROR_FAILURE
)
);
return;
}
const browser = this.browsingContext.top.embedderElement;
const window = browser.ownerGlobal;
const windowUtils = window.windowUtils;
if (this._pendingRequest) {
reject(
Components.Exception(
"There is an ongoing request.",
Cr.NS_ERROR_FAILURE
)
);
return;
}
this._pendingRequest = { resolve, reject };
this._menupopup = this._getMenupopup(document);
this._menuitem = this._menupopup.firstElementChild;
this._addMenupopupEventListeners();
let mouseXInCSSPixels = {};
let mouseYInCSSPixels = {};
windowUtils.getLastOverWindowPointerLocationInCSSPixels(
document.ownerGlobal.windowUtils.getLastOverWindowPointerLocationInCSSPixels(
mouseXInCSSPixels,
mouseYInCSSPixels
);
@ -121,19 +143,19 @@ export class ClipboardReadPasteParent extends JSWindowActorParent {
true /* isContextMenu */
);
this._refreshDelayTimer();
}
}
this._refreshDelayTimer(document);
});
},
_addMenupopupEventListeners() {
this._menupopup.addEventListener("command", this);
this._menupopup.addEventListener("popuphiding", this);
}
},
_removeMenupopupEventListeners() {
this._menupopup.removeEventListener("command", this);
this._menupopup.removeEventListener("popuphiding", this);
}
},
_createMenupopup(aChromeDoc) {
let menuitem = aChromeDoc.createXULElement("menuitem");
@ -141,36 +163,34 @@ export class ClipboardReadPasteParent extends JSWindowActorParent {
aChromeDoc.l10n.setAttributes(menuitem, "text-action-paste");
let menupopup = aChromeDoc.createXULElement("menupopup");
menupopup.id = kMenuPopupId;
menupopup.id = this.MENU_POPUP_ID;
menupopup.appendChild(menuitem);
return menupopup;
}
},
_getMenupopup() {
let browser = this.browsingContext.top.embedderElement;
let window = browser.ownerGlobal;
let chromeDoc = window.document;
let menupopup = chromeDoc.getElementById(kMenuPopupId);
_getMenupopup(aChromeDoc) {
let menupopup = aChromeDoc.getElementById(this.MENU_POPUP_ID);
if (menupopup == null) {
menupopup = this._createMenupopup(chromeDoc);
menupopup = this._createMenupopup(aChromeDoc);
const parent =
chromeDoc.querySelector("popupset") || chromeDoc.documentElement;
aChromeDoc.querySelector("popupset") || aChromeDoc.documentElement;
parent.appendChild(menupopup);
}
return menupopup;
}
},
_startWatchingForSpammyActivation() {
let doc = this._menuitem.ownerDocument;
Services.els.addSystemEventListener(doc, "keydown", this, true);
}
},
_stopWatchingForSpammyActivation() {
let doc = this._menuitem.ownerDocument;
Services.els.removeSystemEventListener(doc, "keydown", this, true);
}
},
_delayTimer: null,
_clearDelayTimer() {
if (this._delayTimer) {
@ -178,7 +198,7 @@ export class ClipboardReadPasteParent extends JSWindowActorParent {
window.clearTimeout(this._delayTimer);
this._delayTimer = null;
}
}
},
_refreshDelayTimer() {
this._clearDelayTimer();
@ -190,5 +210,5 @@ export class ClipboardReadPasteParent extends JSWindowActorParent {
this._stopWatchingForSpammyActivation();
this._delayTimer = null;
}, delay);
}
}
},
};

View file

@ -245,6 +245,7 @@ EXTRA_PP_JS_MODULES += [
if "Android" != CONFIG["OS_TARGET"]:
EXTRA_JS_MODULES += [
"ClipboardContextMenu.sys.mjs",
"GMPExtractorWorker.js",
"GMPInstallManager.sys.mjs",
"GMPUtils.sys.mjs",

View file

@ -5,18 +5,172 @@
#include "nsBaseClipboard.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PromiseNativeHandler.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/RefPtr.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_widget.h"
#include "nsContentUtils.h"
#include "nsIClipboardOwner.h"
#include "nsIPromptService.h"
#include "nsError.h"
#include "nsXPCOM.h"
using mozilla::GenericPromise;
using mozilla::LogLevel;
using mozilla::UniquePtr;
using mozilla::dom::BrowsingContext;
using mozilla::dom::CanonicalBrowsingContext;
using mozilla::dom::ClipboardCapabilities;
using mozilla::dom::Document;
static const int32_t kGetAvailableFlavorsRetryCount = 5;
namespace {
struct ClipboardGetRequest {
ClipboardGetRequest(const nsTArray<nsCString>& aFlavorList,
nsIAsyncClipboardGetCallback* aCallback)
: mFlavorList(aFlavorList.Clone()), mCallback(aCallback) {}
const nsTArray<nsCString> mFlavorList;
const nsCOMPtr<nsIAsyncClipboardGetCallback> mCallback;
};
class UserConfirmationRequest final
: public mozilla::dom::PromiseNativeHandler {
public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_CLASS(UserConfirmationRequest)
UserConfirmationRequest(int32_t aClipboardType,
Document* aRequestingChromeDocument,
nsBaseClipboard* aClipboard)
: mClipboardType(aClipboardType),
mRequestingChromeDocument(aRequestingChromeDocument),
mClipboard(aClipboard) {
MOZ_ASSERT(
mClipboard->nsIClipboard::IsClipboardTypeSupported(aClipboardType));
}
void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
mozilla::ErrorResult& aRv) override;
void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
mozilla::ErrorResult& aRv) override;
bool IsEqual(int32_t aClipboardType,
Document* aRequestingChromeDocument) const {
return ClipboardType() == aClipboardType &&
RequestingChromeDocument() == aRequestingChromeDocument;
}
int32_t ClipboardType() const { return mClipboardType; }
Document* RequestingChromeDocument() const {
return mRequestingChromeDocument;
}
void AddClipboardGetRequest(const nsTArray<nsCString>& aFlavorList,
nsIAsyncClipboardGetCallback* aCallback) {
MOZ_ASSERT(!aFlavorList.IsEmpty());
MOZ_ASSERT(aCallback);
mPendingClipboardGetRequests.AppendElement(
mozilla::MakeUnique<ClipboardGetRequest>(aFlavorList, aCallback));
}
void RejectPendingClipboardGetRequests(nsresult aError) {
MOZ_ASSERT(NS_FAILED(aError));
auto requests = std::move(mPendingClipboardGetRequests);
for (const auto& request : requests) {
MOZ_ASSERT(request);
MOZ_ASSERT(request->mCallback);
request->mCallback->OnError(aError);
}
}
void ProcessPendingClipboardGetRequests() {
auto requests = std::move(mPendingClipboardGetRequests);
for (const auto& request : requests) {
MOZ_ASSERT(request);
MOZ_ASSERT(!request->mFlavorList.IsEmpty());
MOZ_ASSERT(request->mCallback);
mClipboard->AsyncGetDataInternal(request->mFlavorList, mClipboardType,
request->mCallback);
}
}
nsTArray<UniquePtr<ClipboardGetRequest>>& GetPendingClipboardGetRequests() {
return mPendingClipboardGetRequests;
}
private:
~UserConfirmationRequest() = default;
const int32_t mClipboardType;
RefPtr<Document> mRequestingChromeDocument;
const RefPtr<nsBaseClipboard> mClipboard;
// Track the pending read requests that wait for user confirmation.
nsTArray<UniquePtr<ClipboardGetRequest>> mPendingClipboardGetRequests;
};
NS_IMPL_CYCLE_COLLECTION(UserConfirmationRequest, mRequestingChromeDocument)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UserConfirmationRequest)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(UserConfirmationRequest)
NS_IMPL_CYCLE_COLLECTING_RELEASE(UserConfirmationRequest)
static mozilla::StaticRefPtr<UserConfirmationRequest> sUserConfirmationRequest;
void UserConfirmationRequest::ResolvedCallback(JSContext* aCx,
JS::Handle<JS::Value> aValue,
mozilla::ErrorResult& aRv) {
MOZ_DIAGNOSTIC_ASSERT(sUserConfirmationRequest == this);
sUserConfirmationRequest = nullptr;
JS::Rooted<JSObject*> detailObj(aCx, &aValue.toObject());
nsCOMPtr<nsIPropertyBag2> propBag;
nsresult rv = mozilla::dom::UnwrapArg<nsIPropertyBag2>(
aCx, detailObj, getter_AddRefs(propBag));
if (NS_FAILED(rv)) {
RejectPendingClipboardGetRequests(rv);
return;
}
bool result = false;
rv = propBag->GetPropertyAsBool(u"ok"_ns, &result);
if (NS_FAILED(rv)) {
RejectPendingClipboardGetRequests(rv);
return;
}
if (!result) {
RejectPendingClipboardGetRequests(NS_ERROR_DOM_NOT_ALLOWED_ERR);
return;
}
ProcessPendingClipboardGetRequests();
}
void UserConfirmationRequest::RejectedCallback(JSContext* aCx,
JS::Handle<JS::Value> aValue,
mozilla::ErrorResult& aRv) {
MOZ_DIAGNOSTIC_ASSERT(sUserConfirmationRequest == this);
sUserConfirmationRequest = nullptr;
RejectPendingClipboardGetRequests(NS_ERROR_FAILURE);
}
} // namespace
NS_IMPL_ISUPPORTS(nsBaseClipboard::AsyncSetClipboardData,
nsIAsyncSetClipboardData)
@ -322,10 +476,12 @@ void nsBaseClipboard::MaybeRetryGetAvailableFlavors(
NS_IMETHODIMP nsBaseClipboard::AsyncGetData(
const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
mozilla::dom::WindowContext* aRequestingWindowContext,
nsIPrincipal* aRequestingPrincipal,
nsIAsyncClipboardGetCallback* aCallback) {
MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard);
if (!aCallback || aFlavorList.IsEmpty()) {
if (!aCallback || !aRequestingPrincipal || aFlavorList.IsEmpty()) {
return NS_ERROR_INVALID_ARG;
}
@ -335,11 +491,37 @@ NS_IMETHODIMP nsBaseClipboard::AsyncGetData(
return NS_ERROR_FAILURE;
}
// We want to disable security check for automated tests that have the pref
// set to true, or extension that have clipboard read permission.
if (mozilla::StaticPrefs::
dom_events_testing_asyncClipboard_DoNotUseDirectly() ||
nsContentUtils::PrincipalHasPermission(*aRequestingPrincipal,
nsGkAtoms::clipboardRead)) {
AsyncGetDataInternal(aFlavorList, aWhichClipboard, aCallback);
return NS_OK;
}
// TODO: enable showing the "Paste" button in this case; see bug 1773681.
if (aRequestingPrincipal->GetIsAddonOrExpandedAddonPrincipal()) {
MOZ_CLIPBOARD_LOG("%s: Addon without read permission.", __FUNCTION__);
return aCallback->OnError(NS_ERROR_FAILURE);
}
RequestUserConfirmation(aWhichClipboard, aFlavorList,
aRequestingWindowContext, aCallback);
return NS_OK;
}
void nsBaseClipboard::AsyncGetDataInternal(
const nsTArray<nsCString>& aFlavorList, int32_t aClipboardType,
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(aWhichClipboard)) {
if (auto* clipboardCache = GetClipboardCacheIfValid(aClipboardType)) {
nsITransferable* cachedTransferable = clipboardCache->GetTransferable();
MOZ_ASSERT(cachedTransferable);
@ -363,10 +545,10 @@ NS_IMETHODIMP nsBaseClipboard::AsyncGetData(
// XXX Do we need to check system clipboard for the flavors that cannot
// be found in cache?
auto asyncGetClipboardData = mozilla::MakeRefPtr<AsyncGetClipboardData>(
aWhichClipboard, clipboardCache->GetSequenceNumber(),
aClipboardType, clipboardCache->GetSequenceNumber(),
std::move(results), true, this);
aCallback->OnSuccess(asyncGetClipboardData);
return NS_OK;
return;
}
}
@ -374,9 +556,8 @@ NS_IMETHODIMP nsBaseClipboard::AsyncGetData(
// for things other people put on the system clipboard.
}
MaybeRetryGetAvailableFlavors(aFlavorList, aWhichClipboard, aCallback,
MaybeRetryGetAvailableFlavors(aFlavorList, aClipboardType, aCallback,
kGetAvailableFlavorsRetryCount);
return NS_OK;
}
NS_IMETHODIMP nsBaseClipboard::EmptyClipboard(int32_t aWhichClipboard) {
@ -539,6 +720,65 @@ void nsBaseClipboard::ClearClipboardCache(int32_t aClipboardType) {
cache->Clear();
}
void nsBaseClipboard::RequestUserConfirmation(
int32_t aClipboardType, const nsTArray<nsCString>& aFlavorList,
mozilla::dom::WindowContext* aWindowContext,
nsIAsyncClipboardGetCallback* aCallback) {
MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType));
MOZ_ASSERT(aCallback);
if (!aWindowContext) {
aCallback->OnError(NS_ERROR_FAILURE);
return;
}
CanonicalBrowsingContext* cbc =
CanonicalBrowsingContext::Cast(aWindowContext->GetBrowsingContext());
if (!cbc) {
aCallback->OnError(NS_ERROR_FAILURE);
return;
}
RefPtr<CanonicalBrowsingContext> chromeTop = cbc->TopCrossChromeBoundary();
Document* chromeDoc = chromeTop ? chromeTop->GetDocument() : nullptr;
if (!chromeDoc) {
aCallback->OnError(NS_ERROR_FAILURE);
return;
}
// If there is a pending user confirmation request, check if we could reuse
// it. If not, reject the request.
if (sUserConfirmationRequest) {
if (sUserConfirmationRequest->IsEqual(aClipboardType, chromeDoc)) {
sUserConfirmationRequest->AddClipboardGetRequest(aFlavorList, aCallback);
return;
}
aCallback->OnError(NS_ERROR_DOM_NOT_ALLOWED_ERR);
return;
}
nsresult rv = NS_ERROR_FAILURE;
nsCOMPtr<nsIPromptService> promptService =
do_GetService("@mozilla.org/prompter;1", &rv);
if (NS_FAILED(rv)) {
aCallback->OnError(NS_ERROR_DOM_NOT_ALLOWED_ERR);
return;
}
RefPtr<mozilla::dom::Promise> promise;
if (NS_FAILED(promptService->ConfirmUserPaste(aWindowContext->Canonical(),
getter_AddRefs(promise)))) {
aCallback->OnError(NS_ERROR_DOM_NOT_ALLOWED_ERR);
return;
}
sUserConfirmationRequest =
new UserConfirmationRequest(aClipboardType, chromeDoc, this);
sUserConfirmationRequest->AddClipboardGetRequest(aFlavorList, aCallback);
promise->AppendNativeHandler(sUserConfirmationRequest);
}
NS_IMPL_ISUPPORTS(nsBaseClipboard::AsyncGetClipboardData,
nsIAsyncGetClipboardData)

View file

@ -22,8 +22,13 @@ static mozilla::LazyLogModule sWidgetClipboardLog("WidgetClipboard");
class nsITransferable;
class nsIClipboardOwner;
class nsIPrincipal;
class nsIWidget;
namespace mozilla::dom {
class WindowContext;
} // namespace mozilla::dom
/**
* A base clipboard class for all platform, so that they can share the same
* implementation.
@ -46,6 +51,8 @@ class nsBaseClipboard : public nsIClipboard {
int32_t aWhichClipboard) override final;
NS_IMETHOD AsyncGetData(
const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
mozilla::dom::WindowContext* aRequestingWindowContext,
nsIPrincipal* aRequestingPrincipal,
nsIAsyncClipboardGetCallback* aCallback) override final;
NS_IMETHOD EmptyClipboard(int32_t aWhichClipboard) override final;
NS_IMETHOD HasDataMatchingFlavors(const nsTArray<nsCString>& aFlavorList,
@ -54,6 +61,10 @@ class nsBaseClipboard : public nsIClipboard {
NS_IMETHOD IsClipboardTypeSupported(int32_t aWhichClipboard,
bool* aRetval) override final;
void AsyncGetDataInternal(const nsTArray<nsCString>& aFlavorList,
int32_t aClipboardType,
nsIAsyncClipboardGetCallback* aCallback);
using GetDataCallback = mozilla::MoveOnlyFunction<void(nsresult)>;
using HasMatchingFlavorsCallback = mozilla::MoveOnlyFunction<void(
mozilla::Result<nsTArray<nsCString>, nsresult>)>;
@ -183,6 +194,11 @@ class nsBaseClipboard : public nsIClipboard {
nsresult GetDataFromClipboardCache(nsITransferable* aTransferable,
int32_t aClipboardType);
void RequestUserConfirmation(int32_t aClipboardType,
const nsTArray<nsCString>& aFlavorList,
mozilla::dom::WindowContext* aWindowContext,
nsIAsyncClipboardGetCallback* aCallback);
// 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.

View file

@ -170,8 +170,10 @@ NS_IMETHODIMP AsyncGetClipboardDataProxy::GetData(
NS_IMETHODIMP nsClipboardProxy::AsyncGetData(
const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
mozilla::dom::WindowContext* aRequestingWindowContext,
nsIPrincipal* aRequestingPrincipal,
nsIAsyncClipboardGetCallback* aCallback) {
if (!aCallback || aFlavorList.IsEmpty()) {
if (!aCallback || !aRequestingPrincipal || aFlavorList.IsEmpty()) {
return NS_ERROR_INVALID_ARG;
}
@ -182,7 +184,9 @@ NS_IMETHODIMP nsClipboardProxy::AsyncGetData(
}
ContentChild::GetSingleton()
->SendGetClipboardAsync(aFlavorList, aWhichClipboard)
->SendGetClipboardAsync(aFlavorList, aWhichClipboard,
aRequestingWindowContext,
WrapNotNull(aRequestingPrincipal))
->Then(
GetMainThreadSerialEventTarget(), __func__,
/* resolve */

View file

@ -11,6 +11,8 @@
interface nsIArray;
webidl WindowContext;
[scriptable, builtinclass, uuid(801e2318-c8fa-11ed-afa1-0242ac120002)]
interface nsIAsyncSetClipboardData : nsISupports {
/**
@ -166,6 +168,8 @@ interface nsIClipboard : nsISupports
*/
void asyncGetData(in Array<ACString> aFlavorList,
in long aWhichClipboard,
in WindowContext aRequestingWindowContext,
in nsIPrincipal aRequestingPrincipal,
in nsIAsyncClipboardGetCallback aCallback);
/**

View file

@ -14,6 +14,8 @@ skip-if = [
"os == 'linux'", # Bug 1792749
]
["browser_test_clipboard_contextmenu.js"]
["browser_test_fullscreen_size.js"]
["browser_test_ime_state_in_contenteditable_on_focus_move_in_remote_content.js"]

View file

@ -0,0 +1,127 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const clipboard = SpecialPowers.Services.clipboard;
const clipboardTypes = [
clipboard.kGlobalClipboard,
clipboard.kSelectionClipboard,
clipboard.kFindClipboard,
clipboard.kSelectionCache,
];
const supportedClipboardTypes = clipboardTypes.filter(type =>
clipboard.isClipboardTypeSupported(type)
);
const kPasteMenuPopupId = "clipboardReadPasteMenuPopup";
const kPasteMenuItemId = "clipboardReadPasteMenuItem";
function waitForPasteMenuPopupEvent(aEventSuffix) {
// The element with id `kPasteMenuPopupId` is inserted dynamically, hence
// calling `BrowserTestUtils.waitForEvent` instead of
// `BrowserTestUtils.waitForPopupEvent`.
return BrowserTestUtils.waitForEvent(
document,
"popup" + aEventSuffix,
false /* capture */,
e => {
return e.target.getAttribute("id") == kPasteMenuPopupId;
}
);
}
async function waitForPasteContextMenu() {
await waitForPasteMenuPopupEvent("shown");
const pasteButton = document.getElementById(kPasteMenuItemId);
info("Wait for paste button enabled");
await BrowserTestUtils.waitForMutationCondition(
pasteButton,
{ attributeFilter: ["disabled"] },
() => !pasteButton.disabled,
"Wait for paste button enabled"
);
}
function promiseClickPasteButton() {
info("Wait for clicking paste contextmenu");
const pasteButton = document.getElementById(kPasteMenuItemId);
let promise = BrowserTestUtils.waitForEvent(pasteButton, "click");
EventUtils.synthesizeMouseAtCenter(pasteButton, {});
return promise;
}
async function clipboardAsyncGetData(aBrowser, aClipboardType) {
await SpecialPowers.spawn(aBrowser, [aClipboardType], async type => {
return new Promise((resolve, reject) => {
SpecialPowers.Services.clipboard.asyncGetData(
["text/plain"],
type,
content.browsingContext.currentWindowContext,
content.document.nodePrincipal,
{
QueryInterface: SpecialPowers.ChromeUtils.generateQI([
"nsIAsyncClipboardGetCallback",
]),
// nsIAsyncClipboardGetCallback
onSuccess: aAsyncGetClipboardData => {
resolve();
},
onError: aResult => {
reject(aResult);
},
}
);
});
});
}
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
// Avoid paste button delay enabling making test too long.
["security.dialog_enable_delay", 0],
],
});
});
supportedClipboardTypes.forEach(type => {
add_task(async function test_clipboard_contextmenu() {
await BrowserTestUtils.withNewTab(
"https://example.com",
async function (browser) {
info(`Test clipboard contextmenu for clipboard type ${type}`);
let pasteContextMenuPromise = waitForPasteContextMenu();
const asyncRead = clipboardAsyncGetData(browser, type);
await pasteContextMenuPromise;
// We don't allow requests for different clipboard type to be
// consolidated, so when the context menu is shown for a specific
// clipboard type and has not yet got user response, any new requests
// for a different type should be rejected.
info(
"Test other clipboard types before interact with paste contextmenu"
);
for (let otherType of supportedClipboardTypes) {
if (type == otherType) {
continue;
}
info(`Test other clipboard type ${otherType}`);
await Assert.rejects(
clipboardAsyncGetData(browser, otherType),
ex => ex == Cr.NS_ERROR_DOM_NOT_ALLOWED_ERR,
`Request for clipboard type ${otherType} should be rejected`
);
}
await promiseClickPasteButton();
try {
await asyncRead;
ok(true, `nsIClipboard.asyncGetData() should success`);
} catch (e) {
ok(false, `nsIClipboard.asyncGetData() should not fail with ${e}`);
}
}
);
});
});

View file

@ -159,6 +159,8 @@ function asyncGetClipboardData(aClipboardType) {
clipboard.asyncGetData(
["text/plain", "text/html", "image/png"],
aClipboardType,
null,
SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
{
QueryInterface: SpecialPowers.ChromeUtils.generateQI([
"nsIAsyncClipboardGetCallback",

View file

@ -37,17 +37,23 @@ clipboardTypes.forEach(function (type) {
writeRandomStringToClipboard("text/plain", type);
let request = await new Promise(resolve => {
clipboard.asyncGetData(["text/html"], type, {
QueryInterface: SpecialPowers.ChromeUtils.generateQI([
"nsIAsyncClipboardGetCallback",
]),
// nsIAsyncClipboardGetCallback
onSuccess: SpecialPowers.wrapCallback(function (
aAsyncGetClipboardData
) {
resolve(aAsyncGetClipboardData);
}),
});
clipboard.asyncGetData(
["text/html"],
type,
null,
SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal(),
{
QueryInterface: SpecialPowers.ChromeUtils.generateQI([
"nsIAsyncClipboardGetCallback",
]),
// nsIAsyncClipboardGetCallback
onSuccess: SpecialPowers.wrapCallback(function (
aAsyncGetClipboardData
) {
resolve(aAsyncGetClipboardData);
}),
}
);
});
isDeeply(request.flavorList, [], "Check flavorList");
});