Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions platform/mv3/ADN-MV3-TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# AdNauseam MV3 — Implementation TODO

Status of the MV3 port of AdNauseam, with concrete next tasks.
Backend (collect → store → visit) is largely working; the UI layer and
image persistence are the main gaps.

Legend: ✅ done · 🟡 partial · ❌ missing

---

## P0 — Image persistence (fixes the `placeholder.svg` bug)

**Problem:** the parser stores the *remote* image URL, not the image bytes.
On display, the popup/vault re-fetches the URL, which is blocked by DNR or
expired → `onerror` → `img/placeholder.svg`. MV2 stored ads as base64 data URIs
via `fetchImageAsBase64` (`src/js/adn/adn-utils.js:447`), which is unported.

**Constraint:** the MV3 service worker has no DOM/canvas, so conversion must run
in a context that has one: **parser.js (content script)** or the **offscreen doc**.

Tasks:
- [ ] Port `fetchImageAsBase64` (canvas → `toDataURL`) into `parser.js`.
- [ ] In `parser.js`, convert `data.imgSrc` to a data URI *before* `sendMessage('registerAd')`;
stop preferring remote URLs over data URIs (`parser.js:175-181`).
- [ ] Cap stored image size (resize on canvas) to keep `chrome.storage.local` sane.
- [ ] Fallback: if conversion fails, keep the remote URL + current `onerror` path.

**Acceptance:** collected ads show real thumbnails in the popup and persist
across reloads with no network re-fetch.

---

## P1 — Vault UI (hard blocker)

**Problem:** `vault.html` loads `js/adn/vault.js` which **does not exist**.
MV2 vault is `src/js/adn/vault.js` (~2441 lines).

Tasks:
- [ ] Create `platform/mv3/extension/js/adn/vault.js` (ES module).
- [ ] Wire to existing background API: `adsForVault`, `getAdNauseamStats`,
`deleteAd`, `deleteAdSet`, `purgeDeadAds`, `getCostPerClick`,
`getHideDeadAds`/`setHideDeadAds`, `clearAds`, `exportAds`/`importAds`.
- [ ] Port: ad grid/bubble layout, zoom, hover detail, cost-per-click display,
filter-by-page, dead-ad hiding/purge, export.
- [ ] Listen for broadcasts (`adDetected`, `adAttempt`, `adVisited`) for live updates.

**Acceptance:** opening the vault renders all collected ads with working
zoom/delete/export and live updates.

---

## P2 — Settings / options UI

**Problem:** settings only exist in background handlers; no UI to change them.
MV2 is `src/js/adn/options.js` (~423 lines).

Tasks:
- [ ] Add an AdNauseam pane to `dashboard.html` (or a dedicated options page).
- [ ] Expose: `clickingAds`, `clickProbability`, `costPerClick`, `blurCollectedAds`,
`hideDeadAds`, `clickOnlyWhenIdleFor`, `disableWarnings`.
- [ ] Persist via `getAdnSettings`/`setAdnSettings` (already in `background.js`).

**Acceptance:** toggling settings persists and changes runtime behavior
(esp. `clickingAds` start/stop, `clickProbability`).

---

## P3 — Visit queue robustness

**Problem:** `visitor.js` uses `setInterval`, which dies when the SW is killed;
idle detection is a stub (`visitor.js:168`).

Tasks:
- [ ] Replace `setInterval` polling with `chrome.alarms` so the queue survives
SW termination.
- [ ] Wire real idle detection via `chrome.idle` for `clickOnlyWhenIdleFor`.
- [ ] Verify offscreen-doc lifecycle (creation/teardown) under SW restarts.

**Acceptance:** clicking continues after the SW is evicted; idle setting honored.

---

## P4 — Notifications system

**Problem:** `getNotifications` is stubbed to `[]` (`background.js:402`).
MV2 is `src/js/adn/notifications.js` (~482 lines).

Tasks:
- [ ] Port notification rules (ad-blocker conflicts, DNT, warnings).
- [ ] Drive the menu warning/alert badges from real data.

---

## P5 — Remaining MV2 modules

- [ ] `dnt.js` (~173 lines) — EFF Do Not Track allowlist integration.
- [ ] `firstrun.js` — welcome/onboarding page.
- [ ] `strictblocklist.js` — strict-block list support.
- [ ] `tests.js` — port beyond the existing `test-adn-allow.js`.

---

## Verify (cross-cutting)

- [ ] Confirm `adn-allow` ruleset is actually built and enabled at runtime so
ads load on the page (Layer A of collection). Check built `rulesets/main/adn-allow.json`.
- [ ] Confirm element-hiding uses `opacity:0` (not `display:none`) so ads stay
clickable for click-obfuscation.
- [ ] End-to-end smoke test: load page with ads → collected → visited → shown
in popup + vault.

---

## Recently fixed
- ✅ `recent-ads` class now toggled on `#ad-list-items` when showing recent ads
(`menu.js` `renderAdList`).
4 changes: 4 additions & 0 deletions platform/mv3/extension/css/menu.css
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ header {
border-radius: 3px;
}

.recent-ads {
margin: 125px 30px 0px 0px;
}

/* Alert */
.alert {
display: flex;
Expand Down
27 changes: 11 additions & 16 deletions platform/mv3/extension/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,23 @@
<!-- -------- -->
<header>
<nav id="dashboard-nav">
<span class="logo"><img data-i18n-title="extName" src="img/ublock.svg" alt="uBO Lite"></span><!--
<span class="logo"><img data-i18n-title="extName" src="img/ublock.svg" alt="AdNauseam"></span><!--
--><button class="tabButton" type="button" data-pane="settings" data-i18n="settingsPageName" tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="rulesets" data-i18n="aboutFilterLists" tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="filters" data-i18n="customFiltersPageName"tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="develop" data-i18n="developPageName"tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="about" data-i18n="aboutPageName" tabindex="0"></button>
</nav>
<!-- -------- -->
<header>
<nav id="dashboard-nav">
<span class="logo"><img data-i18n-title="extName" src="img/ublock.svg" alt="AdNauseam"></span><!--
--><button class="tabButton" type="button" data-pane="settings" data-i18n="settingsPageName" tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="rulesets" data-i18n="aboutFilterLists" tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="filters" data-i18n="customFiltersPageName"tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="develop" data-i18n="developPageName"tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="about" data-i18n="aboutPageName" tabindex="0"></button>
</nav>
<!-- -------- -->
<div data-pane-related="rulesets">
<p><span id="commit-spinner" class="fa-icon">spinner</span> <span id="listsOfBlockedHostsPrompt"></span></p>
<p id="dnrError"></p>
<p><input id="findInLists" type="search" spellcheck="false" placeholder="findListsPlaceholder" /></p>
</div>
<div data-pane-related="rulesets">
<p><span id="commit-spinner" class="fa-icon">spinner</span> <span id="listsOfBlockedHostsPrompt"></span></p>
<p id="dnrError"></p>
<p><input id="findInLists" type="search" spellcheck="false" placeholder="findListsPlaceholder" /></p>
</div>
</header>

<!-- -------- -->
<section data-pane="settings">
<div>
<h3 data-i18n="behaviorSectionLabel"></h3>
<p><label id="autoReload" data-i18n="autoReloadLabel"><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span>_</label>
Expand All @@ -59,6 +52,7 @@ <h3 data-i18n="behaviorSectionLabel"></h3>
<p data-platform-exclude="safari"><label id="strictBlockMode" data-i18n="enableStrictBlockLabel"><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span>_</label><legend data-i18n="enableStrictBlockLegend"></legend>
<p><label id="popupBlockMode" data-i18n="enablePopupBlockLabel"><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span>_</label><legend data-i18n="enablePopupBlockLegend"></legend>
<p id="developerMode"><label data-i18n="developerModeLabel"><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span>_</label><legend data-i18n="developerModeLegend"></legend>
<p id="adnHidingStyle"><label><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span>Hide ads with display:none instead of opacity:0</label><legend>Removes hidden ads from the page layout. Note: fewer ads may load or be collected for clicking.</legend>
</div>
<div>
<h3 data-i18n="settingsBackupRestoreLabel">_</h3>
Expand Down Expand Up @@ -263,6 +257,7 @@ <h3 class="listname"></h3>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard.js" type="module"></script>
<script src="js/settings.js" type="module"></script>
<script src="js/adn/options.js" type="module"></script>
<script src="js/filter-manager-ui.js" type="module"></script>
<script src="js/develop.js" type="module"></script>

Expand Down
Binary file modified platform/mv3/extension/img/icon_128_off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified platform/mv3/extension/img/icon_16_off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified platform/mv3/extension/img/icon_32_off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified platform/mv3/extension/img/icon_64_off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions platform/mv3/extension/img/placeholder.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 77 additions & 2 deletions platform/mv3/extension/js/adn/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ async function initialize() {
validateAdStorage();
computeNextId();
adsetSize = adCount();
adIndex = null;

log('[ADN INIT] Loaded ' + adsetSize + ' ads');
initialized = true;
Expand Down Expand Up @@ -139,6 +140,17 @@ function parseHostnameFromUrl(url) {
}
}

// Detect strings that are mistakenly JavaScript/CSS code rather than ad text.
function looksLikeCode(s) {
if (typeof s !== 'string' || s.length === 0) return false;
return /^\s*[!;(]*\s*function\b/.test(s) || // function… / (function… / !function
/\bfunction\s*\(/.test(s) || // function(
/\b(var|let|const)\s+[\w$]+\s*=/.test(s) || // var u = … / let x =
/=>\s*[{(]/.test(s) || // arrow function body
/\b(document|window)\.\w/.test(s) || // document.x / window.x
(s.match(/;/g) || []).length >= 2; // multiple statements
}

function validate(ad) {
if (!validateFields(ad)) {
return warn('Invalid ad-fields: ', ad);
Expand All @@ -165,7 +177,13 @@ function validate(ad) {
warn('Invalid ad type: ' + ct);
}

return validateTarget(ad);
if (!validateTarget(ad)) return false;

// Reject titles/text that are mistakenly JavaScript/CSS code (MV2 parity).
if (looksLikeCode(ad.title)) ad.title = ad.targetDomain || 'Pending';
if (ct === 'text' && looksLikeCode(cd.text)) cd.text = '';

return true;
}

function validateTarget(ad) {
Expand Down Expand Up @@ -319,6 +337,49 @@ function adCount() {
return adlist().length;
}

// Normalize an image URL to origin+path (ignoring query) for duplicate checks.
function srcPath(src) {
if (!src) return '';
try {
const u = new URL(src);
return u.origin + u.pathname;
} catch (e) {
return src;
}
}

// Index of existing ads keyed by targetUrl and image src path, for O(1)
// duplicate lookups. Lazily (re)built; invalidated (set null) when ads change.
let adIndex = null;

function indexAd(ad) {
if (adIndex === null) return;
if (ad.targetUrl) adIndex.set('t:' + ad.targetUrl, ad);
const sp = ad.contentData ? srcPath(ad.contentData.src) : '';
if (sp) adIndex.set('s:' + sp, ad);
}

function buildAdIndex() {
adIndex = new Map();
const ads = adlist();
for (let i = 0; i < ads.length; i++) indexAd(ads[i]);
}

// Find an existing ad that is the same by targetUrl OR by image src path.
function findDuplicateAd(ad) {
if (adIndex === null) buildAdIndex();
if (ad.targetUrl) {
const hit = adIndex.get('t:' + ad.targetUrl);
if (hit) return hit;
}
const sp = ad.contentData ? srcPath(ad.contentData.src) : '';
if (sp) {
const hit = adIndex.get('s:' + sp);
if (hit) return hit;
}
return null;
}

function adById(id) {
const list = adlist();
for (let i = 0; i < list.length; i++) {
Expand Down Expand Up @@ -363,6 +424,13 @@ async function registerAd(ad, tab) {
return warn('[ADN INTERN] Ignoring Ad on ' + ad.pageDomain + ', target: ' + ad.targetUrl);
}

// Skip duplicates across all pages by targetUrl OR image src path.
const dup = findDuplicateAd(ad);
if (dup) {
log('[ADN EXISTS] Duplicate of Ad#' + (dup.id || '?') + ': ' + ad.targetUrl);
return;
}

const pageHash = YaMD5.hashStr(ad.pageUrl);
if (!admap[pageHash]) admap[pageHash] = {};

Expand All @@ -389,6 +457,7 @@ async function registerAd(ad, tab) {

// Store in admap (overwrites older ad with same key)
admap[pageHash][adhash] = ad;
indexAd(ad);
adsetSize++;

log('[ADN FOUND] Ad#' + ad.id + ' (' + ad.contentType + ') ' + ad.targetUrl);
Expand All @@ -415,6 +484,7 @@ async function deleteAd(arg) {

if (removeAdFromMap(ad)) {
adsetSize--;
adIndex = null;
log('[ADN DELETE] Ad#' + (ad.id || '?'));
await storeAdData(true);
} else {
Expand All @@ -434,6 +504,7 @@ async function clearAds() {
admap = {};
adsetSize = 0;
idgen = 0;
adIndex = null;
await chrome.storage.local.set({ admap: {}, adnIdgen: 0 });
log('[ADN] All ads cleared');
}
Expand Down Expand Up @@ -483,6 +554,7 @@ async function purgeDeadAds(deadAdIds) {
}
}
adsetSize = adCount();
adIndex = null;
await storeAdData(true);
return { data: adlist() };
}
Expand Down Expand Up @@ -563,6 +635,7 @@ async function importAds(data) {
}
computeNextId();
adsetSize = adCount();
adIndex = null;
await storeAdData(true);
return { count };
}
Expand All @@ -586,6 +659,7 @@ async function importAds(data) {
}

adsetSize = adCount();
adIndex = null;
await storeAdData(true);
return { count };
}
Expand All @@ -609,7 +683,8 @@ async function getSettings() {
showIconBadge: true,
disableWarnings: false,
blurCollectedAds: false,
costPerClick: 1.58
costPerClick: 1.58,
hidingStyle: 'opacity' // 'opacity' (clickable/collectable) | 'display' (display:none)
}, data.adnSettings || {});
}

Expand Down
Loading