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:
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"],