Skip to content

Commit 1aed352

Browse files
Support navigation based conditional matching (#1610)
* Add patching changes * Rename matchDomainFeatureSetting to matchConditionalFeatureSetting Additionally ensure all checks match * Add in array matching of conditions and simplify logic to prevent errors in future * PoC handle navigation centrally * Fix missing pathname * Add test case and inline contentfeature * Move url change to before callbacks, add testing * Add back listener test case * Add navigation listener, make top frame only * Document the listenForUrlChanges * Simplify listening and site computation * Add test case for fallback logic
1 parent 2e56394 commit 1aed352

File tree

14 files changed

+242
-33
lines changed

14 files changed

+242
-33
lines changed

injected/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ The exposed API is a global called contentScopeFeatures and has three methods:
2323
- 'allowlisted' true if the user has disabled protections.
2424
- 'domain' the hostname of the site in the URL bar
2525
- 'enabledFeatures' this is an array of features/ to enable
26+
- urlChanged
27+
- Called when the top frame URL is changed (for Single Page Apps)
28+
- Also ensures that path changes for config 'conditional matching' are applied.
2629
- update
2730
- Calls the update method on all the features
2831

injected/integration-test/pages.spec.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,28 @@ test.describe('Test integration pages', () => {
2828
}
2929
}
3030

31+
test('Test infra', async ({ page }, testInfo) => {
32+
await testPage(
33+
page,
34+
testInfo,
35+
'/infra/pages/conditional-matching.html',
36+
'./integration-test/test-pages/infra/config/conditional-matching.json',
37+
);
38+
});
39+
40+
test('Test infra fallback', async ({ page }, testInfo) => {
41+
await page.addInitScript(() => {
42+
// This ensures that our fallback code applies and so we simulate other platforms than Chromium.
43+
delete globalThis.navigation;
44+
});
45+
await testPage(
46+
page,
47+
testInfo,
48+
'/infra/pages/conditional-matching.html',
49+
'./integration-test/test-pages/infra/config/conditional-matching.json',
50+
);
51+
});
52+
3153
test('Test manipulating APIs', async ({ page }, testInfo) => {
3254
await testPage(
3355
page,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"features": {
3+
"apiManipulation": {
4+
"state": "enabled",
5+
"settings": {
6+
"apiChanges": {
7+
"Navigator.prototype.hardwareConcurrency": {
8+
"type": "descriptor",
9+
"getterValue": {
10+
"type": "number",
11+
"value": 222
12+
}
13+
}
14+
},
15+
"conditionalChanges": [
16+
{
17+
"condition": {
18+
"urlPattern": "/test/*"
19+
},
20+
"patchSettings": [
21+
{
22+
"op": "replace",
23+
"path": "/apiChanges/Navigator.prototype.hardwareConcurrency/getterValue/value",
24+
"value": 333
25+
}
26+
]
27+
}
28+
]
29+
}
30+
}
31+
},
32+
"unprotectedTemporary": []
33+
}
34+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width">
6+
<title>API Interventions</title>
7+
<link rel="stylesheet" href="../shared/style.css">
8+
</head>
9+
<body>
10+
<p><a href="../../index.html">[Home]</a></p>
11+
<ul>
12+
<li><a href="./pages/conditional-matching.html">Conditional matching</a> - <a href="./config/conditional-matching.json">Config</a></li>
13+
</ul>
14+
</body>
15+
</html>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width">
6+
<title>Conditional Matching</title>
7+
<link rel="stylesheet" href="../../shared/style.css">
8+
</head>
9+
<body>
10+
<script src="../../shared/utils.js"></script>
11+
<p><a href="../index.html">[Infra]</a></p>
12+
13+
<p>This page verifies that APIs get modified</p>
14+
15+
<script>
16+
test('Conditional matching', async () => {
17+
const results = [
18+
{
19+
name: "APIs changing, expecting to always match",
20+
result: navigator.hardwareConcurrency,
21+
expected: 222
22+
}
23+
];
24+
const oldPathname = window.location.pathname;
25+
const newUrl = new URL(window.location.href);
26+
newUrl.pathname = "/test/test/path";
27+
window.history.pushState(null, '', newUrl.href);
28+
await new Promise(resolve => requestIdleCallback(resolve));
29+
results.push({
30+
name: "Expect URL to be changed",
31+
result: window.location.pathname,
32+
expected: '/test/test/path'
33+
})
34+
results.push({
35+
name: "APIs changing, expecting to match only when the URL is correct",
36+
result: navigator.hardwareConcurrency,
37+
expected: 333
38+
})
39+
const popStatePromise = new Promise(resolve => {
40+
window.addEventListener('popstate', resolve, { once: true });
41+
});
42+
// Call pop state to revert the URL
43+
window.history.back();
44+
await popStatePromise;
45+
results.push({
46+
name: "Expect URL to be reverted",
47+
result: window.location.pathname,
48+
expected: oldPathname
49+
})
50+
results.push({
51+
name: "APIs changing, expecting to match only when the URL is correct",
52+
result: navigator.hardwareConcurrency,
53+
expected: 222
54+
})
55+
56+
return results;
57+
});
58+
59+
60+
// eslint-disable-next-line no-undef
61+
renderResults();
62+
</script>
63+
</body>
64+
</html>

injected/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"test-int-x": "xvfb-run --server-args='-screen 0 1024x768x24' npm run test-int",
1717
"test-int-snapshots": "playwright test --grep '@screenshots'",
1818
"test-int-snapshots-update": "playwright test --grep '@screenshots' --update-snapshots --last-failed",
19-
"test": "npm run lint && npm run test-unit && npm run test-int && npm run playwright",
19+
"test": "npm run test-unit && npm run test-int && npm run playwright",
2020
"serve": "http-server -c-1 --port 3220 integration-test/test-pages",
2121
"playwright": "playwright test --grep-invert '@screenshots'",
2222
"playwright-screenshots": "playwright test --grep '@screenshots'",

injected/src/config-feature.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { immutableJSONPatch } from 'immutable-json-patch';
2-
import { camelcase, computeEnabledFeatures, matchHostname, parseFeatureSettings } from './utils.js';
2+
import { camelcase, computeEnabledFeatures, matchHostname, parseFeatureSettings, computeLimitedSiteObject } from './utils.js';
33
import { URLPattern } from 'urlpattern-polyfill';
44

55
export default class ConfigFeature {
@@ -29,6 +29,16 @@ export default class ConfigFeature {
2929
}
3030
}
3131

32+
/**
33+
* Call this when the top URL has changed, to recompute the site object.
34+
* This is used to update the path matching for urlPattern.
35+
*/
36+
recomputeSiteObject() {
37+
if (this.#args) {
38+
this.#args.site = computeLimitedSiteObject();
39+
}
40+
}
41+
3242
get args() {
3343
return this.#args;
3444
}

injected/src/content-feature.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export default class ContentFeature extends ConfigFeature {
3030
#messaging;
3131
/** @type {boolean} */
3232
#isDebugFlagSet = false;
33+
/**
34+
* Set this to true if you wish to listen to top level URL changes for config matching.
35+
* @type {boolean}
36+
*/
37+
listenForUrlChanges = false;
3338

3439
/** @type {ImportMeta} */
3540
#importConfig;

injected/src/content-scope-features.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { initStringExemptionLists, isFeatureBroken, isGloballyDisabled, platform
22
import { platformSupport } from './features';
33
import { PerformanceMonitor } from './performance';
44
import platformFeatures from 'ddg:platformFeatures';
5+
import { registerForURLChanges } from './url-change';
56

67
let initArgs = null;
78
const updates = [];
@@ -74,6 +75,16 @@ export async function init(args) {
7475
resolvedFeatures.forEach(({ featureInstance, featureName }) => {
7576
if (!isFeatureBroken(args, featureName) || alwaysInitExtensionFeatures(args, featureName)) {
7677
featureInstance.callInit(args);
78+
// Either listenForUrlChanges or urlChanged ensures the feature listens.
79+
if (featureInstance.listenForUrlChanges || featureInstance.urlChanged) {
80+
registerForURLChanges(() => {
81+
// The rationale for the two separate call here is to ensure that
82+
// extensions to the class don't need to call super.urlChanged()
83+
featureInstance.recomputeSiteObject();
84+
// Called if the feature instance has a urlChanged method
85+
featureInstance?.urlChanged();
86+
});
87+
}
7788
}
7889
});
7990
// Fire off updates that came in faster than the init

injected/src/features/api-manipulation.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { processAttr } from '../utils';
1313
* @internal
1414
*/
1515
export default class ApiManipulation extends ContentFeature {
16+
listenForUrlChanges = true;
17+
1618
init() {
1719
const apiChanges = this.getFeatureSetting('apiChanges');
1820
if (apiChanges) {
@@ -26,6 +28,10 @@ export default class ApiManipulation extends ContentFeature {
2628
}
2729
}
2830

31+
urlChanged() {
32+
this.init();
33+
}
34+
2935
/**
3036
* Checks if the config API change is valid.
3137
* @param {any} change

0 commit comments

Comments
 (0)