tor-browser

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

commit 16f5e48190f12d048ec1a285ed0c6303a3139644
parent 880a9e5969b3ebcdc339e876dcce4b915aa4537b
Author: Edgar Chen <echen@mozilla.com>
Date:   Fri, 21 Nov 2025 19:31:13 +0000

Bug 1773681 - Show clipboard context menu on extension; r=smaug,robwu

This patch makes extensions without clipboard permission behave the same as web
content. When calling readText() and read(), a clipboard paste button is shown
to get user confirmation if extension doesn't have clipboard permission. If the
extension does have clipboard permssion, it's allowed to read clipboard data
without requiring user confirmation.

Differential Revision: https://phabricator.services.mozilla.com/D159896

Diffstat:
Mdom/events/test/clipboard/browser.toml | 32+++++++++++++++++++++++++++-----
Adom/events/test/clipboard/browser_navigator_clipboard_readText_ext.js | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adom/events/test/clipboard/browser_navigator_clipboard_read_ext.js | 247+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adom/events/test/clipboard/simple_page_ext.html | 2++
Mtoolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html | 28++++++++++++++--------------
Mwidget/nsBaseClipboard.cpp | 6------
6 files changed, 517 insertions(+), 25 deletions(-)

diff --git a/dom/events/test/clipboard/browser.toml b/dom/events/test/clipboard/browser.toml @@ -7,14 +7,18 @@ support-files = [ ["browser_navigator_clipboard_clickjacking.js"] support-files = ["simple_navigator_clipboard_keydown.html"] -run-if = ["os != 'win'"] # The popupmenus dismiss when access keys for disabled items are pressed on windows +run-if = [ + "os != 'win'", # The popupmenus dismiss when access keys for disabled items are pressed on windows +] ["browser_navigator_clipboard_contextmenu_suppression.js"] support-files = [ "file_toplevel.html", "file_iframe.html", ] -skip-if = ["headless"] # bug 1989339 +skip-if = [ + "headless", # bug 1989339 +] ["browser_navigator_clipboard_contextmenu_suppression_ext.js"] support-files = [ @@ -24,18 +28,36 @@ support-files = [ ["browser_navigator_clipboard_read.js"] support-files = ["simple_navigator_clipboard_read.html"] -skip-if = ["headless"] # bug 1989339 +skip-if = [ + "headless", # bug 1989339 +] ["browser_navigator_clipboard_readText.js"] support-files = ["simple_navigator_clipboard_readText.html"] -skip-if = ["headless"] # bug 1989339 +skip-if = [ + "headless", # bug 1989339 +] + +["browser_navigator_clipboard_readText_ext.js"] +support-files = ["simple_page_ext.html"] +skip-if = [ + "headless", # bug 1989339 +] ["browser_navigator_clipboard_readText_multiple.js"] support-files = [ "file_toplevel.html", "file_iframe.html", ] -skip-if = ["headless"] # bug 1989339 +skip-if = [ + "headless", # bug 1989339 +] + +["browser_navigator_clipboard_read_ext.js"] +support-files = ["simple_page_ext.html"] +skip-if = [ + "headless", # bug 1989339 +] ["browser_navigator_clipboard_touch.js"] support-files = ["simple_navigator_clipboard_readText.html"] diff --git a/dom/events/test/clipboard/browser_navigator_clipboard_readText_ext.js b/dom/events/test/clipboard/browser_navigator_clipboard_readText_ext.js @@ -0,0 +1,227 @@ +/* -*- 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"; + +/* import-globals-from head.js */ + +const kContentFileUrl = kBaseUrlForContent + "simple_page_ext.html"; + +const sharedScript = function () { + this.clipboardReadText = function () { + return navigator.clipboard.readText().then( + data => { + browser.test.sendMessage("result", data); + }, + error => { + browser.test.sendMessage("result", [error.name, error.message]); + } + ); + }; +}; + +let contentScript = function () { + document.querySelector("button").addEventListener("click", function (e) { + clipboardReadText(); + }); + browser.test.sendMessage("ready"); +}; + +const backgroundScript = function () { + clipboardReadText(); +}; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["test.events.async.enabled", true]], + }); +}); + +// There’s another test that checks calling readText() without permission in +// toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html. +describe("test extension without clipboardRead permission", () => { + it("test content script", async function () { + const text = await promiseWritingRandomTextToClipboard(); + const extensionData = { + manifest: { + content_scripts: [ + { + js: ["sharedScript.js", "contentscript.js"], + matches: ["https://example.com/*"], + }, + ], + }, + files: { + "sharedScript.js": sharedScript, + "contentscript.js": contentScript, + }, + }; + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await BrowserTestUtils.withNewTab(kContentFileUrl, async browser => { + await extension.awaitMessage("ready"); + + // click on the content and wait for pasted button shown. + await Promise.all([ + promisePasteButtonIsShown(), + promiseClickContentElement(browser, "btn"), + ]); + + // click on the paste button. + let resultPromise = extension.awaitMessage("result"); + await Promise.all([ + promisePasteButtonIsHidden(), + promiseClickPasteButton(), + ]); + is(await resultPromise, text, "check readText() result"); + + // click on the content and wait for pasted button shown. + await Promise.all([ + promisePasteButtonIsShown(), + promiseClickContentElement(browser, "btn"), + ]); + + // dismiss the paste button. + resultPromise = extension.awaitMessage("result"); + await Promise.all([ + promisePasteButtonIsHidden(), + promiseDismissPasteButton(), + ]); + const [name, message] = await resultPromise; + is(name, "NotAllowedError", "check readText() error name"); + is( + message, + "Clipboard read operation is not allowed.", + "check readText() error message" + ); + }); + await extension.unload(); + }); + + describe("test background script", () => { + // paste button should not be shown during the test. + const popupShownListener = function (e) { + const pastePopup = document.getElementById(kPasteMenuPopupId); + if (e.target != pastePopup) { + return; + } + ok( + false, + "Paste popup should not be shown for extension with permission" + ); + promiseDismissPasteButton(); + }; + beforeEach(() => { + document.addEventListener("popupshown", popupShownListener); + }); + afterEach(() => { + document.removeEventListener("popupshown", popupShownListener); + }); + + it("test without user activation", async function () { + const text = await promiseWritingRandomTextToClipboard(); + const extensionData = { + background: [sharedScript, backgroundScript], + }; + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + const [name, message] = await extension.awaitMessage("result"); + is(name, "NotAllowedError", "check readText() error name"); + is( + message, + "Clipboard read request was blocked due to lack of user activation.", + "check readText() error message" + ); + await extension.unload(); + }); + + it("test with user activation", async function () { + const backgroundScriptWithUserActivation = function () { + browser.test.withHandlingUserInput(() => { + clipboardReadText(); + }); + }; + const text = await promiseWritingRandomTextToClipboard(); + const extensionData = { + background: [sharedScript, backgroundScriptWithUserActivation], + }; + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + const [name, message] = await extension.awaitMessage("result"); + is(name, "NotAllowedError", "check readText() error name"); + is( + message, + "Clipboard read operation is not allowed.", + "check readText() error message" + ); + await extension.unload(); + }); + }); +}); + +// There’s another test that checks calling readText() with permission in +// toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html. +describe("test extension with clipboardRead permission", () => { + // paste button should not be shown during the test. + const popupShownListener = function (e) { + const pastePopup = document.getElementById(kPasteMenuPopupId); + if (e.target != pastePopup) { + return; + } + ok(false, "Paste popup should not be shown for extension with permission"); + promiseDismissPasteButton(); + }; + beforeEach(() => { + document.addEventListener("popupshown", popupShownListener); + }); + afterEach(() => { + document.removeEventListener("popupshown", popupShownListener); + }); + + it("test content script", async function () { + const text = await promiseWritingRandomTextToClipboard(); + const extensionData = { + manifest: { + content_scripts: [ + { + js: ["sharedScript.js", "contentscript.js"], + matches: ["https://example.com/*"], + }, + ], + permissions: ["clipboardRead"], + }, + files: { + "sharedScript.js": sharedScript, + "contentscript.js": contentScript, + }, + }; + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await BrowserTestUtils.withNewTab(kContentFileUrl, async browser => { + await extension.awaitMessage("ready"); + + // extension with clipboardRead permission should not show paste button. + const resultPromise = extension.awaitMessage("result"); + await promiseClickContentElement(browser, "btn"); + is(await resultPromise, text, "check readText() result"); + }); + await extension.unload(); + }); + + it("test background script", async function () { + const text = await promiseWritingRandomTextToClipboard(); + const extensionData = { + background: [sharedScript, backgroundScript], + manifest: { + permissions: ["clipboardRead"], + }, + }; + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + is(await extension.awaitMessage("result"), text, "check readText() result"); + await extension.unload(); + }); +}); diff --git a/dom/events/test/clipboard/browser_navigator_clipboard_read_ext.js b/dom/events/test/clipboard/browser_navigator_clipboard_read_ext.js @@ -0,0 +1,247 @@ +/* -*- 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"; + +/* import-globals-from head.js */ + +const kContentFileUrl = kBaseUrlForContent + "simple_page_ext.html"; + +const sharedScript = function () { + this.clipboardRead = function () { + return navigator.clipboard + .read() + .then(items => { + browser.test.assertEq(1, items.length, "Should only 1 ClipboardItem"); + + const item = items[0]; + browser.test.assertEq( + 1, + item.types.length, + "ClipboardItem should contain only one type" + ); + browser.test.assertEq( + "text/plain", + item.types[0], + "Type should be text/plain" + ); + + return item.getType("text/plain"); + }) + .then(blob => { + return blob.text(); + }) + .then(text => { + browser.test.sendMessage("result", text); + }) + .catch(error => { + browser.test.sendMessage("result", [error.name, error.message]); + }); + }; +}; + +const contentScript = function () { + document.querySelector("button").addEventListener("click", function (e) { + clipboardRead(); + }); + browser.test.sendMessage("ready"); +}; + +const backgroundScript = function () { + clipboardRead(); +}; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["test.events.async.enabled", true]], + }); +}); + +// There’s another test that checks calling read() without permission in +// toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html. +describe("test extension without clipboardRead permission", () => { + it("test content script", async function () { + const text = await promiseWritingRandomTextToClipboard(); + const extensionData = { + manifest: { + content_scripts: [ + { + js: ["sharedScript.js", "contentscript.js"], + matches: ["https://example.com/*"], + }, + ], + }, + files: { + "sharedScript.js": sharedScript, + "contentscript.js": contentScript, + }, + }; + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await BrowserTestUtils.withNewTab(kContentFileUrl, async browser => { + await extension.awaitMessage("ready"); + + // click on the content and wait for pasted button shown. + await Promise.all([ + promisePasteButtonIsShown(), + promiseClickContentElement(browser, "btn"), + ]); + + // click on the paste button. + let resultPromise = extension.awaitMessage("result"); + await Promise.all([ + promisePasteButtonIsHidden(), + promiseClickPasteButton(), + ]); + is(await resultPromise, text, "check read() result"); + + // click on the content and wait for pasted button shown. + await Promise.all([ + promisePasteButtonIsShown(), + promiseClickContentElement(browser, "btn"), + ]); + + // dismiss the paste button. + resultPromise = extension.awaitMessage("result"); + await Promise.all([ + promisePasteButtonIsHidden(), + promiseDismissPasteButton(), + ]); + const [name, message] = await resultPromise; + is(name, "NotAllowedError", "check read() error name"); + is( + message, + "Clipboard read operation is not allowed.", + "check read() error message" + ); + }); + await extension.unload(); + }); + + describe("test background script", () => { + // paste button should not be shown during the test. + const popupShownListener = function (e) { + const pastePopup = document.getElementById(kPasteMenuPopupId); + if (e.target != pastePopup) { + return; + } + ok( + false, + "Paste popup should not be shown for extension with permission" + ); + promiseDismissPasteButton(); + }; + beforeEach(() => { + document.addEventListener("popupshown", popupShownListener); + }); + afterEach(() => { + document.removeEventListener("popupshown", popupShownListener); + }); + + it("test without user activation", async function () { + const text = await promiseWritingRandomTextToClipboard(); + const extensionData = { + background: [sharedScript, backgroundScript], + }; + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + const [name, message] = await extension.awaitMessage("result"); + is(name, "NotAllowedError", "check read() error name"); + is( + message, + "Clipboard read request was blocked due to lack of user activation.", + "check read() error message" + ); + await extension.unload(); + }); + + it("test with user activation", async function () { + const backgroundScriptWithUserActivation = function () { + browser.test.withHandlingUserInput(() => { + clipboardRead(); + }); + }; + const text = await promiseWritingRandomTextToClipboard(); + const extensionData = { + background: [sharedScript, backgroundScriptWithUserActivation], + }; + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + const [name, message] = await extension.awaitMessage("result"); + is(name, "NotAllowedError", "check read() error name"); + is( + message, + "Clipboard read operation is not allowed.", + "check read() error message" + ); + await extension.unload(); + }); + }); +}); + +// There’s another test that checks calling read() with permission in +// toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html. +describe("test extension with clipboardRead permission", () => { + // paste button should not be shown during the test. + const popupShownListener = function (e) { + const pastePopup = document.getElementById(kPasteMenuPopupId); + if (e.target != pastePopup) { + return; + } + ok(false, "Paste popup should not be shown for extension with permission"); + promiseDismissPasteButton(); + }; + beforeEach(() => { + document.addEventListener("popupshown", popupShownListener); + }); + afterEach(() => { + document.removeEventListener("popupshown", popupShownListener); + }); + + it("test content script", async function () { + const text = await promiseWritingRandomTextToClipboard(); + const extensionData = { + manifest: { + content_scripts: [ + { + js: ["sharedScript.js", "contentscript.js"], + matches: ["https://example.com/*"], + }, + ], + permissions: ["clipboardRead"], + }, + files: { + "sharedScript.js": sharedScript, + "contentscript.js": contentScript, + }, + }; + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + await BrowserTestUtils.withNewTab(kContentFileUrl, async browser => { + await extension.awaitMessage("ready"); + + // extension with clipboardRead permission should not show paste button. + const resultPromise = extension.awaitMessage("result"); + await promiseClickContentElement(browser, "btn"); + is(await resultPromise, text, "check read() result"); + }); + await extension.unload(); + }); + + it("test background script", async function () { + const text = await promiseWritingRandomTextToClipboard(); + const extensionData = { + background: [sharedScript, backgroundScript], + manifest: { + permissions: ["clipboardRead"], + }, + }; + const extension = ExtensionTestUtils.loadExtension(extensionData); + await extension.startup(); + is(await extension.awaitMessage("result"), text, "check read() result"); + await extension.unload(); + }); +}); diff --git a/dom/events/test/clipboard/simple_page_ext.html b/dom/events/test/clipboard/simple_page_ext.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<button id=btn>X</button> diff --git a/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html b/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html @@ -58,26 +58,26 @@ function clearClipboard() { // Test that without enough permissions, we are NOT allowed to use writeText, write, read or readText in background script add_task(async function test_background_async_clipboard_no_permissions() { - function backgroundScript() { + async function backgroundScript() { const item = new ClipboardItem({ "text/plain": new Blob(["HI"], {type: "text/plain"}) }); - browser.test.assertRejects( + await browser.test.assertRejects( clipboardRead(), "Clipboard read request was blocked due to lack of user activation.", "Read should be denied without permission" ); - browser.test.assertRejects( + await browser.test.assertRejects( clipboardWrite([item]), "Clipboard write was blocked due to lack of user activation.", "Write should be denied without permission" ); - browser.test.assertRejects( + await browser.test.assertRejects( clipboardWriteText("blabla"), "Clipboard write was blocked due to lack of user activation.", "WriteText should be denied without permission" ); - browser.test.assertRejects( + await browser.test.assertRejects( clipboardReadText(), "Clipboard read request was blocked due to lack of user activation.", "ReadText should be denied without permission" @@ -95,26 +95,26 @@ add_task(async function test_background_async_clipboard_no_permissions() { // Test that without enough permissions, we are NOT allowed to use writeText, write, read or readText in content script add_task(async function test_contentscript_async_clipboard_no_permission() { - function contentScript() { + async function contentScript() { const item = new ClipboardItem({ "text/plain": new Blob(["HI"], {type: "text/plain"}) }); - browser.test.assertRejects( + await browser.test.assertRejects( clipboardRead(), "Clipboard read request was blocked due to lack of user activation.", "Read should be denied without permission" ); - browser.test.assertRejects( + await browser.test.assertRejects( clipboardWrite([item]), "Clipboard write was blocked due to lack of user activation.", "Write should be denied without permission" ); - browser.test.assertRejects( + await browser.test.assertRejects( clipboardWriteText("blabla"), "Clipboard write was blocked due to lack of user activation.", "WriteText should be denied without permission" ); - browser.test.assertRejects( + await browser.test.assertRejects( clipboardReadText(), "Clipboard read request was blocked due to lack of user activation.", "ReadText should be denied without permission" @@ -161,7 +161,6 @@ add_task(async function test_contentscript_clipboard_permission_writetext() { }], permissions: [ "clipboardWrite", - "clipboardRead", ], }, files: { @@ -180,6 +179,8 @@ add_task(async function test_contentscript_clipboard_permission_writetext() { }); // Test that with enough permissions, we are allowed to use readText in content script +// There’s another test that checks calling readText() with permission does not +// show the paste button, in dom/events/test/clipboard/browser_navigator_clipboard_readText_ext.js. add_task(async function test_contentscript_clipboard_permission_readtext() { function contentScript() { let str = "HI"; @@ -202,7 +203,6 @@ add_task(async function test_contentscript_clipboard_permission_readtext() { matches: ["https://example.com/*/file_sample.html"], }], permissions: [ - "clipboardWrite", "clipboardRead", ], }, @@ -244,7 +244,6 @@ add_task(async function test_contentscript_clipboard_permission_write() { }], permissions: [ "clipboardWrite", - "clipboardRead", ], }, files: { @@ -263,6 +262,8 @@ add_task(async function test_contentscript_clipboard_permission_write() { }); // Test that with enough permissions, we are allowed to use read in content script +// There’s another test that checks calling readText() with permission does not +// show the paste button, in dom/events/test/clipboard/browser_navigator_clipboard_read_ext.js. add_task(async function test_contentscript_clipboard_permission_read() { function contentScript() { clipboardRead().then(async function(items) { @@ -286,7 +287,6 @@ add_task(async function test_contentscript_clipboard_permission_read() { matches: ["https://example.com/*/file_sample.html"], }], permissions: [ - "clipboardWrite", "clipboardRead", ], }, diff --git a/widget/nsBaseClipboard.cpp b/widget/nsBaseClipboard.cpp @@ -619,12 +619,6 @@ NS_IMETHODIMP nsBaseClipboard::GetDataSnapshot( } } - // TODO: enable showing the "Paste" button in this case; see bug 1773681. - if (aRequestingPrincipal->GetIsAddonOrExpandedAddonPrincipal()) { - MOZ_CLIPBOARD_LOG("%s: Addon without read permission.", __FUNCTION__); - return aCallback->OnError(NS_ERROR_FAILURE); - } - RequestUserConfirmation(aWhichClipboard, aFlavorList, aRequestingWindowContext, aRequestingPrincipal, aCallback);