Skip to content

Commit bae6f2a

Browse files
Add patching changes
1 parent 847694d commit bae6f2a

File tree

5 files changed

+85
-27
lines changed

5 files changed

+85
-27
lines changed

injected/src/config-feature.js

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { immutableJSONPatch } from 'immutable-json-patch';
22
import { camelcase, computeEnabledFeatures, matchHostname, parseFeatureSettings } from './utils.js';
3+
import { URLPattern } from 'urlpattern-polyfill';
34

45
export default class ConfigFeature {
56
/** @type {import('./utils.js').RemoteConfig | undefined} */
@@ -48,19 +49,55 @@ export default class ConfigFeature {
4849
* @protected
4950
*/
5051
matchDomainFeatureSetting(featureKeyName) {
51-
const domain = this.args?.site.domain;
52-
if (!domain) return [];
5352
const domains = this._getFeatureSettings()?.[featureKeyName] || [];
5453
return domains.filter((rule) => {
55-
if (Array.isArray(rule.domain)) {
56-
return rule.domain.some((domainRule) => {
57-
return matchHostname(domain, domainRule);
58-
});
59-
}
60-
return matchHostname(domain, rule.domain);
54+
return this.matchConditionalChanges(rule);
6155
});
6256
}
6357

58+
/**
59+
* Used to match conditional changes for a settings feature.
60+
* @typedef {object} ConditionBlock
61+
* @property {string[] | string} domain?
62+
* @property {object} urlPattern?
63+
*/
64+
65+
/**
66+
* Takes a conditional block and returns true if it applies.
67+
* All conditions must be met to return true.
68+
* @param {ConditionBlock} conditionBlock
69+
* @returns {boolean}
70+
*/
71+
matchConditionalChanges(conditionBlock) {
72+
// Check domain condition
73+
if (conditionBlock.domain && !this._matchDomainConditional(conditionBlock)) {
74+
return false;
75+
}
76+
77+
// Check URL pattern condition
78+
if (conditionBlock.urlPattern) {
79+
const pattern = new URLPattern(conditionBlock.urlPattern);
80+
if (!this.args?.site.url || !pattern.test(this.args?.site.url)) {
81+
return false;
82+
}
83+
}
84+
85+
// All conditions are met
86+
return true;
87+
}
88+
89+
_matchDomainConditional(conditionBlock) {
90+
if (!conditionBlock.domain) return false;
91+
const domain = this.args?.site.domain;
92+
if (!domain) return false;
93+
if (Array.isArray(conditionBlock.domain)) {
94+
return conditionBlock.domain.some((domainRule) => {
95+
return matchHostname(domain, domainRule);
96+
});
97+
}
98+
return matchHostname(domain, conditionBlock.domain);
99+
}
100+
64101
/**
65102
* Return the settings object for a feature
66103
* @param {string} [featureName] - The name of the feature to get the settings for; defaults to the name of the feature
@@ -131,13 +168,14 @@ export default class ConfigFeature {
131168
*/
132169
getFeatureSetting(featureKeyName, featureName) {
133170
let result = this._getFeatureSettings(featureName);
134-
if (featureKeyName === 'domains') {
135-
throw new Error('domains is a reserved feature setting key name');
171+
if (featureKeyName in ['domains', 'conditionalChanges']) {
172+
throw new Error(`${featureKeyName} is a reserved feature setting key name`);
136173
}
137-
const domainMatch = [...this.matchDomainFeatureSetting('domains')].sort((a, b) => {
138-
return a.domain.length - b.domain.length;
139-
});
140-
for (const match of domainMatch) {
174+
// We only support one of these keys at a time, where conditionalChanges takes precedence
175+
// TODO should we rename these?
176+
// TODO should we only support conditionalChanges to support other types of settings?
177+
const conditionalMatch = this.matchDomainFeatureSetting('conditionalChanges') || this.matchDomainFeatureSetting('domains');
178+
for (const match of conditionalMatch) {
141179
if (match.patchSettings === undefined) {
142180
continue;
143181
}

injected/src/content-feature.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import ConfigFeature from './config-feature.js';
1717
/**
1818
* @typedef {object} Site
1919
* @property {string | null} domain
20+
* @property {URL | null} url
2021
* @property {boolean} [isBroken]
2122
* @property {boolean} [allowlisted]
2223
* @property {string[]} [enabledFeatures]

injected/src/utils.js

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,31 +118,40 @@ export function hasThirdPartyOrigin(scriptOrigins) {
118118
}
119119

120120
/**
121-
* Best guess effort of the tabs hostname; where possible always prefer the args.site.domain
122-
* @returns {string|null} inferred tab hostname
121+
* @returns {URL | null}
123122
*/
124-
export function getTabHostname() {
125-
let framingOrigin = null;
123+
export function getTabUrl() {
124+
let framingURLString = null;
126125
try {
127126
// @ts-expect-error - globalThis.top is possibly 'null' here
128-
framingOrigin = globalThis.top.location.href;
127+
framingURLString = globalThis.top.location.href;
129128
} catch {
130-
framingOrigin = globalThis.document.referrer;
129+
framingURLString = globalThis.document.referrer;
131130
}
132131

132+
// TODO validate if we need this, is it for Blob? Or data or something like that? My memory is fuzzy
133133
// Not supported in Firefox
134134
if ('ancestorOrigins' in globalThis.location && globalThis.location.ancestorOrigins.length) {
135135
// ancestorOrigins is reverse order, with the last item being the top frame
136-
framingOrigin = globalThis.location.ancestorOrigins.item(globalThis.location.ancestorOrigins.length - 1);
136+
framingURLString = globalThis.location.ancestorOrigins.item(globalThis.location.ancestorOrigins.length - 1);
137137
}
138138

139+
let framingURL;
139140
try {
140141
// @ts-expect-error - framingOrigin is possibly 'null' here
141-
framingOrigin = new URL(framingOrigin).hostname;
142+
framingURL = new URL(framingURLString)
142143
} catch {
143-
framingOrigin = null;
144+
framingURL = null;
144145
}
145-
return framingOrigin;
146+
return framingURL;
147+
}
148+
149+
/**
150+
* Best guess effort of the tabs hostname; where possible always prefer the args.site.domain
151+
* @returns {string|null} inferred tab hostname
152+
*/
153+
export function getTabHostname() {
154+
return getTabUrl()?.hostname || null;
146155
}
147156

148157
/**
@@ -524,6 +533,7 @@ export function computeLimitedSiteObject() {
524533
const topLevelHostname = getTabHostname();
525534
return {
526535
domain: topLevelHostname,
536+
url: getTabUrl()
527537
};
528538
}
529539

package-lock.json

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
"stylelint-csstree-validator": "^3.0.0",
5050
"typedoc": "^0.27.9",
5151
"typescript": "^5.8.2",
52-
"typescript-eslint": "^8.27.0"
52+
"typescript-eslint": "^8.27.0",
53+
"urlpattern-polyfill": "^10.0.0"
5354
},
5455
"dependencies": {
5556
"immutable-json-patch": "^6.0.1"

0 commit comments

Comments
 (0)