Skip to content

Commit 43ff857

Browse files
Merge remote-tracking branch 'origin/main' into jkt/web-interventions-patching
2 parents 5629257 + f96c655 commit 43ff857

File tree

25 files changed

+697
-290
lines changed

25 files changed

+697
-290
lines changed

CODEOWNERS

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ injected/src/element-hiding.js @duckduckgo/content-scope-scripts-owners @jonatha
77
injected/src/features/click-to-load.js @duckduckgo/content-scope-scripts-owners @kzar @ladamski @franfaccin @jonathanKingston @shakyShane
88
injected/src/features/click-to-load/ @duckduckgo/content-scope-scripts-owners @kzar @ladamski @franfaccin @jonathanKingston @shakyShane
99
injected/src/locales/click-to-load/ @duckduckgo/content-scope-scripts-owners @kzar @ladamski @franfaccin @jonathanKingston @shakyShane
10-
injected/src/features/broker-protection.js @duckduckgo/content-scope-scripts-owners @brianhall @shakyShane
11-
injected/src/features/broker-protection/ @duckduckgo/content-scope-scripts-owners @brianhall @shakyShane
10+
injected/src/features/broker-protection.js @duckduckgo/content-scope-scripts-owners @brianhall @shakyShane @madblex
11+
injected/src/features/broker-protection/ @duckduckgo/content-scope-scripts-owners @brianhall @shakyShane @madblex
1212
injected/src/features/autofill-password-import.js @duckduckgo/content-scope-scripts-owners @dbajpeyi
1313

1414
# Platform owners

injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { test as base } from '@playwright/test';
22
import { createConfiguredDbpTest } from './fixtures';
3-
import { createGetRecaptchaInfoAction, createSolveRecaptchaAction } from '../mocks/broker-protection/captcha.js';
3+
import {
4+
createGetRecaptchaInfoAction,
5+
createSolveRecaptchaAction,
6+
createGetImageCaptchaInfoAction,
7+
createSolveImageCaptchaAction,
8+
} from '../mocks/broker-protection/captcha.js';
49
import { BROKER_PROTECTION_CONFIGS } from './tests-config.js';
510

611
const test = createConfiguredDbpTest(base);
@@ -99,4 +104,38 @@ test.describe('Broker Protection Captcha', () => {
99104
dbp.isQueryParamRemoved(sucessResponse);
100105
});
101106
});
107+
108+
test.describe('image captcha', () => {
109+
const imageCaptchaTargetPage = 'image-captcha.html';
110+
const imageCaptchaResponseSelector = '#svgCaptchaInputId';
111+
112+
test.describe('getCaptchaInfo', () => {
113+
test('returns the expected response for the correct action data', async ({ createConfiguredDbp }) => {
114+
const dbp = await createConfiguredDbp(BROKER_PROTECTION_CONFIGS.default);
115+
await dbp.navigatesTo(imageCaptchaTargetPage);
116+
await dbp.receivesInlineAction(createGetImageCaptchaInfoAction({ selector: '#svg-captcha-rendering svg' }));
117+
const sucessResponse = await dbp.getSuccessResponse();
118+
dbp.isCaptchaMatch(sucessResponse, { captchaType: 'image', targetPage: imageCaptchaTargetPage });
119+
});
120+
121+
test('returns an error response when the selector is not an svg or image tag', async ({ createConfiguredDbp }) => {
122+
const dbp = await createConfiguredDbp(BROKER_PROTECTION_CONFIGS.default);
123+
await dbp.navigatesTo(imageCaptchaTargetPage);
124+
await dbp.receivesInlineAction(createGetRecaptchaInfoAction({ selector: '#svg-captcha-rendering' }));
125+
126+
await dbp.isCaptchaError();
127+
});
128+
});
129+
130+
test.describe('solveCaptchaInfo', () => {
131+
test('solves the captcha for the correct action data', async ({ createConfiguredDbp }) => {
132+
const dbp = await createConfiguredDbp(BROKER_PROTECTION_CONFIGS.default);
133+
await dbp.navigatesTo(imageCaptchaTargetPage);
134+
await dbp.receivesInlineAction(createSolveImageCaptchaAction({ selector: imageCaptchaResponseSelector }));
135+
dbp.getSuccessResponse();
136+
137+
await dbp.isCaptchaTokenFilled(imageCaptchaResponseSelector);
138+
});
139+
});
140+
});
102141
});

injected/integration-test/mocks/broker-protection/captcha.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ export function createGetRecaptchaInfoAction(actionOverrides = {}) {
3434
});
3535
}
3636

37+
/**
38+
* @param {Partial<PirAction>} [actionOverrides]
39+
*/
40+
export function createGetImageCaptchaInfoAction(actionOverrides = {}) {
41+
return createGetCaptchaInfoAction({
42+
action: {
43+
captchaType: 'image',
44+
...actionOverrides,
45+
},
46+
});
47+
}
48+
3749
/**
3850
* @param {object} params
3951
* @param {Omit<PirAction, 'id' | 'actionType'>} params.action
@@ -66,6 +78,18 @@ export function createSolveRecaptchaAction(actionOverrides = {}) {
6678
});
6779
}
6880

81+
/**
82+
* @param {Partial<PirAction>} [actionOverrides]
83+
*/
84+
export function createSolveImageCaptchaAction(actionOverrides = {}) {
85+
return createSolveCaptchaAction({
86+
action: {
87+
captchaType: 'image',
88+
...actionOverrides,
89+
},
90+
});
91+
}
92+
6993
// Captcha responses
7094

7195
/**

injected/integration-test/page-objects/broker-protection.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ export class BrokerProtectionPage {
7070
* @return {Promise<void>}
7171
*/
7272
async isCaptchaTokenFilled(responseElementSelector) {
73-
const captchaTextArea = await this.page.$(responseElementSelector);
74-
const captchaToken = await captchaTextArea?.evaluate((element) => element.innerHTML);
73+
const captchaTarget = await this.page.$(responseElementSelector);
74+
const captchaToken = await captchaTarget?.evaluate((element) => ('value' in element ? element.value : element.innerHTML));
7575
expect(captchaToken).toBe('test_token');
7676
}
7777

@@ -99,7 +99,17 @@ export class BrokerProtectionPage {
9999
*/
100100
isCaptchaMatch(response, { captchaType, targetPage }) {
101101
const expectedResponse = createCaptchaResponse({ captchaType, targetPage });
102-
expect(response).toStrictEqual(expectedResponse);
102+
103+
switch (captchaType) {
104+
case 'image':
105+
// Validate that the correct keys are present in the response
106+
expect(Object.keys(response).sort()).toStrictEqual(Object.keys(expectedResponse).sort());
107+
// Validate that the siteKey looks like a base64 encoded image
108+
expect(response.siteKey).toMatch(/^data:image\/jpeg;base64,/);
109+
break;
110+
default:
111+
expect(response).toStrictEqual(expectedResponse);
112+
}
103113
}
104114

105115
async isCaptchaError() {

injected/integration-test/test-pages/broker-protection/pages/image-captcha.html

Lines changed: 66 additions & 0 deletions
Large diffs are not rendered by default.

injected/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"bundle-config": "node scripts/bundleConfig.mjs",
88
"bundle-entry-points": "node scripts/entry-points.js",
99
"build-chrome-mv3": "node scripts/entry-points.js",
10+
"build-firefox": "node scripts/entry-points.js",
1011
"build-locales": "node scripts/buildLocales.js",
1112
"build-types": "node scripts/types.mjs",
1213
"bundle-trackers": "node scripts/bundleTrackers.mjs --output ../build/tracker-lookup.json",
@@ -32,9 +33,9 @@
3233
"@canvas/image-data": "^1.0.0",
3334
"@duckduckgo/privacy-configuration": "github:duckduckgo/privacy-configuration#ca6101bb972756a87a8960ffb3029f603052ea9d",
3435
"@fingerprintjs/fingerprintjs": "^4.5.1",
35-
"@types/chrome": "^0.0.308",
36+
"@types/chrome": "^0.0.315",
3637
"@types/jasmine": "^5.1.7",
37-
"@types/node": "^22.13.9",
38+
"@types/node": "^22.14.1",
3839
"@typescript-eslint/eslint-plugin": "^8.20.0",
3940
"fast-check": "^3.23.2",
4041
"jasmine": "^5.6.0",
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
export { extract } from './extract';
2-
export { fillForm } from './fill-form';
3-
export { click } from './click';
4-
export { expectation } from './expectation';
5-
export { navigate } from './navigate';
6-
export { getCaptchaInfo, solveCaptcha } from '../captcha-services/captcha.service';
1+
export { extract } from './extract.js';
2+
export { fillForm } from './fill-form.js';
3+
export { click } from './click.js';
4+
export { expectation } from './expectation.js';
5+
export { navigate } from './navigate.js';
6+
export { getCaptchaInfo, solveCaptcha } from '../captcha-services/captcha.service.js';

injected/src/features/broker-protection/captcha-services/captcha.service.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ export function getSupportingCodeToInject(action) {
5151
*
5252
* @param {import('../types.js').PirAction} action
5353
* @param {Document | HTMLElement} root
54-
* @return {import('../types.js').ActionResponse}
54+
* @return {Promise<import('../types.js').ActionResponse>}
5555
*/
56-
export function getCaptchaInfo(action, root = document) {
56+
export async function getCaptchaInfo(action, root = document) {
5757
const { id: actionID, actionType, captchaType, selector } = action;
5858
if (!captchaType) {
5959
// ensures backward compatibility with old actions
@@ -72,7 +72,7 @@ export function getCaptchaInfo(action, root = document) {
7272
return createError(captchaProvider.error.message);
7373
}
7474

75-
const captchaIdentifier = captchaProvider.getCaptchaIdentifier(captchaContainer);
75+
const captchaIdentifier = await captchaProvider.getCaptchaIdentifier(captchaContainer);
7676
if (!captchaIdentifier) {
7777
return createError(`could not extract captcha identifier from the container with selector ${selector}`);
7878
}

injected/src/features/broker-protection/captcha-services/providers/cloudflare-turnstile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class CloudFlareTurnstileProvider {
2222
*/
2323
getCaptchaIdentifier(_captchaContainerElement) {
2424
// TODO: Implement
25-
return null;
25+
return Promise.resolve(null);
2626
}
2727

2828
getSupportingCodeToInject() {

injected/src/features/broker-protection/captcha-services/providers/hcaptcha.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class HCaptchaProvider {
2222
*/
2323
getCaptchaIdentifier(_captchaContainerElement) {
2424
// TODO: Implement
25-
return null;
25+
return Promise.resolve(null);
2626
}
2727

2828
getSupportingCodeToInject() {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { PirError } from '../../types';
2+
import { svgToBase64Jpg, imageToBase64 } from '../utils/image';
3+
import { injectTokenIntoElement } from '../utils/token';
4+
import { isElementType } from '../utils/element';
5+
import { stringifyFunction } from '../utils/stringify-function';
6+
7+
/**
8+
* @import { CaptchaProvider } from './provider.interface';
9+
* @implements {CaptchaProvider}
10+
*/
11+
export class ImageProvider {
12+
getType() {
13+
return 'image';
14+
}
15+
16+
/**
17+
* @param {HTMLElement} captchaImageElement - The captcha image element
18+
*/
19+
isSupportedForElement(captchaImageElement) {
20+
if (!captchaImageElement) {
21+
return false;
22+
}
23+
24+
return isElementType(captchaImageElement, ['img', 'svg']);
25+
}
26+
27+
/**
28+
* @param {HTMLElement} captchaImageElement - The captcha image element
29+
*/
30+
async getCaptchaIdentifier(captchaImageElement) {
31+
if (isSVGElement(captchaImageElement)) {
32+
return await svgToBase64Jpg(captchaImageElement);
33+
}
34+
35+
if (isImgElement(captchaImageElement)) {
36+
return imageToBase64(captchaImageElement);
37+
}
38+
39+
return PirError.create(
40+
`[ImageProvider.getCaptchaIdentifier] could not extract Base64 from image with tag name: ${captchaImageElement.tagName}`,
41+
);
42+
}
43+
44+
getSupportingCodeToInject() {
45+
return null;
46+
}
47+
48+
/**
49+
* @param {HTMLElement} captchaInputElement - The captcha input element
50+
*/
51+
canSolve(captchaInputElement) {
52+
return isElementType(captchaInputElement, ['input', 'textarea']);
53+
}
54+
55+
/**
56+
* @param {HTMLInputElement} captchaInputElement - The captcha input element
57+
* @param {string} token - The solved captcha token
58+
*/
59+
injectToken(captchaInputElement, token) {
60+
return injectTokenIntoElement({ captchaInputElement, token });
61+
}
62+
63+
/**
64+
* @param {HTMLElement} _captchaInputElement - The element containing the captcha
65+
* @param {string} _token - The solved captcha token
66+
*/
67+
getSolveCallback(_captchaInputElement, _token) {
68+
return stringifyFunction({
69+
functionBody: function callbackNoop() {},
70+
functionName: 'callbackNoop',
71+
args: {},
72+
});
73+
}
74+
}
75+
76+
/**
77+
* @param {HTMLElement} element
78+
* @return {element is SVGElement}
79+
*/
80+
function isSVGElement(element) {
81+
return isElementType(element, 'svg');
82+
}
83+
84+
/**
85+
* @param {HTMLElement} element
86+
* @return {element is HTMLImageElement}
87+
*/
88+
function isImgElement(element) {
89+
return isElementType(element, 'img');
90+
}

injected/src/features/broker-protection/captcha-services/providers/provider.interface.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ export class CaptchaProvider {
2828
* Extracts the site key from the captcha container element
2929
* @abstract
3030
* @param {HTMLElement} _captchaContainerElement - The element containing the captcha
31-
* @returns {PirError | string | null} The site key or null if not found
31+
* @returns {Promise<PirError | string | null>} The site key or null if not found
3232
*/
3333
getCaptchaIdentifier(_captchaContainerElement) {
34-
throw new Error('getCaptchaIdentifier() missing implementation');
34+
return Promise.reject(new Error('getCaptchaIdentifier() missing implementation'));
3535
}
3636

3737
/**

injected/src/features/broker-protection/captcha-services/providers/recaptcha.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@ export class ReCaptchaProvider {
4646
* @param {HTMLElement} captchaContainerElement
4747
*/
4848
getCaptchaIdentifier(captchaContainerElement) {
49-
return safeCallWithError(
50-
() => getSiteKeyFromSearchParam({ captchaElement: this._getCaptchaElement(captchaContainerElement), siteKeyAttrName: 'k' }),
51-
{ errorMessage: '[ReCaptchaProvider.getCaptchaIdentifier] could not extract site key' },
49+
return Promise.resolve(
50+
safeCallWithError(
51+
() => getSiteKeyFromSearchParam({ captchaElement: this._getCaptchaElement(captchaContainerElement), siteKeyAttrName: 'k' }),
52+
{ errorMessage: '[ReCaptchaProvider.getCaptchaIdentifier] could not extract site key' },
53+
),
5254
);
5355
}
5456

injected/src/features/broker-protection/captcha-services/providers/registry.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { CaptchaFactory } from '../factory';
22
import { ReCaptchaProvider } from './recaptcha';
3+
import { ImageProvider } from './image';
34

45
const captchaFactory = new CaptchaFactory();
56

@@ -19,4 +20,6 @@ captchaFactory.registerProvider(
1920
}),
2021
);
2122

23+
captchaFactory.registerProvider(new ImageProvider());
24+
2225
export { captchaFactory };
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
*
3+
* @param {HTMLElement} element - The element to check
4+
* @param {string | string[]} tag - The tag name(s) to check against
5+
* @returns {boolean} - True if the element is of the specified tag name(s), false otherwise
6+
*/
7+
export function isElementType(element, tag) {
8+
if (Array.isArray(tag)) {
9+
return tag.some((t) => isElementType(element, t));
10+
}
11+
12+
return element.tagName.toLowerCase() === tag.toLowerCase();
13+
}

0 commit comments

Comments
 (0)