@duckduckgo/autoconsent 14.97.0 → 15.0.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 (39) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/addon-firefox/background.bundle.js +10 -1
  3. package/dist/addon-firefox/compact-rules.json +1 -1
  4. package/dist/addon-firefox/content.bundle.js +142 -33
  5. package/dist/addon-firefox/manifest.json +1 -1
  6. package/dist/addon-firefox/popup.bundle.js +10 -1
  7. package/dist/addon-firefox/rules.json +1 -1
  8. package/dist/addon-mv3/background.bundle.js +10 -1
  9. package/dist/addon-mv3/compact-rules.json +1 -1
  10. package/dist/addon-mv3/content.bundle.js +142 -33
  11. package/dist/addon-mv3/manifest.json +1 -1
  12. package/dist/addon-mv3/popup.bundle.js +10 -1
  13. package/dist/addon-mv3/rules.json +1 -1
  14. package/dist/autoconsent.cjs.js +142 -33
  15. package/dist/autoconsent.esm.js +142 -33
  16. package/dist/autoconsent.extra.cjs.js +142 -33
  17. package/dist/autoconsent.extra.esm.js +142 -33
  18. package/dist/autoconsent.playwright.js +142 -33
  19. package/dist/types/cmps/base.d.ts +4 -2
  20. package/dist/types/heuristic-patterns.d.ts +3 -0
  21. package/dist/types/heuristics.d.ts +4 -7
  22. package/dist/types/types.d.ts +18 -2
  23. package/lib/cmps/base.ts +20 -7
  24. package/lib/heuristic-patterns.ts +68 -1
  25. package/lib/heuristics.ts +76 -27
  26. package/lib/types.ts +20 -2
  27. package/lib/utils.ts +2 -1
  28. package/lib/web.ts +3 -1
  29. package/package.json +1 -1
  30. package/rules/autoconsent/twitch.json +1 -1
  31. package/rules/compact-rules.json +1 -1
  32. package/rules/rules.json +1 -1
  33. package/tests-wtr/heuristics/get-actionable-popups.html +16 -0
  34. package/tests-wtr/heuristics/get-actionable-popups.ts +85 -6
  35. package/tests-wtr/heuristics/heuristic-cmp.html +69 -0
  36. package/tests-wtr/heuristics/heuristic-cmp.ts +178 -0
  37. package/tests-wtr/heuristics/heuristics-utils.test.ts +96 -28
  38. package/tests-wtr/lifecycle/find-cmp.html +14 -0
  39. package/tests-wtr/lifecycle/find-cmp.ts +44 -2
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
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.0.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])"