@duckduckgo/autoconsent 16.58.0 → 16.60.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 (332) hide show
  1. package/.agents/lib/regional-testing/harness.mjs +4 -172
  2. package/.agents/skills/oxylabs-testing/SKILL.md +4 -4
  3. package/.agents/skills/oxylabs-testing/scripts/oxylabs.mjs +17 -164
  4. package/.agents/skills/oxylabs-testing/scripts/oxylabs.test.mjs +6 -3
  5. package/.agents/skills/verify-pr/SKILL.md +1 -1
  6. package/AGENTS.md +1 -1
  7. package/CHANGELOG.md +176 -0
  8. package/Jenkinsfile +36 -1
  9. package/dist/addon-firefox/background.bundle.js +22 -1
  10. package/dist/addon-firefox/compact-rules.json +1 -1
  11. package/dist/addon-firefox/content.bundle.js +253 -93
  12. package/dist/addon-firefox/manifest.json +1 -1
  13. package/dist/addon-firefox/popup.bundle.js +1 -1
  14. package/dist/addon-firefox/rule-index.json +1 -1
  15. package/dist/addon-firefox/rules.json +1 -1
  16. package/dist/addon-mv3/background.bundle.js +22 -1
  17. package/dist/addon-mv3/compact-rules.json +1 -1
  18. package/dist/addon-mv3/content.bundle.js +253 -93
  19. package/dist/addon-mv3/manifest.json +1 -1
  20. package/dist/addon-mv3/popup.bundle.js +1 -1
  21. package/dist/addon-mv3/rule-index.json +1 -1
  22. package/dist/addon-mv3/rules.json +1 -1
  23. package/dist/autoconsent.cjs.js +253 -93
  24. package/dist/autoconsent.esm.js +253 -93
  25. package/dist/autoconsent.playwright.js +253 -93
  26. package/dist/autoconsent.standalone.js +254 -94
  27. package/dist/heuristics.cjs.js +29 -3
  28. package/dist/heuristics.mjs +28 -3
  29. package/dist/types/cmps/onetrust.d.ts +1 -0
  30. package/dist/types/eval-snippets.d.ts +3 -0
  31. package/dist/types/heuristic-classify.d.ts +1 -0
  32. package/dist/types/heuristics.d.ts +10 -1
  33. package/lib/cmps/onetrust.ts +15 -0
  34. package/lib/eval-snippets.ts +21 -0
  35. package/lib/heuristic-classify.ts +6 -0
  36. package/lib/heuristic-patterns.ts +26 -1
  37. package/lib/heuristics.ts +256 -104
  38. package/package.json +4 -6
  39. package/playwright/isolated-world.mjs +217 -0
  40. package/playwright/oxylabs.mjs +196 -0
  41. package/playwright/runner.ts +183 -39
  42. package/playwright.config.ts +4 -1
  43. package/rules/autoconsent/123inkt.json +9 -0
  44. package/rules/autoconsent/aeolservice.json +39 -0
  45. package/rules/autoconsent/amazon-music.json +13 -0
  46. package/rules/autoconsent/animatetimes.json +34 -0
  47. package/rules/autoconsent/annunci69.json +19 -0
  48. package/rules/autoconsent/arbeidsplassen-nav-no.json +33 -0
  49. package/rules/autoconsent/asahi-interactive.json +30 -0
  50. package/rules/autoconsent/att.json +36 -0
  51. package/rules/autoconsent/auctionet.json +39 -0
  52. package/rules/autoconsent/autodoc.json +18 -0
  53. package/rules/autoconsent/avada-privacy-bar-cosmetic.json +28 -0
  54. package/rules/autoconsent/avada-privacy-bar.json +70 -0
  55. package/rules/autoconsent/axesor.json +13 -0
  56. package/rules/autoconsent/bbc.com-orb-cosmetic.json +34 -0
  57. package/rules/autoconsent/bbc.com.json +8 -5
  58. package/rules/autoconsent/blitzrechner.json +16 -0
  59. package/rules/autoconsent/bokadirekt.se.json +50 -0
  60. package/rules/autoconsent/bouvet-cookiepanel.json +29 -0
  61. package/rules/autoconsent/brainsum-cookieconsent.json +25 -0
  62. package/rules/autoconsent/bwr-cookie.json +13 -0
  63. package/rules/autoconsent/capitalone-ca.json +13 -0
  64. package/rules/autoconsent/carpetaciudadana.json +51 -0
  65. package/rules/autoconsent/cf-gdpr-cookie-consent.json +23 -0
  66. package/rules/autoconsent/chatgpt.json +3 -3
  67. package/rules/autoconsent/cksmodal.json +9 -0
  68. package/rules/autoconsent/consent-studio.json +41 -0
  69. package/rules/autoconsent/cookie-popup-cookieno.json +29 -0
  70. package/rules/autoconsent/cookie-popup-cookieok.json +25 -0
  71. package/rules/autoconsent/cookielegit.json +21 -0
  72. package/rules/autoconsent/cookieman.json +23 -4
  73. package/rules/autoconsent/cookieprompter.json +21 -0
  74. package/rules/autoconsent/cookiereports.json +10 -0
  75. package/rules/autoconsent/cookietractor.json +37 -0
  76. package/rules/autoconsent/danskebank.json +17 -0
  77. package/rules/autoconsent/de-volksbank.json +14 -0
  78. package/rules/autoconsent/dintel.json +13 -0
  79. package/rules/autoconsent/dpgmedia-nl.json +20 -8
  80. package/rules/autoconsent/e-grip.json +63 -0
  81. package/rules/autoconsent/eccmm.json +30 -0
  82. package/rules/autoconsent/england-nhs-uk.json +18 -0
  83. package/rules/autoconsent/eon.pl.json +18 -0
  84. package/rules/autoconsent/erdee-media.json +24 -0
  85. package/rules/autoconsent/eroticiracconti-it.json +13 -0
  86. package/rules/autoconsent/ersatzteile-24.json +49 -0
  87. package/rules/autoconsent/escorteintime.json +15 -0
  88. package/rules/autoconsent/gays-cruising.json +18 -0
  89. package/rules/autoconsent/gov-pl-cookies-info.json +28 -0
  90. package/rules/autoconsent/gravito.json +23 -16
  91. package/rules/autoconsent/grupa-pracuj.json +73 -0
  92. package/rules/autoconsent/grupa-rmf.json +21 -0
  93. package/rules/autoconsent/gsb-cookiebanner-selection.json +12 -0
  94. package/rules/autoconsent/gwo.json +53 -0
  95. package/rules/autoconsent/hacon-next-gen.json +41 -0
  96. package/rules/autoconsent/hsbc-au.json +35 -0
  97. package/rules/autoconsent/ign-es.json +35 -0
  98. package/rules/autoconsent/impress-co-jp.json +10 -0
  99. package/rules/autoconsent/ing-de.json +31 -0
  100. package/rules/autoconsent/interia.json +1 -4
  101. package/rules/autoconsent/intesasanpaolo.json +37 -0
  102. package/rules/autoconsent/iworks-footer-notice.json +19 -0
  103. package/rules/autoconsent/jobindex.json +40 -0
  104. package/rules/autoconsent/juzaphoto.json +13 -0
  105. package/rules/autoconsent/kaspersky-cookies-notification.json +9 -0
  106. package/rules/autoconsent/lantmateriet.json +13 -0
  107. package/rules/autoconsent/leukhuis-nl.json +35 -0
  108. package/rules/autoconsent/lg-cookiecontrol.json +16 -0
  109. package/rules/autoconsent/libris.nl.json +19 -0
  110. package/rules/autoconsent/lineagrafica-reject.json +32 -0
  111. package/rules/autoconsent/mainichi.json +39 -0
  112. package/rules/autoconsent/mango.json +56 -0
  113. package/rules/autoconsent/matemaks.json +29 -0
  114. package/rules/autoconsent/merito.pl.json +44 -0
  115. package/rules/autoconsent/minitool.json +31 -0
  116. package/rules/autoconsent/miteco.json +41 -0
  117. package/rules/autoconsent/mobile-de.json +13 -0
  118. package/rules/autoconsent/mq-edu-au.json +26 -0
  119. package/rules/autoconsent/nederlandseloterij.json +35 -0
  120. package/rules/autoconsent/nordea.json +21 -0
  121. package/rules/autoconsent/nordnet.json +33 -0
  122. package/rules/autoconsent/nowaera.json +44 -0
  123. package/rules/autoconsent/ok.json +4 -12
  124. package/rules/autoconsent/olvg.json +45 -0
  125. package/rules/autoconsent/onlineradiobox.json +35 -0
  126. package/rules/autoconsent/optyczne-pl.json +28 -0
  127. package/rules/autoconsent/orbys.json +35 -0
  128. package/rules/autoconsent/payfip.json +36 -0
  129. package/rules/autoconsent/pdf24.json +48 -0
  130. package/rules/autoconsent/pe-cookies.json +30 -0
  131. package/rules/autoconsent/pekao24.json +33 -0
  132. package/rules/autoconsent/pensionsmyndigheten.json +13 -0
  133. package/rules/autoconsent/praca-pl.json +17 -0
  134. package/rules/autoconsent/pro-euclp-cookies-box.json +31 -0
  135. package/rules/autoconsent/pubfactory.json +45 -0
  136. package/rules/autoconsent/puesc-gov-pl.json +37 -0
  137. package/rules/autoconsent/rambler.json +30 -0
  138. package/rules/autoconsent/rejestr-io.json +36 -0
  139. package/rules/autoconsent/remove-cookie-notice.json +18 -0
  140. package/rules/autoconsent/rijksoverheid-notification-bar.json +34 -0
  141. package/rules/autoconsent/route-inn.json +13 -0
  142. package/rules/autoconsent/schumacher.com.json +53 -0
  143. package/rules/autoconsent/sedlak-gdpr-cookie.json +10 -0
  144. package/rules/autoconsent/seniorweb.json +42 -0
  145. package/rules/autoconsent/shouldianswer.json +14 -0
  146. package/rules/autoconsent/shueisha-gigaviewer.json +10 -0
  147. package/rules/autoconsent/sitevision.json +18 -1
  148. package/rules/autoconsent/skatteverket-se.json +33 -0
  149. package/rules/autoconsent/smile-insurances.json +13 -0
  150. package/rules/autoconsent/snb.ch.json +18 -0
  151. package/rules/autoconsent/sujetdebac.fr.json +10 -0
  152. package/rules/autoconsent/svt.se.json +7 -7
  153. package/rules/autoconsent/swisscom-ecare.json +31 -0
  154. package/rules/autoconsent/telegram-im.json +36 -0
  155. package/rules/autoconsent/temu.json +12 -16
  156. package/rules/autoconsent/thecode.json +21 -0
  157. package/rules/autoconsent/thomsonlocal.com.json +10 -0
  158. package/rules/autoconsent/todostuslibros.json +51 -0
  159. package/rules/autoconsent/turkishairlines.com.json +33 -0
  160. package/rules/autoconsent/uk-parliament.json +19 -0
  161. package/rules/autoconsent/usherbrooke.json +16 -0
  162. package/rules/autoconsent/vdg-cookies.json +36 -0
  163. package/rules/autoconsent/warszawa19115.pl.json +52 -0
  164. package/rules/autoconsent/wirtualna-polska.json +41 -0
  165. package/rules/autoconsent/wojsko-polskie-pl.json +28 -0
  166. package/rules/autoconsent/xenforo-cookie-consent.json +62 -0
  167. package/rules/autoconsent/xtb.json +18 -0
  168. package/rules/autoconsent/zorginstituut-cookiewarning.json +33 -0
  169. package/rules/autoconsent/zurich-cookies-notification-banner.json +17 -0
  170. package/rules/compact-rules.json +1 -1
  171. package/rules/rule-index.json +1 -1
  172. package/rules/rules.json +1 -1
  173. package/tests/123inkt.spec.ts +5 -0
  174. package/tests/aeolservice.spec.ts +5 -0
  175. package/tests/amazon-music.spec.ts +9 -0
  176. package/tests/animatetimes.spec.ts +3 -0
  177. package/tests/annunci69.spec.ts +5 -0
  178. package/tests/arbeidsplassen-nav-no.spec.ts +3 -0
  179. package/tests/asahi-interactive.spec.ts +6 -0
  180. package/tests/att.spec.ts +3 -0
  181. package/tests/auctionet.spec.ts +3 -0
  182. package/tests/autodoc.spec.ts +8 -0
  183. package/tests/avada-privacy-bar-cosmetic.spec.ts +3 -0
  184. package/tests/avada-privacy-bar.spec.ts +12 -0
  185. package/tests/axesor.spec.ts +3 -0
  186. package/tests/bbc.com-orb-cosmetic.spec.ts +8 -0
  187. package/tests/bbc.com.spec.ts +11 -9
  188. package/tests/blitzrechner.spec.ts +3 -0
  189. package/tests/bokadirekt.se.spec.ts +3 -0
  190. package/tests/bouvet-cookiepanel.spec.ts +9 -0
  191. package/tests/brainsum-cookieconsent.spec.ts +9 -0
  192. package/tests/bwr-cookie.spec.ts +3 -0
  193. package/tests/capitalone-ca.spec.ts +3 -0
  194. package/tests/carpetaciudadana.spec.ts +6 -0
  195. package/tests/cf-gdpr-cookie-consent.spec.ts +3 -0
  196. package/tests/chatgpt.spec.ts +3 -1
  197. package/tests/cksmodal.spec.ts +3 -0
  198. package/tests/consent-studio.spec.ts +15 -0
  199. package/tests/cookie-popup-cookieno.spec.ts +8 -0
  200. package/tests/cookie-popup-cookieok.spec.ts +9 -0
  201. package/tests/cookiebot.spec.ts +1 -0
  202. package/tests/cookielegit.spec.ts +3 -0
  203. package/tests/cookieman.spec.ts +1 -0
  204. package/tests/cookieprompter.spec.ts +9 -0
  205. package/tests/cookiereports.spec.ts +13 -0
  206. package/tests/cookietractor.spec.ts +12 -0
  207. package/tests/danskebank.spec.ts +12 -0
  208. package/tests/de-volksbank.spec.ts +5 -0
  209. package/tests/dintel.spec.ts +10 -0
  210. package/tests/dpgmedia-nl.spec.ts +15 -1
  211. package/tests/e-grip.spec.ts +8 -0
  212. package/tests/eccmm.spec.ts +6 -0
  213. package/tests/england-nhs-uk.spec.ts +3 -0
  214. package/tests/eon.pl.spec.ts +3 -0
  215. package/tests/erdee-media.spec.ts +3 -0
  216. package/tests/eroticiracconti-it.spec.ts +3 -0
  217. package/tests/ersatzteile-24.spec.ts +6 -0
  218. package/tests/escorteintime.spec.ts +3 -0
  219. package/tests/gays-cruising.spec.ts +3 -0
  220. package/tests/gov-pl-cookies-info.spec.ts +3 -0
  221. package/tests/gravito.spec.ts +7 -1
  222. package/tests/grupa-pracuj.spec.ts +8 -0
  223. package/tests/grupa-rmf.spec.ts +5 -0
  224. package/tests/gsb-cookiebanner-selection.spec.ts +7 -0
  225. package/tests/gwo.spec.ts +6 -0
  226. package/tests/hacon-next-gen.spec.ts +6 -0
  227. package/tests/hsbc-au.spec.ts +6 -0
  228. package/tests/ign-es.spec.ts +3 -0
  229. package/tests/impress-co-jp.spec.ts +8 -0
  230. package/tests/ing-de.spec.ts +3 -0
  231. package/tests/interia.spec.ts +1 -1
  232. package/tests/intesasanpaolo.spec.ts +3 -0
  233. package/tests/iworks-footer-notice.spec.ts +3 -0
  234. package/tests/jobindex.spec.ts +3 -0
  235. package/tests/juzaphoto.spec.ts +7 -0
  236. package/tests/kaspersky-cookies-notification.spec.ts +8 -0
  237. package/tests/lantmateriet.spec.ts +3 -0
  238. package/tests/leukhuis-nl.spec.ts +3 -0
  239. package/tests/lg-cookiecontrol.spec.ts +10 -0
  240. package/tests/libris.nl.spec.ts +3 -0
  241. package/tests/lineagrafica-reject.spec.ts +8 -0
  242. package/tests/mainichi.spec.ts +10 -0
  243. package/tests/mango.spec.ts +3 -0
  244. package/tests/matemaks.spec.ts +3 -0
  245. package/tests/merito.pl.spec.ts +3 -0
  246. package/tests/minitool.spec.ts +8 -0
  247. package/tests/miteco.spec.ts +8 -0
  248. package/tests/mobile-de.spec.ts +3 -0
  249. package/tests/mq-edu-au.spec.ts +3 -0
  250. package/tests/nederlandseloterij.spec.ts +9 -0
  251. package/tests/nordea.spec.ts +6 -0
  252. package/tests/nordnet.spec.ts +8 -0
  253. package/tests/nowaera.spec.ts +3 -0
  254. package/tests/olvg.spec.ts +6 -0
  255. package/tests/onlineradiobox.spec.ts +5 -0
  256. package/tests/optyczne-pl.spec.ts +3 -0
  257. package/tests/orbys.spec.ts +3 -0
  258. package/tests/payfip.spec.ts +3 -0
  259. package/tests/pdf24.spec.ts +3 -0
  260. package/tests/pe-cookies.spec.ts +6 -0
  261. package/tests/pekao24.spec.ts +3 -0
  262. package/tests/pensionsmyndigheten.spec.ts +3 -0
  263. package/tests/praca-pl.spec.ts +3 -0
  264. package/tests/pro-euclp-cookies-box.spec.ts +8 -0
  265. package/tests/pubfactory.spec.ts +10 -0
  266. package/tests/puesc-gov-pl.spec.ts +3 -0
  267. package/tests/rambler.spec.ts +3 -0
  268. package/tests/rejestr-io.spec.ts +3 -0
  269. package/tests/remove-cookie-notice.spec.ts +3 -0
  270. package/tests/rijksoverheid-notification-bar.spec.ts +3 -0
  271. package/tests/route-inn.spec.ts +3 -0
  272. package/tests/schumacher.com.spec.ts +6 -0
  273. package/tests/sedlak-gdpr-cookie.spec.ts +6 -0
  274. package/tests/seniorweb.spec.ts +3 -0
  275. package/tests/shouldianswer.spec.ts +8 -0
  276. package/tests/shueisha-gigaviewer.spec.ts +3 -0
  277. package/tests/sitevision-cookie-consent.spec.ts +1 -0
  278. package/tests/sitevision.spec.ts +7 -0
  279. package/tests/skatteverket-se.spec.ts +3 -0
  280. package/tests/smile-insurances.spec.ts +6 -0
  281. package/tests/snb.ch.spec.ts +3 -0
  282. package/tests/sujetdebac.fr.spec.ts +3 -0
  283. package/tests/svt.se.spec.ts +1 -3
  284. package/tests/swisscom-ecare.spec.ts +7 -0
  285. package/tests/telegram-im.spec.ts +6 -0
  286. package/tests/temu.spec.ts +3 -1
  287. package/tests/thecode.spec.ts +3 -0
  288. package/tests/thomsonlocal.com.spec.ts +3 -0
  289. package/tests/todostuslibros.spec.ts +3 -0
  290. package/tests/turkishairlines.com.spec.ts +3 -0
  291. package/tests/uk-parliament.spec.ts +8 -0
  292. package/tests/usherbrooke.spec.ts +6 -0
  293. package/tests/vdg-cookies.spec.ts +8 -0
  294. package/tests/warszawa19115.pl.spec.ts +3 -0
  295. package/tests/wirtualna-polska.spec.ts +8 -0
  296. package/tests/wojsko-polskie-pl.spec.ts +3 -0
  297. package/tests/xenforo-cookie-consent.spec.ts +6 -0
  298. package/tests/xtb.spec.ts +3 -0
  299. package/tests/zorginstituut-cookiewarning.spec.ts +10 -0
  300. package/tests/zurich-cookies-notification-banner.spec.ts +3 -0
  301. package/tests-wtr/heuristics/get-potential-popups.test.ts +330 -9
  302. package/tests-wtr/heuristics/heuristics-utils.test.ts +98 -0
  303. package/rules/autoconsent/twitter.json +0 -34
  304. package/rules/generated/auto_CH_autodoc.de_krd.json +0 -40
  305. package/rules/generated/auto_CH_svtplay.se_d39.json +0 -40
  306. package/rules/generated/auto_GB_parliamentlive.tv_r3v.json +0 -28
  307. package/rules/generated/auto_NL_asnbank.nl_e28.json +0 -40
  308. package/rules/generated/auto_NL_eurojackpot.nederlandseloterij.nl_7hr.json +0 -40
  309. package/rules/generated/auto_NL_lotto.nederlandseloterij.nl_7c8.json +0 -40
  310. package/rules/generated/auto_NL_nederlandseloterij.nl_b60.json +0 -40
  311. package/rules/generated/auto_NL_staatsloterij.nederlandseloterij.nl_197.json +0 -40
  312. package/rules/generated/auto_NO_arronet.se_vo5.json +0 -40
  313. package/rules/generated/auto_NO_liseberg.se_qy4.json +0 -40
  314. package/rules/generated/auto_NO_nordnet.se_2en.json +0 -40
  315. package/rules/generated/auto_NO_regeringen.se_uhv.json +0 -40
  316. package/rules/generated/auto_NO_svtplay.se_ujn.json +0 -40
  317. package/rules/generated/auto_NO_uu.se_eea.json +0 -40
  318. package/tests/generated/auto_CH_autodoc.de_krd.spec.ts +0 -2
  319. package/tests/generated/auto_CH_svtplay.se_d39.spec.ts +0 -2
  320. package/tests/generated/auto_GB_parliamentlive.tv_r3v.spec.ts +0 -6
  321. package/tests/generated/auto_NL_asnbank.nl_e28.spec.ts +0 -6
  322. package/tests/generated/auto_NL_eurojackpot.nederlandseloterij.nl_7hr.spec.ts +0 -6
  323. package/tests/generated/auto_NL_lotto.nederlandseloterij.nl_7c8.spec.ts +0 -6
  324. package/tests/generated/auto_NL_nederlandseloterij.nl_b60.spec.ts +0 -6
  325. package/tests/generated/auto_NL_staatsloterij.nederlandseloterij.nl_197.spec.ts +0 -6
  326. package/tests/generated/auto_NO_arronet.se_vo5.spec.ts +0 -2
  327. package/tests/generated/auto_NO_liseberg.se_qy4.spec.ts +0 -2
  328. package/tests/generated/auto_NO_nordnet.se_2en.spec.ts +0 -2
  329. package/tests/generated/auto_NO_regeringen.se_uhv.spec.ts +0 -6
  330. package/tests/generated/auto_NO_svtplay.se_ujn.spec.ts +0 -2
  331. package/tests/generated/auto_NO_uu.se_eea.spec.ts +0 -2
  332. package/tests/twitter.spec.ts +0 -10
package/lib/heuristics.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { checkHeuristicPatterns, classifyButtons, classifyPopup, isExcludedPopup } from './heuristic-classify';
1
+ import { classifyButtons, classifyPopup, hasHeuristicPatterns, isExcludedPopup } from './heuristic-classify';
2
2
  import { ButtonData, HeuristicLevel, PopupClassification, PopupData } from './types';
3
3
  import { isElementVisible, isTopFrame } from './utils';
4
4
 
@@ -8,69 +8,191 @@ const POPUP_SEARCH_MAX_TIME = 100;
8
8
  export function getActionablePopups(mode: HeuristicLevel = 'reject', timeout = POPUP_SEARCH_MAX_TIME): PopupData[] {
9
9
  const acceptedLevels: PopupClassification[] =
10
10
  mode === 'tier2' ? ['reject', 'tier1', 'tier2'] : mode === 'tier1' ? ['reject', 'tier1'] : ['reject'];
11
- if (acceptedLevels.length === 0) {
12
- return [];
13
- }
14
- const popups = getPotentialPopups(timeout);
15
- const result = popups.reduce((acc, popup) => {
11
+ const popups = getPotentialPopups(timeout).filter((popup) => {
16
12
  const popupText = popup.text?.trim();
17
- if (popupText) {
18
- if (isExcludedPopup(popupText)) {
19
- return acc;
20
- }
21
- const { patterns } = checkHeuristicPatterns(popupText);
22
- if (patterns.length > 0) {
23
- classifyButtons(popup.buttons);
24
- popup.regexClassification = classifyPopup(popup.buttons);
25
- acc.push({
26
- ...popup,
27
- });
28
- }
13
+ if (!popupText || isExcludedPopup(popupText) || !hasHeuristicPatterns(popupText)) {
14
+ return false;
29
15
  }
30
- return acc;
31
- }, [] as PopupData[]);
32
- // popups filtered by mode and sorted so a popup with reject will always win.
33
- return result
34
- .filter((popup) => popup.regexClassification !== undefined && acceptedLevels.includes(popup.regexClassification))
35
- .sort((a, b) => ((a.regexClassification ?? '') > (b.regexClassification ?? '') ? 1 : -1));
16
+ popup.regexClassification = classifyPopup(popup.buttons);
17
+ return popup.regexClassification !== undefined && acceptedLevels.includes(popup.regexClassification);
18
+ });
19
+ // a popup with reject will always win
20
+ return popups.sort((a, b) => ((a.regexClassification ?? '') > (b.regexClassification ?? '') ? 1 : -1));
36
21
  }
37
22
 
23
+ /**
24
+ * @param timeout time budget for the whole search, in ms
25
+ */
38
26
  export function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME): PopupData[] {
39
- const isFramed = !isTopFrame();
40
- // do not inspect frames that are more than one level deep
41
- if (isFramed && window.parent && window.parent !== window.top) {
42
- return [];
27
+ if (!isTopFrame()) {
28
+ // do not inspect frames that are more than one level deep
29
+ if (window.parent && window.parent !== window.top) {
30
+ return [];
31
+ }
32
+ // for iframes, just take the whole document
33
+ const frameElement = getFramePopupElement(document);
34
+ const popup = frameElement && toPopupData(frameElement);
35
+ return popup ? [popup] : [];
36
+ }
37
+ const deadline = performance.now() + timeout;
38
+ // fixed, sticky and dialog-like elements, with fragments expanded to their popup root
39
+ const { elements, shadowHosts } = getPopupLikeElements(deadline);
40
+ const potentialPopups: PopupData[] = [];
41
+ const expanded: HTMLElement[] = [];
42
+ for (const el of elements) {
43
+ const popup = toPopupData(el, shadowHosts);
44
+ if (!popup) {
45
+ continue;
46
+ }
47
+ // past the deadline, keep the candidates found so far as they are
48
+ const root = performance.now() > deadline ? popup : expandPopupFragment(popup, shadowHosts);
49
+ if (potentialPopups.some((p) => p.element === root.element)) {
50
+ continue;
51
+ }
52
+ if (root !== popup) {
53
+ expanded.push(root.element);
54
+ }
55
+ potentialPopups.push(root);
56
+ }
57
+ // a fragment expanded to its popup root replaces any other candidates inside that root
58
+ return potentialPopups.filter((p) => !expanded.some((root) => root !== p.element && composedContains(root, p.element)));
59
+ }
60
+
61
+ function toPopupData(el: HTMLElement, shadowHosts?: HTMLElement[], text = getElementText(el)): PopupData | null {
62
+ if (!text) {
63
+ return null;
43
64
  }
65
+ const buttons = getButtonData(el, shadowHosts);
66
+ classifyButtons(buttons);
67
+ return { text, element: el, buttons };
68
+ }
44
69
 
45
- return collectPotentialPopups(isFramed, timeout);
70
+ // buttons beyond their labels, and at least one button the heuristics recognise (not just policy links)
71
+ function hasPopupButtons(text: string, buttons: ButtonData[]): boolean {
72
+ return !isOnlyButtonText(text, buttons) && buttons.some((b) => b.regexClassification !== 'other');
46
73
  }
47
74
 
48
- function collectPotentialPopups(isFramed: boolean, timeout = POPUP_SEARCH_MAX_TIME): PopupData[] {
49
- let elements = [];
50
- if (!isFramed) {
51
- elements = getPopupLikeElements(timeout);
52
- } else {
53
- // for iframes, just take the whole document
54
- const doc = document.body || document.documentElement;
55
- if (doc && isElementVisible(doc) && doc.innerText) {
56
- elements.push(doc);
75
+ // detect patterns include button wording, so a bare button bar matches; expand it so exclusions and crawl text see the whole popup
76
+ function isOnlyButtonText(text: string, buttons: ButtonData[]): boolean {
77
+ let rest = text;
78
+ for (const button of buttons) {
79
+ rest = rest.replace(button.text, '');
80
+ }
81
+ return rest.replace(/\s+/g, '').length < 10;
82
+ }
83
+
84
+ // Bounds how far expansion climbs. The stop at page-level containers is the main guard against false positives;
85
+ // this limit only keeps the search cheap on deep trees.
86
+ const MAX_FRAGMENT_EXPANSION_DEPTH = 6;
87
+
88
+ /**
89
+ * A positioned candidate is often only part of a popup, such as a sticky button bar or heading.
90
+ * If it does not look like a consent popup on its own, use the nearest ancestor that does.
91
+ */
92
+ function expandPopupFragment(popup: PopupData, shadowHosts?: HTMLElement[]): PopupData {
93
+ if (hasHeuristicPatterns(popup.text) && hasPopupButtons(popup.text, popup.buttons)) {
94
+ return popup;
95
+ }
96
+ let el = getComposedParent(popup.element);
97
+ for (let depth = 0; el && depth < MAX_FRAGMENT_EXPANSION_DEPTH; depth++, el = getComposedParent(el)) {
98
+ // stop before reaching page-level containers, whose text includes unrelated page copy
99
+ if (el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
100
+ break;
101
+ }
102
+ // check the text first: it is cheaper than collecting buttons
103
+ const text = getElementText(el);
104
+ if (!text || !hasHeuristicPatterns(text)) {
105
+ continue;
106
+ }
107
+ const root = toPopupData(el, shadowHosts, text);
108
+ if (root && hasPopupButtons(text, root.buttons)) {
109
+ return root;
57
110
  }
58
111
  }
112
+ return popup;
113
+ }
59
114
 
60
- const potentialPopups: PopupData[] = [];
115
+ function getComposedParent(el: HTMLElement): HTMLElement | null {
116
+ if (el.parentElement) {
117
+ return el.parentElement;
118
+ }
119
+ const parent = el.parentNode;
120
+ return parent instanceof ShadowRoot && parent.host instanceof HTMLElement ? parent.host : null;
121
+ }
61
122
 
62
- // for each potential popup, get the buttons
63
- for (const el of elements) {
64
- if (el.innerText) {
65
- potentialPopups.push({
66
- text: el.innerText,
67
- element: el,
68
- buttons: getButtonData(el),
69
- });
123
+ /**
124
+ * The whole document of a frame, if it is rendered and has text. The body can have zero height when all of its
125
+ * content is positioned, so only check visibility styles, not size.
126
+ */
127
+ export function getFramePopupElement(doc: Document): HTMLElement | null {
128
+ const el = doc.body || doc.documentElement;
129
+ const win = doc.defaultView;
130
+ if (!el || !win || win.innerWidth === 0 || win.innerHeight === 0 || !isStyleVisible(el)) {
131
+ return null;
132
+ }
133
+ return getElementText(el).trim() ? el : null;
134
+ }
135
+
136
+ // catches elements hidden by an ancestor's visibility; checkVisibility is not available in older browsers.
137
+ // Opacity is not checked: prehide hides popups with opacity: 0, and they must stay detectable.
138
+ function isStyleVisible(el: HTMLElement): boolean {
139
+ return typeof el.checkVisibility !== 'function' || el.checkVisibility({ checkVisibilityCSS: true });
140
+ }
141
+
142
+ /**
143
+ * Rendered text of an element, including the content of its own open shadow root.
144
+ */
145
+ function getElementText(el: HTMLElement): string {
146
+ const text = el.innerText || '';
147
+ if (!el.shadowRoot) {
148
+ return text;
149
+ }
150
+ // innerText of a shadow host does not include its shadow content
151
+ const shadowTexts = Array.from(el.shadowRoot.children, (child) => (isRenderedTextElement(child) ? getElementText(child) : ''));
152
+ return [text, ...shadowTexts].filter((t) => t.trim()).join('\n');
153
+ }
154
+
155
+ // innerText of an element that is not rendered falls back to textContent, which would leak hidden text
156
+ function isRenderedTextElement(el: Element): el is HTMLElement {
157
+ return (
158
+ el instanceof HTMLElement &&
159
+ !['STYLE', 'SCRIPT', 'TEMPLATE', 'NOSCRIPT', 'LINK', 'SLOT'].includes(el.tagName) &&
160
+ window.getComputedStyle(el).display !== 'none'
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Like Node.contains, but also looks through shadow root boundaries.
166
+ */
167
+ function composedContains(container: Node, node: Node): boolean {
168
+ if (container.contains(node)) {
169
+ return true;
170
+ }
171
+ let root = node.getRootNode();
172
+ while (root instanceof ShadowRoot) {
173
+ if (container.contains(root.host)) {
174
+ return true;
70
175
  }
176
+ root = root.host.getRootNode();
71
177
  }
178
+ return false;
179
+ }
180
+
181
+ /**
182
+ * Stricter visibility check for popup candidates: catches panels hidden by an ancestor's visibility,
183
+ * and elements outside the viewport, which isElementVisible lets through.
184
+ */
185
+ function isPopupCandidateVisible(el: HTMLElement): boolean {
186
+ return isElementVisible(el) && isStyleVisible(el) && isOnScreen(el);
187
+ }
72
188
 
73
- return potentialPopups;
189
+ // A zero-size wrapper can still show its content: then any child that is on screen counts, unless the wrapper clips it.
190
+ function isOnScreen(el: Element): boolean {
191
+ const rect = el.getBoundingClientRect();
192
+ if (rect.width === 0 || rect.height === 0) {
193
+ return window.getComputedStyle(el).overflow === 'visible' && Array.from(el.children).some((child) => isOnScreen(child));
194
+ }
195
+ return rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth;
74
196
  }
75
197
 
76
198
  export function isDialogLikeElement(node: HTMLElement): boolean {
@@ -85,64 +207,99 @@ export function isDialogLikeElement(node: HTMLElement): boolean {
85
207
 
86
208
  /**
87
209
  * Heuristic to get all elements that look like "popups"
88
- * TODO: this heuristic is too strict, not all popups are actually sticky/fixed
210
+ * TODO: popups in the normal page flow are only found when part of them is fixed, sticky or dialog-like
89
211
  */
90
- function getPopupLikeElements(timeout = POPUP_SEARCH_MAX_TIME): HTMLElement[] {
91
- const startTime = performance.now();
92
- const walker = document.createTreeWalker(
93
- document.documentElement,
94
- NodeFilter.SHOW_ELEMENT, // visit only element nodes
95
- {
96
- acceptNode(node: HTMLElement) {
97
- if (node.tagName === 'BODY') {
98
- return NodeFilter.FILTER_SKIP;
99
- }
100
- if (isElementVisible(node)) {
101
- const cssPosition = window.getComputedStyle(node).position;
102
- if (cssPosition === 'fixed' || cssPosition === 'sticky') {
103
- return NodeFilter.FILTER_ACCEPT;
212
+ function getPopupLikeElements(deadline: number): { elements: HTMLElement[]; shadowHosts: HTMLElement[] } {
213
+ const found: HTMLElement[] = [];
214
+ const shadowHosts: HTMLElement[] = [];
215
+ // open shadow roots are walked after the tree that hosts them
216
+ const roots: Node[] = [document.documentElement];
217
+ for (let i = 0; i < roots.length; i++) {
218
+ if (i > 0 && performance.now() > deadline) {
219
+ break;
220
+ }
221
+ const walker = document.createTreeWalker(
222
+ roots[i],
223
+ NodeFilter.SHOW_ELEMENT, // visit only element nodes
224
+ {
225
+ acceptNode(node: HTMLElement) {
226
+ if (node.shadowRoot) {
227
+ shadowHosts.push(node);
228
+ roots.push(node.shadowRoot);
229
+ }
230
+ if (node.tagName === 'BODY') {
231
+ return NodeFilter.FILTER_SKIP;
104
232
  }
105
- if (isDialogLikeElement(node)) {
233
+ const style = window.getComputedStyle(node);
234
+ // nothing under display: none can be visible, and styling hidden subtrees is slow
235
+ if (style.display === 'none') {
236
+ return NodeFilter.FILTER_REJECT;
237
+ }
238
+ const cssPosition = style.position;
239
+ if (
240
+ (cssPosition === 'fixed' || cssPosition === 'sticky' || isDialogLikeElement(node)) &&
241
+ isPopupCandidateVisible(node)
242
+ ) {
106
243
  return NodeFilter.FILTER_ACCEPT;
107
244
  }
108
- }
109
- // start rejecting after POPUP_SEARCH_MAX_TIME to avoid blocking the main thread
110
- if (performance.now() - startTime > timeout) {
111
- return NodeFilter.FILTER_REJECT;
112
- }
113
- return NodeFilter.FILTER_SKIP;
245
+ // start rejecting after the deadline to avoid blocking the main thread
246
+ if (performance.now() > deadline) {
247
+ return NodeFilter.FILTER_REJECT;
248
+ }
249
+ return NodeFilter.FILTER_SKIP;
250
+ },
114
251
  },
115
- },
116
- );
117
- const found = [];
118
- for (let node = walker.nextNode(); node; node = walker.nextNode()) {
119
- found.push(node as HTMLElement);
252
+ );
253
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
254
+ found.push(node as HTMLElement);
255
+ }
120
256
  }
121
257
  // Empty positioned children should not displace a contentful popup container.
122
- return excludeContainers(found.filter((element) => element.innerText?.trim()));
258
+ return { elements: excludeContainers(found.filter((element) => getElementText(element).trim())), shadowHosts };
123
259
  }
124
260
 
125
261
  /**
126
262
  * Serialize all actionable buttons on the page
263
+ * @param shadowHosts open shadow hosts on the page, if already known; otherwise they are searched for under el
127
264
  */
128
- export function getButtonData(el: HTMLElement): ButtonData[] {
129
- const actionableButtons = excludeContainers(getButtonLikeElements(el)).filter(
130
- (b) =>
131
- isElementVisible(b) &&
132
- !isDisabled(b) &&
133
- (b.innerText?.trim() ||
134
- // <input> values do not appear in innerText
135
- (b instanceof HTMLInputElement && ['submit', 'button'].includes(b.type) && b.value?.trim())),
136
- );
265
+ export function getButtonData(el: HTMLElement, shadowHosts?: HTMLElement[]): ButtonData[] {
266
+ const buttons: ButtonData[] = [];
267
+ for (const b of excludeContainers(getButtonLikeElements(el, shadowHosts))) {
268
+ if (!isElementVisible(b) || isDisabled(b)) {
269
+ continue;
270
+ }
271
+ const text =
272
+ getElementText(b).trim() ||
273
+ // <input> values do not appear in innerText
274
+ (b instanceof HTMLInputElement && ['submit', 'button'].includes(b.type) ? b.value?.trim() : '');
275
+ if (text) {
276
+ buttons.push({ text, element: b });
277
+ }
278
+ }
279
+ return buttons;
280
+ }
137
281
 
138
- return actionableButtons.map((b) => ({
139
- text: (b.innerText || b.textContent || '').trim() || (b as HTMLInputElement).value?.trim() || '',
140
- element: b,
141
- }));
282
+ function getButtonLikeElements(el: HTMLElement, shadowHosts = findShadowHosts(el)): HTMLElement[] {
283
+ const buttons = Array.from(el.querySelectorAll<HTMLElement>(BUTTON_LIKE_ELEMENT_SELECTOR));
284
+ for (const host of shadowHosts) {
285
+ if (composedContains(el, host)) {
286
+ buttons.push(...host.shadowRoot!.querySelectorAll<HTMLElement>(BUTTON_LIKE_ELEMENT_SELECTOR));
287
+ }
288
+ }
289
+ return buttons;
142
290
  }
143
291
 
144
- function getButtonLikeElements(el: HTMLElement): HTMLElement[] {
145
- return Array.from(el.querySelectorAll(BUTTON_LIKE_ELEMENT_SELECTOR));
292
+ // all open shadow hosts at or under root, including those nested in other shadow roots
293
+ function findShadowHosts(root: HTMLElement | ShadowRoot): HTMLElement[] {
294
+ const hosts: HTMLElement[] = [];
295
+ const candidates =
296
+ root instanceof HTMLElement ? [root, ...root.querySelectorAll<HTMLElement>('*')] : root.querySelectorAll<HTMLElement>('*');
297
+ for (const el of candidates) {
298
+ if (el.shadowRoot) {
299
+ hosts.push(el, ...findShadowHosts(el.shadowRoot));
300
+ }
301
+ }
302
+ return hosts;
146
303
  }
147
304
 
148
305
  export function isDisabled(el: HTMLElement): boolean {
@@ -154,20 +311,15 @@ export function isDisabled(el: HTMLElement): boolean {
154
311
  * Leave only elements that do not contain any other elements
155
312
  */
156
313
  export function excludeContainers(elements: HTMLElement[]): HTMLElement[] {
157
- const results = [];
158
- if (elements.length > 0) {
159
- for (let i = elements.length - 1; i >= 0; i--) {
160
- let container = false;
161
- for (let j = 0; j < elements.length; j++) {
162
- if (i !== j && elements[i].contains(elements[j])) {
163
- container = true;
164
- break;
165
- }
166
- }
167
- if (!container) {
168
- results.push(elements[i]);
314
+ // mark every element that is an ancestor of another one, instead of comparing all pairs
315
+ const candidates = new Set<Node>(elements);
316
+ const containers = new Set<Node>();
317
+ for (const el of elements) {
318
+ for (let node = el.parentNode; node; node = node.parentNode || (node instanceof ShadowRoot ? node.host : null)) {
319
+ if (candidates.has(node)) {
320
+ containers.add(node);
169
321
  }
170
322
  }
171
323
  }
172
- return results;
324
+ return elements.filter((el) => !containers.has(el)).reverse();
173
325
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duckduckgo/autoconsent",
3
- "version": "16.58.0",
3
+ "version": "16.60.0",
4
4
  "description": "",
5
5
  "types": "./dist/types/web.d.ts",
6
6
  "exports": {
@@ -48,7 +48,7 @@
48
48
  "author": "Sam Macbeth",
49
49
  "license": "MPL-2.0",
50
50
  "devDependencies": {
51
- "@duckduckgo/eslint-config": "github:duckduckgo/eslint-config#v0.3.1",
51
+ "@duckduckgo/eslint-config": "github:duckduckgo/eslint-config#v0.4.0",
52
52
  "@esm-bundle/chai": "^4.3.4-fix.0",
53
53
  "@octokit/rest": "^22.0.1",
54
54
  "@playwright/test": "^1.63.0",
@@ -72,9 +72,10 @@
72
72
  "jsonc-parser": "^3.3.1",
73
73
  "markdown-it": "^15.0.2",
74
74
  "mocha": "^12.0.3",
75
- "prettier": "3.9.9",
75
+ "prettier": "3.9.10",
76
76
  "randexp": "^0.5.3",
77
77
  "sinon": "^22.0.0",
78
+ "tldts-experimental": "^7.4.3",
78
79
  "ts-json-schema-generator": "^2.3.0",
79
80
  "ts-node": "^10.9.2",
80
81
  "tslib": "^2.4.0",
@@ -198,8 +199,5 @@
198
199
  "plugins": [
199
200
  "npm"
200
201
  ]
201
- },
202
- "dependencies": {
203
- "tldts-experimental": "^7.4.3"
204
202
  }
205
203
  }