forked from mirrors/gecko-dev
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:
parent
96c69d9504
commit
fd9072b080
36 changed files with 1028 additions and 552 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
2
dom/events/test/clipboard/file_iframe.html
Normal file
2
dom/events/test/clipboard/file_iframe.html
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<!DOCTYPE html>
|
||||
<body>Dummy page</body>
|
||||
10
dom/events/test/clipboard/file_toplevel.html
Normal file
10
dom/events/test/clipboard/file_toplevel.html
Normal 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>
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 ();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
|
|
@ -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"
|
||||
);
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
127
widget/tests/browser/browser_test_clipboard_contextmenu.js
Normal file
127
widget/tests/browser/browser_test_clipboard_contextmenu.js
Normal 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}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue