@duckduckgo/autoconsent 16.63.0 → 17.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 (84) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/addon-firefox/background.bundle.js +1 -0
  3. package/dist/addon-firefox/compact-rules.json +1 -1
  4. package/dist/addon-firefox/content.bundle.js +162 -35
  5. package/dist/addon-firefox/manifest.json +1 -1
  6. package/dist/addon-firefox/popup.bundle.js +1 -0
  7. package/dist/addon-firefox/rule-index.json +1 -1
  8. package/dist/addon-firefox/rules.json +1 -1
  9. package/dist/addon-mv3/background.bundle.js +1 -0
  10. package/dist/addon-mv3/compact-rules.json +1 -1
  11. package/dist/addon-mv3/content.bundle.js +162 -35
  12. package/dist/addon-mv3/manifest.json +1 -1
  13. package/dist/addon-mv3/popup.bundle.js +1 -0
  14. package/dist/addon-mv3/rule-index.json +1 -1
  15. package/dist/addon-mv3/rules.json +1 -1
  16. package/dist/autoconsent.cjs.js +162 -35
  17. package/dist/autoconsent.esm.js +162 -35
  18. package/dist/autoconsent.playwright.js +165 -35
  19. package/dist/autoconsent.standalone.js +163 -36
  20. package/dist/types/heuristics.d.ts +12 -3
  21. package/dist/types/types.d.ts +12 -1
  22. package/lib/cmps/base.ts +5 -1
  23. package/lib/heuristics.ts +240 -45
  24. package/lib/types.ts +16 -1
  25. package/lib/utils.ts +1 -0
  26. package/package.json +1 -1
  27. package/readme.md +1 -1
  28. package/rules/autoconsent/asrock.json +44 -0
  29. package/rules/autoconsent/consentmo.json +5 -2
  30. package/rules/autoconsent/cookie-script.json +34 -15
  31. package/rules/autoconsent/epaas.json +18 -1
  32. package/rules/autoconsent/fnsp-cookieconsent.json +30 -0
  33. package/rules/autoconsent/fod-fujitv.json +13 -0
  34. package/rules/autoconsent/hkdir.json +33 -0
  35. package/rules/autoconsent/jokul-cookie-consent.json +28 -0
  36. package/rules/autoconsent/lego.json +27 -5
  37. package/rules/autoconsent/mars-cookies-prompt.json +20 -0
  38. package/rules/autoconsent/niedziela-pl.json +35 -0
  39. package/rules/autoconsent/nikkeibp.json +10 -0
  40. package/rules/autoconsent/onecom-cookie-banner.json +25 -0
  41. package/rules/autoconsent/oslosportslager.no.json +34 -0
  42. package/rules/autoconsent/plex.tv.json +52 -0
  43. package/rules/autoconsent/radio-canada.json +28 -0
  44. package/rules/autoconsent/regjeringen.no.json +13 -0
  45. package/rules/autoconsent/roll20.json +50 -0
  46. package/rules/autoconsent/sirdata.json +8 -3
  47. package/rules/autoconsent/site-notifications-cookie-usage.json +25 -0
  48. package/rules/autoconsent/sparebank1-optin.json +10 -0
  49. package/rules/autoconsent/tri-table.json +6 -2
  50. package/rules/autoconsent/vinnova-se.json +50 -0
  51. package/rules/autoconsent/vortex-consent.json +33 -0
  52. package/rules/compact-rules.json +1 -1
  53. package/rules/rule-index.json +1 -1
  54. package/rules/rules.json +1 -1
  55. package/tests/asrock.spec.ts +3 -0
  56. package/tests/consentmo.spec.ts +2 -0
  57. package/tests/cookie-script.spec.ts +5 -0
  58. package/tests/epaas.spec.ts +3 -0
  59. package/tests/fnsp-cookieconsent.spec.ts +9 -0
  60. package/tests/fod-fujitv.spec.ts +3 -0
  61. package/tests/hkdir.spec.ts +10 -0
  62. package/tests/jokul-cookie-consent.spec.ts +3 -0
  63. package/tests/mars-cookies-prompt.spec.ts +3 -0
  64. package/tests/niedziela-pl.spec.ts +3 -0
  65. package/tests/nikkeibp.spec.ts +8 -0
  66. package/tests/onecom-cookie-banner.spec.ts +5 -0
  67. package/tests/oslosportslager.no.spec.ts +3 -0
  68. package/tests/plex.tv.spec.ts +5 -0
  69. package/tests/radio-canada.spec.ts +15 -0
  70. package/tests/regjeringen.no.spec.ts +3 -0
  71. package/tests/roll20.spec.ts +3 -0
  72. package/tests/sirdata.spec.ts +13 -1
  73. package/tests/site-notifications-cookie-usage.spec.ts +3 -0
  74. package/tests/sparebank1-optin.spec.ts +8 -0
  75. package/tests/tri-table.spec.ts +1 -1
  76. package/tests/vinnova-se.spec.ts +6 -0
  77. package/tests/vortex-consent.spec.ts +18 -0
  78. package/tests-wtr/heuristics/get-actionable-popups.ts +41 -7
  79. package/tests-wtr/heuristics/get-potential-popups.test.ts +235 -4
  80. package/tests-wtr/heuristics/heuristic-cmp.ts +16 -1
  81. package/rules/generated/auto_CA_plex.tv_ee4.json +0 -40
  82. package/rules/generated/auto_CA_support.plex.tv_gn3.json +0 -40
  83. package/tests/generated/auto_CA_plex.tv_ee4.spec.ts +0 -2
  84. package/tests/generated/auto_CA_support.plex.tv_gn3.spec.ts +0 -6
@@ -159,6 +159,7 @@
159
159
  },
160
160
  performanceLoggingEnabled: false,
161
161
  heuristicPopupSearchTimeout: 100,
162
+ enableHeuristicHitTest: false,
162
163
  heuristicMode: "off"
163
164
  // heuristic disabled by default
164
165
  };
@@ -1334,28 +1335,73 @@
1334
1335
  // lib/heuristics.ts
1335
1336
  var BUTTON_LIKE_ELEMENT_SELECTOR = 'button, input[type="button"], input[type="submit"], a, [role="button"], [class*="button"]';
1336
1337
  var POPUP_SEARCH_MAX_TIME = 100;
1337
- function getActionablePopups(mode = "reject", timeout = POPUP_SEARCH_MAX_TIME) {
1338
- const acceptedLevels = mode === "tier2" ? ["reject", "tier1", "tier2"] : mode === "tier1" ? ["reject", "tier1"] : ["reject"];
1339
- const popups = getPotentialPopups(timeout).filter((popup) => {
1340
- const popupText = popup.text?.trim();
1341
- if (!popupText || isExcludedPopup(popupText) || !hasHeuristicPatterns(popupText)) {
1342
- return false;
1343
- }
1344
- popup.regexClassification = classifyPopup(popup.buttons);
1345
- return popup.regexClassification !== void 0 && acceptedLevels.includes(popup.regexClassification);
1346
- });
1347
- return popups.sort((a, b) => (a.regexClassification ?? "") > (b.regexClassification ?? "") ? 1 : -1);
1338
+ var ACCEPTED_LEVELS = {
1339
+ off: [],
1340
+ reject: ["reject"],
1341
+ tier1: ["reject", "tier1"],
1342
+ tier2: ["reject", "tier1", "tier2"]
1343
+ };
1344
+ function getActionablePopups(mode = "reject", timeout = POPUP_SEARCH_MAX_TIME, hitTest = false) {
1345
+ const acceptedLevels = ACCEPTED_LEVELS[mode];
1346
+ const isActionable = (popup) => acceptedLevels.includes(popup.regexClassification) && isConsentPopup(popup);
1347
+ const popups = searchPopups(timeout, hitTest, DEFAULT_HIT_TEST_GRID, true).filter(isActionable);
1348
+ return popups.sort((a, b) => classificationRank(a) - classificationRank(b));
1348
1349
  }
1349
- function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1350
+ function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME, hitTest = false, hitTestGrid = DEFAULT_HIT_TEST_GRID) {
1351
+ return searchPopups(timeout, hitTest, hitTestGrid);
1352
+ }
1353
+ function searchPopups(timeout, hitTest, hitTestGrid, stopOnReject = false) {
1350
1354
  if (!isTopFrame()) {
1351
- if (window.parent && window.parent !== window.top) {
1352
- return [];
1353
- }
1354
- const frameElement = getFramePopupElement(document);
1355
- const popup = frameElement && toPopupData(frameElement);
1356
- return popup ? [popup] : [];
1355
+ return getFramePopups();
1357
1356
  }
1358
1357
  const deadline = performance.now() + timeout;
1358
+ const hitTestPopups = hitTest ? findPopupsByHitTest(deadline, hitTestGrid) : [];
1359
+ if (stopOnReject && hitTestPopups.some((p) => p.regexClassification === "reject" && isConsentPopup(p))) {
1360
+ return hitTestPopups;
1361
+ }
1362
+ const walkPopups = collectPositionedPopups(deadline);
1363
+ return [...walkPopups, ...mergeWalkOverlaps(hitTestPopups, walkPopups)];
1364
+ }
1365
+ var consentPopups = /* @__PURE__ */ new WeakMap();
1366
+ function classificationRank(popup) {
1367
+ return ACCEPTED_LEVELS.tier2.indexOf(popup.regexClassification);
1368
+ }
1369
+ function isConsentPopup(popup) {
1370
+ if (!ACCEPTED_LEVELS.tier2.includes(popup.regexClassification)) {
1371
+ return false;
1372
+ }
1373
+ let result = consentPopups.get(popup);
1374
+ if (result === void 0) {
1375
+ const popupText = popup.text.trim();
1376
+ result = !!popupText && !isExcludedPopup(popupText) && hasHeuristicPatterns(popupText);
1377
+ consentPopups.set(popup, result);
1378
+ }
1379
+ return result;
1380
+ }
1381
+ function mergeWalkOverlaps(hitTestPopups, walkPopups) {
1382
+ return hitTestPopups.filter((h) => {
1383
+ const hitIsConsent = isConsentPopup(h);
1384
+ const absorbs = (p) => !hitIsConsent || isConsentPopup(p) && classificationRank(p) <= classificationRank(h);
1385
+ const walkPopup = walkPopups.find((p) => composedOverlaps(p.element, h.element) && absorbs(p));
1386
+ if (walkPopup) {
1387
+ (walkPopup.hitPoints ??= []).push(...h.hitPoints ?? []);
1388
+ }
1389
+ return !walkPopup;
1390
+ });
1391
+ }
1392
+ function getFramePopups() {
1393
+ if (window.parent && window.parent !== window.top) {
1394
+ return [];
1395
+ }
1396
+ const frameElement = getFramePopupElement(document);
1397
+ const popup = frameElement && toPopupData(frameElement);
1398
+ if (!popup) {
1399
+ return [];
1400
+ }
1401
+ popup.source = "frame";
1402
+ return [popup];
1403
+ }
1404
+ function collectPositionedPopups(deadline) {
1359
1405
  const { elements, shadowHosts } = getPopupLikeElements(deadline);
1360
1406
  const potentialPopups = [];
1361
1407
  const expanded = [];
@@ -1371,17 +1417,86 @@
1371
1417
  if (root !== popup) {
1372
1418
  expanded.push(root.element);
1373
1419
  }
1420
+ root.source = "walk";
1374
1421
  potentialPopups.push(root);
1375
1422
  }
1376
1423
  return potentialPopups.filter((p) => !expanded.some((root) => root !== p.element && composedContains(root, p.element)));
1377
1424
  }
1425
+ var DEFAULT_HIT_TEST_GRID = {
1426
+ columns: [0.1, 0.5, 0.9],
1427
+ // banners sit along the top and bottom edges and modals in the middle; rows 150px in added almost nothing in crawls
1428
+ edgeRows: [30, 90]
1429
+ };
1430
+ function findPopupsByHitTest(deadline, grid) {
1431
+ const climbed = /* @__PURE__ */ new Map();
1432
+ const found = [];
1433
+ const toPopup = (el) => el.querySelector(TEXT_ENTRY_SELECTOR) ? null : toPopupRoot(el);
1434
+ for (const [x, y] of getHitTestPoints(grid)) {
1435
+ if (performance.now() > deadline) {
1436
+ return found;
1437
+ }
1438
+ const path = [];
1439
+ let reached = null;
1440
+ const shouldStop = (el) => {
1441
+ if (climbed.has(el)) {
1442
+ reached = climbed.get(el) ?? null;
1443
+ return true;
1444
+ }
1445
+ const overlapped = found.find((p) => composedOverlaps(p.element, el));
1446
+ if (overlapped) {
1447
+ reached = composedContains(overlapped.element, el) ? overlapped : null;
1448
+ return true;
1449
+ }
1450
+ path.push(el);
1451
+ return false;
1452
+ };
1453
+ const hit = closestHTMLElement(deepElementFromPoint(x, y));
1454
+ const popup = climbToPopup(hit, MAX_HIT_TEST_CLIMB_DEPTH, toPopup, shouldStop);
1455
+ if (popup) {
1456
+ popup.source = "hitTest";
1457
+ found.push(popup);
1458
+ reached = popup;
1459
+ }
1460
+ for (const el of path) {
1461
+ climbed.set(el, reached);
1462
+ }
1463
+ if (reached) {
1464
+ (reached.hitPoints ??= []).push([x, y]);
1465
+ }
1466
+ }
1467
+ return found;
1468
+ }
1469
+ function getHitTestPoints(grid) {
1470
+ const { innerWidth: width, innerHeight: height } = window;
1471
+ const rows = [...grid.edgeRows, height / 2, ...grid.edgeRows.map((y) => height - y)].map((y) => Math.round(y));
1472
+ const ys = [...new Set(rows)].filter((y) => y >= 0 && y < height);
1473
+ return ys.flatMap((y) => grid.columns.map((column) => [Math.round(column * width), y]));
1474
+ }
1475
+ var TEXT_ENTRY_SELECTOR = 'textarea, input:not([type]), input[type="text"], input[type="email"], input[type="password"], input[type="search"], input[type="tel"], input[type="number"], input[type="url"]';
1476
+ function deepElementFromPoint(x, y) {
1477
+ let el = document.elementFromPoint(x, y);
1478
+ while (el?.shadowRoot) {
1479
+ const inner = el.shadowRoot.elementFromPoint(x, y);
1480
+ if (!inner || inner === el) {
1481
+ break;
1482
+ }
1483
+ el = inner;
1484
+ }
1485
+ return el;
1486
+ }
1487
+ function closestHTMLElement(el) {
1488
+ while (el && !(el instanceof HTMLElement)) {
1489
+ el = el.parentElement;
1490
+ }
1491
+ return el;
1492
+ }
1378
1493
  function toPopupData(el, shadowHosts, text = getElementText(el)) {
1379
1494
  if (!text) {
1380
1495
  return null;
1381
1496
  }
1382
1497
  const buttons = getButtonData(el, shadowHosts);
1383
1498
  classifyButtons(buttons);
1384
- return { text, element: el, buttons };
1499
+ return { text, element: el, buttons, regexClassification: classifyPopup(buttons) };
1385
1500
  }
1386
1501
  function hasPopupButtons(text, buttons) {
1387
1502
  return !isOnlyButtonText(text, buttons) && buttons.some((b) => b.regexClassification !== "other");
@@ -1393,26 +1508,34 @@
1393
1508
  }
1394
1509
  return rest.replace(/\s+/g, "").length < 10;
1395
1510
  }
1511
+ function toPopupRoot(el, shadowHosts) {
1512
+ const text = getElementText(el);
1513
+ if (!text || !hasHeuristicPatterns(text)) {
1514
+ return null;
1515
+ }
1516
+ const popup = toPopupData(el, shadowHosts, text);
1517
+ return popup && hasPopupButtons(text, popup.buttons) ? popup : null;
1518
+ }
1396
1519
  var MAX_FRAGMENT_EXPANSION_DEPTH = 6;
1520
+ var MAX_HIT_TEST_CLIMB_DEPTH = 8;
1521
+ function climbToPopup(el, maxDepth, toPopup, shouldStop) {
1522
+ for (let depth = 0; el && depth < maxDepth; depth++, el = getComposedParent(el)) {
1523
+ if (shouldStop?.(el) || el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
1524
+ return null;
1525
+ }
1526
+ const popup = toPopup(el);
1527
+ if (popup) {
1528
+ return popup;
1529
+ }
1530
+ }
1531
+ return null;
1532
+ }
1397
1533
  function expandPopupFragment(popup, shadowHosts) {
1398
1534
  if (hasHeuristicPatterns(popup.text) && hasPopupButtons(popup.text, popup.buttons)) {
1399
1535
  return popup;
1400
1536
  }
1401
- let el = getComposedParent(popup.element);
1402
- for (let depth = 0; el && depth < MAX_FRAGMENT_EXPANSION_DEPTH; depth++, el = getComposedParent(el)) {
1403
- if (el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
1404
- break;
1405
- }
1406
- const text = getElementText(el);
1407
- if (!text || !hasHeuristicPatterns(text)) {
1408
- continue;
1409
- }
1410
- const root = toPopupData(el, shadowHosts, text);
1411
- if (root && hasPopupButtons(text, root.buttons)) {
1412
- return root;
1413
- }
1414
- }
1415
- return popup;
1537
+ const parent = getComposedParent(popup.element);
1538
+ return climbToPopup(parent, MAX_FRAGMENT_EXPANSION_DEPTH, (el) => toPopupRoot(el, shadowHosts)) ?? popup;
1416
1539
  }
1417
1540
  function getComposedParent(el) {
1418
1541
  if (el.parentElement) {
@@ -1443,6 +1566,9 @@
1443
1566
  function isRenderedTextElement(el) {
1444
1567
  return el instanceof HTMLElement && !["STYLE", "SCRIPT", "TEMPLATE", "NOSCRIPT", "LINK", "SLOT"].includes(el.tagName) && window.getComputedStyle(el).display !== "none";
1445
1568
  }
1569
+ function composedOverlaps(a, b) {
1570
+ return composedContains(a, b) || composedContains(b, a);
1571
+ }
1446
1572
  function composedContains(container, node) {
1447
1573
  if (container.contains(node)) {
1448
1574
  return true;
@@ -1934,7 +2060,11 @@
1934
2060
  async detectCmp() {
1935
2061
  await new Promise((resolve) => setTimeout(resolve, 0));
1936
2062
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorStart");
1937
- this.popups = getActionablePopups(this.mode, this.autoconsent.config.heuristicPopupSearchTimeout);
2063
+ this.popups = getActionablePopups(
2064
+ this.mode,
2065
+ this.autoconsent.config.heuristicPopupSearchTimeout,
2066
+ this.autoconsent.config.enableHeuristicHitTest
2067
+ );
1938
2068
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorEnd");
1939
2069
  this.autoconsent.config.performanceLoggingEnabled && performance.measure("heuristicDetector", "heuristicDetectorStart", "heuristicDetectorEnd");
1940
2070
  if (this.popups.length > 0) {