tor-browser

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

commit 18b0d05db441fd56ec63739a6aeba51f441437f3
parent 57c3d08ee2555fa6737aff9c7a21309ad057cd16
Author: Dharma Ong <dharmaong00@gmail.com>
Date:   Wed, 17 Dec 2025 15:38:16 +0000

Bug 1995349 - Add ability to detect a client param in the urlbar. r=daleharvey,omc-reviewers,aminomancer

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

Diffstat:
Mbrowser/components/asrouter/modules/ASRouter.sys.mjs | 3++-
Mbrowser/components/asrouter/modules/ASRouterTriggerListeners.sys.mjs | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mbrowser/components/asrouter/tests/browser/browser_asrouter_cfr.js | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/asrouter/tests/unit/ASRouter.test.js | 6++++--
Mbrowser/components/asrouter/tests/unit/ASRouterTriggerListeners.test.js | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtoolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json | 21+++++++++++++++++++++
6 files changed, 248 insertions(+), 14 deletions(-)

diff --git a/browser/components/asrouter/modules/ASRouter.sys.mjs b/browser/components/asrouter/modules/ASRouter.sys.mjs @@ -991,7 +991,8 @@ export class _ASRouter { lazy.ASRouterTriggerListeners.get(trigger.id).init( this._triggerHandler, trigger.params, - trigger.patterns + trigger.patterns, + trigger.regexPatterns ); unseenListeners.delete(trigger.id); } diff --git a/browser/components/asrouter/modules/ASRouterTriggerListeners.sys.mjs b/browser/components/asrouter/modules/ASRouterTriggerListeners.sys.mjs @@ -47,7 +47,11 @@ function isPrivateWindow(win) { * * @returns {object} - {host, url} pair that matched the list of allowed hosts */ -function checkURLMatch(aLocationURI, { hosts, matchPatternSet }, aRequest) { +function checkURLMatch( + aLocationURI, + { hosts, matchPatternSet, regexPatterns }, + aRequest +) { // If checks pass we return a match let match; try { @@ -68,6 +72,15 @@ function checkURLMatch(aLocationURI, { hosts, matchPatternSet }, aRequest) { } } + // Check against regex patterns + if (regexPatterns) { + for (const regex of regexPatterns) { + if (regex.test(match.url)) { + return match; + } + } + } + // Nothing else to check, return early if (!aRequest) { return false; @@ -77,12 +90,23 @@ function checkURLMatch(aLocationURI, { hosts, matchPatternSet }, aRequest) { const originalLocation = aRequest.QueryInterface(Ci.nsIChannel).originalURI; // We have been redirected if (originalLocation.spec !== aLocationURI.spec) { - return ( - hosts.has(originalLocation.host) && { + if (hosts.has(originalLocation.host)) { + return { host: originalLocation.host, url: originalLocation.spec, + }; + } + + if (regexPatterns) { + for (const regex of regexPatterns) { + if (regex.test(originalLocation.spec)) { + return { + host: originalLocation.host, + url: originalLocation.spec, + }; + } } - ); + } } return false; @@ -190,8 +214,9 @@ export const ASRouterTriggerListeners = new Map([ _hosts: new Set(), _matchPatternSet: null, readerModeEvent: "Reader:UpdateReaderButton", + _regexPatterns: null, - init(triggerHandler, hosts, patterns) { + init(triggerHandler, hosts, patterns, regexPatterns) { if (!this._initialized) { this.receiveMessage = this.receiveMessage.bind(this); lazy.AboutReaderParent.addMessageListener(this.readerModeEvent, this); @@ -207,6 +232,13 @@ export const ASRouterTriggerListeners = new Map([ if (hosts) { hosts.forEach(h => this._hosts.add(h)); } + + if (regexPatterns) { + this._regexPatterns = [ + ...(this._regexPatterns || []), + ...regexPatterns.map(pattern => new RegExp(pattern)), + ]; + } }, receiveMessage({ data, target }) { @@ -214,6 +246,7 @@ export const ASRouterTriggerListeners = new Map([ const match = checkURLMatch(target.currentURI, { hosts: this._hosts, matchPatternSet: this._matchPatternSet, + regexPatterns: this._regexPatterns, }); if (match) { this._triggerHandler(target, { id: this.id, param: match }); @@ -231,6 +264,7 @@ export const ASRouterTriggerListeners = new Map([ this._triggerHandler = null; this._hosts = new Set(); this._matchPatternSet = null; + this._regexPatterns = null; } }, }, @@ -282,8 +316,9 @@ export const ASRouterTriggerListeners = new Map([ _hosts: null, _matchPatternSet: null, _visits: null, + regexPatterns: null, - init(triggerHandler, hosts = [], patterns) { + init(triggerHandler, hosts = [], patterns, regexPatterns) { if (!this._initialized) { this.onTabSwitch = this.onTabSwitch.bind(this); lazy.EveryWindow.registerCallback( @@ -316,6 +351,13 @@ export const ASRouterTriggerListeners = new Map([ } else { this._hosts = new Set(hosts); // Clone the hosts to avoid unexpected behaviour } + + if (regexPatterns) { + this._regexPatterns = [ + ...(this._regexPatterns || []), + ...regexPatterns.map(pattern => new RegExp(pattern)), + ]; + } }, /** @@ -349,6 +391,7 @@ export const ASRouterTriggerListeners = new Map([ const match = checkURLMatch(gBrowser.currentURI, { hosts: this._hosts, matchPatternSet: this._matchPatternSet, + regexPatterns: this._regexPatterns, }); if (match) { this.triggerHandler(gBrowser.selectedBrowser, match); @@ -387,7 +430,11 @@ export const ASRouterTriggerListeners = new Map([ if (aWebProgress.isTopLevel && !isSameDocument) { const match = checkURLMatch( aLocationURI, - { hosts: this._hosts, matchPatternSet: this._matchPatternSet }, + { + hosts: this._hosts, + matchPatternSet: this._matchPatternSet, + regexPatterns: this._regexPatterns, + }, aRequest ); if (match) { @@ -405,6 +452,7 @@ export const ASRouterTriggerListeners = new Map([ this._hosts = null; this._matchPatternSet = null; this._visits = null; + this._regexPatterns = null; } }, }, @@ -424,12 +472,13 @@ export const ASRouterTriggerListeners = new Map([ _hosts: null, _matchPatternSet: null, _visits: null, + _regexPatterns: null, /* * If the listener is already initialised, `init` will replace the trigger * handler and add any new hosts to `this._hosts`. */ - init(triggerHandler, hosts = [], patterns) { + init(triggerHandler, hosts = [], patterns, regexPatterns) { if (!this._initialized) { this.onLocationChange = this.onLocationChange.bind(this); lazy.EveryWindow.registerCallback( @@ -461,6 +510,13 @@ export const ASRouterTriggerListeners = new Map([ } else { this._hosts = new Set(hosts); // Clone the hosts to avoid unexpected behaviour } + + if (regexPatterns) { + this._regexPatterns = [ + ...(this._regexPatterns || []), + ...regexPatterns.map(pattern => new RegExp(pattern)), + ]; + } }, uninit() { @@ -472,6 +528,7 @@ export const ASRouterTriggerListeners = new Map([ this._hosts = null; this._matchPatternSet = null; this._visits = null; + this._regexPatterns = null; } }, @@ -485,7 +542,11 @@ export const ASRouterTriggerListeners = new Map([ if (aWebProgress.isTopLevel && !isSameDocument) { const match = checkURLMatch( aLocationURI, - { hosts: this._hosts, matchPatternSet: this._matchPatternSet }, + { + hosts: this._hosts, + matchPatternSet: this._matchPatternSet, + regexPatterns: this._regexPatterns, + }, aRequest ); if (match) { diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_cfr.js b/browser/components/asrouter/tests/browser/browser_asrouter_cfr.js @@ -673,6 +673,62 @@ add_task(async function test_matchPattern() { Services.fog.testResetFOG(); }); +add_task(async function test_matchRegex() { + let count = 0; + const triggerHandler = () => ++count; + const frequentVisitsTrigger = ASRouterTriggerListeners.get("frequentVisits"); + await frequentVisitsTrigger.init( + triggerHandler, + [], + [], + ["example\\.com/?$"] + ); + + const browser = gBrowser.selectedBrowser; + BrowserTestUtils.startLoadingURIString(browser, "http://example.com/"); + await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/"); + + await BrowserTestUtils.waitForCondition( + () => frequentVisitsTrigger._visits.get("example.com").length === 1, + "Registered regex matched the current location" + ); + + BrowserTestUtils.startLoadingURIString(browser, "about:config"); + await BrowserTestUtils.browserLoaded(browser, false, "about:config"); + + await BrowserTestUtils.waitForCondition( + () => frequentVisitsTrigger._visits.get("example.com").length === 1, + "Navigated to a new page but not a match" + ); + + BrowserTestUtils.startLoadingURIString(browser, "http://example.com/"); + await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/"); + + await BrowserTestUtils.waitForCondition( + () => frequentVisitsTrigger._visits.get("example.com").length === 1, + "Navigated to a location that matches the pattern but within 15 mins" + ); + + BrowserTestUtils.startLoadingURIString(browser, "http://www.example.com/"); + await BrowserTestUtils.browserLoaded( + browser, + false, + "http://www.example.com/" + ); + + await BrowserTestUtils.waitForCondition( + () => frequentVisitsTrigger._visits.get("www.example.com").length === 1, + "www.example.com is a different host that also matches the pattern." + ); + await BrowserTestUtils.waitForCondition( + () => frequentVisitsTrigger._visits.get("example.com").length === 1, + "www.example.com is a different host that also matches the pattern." + ); + + ASRouterTriggerListeners.get("frequentVisits").uninit(); + Services.fog.testResetFOG(); +}); + add_task(async function test_providerNames() { const providersBranch = "browser.newtabpage.activity-stream.asrouter.providers."; diff --git a/browser/components/asrouter/tests/unit/ASRouter.test.js b/browser/components/asrouter/tests/unit/ASRouter.test.js @@ -1012,13 +1012,15 @@ describe("ASRouter", () => { ASRouterTriggerListeners.get("openURL").init, Router._triggerHandler, ["www.mozilla.org", "www.mozilla.com"], - undefined + undefined, // patterns + undefined // regexPatterns ); assert.calledWithExactly( ASRouterTriggerListeners.get("openURL").init, Router._triggerHandler, ["www.example.com"], - undefined + undefined, // patterns + undefined // regexPatterns ); }); it("should parse the message's messagesLoaded trigger and immediately fire trigger", async () => { diff --git a/browser/components/asrouter/tests/unit/ASRouterTriggerListeners.test.js b/browser/components/asrouter/tests/unit/ASRouterTriggerListeners.test.js @@ -25,6 +25,7 @@ describe("ASRouterTriggerListeners", () => { "cookieBannerHandled" ); const hosts = ["www.mozilla.com", "www.mozilla.org"]; + const regexPatterns = ["mozilla"]; const regionFake = { _home: "cn", @@ -44,7 +45,7 @@ describe("ASRouterTriggerListeners", () => { gBrowser: { addTabsProgressListener: sandbox.stub(), removeTabsProgressListener: sandbox.stub(), - currentURI: { host: "" }, + currentURI: { host: "", regexPattern: "" }, }, addEventListener: sinon.stub(), removeEventListener: sinon.stub(), @@ -210,6 +211,24 @@ describe("ASRouterTriggerListeners", () => { param: { host: null, url: hosts[1] }, }); }); + it("should match URL using regexPatterns", () => { + const stub = sandbox.stub(); + + const target = { currentURI: { host: null, spec: hosts[1] } }; + + // match exactly the string "mozilla" + openArticleURLListener.init(stub, [], [], ["mozilla"]); + + const [, { receiveMessage }] = + global.AboutReaderParent.addMessageListener.firstCall.args; + receiveMessage({ data: { isArticle: true }, target }); + + assert.calledOnce(stub); + assert.calledWithExactly(stub, target, { + id: openArticleURLListener.id, + param: { host: null, url: hosts[1] }, + }); + }); it("should remove the message listener", () => { openArticleURLListener.init(sandbox.stub(), hosts, hosts); openArticleURLListener.uninit(); @@ -222,9 +241,17 @@ describe("ASRouterTriggerListeners", () => { describe("frequentVisits", () => { let _triggerHandler; beforeEach(() => { + globals.set( + "MatchPatternSet", + sandbox.stub().callsFake(patterns => ({ + patterns, + matches: url => patterns.has(url), + })) + ); + _triggerHandler = sandbox.stub(); sandbox.useFakeTimers(); - frequentVisitsListener.init(_triggerHandler, hosts); + frequentVisitsListener.init(_triggerHandler, hosts, [], regexPatterns); }); afterEach(() => { sandbox.clock.restore(); @@ -279,6 +306,21 @@ describe("ASRouterTriggerListeners", () => { assert.notCalled(stub); }); + it("should call triggerHandler when regexPatterns match", () => { + const stub = sandbox.stub(frequentVisitsListener, "triggerHandler"); + + existingWindow.gBrowser.currentURI = { + host: "www.example.com", + spec: "https://www.mozilla.org", + }; + + frequentVisitsListener.onTabSwitch({ + target: { ownerGlobal: existingWindow }, + }); + + assert.calledOnce(stub); + }); + describe("MatchPattern", () => { beforeEach(() => { globals.set( @@ -502,6 +544,16 @@ describe("ASRouterTriggerListeners", () => { describe("#init", () => { beforeEach(() => { + globals.set( + "MatchPatternSet", + sandbox.stub().callsFake(patterns => ({ + patterns, + matches: url => patterns.has(url), + })) + ); + sandbox.stub(global.AboutReaderParent, "addMessageListener"); + sandbox.stub(global.AboutReaderParent, "removeMessageListener"); + openURLListener.init(triggerHandler, hosts); }); afterEach(() => { @@ -568,6 +620,15 @@ describe("ASRouterTriggerListeners", () => { }); describe("#onLocationChange", () => { + beforeEach(() => { + globals.set( + "MatchPatternSet", + sandbox.stub().callsFake(patterns => ({ + patterns, + matches: url => patterns.has(url), + })) + ); + }); afterEach(() => { openURLListener.uninit(); frequentVisitsListener.uninit(); @@ -724,6 +785,38 @@ describe("ASRouterTriggerListeners", () => { assert.calledOnce(aRequest.QueryInterface); assert.notCalled(newTriggerHandler); }); + it("should call triggerHandler when regexPatterns match the URL", () => { + const newTriggerHandler = sinon.stub(); + openURLListener.init(newTriggerHandler, [], [], ["mozilla"]); + + const browser = {}; + const webProgress = { isTopLevel: true }; + const aLocationURI = { + host: "www.mozilla.org", + spec: "www.mozilla.org", + }; + const aRequest = { + QueryInterface: sandbox.stub().returns({ + originalURI: { spec: "www.mozilla.org", host: "www.mozilla.org" }, + }), + }; + openURLListener.onLocationChange( + browser, + webProgress, + aRequest, + aLocationURI + ); + + assert.calledOnce(newTriggerHandler); + assert.calledWithExactly(newTriggerHandler, browser, { + id: "openURL", + param: { + host: "www.mozilla.org", + url: "www.mozilla.org", + }, + context: { visitsCount: 1 }, + }); + }); }); }); diff --git a/toolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json @@ -24,6 +24,13 @@ "type": "string" }, "description": "List of Match pattern compatible strings to match against" + }, + "regexPatterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of regular expression patterns to match against URLs" } }, "required": ["id"], @@ -50,6 +57,13 @@ "type": "string" }, "description": "List of Match pattern compatible strings to match against" + }, + "regexPatterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of regular expression patterns to match against URLs" } }, "required": ["id"], @@ -88,6 +102,13 @@ "type": "string" }, "description": "List of Match pattern compatible strings to match against" + }, + "regexPatterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of regular expression patterns to match against URLs" } }, "required": ["id"],