/* -*- 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"; requestLongerTimeout(2); const kBaseUrlForContent = getRootDirectory(gTestPath).replace( "chrome://mochitests/content", "https://example.com" ); const kContentFileName = "file_toplevel.html"; const kContentFileUrl = kBaseUrlForContent + kContentFileName; const kIsMac = navigator.platform.indexOf("Mac") > -1; async function waitForPasteContextMenu() { await waitForPasteMenuPopupEvent("shown"); let pasteButton = document.getElementById(kPasteMenuItemId); info("Wait for paste button enabled"); await BrowserTestUtils.waitForMutationCondition( pasteButton, { attributeFilter: ["disabled"] }, () => !pasteButton.disabled, "Wait for paste button enabled" ); } async function readText(aBrowser) { return SpecialPowers.spawn(aBrowser, [], async () => { content.document.notifyUserGestureActivation(); return content.eval(`navigator.clipboard.readText();`); }); } function testPasteContextMenuSuppression(aWriteFun, aMsg) { add_task(async function test_context_menu_suppression_sameorigin() { await BrowserTestUtils.withNewTab( kContentFileUrl, async function (browser) { info(`Write data by ${aMsg}`); let clipboardText = await aWriteFun(browser); info("Test read from same-origin frame"); let listener = function (e) { if (e.target.getAttribute("id") == kPasteMenuPopupId) { ok(false, "paste contextmenu should not be shown"); } }; document.addEventListener("popupshown", listener); is( await readText(browser.browsingContext.children[0]), clipboardText, "read should just be resolved without paste contextmenu shown" ); document.removeEventListener("popupshown", listener); } ); }); add_task(async function test_context_menu_suppression_crossorigin() { await BrowserTestUtils.withNewTab( kContentFileUrl, async function (browser) { info(`Write data by ${aMsg}`); let clipboardText = await aWriteFun(browser); info("Test read from cross-origin frame"); let pasteButtonIsShown = waitForPasteContextMenu(); let readTextRequest = readText(browser.browsingContext.children[1]); await pasteButtonIsShown; info("Click paste button, request should be resolved"); await promiseClickPasteButton(); is(await readTextRequest, clipboardText, "Request should be resolved"); } ); }); add_task(async function test_context_menu_suppression_multiple() { await BrowserTestUtils.withNewTab( kContentFileUrl, async function (browser) { info(`Write data by ${aMsg}`); let clipboardText = await aWriteFun(browser); info("Test read from cross-origin frame"); let pasteButtonIsShown = waitForPasteContextMenu(); let readTextRequest1 = readText(browser.browsingContext.children[1]); await pasteButtonIsShown; info( "Test read from same-origin frame before paste contextmenu is closed" ); is( await readText(browser.browsingContext.children[0]), clipboardText, "read from same-origin should just be resolved without showing paste contextmenu shown" ); info("Dismiss paste button, cross-origin request should be rejected"); await promiseDismissPasteButton(); await Assert.rejects( readTextRequest1, /NotAllowedError/, "cross-origin request should be rejected" ); } ); }); } add_setup(async function () { await SpecialPowers.pushPrefEnv({ set: [ ["dom.events.asyncClipboard.readText", true], ["dom.events.asyncClipboard.clipboardItem", true], ["test.events.async.enabled", true], // Avoid paste button delay enabling making test too long. ["security.dialog_enable_delay", 0], ], }); }); testPasteContextMenuSuppression(async aBrowser => { const clipboardText = "X" + Math.random(); await SpecialPowers.spawn(aBrowser, [clipboardText], async text => { content.document.notifyUserGestureActivation(); return content.eval(`navigator.clipboard.writeText("${text}");`); }); return clipboardText; }, "clipboard.writeText()"); testPasteContextMenuSuppression(async aBrowser => { const clipboardText = "X" + Math.random(); await SpecialPowers.spawn(aBrowser, [clipboardText], async text => { content.document.notifyUserGestureActivation(); return content.eval(` const itemInput = new ClipboardItem({["text/plain"]: "${text}"}); navigator.clipboard.write([itemInput]); `); }); return clipboardText; }, "clipboard.write()"); testPasteContextMenuSuppression(async aBrowser => { const clipboardText = "X" + Math.random(); await SpecialPowers.spawn(aBrowser, [clipboardText], async text => { let div = content.document.createElement("div"); div.innerText = text; content.document.documentElement.appendChild(div); // select text content .getSelection() .setBaseAndExtent(div.firstChild, text.length, div.firstChild, 0); }); // trigger keyboard shortcut to copy. await EventUtils.synthesizeAndWaitKey( "c", kIsMac ? { accelKey: true } : { ctrlKey: true } ); return clipboardText; }, "keyboard shortcut"); testPasteContextMenuSuppression(async aBrowser => { const clipboardText = "X" + Math.random(); await SpecialPowers.spawn(aBrowser, [clipboardText], async text => { return content.eval(` document.addEventListener("copy", function(e) { e.preventDefault(); e.clipboardData.setData("text/plain", "${text}"); }, { once: true }); `); }); // trigger keyboard shortcut to copy. await EventUtils.synthesizeAndWaitKey( "c", kIsMac ? { accelKey: true } : { ctrlKey: true } ); return clipboardText; }, "keyboard shortcut with custom data"); testPasteContextMenuSuppression(async aBrowser => { const clipboardText = "X" + Math.random(); await SpecialPowers.spawn(aBrowser, [clipboardText], async text => { let div = content.document.createElement("div"); div.innerText = text; content.document.documentElement.appendChild(div); // select text content .getSelection() .setBaseAndExtent(div.firstChild, text.length, div.firstChild, 0); return SpecialPowers.doCommand(content, "cmd_copy"); }); return clipboardText; }, "copy command"); async function readTypes(aBrowser) { return SpecialPowers.spawn(aBrowser, [], async () => { content.document.notifyUserGestureActivation(); let items = await content.eval(`navigator.clipboard.read();`); return items[0].types; }); } add_task(async function test_context_menu_suppression_image() { await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) { await SpecialPowers.spawn(browser, [], async () => { let image = content.document.createElement("img"); let copyImagePromise = new Promise(resolve => { image.addEventListener( "load", e => { let documentViewer = content.docShell.docViewer.QueryInterface( SpecialPowers.Ci.nsIDocumentViewerEdit ); documentViewer.setCommandNode(image); documentViewer.copyImage(documentViewer.COPY_IMAGE_ALL); resolve(); }, { once: true } ); }); image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABHCAIAAADQjmMaAA" + "AACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3goUAwAgSAORBwAAABl0RVh0Q29tbW" + "VudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAABPSURBVGje7c4BDQAACAOga//OmuMbJG" + "AurTbq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6u" + "rq6s31B0IqAY2/tQVCAAAAAElFTkSuQmCC"; content.document.documentElement.appendChild(image); await copyImagePromise; }); info("Test read from cross-origin frame"); let pasteButtonIsShown = waitForPasteContextMenu(); let readTypesRequest1 = readTypes(browser.browsingContext.children[1]); await pasteButtonIsShown; info("Test read from same-origin frame before paste contextmenu is closed"); // If the cached data is used, it uses type order in cached transferable. SimpleTest.isDeeply( await readTypes(browser.browsingContext.children[0]), ["text/html", "text/plain", "image/png"], "read from same-origin should just be resolved without showing paste contextmenu shown" ); info("Dismiss paste button, cross-origin request should be rejected"); await promiseDismissPasteButton(); // XXX edgar: not sure why first promiseDismissPasteButton doesn't work on Windows opt build. await promiseDismissPasteButton(); await Assert.rejects( readTypesRequest1, /NotAllowedError/, "cross-origin request should be rejected" ); }); }); 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" );