diff --git a/backend/src/index.js b/backend/src/index.js index 99ccdd9..140ec8a 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -40,6 +40,7 @@ app.use((_req, res, next) => { "object-src 'none'", "base-uri 'self'", "form-action 'self'", + "frame-src https://archive.ph https://archive.is https://archive.today", ].join('; ')); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); diff --git a/frontend/src/tools/paywallkiller.jsx b/frontend/src/tools/paywallkiller.jsx index ad399b5..01cd6fa 100644 --- a/frontend/src/tools/paywallkiller.jsx +++ b/frontend/src/tools/paywallkiller.jsx @@ -1,12 +1,11 @@ -import { useState, useRef } from 'react'; -import { S, api } from '../lib.js'; +import { useState, useRef, useEffect } from 'react'; +import { S } from '../lib.js'; const C = { accent: '#f59e0b', success: '#4ade80', error: '#f87171', muted: '#6b7280', - dim: '#1f2937', }; function getDomain(url) { @@ -14,34 +13,135 @@ 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 }; +} + export default function PaywallKiller() { const [inputUrl, setInputUrl] = useState(''); const [status, setStatus] = useState('idle'); const [archiveUrl, setArchiveUrl] = useState(''); - const [archiveType, setArchiveType] = useState(''); const [errMsg, setErrMsg] = useState(''); const [pdfLoading, setPdfLoading] = useState(false); 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(''); setArchiveUrl(''); - setArchiveType(''); try { - const data = await api('/tools/paywallkiller/check', { body: { url } }); - setArchiveUrl(data.archiveUrl); - setArchiveType(data.type); - setStatus('found'); - } catch (err) { - if (err.message && err.message.includes('Kein Archiv')) { - setStatus('notfound'); + const result = await search(targetUrl); + if (result.found) { + setArchiveUrl(result.url); + setStatus('found'); } else { - setErrMsg(err.message || 'Unbekannter Fehler'); - setStatus('error'); + setStatus('notfound'); } + } catch (err) { + setErrMsg(err.message || 'Fehler beim Suchen'); + setStatus('error'); } } @@ -82,7 +182,6 @@ export default function PaywallKiller() { setInputUrl(''); setStatus('idle'); setArchiveUrl(''); - setArchiveType(''); setErrMsg(''); setPdfLoading(false); setTimeout(() => inputRef.current?.focus(), 50); @@ -92,9 +191,7 @@ export default function PaywallKiller() { try { const text = await navigator.clipboard.readText(); if (text) setInputUrl(text.trim()); - } catch { - inputRef.current?.focus(); - } + } catch { inputRef.current?.focus(); } } const isChecking = status === 'checking'; @@ -106,24 +203,18 @@ export default function PaywallKiller() { return (
- {/* Header */}
🔓 - - PAYWALL-KILLER - + PAYWALL-KILLER
- Artikel über archive.is abrufen · Als PDF speichern + Artikel über archive.ph abrufen · Als PDF speichern
- {/* Eingabe */}
ARTIKEL-URL
- - {/* Input + Paste-Button */}
-
- - {/* Action-Buttons */}
- {hasResult && ( - )}
+ {isChecking && ( +
+ archive.ph wird direkt im Browser abgefragt... +
+ )}
- {/* Gefunden */} {isFound && (
- - Archiv gefunden - - - {archiveType === 'newest' ? 'neuester Snapshot' : 'ältester Snapshot'} - + Archiv gefunden
- -
- +
+ {archiveUrl}
-
- + 🌐 Im Browser öffnen -
- {pdfLoading && (
- Seite wird gerendert, das dauert 15–30 Sekunden... + Seite wird serverseitig gerendert, dauert 15–30 Sekunden...
)}
)} - {/* Kein Archiv */} {isNotFound && (
📭 - - Kein Archiv gefunden - + Kein Archiv gefunden
- Für {getDomain(inputUrl)} wurde - kein Snapshot auf archive.is gefunden. + Für {getDomain(inputUrl)} wurde kein Snapshot auf archive.ph gefunden.
- - 🔗 Manuell auf archive.is prüfen + + 🔗 Manuell auf archive.ph prüfen
)} - {/* Fehler */} {isError && (
⚠️ - - {errMsg} - + {errMsg}
)} - {/* Idle-Info */} {status === 'idle' && (
SO FUNKTIONIERT ES
{[ ['1', 'URL des gesperrten Artikels einfügen'], - ['2', 'Archiv auf archive.is suchen'], + ['2', 'Archiv auf archive.ph suchen (direkt im Browser)'], ['3', 'Im Browser öffnen oder als PDF speichern'], ].map(([num, text]) => (
- {num} + {num} {text}
))}