@duckduckgo/autoconsent 14.97.0 → 15.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/build.sh +3 -0
- package/dist/addon-firefox/background.bundle.js +10 -1
- package/dist/addon-firefox/compact-rules.json +1 -1
- package/dist/addon-firefox/content.bundle.js +157 -40
- package/dist/addon-firefox/manifest.json +1 -1
- package/dist/addon-firefox/popup.bundle.js +190 -14
- package/dist/addon-firefox/popup.html +93 -8
- package/dist/addon-firefox/rule-index.json +1 -0
- package/dist/addon-firefox/rules.json +1 -1
- package/dist/addon-mv3/background.bundle.js +10 -1
- package/dist/addon-mv3/compact-rules.json +1 -1
- package/dist/addon-mv3/content.bundle.js +157 -40
- package/dist/addon-mv3/manifest.json +1 -1
- package/dist/addon-mv3/popup.bundle.js +190 -14
- package/dist/addon-mv3/popup.html +93 -8
- package/dist/addon-mv3/rule-index.json +1 -0
- package/dist/addon-mv3/rules.json +1 -1
- package/dist/autoconsent.cjs.js +157 -40
- package/dist/autoconsent.esm.js +157 -40
- package/dist/autoconsent.extra.cjs.js +157 -40
- package/dist/autoconsent.extra.esm.js +157 -40
- package/dist/autoconsent.playwright.js +157 -40
- package/dist/autoconsent.standalone.js +4138 -0
- package/dist/types/cmps/base.d.ts +4 -2
- package/dist/types/cmps/trustarc-top.d.ts +1 -1
- package/dist/types/eval-handler.d.ts +3 -3
- package/dist/types/heuristic-patterns.d.ts +3 -0
- package/dist/types/heuristics.d.ts +4 -7
- package/dist/types/types.d.ts +18 -2
- package/lib/cmps/base.ts +20 -7
- package/lib/cmps/conversant.ts +7 -4
- package/lib/cmps/trustarc-top.ts +1 -1
- package/lib/cmps/uniconsent.ts +4 -4
- package/lib/eval-handler.ts +6 -3
- package/lib/heuristic-patterns.ts +68 -1
- package/lib/heuristics.ts +76 -27
- package/lib/types.ts +20 -2
- package/lib/utils.ts +13 -6
- package/lib/web.ts +3 -1
- package/package.json +1 -1
- package/rules/autoconsent/twitch.json +1 -1
- package/rules/build.ts +4 -0
- package/rules/compact-rules.json +1 -1
- package/rules/rule-index-builder.ts +63 -0
- package/rules/rule-index.json +1 -0
- package/rules/rules.json +1 -1
- package/standalone/content.ts +103 -0
- package/tests-wtr/heuristics/get-actionable-popups.html +16 -0
- package/tests-wtr/heuristics/get-actionable-popups.ts +85 -6
- package/tests-wtr/heuristics/heuristic-cmp.html +69 -0
- package/tests-wtr/heuristics/heuristic-cmp.ts +178 -0
- package/tests-wtr/heuristics/heuristics-utils.test.ts +96 -28
- package/tests-wtr/lifecycle/find-cmp.html +14 -0
- package/tests-wtr/lifecycle/find-cmp.ts +44 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AutoCMP, DomActionsProvider, PopupData } from '../types';
|
|
1
|
+
import { AutoCMP, DomActionsProvider, PopupData, PopupHandlingMode } from '../types';
|
|
2
2
|
import { AutoConsentCMPRule, AutoConsentRuleStep, ElementSelector, HideMethod, RunContext, VisibilityCheck } from '../rules';
|
|
3
3
|
import AutoConsent from '../web';
|
|
4
4
|
import { snippets } from '../eval-snippets';
|
|
@@ -62,12 +62,14 @@ export declare class AutoConsentCMP extends AutoConsentCMPBase {
|
|
|
62
62
|
}
|
|
63
63
|
export declare class AutoConsentHeuristicCMP extends AutoConsentCMPBase {
|
|
64
64
|
popups: PopupData[];
|
|
65
|
-
|
|
65
|
+
mode: PopupHandlingMode;
|
|
66
|
+
constructor(autoconsentInstance: AutoConsent, mode?: PopupHandlingMode);
|
|
66
67
|
get hasSelfTest(): boolean;
|
|
67
68
|
get isIntermediate(): boolean;
|
|
68
69
|
get isCosmetic(): boolean;
|
|
69
70
|
detectCmp(): Promise<boolean>;
|
|
70
71
|
detectPopup(): Promise<boolean>;
|
|
72
|
+
getTargetButton(): import("../types").ButtonData | undefined;
|
|
71
73
|
optOut(): Promise<boolean>;
|
|
72
74
|
optIn(): Promise<boolean>;
|
|
73
75
|
openCmp(): Promise<boolean>;
|
|
@@ -5,7 +5,7 @@ export default class TrustArcTop extends AutoConsentCMPBase {
|
|
|
5
5
|
name: string;
|
|
6
6
|
prehideSelectors: string[];
|
|
7
7
|
runContext: RunContext;
|
|
8
|
-
_shortcutButton: HTMLElement;
|
|
8
|
+
_shortcutButton: HTMLElement | null;
|
|
9
9
|
_optInDone: boolean;
|
|
10
10
|
constructor(autoconsentInstance: AutoConsent);
|
|
11
11
|
get hasSelfTest(): boolean;
|
|
@@ -3,14 +3,14 @@ import { ContentScriptMessage } from './messages';
|
|
|
3
3
|
declare class Deferred<T> {
|
|
4
4
|
id: string;
|
|
5
5
|
promise: Promise<T>;
|
|
6
|
-
resolve: (value
|
|
7
|
-
reject: (reason?:
|
|
6
|
+
resolve: (value: T) => void;
|
|
7
|
+
reject: (reason?: unknown) => void;
|
|
8
8
|
timer: number;
|
|
9
9
|
constructor(id: string, timeout?: number);
|
|
10
10
|
}
|
|
11
11
|
type EvalState = {
|
|
12
12
|
pending: Map<string, Deferred<boolean>>;
|
|
13
|
-
sendContentMessage: (message: ContentScriptMessage) => void;
|
|
13
|
+
sendContentMessage: ((message: ContentScriptMessage) => void) | null;
|
|
14
14
|
};
|
|
15
15
|
export declare const evalState: EvalState;
|
|
16
16
|
export declare function requestEval(code: string, snippetId?: keyof typeof snippets): Promise<boolean>;
|
|
@@ -4,3 +4,6 @@ export declare const DETECT_PATTERNS: RegExp[];
|
|
|
4
4
|
*/
|
|
5
5
|
export declare const REJECT_PATTERNS: (string | RegExp)[];
|
|
6
6
|
export declare const NEVER_MATCH_PATTERNS: RegExp[];
|
|
7
|
+
export declare const SETTINGS_PATTERNS: (string | RegExp)[];
|
|
8
|
+
export declare const ACCEPT_PATTERNS: (string | RegExp)[];
|
|
9
|
+
export declare const ACKNOWLEDGE_PATTERNS: (string | RegExp)[];
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import { ButtonData, PopupData } from './types';
|
|
1
|
+
import { ButtonData, ButtonRegexClassification, PopupData, PopupHandlingMode } from './types';
|
|
2
2
|
export declare function checkHeuristicPatterns(allText: string, detectPatterns?: RegExp[]): {
|
|
3
3
|
patterns: string[];
|
|
4
4
|
snippets: string[];
|
|
5
5
|
};
|
|
6
|
-
export declare function getActionablePopups(timeout?: number): PopupData[];
|
|
7
|
-
export declare function classifyButtons(buttons: ButtonData[]):
|
|
8
|
-
rejectButtons: ButtonData[];
|
|
9
|
-
otherButtons: ButtonData[];
|
|
10
|
-
};
|
|
11
|
-
export declare function isRejectButton(buttonText: string, rejectPatterns?: (string | RegExp)[], neverMatchPatterns?: RegExp[]): boolean;
|
|
6
|
+
export declare function getActionablePopups(mode?: PopupHandlingMode, timeout?: number): PopupData[];
|
|
7
|
+
export declare function classifyButtons(buttons: ButtonData[]): void;
|
|
12
8
|
export declare function cleanButtonText(buttonText: string): string;
|
|
9
|
+
export declare function classifyButtonTextRegex(buttonText: string): ButtonRegexClassification;
|
|
13
10
|
export declare function isDialogLikeElement(node: HTMLElement): boolean;
|
|
14
11
|
/**
|
|
15
12
|
* Serialize all actionable buttons on the page
|
package/dist/types/types.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ export type Config = {
|
|
|
74
74
|
};
|
|
75
75
|
performanceLoggingEnabled: boolean;
|
|
76
76
|
heuristicPopupSearchTimeout: number;
|
|
77
|
+
heuristicMode: PopupHandlingMode;
|
|
77
78
|
};
|
|
78
79
|
export type LifecycleState = 'loading' | 'initialized' | 'waitingForInitResponse' | 'started' | 'nothingDetected' | 'cosmeticFiltersDetected' | 'cmpDetected' | 'openPopupDetected' | 'runningOptOut' | 'runningOptIn' | 'optOutSucceeded' | 'optOutFailed' | 'optInSucceeded' | 'optInFailed' | 'done';
|
|
79
80
|
export type ConsentState = {
|
|
@@ -92,14 +93,29 @@ export type ConsentState = {
|
|
|
92
93
|
endTime: number;
|
|
93
94
|
performance?: Record<string, number[]>;
|
|
94
95
|
};
|
|
96
|
+
export type ButtonRegexClassification = 'reject' | 'settings' | 'accept' | 'acknowledge' | 'other';
|
|
95
97
|
export interface ButtonData {
|
|
96
98
|
text: string;
|
|
97
99
|
element: HTMLElement;
|
|
100
|
+
regexClassification?: ButtonRegexClassification;
|
|
98
101
|
}
|
|
99
102
|
export interface PopupData {
|
|
100
103
|
text: string;
|
|
101
104
|
element: HTMLElement;
|
|
102
105
|
buttons: ButtonData[];
|
|
103
|
-
|
|
104
|
-
otherButtons?: ButtonData[];
|
|
106
|
+
regexClassification?: PopupHandlingMode;
|
|
105
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Controls the behavior of the heuristic popup detection.
|
|
110
|
+
* - Reject: Will click the reject button on a popup if it exists.
|
|
111
|
+
* - Tier1: Will also click the acknowledge button on a popup if it exists, and no Reject button exists.
|
|
112
|
+
* - Tier2: Will also click the accept button on a popup if it exists, and no Reject or Acknowledge button exists.
|
|
113
|
+
* - None: Disabled
|
|
114
|
+
*/
|
|
115
|
+
export declare const PopupHandlingModes: {
|
|
116
|
+
readonly None: -1;
|
|
117
|
+
readonly Reject: 0;
|
|
118
|
+
readonly Tier1: 1;
|
|
119
|
+
readonly Tier2: 2;
|
|
120
|
+
};
|
|
121
|
+
export type PopupHandlingMode = (typeof PopupHandlingModes)[keyof typeof PopupHandlingModes];
|
package/lib/cmps/base.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AutoCMP, DomActionsProvider, PopupData } from '../types';
|
|
1
|
+
import { AutoCMP, DomActionsProvider, PopupData, PopupHandlingMode, PopupHandlingModes } from '../types';
|
|
2
2
|
import { AutoConsentCMPRule, AutoConsentRuleStep, ElementSelector, HideMethod, RunContext, VisibilityCheck } from '../rules';
|
|
3
3
|
import { requestEval } from '../eval-handler';
|
|
4
4
|
import AutoConsent from '../web';
|
|
@@ -419,14 +419,16 @@ export class AutoConsentCMP extends AutoConsentCMPBase {
|
|
|
419
419
|
|
|
420
420
|
export class AutoConsentHeuristicCMP extends AutoConsentCMPBase {
|
|
421
421
|
popups: PopupData[] = [];
|
|
422
|
+
mode: PopupHandlingMode;
|
|
422
423
|
|
|
423
|
-
constructor(autoconsentInstance: AutoConsent) {
|
|
424
|
+
constructor(autoconsentInstance: AutoConsent, mode: PopupHandlingMode = PopupHandlingModes.Reject) {
|
|
424
425
|
super(autoconsentInstance);
|
|
425
426
|
this.name = 'HEURISTIC';
|
|
426
427
|
this.runContext = {
|
|
427
428
|
main: true,
|
|
428
429
|
frame: false, // do not run in iframes for security reasons
|
|
429
430
|
} as RunContext;
|
|
431
|
+
this.mode = mode;
|
|
430
432
|
}
|
|
431
433
|
|
|
432
434
|
get hasSelfTest(): boolean {
|
|
@@ -445,11 +447,13 @@ export class AutoConsentHeuristicCMP extends AutoConsentCMPBase {
|
|
|
445
447
|
// wait for one tick to deprioritize heavy DOM operations
|
|
446
448
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
447
449
|
this.autoconsent.config.performanceLoggingEnabled && performance.mark('heuristicDetectorStart');
|
|
448
|
-
this.popups = getActionablePopups(this.autoconsent.config.heuristicPopupSearchTimeout);
|
|
450
|
+
this.popups = getActionablePopups(this.mode, this.autoconsent.config.heuristicPopupSearchTimeout);
|
|
449
451
|
this.autoconsent.config.performanceLoggingEnabled && performance.mark('heuristicDetectorEnd');
|
|
450
452
|
this.autoconsent.config.performanceLoggingEnabled &&
|
|
451
453
|
performance.measure('heuristicDetector', 'heuristicDetectorStart', 'heuristicDetectorEnd');
|
|
454
|
+
|
|
452
455
|
if (this.popups.length > 0) {
|
|
456
|
+
this.name = `HEURISTIC-TIER${this.popups[0].regexClassification}`;
|
|
453
457
|
return Promise.resolve(true);
|
|
454
458
|
}
|
|
455
459
|
return Promise.resolve(false);
|
|
@@ -457,17 +461,26 @@ export class AutoConsentHeuristicCMP extends AutoConsentCMPBase {
|
|
|
457
461
|
|
|
458
462
|
async detectPopup() {
|
|
459
463
|
if (this.popups.length > 0) {
|
|
460
|
-
if (this.popups.length > 1
|
|
461
|
-
this.autoconsent.config.logs.errors && console.warn('Heuristic found multiple
|
|
464
|
+
if (this.popups.length > 1) {
|
|
465
|
+
this.autoconsent.config.logs.errors && console.warn('Heuristic found multiple popups');
|
|
462
466
|
}
|
|
463
467
|
return true;
|
|
464
468
|
}
|
|
465
469
|
return false;
|
|
466
470
|
}
|
|
467
471
|
|
|
472
|
+
getTargetButton() {
|
|
473
|
+
const popup = this.popups[0];
|
|
474
|
+
const level = popup.regexClassification;
|
|
475
|
+
const buttons = popup.buttons;
|
|
476
|
+
const targetButtonType =
|
|
477
|
+
level === PopupHandlingModes.Reject ? 'reject' : level === PopupHandlingModes.Tier1 ? 'acknowledge' : 'accept';
|
|
478
|
+
return buttons.find((button) => button.regexClassification === targetButtonType);
|
|
479
|
+
}
|
|
480
|
+
|
|
468
481
|
optOut(): Promise<boolean> {
|
|
469
482
|
// use only the first found popup candidate
|
|
470
|
-
const button = this.
|
|
483
|
+
const button = this.getTargetButton();
|
|
471
484
|
if (button) {
|
|
472
485
|
return this.clickElement(button.element);
|
|
473
486
|
}
|
|
@@ -483,7 +496,7 @@ export class AutoConsentHeuristicCMP extends AutoConsentCMPBase {
|
|
|
483
496
|
}
|
|
484
497
|
|
|
485
498
|
async test(): Promise<boolean> {
|
|
486
|
-
const button = this.
|
|
499
|
+
const button = this.getTargetButton();
|
|
487
500
|
if (button) {
|
|
488
501
|
await this.wait(500);
|
|
489
502
|
return !isElementVisible(button.element);
|
package/lib/cmps/conversant.ts
CHANGED
|
@@ -41,12 +41,15 @@ export default class Conversant extends AutoConsentCMPBase {
|
|
|
41
41
|
(<HTMLElement>item.querySelector('.cmp-accordion-item-title')).click();
|
|
42
42
|
await waitFor(() => !!item.querySelector('.cmp-accordion-item-content.cmp-active'), 10, 50);
|
|
43
43
|
const content = item.querySelector('.cmp-accordion-item-content.cmp-active');
|
|
44
|
+
if (!content) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
44
47
|
content
|
|
45
|
-
.querySelectorAll('.cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)')
|
|
46
|
-
.forEach((e
|
|
48
|
+
.querySelectorAll<HTMLElement>('.cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)')
|
|
49
|
+
.forEach((e) => e.click());
|
|
47
50
|
content
|
|
48
|
-
.querySelectorAll('.cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)')
|
|
49
|
-
.forEach((e
|
|
51
|
+
.querySelectorAll<HTMLElement>('.cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)')
|
|
52
|
+
.forEach((e) => e.click());
|
|
50
53
|
// await waitFor(() => !item.querySelector('.cmp-toggle-deny--active,.cmp-toggle-checkbox--active'), 5, 50); // this may take a long time
|
|
51
54
|
}
|
|
52
55
|
await this.click('.cmp-main-button:not(.cmp-main-button--primary)');
|
package/lib/cmps/trustarc-top.ts
CHANGED
package/lib/cmps/uniconsent.ts
CHANGED
|
@@ -29,8 +29,8 @@ export default class Uniconsent extends AutoConsentCMPBase {
|
|
|
29
29
|
|
|
30
30
|
async optOut() {
|
|
31
31
|
await this.waitForElement('.unic button', 1000);
|
|
32
|
-
document.querySelectorAll('.unic button').forEach((button
|
|
33
|
-
const text = button.textContent;
|
|
32
|
+
document.querySelectorAll<HTMLButtonElement>('.unic button').forEach((button) => {
|
|
33
|
+
const text = button.textContent || '';
|
|
34
34
|
if (text.includes('Manage Options') || text.includes('Optionen verwalten')) {
|
|
35
35
|
button.click();
|
|
36
36
|
}
|
|
@@ -39,14 +39,14 @@ export default class Uniconsent extends AutoConsentCMPBase {
|
|
|
39
39
|
if (await this.waitForElement('.unic input[type=checkbox]', 1000)) {
|
|
40
40
|
await this.waitForElement('.unic button', 1000);
|
|
41
41
|
|
|
42
|
-
document.querySelectorAll('.unic input[type=checkbox]').forEach((c
|
|
42
|
+
document.querySelectorAll<HTMLInputElement>('.unic input[type=checkbox]').forEach((c) => {
|
|
43
43
|
if (c.checked) {
|
|
44
44
|
c.click();
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
for (const b of <NodeListOf<HTMLButtonElement>>document.querySelectorAll('.unic button')) {
|
|
49
|
-
const text = b.textContent;
|
|
49
|
+
const text = b.textContent || '';
|
|
50
50
|
for (const pattern of ['Confirm Choices', 'Save Choices', 'Auswahl speichern']) {
|
|
51
51
|
if (text.includes(pattern)) {
|
|
52
52
|
b.click();
|
package/lib/eval-handler.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { getRandomID } from './random';
|
|
|
5
5
|
class Deferred<T> {
|
|
6
6
|
id: string;
|
|
7
7
|
promise: Promise<T>;
|
|
8
|
-
resolve
|
|
9
|
-
reject
|
|
8
|
+
resolve!: (value: T) => void;
|
|
9
|
+
reject!: (reason?: unknown) => void;
|
|
10
10
|
timer: number;
|
|
11
11
|
|
|
12
12
|
constructor(id: string, timeout = 1000) {
|
|
@@ -23,7 +23,7 @@ class Deferred<T> {
|
|
|
23
23
|
|
|
24
24
|
type EvalState = {
|
|
25
25
|
pending: Map<string, Deferred<boolean>>;
|
|
26
|
-
sendContentMessage: (message: ContentScriptMessage) => void;
|
|
26
|
+
sendContentMessage: ((message: ContentScriptMessage) => void) | null;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
export const evalState: EvalState = {
|
|
@@ -32,6 +32,9 @@ export const evalState: EvalState = {
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
export function requestEval(code: string, snippetId?: keyof typeof snippets): Promise<boolean> {
|
|
35
|
+
if (!evalState.sendContentMessage) {
|
|
36
|
+
return Promise.reject(new Error('AutoConsent is not initialized yet'));
|
|
37
|
+
}
|
|
35
38
|
const id = getRandomID();
|
|
36
39
|
evalState.sendContentMessage({
|
|
37
40
|
type: 'eval',
|
|
@@ -115,14 +115,19 @@ const REJECT_PATTERNS_ENGLISH = [
|
|
|
115
115
|
|
|
116
116
|
// e.g. "do not sell or share my personal information", "do not sell my personal information"
|
|
117
117
|
// often used in CCPA
|
|
118
|
-
/^\s*do\s+not\s+sell(\s+or\s+share)?\s*my\s*personal\s*
|
|
118
|
+
/^\s*do\s+not\s+sell(\s+or\s+share)?\s*my\s*personal\s*info(rmation)?\s*$/is,
|
|
119
119
|
|
|
120
120
|
'allow selection',
|
|
121
121
|
'disagree and close',
|
|
122
122
|
|
|
123
123
|
// These are impactful, but look error-prone
|
|
124
|
+
// // e.g. "disagree"
|
|
125
|
+
/^(i)?\s*disagree\s*(and\s+close)?$/i,
|
|
124
126
|
// // e.g. "i do not agree"
|
|
125
127
|
// /^\s*(i\s+)?do\s+not\s+agree\s*$/i,
|
|
128
|
+
'no',
|
|
129
|
+
/^no,? thanks$/is,
|
|
130
|
+
/^opt[ -]out$/is,
|
|
126
131
|
];
|
|
127
132
|
|
|
128
133
|
const REJECT_PATTERNS_DUTCH = [
|
|
@@ -715,3 +720,65 @@ export const NEVER_MATCH_PATTERNS = [
|
|
|
715
720
|
/sostienici/is,
|
|
716
721
|
/suscribir/is,
|
|
717
722
|
];
|
|
723
|
+
|
|
724
|
+
export const SETTINGS_PATTERNS = [
|
|
725
|
+
'settings',
|
|
726
|
+
'preferences',
|
|
727
|
+
/customi(s|z)e/is,
|
|
728
|
+
'show details',
|
|
729
|
+
'more options',
|
|
730
|
+
/(manage|configure) (my|your) (preferences|choices|cookies)/is,
|
|
731
|
+
'manage choices',
|
|
732
|
+
/(cookie )?preference center/is,
|
|
733
|
+
'change settings',
|
|
734
|
+
'configure',
|
|
735
|
+
'change my preferences',
|
|
736
|
+
'cookie manager',
|
|
737
|
+
'cookie preference',
|
|
738
|
+
'let me choose',
|
|
739
|
+
'cookieconsent preferences',
|
|
740
|
+
/privacy choices/is,
|
|
741
|
+
/(privacy|cookie|custom) settings/is,
|
|
742
|
+
/cookies? (settings|preferences|setting)/is,
|
|
743
|
+
/(manage|customize|customise|opt-out|edit).*(cookies|preferences|settings|options)/is,
|
|
744
|
+
'cookie consent options',
|
|
745
|
+
'privacy controls',
|
|
746
|
+
'show purposes',
|
|
747
|
+
// German
|
|
748
|
+
'einstellungen',
|
|
749
|
+
];
|
|
750
|
+
|
|
751
|
+
export const ACCEPT_PATTERNS = [
|
|
752
|
+
/^(accept|allow)( all)?( cookies)?$/is,
|
|
753
|
+
/i (accept|allow)( all)?/is,
|
|
754
|
+
'yes',
|
|
755
|
+
/^(i )?agree$/is,
|
|
756
|
+
'continue with all',
|
|
757
|
+
'accept and continue',
|
|
758
|
+
/accept all above/is,
|
|
759
|
+
/^(accept|agree) and close/is,
|
|
760
|
+
'accept continue',
|
|
761
|
+
'agree proceed',
|
|
762
|
+
'allow and continue',
|
|
763
|
+
'close and accept',
|
|
764
|
+
/accept all$/is,
|
|
765
|
+
'im ok with that',
|
|
766
|
+
'accept optional cookies',
|
|
767
|
+
|
|
768
|
+
/^alle (cookies )?akzeptieren$/is,
|
|
769
|
+
];
|
|
770
|
+
|
|
771
|
+
export const ACKNOWLEDGE_PATTERNS = [
|
|
772
|
+
'ok',
|
|
773
|
+
'close',
|
|
774
|
+
'continue',
|
|
775
|
+
'x',
|
|
776
|
+
/^got it!?$/,
|
|
777
|
+
'i understand',
|
|
778
|
+
'dismiss',
|
|
779
|
+
'okay',
|
|
780
|
+
'acknowledge',
|
|
781
|
+
/^close (banner|cookie notification)$/is,
|
|
782
|
+
/understood$/is,
|
|
783
|
+
'confirm my choices',
|
|
784
|
+
];
|
package/lib/heuristics.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ACCEPT_PATTERNS,
|
|
3
|
+
ACKNOWLEDGE_PATTERNS,
|
|
4
|
+
DETECT_PATTERNS,
|
|
5
|
+
NEVER_MATCH_PATTERNS,
|
|
6
|
+
REJECT_PATTERNS,
|
|
7
|
+
SETTINGS_PATTERNS,
|
|
8
|
+
} from './heuristic-patterns';
|
|
9
|
+
import { ButtonData, ButtonRegexClassification, PopupData, PopupHandlingMode, PopupHandlingModes } from './types';
|
|
3
10
|
import { isElementVisible, isTopFrame } from './utils';
|
|
4
11
|
|
|
5
12
|
const BUTTON_LIKE_ELEMENT_SELECTOR = 'button, input[type="button"], input[type="submit"], a, [role="button"], [class*="button"]';
|
|
@@ -21,52 +28,78 @@ export function checkHeuristicPatterns(allText: string, detectPatterns = DETECT_
|
|
|
21
28
|
return { patterns, snippets };
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
export function getActionablePopups(timeout = POPUP_SEARCH_MAX_TIME): PopupData[] {
|
|
31
|
+
export function getActionablePopups(mode: PopupHandlingMode = PopupHandlingModes.Reject, timeout = POPUP_SEARCH_MAX_TIME): PopupData[] {
|
|
25
32
|
const popups = getPotentialPopups(timeout);
|
|
26
33
|
const result = popups.reduce((acc, popup) => {
|
|
27
34
|
const popupText = popup.text?.trim();
|
|
28
35
|
if (popupText) {
|
|
29
36
|
const { patterns } = checkHeuristicPatterns(popupText);
|
|
30
37
|
if (patterns.length > 0) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
otherButtons,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
38
|
+
classifyButtons(popup.buttons);
|
|
39
|
+
popup.regexClassification = classifyPopup(popup.buttons);
|
|
40
|
+
acc.push({
|
|
41
|
+
...popup,
|
|
42
|
+
});
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
return acc;
|
|
42
46
|
}, [] as PopupData[]);
|
|
43
|
-
|
|
47
|
+
// popups filtered by mode and sorted so a popup with reject will always win.
|
|
48
|
+
return result
|
|
49
|
+
.filter(
|
|
50
|
+
(popup) =>
|
|
51
|
+
popup.regexClassification !== undefined &&
|
|
52
|
+
popup.regexClassification !== PopupHandlingModes.None &&
|
|
53
|
+
popup.regexClassification <= mode,
|
|
54
|
+
)
|
|
55
|
+
.sort((a, b) => (a.regexClassification ?? 0) - (b.regexClassification ?? 0));
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
export function classifyButtons(buttons: ButtonData[])
|
|
47
|
-
const rejectButtons = [];
|
|
48
|
-
const otherButtons = [];
|
|
58
|
+
export function classifyButtons(buttons: ButtonData[]) {
|
|
49
59
|
for (const button of buttons) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
button.regexClassification = classifyButtonTextRegex(button.text);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function classifyPopup(buttons: ButtonData[]): PopupHandlingMode {
|
|
65
|
+
const { reject, settings, accept, acknowledge } = buttons.reduce(
|
|
66
|
+
(acc, button) => {
|
|
67
|
+
if (button.regexClassification && button.regexClassification !== 'other') {
|
|
68
|
+
acc[button.regexClassification]++;
|
|
69
|
+
}
|
|
70
|
+
return acc;
|
|
71
|
+
},
|
|
72
|
+
{ reject: 0, settings: 0, accept: 0, acknowledge: 0 },
|
|
73
|
+
);
|
|
74
|
+
if (reject > 0) {
|
|
75
|
+
return PopupHandlingModes.Reject;
|
|
76
|
+
}
|
|
77
|
+
if (settings > 0) {
|
|
78
|
+
return PopupHandlingModes.None;
|
|
55
79
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
if (acknowledge > 0) {
|
|
81
|
+
return PopupHandlingModes.Tier1;
|
|
82
|
+
}
|
|
83
|
+
if (accept > 0) {
|
|
84
|
+
return accept === 1 ? PopupHandlingModes.Tier2 : PopupHandlingModes.None;
|
|
85
|
+
}
|
|
86
|
+
return PopupHandlingModes.None;
|
|
60
87
|
}
|
|
61
88
|
|
|
62
|
-
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} buttonText
|
|
91
|
+
* @param {Array<string|RegExp>} matchPatterns
|
|
92
|
+
* @param {Array<string|RegExp>} neverMatchPatterns
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
function testButtonMatches(buttonText: string, matchPatterns: (string | RegExp)[], neverMatchPatterns: (string | RegExp)[]): boolean {
|
|
63
96
|
if (!buttonText) {
|
|
64
97
|
return false;
|
|
65
98
|
}
|
|
66
99
|
const cleanedButtonText = cleanButtonText(buttonText);
|
|
67
100
|
return (
|
|
68
|
-
!neverMatchPatterns.some((p) => p.test(cleanedButtonText)) &&
|
|
69
|
-
|
|
101
|
+
!neverMatchPatterns.some((p) => (p instanceof RegExp && p.test(cleanedButtonText)) || p === cleanedButtonText) &&
|
|
102
|
+
matchPatterns.some((p) => (p instanceof RegExp && p.test(cleanedButtonText)) || p === cleanedButtonText)
|
|
70
103
|
);
|
|
71
104
|
}
|
|
72
105
|
|
|
@@ -89,6 +122,22 @@ export function cleanButtonText(buttonText: string): string {
|
|
|
89
122
|
return result;
|
|
90
123
|
}
|
|
91
124
|
|
|
125
|
+
export function classifyButtonTextRegex(buttonText: string): ButtonRegexClassification {
|
|
126
|
+
if (testButtonMatches(buttonText, REJECT_PATTERNS, NEVER_MATCH_PATTERNS)) {
|
|
127
|
+
return 'reject';
|
|
128
|
+
}
|
|
129
|
+
if (testButtonMatches(buttonText, SETTINGS_PATTERNS, NEVER_MATCH_PATTERNS)) {
|
|
130
|
+
return 'settings';
|
|
131
|
+
}
|
|
132
|
+
if (testButtonMatches(buttonText, ACCEPT_PATTERNS, NEVER_MATCH_PATTERNS)) {
|
|
133
|
+
return 'accept';
|
|
134
|
+
}
|
|
135
|
+
if (testButtonMatches(buttonText, ACKNOWLEDGE_PATTERNS, NEVER_MATCH_PATTERNS)) {
|
|
136
|
+
return 'acknowledge';
|
|
137
|
+
}
|
|
138
|
+
return 'other';
|
|
139
|
+
}
|
|
140
|
+
|
|
92
141
|
function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
|
|
93
142
|
const isFramed = !isTopFrame();
|
|
94
143
|
// do not inspect frames that are more than one level deep
|
package/lib/types.ts
CHANGED
|
@@ -78,6 +78,7 @@ export type Config = {
|
|
|
78
78
|
};
|
|
79
79
|
performanceLoggingEnabled: boolean;
|
|
80
80
|
heuristicPopupSearchTimeout: number;
|
|
81
|
+
heuristicMode: PopupHandlingMode; // controls the behavior of the heuristic popup detection. Has no effect if enableHeuristicDetection is false.
|
|
81
82
|
};
|
|
82
83
|
|
|
83
84
|
export type LifecycleState =
|
|
@@ -114,15 +115,32 @@ export type ConsentState = {
|
|
|
114
115
|
performance?: Record<string, number[]>;
|
|
115
116
|
};
|
|
116
117
|
|
|
118
|
+
export type ButtonRegexClassification = 'reject' | 'settings' | 'accept' | 'acknowledge' | 'other';
|
|
119
|
+
|
|
117
120
|
export interface ButtonData {
|
|
118
121
|
text: string;
|
|
119
122
|
element: HTMLElement;
|
|
123
|
+
regexClassification?: ButtonRegexClassification;
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
export interface PopupData {
|
|
123
127
|
text: string;
|
|
124
128
|
element: HTMLElement;
|
|
125
129
|
buttons: ButtonData[];
|
|
126
|
-
|
|
127
|
-
otherButtons?: ButtonData[];
|
|
130
|
+
regexClassification?: PopupHandlingMode;
|
|
128
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Controls the behavior of the heuristic popup detection.
|
|
135
|
+
* - Reject: Will click the reject button on a popup if it exists.
|
|
136
|
+
* - Tier1: Will also click the acknowledge button on a popup if it exists, and no Reject button exists.
|
|
137
|
+
* - Tier2: Will also click the accept button on a popup if it exists, and no Reject or Acknowledge button exists.
|
|
138
|
+
* - None: Disabled
|
|
139
|
+
*/
|
|
140
|
+
export const PopupHandlingModes = {
|
|
141
|
+
None: -1,
|
|
142
|
+
Reject: 0,
|
|
143
|
+
Tier1: 1,
|
|
144
|
+
Tier2: 2,
|
|
145
|
+
} as const;
|
|
146
|
+
export type PopupHandlingMode = (typeof PopupHandlingModes)[keyof typeof PopupHandlingModes];
|
package/lib/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HideMethod } from './rules';
|
|
2
|
-
import { Config } from './types';
|
|
2
|
+
import { Config, PopupHandlingModes } from './types';
|
|
3
3
|
|
|
4
4
|
// get or create a style container for CSS overrides
|
|
5
5
|
export function getStyleElement(styleOverrideElementId = 'autoconsent-css-rules'): HTMLStyleElement {
|
|
@@ -95,6 +95,7 @@ export function normalizeConfig(providedConfig: any): Config {
|
|
|
95
95
|
},
|
|
96
96
|
performanceLoggingEnabled: false,
|
|
97
97
|
heuristicPopupSearchTimeout: 100,
|
|
98
|
+
heuristicMode: PopupHandlingModes.Reject,
|
|
98
99
|
};
|
|
99
100
|
const updatedConfig: Config = copyObject(defaultConfig);
|
|
100
101
|
// filter out any unknown entries
|
|
@@ -116,13 +117,18 @@ export function scheduleWhenIdle(callback: () => void, timeout = 500) {
|
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
type HighlightedHTMLElement = HTMLElement & {
|
|
121
|
+
__oldStyles?: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
119
124
|
export function highlightNode(node: HTMLElement) {
|
|
125
|
+
const highlightedNode = node as HighlightedHTMLElement;
|
|
120
126
|
if (!node.style) return;
|
|
121
|
-
if (
|
|
127
|
+
if (highlightedNode.__oldStyles !== undefined) {
|
|
122
128
|
return; // already highlighted
|
|
123
129
|
}
|
|
124
130
|
if (node.hasAttribute('style')) {
|
|
125
|
-
|
|
131
|
+
highlightedNode.__oldStyles = node.style.cssText;
|
|
126
132
|
}
|
|
127
133
|
node.style.animation = 'pulsate .5s infinite';
|
|
128
134
|
node.style.outline = 'solid red';
|
|
@@ -153,10 +159,11 @@ export function highlightNode(node: HTMLElement) {
|
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
export function unhighlightNode(node: HTMLElement) {
|
|
162
|
+
const highlightedNode = node as HighlightedHTMLElement;
|
|
156
163
|
if (!node.style || !node.hasAttribute('style')) return;
|
|
157
|
-
if (
|
|
158
|
-
node.style.cssText =
|
|
159
|
-
delete
|
|
164
|
+
if (highlightedNode.__oldStyles !== undefined) {
|
|
165
|
+
node.style.cssText = highlightedNode.__oldStyles;
|
|
166
|
+
delete highlightedNode.__oldStyles;
|
|
160
167
|
} else {
|
|
161
168
|
node.removeAttribute('style');
|
|
162
169
|
}
|
package/lib/web.ts
CHANGED
|
@@ -296,7 +296,9 @@ export default class AutoConsent {
|
|
|
296
296
|
});
|
|
297
297
|
// heuristic CMP is only run in the top frame and only if heuristic action is enabled and retries is odd
|
|
298
298
|
const heuristicRules =
|
|
299
|
-
isTop && this.config.enableHeuristicAction && this.state.findCmpAttempts % 2 === 0
|
|
299
|
+
isTop && this.config.enableHeuristicAction && this.state.findCmpAttempts % 2 === 0
|
|
300
|
+
? [new AutoConsentHeuristicCMP(this, this.config.heuristicMode)]
|
|
301
|
+
: [];
|
|
300
302
|
|
|
301
303
|
const rulesPriorityStages: [string, AutoCMP[]][] = [
|
|
302
304
|
['site-specific', siteSpecificRules],
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twitch.tv",
|
|
3
3
|
"runContext": {
|
|
4
|
-
"urlPattern": "^https?://(
|
|
4
|
+
"urlPattern": "^https?://([\\w-]+\\.)?twitch\\.tv"
|
|
5
5
|
},
|
|
6
6
|
"prehideSelectors": [
|
|
7
7
|
"div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"
|
package/rules/build.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import https from 'https';
|
|
5
5
|
import { encodeRules, deduplicateRules } from '../lib/encoding';
|
|
6
6
|
import { AutoConsentCMPRule } from '../lib/rules';
|
|
7
|
+
import { buildRuleIndex } from './rule-index-builder';
|
|
7
8
|
|
|
8
9
|
export const rulesDir = __dirname;
|
|
9
10
|
|
|
@@ -61,6 +62,9 @@ export async function buildConsentOMaticRules() {
|
|
|
61
62
|
|
|
62
63
|
fs.writeFile(path.join(rulesDir, 'rules.json'), stringify({ autoconsent }), () => console.log('Written rules.json'));
|
|
63
64
|
fs.writeFile(path.join(rulesDir, 'consentomatic.json'), stringify({ consentomatic }), () => console.log('Written consentomatic.json'));
|
|
65
|
+
fs.writeFile(path.join(rulesDir, 'rule-index.json'), stringify(buildRuleIndex(autoconsent, consentomatic)), () =>
|
|
66
|
+
console.log('Written rule-index.json'),
|
|
67
|
+
);
|
|
64
68
|
fs.writeFile(compactRulesPath, stringify(encodeRules(autoconsent, existingCompactRules)), () =>
|
|
65
69
|
console.log('Written compact-rules.json'),
|
|
66
70
|
);
|