Bug 1858627 - Suppress the paste contextmenu in paste event handler; r=nika

Differential Revision: https://phabricator.services.mozilla.com/D191131
This commit is contained in:
Edgar Chen 2024-03-13 20:44:07 +00:00
parent fdaf5ca11a
commit 6d46af241a
7 changed files with 297 additions and 25 deletions

View File

@ -5,6 +5,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsCopySupport.h"
#include "nsGlobalWindowInner.h"
#include "nsIDocumentEncoder.h"
#include "nsISupports.h"
#include "nsIContent.h"
@ -713,6 +714,33 @@ static Element* GetElementOrNearestFlattenedTreeParentElement(nsINode* aNode) {
return nullptr;
}
/**
* This class is used while processing clipboard paste event.
*/
class MOZ_RAII AutoHandlingPasteEvent final {
public:
explicit AutoHandlingPasteEvent(nsGlobalWindowInner* aWindow,
DataTransfer* aDataTransfer,
const EventMessage& aEventMessage,
const int32_t& aClipboardType) {
MOZ_ASSERT(aDataTransfer);
if (aWindow && aEventMessage == ePaste &&
aClipboardType == nsIClipboard::kGlobalClipboard) {
aWindow->SetCurrentPasteDataTransfer(aDataTransfer);
mInnerWindow = aWindow;
}
}
~AutoHandlingPasteEvent() {
if (mInnerWindow) {
mInnerWindow->SetCurrentPasteDataTransfer(nullptr);
}
}
private:
RefPtr<nsGlobalWindowInner> mInnerWindow;
};
bool nsCopySupport::FireClipboardEvent(EventMessage aEventMessage,
int32_t aClipboardType,
PresShell* aPresShell,
@ -790,9 +818,16 @@ bool nsCopySupport::FireClipboardEvent(EventMessage aEventMessage,
InternalClipboardEvent evt(true, originalEventMessage);
evt.mClipboardData = clipboardData;
RefPtr<nsPresContext> presContext = presShell->GetPresContext();
EventDispatcher::Dispatch(targetElement, presContext, &evt, nullptr,
&status);
{
AutoHandlingPasteEvent autoHandlingPasteEvent(
nsGlobalWindowInner::Cast(doc->GetInnerWindow()), clipboardData,
aEventMessage, aClipboardType);
RefPtr<nsPresContext> presContext = presShell->GetPresContext();
EventDispatcher::Dispatch(targetElement, presContext, &evt, nullptr,
&status);
}
// If the event was cancelled, don't do the clipboard operation
doDefault = (status != nsEventStatus_eConsumeNoDefault);
}

View File

@ -225,6 +225,7 @@
#include "nsIBrowserChild.h"
#include "nsICancelableRunnable.h"
#include "nsIChannel.h"
#include "nsIClipboard.h"
#include "nsIContentSecurityPolicy.h"
#include "nsIControllers.h"
#include "nsICookieJarSettings.h"
@ -1451,6 +1452,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(nsGlobalWindowInner)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInstallTrigger)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIntlUtils)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVisualViewport)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCurrentPasteDataTransfer)
tmp->TraverseObjectsInGlobal(cb);
@ -1561,6 +1563,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsGlobalWindowInner)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mInstallTrigger)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mIntlUtils)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mVisualViewport)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCurrentPasteDataTransfer)
tmp->UnlinkObjectsInGlobal();
@ -7610,6 +7613,19 @@ JS::loader::ModuleLoaderBase* nsGlobalWindowInner::GetModuleLoader(
return loader->GetModuleLoader();
}
void nsGlobalWindowInner::SetCurrentPasteDataTransfer(
DataTransfer* aDataTransfer) {
MOZ_ASSERT_IF(aDataTransfer, aDataTransfer->GetEventMessage() == ePaste);
MOZ_ASSERT_IF(aDataTransfer, aDataTransfer->ClipboardType() ==
nsIClipboard::kGlobalClipboard);
MOZ_ASSERT_IF(aDataTransfer, aDataTransfer->GetAsyncGetClipboardData());
mCurrentPasteDataTransfer = aDataTransfer;
}
DataTransfer* nsGlobalWindowInner::GetCurrentPasteDataTransfer() const {
return mCurrentPasteDataTransfer;
}
TrustedTypePolicyFactory* nsGlobalWindowInner::TrustedTypes() {
if (!mTrustedTypePolicyFactory) {
mTrustedTypePolicyFactory = MakeRefPtr<TrustedTypePolicyFactory>(this);

View File

@ -104,6 +104,7 @@ class ClientSource;
class Console;
class Crypto;
class CustomElementRegistry;
class DataTransfer;
class DocGroup;
class External;
class Function;
@ -1257,6 +1258,9 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget,
mozilla::dom::TrustedTypePolicyFactory* TrustedTypes();
void SetCurrentPasteDataTransfer(mozilla::dom::DataTransfer* aDataTransfer);
mozilla::dom::DataTransfer* GetCurrentPasteDataTransfer() const;
private:
RefPtr<mozilla::dom::ContentMediaController> mContentMediaController;
@ -1465,6 +1469,10 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget,
mGroupMessageManagers{1};
} mChromeFields;
// Cache the DataTransfer created for a paste event, this will be reset after
// the event is dispatched.
RefPtr<mozilla::dom::DataTransfer> mCurrentPasteDataTransfer;
// These fields are used by the inner and outer windows to prevent
// programatically moving the window while the mouse is down.
static bool sMouseDown;

View File

@ -29,6 +29,7 @@
#include "nsArrayUtils.h"
#include "nsComponentManagerUtils.h"
#include "nsContentUtils.h"
#include "nsGlobalWindowInner.h"
#include "nsIClipboard.h"
#include "nsIInputStream.h"
#include "nsIParserUtils.h"
@ -86,6 +87,16 @@ class ClipboardGetCallback : public nsIAsyncClipboardGetCallback {
RefPtr<Promise> mPromise;
};
static nsTArray<nsCString> MandatoryDataTypesAsCStrings() {
// Mandatory data types defined in
// https://w3c.github.io/clipboard-apis/#mandatory-data-types-x. The types
// should be in the same order as kNonPlainTextExternalFormats in
// DataTransfer.
return nsTArray<nsCString>{nsLiteralCString(kHTMLMime),
nsLiteralCString(kTextMime),
nsLiteralCString(kPNGImageMime)};
}
class ClipboardGetCallbackForRead final : public ClipboardGetCallback {
public:
explicit ClipboardGetCallbackForRead(nsIGlobalObject* aGlobal,
@ -109,11 +120,15 @@ class ClipboardGetCallbackForRead final : public ClipboardGetCallback {
}
AutoTArray<RefPtr<ClipboardItem::ItemEntry>, 3> entries;
for (const auto& format : flavorList) {
auto entry = MakeRefPtr<ClipboardItem::ItemEntry>(
mGlobal, NS_ConvertUTF8toUTF16(format));
entry->LoadDataFromSystemClipboard(aAsyncGetClipboardData);
entries.AppendElement(std::move(entry));
// We might reuse the request from DataTransfer created for paste event,
// which could contain more types that are not in the mandatory list.
for (const auto& format : MandatoryDataTypesAsCStrings()) {
if (flavorList.Contains(format)) {
auto entry = MakeRefPtr<ClipboardItem::ItemEntry>(
mGlobal, NS_ConvertUTF8toUTF16(format));
entry->LoadDataFromSystemClipboard(aAsyncGetClipboardData);
entries.AppendElement(std::move(entry));
}
}
RefPtr<Promise> p(std::move(mPromise));
@ -214,6 +229,36 @@ NS_IMPL_ISUPPORTS(ClipboardGetCallbackForReadText, nsIAsyncClipboardGetCallback,
} // namespace
void Clipboard::RequestRead(Promise& aPromise, const ReadRequestType& aType,
nsPIDOMWindowInner& aOwner,
nsIPrincipal& aSubjectPrincipal,
nsIAsyncGetClipboardData& aRequest) {
#ifdef DEBUG
bool isValid = false;
MOZ_ASSERT(NS_SUCCEEDED(aRequest.GetValid(&isValid)) && isValid);
#endif
RefPtr<ClipboardGetCallback> callback;
switch (aType) {
case ReadRequestType::eRead: {
callback =
MakeRefPtr<ClipboardGetCallbackForRead>(aOwner.AsGlobal(), &aPromise);
break;
}
case ReadRequestType::eReadText: {
callback = MakeRefPtr<ClipboardGetCallbackForReadText>(&aPromise);
break;
}
default: {
MOZ_ASSERT_UNREACHABLE("Unknown read type");
return;
}
}
MOZ_ASSERT(callback);
callback->OnSuccess(&aRequest);
}
void Clipboard::RequestRead(Promise* aPromise, ReadRequestType aType,
nsPIDOMWindowInner* aOwner,
nsIPrincipal& aPrincipal) {
@ -239,19 +284,14 @@ void Clipboard::RequestRead(Promise* aPromise, ReadRequestType aType,
callback = MakeRefPtr<ClipboardGetCallbackForRead>(global, std::move(p));
rv = clipboardService->AsyncGetData(
// Mandatory data types defined in
// https://w3c.github.io/clipboard-apis/#mandatory-data-types-x
AutoTArray<nsCString, 3>{nsDependentCString(kHTMLMime),
nsDependentCString(kTextMime),
nsDependentCString(kPNGImageMime)},
nsIClipboard::kGlobalClipboard, owner->GetWindowContext(),
&aPrincipal, callback);
MandatoryDataTypesAsCStrings(), nsIClipboard::kGlobalClipboard,
owner->GetWindowContext(), &aPrincipal, callback);
break;
}
case ReadRequestType::eReadText: {
callback = MakeRefPtr<ClipboardGetCallbackForReadText>(std::move(p));
rv = clipboardService->AsyncGetData(
AutoTArray<nsCString, 1>{nsDependentCString(kTextMime)},
AutoTArray<nsCString, 1>{nsLiteralCString(kTextMime)},
nsIClipboard::kGlobalClipboard, owner->GetWindowContext(),
&aPrincipal, callback);
break;
@ -288,6 +328,24 @@ already_AddRefed<Promise> Clipboard::ReadHelper(nsIPrincipal& aSubjectPrincipal,
return p.forget();
}
// If a "paste" clipboard event is actively being processed, we're
// intentionally skipping permission/user-activation checks and giving the
// webpage access to the clipboard.
if (RefPtr<DataTransfer> dataTransfer =
nsGlobalWindowInner::Cast(owner)->GetCurrentPasteDataTransfer()) {
// If there is valid nsIAsyncGetClipboardData, use it directly.
if (nsCOMPtr<nsIAsyncGetClipboardData> asyncGetClipboardData =
dataTransfer->GetAsyncGetClipboardData()) {
bool isValid = false;
asyncGetClipboardData->GetValid(&isValid);
if (isValid) {
RequestRead(*p, aType, *owner, aSubjectPrincipal,
*asyncGetClipboardData);
return p.forget();
}
}
}
if (IsTestingPrefEnabledOrHasReadPermission(aSubjectPrincipal)) {
MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
("%s: testing pref enabled or has read permission", __FUNCTION__));

View File

@ -13,7 +13,8 @@
#include "mozilla/Logging.h"
#include "mozilla/RefPtr.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/dom/DataTransfer.h"
class nsIAsyncGetClipboardData;
namespace mozilla::dom {
@ -75,6 +76,10 @@ class Clipboard : public DOMEventTargetHelper {
void RequestRead(Promise* aPromise, ReadRequestType aType,
nsPIDOMWindowInner* aOwner, nsIPrincipal& aPrincipal);
void RequestRead(Promise& aPromise, const ReadRequestType& aType,
nsPIDOMWindowInner& aOwner, nsIPrincipal& aSubjectPrincipal,
nsIAsyncGetClipboardData& aRequest);
};
} // namespace mozilla::dom

View File

@ -622,7 +622,8 @@ already_AddRefed<DataTransfer> DataTransfer::MozCloneForEvent(
}
// The order of the types matters. `kFileMime` needs to be one of the first two
// types.
// types. And the order should be the same as the types order defined in
// MandatoryDataTypesAsCStrings() for Clipboard API.
static const nsCString kNonPlainTextExternalFormats[] = {
nsLiteralCString(kCustomTypesMime), nsLiteralCString(kFileMime),
nsLiteralCString(kHTMLMime), nsLiteralCString(kRTFMime),

View File

@ -238,16 +238,10 @@ add_task(async function test_context_menu_suppression_image() {
await pasteButtonIsShown;
info("Test read from same-origin frame before paste contextmenu is closed");
const clipboarCacheEnabled = SpecialPowers.getBoolPref(
"widget.clipboard.use-cached-data.enabled",
false
);
// If the cached data is used, it uses type order in cached transferable.
SimpleTest.isDeeply(
await readTypes(browser.browsingContext.children[0]),
clipboarCacheEnabled
? ["text/plain", "text/html", "image/png"]
: ["text/html", "text/plain", "image/png"],
["text/html", "text/plain", "image/png"],
"read from same-origin should just be resolved without showing paste contextmenu shown"
);
@ -262,3 +256,158 @@ add_task(async function test_context_menu_suppression_image() {
);
});
});
function testPasteContextMenuSuppressionPasteEvent(
aTriggerPasteFun,
aSuppress,
aMsg
) {
add_task(async function test_context_menu_suppression_paste_event() {
await BrowserTestUtils.withNewTab(
kContentFileUrl,
async function (browser) {
info(`Write data by in cross-origin frame`);
const clipboardText = "X" + Math.random();
await SpecialPowers.spawn(
browser.browsingContext.children[1],
[clipboardText],
async text => {
content.document.notifyUserGestureActivation();
return content.eval(`navigator.clipboard.writeText("${text}");`);
}
);
info("Test read should show contextmenu");
let pasteButtonIsShown = waitForPasteContextMenu();
let readTextRequest = readText(browser);
await pasteButtonIsShown;
info("Click paste button, request should be resolved");
await promiseClickPasteButton();
is(await readTextRequest, clipboardText, "Request should be resolved");
info("Test read in paste event handler");
readTextRequest = SpecialPowers.spawn(browser, [], async () => {
content.document.notifyUserGestureActivation();
return content.eval(`
(() => {
return new Promise(resolve => {
document.addEventListener("paste", function(e) {
e.preventDefault();
resolve(navigator.clipboard.readText());
}, { once: true });
});
})();
`);
});
if (aSuppress) {
let listener = function (e) {
if (e.target.getAttribute("id") == kPasteMenuPopupId) {
ok(!aSuppress, "paste contextmenu should not be shown");
}
};
document.addEventListener("popupshown", listener);
info(`Trigger paste event by ${aMsg}`);
// trigger paste event
await aTriggerPasteFun(browser);
is(
await readTextRequest,
clipboardText,
"Request should be resolved"
);
document.removeEventListener("popupshown", listener);
} else {
let pasteButtonIsShown = waitForPasteContextMenu();
info(
`Trigger paste event by ${aMsg}, read should still show contextmenu`
);
// trigger paste event
await aTriggerPasteFun(browser);
await pasteButtonIsShown;
info("Click paste button, request should be resolved");
await promiseClickPasteButton();
is(
await readTextRequest,
clipboardText,
"Request should be resolved"
);
}
info("Test read should still show contextmenu");
pasteButtonIsShown = waitForPasteContextMenu();
readTextRequest = readText(browser);
await pasteButtonIsShown;
info("Click paste button, request should be resolved");
await promiseClickPasteButton();
is(await readTextRequest, clipboardText, "Request should be resolved");
}
);
});
}
// If platform supports selection clipboard, the middle click paste the content
// from selection clipboard instead, in such case, we don't suppress the
// contextmenu when access global clipboard via async clipboard API.
if (
!Services.clipboard.isClipboardTypeSupported(
Services.clipboard.kSelectionClipboard
)
) {
testPasteContextMenuSuppressionPasteEvent(
async browser => {
await SpecialPowers.pushPrefEnv({
set: [["middlemouse.paste", true]],
});
await SpecialPowers.spawn(browser, [], async () => {
EventUtils.synthesizeMouse(
content.document.documentElement,
1,
1,
{ button: 1 },
content.window
);
});
},
true,
"middle click"
);
}
testPasteContextMenuSuppressionPasteEvent(
async browser => {
await EventUtils.synthesizeAndWaitKey(
"v",
kIsMac ? { accelKey: true } : { ctrlKey: true }
);
},
true,
"keyboard shortcut"
);
testPasteContextMenuSuppressionPasteEvent(
async browser => {
await SpecialPowers.spawn(browser, [], async () => {
return SpecialPowers.doCommand(content.window, "cmd_paste");
});
},
true,
"paste command"
);
testPasteContextMenuSuppressionPasteEvent(
async browser => {
await SpecialPowers.spawn(browser, [], async () => {
let div = content.document.createElement("div");
div.setAttribute("contenteditable", "true");
content.document.documentElement.appendChild(div);
div.focus();
return SpecialPowers.doCommand(content.window, "cmd_pasteNoFormatting");
});
},
false,
"pasteNoFormatting command"
);