tor-browser

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

commit d77786ccccc767d9b139ed1dd8d026d4b8b73d12
parent dc5e86c79bf3d6459e305be849dabbe56d5ca8da
Author: Duncan McIntosh <dmcintosh@mozilla.com>
Date:   Wed,  1 Oct 2025 13:51:38 +0000

Bug 1982087 - Additionally distinguish Taskbar Tabs by their manifest's scope, if provided. r=mossop

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

Diffstat:
Mbrowser/components/taskbartabs/TaskbarTabs.sys.mjs | 5+++++
Mbrowser/components/taskbartabs/TaskbarTabsRegistry.sys.mjs | 55+++++++++++++++++++++++++++++++++++++++++++------------
Mbrowser/components/taskbartabs/test/browser/browser_taskbarTabs_manifest.js | 121++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mbrowser/components/taskbartabs/test/xpcshell/test_TaskbarTabsRegistry.js | 35+++++++++++++++++++++++++++++++++++
4 files changed, 185 insertions(+), 31 deletions(-)

diff --git a/browser/components/taskbartabs/TaskbarTabs.sys.mjs b/browser/components/taskbartabs/TaskbarTabs.sys.mjs @@ -59,6 +59,11 @@ export const TaskbarTabs = new (class { return this.#registry.findOrCreateTaskbarTab(...args); } + async findTaskbarTab(...args) { + await this.#ready; + return this.#registry.findTaskbarTab(...args); + } + /** * Moves an existing tab into a new Taskbar Tab window. * diff --git a/browser/components/taskbartabs/TaskbarTabsRegistry.sys.mjs b/browser/components/taskbartabs/TaskbarTabsRegistry.sys.mjs @@ -39,7 +39,12 @@ async function getJsonSchema() { class TaskbarTab { // Unique identifier for the Taskbar Tab. #id; - // List of hosts associated with this Taskbar tab. + // List of scopes associated with this Taskbar Tab. A scope has a 'hostname' + // property, and a 'prefix' property. If a 'prefix' is set, then the path + // must literally start with that prefix; this matches the 'within scope' + // algorithm of the Web App Manifest specification. + // + // @type {{ hostname: string; [prefix]: string }[]} #scopes = []; // Container the Taskbar Tab is opened in when opened from the Taskbar. #userContextId; @@ -237,10 +242,23 @@ export class TaskbarTabsRegistry { return taskbarTab; } + let scope = { hostname: aUrl.host }; + if ("scope" in manifest) { + // Note: manifest.scope will not be set unless the start_url is + // within scope. As such, this scope always contains the start_url. + // If a manifest is used but there isn't a scope, it uses the parent + // of the start_url; e.g. '/a/b/c.html' --> '/a/b'. + const scopeUri = Services.io.newURI(manifest.scope); + scope = { + hostname: scopeUri.host, + prefix: scopeUri.filePath, + }; + } + let id = Services.uuid.generateUUID().toString().slice(1, -1); taskbarTab = new TaskbarTab({ id, - scopes: [{ hostname: aUrl.host }], + scopes: [scope], userContextId: aUserContextId, name: manifest.name ?? generateName(aUrl), startUrl: manifest.start_url ?? aUrl.prePath, @@ -299,19 +317,32 @@ export class TaskbarTabsRegistry { } for (const tt of this.#taskbarTabs) { + let bestPrefix = ""; for (const scope of tt.scopes) { - if (aUrl.host === scope.hostname) { - if (aUserContextId !== tt.userContextId) { - lazy.logConsole.info( - `Matched TaskbarTab for URL ${aUrl.host} to ${scope.hostname}, but container ${aUserContextId} mismatched ${tt.userContextId}.` - ); - } else { - lazy.logConsole.info( - `Matched TaskbarTab for URL ${aUrl.host} to ${scope.hostname} with container ${aUserContextId}.` - ); - return tt; + if (aUrl.host !== scope.hostname) { + continue; + } + if ("prefix" in scope) { + if (scope.prefix.length < bestPrefix.length) { + // We've already found something better. + continue; + } + if (!aUrl.filePath.startsWith(scope.prefix)) { + // This URL wouldn't be within scope. + continue; } } + + if (aUserContextId !== tt.userContextId) { + lazy.logConsole.info( + `Matched TaskbarTab for URL ${aUrl.host} to ${scope.hostname}, but container ${aUserContextId} mismatched ${tt.userContextId}.` + ); + } else { + lazy.logConsole.info( + `Matched TaskbarTab for URL ${aUrl.host} to ${scope.hostname} with container ${aUserContextId}.` + ); + return tt; + } } } diff --git a/browser/components/taskbartabs/test/browser/browser_taskbarTabs_manifest.js b/browser/components/taskbartabs/test/browser/browser_taskbarTabs_manifest.js @@ -100,30 +100,113 @@ add_task(async function test_nameAndStartUrl() { httpUrl("/example/path/string/here"), "Page action detected correct start URL" ); - }); + }, "/example/path/string/here"); }); -async function usingManifest(aCallback) { - const location = httpUrl("/taskbartabs-manifest.json"); +add_task(async function test_scope() { + gManifest = { + scope: "/example/", + }; - await BrowserTestUtils.withNewTab(httpUrl("/"), async browser => { - await SpecialPowers.spawn(browser, [location], async url => { - content.document.body.innerHTML = `<link rel="manifest" href="${url}">`; + await usingManifest((aWindow, aTaskbarTab) => { + Assert.deepEqual(aTaskbarTab.scopes[0], { + hostname: "localhost", + prefix: "/example/", }); + }, "/example/file"); +}); - const tab = window.gBrowser.getTabForBrowser(browser); - let result = await TaskbarTabs.moveTabIntoTaskbarTab(tab); +add_task(async function test_startUrlOutOfScope() { + gManifest = { + scope: "/example/", + start_url: "/example", + }; - const uri = Services.io.newURI(httpUrl("/")); - const tt = await TaskbarTabs.findOrCreateTaskbarTab(uri, 0); - is( - await TaskbarTabsUtils.getTaskbarTabIdFromWindow(result.window), - tt.id, - "moveTabIntoTaskbarTab created a Taskbar Tab" - ); - aCallback(result.window, tt); + await usingManifest((aWindow, aTaskbarTab) => { + Assert.deepEqual(aTaskbarTab.scopes[0], { + hostname: "localhost", + prefix: "/", + }); + }, "/example"); +}); - await TaskbarTabs.removeTaskbarTab(tt.id); - await BrowserTestUtils.closeWindow(result.window); - }); +add_task(async function test_scopeDistinguishesTaskbarTabs() { + const find = prefix => + TaskbarTabs.findTaskbarTab(Services.io.newURI(httpUrl(prefix)), 0); + + gManifest = { + scope: "/example/", + }; + + await usingManifest(async (aWindowOuter, aTaskbarTabOuter) => { + gManifest = { + scope: "/example/another/", + }; + + await usingManifest(async (aWindowInner, aTaskbarTabInner) => { + is( + (await find("/example/another/still")).id, + aTaskbarTabInner.id, + "/example/another/still matches to inner Taskbar Tab" + ); + is( + (await find("/example/another/")).id, + aTaskbarTabInner.id, + "/example/another/ matches to inner Taskbar Tab" + ); + is( + (await find("/example/another")).id, + aTaskbarTabOuter.id, + "/example/another matches to outer Taskbar Tab" + ); + is( + (await find("/example/different")).id, + aTaskbarTabOuter.id, + "/example/different matches to outer Taskbar Tab" + ); + is( + (await find("/example/")).id, + aTaskbarTabOuter.id, + "/example/ matches to outer Taskbar Tab" + ); + + is(await find("/example"), null, "/example does not match a Taskbar Tab"); + is( + await find("/unrelated"), + null, + "/unrelated does not match any Taskbar Tab" + ); + }, "/example/another/main"); + }, "/example/main"); +}); + +async function usingManifest(aCallback, aLocation = "/") { + const location = httpUrl("/taskbartabs-manifest.json"); + + await BrowserTestUtils.withNewTab( + { + url: httpUrl(aLocation), + gBrowser: window.gBrowser, + }, + async browser => { + await SpecialPowers.spawn(browser, [location], async url => { + content.document.body.innerHTML = `<link rel="manifest" href="${url}">`; + }); + + const tab = window.gBrowser.getTabForBrowser(browser); + let result = await TaskbarTabs.moveTabIntoTaskbarTab(tab); + + const uri = Services.io.newURI(httpUrl(aLocation)); + const tt = await TaskbarTabs.findOrCreateTaskbarTab(uri, 0); + is( + await TaskbarTabsUtils.getTaskbarTabIdFromWindow(result.window), + tt.id, + "moveTabIntoTaskbarTab created a Taskbar Tab" + ); + await aCallback(result.window, tt); + + await TaskbarTabs.removeTaskbarTab(tt.id); + await BrowserTestUtils.closeWindow(result.window); + } + ); } diff --git a/browser/components/taskbartabs/test/xpcshell/test_TaskbarTabsRegistry.js b/browser/components/taskbartabs/test/xpcshell/test_TaskbarTabsRegistry.js @@ -305,3 +305,38 @@ add_task(async function test_shortcutRelativePath_is_saved() { "Shortcut relative path should be saved" ); }); + +add_task(async function test_multiple_match_longest_prefix() { + const registry = new TaskbarTabsRegistry(); + + const uriWithPrefix = prefix => + Services.io.newURI("https://example.com" + prefix); + + const createWithScope = uri => + registry.findOrCreateTaskbarTab(uri, 0, { + manifest: { + scope: uri.spec, + }, + }); + const find = prefix => registry.findTaskbarTab(uriWithPrefix(prefix), 0); + + // Register them in an arbitrary order. + const ttAB = createWithScope(uriWithPrefix("/ab")); + const ttA = createWithScope(uriWithPrefix("/a")); + const ttABC = createWithScope(uriWithPrefix("/abc")); + const ttABCD = createWithScope(uriWithPrefix("/abc/d/")); + + equal(find("/q"), null, "/q does not exist"); + equal(find("/a").id, ttA.id, "/a matches /a"); + equal(find("/az").id, ttA.id, "/a matches /az"); + + equal(find("/ab").id, ttAB.id, "/ab matches /ab"); + equal(find("/abq").id, ttAB.id, "/abq matches /ab"); + + equal(find("/abc").id, ttABC.id, "/abc matches /abc"); + equal(find("/abc/").id, ttABC.id, "/abc/ matches /abc"); + equal(find("/abc/d").id, ttABC.id, "/abc/d matches /abc (not /abc/d/)"); + + equal(find("/abc/d/").id, ttABCD.id, "/abc/d/ matches /abc/d/"); + equal(find("/abc/d/efgh").id, ttABCD.id, "/abc/d/efgh matches /abc/d/"); +});