diff --git a/backend/src/tools/paywallkiller/routes.js b/backend/src/tools/paywallkiller/routes.js index 9b9d28a..7c391fb 100644 --- a/backend/src/tools/paywallkiller/routes.js +++ b/backend/src/tools/paywallkiller/routes.js @@ -166,8 +166,8 @@ router.post('/check', authenticate, async (req, res) => { router.post('/pdf', authenticate, async (req, res) => { const { archiveUrl } = req.body; if (!archiveUrl || typeof archiveUrl !== 'string') return res.status(400).json({ error: 'archiveUrl fehlt' }); - if (!/^https?:\/\/archive\.(is|ph|today|fo|li|vn|md|gg)\//.test(archiveUrl)) { - return res.status(400).json({ error: 'Ungültige archive.is URL' }); + if (!/^https?:\/\/(archive\.(is|ph|today|fo|li|vn|md|gg)|web\.archive\.org)\//.test(archiveUrl)) { + return res.status(400).json({ error: 'Ungültige Archiv-URL' }); } let browser; diff --git a/frontend/src/tools/paywallkiller.jsx b/frontend/src/tools/paywallkiller.jsx index 01cd6fa..e5d9176 100644 --- a/frontend/src/tools/paywallkiller.jsx +++ b/frontend/src/tools/paywallkiller.jsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef } from 'react'; import { S } from '../lib.js'; const C = { @@ -13,131 +13,56 @@ function getDomain(url) { catch { return url; } } -// ── Iframe-basierte Archivsuche (umgeht Server-IP-Block) ────────────────── -// Öffnet archive.ph/?url=... in einem versteckten iframe und liest -// nach dem Load die finale URL aus (Snapshot-Hash-URL). -function useArchiveSearch() { - const iframeRef = useRef(null); - const resolveRef = useRef(null); - const timeoutRef = useRef(null); - - useEffect(() => { - return () => { - if (timeoutRef.current) clearTimeout(timeoutRef.current); - }; - }, []); - - function search(targetUrl) { - return new Promise((resolve, reject) => { - resolveRef.current = { resolve, reject }; - - // Altes iframe entfernen - if (iframeRef.current) { - document.body.removeChild(iframeRef.current); - iframeRef.current = null; - } - - const iframe = document.createElement('iframe'); - iframe.style.cssText = 'position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;border:none;'; - iframe.sandbox = 'allow-scripts allow-same-origin allow-forms'; - - iframe.onload = () => { - if (timeoutRef.current) clearTimeout(timeoutRef.current); - try { - // Nach dem Load: finale URL des iframes prüfen - const finalUrl = iframe.contentWindow?.location?.href || ''; - cleanup(); - - if (!finalUrl || finalUrl === 'about:blank') { - return resolve({ found: false }); - } - - // Snapshot-URL hat Form https://archive.ph/HASH (4-10 Zeichen) - if (/archive\.(is|ph|today|fo|li|vn|md|gg)\/[a-zA-Z0-9]{4,10}$/.test(finalUrl)) { - return resolve({ found: true, url: finalUrl }); - } - - // Suchergebnis-Seite geladen: Links nach Snapshots durchsuchen - try { - const doc = iframe.contentDocument || iframe.contentWindow?.document; - if (doc) { - // Alle Links durchsuchen - const links = Array.from(doc.querySelectorAll('a[href]')); - for (const a of links) { - const href = a.getAttribute('href') || ''; - const abs = href.startsWith('http') ? href : `https://archive.ph${href}`; - if (/archive\.(is|ph|today|fo|li|vn|md|gg)\/[a-zA-Z0-9]{4,10}$/.test(abs)) { - const h = abs.split('/').pop(); - const blacklist = ['newest','oldest','submit','search','about','rss','api','timemap']; - if (!blacklist.includes(h) && !/^\d+$/.test(h)) { - return resolve({ found: true, url: abs }); - } - } - } - } - } catch { - // Cross-Origin Zugriff blockiert – trotzdem finalUrl prüfen - } - - resolve({ found: false }); - } catch (e) { - cleanup(); - resolve({ found: false }); - } - }; - - iframe.onerror = () => { - cleanup(); - resolve({ found: false }); - }; - - // Timeout nach 15s - timeoutRef.current = setTimeout(() => { - cleanup(); - reject(new Error('Timeout beim Laden von archive.ph')); - }, 15000); - - const searchUrl = `https://archive.ph/?url=${encodeURIComponent(targetUrl)}`; - iframe.src = searchUrl; - document.body.appendChild(iframe); - iframeRef.current = iframe; - - function cleanup() { - if (timeoutRef.current) clearTimeout(timeoutRef.current); - if (iframeRef.current) { - try { document.body.removeChild(iframeRef.current); } catch {} - iframeRef.current = null; - } - } - }); - } - - return { search }; +// ── Wayback Machine CDX API – direkt im Browser (CORS: *) ───────────────── +async function checkWayback(targetUrl) { + // Neuesten Snapshot suchen (limit=1, sortiert newest-first via fl+limit) + const cdxUrl = `https://web.archive.org/cdx/search/cdx?url=${encodeURIComponent(targetUrl)}&output=json&limit=1&fl=timestamp&filter=statuscode:200&fastLatest=true`; + const res = await fetch(cdxUrl, { signal: AbortSignal.timeout(10000) }); + if (!res.ok) return null; + const data = await res.json(); + // data = [["timestamp"],["20260608201434"]] oder [] wenn nichts gefunden + if (!Array.isArray(data) || data.length < 2) return null; + const timestamp = data[1]?.[0]; + if (!timestamp) return null; + return `https://web.archive.org/web/${timestamp}/${targetUrl}`; } export default function PaywallKiller() { - const [inputUrl, setInputUrl] = useState(''); - const [status, setStatus] = useState('idle'); - const [archiveUrl, setArchiveUrl] = useState(''); - const [errMsg, setErrMsg] = useState(''); - const [pdfLoading, setPdfLoading] = useState(false); + const [inputUrl, setInputUrl] = useState(''); + const [status, setStatus] = useState('idle'); + const [waybackUrl, setWaybackUrl] = useState(''); + const [archiveUrl, setArchiveUrl] = useState(''); + const [errMsg, setErrMsg] = useState(''); + const [pdfLoading, setPdfLoading] = useState(false); + const [pdfTarget, setPdfTarget] = useState(''); const inputRef = useRef(null); - const { search } = useArchiveSearch(); async function handleCheck() { const url = inputUrl.trim(); if (!url) return; let targetUrl = url.startsWith('http') ? url : 'https://' + url; + setStatus('checking'); setErrMsg(''); + setWaybackUrl(''); setArchiveUrl(''); + try { - const result = await search(targetUrl); - if (result.found) { - setArchiveUrl(result.url); - setStatus('found'); + // archive.ph URL immer konstruieren (ohne Prüfung – direkt nutzbar) + const archivePh = `https://archive.ph/newest/${targetUrl}`; + + // Wayback CDX parallel prüfen + let wb = null; + try { wb = await checkWayback(targetUrl); } catch { /* ignorieren */ } + + setWaybackUrl(wb || ''); + setArchiveUrl(archivePh); + + if (wb) { + setStatus('found_both'); } else { - setStatus('notfound'); + // Auch ohne Wayback-Treffer archive.ph anbieten + setStatus('found_archive'); } } catch (err) { setErrMsg(err.message || 'Fehler beim Suchen'); @@ -145,7 +70,8 @@ export default function PaywallKiller() { } } - async function handlePdf() { + async function handlePdf(url) { + setPdfTarget(url); setPdfLoading(true); try { const token = localStorage.getItem('sk_token'); @@ -155,7 +81,7 @@ export default function PaywallKiller() { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, - body: JSON.stringify({ archiveUrl }), + body: JSON.stringify({ archiveUrl: url }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); @@ -172,15 +98,16 @@ export default function PaywallKiller() { setTimeout(() => URL.revokeObjectURL(objUrl), 5000); } catch (err) { setErrMsg(err.message || 'PDF konnte nicht erstellt werden'); - setStatus('error'); } finally { setPdfLoading(false); + setPdfTarget(''); } } function handleReset() { setInputUrl(''); setStatus('idle'); + setWaybackUrl(''); setArchiveUrl(''); setErrMsg(''); setPdfLoading(false); @@ -195,10 +122,20 @@ export default function PaywallKiller() { } const isChecking = status === 'checking'; - const isFound = status === 'found'; - const isNotFound = status === 'notfound'; - const isError = status === 'error'; - const hasResult = isFound || isNotFound || isError; + const hasResult = status.startsWith('found') || status === 'error'; + const hasWayback = status === 'found_both' && waybackUrl; + const hasArchive = (status === 'found_both' || status === 'found_archive') && archiveUrl; + + // PDF-Button für eine bestimmte URL + function PdfBtn({ url }) { + const loading = pdfLoading && pdfTarget === url; + return ( + + ); + } return (
@@ -209,10 +146,11 @@ export default function PaywallKiller() { PAYWALL-KILLER
- Artikel über archive.ph abrufen · Als PDF speichern + Wayback Machine · archive.ph · Als PDF speichern
+ {/* Eingabe */}
ARTIKEL-URL
@@ -232,7 +170,7 @@ export default function PaywallKiller() {
{hasResult && (
- {isChecking && ( -
- archive.ph wird direkt im Browser abgefragt... -
- )}
- {isFound && ( -
-
- - Archiv gefunden + {/* Wayback Machine Treffer */} + {hasWayback && ( +
+
+ 🏛 + Wayback Machine + gefunden
-
- - {archiveUrl} - + -
- - 🌐 Im Browser öffnen - - + - {pdfLoading && ( -
- Seite wird serverseitig gerendert, dauert 15–30 Sekunden... +
+ )} + + {/* archive.ph Link */} + {hasArchive && ( +
+
+ 📦 + archive.ph + {!hasWayback && neuester Snapshot} +
+ +
+ 🌐 Öffnen + +
+ {!hasWayback && ( +
+ Kein Wayback-Snapshot gefunden. archive.ph-Link öffnet direkt den neuesten Snapshot (falls vorhanden).
)}
)} - {isNotFound && ( -
-
- 📭 - Kein Archiv gefunden + {/* PDF-Ladehinweis */} + {pdfLoading && ( +
+
+ ⏳ Seite wird serverseitig gerendert, bitte warten (15–30s)...
-
- Für {getDomain(inputUrl)} wurde kein Snapshot auf archive.ph gefunden. -
- - 🔗 Manuell auf archive.ph prüfen -
)} - {isError && ( -
+ {/* Fehler */} + {errMsg && ( +
⚠️ {errMsg} @@ -301,12 +241,13 @@ export default function PaywallKiller() {
)} + {/* Idle-Info */} {status === 'idle' && (
SO FUNKTIONIERT ES
{[ ['1', 'URL des gesperrten Artikels einfügen'], - ['2', 'Archiv auf archive.ph suchen (direkt im Browser)'], + ['2', 'Wayback Machine + archive.ph werden geprüft'], ['3', 'Im Browser öffnen oder als PDF speichern'], ].map(([num, text]) => (