@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/build.sh +3 -0
  3. package/dist/addon-firefox/background.bundle.js +10 -1
  4. package/dist/addon-firefox/compact-rules.json +1 -1
  5. package/dist/addon-firefox/content.bundle.js +157 -40
  6. package/dist/addon-firefox/manifest.json +1 -1
  7. package/dist/addon-firefox/popup.bundle.js +190 -14
  8. package/dist/addon-firefox/popup.html +93 -8
  9. package/dist/addon-firefox/rule-index.json +1 -0
  10. package/dist/addon-firefox/rules.json +1 -1
  11. package/dist/addon-mv3/background.bundle.js +10 -1
  12. package/dist/addon-mv3/compact-rules.json +1 -1
  13. package/dist/addon-mv3/content.bundle.js +157 -40
  14. package/dist/addon-mv3/manifest.json +1 -1
  15. package/dist/addon-mv3/popup.bundle.js +190 -14
  16. package/dist/addon-mv3/popup.html +93 -8
  17. package/dist/addon-mv3/rule-index.json +1 -0
  18. package/dist/addon-mv3/rules.json +1 -1
  19. package/dist/autoconsent.cjs.js +157 -40
  20. package/dist/autoconsent.esm.js +157 -40
  21. package/dist/autoconsent.extra.cjs.js +157 -40
  22. package/dist/autoconsent.extra.esm.js +157 -40
  23. package/dist/autoconsent.playwright.js +157 -40
  24. package/dist/autoconsent.standalone.js +4138 -0
  25. package/dist/types/cmps/base.d.ts +4 -2
  26. package/dist/types/cmps/trustarc-top.d.ts +1 -1
  27. package/dist/types/eval-handler.d.ts +3 -3
  28. package/dist/types/heuristic-patterns.d.ts +3 -0
  29. package/dist/types/heuristics.d.ts +4 -7
  30. package/dist/types/types.d.ts +18 -2
  31. package/lib/cmps/base.ts +20 -7
  32. package/lib/cmps/conversant.ts +7 -4
  33. package/lib/cmps/trustarc-top.ts +1 -1
  34. package/lib/cmps/uniconsent.ts +4 -4
  35. package/lib/eval-handler.ts +6 -3
  36. package/lib/heuristic-patterns.ts +68 -1
  37. package/lib/heuristics.ts +76 -27
  38. package/lib/types.ts +20 -2
  39. package/lib/utils.ts +13 -6
  40. package/lib/web.ts +3 -1
  41. package/package.json +1 -1
  42. package/rules/autoconsent/twitch.json +1 -1
  43. package/rules/build.ts +4 -0
  44. package/rules/compact-rules.json +1 -1
  45. package/rules/rule-index-builder.ts +63 -0
  46. package/rules/rule-index.json +1 -0
  47. package/rules/rules.json +1 -1
  48. package/standalone/content.ts +103 -0
  49. package/tests-wtr/heuristics/get-actionable-popups.html +16 -0
  50. package/tests-wtr/heuristics/get-actionable-popups.ts +85 -6
  51. package/tests-wtr/heuristics/heuristic-cmp.html +69 -0
  52. package/tests-wtr/heuristics/heuristic-cmp.ts +178 -0
  53. package/tests-wtr/heuristics/heuristics-utils.test.ts +96 -28
  54. package/tests-wtr/lifecycle/find-cmp.html +14 -0
  55. 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
- constructor(autoconsentInstance: AutoConsent);
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?: T) => void;
7
- reject: (reason?: any) => void;
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
@@ -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
- rejectButtons?: ButtonData[];
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 || (this.popups[0].rejectButtons && this.popups[0].rejectButtons.length > 1)) {
461
- this.autoconsent.config.logs.errors && console.warn('Heuristic found multiple reject buttons');
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.popups[0]?.rejectButtons?.[0];
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.popups[0].rejectButtons?.[0];
499
+ const button = this.getTargetButton();
487
500
  if (button) {
488
501
  await this.wait(500);
489
502
  return !isElementVisible(button.element);
@@ -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: HTMLElement) => e.click());
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: HTMLElement) => e.click());
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)');
@@ -18,7 +18,7 @@ export default class TrustArcTop extends AutoConsentCMPBase {
18
18
  frame: false,
19
19
  };
20
20
 
21
- _shortcutButton: HTMLElement;
21
+ _shortcutButton: HTMLElement | null;
22
22
  _optInDone: boolean;
23
23
 
24
24
  constructor(autoconsentInstance: AutoConsent) {
@@ -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: HTMLButtonElement) => {
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: HTMLInputElement) => {
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();
@@ -5,8 +5,8 @@ import { getRandomID } from './random';
5
5
  class Deferred<T> {
6
6
  id: string;
7
7
  promise: Promise<T>;
8
- resolve: (value?: T) => void;
9
- reject: (reason?: any) => void;
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*information\s*$/is,
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 { DETECT_PATTERNS, NEVER_MATCH_PATTERNS, REJECT_PATTERNS } from './heuristic-patterns';
2
- import { ButtonData, PopupData } from './types';
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
- const { rejectButtons, otherButtons } = classifyButtons(popup.buttons);
32
- if (rejectButtons.length > 0) {
33
- acc.push({
34
- ...popup,
35
- rejectButtons,
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
- return result;
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[]): { rejectButtons: ButtonData[]; otherButtons: ButtonData[] } {
47
- const rejectButtons = [];
48
- const otherButtons = [];
58
+ export function classifyButtons(buttons: ButtonData[]) {
49
59
  for (const button of buttons) {
50
- if (isRejectButton(button.text)) {
51
- rejectButtons.push(button);
52
- } else {
53
- otherButtons.push(button);
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
- return {
57
- rejectButtons,
58
- otherButtons,
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
- export function isRejectButton(buttonText: string, rejectPatterns = REJECT_PATTERNS, neverMatchPatterns = NEVER_MATCH_PATTERNS): boolean {
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
- rejectPatterns.some((p) => (p instanceof RegExp && p.test(cleanedButtonText)) || p === cleanedButtonText)
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
- rejectButtons?: ButtonData[];
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 (node.__oldStyles !== undefined) {
127
+ if (highlightedNode.__oldStyles !== undefined) {
122
128
  return; // already highlighted
123
129
  }
124
130
  if (node.hasAttribute('style')) {
125
- node.__oldStyles = node.style.cssText;
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 (node.__oldStyles !== undefined) {
158
- node.style.cssText = node.__oldStyles;
159
- delete node.__oldStyles;
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 ? [new AutoConsentHeuristicCMP(this)] : [];
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@duckduckgo/autoconsent",
3
- "version": "14.97.0",
3
+ "version": "15.1.0",
4
4
  "description": "",
5
5
  "types": "./dist/types/web.d.ts",
6
6
  "exports": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "twitch.tv",
3
3
  "runContext": {
4
- "urlPattern": "^https?://(www\\.)?twitch\\.tv"
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
  );