Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions injected/integration-test/pages.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ test.describe('Test integration pages', () => {
}
}

test('Test infra', async ({ page }, testInfo) => {
await testPage(
page,
testInfo,
'/infra/pages/conditional-matching.html',
'./integration-test/test-pages/infra/config/conditional-matching.json',
);
});

test('Test manipulating APIs', async ({ page }, testInfo) => {
await testPage(
page,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"features": {
"apiManipulation": {
"state": "enabled",
"settings": {
"apiChanges": {
"Navigator.prototype.hardwareConcurrency": {
"type": "descriptor",
"getterValue": {
"type": "number",
"value": 222
}
}
},
"conditionalChanges": [
{
"condition": {
"urlPattern": "/test/*"
},
"patchSettings": [
{
"op": "replace",
"path": "/apiChanges/Navigator.prototype.hardwareConcurrency/getterValue/value",
"value": 333
}
]
}
]
}
}
},
"unprotectedTemporary": []
}

15 changes: 15 additions & 0 deletions injected/integration-test/test-pages/infra/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>API Interventions</title>
<link rel="stylesheet" href="../shared/style.css">
</head>
<body>
<p><a href="../../index.html">[Home]</a></p>
<ul>
<li><a href="./pages/conditional-matching.html">Conditional matching</a> - <a href="./config/conditional-matching.json">Config</a></li>
</ul>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Conditional Matching</title>
<link rel="stylesheet" href="../../shared/style.css">
</head>
<body>
<script src="../../shared/utils.js"></script>
<p><a href="../index.html">[Infra]</a></p>

<p>This page verifies that APIs get modified</p>

<script>
test('Conditional matching', async () => {
const results = [
{
name: "APIs changing, expecting to always match",
result: navigator.hardwareConcurrency,
expected: 222
}
];

const newUrl = new URL(window.location.href);
newUrl.pathname = "/test/test/path";
window.history.pushState(null, '', newUrl.href);
await new Promise(resolve => requestIdleCallback(resolve));
results.push({
name: "Expect URL to be changed",
result: window.location.pathname,
expected: '/test/test/path'
})
results.push({
name: "APIs changing, expecting to match only when the URL is correct",
result: navigator.hardwareConcurrency,
expected: 333
})

return results;
});


// eslint-disable-next-line no-undef
renderResults();
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion injected/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"test-int-x": "xvfb-run --server-args='-screen 0 1024x768x24' npm run test-int",
"test-int-snapshots": "playwright test --grep '@screenshots'",
"test-int-snapshots-update": "playwright test --grep '@screenshots' --update-snapshots --last-failed",
"test": "npm run lint && npm run test-unit && npm run test-int && npm run playwright",
"test": "npm run test-unit && npm run test-int && npm run playwright",
"serve": "http-server -c-1 --port 3220 integration-test/test-pages",
"playwright": "playwright test --grep-invert '@screenshots'",
"playwright-screenshots": "playwright test --grep '@screenshots'",
Expand Down
9 changes: 9 additions & 0 deletions injected/src/config-feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ export default class ConfigFeature {
}
}

/**
* @param {import('./content-feature.js').Site} site
*/
urlChanged(site) {
if (this.#args) {
this.#args.site = site;
}
}

get args() {
return this.#args;
}
Expand Down
1 change: 1 addition & 0 deletions injected/src/content-feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default class ContentFeature extends ConfigFeature {
#messaging;
/** @type {boolean} */
#isDebugFlagSet = false;
listenForUrlChanges = false;

/** @type {ImportMeta} */
#importConfig;
Expand Down
4 changes: 4 additions & 0 deletions injected/src/content-scope-features.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { initStringExemptionLists, isFeatureBroken, isGloballyDisabled, platform
import { platformSupport } from './features';
import { PerformanceMonitor } from './performance';
import platformFeatures from 'ddg:platformFeatures';
import { registerForURLChanges } from './url-change';

let initArgs = null;
const updates = [];
Expand Down Expand Up @@ -74,6 +75,9 @@ export async function init(args) {
resolvedFeatures.forEach(({ featureInstance, featureName }) => {
if (!isFeatureBroken(args, featureName) || alwaysInitExtensionFeatures(args, featureName)) {
featureInstance.callInit(args);
if (featureInstance.listenForUrlChanges) {
registerForURLChanges((args) => featureInstance.urlChanged(args));
}
}
});
// Fire off updates that came in faster than the init
Expand Down
7 changes: 7 additions & 0 deletions injected/src/features/api-manipulation.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { processAttr } from '../utils';
* @internal
*/
export default class ApiManipulation extends ContentFeature {
listenForUrlChanges = true;

init() {
const apiChanges = this.getFeatureSetting('apiChanges');
if (apiChanges) {
Expand All @@ -26,6 +28,11 @@ export default class ApiManipulation extends ContentFeature {
}
}

urlChanged(site) {
super.urlChanged(site);
this.init();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means we'll rerun rules on navigation change.

}

/**
* Checks if the config API change is valid.
* @param {any} change
Expand Down
21 changes: 7 additions & 14 deletions injected/src/features/autofill-password-import.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ContentFeature from '../content-feature';
import { DDGProxy, DDGReflect, withExponentialBackoff } from '../utils';
import { withExponentialBackoff } from '../utils';

export const ANIMATION_DURATION_MS = 1000;
export const ANIMATION_ITERATIONS = Infinity;
Expand Down Expand Up @@ -34,6 +34,7 @@ export const DELAY_BEFORE_ANIMATION = 300;
* 3. Animate the element, or tap it if it should be autotapped.
*/
export default class AutofillPasswordImport extends ContentFeature {
listenForUrlChanges = true;
#exportButtonSettings;

#settingsButtonSettings;
Expand Down Expand Up @@ -490,23 +491,15 @@ export default class AutofillPasswordImport extends ContentFeature {
this.#settingsButtonSettings = this.getFeatureSetting('settingsButton');
}

urlChanged(site) {
this.handlePath(window.location.pathname);
super.urlChanged(site);
}

init() {
this.setButtonSettings();

const handlePath = this.handlePath.bind(this);
const historyMethodProxy = new DDGProxy(this, History.prototype, 'pushState', {
async apply(target, thisArg, args) {
const path = args[1] === '' ? args[2].split('?')[0] : args[1];
await handlePath(path);
return DDGReflect.apply(target, thisArg, args);
},
});
historyMethodProxy.overload();
// listen for popstate events in order to run on back/forward navigations
window.addEventListener('popstate', async () => {
const path = window.location.pathname;
await handlePath(path);
});

this.#domLoaded = new Promise((resolve) => {
if (document.readyState !== 'loading') {
Expand Down
24 changes: 10 additions & 14 deletions injected/src/features/element-hiding.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ContentFeature from '../content-feature';
import { isBeingFramed, DDGProxy, DDGReflect, injectGlobalStyles } from '../utils';
import { isBeingFramed, injectGlobalStyles } from '../utils';

let adLabelStrings = [];
const parser = new DOMParser();
Expand Down Expand Up @@ -302,6 +302,7 @@ function forgivingSelector(selector) {
}

export default class ElementHiding extends ContentFeature {
listenForUrlChanges = true;
init() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
featureInstance = this;
Expand Down Expand Up @@ -360,19 +361,14 @@ export default class ElementHiding extends ContentFeature {
} else {
applyRules(activeRules);
}
// single page applications don't have a DOMContentLoaded event on navigations, so
// we use proxy/reflect on history.pushState to call applyRules on page navigations
const historyMethodProxy = new DDGProxy(this, History.prototype, 'pushState', {
apply(target, thisArg, args) {
applyRules(activeRules);
return DDGReflect.apply(target, thisArg, args);
},
});
historyMethodProxy.overload();
// listen for popstate events in order to run on back/forward navigations
window.addEventListener('popstate', () => {
applyRules(activeRules);
});
this.activeRules = activeRules;
}

urlChanged(site) {
if (this.activeRules) {
this.applyRules(this.activeRules);
}
super.urlChanged(site);
}

/**
Expand Down
39 changes: 39 additions & 0 deletions injected/src/url-change.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { DDGProxy, DDGReflect, computeLimitedSiteObject } from './utils.js';
import ContentFeature from './content-feature.js';

const urlChangeListeners = new Set();
/**
* Register a listener to be called when the URL changes.
* @param {function} listener
*/
export function registerForURLChanges(listener) {
if (urlChangeListeners.size === 0) {
listenForURLChanges();
}
urlChangeListeners.add(listener);
}

function handleURLChange() {
const site = computeLimitedSiteObject();
for (const listener of urlChangeListeners) {
listener(site);
}
}

function listenForURLChanges() {
const urlChangedInstance = new ContentFeature('urlChanged', {}, {});
// single page applications don't have a DOMContentLoaded event on navigations, so
// we use proxy/reflect on history.pushState to call applyRules on page navigations
const historyMethodProxy = new DDGProxy(urlChangedInstance, History.prototype, 'pushState', {
apply(target, thisArg, args) {
const changeResult = DDGReflect.apply(target, thisArg, args);
handleURLChange();
return changeResult;
},
});
historyMethodProxy.overload();
// listen for popstate events in order to run on back/forward navigations
window.addEventListener('popstate', () => {
handleURLChange();
});
}
6 changes: 3 additions & 3 deletions injected/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -546,10 +546,10 @@ export function isUnprotectedDomain(topLevelHostname, featureList) {
* Used to inialize extension code in the load phase
*/
export function computeLimitedSiteObject() {
const topLevelHostname = getTabHostname();
const tabURL = getTabUrl();
return {
domain: topLevelHostname,
url: getTabUrl()?.href || null,
domain: tabURL?.hostname || null,
url: tabURL?.href || null,
};
}

Expand Down
Loading