Skip to content

Commit d6e8803

Browse files
committed
feat(pir): add error object control flow (#1558)
1 parent 9a2dda1 commit d6e8803

File tree

10 files changed

+250
-105
lines changed

10 files changed

+250
-105
lines changed
Lines changed: 65 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getElement } from '../utils/utils.js';
22
import { removeUrlQueryParams } from '../utils/url.js';
3-
import { ErrorResponse, SuccessResponse } from '../types';
3+
import { ErrorResponse, PirError, SuccessResponse } from '../types';
44
import { getCaptchaProvider, getCaptchaSolveProvider } from './get-captcha-provider';
55
import { captchaFactory } from './providers/registry.js';
66
import { getCaptchaInfo as getCaptchaInfoDeprecated, solveCaptcha as solveCaptchaDeprecated } from '../actions/captcha-deprecated';
@@ -13,20 +13,18 @@ import { getCaptchaInfo as getCaptchaInfoDeprecated, solveCaptcha as solveCaptch
1313
*/
1414
export function getSupportingCodeToInject(action) {
1515
const { id: actionID, actionType, injectCaptchaHandler: captchaType } = action;
16+
const createError = ErrorResponse.generateErrorResponseFunction({ actionID, context: 'getSupportingCodeToInject' });
1617
if (!captchaType) {
1718
// ensures backward compatibility with old actions
18-
return new SuccessResponse({ actionID, actionType, response: {} });
19+
return SuccessResponse.create({ actionID, actionType, response: {} });
1920
}
2021

2122
const captchaProvider = captchaFactory.getProviderByType(captchaType);
2223
if (!captchaProvider) {
23-
return new ErrorResponse({
24-
actionID,
25-
message: `[injectCaptchaHandler] could not find captchaProvider with type ${captchaType}`,
26-
});
24+
return createError(`could not find captchaProvider with type ${captchaType}`);
2725
}
2826

29-
return new SuccessResponse({ actionID, actionType, response: { code: captchaProvider.getSupportingCodeToInject() } });
27+
return SuccessResponse.create({ actionID, actionType, response: { code: captchaProvider.getSupportingCodeToInject() } });
3028
}
3129

3230
/**
@@ -38,36 +36,38 @@ export function getSupportingCodeToInject(action) {
3836
*/
3937
export function getCaptchaInfo(action, root = document) {
4038
const { id: actionID, selector, actionType, captchaType } = action;
41-
try {
42-
if (!captchaType) {
43-
// ensures backward compatibility with old actions
44-
return getCaptchaInfoDeprecated(action, root);
45-
}
46-
47-
if (!selector) {
48-
throw new Error('missing selector');
49-
}
50-
51-
const captchaContainer = getElement(root, selector);
52-
if (!captchaContainer) {
53-
throw new Error(`could not find captchaContainer with selector ${selector}`);
54-
}
55-
56-
const captchaProvider = getCaptchaProvider(captchaContainer, captchaType);
57-
const captchaIdentifier = captchaProvider.getCaptchaIdentifier(captchaContainer);
58-
if (!captchaIdentifier) {
59-
throw new Error(`could not extract captcha identifier from ${captchaType} captcha`);
60-
}
61-
62-
const response = {
63-
url: removeUrlQueryParams(window.location.href), // query params (which may include PII)
64-
siteKey: captchaIdentifier,
65-
type: captchaProvider.getType(),
66-
};
67-
return new SuccessResponse({ actionID, actionType, response });
68-
} catch (e) {
69-
return new ErrorResponse({ actionID, message: `[getCaptchaInfo] ${e.message}` });
39+
if (!captchaType) {
40+
// ensures backward compatibility with old actions
41+
return getCaptchaInfoDeprecated(action, root);
42+
}
43+
44+
const createError = ErrorResponse.generateErrorResponseFunction({ actionID, context: `[getCaptchaInfo] captchaType: ${captchaType}` });
45+
if (!selector) {
46+
return createError('missing selector');
47+
}
48+
49+
const captchaContainer = getElement(root, selector);
50+
if (!captchaContainer) {
51+
return createError(`could not find captcha container with selector ${selector}`);
7052
}
53+
54+
const captchaProvider = getCaptchaProvider(captchaContainer, captchaType);
55+
if (PirError.isError(captchaProvider)) {
56+
return createError(captchaProvider.error.message);
57+
}
58+
59+
const captchaIdentifier = captchaProvider.getCaptchaIdentifier(captchaContainer);
60+
if (!captchaIdentifier) {
61+
return createError(`could not extract captcha identifier from the container with selector ${selector}`);
62+
}
63+
64+
const response = {
65+
url: removeUrlQueryParams(window.location.href), // query params (which may include PII)
66+
siteKey: captchaIdentifier,
67+
type: captchaProvider.getType(),
68+
};
69+
70+
return SuccessResponse.create({ actionID, actionType, response });
7171
}
7272

7373
/**
@@ -80,28 +80,34 @@ export function getCaptchaInfo(action, root = document) {
8080
*/
8181
export function solveCaptcha(action, token, root = document) {
8282
const { id: actionID, actionType, captchaType } = action;
83-
try {
84-
if (!captchaType) {
85-
// ensures backward compatibility with old actions
86-
return solveCaptchaDeprecated(action, token, root);
87-
}
88-
89-
const captchaSolveProvider = getCaptchaSolveProvider(root, captchaType);
90-
if (!captchaSolveProvider.canSolve(root)) {
91-
throw new Error(`[solveCaptcha] cannot solve captcha with type ${captchaType}`);
92-
}
93-
94-
const isTokenInjected = captchaSolveProvider.injectToken(root, token);
95-
if (!isTokenInjected) {
96-
throw new Error(`[solveCaptcha] could not inject token for captcha with type ${captchaType}`);
97-
}
98-
99-
return new SuccessResponse({
100-
actionID,
101-
actionType,
102-
response: { callback: { eval: captchaSolveProvider.getSolveCallback(token) } },
103-
});
104-
} catch (e) {
105-
return new ErrorResponse({ actionID, message: `[solveCaptcha] ${e.message}` });
83+
if (!captchaType) {
84+
// ensures backward compatibility with old actions
85+
return solveCaptchaDeprecated(action, token, root);
86+
}
87+
88+
const createError = ErrorResponse.generateErrorResponseFunction({ actionID, context: `[solveCaptcha] captchaType: ${captchaType}` });
89+
const captchaSolveProvider = getCaptchaSolveProvider(root, captchaType);
90+
91+
if (PirError.isError(captchaSolveProvider)) {
92+
return createError(captchaSolveProvider.error.message);
93+
}
94+
95+
if (!captchaSolveProvider.canSolve(root)) {
96+
return createError('cannot solve captcha');
97+
}
98+
99+
const tokenResponse = captchaSolveProvider.injectToken(root, token);
100+
if (PirError.isError(tokenResponse)) {
101+
return createError(tokenResponse.error.message);
106102
}
103+
104+
if (!tokenResponse.response.injected) {
105+
return createError('could not inject token');
106+
}
107+
108+
return SuccessResponse.create({
109+
actionID,
110+
actionType,
111+
response: { callback: { eval: captchaSolveProvider.getSolveCallback(token) } },
112+
});
107113
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export class CaptchaFactory {
3232
return this._getAllProviders().find((provider) => provider.isSupportedForElement(element)) || null;
3333
}
3434

35+
/**
36+
* Detect the captcha provider based on the root document
37+
* @param {Document} root - The root document
38+
* @returns {import('./providers/provider.interface').CaptchaProvider|null}
39+
*/
3540
detectSolveProvider(root) {
3641
return this._getAllProviders().find((provider) => provider.canSolve(root)) || null;
3742
}

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1+
import { PirError } from '../types';
12
import { captchaFactory } from './providers/registry';
23

34
/**
45
* Gets the captcha provider for the getCaptchaInfo action
56
*
67
* @param {HTMLElement} captchaDiv
78
* @param {string} captchaType
8-
* @return {import('./providers/provider.interface.js').CaptchaProvider}
9-
* @throws {Error}
109
*/
1110
export function getCaptchaProvider(captchaDiv, captchaType) {
1211
const captchaProvider = captchaFactory.getProviderByType(captchaType);
1312
if (!captchaProvider) {
14-
throw new Error(`[getCaptchaProvider] could not find captchaProvider with type ${captchaType}`);
13+
return PirError.create(`[getCaptchaProvider] could not find captchaProvider with type ${captchaType}`);
1514
}
1615

1716
if (captchaProvider.isSupportedForElement(captchaDiv)) {
@@ -25,7 +24,9 @@ export function getCaptchaProvider(captchaDiv, captchaType) {
2524
);
2625
const detectedProvider = captchaFactory.detectProvider(captchaDiv);
2726
if (!detectedProvider) {
28-
throw new Error(`[getCaptchaProvider] could not detect captcha provider for ${captchaType} captcha and element ${captchaDiv}`);
27+
return PirError.create(
28+
`[getCaptchaProvider] could not detect captcha provider for ${captchaType} captcha and element ${captchaDiv}`,
29+
);
2930
}
3031

3132
return detectedProvider;
@@ -35,13 +36,11 @@ export function getCaptchaProvider(captchaDiv, captchaType) {
3536
* Gets the captcha provider for the solveCaptcha action
3637
* @param {Document} root
3738
* @param {string} captchaType
38-
* @return {import('./providers/provider.interface.js').CaptchaProvider}
39-
* @throws {Error}
4039
*/
4140
export function getCaptchaSolveProvider(root, captchaType) {
4241
const captchaProvider = captchaFactory.getProviderByType(captchaType);
4342
if (!captchaProvider) {
44-
throw new Error(`[getCaptchaSolveProvider] could not find captchaProvider with type ${captchaType}`);
43+
return PirError.create(`[getCaptchaSolveProvider] could not find captchaProvider with type ${captchaType}`);
4544
}
4645

4746
if (captchaProvider.canSolve(root)) {
@@ -55,7 +54,9 @@ export function getCaptchaSolveProvider(root, captchaType) {
5554
);
5655
const detectedProvider = captchaFactory.detectSolveProvider(root);
5756
if (!detectedProvider) {
58-
throw new Error(`[getCaptchaSolveProvider] could not detect captcha provider for ${captchaType} captcha and element ${root}`);
57+
return PirError.create(
58+
`[getCaptchaSolveProvider] could not detect captcha provider for ${captchaType} captcha and element ${root}`,
59+
);
5960
}
6061

6162
return detectedProvider;

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { PirError } from '../../types';
2+
13
/**
24
* @import { CaptchaProvider } from './provider.interface';
35
* @implements {CaptchaProvider}
@@ -42,7 +44,7 @@ export class CloudFlareTurnstileProvider {
4244
*/
4345
injectToken(root, token) {
4446
// TODO: Implement
45-
return false;
47+
return PirError.create('Not implemented');
4648
}
4749

4850
/**

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { PirError } from '../../types';
12
import { getUrlHashParameter, getUrlParameter } from '../../utils/url';
23
import { getElementByName, getElementWithSrcStart } from '../../utils/utils';
34
import { stringifyFunction } from '../utils/stringify-function';
@@ -24,18 +25,16 @@ export class HCaptchaProvider {
2425

2526
/**
2627
* @param {HTMLElement} captchaContainerElement - The element to check
27-
* @returns {string|null} - The site key or null if not found
28-
* @throws {Error}
2928
*/
3029
getCaptchaIdentifier(captchaContainerElement) {
3130
const captchaElement = this._getCaptchaElement(captchaContainerElement);
3231
if (!captchaElement) {
33-
throw new Error('[getCaptchaIdentifier] could not find captcha');
32+
return PirError.create('[getCaptchaIdentifier] could not find captcha');
3433
}
3534

3635
const siteKey = this._getSiteKeyFromElement(captchaElement) || this._getSiteKeyFromUrl(captchaElement);
3736
if (!siteKey) {
38-
throw new Error('[HCaptchaProvider.getCaptchaIdentifier] could not extract site key');
37+
return PirError.create('[HCaptchaProvider.getCaptchaIdentifier] could not extract site key');
3938
}
4039

4140
return siteKey;
@@ -68,7 +67,6 @@ export class HCaptchaProvider {
6867
* Injects the token to solve the captcha
6968
* @param {Document} root - The document root
7069
* @param {string} token - The solved captcha token
71-
* @returns {boolean} - Whether the injection was successful
7270
*/
7371
injectToken(root, token) {
7472
return injectTokenIntoElement({ root, elementName: this.#CAPTCHA_RESPONSE_ELEMENT_NAME, token });
@@ -95,7 +93,6 @@ export class HCaptchaProvider {
9593
/**
9694
* @private
9795
* @param {HTMLElement} captchaElement
98-
* @returns {string|null} The site key or null if not found
9996
*/
10097
_getSiteKeyFromUrl(captchaElement) {
10198
if (!('src' in captchaElement)) {

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/**
22
* Base interface for captcha providers
3+
* @import {PirError, PirResponse} from '../../types';
34
* @interface
45
* @abstract
56
*/
@@ -27,7 +28,7 @@ export class CaptchaProvider {
2728
* Extracts the site key from the captcha container element
2829
* @abstract
2930
* @param {HTMLElement} captchaContainerElement - The element containing the captcha
30-
* @returns {string|null} The site key or null if not found
31+
* @returns {PirError | string | null} The site key or null if not found
3132
*/
3233
getCaptchaIdentifier(captchaContainerElement) {
3334
throw new Error('getCaptchaIdentifier() missing implementation');
@@ -56,7 +57,7 @@ export class CaptchaProvider {
5657
* @abstract
5758
* @param {Document} root - The document root containing the captcha
5859
* @param {string} token - The solved captcha token
59-
* @returns {boolean} True if the token was successfully injected
60+
* @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
6061
*/
6162
injectToken(root, token) {
6263
throw new Error('injectToken() missing implementation');
@@ -66,7 +67,7 @@ export class CaptchaProvider {
6667
* Creates a callback function to execute when the captcha is solved
6768
* @abstract
6869
* @param {string} token - The solved captcha token
69-
* @returns {string|null} Callback function to execute when the captcha is solved
70+
* @returns {PirError|string|null} Callback function to execute when the captcha is solved
7071
*/
7172
getSolveCallback(token) {
7273
throw new Error('getSolveCallback() missing implementation');

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { stringifyFunction } from '../utils/stringify-function';
44
import { injectTokenIntoElement } from '../utils/token';
55
// TODO move on the same folder level once we deprecate the existing captcha scripts
66
import { captchaCallback } from '../../actions/captcha-callback';
7+
import { safeCallWithError } from '../../utils/safe-call';
78

89
// define the config below to reuse it in the class
910
/**
@@ -43,10 +44,12 @@ export class ReCaptchaProvider {
4344

4445
/**
4546
* @param {HTMLElement} captchaContainerElement
46-
* @throws {Error}
4747
*/
4848
getCaptchaIdentifier(captchaContainerElement) {
49-
return getSiteKeyFromSearchParam({ captchaElement: this._getCaptchaElement(captchaContainerElement), siteKeyAttrName: 'k' });
49+
return safeCallWithError(
50+
() => getSiteKeyFromSearchParam({ captchaElement: this._getCaptchaElement(captchaContainerElement), siteKeyAttrName: 'k' }),
51+
{ errorMessage: '[ReCaptchaProvider.getCaptchaIdentifier] could not extract site key' },
52+
);
5053
}
5154

5255
getSupportingCodeToInject() {
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1-
import { safeCall } from '../../utils/safe-call';
1+
import { PirError, PirSuccess } from '../../types';
2+
import { safeCallWithError } from '../../utils/safe-call';
23
import { getElementByName } from '../../utils/utils';
34

45
/**
56
* Inject a token into a named element
7+
* @import { PirResponse } from '../../types';
68
* @param {object} params
79
* @param {Document} params.root - The document root
810
* @param {string} params.elementName - Name attribute of the element to inject into
911
* @param {string} params.token - The token to inject
10-
* @returns {boolean} - Whether the injection was successful
12+
* @returns {PirResponse<{ injected: boolean }>} - Whether the token was injected
1113
*/
1214
export function injectTokenIntoElement({ root, elementName, token }) {
1315
const element = getElementByName(root, elementName);
1416
if (!element) {
15-
return false;
17+
return PirError.create(`[injectTokenIntoElement] could not find element with name ${elementName}`);
1618
}
1719

18-
return (
19-
safeCall(
20-
() => {
21-
element.innerHTML = token;
22-
return true;
23-
},
24-
{ errorMessage: `[injectTokenIntoElement] error injecting token into element ${elementName}` },
25-
) ?? false
20+
return safeCallWithError(
21+
() => {
22+
element.innerHTML = token;
23+
return PirSuccess.create({ injected: true });
24+
},
25+
{ errorMessage: `[injectTokenIntoElement] error injecting token into element ${elementName}` },
2626
);
2727
}

0 commit comments

Comments
 (0)