Skip to content

Commit a3fdbf2

Browse files
authored
Merge branch 'main' into max/dev-firefox-fix
2 parents e4085d6 + f467cc3 commit a3fdbf2

File tree

65 files changed

+921
-293
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+921
-293
lines changed

.github/workflows/asana.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ jobs:
1515
runs-on: ubuntu-latest
1616
steps:
1717
- uses: actions/checkout@v4
18-
- uses: sammacbeth/action-asana-sync@v6
18+
- uses: duckduckgo/action-asana-sync@v9
1919
with:
2020
ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
2121
ASANA_WORKSPACE_ID: ${{ secrets.ASANA_WORKSPACE_ID }}
2222
ASANA_PROJECT_ID: '1208598406046969'
23+
GITHUB_PAT: ${{ secrets.GH_RO_PAT }}
2324
USER_MAP: ${{ vars.USER_MAP }}
25+
ASSIGN_PR_AUTHOR: 'true'

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: 109 additions & 162 deletions
Large diffs are not rendered by default.

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

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -334,19 +334,9 @@ test.describe('Broker Protection communications', () => {
334334
});
335335
});
336336
test.describe('Executes action and sends success message', () => {
337-
test('buildUrl with useEnhancedCaptchaSystem: "enabled"', async ({ page }, workerInfo) => {
337+
test('buildUrl', async ({ page }, workerInfo) => {
338338
const dbp = BrokerProtectionPage.create(page, workerInfo.project.use);
339-
await dbp.withFeatureConfig(BROKER_PROTECTION_CONFIGS.enhancedCaptchaSystemEnabled);
340-
await dbp.navigatesTo('results.html');
341-
await dbp.receivesAction('navigate.json');
342-
const response = await dbp.collector.waitForMessage('actionCompleted');
343-
dbp.isSuccessMessage(response);
344-
dbp.isUrlMatch(response[0].payload.params.result.success.response);
345-
});
346-
347-
test('buildUrl with useEnhancedCaptchaSystem: "disabled"', async ({ page }, workerInfo) => {
348-
const dbp = BrokerProtectionPage.create(page, workerInfo.project.use);
349-
await dbp.withFeatureConfig(BROKER_PROTECTION_CONFIGS.enhancedCaptchaSystemDisabled);
339+
await dbp.withFeatureConfig(BROKER_PROTECTION_CONFIGS.default);
350340
await dbp.navigatesTo('results.html');
351341
await dbp.receivesAction('navigate.json');
352342
const response = await dbp.collector.waitForMessage('actionCompleted');
Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
11
import { createFeatureConfig } from '../mocks/broker-protection/feature-config';
22

33
export const BROKER_PROTECTION_CONFIGS = Object.freeze({
4-
enhancedCaptchaSystemEnabled: createFeatureConfig({
5-
state: 'enabled',
6-
settings: {
7-
useEnhancedCaptchaSystem: 'enabled',
8-
},
9-
}),
10-
enhancedCaptchaSystemDisabled: createFeatureConfig({
11-
state: 'enabled',
12-
settings: {
13-
useEnhancedCaptchaSystem: 'disabled',
14-
},
15-
}),
4+
default: createFeatureConfig(),
165
});

injected/integration-test/duckplayer-mobile-drawer.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ test.describe('Duck Player - Drawer UI variant', () => {
9494
await overlays.mobile.clicksOnVideoThumbnail();
9595
await overlays.pixels.sendsPixels([
9696
{ pixelName: 'overlay', params: {} },
97-
{ pixelName: 'play.do_not_use.thumbnail', params: {} },
97+
{ pixelName: 'play.do_not_use.dismiss', params: {} },
9898
]);
9999
await overlays.userSettingWasNotUpdated();
100100
});
@@ -112,7 +112,7 @@ test.describe('Duck Player - Drawer UI variant', () => {
112112
await overlays.mobile.clicksOnDrawerBackdrop();
113113
await overlays.pixels.sendsPixels([
114114
{ pixelName: 'overlay', params: {} },
115-
{ pixelName: 'play.do_not_use', params: {} },
115+
{ pixelName: 'play.do_not_use.dismiss', params: {} },
116116
]);
117117
await overlays.userSettingWasNotUpdated();
118118
});

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/mocks/broker-protection/feature-config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
* @param {'enabled' | 'disabled'} [brokerProtection.state] - optional state of the broker protection feature
44
* @param {string[]} [brokerProtection.exceptions] - optional list of exceptions
55
* @param {object} [brokerProtection.settings] - optional settings
6-
* @param {'enabled' | 'disabled'} [brokerProtection.settings.useEnhancedCaptchaSystem] - optional flag to use new action handlers
76
*/
87
export function createFeatureConfig(brokerProtection = {}) {
98
return {

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
},
3232
"devDependencies": {
3333
"@canvas/image-data": "^1.0.0",
34-
"@duckduckgo/privacy-configuration": "github:duckduckgo/privacy-configuration#main",
34+
"@duckduckgo/privacy-configuration": "github:duckduckgo/privacy-configuration#ca6101bb972756a87a8960ffb3029f603052ea9d",
3535
"@fingerprintjs/fingerprintjs": "^4.5.1",
3636
"@types/chrome": "^0.0.308",
3737
"@types/jasmine": "^5.1.7",

injected/src/features/broker-protection.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ export default class BrokerProtection extends ContentFeature {
5858
*/
5959
async exec(action, data) {
6060
const retryConfig = this.retryConfigFor(action);
61-
const options = { useEnhancedCaptchaSystem: this.getFeatureSettingEnabled('useEnhancedCaptchaSystem') };
62-
const { result, exceptions } = await retry(() => execute(action, data, document, options), retryConfig);
61+
const { result, exceptions } = await retry(() => execute(action, data, document), retryConfig);
6362

6463
if (result) {
6564
if ('success' in result && Array.isArray(result.success.next)) {
Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,6 @@
1-
import { extract } from './extract';
2-
import { fillForm } from './fill-form';
3-
import { click } from './click';
4-
import { expectation } from './expectation';
5-
import { navigate } from './navigate';
6-
import { buildUrl } from './build-url';
7-
import * as captchaHandlers from '../captcha-services/captcha.service';
8-
import * as deprecatedCaptchaHandlers from './captcha-deprecated';
9-
10-
/**
11-
* Returns the captcha handlers based on the useEnhancedCaptchaSystem flag
12-
* @param {Object} params
13-
* @param {boolean} params.useEnhancedCaptchaSystem
14-
*/
15-
export function resolveActionHandlers({ useEnhancedCaptchaSystem }) {
16-
return {
17-
extract,
18-
fillForm,
19-
click,
20-
expectation,
21-
...(useEnhancedCaptchaSystem
22-
? {
23-
navigate,
24-
...captchaHandlers,
25-
}
26-
: {
27-
navigate: buildUrl,
28-
...deprecatedCaptchaHandlers,
29-
}),
30-
};
31-
}
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 };

0 commit comments

Comments
 (0)