pageta/apps/extension/background.js
Till JS 3dbd6611f2 feat: mehrseitige Artikel vollständig sichern
Golems `…-2607-211258.html` hat drei Seiten; gespeichert wurde bisher
Seite 1 — 911 von rund 2400 Wörtern. Serverseitig ist da nichts zu holen:
Seite 2 liegt hinter derselben Zustimmungs-Wand wie Seite 1. Ein `fetch`
aus dem Tab bekommt sie (gemessen: 200, kein Redirect). Also sammelt der
Tab sie.

- `page-collector.js` (`collectPageta`) findet Folgeseiten ohne
  quellenspezifische Selektoren: Zähler-Adressen (`-2.html`, `?page=2`,
  `/seite/2/`), sonst die `rel="next"`-Kette. Deckel 20 Seiten / 6 MB,
  300 ms Pause je Abruf. Läuft im Bookmarklet (Quelltext in der
  `javascript:`-Adresse) und in der Erweiterung (`executeScript`) —
  eine Quelle, zeichengleiche Kopie, von `check.mjs` erzwungen.
- Weiches Strippen vor dem Senden: `<style>/<noscript>/<template>` raus,
  Skript-Inhalte geleert, Tags/Attribute und JSON-LD bleiben. Sonst
  verlören `siteNameFromJsonLd` und die CMP-Signaturen ihre Grundlage.
  Extraktions-Ergebnis nachweislich unverändert (the-decoder, winfuture).
- API nimmt `pages` (max 20) zusätzlich zu `html` und antwortet mit
  `pageCount`; `merge-pages.ts` fügt in Lese-Reihenfolge zusammen.
  `html` bleibt gültig — Bookmarklets werden nicht mitdeployt.
- Fehlt eine erkannte Seite, springt die add-Seite nicht weiter, sondern
  sagt „x von y Seiten".

Am Golem-Artikel verifiziert: 3 Seiten, 0 fehlend, 298 KB statt ~1,2 MB.
Wer ein Bookmarklet installiert hat, muss es einmal neu ablegen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 14:25:03 +02:00

125 lines
4.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Pageta-Browser-Erweiterung — Hintergrund-Dienst.
*
* Warum es diese Erweiterung gibt: Manche Quellen antworten auf jede Anfrage
* ohne Cookies mit ihrer Zustimmungs-Abfrage statt mit dem Artikel — golem.de
* etwa leitet jeden Cookie-losen Aufruf auf `/sonstiges/zustimmung/auswahl.html`
* um, auch wenn sich der Abrufer als normaler Browser ausgibt (gemessen
* 2026-07-27, siehe docs/EXTRAKTION.md). Kein Server-seitiger Trick kommt da
* vorbei. Der Text, den DU siehst, existiert nur in DEINEM Tab — also holen
* wir ihn dort ab.
*
* Dasselbe gilt für Folgeseiten: ein Artikel auf drei Seiten liegt komplett
* hinter derselben Wand. Darum sammelt `collectPageta` sie im Tab gleich mit
* (siehe page-collector.js).
*
* Wie es läuft:
* Klick auf das Symbol
* → `activeTab` erlaubt uns EINMALIG, diesen einen Tab zu lesen
* → Skript im Tab liefert { url, title, pages, missing }
* → POST an <instanz>/lese-liste/add/receive (dieselbe Route, die auch
* das Bookmarklet nutzt; legt das Payload für 2 Minuten unter einem
* Einweg-Token ab und antwortet 303 auf /lese-liste/add?pickup=…)
* → wir öffnen diese Adresse in einem neuen Tab; die angemeldete Seite
* holt das Payload ab und speichert lokal-first über event-sync.
*
* Was die Erweiterung NICHT tut, bewusst:
* - keine dauerhafte Leseerlaubnis für alle Seiten (`activeTab` gilt nur
* für den Klick, nicht für den Verlauf),
* - keinen API-Schlüssel speichern (es zählt deine Browser-Sitzung),
* - keine Telemetrie, keine Fremd-Server, kein Hintergrund-Verkehr.
*/
import { collectPageta } from './page-collector.js';
const DEFAULT_INSTANCE = 'https://app.pageta.com';
/** In Chrome heißt es `chrome`, in Firefox `browser` — beide sprechen dasselbe. */
const api = typeof browser !== 'undefined' ? browser : chrome;
async function instanceOrigin() {
try {
const stored = await api.storage.sync.get('instance');
const raw = (stored?.instance ?? '').trim();
if (!raw) return DEFAULT_INSTANCE;
return new URL(raw).origin;
} catch {
return DEFAULT_INSTANCE;
}
}
/** Kurze Rückmeldung am Symbol — die Erweiterung hat kein eigenes Fenster. */
async function flashBadge(tabId, text, color) {
try {
await api.action.setBadgeText({ tabId, text });
await api.action.setBadgeBackgroundColor({ tabId, color });
setTimeout(() => api.action.setBadgeText({ tabId, text: '' }), 4000);
} catch {
// Badge ist Komfort, kein Vertrag.
}
}
api.action.onClicked.addListener(async (tab) => {
if (!tab?.id || !tab.url) return;
// Interne Seiten (chrome://, about:, Store-Seiten) lassen kein Skript zu.
if (!/^https?:/i.test(tab.url)) {
await flashBadge(tab.id, '–', '#8a8a8a');
return;
}
const origin = await instanceOrigin();
// Die eigene App speichert man nicht in sich selbst.
if (tab.url.startsWith(origin)) {
await flashBadge(tab.id, '–', '#8a8a8a');
return;
}
try {
await flashBadge(tab.id, '…', '#7a6a5a');
// `func` wird als Quelltext in den fremden Tab übertragen —
// `collectPageta` ist deshalb bewusst eigenständig geschrieben.
// Mehrseitige Artikel holt es dort selbst nach, mit der Sitzung des Tabs.
const [result] = await api.scripting.executeScript({
target: { tabId: tab.id },
func: collectPageta,
});
const page = result?.result;
if (!page?.pages?.length || !page?.url) throw new Error('Seite konnte nicht gelesen werden');
// Formular-POST wie beim Bookmarklet — dieselbe Route, damit es genau
// einen Eingang gibt, der gepflegt wird.
const body = new FormData();
body.set('url', page.url);
body.set('title', page.title ?? '');
body.set('pages', JSON.stringify(page.pages));
body.set('missing', JSON.stringify(page.missing ?? []));
// `credentials: include` reicht die Sitzung mit; die Route selbst
// verlangt keine Anmeldung (anonymes Speichern bleibt möglich).
const response = await fetch(`${origin}/lese-liste/add/receive`, {
method: 'POST',
body,
credentials: 'include',
redirect: 'follow',
});
if (!response.ok) {
throw new Error(`Pageta antwortete mit ${response.status}`);
}
// `response.url` ist die Adresse NACH dem 303 — inklusive
// `?pickup=<token>`. Das Abholen passiert erst im Browser-Tab
// (client-seitig), dieser Abruf verbraucht den Token also nicht.
const target = response.url.includes('pickup=') ? response.url : `${origin}/lese-liste/add`;
await api.tabs.create({ url: target, active: true });
// Bei mehrseitigen Artikeln die Seitenzahl zeigen — sonst nur ein Haken.
await flashBadge(tab.id, page.pages.length > 1 ? String(page.pages.length) : '✓', '#3f6675');
} catch (err) {
console.warn('[pageta] Speichern fehlgeschlagen:', err);
await flashBadge(tab.id, '!', '#a81f37');
}
});