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 );