@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
@@ -400,6 +400,14 @@
400
400
  });
401
401
  }
402
402
 
403
+ // lib/types.ts
404
+ var PopupHandlingModes = {
405
+ None: -1,
406
+ Reject: 0,
407
+ Tier1: 1,
408
+ Tier2: 2
409
+ };
410
+
403
411
  // lib/random.ts
404
412
  function getRandomID() {
405
413
  if (crypto && typeof crypto.randomUUID !== "undefined") {
@@ -426,6 +434,9 @@
426
434
  sendContentMessage: null
427
435
  };
428
436
  function requestEval(code, snippetId) {
437
+ if (!evalState.sendContentMessage) {
438
+ return Promise.reject(new Error("AutoConsent is not initialized yet"));
439
+ }
429
440
  const id = getRandomID();
430
441
  evalState.sendContentMessage({
431
442
  type: "eval",
@@ -709,7 +720,8 @@
709
720
  waits: false
710
721
  },
711
722
  performanceLoggingEnabled: false,
712
- heuristicPopupSearchTimeout: 100
723
+ heuristicPopupSearchTimeout: 100,
724
+ heuristicMode: PopupHandlingModes.Reject
713
725
  };
714
726
  const updatedConfig = copyObject(defaultConfig);
715
727
  for (const key of Object.keys(defaultConfig)) {
@@ -727,12 +739,13 @@
727
739
  }
728
740
  }
729
741
  function highlightNode(node) {
742
+ const highlightedNode = node;
730
743
  if (!node.style) return;
731
- if (node.__oldStyles !== void 0) {
744
+ if (highlightedNode.__oldStyles !== void 0) {
732
745
  return;
733
746
  }
734
747
  if (node.hasAttribute("style")) {
735
- node.__oldStyles = node.style.cssText;
748
+ highlightedNode.__oldStyles = node.style.cssText;
736
749
  }
737
750
  node.style.animation = "pulsate .5s infinite";
738
751
  node.style.outline = "solid red";
@@ -760,10 +773,11 @@
760
773
  document.head.appendChild(styleTag);
761
774
  }
762
775
  function unhighlightNode(node) {
776
+ const highlightedNode = node;
763
777
  if (!node.style || !node.hasAttribute("style")) return;
764
- if (node.__oldStyles !== void 0) {
765
- node.style.cssText = node.__oldStyles;
766
- delete node.__oldStyles;
778
+ if (highlightedNode.__oldStyles !== void 0) {
779
+ node.style.cssText = highlightedNode.__oldStyles;
780
+ delete highlightedNode.__oldStyles;
767
781
  } else {
768
782
  node.removeAttribute("style");
769
783
  }
@@ -876,12 +890,17 @@
876
890
  /^\s*(use|accept|allow|continue\s+with)?\s*only\s*(strictly)?\s*(necessary|essentials?|required)?\s*(cookies)?\s*$/is,
877
891
  // e.g. "do not sell or share my personal information", "do not sell my personal information"
878
892
  // often used in CCPA
879
- /^\s*do\s+not\s+sell(\s+or\s+share)?\s*my\s*personal\s*information\s*$/is,
893
+ /^\s*do\s+not\s+sell(\s+or\s+share)?\s*my\s*personal\s*info(rmation)?\s*$/is,
880
894
  "allow selection",
881
- "disagree and close"
895
+ "disagree and close",
882
896
  // These are impactful, but look error-prone
897
+ // // e.g. "disagree"
898
+ /^(i)?\s*disagree\s*(and\s+close)?$/i,
883
899
  // // e.g. "i do not agree"
884
900
  // /^\s*(i\s+)?do\s+not\s+agree\s*$/i,
901
+ "no",
902
+ /^no,? thanks$/is,
903
+ /^opt[ -]out$/is
885
904
  ];
886
905
  var REJECT_PATTERNS_DUTCH = [
887
906
  "weigeren",
@@ -1457,6 +1476,64 @@
1457
1476
  /sostienici/is,
1458
1477
  /suscribir/is
1459
1478
  ];
1479
+ var SETTINGS_PATTERNS = [
1480
+ "settings",
1481
+ "preferences",
1482
+ /customi(s|z)e/is,
1483
+ "show details",
1484
+ "more options",
1485
+ /(manage|configure) (my|your) (preferences|choices|cookies)/is,
1486
+ "manage choices",
1487
+ /(cookie )?preference center/is,
1488
+ "change settings",
1489
+ "configure",
1490
+ "change my preferences",
1491
+ "cookie manager",
1492
+ "cookie preference",
1493
+ "let me choose",
1494
+ "cookieconsent preferences",
1495
+ /privacy choices/is,
1496
+ /(privacy|cookie|custom) settings/is,
1497
+ /cookies? (settings|preferences|setting)/is,
1498
+ /(manage|customize|customise|opt-out|edit).*(cookies|preferences|settings|options)/is,
1499
+ "cookie consent options",
1500
+ "privacy controls",
1501
+ "show purposes",
1502
+ // German
1503
+ "einstellungen"
1504
+ ];
1505
+ var ACCEPT_PATTERNS = [
1506
+ /^(accept|allow)( all)?( cookies)?$/is,
1507
+ /i (accept|allow)( all)?/is,
1508
+ "yes",
1509
+ /^(i )?agree$/is,
1510
+ "continue with all",
1511
+ "accept and continue",
1512
+ /accept all above/is,
1513
+ /^(accept|agree) and close/is,
1514
+ "accept continue",
1515
+ "agree proceed",
1516
+ "allow and continue",
1517
+ "close and accept",
1518
+ /accept all$/is,
1519
+ "im ok with that",
1520
+ "accept optional cookies",
1521
+ /^alle (cookies )?akzeptieren$/is
1522
+ ];
1523
+ var ACKNOWLEDGE_PATTERNS = [
1524
+ "ok",
1525
+ "close",
1526
+ "continue",
1527
+ "x",
1528
+ /^got it!?$/,
1529
+ "i understand",
1530
+ "dismiss",
1531
+ "okay",
1532
+ "acknowledge",
1533
+ /^close (banner|cookie notification)$/is,
1534
+ /understood$/is,
1535
+ "confirm my choices"
1536
+ ];
1460
1537
 
1461
1538
  // lib/heuristics.ts
1462
1539
  var BUTTON_LIKE_ELEMENT_SELECTOR = 'button, input[type="button"], input[type="submit"], a, [role="button"], [class*="button"]';
@@ -1475,48 +1552,61 @@
1475
1552
  }
1476
1553
  return { patterns, snippets: snippets2 };
1477
1554
  }
1478
- function getActionablePopups(timeout = POPUP_SEARCH_MAX_TIME) {
1555
+ function getActionablePopups(mode = PopupHandlingModes.Reject, timeout = POPUP_SEARCH_MAX_TIME) {
1479
1556
  const popups = getPotentialPopups(timeout);
1480
1557
  const result = popups.reduce((acc, popup) => {
1481
1558
  const popupText = popup.text?.trim();
1482
1559
  if (popupText) {
1483
1560
  const { patterns } = checkHeuristicPatterns(popupText);
1484
1561
  if (patterns.length > 0) {
1485
- const { rejectButtons, otherButtons } = classifyButtons(popup.buttons);
1486
- if (rejectButtons.length > 0) {
1487
- acc.push({
1488
- ...popup,
1489
- rejectButtons,
1490
- otherButtons
1491
- });
1492
- }
1562
+ classifyButtons(popup.buttons);
1563
+ popup.regexClassification = classifyPopup(popup.buttons);
1564
+ acc.push({
1565
+ ...popup
1566
+ });
1493
1567
  }
1494
1568
  }
1495
1569
  return acc;
1496
1570
  }, []);
1497
- return result;
1571
+ return result.filter(
1572
+ (popup) => popup.regexClassification !== void 0 && popup.regexClassification !== PopupHandlingModes.None && popup.regexClassification <= mode
1573
+ ).sort((a, b) => (a.regexClassification ?? 0) - (b.regexClassification ?? 0));
1498
1574
  }
1499
1575
  function classifyButtons(buttons) {
1500
- const rejectButtons = [];
1501
- const otherButtons = [];
1502
1576
  for (const button of buttons) {
1503
- if (isRejectButton(button.text)) {
1504
- rejectButtons.push(button);
1505
- } else {
1506
- otherButtons.push(button);
1507
- }
1577
+ button.regexClassification = classifyButtonTextRegex(button.text);
1508
1578
  }
1509
- return {
1510
- rejectButtons,
1511
- otherButtons
1512
- };
1513
1579
  }
1514
- function isRejectButton(buttonText, rejectPatterns = REJECT_PATTERNS, neverMatchPatterns = NEVER_MATCH_PATTERNS) {
1580
+ function classifyPopup(buttons) {
1581
+ const { reject, settings, accept, acknowledge } = buttons.reduce(
1582
+ (acc, button) => {
1583
+ if (button.regexClassification && button.regexClassification !== "other") {
1584
+ acc[button.regexClassification]++;
1585
+ }
1586
+ return acc;
1587
+ },
1588
+ { reject: 0, settings: 0, accept: 0, acknowledge: 0 }
1589
+ );
1590
+ if (reject > 0) {
1591
+ return PopupHandlingModes.Reject;
1592
+ }
1593
+ if (settings > 0) {
1594
+ return PopupHandlingModes.None;
1595
+ }
1596
+ if (acknowledge > 0) {
1597
+ return PopupHandlingModes.Tier1;
1598
+ }
1599
+ if (accept > 0) {
1600
+ return accept === 1 ? PopupHandlingModes.Tier2 : PopupHandlingModes.None;
1601
+ }
1602
+ return PopupHandlingModes.None;
1603
+ }
1604
+ function testButtonMatches(buttonText, matchPatterns, neverMatchPatterns) {
1515
1605
  if (!buttonText) {
1516
1606
  return false;
1517
1607
  }
1518
1608
  const cleanedButtonText = cleanButtonText(buttonText);
1519
- return !neverMatchPatterns.some((p) => p.test(cleanedButtonText)) && rejectPatterns.some((p) => p instanceof RegExp && p.test(cleanedButtonText) || p === cleanedButtonText);
1609
+ return !neverMatchPatterns.some((p) => p instanceof RegExp && p.test(cleanedButtonText) || p === cleanedButtonText) && matchPatterns.some((p) => p instanceof RegExp && p.test(cleanedButtonText) || p === cleanedButtonText);
1520
1610
  }
1521
1611
  function cleanButtonText(buttonText) {
1522
1612
  let result = buttonText.toLowerCase();
@@ -1530,6 +1620,21 @@
1530
1620
  result = result.trim();
1531
1621
  return result;
1532
1622
  }
1623
+ function classifyButtonTextRegex(buttonText) {
1624
+ if (testButtonMatches(buttonText, REJECT_PATTERNS, NEVER_MATCH_PATTERNS)) {
1625
+ return "reject";
1626
+ }
1627
+ if (testButtonMatches(buttonText, SETTINGS_PATTERNS, NEVER_MATCH_PATTERNS)) {
1628
+ return "settings";
1629
+ }
1630
+ if (testButtonMatches(buttonText, ACCEPT_PATTERNS, NEVER_MATCH_PATTERNS)) {
1631
+ return "accept";
1632
+ }
1633
+ if (testButtonMatches(buttonText, ACKNOWLEDGE_PATTERNS, NEVER_MATCH_PATTERNS)) {
1634
+ return "acknowledge";
1635
+ }
1636
+ return "other";
1637
+ }
1533
1638
  function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1534
1639
  const isFramed = !isTopFrame();
1535
1640
  if (isFramed && window.parent && window.parent !== window.top) {
@@ -1975,7 +2080,7 @@
1975
2080
  }
1976
2081
  };
1977
2082
  var AutoConsentHeuristicCMP = class extends AutoConsentCMPBase {
1978
- constructor(autoconsentInstance) {
2083
+ constructor(autoconsentInstance, mode = PopupHandlingModes.Reject) {
1979
2084
  super(autoconsentInstance);
1980
2085
  this.popups = [];
1981
2086
  this.name = "HEURISTIC";
@@ -1984,6 +2089,7 @@
1984
2089
  frame: false
1985
2090
  // do not run in iframes for security reasons
1986
2091
  };
2092
+ this.mode = mode;
1987
2093
  }
1988
2094
  get hasSelfTest() {
1989
2095
  return true;
@@ -1997,25 +2103,33 @@
1997
2103
  async detectCmp() {
1998
2104
  await new Promise((resolve) => setTimeout(resolve, 0));
1999
2105
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorStart");
2000
- this.popups = getActionablePopups(this.autoconsent.config.heuristicPopupSearchTimeout);
2106
+ this.popups = getActionablePopups(this.mode, this.autoconsent.config.heuristicPopupSearchTimeout);
2001
2107
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorEnd");
2002
2108
  this.autoconsent.config.performanceLoggingEnabled && performance.measure("heuristicDetector", "heuristicDetectorStart", "heuristicDetectorEnd");
2003
2109
  if (this.popups.length > 0) {
2110
+ this.name = `HEURISTIC-TIER${this.popups[0].regexClassification}`;
2004
2111
  return Promise.resolve(true);
2005
2112
  }
2006
2113
  return Promise.resolve(false);
2007
2114
  }
2008
2115
  async detectPopup() {
2009
2116
  if (this.popups.length > 0) {
2010
- if (this.popups.length > 1 || this.popups[0].rejectButtons && this.popups[0].rejectButtons.length > 1) {
2011
- this.autoconsent.config.logs.errors && console.warn("Heuristic found multiple reject buttons");
2117
+ if (this.popups.length > 1) {
2118
+ this.autoconsent.config.logs.errors && console.warn("Heuristic found multiple popups");
2012
2119
  }
2013
2120
  return true;
2014
2121
  }
2015
2122
  return false;
2016
2123
  }
2124
+ getTargetButton() {
2125
+ const popup = this.popups[0];
2126
+ const level = popup.regexClassification;
2127
+ const buttons = popup.buttons;
2128
+ const targetButtonType = level === PopupHandlingModes.Reject ? "reject" : level === PopupHandlingModes.Tier1 ? "acknowledge" : "accept";
2129
+ return buttons.find((button) => button.regexClassification === targetButtonType);
2130
+ }
2017
2131
  optOut() {
2018
- const button = this.popups[0]?.rejectButtons?.[0];
2132
+ const button = this.getTargetButton();
2019
2133
  if (button) {
2020
2134
  return this.clickElement(button.element);
2021
2135
  }
@@ -2028,7 +2142,7 @@
2028
2142
  throw new Error("Not Implemented");
2029
2143
  }
2030
2144
  async test() {
2031
- const button = this.popups[0].rejectButtons?.[0];
2145
+ const button = this.getTargetButton();
2032
2146
  if (button) {
2033
2147
  await this.wait(500);
2034
2148
  return !isElementVisible(button.element);
@@ -2709,7 +2823,7 @@
2709
2823
  async optOut() {
2710
2824
  await this.waitForElement(".unic button", 1e3);
2711
2825
  document.querySelectorAll(".unic button").forEach((button) => {
2712
- const text = button.textContent;
2826
+ const text = button.textContent || "";
2713
2827
  if (text.includes("Manage Options") || text.includes("Optionen verwalten")) {
2714
2828
  button.click();
2715
2829
  }
@@ -2722,7 +2836,7 @@
2722
2836
  }
2723
2837
  });
2724
2838
  for (const b of document.querySelectorAll(".unic button")) {
2725
- const text = b.textContent;
2839
+ const text = b.textContent || "";
2726
2840
  for (const pattern of ["Confirm Choices", "Save Choices", "Auswahl speichern"]) {
2727
2841
  if (text.includes(pattern)) {
2728
2842
  b.click();
@@ -2779,6 +2893,9 @@
2779
2893
  item.querySelector(".cmp-accordion-item-title").click();
2780
2894
  await waitFor(() => !!item.querySelector(".cmp-accordion-item-content.cmp-active"), 10, 50);
2781
2895
  const content = item.querySelector(".cmp-accordion-item-content.cmp-active");
2896
+ if (!content) {
2897
+ return false;
2898
+ }
2782
2899
  content.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e) => e.click());
2783
2900
  content.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e) => e.click());
2784
2901
  }
@@ -3497,7 +3614,7 @@
3497
3614
  }
3498
3615
  }
3499
3616
  });
3500
- const heuristicRules = isTop && this.config.enableHeuristicAction && this.state.findCmpAttempts % 2 === 0 ? [new AutoConsentHeuristicCMP(this)] : [];
3617
+ const heuristicRules = isTop && this.config.enableHeuristicAction && this.state.findCmpAttempts % 2 === 0 ? [new AutoConsentHeuristicCMP(this, this.config.heuristicMode)] : [];
3501
3618
  const rulesPriorityStages = [
3502
3619
  ["site-specific", siteSpecificRules],
3503
3620
  ["generic", genericRules],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Autoconsent",
4
- "version": "2026.6.18",
4
+ "version": "2026.6.20",
5
5
  "background": {
6
6
  "service_worker": "background.bundle.js"
7
7
  },
@@ -761,6 +761,14 @@
761
761
  return parseImpl(url, 5, suffixLookup, options, getEmptyResult());
762
762
  }
763
763
 
764
+ // lib/types.ts
765
+ var PopupHandlingModes = {
766
+ None: -1,
767
+ Reject: 0,
768
+ Tier1: 1,
769
+ Tier2: 2
770
+ };
771
+
764
772
  // lib/utils.ts
765
773
  function copyObject(data) {
766
774
  if (globalThis.structuredClone) {
@@ -795,7 +803,8 @@
795
803
  waits: false
796
804
  },
797
805
  performanceLoggingEnabled: false,
798
- heuristicPopupSearchTimeout: 100
806
+ heuristicPopupSearchTimeout: 100,
807
+ heuristicMode: PopupHandlingModes.Reject
799
808
  };
800
809
  const updatedConfig = copyObject(defaultConfig);
801
810
  for (const key of Object.keys(defaultConfig)) {
@@ -904,6 +913,7 @@
904
913
  }
905
914
 
906
915
  // addon/popup.ts
916
+ var maxRenderedRules = 100;
907
917
  async function init() {
908
918
  const autoconsentConfig = await storageGet("config");
909
919
  const enabledCheckbox = document.querySelector("input#enabled");
@@ -917,8 +927,7 @@
917
927
  const cosmeticOffRadio = document.querySelector("input#cosmetic-off");
918
928
  const generatedOnRadio = document.querySelector("input#generated-on");
919
929
  const generatedOffRadio = document.querySelector("input#generated-off");
920
- const heuristicActionOnRadio = document.querySelector("input#heuristic-action-on");
921
- const heuristicActionOffRadio = document.querySelector("input#heuristic-action-off");
930
+ const heuristicActionCheckbox = document.querySelector("input#heuristic-action");
922
931
  const visualTestOnRadio = document.querySelector("input#visual-test-on");
923
932
  const visualTestOffRadio = document.querySelector("input#visual-test-off");
924
933
  const popupMutationOnRadio = document.querySelector("input#popup-mutation-on");
@@ -933,6 +942,47 @@
933
942
  const logsMessagesCheckbox = document.querySelector("input#logs-messages");
934
943
  const ruleReloadButton = document.querySelector("#reload");
935
944
  const resetButton = document.querySelector("#reset");
945
+ const ruleSectionControls = {
946
+ generated: {
947
+ details: document.querySelector("#generated-rules-section"),
948
+ count: document.querySelector("#generated-rules-count"),
949
+ search: document.querySelector("#generated-rules-search"),
950
+ enableAll: document.querySelector("#generated-rules-enable-all"),
951
+ disableAll: document.querySelector("#generated-rules-disable-all"),
952
+ status: document.querySelector("#generated-rules-status"),
953
+ list: document.querySelector("#generated-rules-list")
954
+ },
955
+ generic: {
956
+ details: document.querySelector("#generic-rules-section"),
957
+ count: document.querySelector("#generic-rules-count"),
958
+ search: document.querySelector("#generic-rules-search"),
959
+ enableAll: document.querySelector("#generic-rules-enable-all"),
960
+ disableAll: document.querySelector("#generic-rules-disable-all"),
961
+ status: document.querySelector("#generic-rules-status"),
962
+ list: document.querySelector("#generic-rules-list")
963
+ },
964
+ code: {
965
+ details: document.querySelector("#code-rules-section"),
966
+ count: document.querySelector("#code-rules-count"),
967
+ search: document.querySelector("#code-rules-search"),
968
+ enableAll: document.querySelector("#code-rules-enable-all"),
969
+ disableAll: document.querySelector("#code-rules-disable-all"),
970
+ status: document.querySelector("#code-rules-status"),
971
+ list: document.querySelector("#code-rules-list")
972
+ },
973
+ consentomatic: {
974
+ details: document.querySelector("#consentomatic-rules-section"),
975
+ count: document.querySelector("#consentomatic-rules-count"),
976
+ search: document.querySelector("#consentomatic-rules-search"),
977
+ enableAll: document.querySelector("#consentomatic-rules-enable-all"),
978
+ disableAll: document.querySelector("#consentomatic-rules-disable-all"),
979
+ status: document.querySelector("#consentomatic-rules-status"),
980
+ list: document.querySelector("#consentomatic-rules-list")
981
+ }
982
+ };
983
+ let ruleIndexPromise = null;
984
+ let ruleIndex = null;
985
+ autoconsentConfig.disabledCmps = autoconsentConfig.disabledCmps || [];
936
986
  const [currentTab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
937
987
  if (!currentTab.url || !currentTab.id) {
938
988
  console.error("Current tab is not valid");
@@ -998,11 +1048,7 @@
998
1048
  } else {
999
1049
  generatedOffRadio.checked = true;
1000
1050
  }
1001
- if (autoconsentConfig.enableHeuristicAction) {
1002
- heuristicActionOnRadio.checked = true;
1003
- } else {
1004
- heuristicActionOffRadio.checked = true;
1005
- }
1051
+ heuristicActionCheckbox.checked = autoconsentConfig.enableHeuristicAction;
1006
1052
  if (autoconsentConfig.visualTest) {
1007
1053
  visualTestOnRadio.checked = true;
1008
1054
  } else {
@@ -1052,11 +1098,10 @@
1052
1098
  generatedOnRadio.addEventListener("change", generatedChange);
1053
1099
  generatedOffRadio.addEventListener("change", generatedChange);
1054
1100
  function heuristicActionChange() {
1055
- autoconsentConfig.enableHeuristicAction = heuristicActionOnRadio.checked;
1101
+ autoconsentConfig.enableHeuristicAction = heuristicActionCheckbox.checked;
1056
1102
  storageSet({ config: autoconsentConfig });
1057
1103
  }
1058
- heuristicActionOnRadio.addEventListener("change", heuristicActionChange);
1059
- heuristicActionOffRadio.addEventListener("change", heuristicActionChange);
1104
+ heuristicActionCheckbox.addEventListener("change", heuristicActionChange);
1060
1105
  function visualTestChange() {
1061
1106
  autoconsentConfig.visualTest = visualTestOnRadio.checked;
1062
1107
  storageSet({ config: autoconsentConfig });
@@ -1102,10 +1147,141 @@
1102
1147
  logsMessagesCheckbox.addEventListener("change", () => {
1103
1148
  updateLogsConfig();
1104
1149
  });
1150
+ async function loadRuleIndex() {
1151
+ if (!ruleIndexPromise) {
1152
+ ruleIndexPromise = fetch("./rule-index.json").then(async (res) => await res.json());
1153
+ }
1154
+ ruleIndex = await ruleIndexPromise;
1155
+ updateRuleCounts();
1156
+ return ruleIndex;
1157
+ }
1158
+ function updateRuleCounts() {
1159
+ if (!ruleIndex) {
1160
+ return;
1161
+ }
1162
+ const disabledCmps = autoconsentConfig.disabledCmps || [];
1163
+ Object.keys(ruleSectionControls).forEach((section) => {
1164
+ const sectionRules = ruleIndex?.filter((rule) => rule.section === section) || [];
1165
+ const disabledInSection = sectionRules.filter((rule) => disabledCmps.includes(rule.name)).length;
1166
+ const count = ruleSectionControls[section].count;
1167
+ count.textContent = `(${disabledInSection}/${sectionRules.length} disabled)`;
1168
+ count.classList.toggle("rule-count-disabled", disabledInSection > 0);
1169
+ });
1170
+ }
1171
+ function setRuleEnabled(ruleName, enabled) {
1172
+ const disabledCmps = new Set(autoconsentConfig.disabledCmps || []);
1173
+ if (enabled) {
1174
+ disabledCmps.delete(ruleName);
1175
+ } else {
1176
+ disabledCmps.add(ruleName);
1177
+ }
1178
+ autoconsentConfig.disabledCmps = Array.from(disabledCmps).sort();
1179
+ storageSet({ config: autoconsentConfig });
1180
+ updateRuleCounts();
1181
+ }
1182
+ function renderOpenRuleSections() {
1183
+ Object.keys(ruleSectionControls).forEach((section) => {
1184
+ if (ruleSectionControls[section].details.open) {
1185
+ renderRuleSection(section);
1186
+ }
1187
+ });
1188
+ }
1189
+ function setRuleSectionEnabled(section, enabled) {
1190
+ if (!ruleIndex) {
1191
+ return;
1192
+ }
1193
+ const disabledCmps = new Set(autoconsentConfig.disabledCmps || []);
1194
+ ruleIndex.filter((rule) => rule.section === section).forEach((rule) => {
1195
+ if (enabled) {
1196
+ disabledCmps.delete(rule.name);
1197
+ } else {
1198
+ disabledCmps.add(rule.name);
1199
+ }
1200
+ });
1201
+ autoconsentConfig.disabledCmps = Array.from(disabledCmps).sort();
1202
+ storageSet({ config: autoconsentConfig });
1203
+ updateRuleCounts();
1204
+ renderOpenRuleSections();
1205
+ }
1206
+ function renderRuleSection(section) {
1207
+ const controls = ruleSectionControls[section];
1208
+ if (!ruleIndex) {
1209
+ controls.status.textContent = "Rules are not loaded yet.";
1210
+ return;
1211
+ }
1212
+ const disabledCmps = autoconsentConfig.disabledCmps || [];
1213
+ const searchQuery = controls.search.value.trim().toLowerCase();
1214
+ const sectionRules = ruleIndex.filter((rule) => rule.section === section);
1215
+ const matchingRules = sectionRules.filter((rule) => {
1216
+ if (!searchQuery) {
1217
+ return disabledCmps.includes(rule.name);
1218
+ }
1219
+ return rule.name.toLowerCase().includes(searchQuery) || !!rule.urlPattern?.toLowerCase().includes(searchQuery);
1220
+ });
1221
+ const renderedRules = matchingRules.slice(0, maxRenderedRules);
1222
+ const disabledInSection = sectionRules.filter((rule) => disabledCmps.includes(rule.name)).length;
1223
+ controls.status.textContent = searchQuery ? `Showing ${renderedRules.length} of ${matchingRules.length} matches. ${disabledInSection} disabled.` : `Showing ${renderedRules.length} of ${disabledInSection} disabled rules. Search to find enabled rules.`;
1224
+ controls.list.replaceChildren(
1225
+ ...renderedRules.map((rule) => {
1226
+ const checkbox = document.createElement("input");
1227
+ checkbox.type = "checkbox";
1228
+ checkbox.checked = !disabledCmps.includes(rule.name);
1229
+ checkbox.addEventListener("change", () => {
1230
+ setRuleEnabled(rule.name, checkbox.checked);
1231
+ renderRuleSection(section);
1232
+ });
1233
+ const label = document.createElement("label");
1234
+ label.className = "rule-row";
1235
+ label.title = rule.urlPattern ? `${rule.name}
1236
+ ${rule.urlPattern}` : rule.name;
1237
+ label.append(checkbox, document.createTextNode(` ${rule.name}${rule.cosmetic ? " (cosmetic)" : ""}`));
1238
+ return label;
1239
+ })
1240
+ );
1241
+ }
1242
+ async function loadAndRenderRuleSection(section) {
1243
+ ruleSectionControls[section].status.textContent = "Loading rules...";
1244
+ try {
1245
+ await loadRuleIndex();
1246
+ renderRuleSection(section);
1247
+ } catch (e) {
1248
+ console.error("Failed to load rule index", e);
1249
+ ruleSectionControls[section].status.textContent = "Failed to load rules.";
1250
+ }
1251
+ }
1252
+ Object.keys(ruleSectionControls).forEach((section) => {
1253
+ const controls = ruleSectionControls[section];
1254
+ controls.details.addEventListener("toggle", () => {
1255
+ if (controls.details.open) {
1256
+ loadAndRenderRuleSection(section);
1257
+ }
1258
+ });
1259
+ controls.search.addEventListener("input", () => renderRuleSection(section));
1260
+ controls.enableAll.addEventListener("click", async () => {
1261
+ await loadRuleIndex();
1262
+ setRuleSectionEnabled(section, true);
1263
+ });
1264
+ controls.disableAll.addEventListener("click", async () => {
1265
+ await loadRuleIndex();
1266
+ setRuleSectionEnabled(section, false);
1267
+ });
1268
+ });
1269
+ loadRuleIndex().catch((e) => {
1270
+ console.error("Failed to load rule index", e);
1271
+ });
1105
1272
  ruleReloadButton.addEventListener("click", async () => {
1106
- const res = await fetch("./rules.json");
1107
- storageSet({
1108
- rules: await res.json()
1273
+ const [compactRulesRes, fullRulesRes] = await Promise.all([fetch("./compact-rules.json"), fetch("./rules.json")]);
1274
+ const fullRules = await fullRulesRes.json();
1275
+ await storageSet({
1276
+ rules: await compactRulesRes.json(),
1277
+ fullRules: fullRules.autoconsent
1278
+ });
1279
+ ruleIndexPromise = null;
1280
+ ruleIndex = null;
1281
+ Object.keys(ruleSectionControls).forEach((section) => {
1282
+ if (ruleSectionControls[section].details.open) {
1283
+ loadAndRenderRuleSection(section);
1284
+ }
1109
1285
  });
1110
1286
  });
1111
1287
  resetButton.addEventListener("click", async () => {