tor-browser

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

commit 0a27214edf22bfa75d85241be829c233364f1fd5
parent 689e5a7f77a435da2762d39415b64ae31a78306b
Author: Yubin Jamora <yjamora@mozilla.com>
Date:   Fri,  3 Oct 2025 01:08:29 +0000

Bug 1906256 - allow to use mic in chatbot if the perm is already granted r=Mardak,firefox-ai-ml-reviewers,emz

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

Diffstat:
Mbrowser/actors/WebRTCParent.sys.mjs | 24++++++++++++++++++++++++
Mbrowser/components/genai/tests/browser/browser_chat_sidebar.js | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 156 insertions(+), 0 deletions(-)

diff --git a/browser/actors/WebRTCParent.sys.mjs b/browser/actors/WebRTCParent.sys.mjs @@ -534,6 +534,18 @@ function prompt(aActor, aBrowser, aRequest) { } } + // If the request comes from a sidebar, + // the user already gave a persistent permission, skip showing a notification + // otherwise deny request. + if (isSidebar(aBrowser)) { + if (!aActor.checkRequestAllowed(aRequest, principal, aBrowser)) { + aActor.denyRequest(aRequest); + return; + } + + return; + } + // If the user has already denied access once in this tab, // deny again without even showing the notification icon. for (const type of requestTypes) { @@ -1639,3 +1651,15 @@ function onCameraPromptShown(doc, isHandlingUserInput) { // matching the user selection. webrtcPreview?.startPreview({ deviceId, mediaSource: "camera" }); } + +function isSidebar(browser) { + const sidebarBrowser = + browser.browsingContext?.topChromeWindow?.SidebarController?.browser; + if (!sidebarBrowser) { + return false; + } + + const nestedBrowsers = + sidebarBrowser.contentDocument.querySelectorAll("browser"); + return Array.from(nestedBrowsers).some(b => b === browser); +} diff --git a/browser/components/genai/tests/browser/browser_chat_sidebar.js b/browser/components/genai/tests/browser/browser_chat_sidebar.js @@ -352,3 +352,135 @@ add_task(async function test_pip_actor_not_chat_sidebar() { await SidebarController.hide(); }); }); + +add_task( + async function test_chatbot_microphone_access_if_persistent_perm_already_granted_in_tab() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.ml.chat.provider", "https://example.org"]], + }); + + await BrowserTestUtils.withNewTab("https://example.org", async browser => { + const { principal, rawId } = await SpecialPowers.spawn( + browser, + [], + async () => { + const stream = await content.navigator.mediaDevices.getUserMedia({ + audio: true, + }); + const track = stream.getAudioTracks()[0]; + const id = track.getSettings().deviceId || "default"; + stream.getTracks().forEach(t => t.stop()); + return { + principal: content.document.nodePrincipal, + rawId: id, + }; + } + ); + + Assert.ok(rawId, "Got microphone rawId from the tab"); + + const key = "microphone"; + SitePermissions.setForPrincipal( + principal, + key, + SitePermissions.ALLOW, + SitePermissions.SCOPE_PERSISTENT, + browser + ); + + await SidebarController.show("viewGenaiChatSidebar"); + + const { document } = SidebarController.browser.contentWindow; + const chatbotBrowserContainer = + document.getElementById("browser-container"); + const chatbotBrowser = chatbotBrowserContainer.querySelector("browser"); + + await BrowserTestUtils.browserLoaded(chatbotBrowser, false, url => { + return new URL(url).origin === "https://example.org"; + }); + + const chatbotPrincipal = await SpecialPowers.spawn( + chatbotBrowser, + [], + async () => content.document.nodePrincipal + ); + + let shown = false; + let onShown = () => { + shown = true; + }; + PopupNotifications.panel.addEventListener("popupshown", onShown); + + await SpecialPowers.spawn(chatbotBrowser, [rawId], async id => { + const { WebRTCChild } = SpecialPowers.ChromeUtils.importESModule( + "resource:///actors/WebRTCChild.sys.mjs" + ); + + const mic = [ + { + type: "audioinput", + rawName: "fake mic", + rawId: id, + id, + QueryInterface: ChromeUtils.generateQI([Ci.nsIMediaDevice]), + mediaSource: "microphone", + }, + ]; + + const req = { + type: "getUserMedia", + windowID: content.windowGlobalChild.outerWindowId, + isSecure: true, + isHandlingUserInput: true, + + audioInputDevices: mic, + videoInputDevices: [], + audioOutputDevices: [], + + deviceIndex: 0, + devices: mic, + + getConstraints: () => ({ audio: true }), + getAudioInputOptions: () => ({ deviceId: id }), + getVideoInputOptions: () => ({}), + getAudioOutputOptions: () => ({}), + }; + + WebRTCChild.observe(req, "getUserMedia:request"); + }); + + const sidebarMicPerm = SitePermissions.getForPrincipal( + chatbotPrincipal, + key, + chatbotBrowser + ); + + is( + sidebarMicPerm.state, + SitePermissions.ALLOW, + "Sidebar chatbot has granted mic allow" + ); + is( + sidebarMicPerm.scope, + SitePermissions.SCOPE_PERSISTENT, + "sidebar chatbot mic access is persistent" + ); + Assert.ok( + !shown, + "PopupNotification didn't fire because the mic access has been granted" + ); + + PopupNotifications.panel.removeEventListener("popupshown", onShown); + SitePermissions.removeFromPrincipal(principal, key, browser); + SitePermissions.removeFromPrincipal( + chatbotPrincipal, + key, + chatbotBrowser + ); + }); + + await SidebarController.hide(); + await SpecialPowers.popPrefEnv(); + Services.fog.testResetFOG(); + } +);