tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_navigator_clipboard_contextmenu_suppression.js (14834B)


      1 /* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 "use strict";
      8 requestLongerTimeout(2);
      9 
     10 const kContentFileUrl = kBaseUrlForContent + "file_toplevel.html";
     11 const kIsMac = navigator.platform.indexOf("Mac") > -1;
     12 
     13 async function waitForPasteContextMenu() {
     14  await waitForPasteMenuPopupEvent("shown");
     15  let pasteButton = document.getElementById(kPasteMenuItemId);
     16  info("Wait for paste button enabled");
     17  await BrowserTestUtils.waitForMutationCondition(
     18    pasteButton,
     19    { attributeFilter: ["disabled"] },
     20    () => !pasteButton.disabled,
     21    "Wait for paste button enabled"
     22  );
     23 }
     24 
     25 async function readText(aBrowser) {
     26  return SpecialPowers.spawn(aBrowser, [], async () => {
     27    content.document.notifyUserGestureActivation();
     28    return content.eval(`navigator.clipboard.readText();`);
     29  });
     30 }
     31 
     32 function testPasteContextMenuSuppression(aWriteFun, aMsg) {
     33  add_task(async function test_context_menu_suppression_sameorigin() {
     34    await BrowserTestUtils.withNewTab(
     35      kContentFileUrl,
     36      async function (browser) {
     37        info(`Write data by ${aMsg}`);
     38        let clipboardText = await aWriteFun(browser);
     39 
     40        info("Test read from same-origin frame");
     41        let listener = function (e) {
     42          if (e.target.getAttribute("id") == kPasteMenuPopupId) {
     43            ok(false, "paste contextmenu should not be shown");
     44          }
     45        };
     46        document.addEventListener("popupshown", listener);
     47        is(
     48          await readText(browser.browsingContext.children[0]),
     49          clipboardText,
     50          "read should just be resolved without paste contextmenu shown"
     51        );
     52        document.removeEventListener("popupshown", listener);
     53      }
     54    );
     55  });
     56 
     57  add_task(async function test_context_menu_suppression_crossorigin() {
     58    await BrowserTestUtils.withNewTab(
     59      kContentFileUrl,
     60      async function (browser) {
     61        info(`Write data by ${aMsg}`);
     62        let clipboardText = await aWriteFun(browser);
     63 
     64        info("Test read from cross-origin frame");
     65        let pasteButtonIsShown = waitForPasteContextMenu();
     66        let readTextRequest = readText(browser.browsingContext.children[1]);
     67        await pasteButtonIsShown;
     68 
     69        info("Click paste button, request should be resolved");
     70        await promiseClickPasteButton();
     71        is(await readTextRequest, clipboardText, "Request should be resolved");
     72      }
     73    );
     74  });
     75 
     76  add_task(async function test_context_menu_suppression_multiple() {
     77    await BrowserTestUtils.withNewTab(
     78      kContentFileUrl,
     79      async function (browser) {
     80        info(`Write data by ${aMsg}`);
     81        let clipboardText = await aWriteFun(browser);
     82 
     83        info("Test read from cross-origin frame");
     84        let pasteButtonIsShown = waitForPasteContextMenu();
     85        let readTextRequest1 = readText(browser.browsingContext.children[1]);
     86        await pasteButtonIsShown;
     87 
     88        info(
     89          "Test read from same-origin frame before paste contextmenu is closed"
     90        );
     91        is(
     92          await readText(browser.browsingContext.children[0]),
     93          clipboardText,
     94          "read from same-origin should just be resolved without showing paste contextmenu shown"
     95        );
     96 
     97        info("Dismiss paste button, cross-origin request should be rejected");
     98        await promiseDismissPasteButton();
     99        // XXX eden: not sure why first promiseDismissPasteButton doesn't work on Windows opt build.
    100        await promiseDismissPasteButton();
    101        await Assert.rejects(
    102          readTextRequest1,
    103          /NotAllowedError/,
    104          "cross-origin request should be rejected"
    105        );
    106      }
    107    );
    108  });
    109 }
    110 
    111 add_setup(async function () {
    112  await SpecialPowers.pushPrefEnv({
    113    set: [
    114      ["test.events.async.enabled", true],
    115      // Avoid paste button delay enabling making test too long.
    116      ["security.dialog_enable_delay", 0],
    117    ],
    118  });
    119 });
    120 
    121 testPasteContextMenuSuppression(async aBrowser => {
    122  const clipboardText = "X" + Math.random();
    123  await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
    124    content.document.notifyUserGestureActivation();
    125    return content.eval(`navigator.clipboard.writeText("${text}");`);
    126  });
    127  return clipboardText;
    128 }, "clipboard.writeText()");
    129 
    130 testPasteContextMenuSuppression(async aBrowser => {
    131  const clipboardText = "X" + Math.random();
    132  await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
    133    content.document.notifyUserGestureActivation();
    134    return content.eval(`
    135      const itemInput = new ClipboardItem({["text/plain"]: "${text}"});
    136      navigator.clipboard.write([itemInput]);
    137    `);
    138  });
    139  return clipboardText;
    140 }, "clipboard.write()");
    141 
    142 testPasteContextMenuSuppression(async aBrowser => {
    143  const clipboardText = "X" + Math.random();
    144  await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
    145    let div = content.document.createElement("div");
    146    div.innerText = text;
    147    content.document.documentElement.appendChild(div);
    148    // select text
    149    content
    150      .getSelection()
    151      .setBaseAndExtent(div.firstChild, text.length, div.firstChild, 0);
    152  });
    153  // trigger keyboard shortcut to copy.
    154  await EventUtils.synthesizeAndWaitKey(
    155    "c",
    156    kIsMac ? { accelKey: true } : { ctrlKey: true }
    157  );
    158  return clipboardText;
    159 }, "keyboard shortcut");
    160 
    161 testPasteContextMenuSuppression(async aBrowser => {
    162  const clipboardText = "X" + Math.random();
    163  await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
    164    return content.eval(`
    165      document.addEventListener("copy", function(e) {
    166        e.preventDefault();
    167        e.clipboardData.setData("text/plain", "${text}");
    168      }, { once: true });
    169    `);
    170  });
    171  // trigger keyboard shortcut to copy.
    172  await EventUtils.synthesizeAndWaitKey(
    173    "c",
    174    kIsMac ? { accelKey: true } : { ctrlKey: true }
    175  );
    176  return clipboardText;
    177 }, "keyboard shortcut with custom data");
    178 
    179 testPasteContextMenuSuppression(async aBrowser => {
    180  const clipboardText = "X" + Math.random();
    181  await SpecialPowers.spawn(aBrowser, [clipboardText], async text => {
    182    let div = content.document.createElement("div");
    183    div.innerText = text;
    184    content.document.documentElement.appendChild(div);
    185    // select text
    186    content
    187      .getSelection()
    188      .setBaseAndExtent(div.firstChild, text.length, div.firstChild, 0);
    189    return SpecialPowers.doCommand(content, "cmd_copy");
    190  });
    191  return clipboardText;
    192 }, "copy command");
    193 
    194 async function readTypes(aBrowser) {
    195  return SpecialPowers.spawn(aBrowser, [], async () => {
    196    content.document.notifyUserGestureActivation();
    197    let items = await content.eval(`navigator.clipboard.read();`);
    198    return items[0].types;
    199  });
    200 }
    201 
    202 add_task(async function test_context_menu_suppression_image() {
    203  await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) {
    204    await SpecialPowers.spawn(browser, [], async () => {
    205      let image = content.document.createElement("img");
    206      let copyImagePromise = new Promise(resolve => {
    207        image.addEventListener(
    208          "load",
    209          e => {
    210            let documentViewer = content.docShell.docViewer.QueryInterface(
    211              SpecialPowers.Ci.nsIDocumentViewerEdit
    212            );
    213            documentViewer.setCommandNode(image);
    214            documentViewer.copyImage(documentViewer.COPY_IMAGE_ALL);
    215            resolve();
    216          },
    217          { once: true }
    218        );
    219      });
    220      image.src =
    221        "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABHCAIAAADQjmMaAA" +
    222        "AACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3goUAwAgSAORBwAAABl0RVh0Q29tbW" +
    223        "VudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAABPSURBVGje7c4BDQAACAOga//OmuMbJG" +
    224        "AurTbq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6u" +
    225        "rq6s31B0IqAY2/tQVCAAAAAElFTkSuQmCC";
    226      content.document.documentElement.appendChild(image);
    227      await copyImagePromise;
    228    });
    229 
    230    info("Test read from cross-origin frame");
    231    let pasteButtonIsShown = waitForPasteContextMenu();
    232    let readTypesRequest1 = readTypes(browser.browsingContext.children[1]);
    233    await pasteButtonIsShown;
    234 
    235    info("Test read from same-origin frame before paste contextmenu is closed");
    236    // If the cached data is used, it uses type order in cached transferable.
    237    SimpleTest.isDeeply(
    238      await readTypes(browser.browsingContext.children[0]),
    239      ["text/html", "text/plain", "image/png"],
    240      "read from same-origin should just be resolved without showing paste contextmenu shown"
    241    );
    242 
    243    info("Dismiss paste button, cross-origin request should be rejected");
    244    await promiseDismissPasteButton();
    245    // XXX edgar: not sure why first promiseDismissPasteButton doesn't work on Windows opt build.
    246    await promiseDismissPasteButton();
    247    await Assert.rejects(
    248      readTypesRequest1,
    249      /NotAllowedError/,
    250      "cross-origin request should be rejected"
    251    );
    252  });
    253 });
    254 
    255 function testPasteContextMenuSuppressionPasteEvent(
    256  aTriggerPasteFun,
    257  aSuppress,
    258  aMsg
    259 ) {
    260  add_task(async function test_context_menu_suppression_paste_event() {
    261    await BrowserTestUtils.withNewTab(
    262      kContentFileUrl,
    263      async function (browser) {
    264        info(`Write data in cross-origin frame`);
    265        const clipboardText = "X" + Math.random();
    266        await SpecialPowers.spawn(
    267          browser.browsingContext.children[1],
    268          [clipboardText],
    269          async text => {
    270            content.document.notifyUserGestureActivation();
    271            return content.eval(`navigator.clipboard.writeText("${text}");`);
    272          }
    273        );
    274 
    275        info("Test read should show contextmenu");
    276        let pasteButtonIsShown = waitForPasteContextMenu();
    277        let readTextRequest = readText(browser);
    278        await pasteButtonIsShown;
    279 
    280        info("Click paste button, request should be resolved");
    281        await promiseClickPasteButton();
    282        is(await readTextRequest, clipboardText, "Request should be resolved");
    283 
    284        info("Test read in paste event handler");
    285        readTextRequest = SpecialPowers.spawn(browser, [], async () => {
    286          content.document.notifyUserGestureActivation();
    287          return content.eval(`
    288            (() => {
    289              return new Promise(resolve => {
    290                document.addEventListener("paste", function(e) {
    291                  e.preventDefault();
    292                  resolve(navigator.clipboard.readText());
    293                }, { once: true });
    294              });
    295            })();
    296          `);
    297        });
    298        // Input events is dispatched with higher priority, and may therefore
    299        // occur before the `SpecialPowers.spawn` call above is processed on the
    300        // remote side to register the event listener. So add a delay to ensure
    301        // the event listener is registered before the paste event is triggered.
    302        await SpecialPowers.spawn(browser, [], () => {
    303          return new Promise(resolve => {
    304            SpecialPowers.executeSoon(resolve);
    305          });
    306        });
    307 
    308        if (aSuppress) {
    309          let listener = function (e) {
    310            if (e.target.getAttribute("id") == kPasteMenuPopupId) {
    311              ok(!aSuppress, "paste contextmenu should not be shown");
    312            }
    313          };
    314          document.addEventListener("popupshown", listener);
    315          info(`Trigger paste event by ${aMsg}`);
    316          // trigger paste event
    317          await aTriggerPasteFun(browser);
    318          is(
    319            await readTextRequest,
    320            clipboardText,
    321            "Request should be resolved"
    322          );
    323          document.removeEventListener("popupshown", listener);
    324        } else {
    325          let pasteButtonIsShown = waitForPasteContextMenu();
    326          info(
    327            `Trigger paste event by ${aMsg}, read should still show contextmenu`
    328          );
    329          // trigger paste event
    330          await aTriggerPasteFun(browser);
    331          await pasteButtonIsShown;
    332 
    333          info("Click paste button, request should be resolved");
    334          await promiseClickPasteButton();
    335          is(
    336            await readTextRequest,
    337            clipboardText,
    338            "Request should be resolved"
    339          );
    340        }
    341 
    342        info("Test read should still show contextmenu");
    343        pasteButtonIsShown = waitForPasteContextMenu();
    344        readTextRequest = readText(browser);
    345        await pasteButtonIsShown;
    346 
    347        info("Click paste button, request should be resolved");
    348        await promiseClickPasteButton();
    349        is(await readTextRequest, clipboardText, "Request should be resolved");
    350      }
    351    );
    352  });
    353 }
    354 
    355 // If platform supports selection clipboard, the middle click paste the content
    356 // from selection clipboard instead, in such case, we don't suppress the
    357 // contextmenu when access global clipboard via async clipboard API.
    358 if (
    359  !Services.clipboard.isClipboardTypeSupported(
    360    Services.clipboard.kSelectionClipboard
    361  )
    362 ) {
    363  testPasteContextMenuSuppressionPasteEvent(
    364    async browser => {
    365      await SpecialPowers.pushPrefEnv({
    366        set: [["middlemouse.paste", true]],
    367      });
    368 
    369      // We intentionally turn off this a11y check, because the following click
    370      // is send on an arbitrary web content that is not expected to be tested
    371      // by itself with the browser mochitests, therefore this rule check shall
    372      // be ignored by a11y-checks suite.
    373      AccessibilityUtils.setEnv({
    374        mustHaveAccessibleRule: false,
    375      });
    376      await SpecialPowers.spawn(browser, [], async () => {
    377        EventUtils.synthesizeMouse(
    378          content.document.documentElement,
    379          1,
    380          1,
    381          { button: 1 },
    382          content.window
    383        );
    384      });
    385      AccessibilityUtils.resetEnv();
    386    },
    387    true,
    388    "middle click"
    389  );
    390 }
    391 
    392 testPasteContextMenuSuppressionPasteEvent(
    393  async browser => {
    394    await EventUtils.synthesizeAndWaitKey(
    395      "v",
    396      kIsMac ? { accelKey: true } : { ctrlKey: true }
    397    );
    398  },
    399  true,
    400  "keyboard shortcut"
    401 );
    402 
    403 testPasteContextMenuSuppressionPasteEvent(
    404  async browser => {
    405    await SpecialPowers.spawn(browser, [], async () => {
    406      return SpecialPowers.doCommand(content.window, "cmd_paste");
    407    });
    408  },
    409  true,
    410  "paste command"
    411 );
    412 
    413 testPasteContextMenuSuppressionPasteEvent(
    414  async browser => {
    415    await SpecialPowers.spawn(browser, [], async () => {
    416      let div = content.document.createElement("div");
    417      div.setAttribute("contenteditable", "true");
    418      content.document.documentElement.appendChild(div);
    419      div.focus();
    420      return SpecialPowers.doCommand(content.window, "cmd_pasteNoFormatting");
    421    });
    422  },
    423  false,
    424  "pasteNoFormatting command"
    425 );