From 7bf9295337ebd2afc178141d0d5fe26696485508 Mon Sep 17 00:00:00 2001 From: Dicken Date: Fri, 12 Jun 2026 16:24:09 +0200 Subject: [PATCH] refactor: Paywall-Killer nur Browser-Flow, kein Server mehr --- backend/src/tools/paywallkiller/routes.js | 166 +--------------------- frontend/src/tools/paywallkiller.jsx | 159 ++++++++------------- 2 files changed, 59 insertions(+), 266 deletions(-) diff --git a/backend/src/tools/paywallkiller/routes.js b/backend/src/tools/paywallkiller/routes.js index db0137d..21bbb33 100644 --- a/backend/src/tools/paywallkiller/routes.js +++ b/backend/src/tools/paywallkiller/routes.js @@ -1,168 +1,8 @@ const express = require('express'); const router = express.Router(); -const { authenticate } = require('../../middleware/auth'); -const https = require('https'); -const http = require('http'); -const crypto = require('crypto'); -const path = require('path'); -const fs = require('fs'); -// ── HTTPS GET Helper ─────────────────────────────────────────────────────── -function fetchHtml(url, redirects = 0) { - return new Promise((resolve, reject) => { - if (redirects > 5) return reject(new Error('Zu viele Redirects')); - const mod = url.startsWith('https') ? https : http; - const req = mod.get(url, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml', - 'Accept-Language': 'de,en;q=0.9', - 'Accept-Encoding': 'identity', - }, - timeout: 20000, - }, (res) => { - if ([301,302,303,307,308].includes(res.statusCode) && res.headers.location) { - let loc = res.headers.location; - if (!loc.startsWith('http')) { - const b = new URL(url); - loc = loc.startsWith('/') ? `${b.protocol}//${b.host}${loc}` : `${b.protocol}//${b.host}/${loc}`; - } - res.resume(); - return fetchHtml(loc, redirects + 1).then(resolve).catch(reject); - } - let body = ''; - res.setEncoding('utf8'); - res.on('data', d => { if (body.length < 5000000) body += d; }); - res.on('end', () => resolve({ status: res.statusCode, body, finalUrl: url })); - res.on('error', reject); - }); - req.on('error', reject); - req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); }); - }); -} - -// ── GET /api/tools/paywallkiller/render?url=...&token=... ──────────────── -// Holt archive.ph Seite, entfernt Banner + schreibt Links um → sauberes HTML -// Token als Query-Parameter da der Endpoint direkt im Browser geöffnet wird -router.get('/render', (req, res, next) => { - const jwt = require('jsonwebtoken'); - const SECRET = process.env.JWT_SECRET || 'dev-secret'; - const token = req.query.token || (req.headers.authorization || '').replace('Bearer ', ''); - if (!token) return res.status(401).send('Nicht eingeloggt'); - try { req.user = jwt.verify(token, SECRET); next(); } - catch { return res.status(401).send('Token ungültig'); } -}, async (req, res) => { - const { url } = req.query; - if (!url) return res.status(400).send('url fehlt'); - - // Nur archive.ph URLs erlauben - if (!/^https?:\/\/archive\.(ph|is|today|fo|li|vn|md|gg)\//.test(url)) { - return res.status(400).send('Nur archive.ph URLs erlaubt'); - } - - try { - const result = await fetchHtml(url); - if (result.status !== 200) { - return res.status(502).send(`archive.ph antwortete mit ${result.status}`); - } - - let html = result.body; - - // ── Banner entfernen ────────────────────────────────────────────────── - // Der archive.ph Banner hat die ID "HEADER" oder ist ein
ganz oben - html = html - // Kompletten Wayback/archive Header-Block entfernen - .replace(/]*id=["']HEADER["'][^>]*>[\s\S]*?<\/div>/gi, '') - .replace(/]*id=["']wm-ipp[^"']*["'][^>]*>[\s\S]*?<\/div>/gi, '') - // archive.ph spezifische Toolbar - .replace(/]*class=["'][^"']*THUMBS-BLOCK[^"']*["'][^>]*>[\s\S]*?<\/div>/gi, '') - .replace(/]*id=["']tools["'][^>]*>[\s\S]*?<\/div>/gi, ''); - - // ── Links zurück auf Originale umschreiben ──────────────────────────── - // archive.ph schreibt Links um zu: https://archive.ph/TIMESTAMP/https://original.url - // Wir extrahieren die Original-URL - html = html.replace( - /https?:\/\/archive\.(?:ph|is|today|fo|li|vn|md|gg)\/[a-zA-Z0-9]+\/(https?:\/\/[^"'\s>]+)/g, - '$1' - ); - // Auch relative Archive-Links - html = html.replace( - /href="\/[a-zA-Z0-9]{4,10}\/(https?:\/\/[^"]+)"/g, - 'href="$1"' - ); - - // ── CSS für sauberes Drucken injizieren ─────────────────────────────── - const printCss = ` - -`; - - // CSS vor einfügen - if (html.includes('')) { - html = html.replace('', printCss + ''); - } else { - html = printCss + html; - } - - // Base-Tag setzen damit relative URLs funktionieren - const baseTag = ``; - if (html.includes('')) { - html = html.replace('', '' + baseTag); - } - - res.setHeader('Content-Type', 'text/html; charset=utf-8'); - // X-Frame-Options entfernen damit wir es anzeigen können - res.removeHeader('X-Frame-Options'); - res.send(html); - } catch (err) { - res.status(500).send(`Fehler: ${err.message}`); - } -}); - -// ── GET /api/tools/paywallkiller/cdx?url=... ────────────────────────────── -router.get('/cdx', authenticate, async (req, res) => { - const { url } = req.query; - if (!url) return res.status(400).json({ error: 'url fehlt' }); - let targetUrl = url.trim(); - if (!targetUrl.startsWith('http')) targetUrl = 'https://' + targetUrl; - const cdxUrl = `https://web.archive.org/cdx/search/cdx?url=${encodeURIComponent(targetUrl)}&output=json&limit=1&fl=timestamp&filter=statuscode:200&fastLatest=true`; - try { - const r = await fetchHtml(cdxUrl); - if (r.status !== 200) return res.status(502).json({ error: `CDX ${r.status}` }); - const data = JSON.parse(r.body); - if (!Array.isArray(data) || data.length < 2 || !data[1]?.[0]) return res.json({ found: false }); - return res.json({ found: true, waybackUrl: `https://web.archive.org/web/${data[1][0]}/${targetUrl}` }); - } catch (err) { - return res.status(500).json({ error: err.message }); - } -}); +// Paywall-Killer hat keinen aktiven Backend-Endpoint mehr. +// archive.ph blockt Server-seitige Requests (429). +// Der komplette Flow läuft im Browser des Users. module.exports = router; diff --git a/frontend/src/tools/paywallkiller.jsx b/frontend/src/tools/paywallkiller.jsx index c8816cf..79d15a3 100644 --- a/frontend/src/tools/paywallkiller.jsx +++ b/frontend/src/tools/paywallkiller.jsx @@ -9,13 +9,10 @@ const C = { }; export default function PaywallKiller() { - const [inputUrl, setInputUrl] = useState(''); - const [snapshotUrl, setSnapshotUrl] = useState(''); - const [status, setStatus] = useState('idle'); // idle | step1 | step2 - const [archiveUrl, setArchiveUrl] = useState(''); - const [captchaDone, setCaptchaDone] = useState(false); - const inputRef = useRef(null); - const snapshotRef = useRef(null); + const [inputUrl, setInputUrl] = useState(''); + const [archiveUrl, setArchiveUrl] = useState(''); + const [done, setDone] = useState(false); + const inputRef = useRef(null); function getTargetUrl() { const url = inputUrl.trim(); @@ -25,37 +22,16 @@ export default function PaywallKiller() { function handleSearch() { if (!inputUrl.trim()) return; setArchiveUrl(`https://archive.ph/newest/${getTargetUrl()}`); - setStatus('step1'); - setCaptchaDone(false); - setSnapshotUrl(''); - } - - function handleCaptchaDone() { - setCaptchaDone(true); - setStatus('step2'); - setTimeout(() => snapshotRef.current?.focus(), 100); - } - - function getRenderUrl() { - const snap = snapshotUrl.trim(); - if (snap && snap.startsWith('https://archive.')) { - const token = localStorage.getItem('sk_token') || ''; - return `/api/tools/paywallkiller/render?url=${encodeURIComponent(snap)}&token=${encodeURIComponent(token)}`; - } - return null; + setDone(false); } function handleReset() { setInputUrl(''); - setSnapshotUrl(''); - setStatus('idle'); setArchiveUrl(''); - setCaptchaDone(false); + setDone(false); setTimeout(() => inputRef.current?.focus(), 50); } - const renderUrl = getRenderUrl(); - return (
@@ -64,114 +40,91 @@ export default function PaywallKiller() { 🔓 PAYWALL-KILLER
-
archive.ph · Bereinigt · Als PDF drucken
+
archive.ph · Als PDF speichern
- {/* URL-Eingabe */} + {/* Eingabe */}
ARTIKEL-URL
setInputUrl(e.target.value)} - onKeyDown={e => e.key === 'Enter' && status === 'idle' && handleSearch()} + onKeyDown={e => e.key === 'Enter' && handleSearch()} placeholder="URL einfügen (Long-Press → Einfügen)" style={{ ...S.inp, width: '100%', boxSizing: 'border-box', marginBottom: 10 }} />
- {status === 'idle' ? ( - - ) : ( + + {archiveUrl && ( )}
- {/* Schritt 1: archive.ph öffnen */} - {(status === 'step1' || status === 'step2') && ( -
-
- 1 - ARCHIVE.PH ÖFFNEN - {status === 'step2' && ✓ erledigt} -
+ {/* Ergebnis */} + {archiveUrl && ( +
+
ARTIKEL ÖFFNEN & ALS PDF SPEICHERN
- {status === 'step1' && ( - <> -
- Öffne archive.ph. Falls ein CAPTCHA erscheint: lösen und warten bis der Artikel vollständig sichtbar ist. Dann die URL aus der Adressleiste kopieren – sie sieht dann so aus:
- archive.ph/AbCdE oder archive.ph/20260608.../https://... + {/* Schritt 1 */} +
+ 1 + - )} - - {/* Schritt 2: Snapshot-URL einfügen + bereinigt öffnen */} - {status === 'step2' && ( -
-
- 2 - SNAPSHOT-URL EINFÜGEN +
-
- Kopiere die URL aus der Adressleiste von archive.ph und füge sie hier ein: -
- - setSnapshotUrl(e.target.value)} - placeholder="https://archive.ph/AbCdE" - style={{ ...S.inp, width: '100%', boxSizing: 'border-box', marginBottom: 12, fontSize: 12 }} - /> - - {renderUrl ? ( - <> - - 🧹 Bereinigt öffnen (ohne Banner & Archiv-Links) - -
-
DANN ALS PDF DRUCKEN
-
- 📱 Android: Menü → Teilen → Drucken → Als PDF · Kopf-/Fußzeilen aus{'\n'} - 🍎 iOS: Teilen → Als PDF sichern{'\n'} - 💻 Desktop: Strg+P → Als PDF · Kopf-/Fußzeilen aus + {/* Schritt 2 */} +
+ 2 +
+
+ Wenn der Artikel sichtbar ist, als PDF speichern: +
+
+ {[ + ['📱', 'Android Chrome', 'Menü (⋮) → Teilen → Drucken → Als PDF · Kopf-/Fußzeilen: aus'], + ['🍎', 'iOS Safari', 'Teilen-Button → Als PDF sichern'], + ['💻', 'Desktop', 'Strg+P / Cmd+P → Als PDF · Kopf-/Fußzeilen: aus'], + ].map(([icon, label, desc]) => ( +
+
{icon} {label}
+
{desc}
+
+ ))} +
+
+
+ ℹ️ Der archive.ph-Balken oben ist Teil der archivierten Seite und kann nicht automatisch entfernt werden. Im Druckdialog "Kopf-/Fußzeilen" deaktivieren hält das PDF sauber.
- - ) : ( -
- Snapshot-URL oben einfügen um fortzufahren...
- )} +
)} - {status === 'idle' && ( + {/* Idle-Info */} + {!archiveUrl && (
SO FUNKTIONIERT ES
{[ ['1', 'URL des gesperrten Artikels einfügen'], - ['2', 'archive.ph öffnen, CAPTCHA einmalig lösen'], - ['3', 'URL aus Adressleiste kopieren und hier einfügen'], - ['4', 'Bereinigt öffnen (kein Banner, normale Links) → als PDF drucken'], + ['2', 'Artikel auf archive.ph öffnen'], + ['3', 'CAPTCHA einmalig lösen (danach dauerhaft gespeichert)'], + ['4', 'Über Drucken-Funktion als PDF speichern'], ].map(([num, text]) => (
{num}