mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-03-03 07:01:19 +00:00
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:
parent
fdaf5ca11a
commit
6d46af241a
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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__));
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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"
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user