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>
125 lines
4.7 KiB
JavaScript
125 lines
4.7 KiB
JavaScript
/**
|
||
* 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');
|
||
}
|
||
});
|