| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/clients/client2/web32/web/tools/ |
Upload File : |
<?php
/**
* ============================================================================
* Wayback Blog Importer — taschenmuschi-kaufen.de
* ----------------------------------------------------------------------------
* Einmalig auszuführendes Migrations-Script.
*
* Quelle: https://web.archive.org/web/<TS>/https://www.taschenmuschi-kaufen.de/blog/
* Ziel : Neue WP-Installation in diesem Projekt.
*
* Aufruf:
* CLI: php tools/import-wayback-blog.php [--limit=5] [--dry-run] [--force]
* Browser: /tools/import-wayback-blog.php?run=1&limit=5
*
* Sicherheits-Mechanismen:
* - Im Browser-Modus erst nach Token-Bestätigung scharf
* - manage_options-Capability erforderlich, wenn Browser-Modus
* - Globales Timeout, Per-Request-Timeout, Retry mit Backoff
* - HTML-Cache verhindert Doppel-Fetches bei Re-Run
* - --dry-run testet das Mapping, ohne in die DB zu schreiben
*
* Permalinks:
* - Setzt die Permalink-Struktur automatisch auf /%year%/%monthnum%/%postname%/.
*
* Idempotenz:
* - Duplikat-Erkennung anhand `_wayback_source_url` + Hash.
*
* Bilder:
* - Werden über media_handle_sideload importiert, deduplicate via URL-Hash.
* - Erstes Bild im Content = Featured. Sonst GD-Fallback (Bordeaux).
* ============================================================================
*
* @package Tools
* @since 2026-05-22
*/
declare( strict_types=1 );
/* ============================================================================
* 0) Setup: WP laden + Mode-Detection
* ============================================================================ */
if ( PHP_SAPI !== 'cli' ) {
// Browser-Modus: minimaler HTML-Header, danach reine UTF-8-Text-Logs.
header( 'Content-Type: text/plain; charset=UTF-8' );
@set_time_limit( 1800 );
@ini_set( 'memory_limit', '512M' );
}
@set_time_limit( 1800 );
ini_set( 'default_socket_timeout', '60' );
// WordPress bootstrappen (zweistufig — Skript darf in /tools/ liegen).
$wp_root = dirname( __DIR__ );
if ( ! file_exists( $wp_root . '/wp-load.php' ) ) {
fwrite( STDERR, "wp-load.php nicht gefunden (erwartet: $wp_root/wp-load.php)\n" );
exit( 1 );
}
define( 'WP_USE_THEMES', false );
require_once $wp_root . '/wp-load.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
// Browser-Sicherheit: Login + Token erforderlich.
if ( PHP_SAPI !== 'cli' ) {
if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) {
echo "Bitte als Administrator einloggen, dann erneut aufrufen.\n";
echo "Login: " . wp_login_url( $_SERVER['REQUEST_URI'] ?? '' ) . "\n";
exit;
}
if ( empty( $_GET['run'] ) ) {
echo "Sicherheitshinweis: Dieses Script verändert die WordPress-Datenbank.\n";
echo "Zum Start: " . esc_url( add_query_arg( array( 'run' => '1' ) ) ) . "\n";
echo "Optional: &limit=5 (nur 5 Artikel) · &dry_run=1 (nichts speichern) · &force=1 (alte überschreiben)\n";
exit;
}
}
/* ============================================================================
* 1) Konfiguration
* ============================================================================ */
$config = array(
'index_url' => 'https://web.archive.org/web/20241114235446/https://www.taschenmuschi-kaufen.de/blog/',
'source_host' => 'www.taschenmuschi-kaufen.de',
'user_agent' => 'Mozilla/5.0 (compatible; TMK-WaybackMigrator/1.0; +https://taschenmuschi-kaufen.de)',
'request_pause_ms' => 500,
'request_timeout' => 25,
'retry' => 2,
'cache_dir' => __DIR__ . '/cache',
'log_file' => __DIR__ . '/import-logs/import.log',
'default_category' => 'Magazin',
'limit' => null,
'dry_run' => false,
'force' => false,
'max_pages' => 10, // Schutz gegen Pagination-Loop
'fallback_img' => array(
'width' => 1200,
'height' => 630,
'filename' => 'default-post-image.jpg',
),
);
// Optionen aus CLI / Browser-Query auflösen
$opts_source = PHP_SAPI === 'cli' ? $argv : $_GET;
foreach ( (array) $opts_source as $arg ) {
if ( is_string( $arg ) && preg_match( '/^--?([\w-]+)(?:=(.+))?$/', $arg, $m ) ) {
$k = str_replace( '-', '_', $m[1] );
$v = $m[2] ?? '1';
switch ( $k ) {
case 'limit': $config['limit'] = (int) $v; break;
case 'dry_run': $config['dry_run'] = (bool) $v; break;
case 'force': $config['force'] = (bool) $v; break;
}
}
}
if ( isset( $_GET['limit'] ) ) $config['limit'] = (int) $_GET['limit'];
if ( isset( $_GET['dry_run'] ) ) $config['dry_run'] = (bool) $_GET['dry_run'];
if ( isset( $_GET['force'] ) ) $config['force'] = (bool) $_GET['force'];
if ( ! is_dir( $config['cache_dir'] ) ) wp_mkdir_p( $config['cache_dir'] );
if ( ! is_dir( dirname( $config['log_file'] ) ) ) wp_mkdir_p( dirname( $config['log_file'] ) );
/* ============================================================================
* 2) Logger
* ============================================================================ */
class Wayback_Logger {
private $fh;
public function __construct( $path ) {
$this->fh = fopen( $path, 'ab' );
if ( $this->fh ) {
fwrite( $this->fh, "\n----- Run " . date( 'c' ) . " -----\n" );
}
}
public function line( $level, $msg, $context = array() ) {
$stamp = date( 'Y-m-d H:i:s' );
$ctx = $context ? ' ' . wp_json_encode( $context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) : '';
$line = sprintf( "[%s] %-5s %s%s\n", $stamp, strtoupper( $level ), $msg, $ctx );
if ( $this->fh ) fwrite( $this->fh, $line );
echo $line;
@flush();
}
public function info( $m, $c = array() ) { $this->line( 'info', $m, $c ); }
public function warn( $m, $c = array() ) { $this->line( 'warn', $m, $c ); }
public function error( $m, $c = array() ) { $this->line( 'error', $m, $c ); }
public function __destruct() { if ( $this->fh ) fclose( $this->fh ); }
}
$LOG = new Wayback_Logger( $config['log_file'] );
/* ============================================================================
* 3) HTTP-Client mit Retry, Throttle, Cache
* ============================================================================ */
class Wayback_Client {
private $cfg;
private $log;
private $last_request_us = 0;
public function __construct( $cfg, Wayback_Logger $log ) {
$this->cfg = $cfg;
$this->log = $log;
}
public function fetch( $url ) {
$cache_key = md5( $url );
$cache_path = rtrim( $this->cfg['cache_dir'], '/' ) . '/' . $cache_key . '.html';
if ( file_exists( $cache_path ) && filesize( $cache_path ) > 100 ) {
return file_get_contents( $cache_path );
}
$this->throttle();
$attempt = 0;
$max = max( 1, (int) $this->cfg['retry'] );
while ( $attempt <= $max ) {
$attempt++;
$resp = wp_remote_get( $url, array(
'timeout' => $this->cfg['request_timeout'],
'redirection' => 8,
'user-agent' => $this->cfg['user_agent'],
'headers' => array(
'Accept' => 'text/html,application/xhtml+xml',
'Accept-Language' => 'de-DE,de;q=0.9,en;q=0.6',
),
) );
if ( ! is_wp_error( $resp ) ) {
$code = (int) wp_remote_retrieve_response_code( $resp );
if ( $code >= 200 && $code < 400 ) {
$body = (string) wp_remote_retrieve_body( $resp );
if ( $body !== '' ) {
@file_put_contents( $cache_path, $body );
return $body;
}
}
$this->log->warn( "HTTP $code für $url — Versuch $attempt" );
} else {
$this->log->warn( 'Fetch-Fehler: ' . $resp->get_error_message() . " — Versuch $attempt" );
}
// Backoff
usleep( 600000 * $attempt );
}
return null;
}
/**
* Lädt eine Datei in einen temp-Pfad. Liefert temp-Pfad oder null.
*/
public function fetch_file( $url ) {
$this->throttle();
$tmp = download_url( $url, 60 );
return is_wp_error( $tmp ) ? null : $tmp;
}
private function throttle() {
$delay_ms = (int) $this->cfg['request_pause_ms'];
if ( $delay_ms <= 0 ) return;
$now = microtime( true ) * 1e6;
$needed = $delay_ms * 1000;
$elapsed = $now - $this->last_request_us;
if ( $this->last_request_us > 0 && $elapsed < $needed ) {
usleep( (int) ( $needed - $elapsed ) );
}
$this->last_request_us = microtime( true ) * 1e6;
}
}
$HTTP = new Wayback_Client( $config, $LOG );
/* ============================================================================
* 4) Wayback-URL-Helfer
* ============================================================================ */
/**
* Entfernt das Wayback-Wrapper-Prefix aus einer URL.
* `https://web.archive.org/web/<TS>(im_|js_)/https://example.com/foo`
* → `https://example.com/foo`
*/
function wb_unwrap_url( $url ) {
if ( ! is_string( $url ) || $url === '' ) return $url;
if ( preg_match( '#^https?://web\.archive\.org/web/[0-9]+(?:[a-z_]+)?/(https?://.+)$#i', $url, $m ) ) {
return $m[1];
}
if ( preg_match( '#^/web/[0-9]+(?:[a-z_]+)?/(https?://.+)$#i', $url, $m ) ) {
return $m[1];
}
return $url;
}
/**
* Extrahiert aus einer Original-URL den /YYYY/MM/slug/-Pfad und liefert Datum + Slug.
*/
function wb_parse_post_path( $original_url ) {
$path = (string) parse_url( $original_url, PHP_URL_PATH );
if ( preg_match( '#^/(\d{4})/(\d{2})/([a-z0-9\-]+)/?$#i', $path, $m ) ) {
return array( 'year' => $m[1], 'month' => $m[2], 'slug' => $m[3] );
}
return null;
}
/* ============================================================================
* 5) Index-Crawler (Pagination)
* ============================================================================ */
function wb_collect_article_urls( $config, $HTTP, $LOG ) {
$urls = array();
$page = 1;
$cur = $config['index_url'];
while ( $cur && $page <= (int) $config['max_pages'] ) {
$LOG->info( "Index-Seite $page laden", array( 'url' => $cur ) );
$html = $HTTP->fetch( $cur );
if ( ! $html ) {
$LOG->warn( "Übersicht $page nicht abrufbar — Stop." );
break;
}
// Artikel-Links extrahieren
if ( preg_match_all(
'#href="(https?://web\.archive\.org/web/[0-9]+/https?://[^/]+/(\d{4})/(\d{2})/[^"]+/)"#i',
$html, $m, PREG_SET_ORDER
) ) {
foreach ( $m as $entry ) {
$urls[ $entry[1] ] = true;
}
}
// Pagination — nächste Seite suchen
$next = null;
if ( preg_match(
'#href="(https?://web\.archive\.org/web/[0-9]+/https?://[^/]+/blog/page/' . ( $page + 1 ) . '/)"#i',
$html, $mn
) ) {
$next = $mn[1];
}
$cur = $next;
$page++;
}
$LOG->info( 'Index-Crawl abgeschlossen', array( 'gesamt' => count( $urls ) ) );
return array_keys( $urls );
}
/* ============================================================================
* 6) Detail-Parser
* ============================================================================ */
function wb_load_dom( $html ) {
if ( ! $html ) return null;
$prev = libxml_use_internal_errors( true );
$dom = new DOMDocument();
$html = preg_replace( '/^\xEF\xBB\xBF/', '', $html );
$dom->loadHTML( '<?xml encoding="UTF-8" ?>' . $html );
libxml_clear_errors();
libxml_use_internal_errors( $prev );
return $dom;
}
function wb_text( $node ) {
return trim( preg_replace( '/\s+/u', ' ', (string) ( $node->textContent ?? '' ) ) );
}
function wb_parse_article( $html, $wayback_url, $config ) {
$dom = wb_load_dom( $html );
if ( ! $dom ) return null;
$xpath = new DOMXPath( $dom );
$data = array(
'wayback_url' => $wayback_url,
'source_url' => wb_unwrap_url( $wayback_url ),
'title' => '',
'date' => '',
'content_html'=> '',
'og_image' => '',
'inline_images'=> array(),
);
/* Titel ------------------------------------------------------------- */
$title = $xpath->query( "//meta[@property='og:title']/@content" )->item( 0 );
if ( $title ) {
$data['title'] = wb_text( $title );
} else {
$h1 = $xpath->query( "//h1" )->item( 0 );
if ( $h1 ) $data['title'] = wb_text( $h1 );
}
// " .:. Erotik Blog"-Suffix der Quelle entfernen, falls vorhanden
$data['title'] = preg_replace( '/\s*\.:\.\s*Erotik Blog\s*$/iu', '', (string) $data['title'] );
$data['title'] = trim( (string) $data['title'] );
/* Datum ------------------------------------------------------------- */
$meta_date = $xpath->query( "//meta[@property='article:published_time']/@content" )->item( 0 );
if ( $meta_date ) {
$data['date'] = $meta_date->nodeValue;
}
if ( ! $data['date'] ) {
$span = $xpath->query( "//*[contains(concat(' ', normalize-space(@class), ' '), ' blog-meta ')]" )->item( 0 );
if ( $span ) {
$data['date'] = wb_normalize_german_date( wb_text( $span ) );
}
}
// Fallback: aus der URL ableiten (/2023/03/...)
if ( ! $data['date'] ) {
$parsed = wb_parse_post_path( $data['source_url'] );
if ( $parsed ) {
$data['date'] = sprintf( '%s-%s-01T00:00:00+00:00', $parsed['year'], $parsed['month'] );
}
}
/* og:image ---------------------------------------------------------- */
$og = $xpath->query( "//meta[@property='og:image']/@content" )->item( 0 );
if ( $og ) {
$data['og_image'] = wb_unwrap_url( $og->nodeValue );
}
/* Content ----------------------------------------------------------- */
// Primär: das <div class="content no-margin"> rund um den H1
$content_node = null;
$candidates = $xpath->query( "//div[contains(concat(' ', normalize-space(@class), ' '), ' content ') and contains(concat(' ', normalize-space(@class), ' '), ' no-margin ')]" );
foreach ( $candidates as $node ) {
// Wähle den Container, der ein <h1> enthält
if ( $node->getElementsByTagName( 'h1' )->length ) {
$content_node = $node;
break;
}
}
if ( ! $content_node ) {
// Fallback: Section, die das blog-meta enthält, eine Ebene höher
$meta = $xpath->query( "//*[contains(@class,'blog-meta')]" )->item( 0 );
if ( $meta ) {
$content_node = $meta->parentNode;
}
}
if ( $content_node ) {
$data['content_html'] = wb_clean_content( $content_node, $dom, $data );
}
// Inline-Bild-URLs sammeln (original URLs, Wayback-entkapselt)
if ( preg_match_all( '#<img[^>]+src="([^"]+)"#i', $data['content_html'], $m ) ) {
foreach ( $m[1] as $img_src ) {
$orig = wb_unwrap_url( $img_src );
if ( $orig ) $data['inline_images'][ $orig ] = true;
}
}
$data['inline_images'] = array_keys( $data['inline_images'] );
return $data;
}
function wb_normalize_german_date( $str ) {
$str = trim( (string) $str );
if ( $str === '' ) return '';
$months = array(
'januar'=>'01','februar'=>'02','märz'=>'03','maerz'=>'03','april'=>'04',
'mai'=>'05','juni'=>'06','juli'=>'07','august'=>'08','september'=>'09',
'oktober'=>'10','november'=>'11','dezember'=>'12',
);
// "6. März 2023"
if ( preg_match( '/(\d{1,2})\.\s*([A-Za-zäöüÄÖÜ]+)\s*(\d{4})/u', $str, $m ) ) {
$key = mb_strtolower( $m[2] );
$month = $months[ $key ] ?? null;
if ( $month ) {
return sprintf( '%04d-%02d-%02dT00:00:00+00:00', (int) $m[3], (int) $month, (int) $m[1] );
}
}
// "06.03.2023"
if ( preg_match( '/(\d{2})\.(\d{2})\.(\d{4})/', $str, $m ) ) {
return sprintf( '%s-%s-%sT00:00:00+00:00', $m[3], $m[2], $m[1] );
}
return '';
}
/* ============================================================================
* 7) Content-Cleanup
* ============================================================================ */
function wb_clean_content( DOMNode $node, DOMDocument $dom, $meta ) {
// 1) H1 entfernen (Titel kommt separat in post_title)
foreach ( iterator_to_array( $node->getElementsByTagName( 'h1' ) ) as $h1 ) {
$h1->parentNode->removeChild( $h1 );
}
// 2) Blog-Meta (Datum-Span) entfernen
$xpath = new DOMXPath( $dom );
foreach ( $xpath->query( ".//*[contains(@class,'blog-meta')]", $node ) as $m ) {
$m->parentNode->removeChild( $m );
}
// 3) Scripts, Styles, Noscript, Wayback-Toolbar, Offer-Boxen entfernen
$strip_tags = array( 'script', 'style', 'noscript', 'iframe' );
foreach ( $strip_tags as $tag ) {
foreach ( iterator_to_array( $node->getElementsByTagName( $tag ) ) as $el ) {
$el->parentNode->removeChild( $el );
}
}
// Wayback-Toolbar-Container / Cloudflare-Beacon etc.
foreach ( $xpath->query( ".//*[@id='wm-ipp-base' or @id='wm-ipp-print' or contains(@class,'wb_ipp')]", $node ) as $el ) {
$el->parentNode->removeChild( $el );
}
// Eingebaute Offer-Rows (Theme-eigene CTAs auf Produktseiten) — sind im Artikel nicht relevant.
foreach ( $xpath->query( ".//*[contains(concat(' ', normalize-space(@class), ' '), ' offer-row ')]", $node ) as $el ) {
$el->parentNode->removeChild( $el );
}
// 4) Inline-Styles, alte Klassen, on*-Attribute entfernen
foreach ( $xpath->query( ".//*", $node ) as $el ) {
if ( ! ( $el instanceof DOMElement ) ) continue;
$strip_attrs = array( 'style', 'onclick', 'onload', 'onerror', 'data-mce-style' );
foreach ( $strip_attrs as $a ) {
if ( $el->hasAttribute( $a ) ) $el->removeAttribute( $a );
}
// Theme-CSS-Klassen abräumen
if ( $el->hasAttribute( 'class' ) ) {
$classes = preg_split( '/\s+/', $el->getAttribute( 'class' ) ?? '' );
$keep = array();
foreach ( $classes as $c ) {
if ( $c === '' ) continue;
// WP-typische sinnvolle Klassen behalten
if ( preg_match( '/^(wp-|gallery|alignleft|alignright|aligncenter|alignnone|size-)/', $c ) ) {
$keep[] = $c;
}
}
if ( $keep ) {
$el->setAttribute( 'class', implode( ' ', $keep ) );
} else {
$el->removeAttribute( 'class' );
}
}
// 5) <a href="…wayback…"> → entkapseln, Anchor mit class wp-*-button etc. behalten
if ( $el->tagName === 'a' && $el->hasAttribute( 'href' ) ) {
$href = wb_unwrap_url( $el->getAttribute( 'href' ) );
$el->setAttribute( 'href', $href );
}
// 6) <img src="…wayback…"> entkapseln, alt sicherstellen
if ( $el->tagName === 'img' && $el->hasAttribute( 'src' ) ) {
$src = wb_unwrap_url( $el->getAttribute( 'src' ) );
$el->setAttribute( 'src', $src );
if ( ! $el->hasAttribute( 'alt' ) || trim( $el->getAttribute( 'alt' ) ) === '' ) {
$el->setAttribute( 'alt', $meta['title'] );
}
// srcset-Encodings normalisieren
if ( $el->hasAttribute( 'srcset' ) ) {
$srcset = $el->getAttribute( 'srcset' );
$srcset = preg_replace_callback(
'#(https?://[^\s,]+)#',
function ( $m ) { return wb_unwrap_url( $m[1] ); },
$srcset
);
$el->setAttribute( 'srcset', $srcset );
}
}
}
// 7) DOM → HTML; nur die Children des Containers (ohne <div class="content no-margin">-Wrapper)
$out = '';
foreach ( $node->childNodes as $child ) {
$out .= $dom->saveHTML( $child );
}
// 8) Leere <p></p>, doppelte Absätze, bereinigen
$out = preg_replace( '#<p>\s*( )?\s*</p>#u', '', $out );
$out = preg_replace( '/\n{3,}/', "\n\n", $out );
$out = trim( $out );
return $out;
}
/* ============================================================================
* 8) Content-Image-Stripper (Bilder werden nicht importiert)
* ============================================================================ */
/**
* Entfernt alle <img>-Tags und ihre einfachen Wrapper aus einem HTML-Block.
*/
function wb_strip_all_images( $html ) {
if ( $html === '' ) return $html;
// Wrapper, die nur ein Bild enthalten, komplett entfernen.
$html = preg_replace(
'#<figure\b[^>]*>\s*(<a\b[^>]*>\s*)?<img\b[^>]*>\s*(</a>\s*)?(<figcaption\b[^>]*>.*?</figcaption>\s*)?</figure>#is',
'',
$html
);
$html = preg_replace(
'#<p\b[^>]*>\s*(<a\b[^>]*>\s*)?<img\b[^>]*>\s*(</a>\s*)?</p>#is',
'',
$html
);
$html = preg_replace( '#<a\b[^>]*>\s*<img\b[^>]*>\s*</a>#is', '', $html );
$html = preg_replace( '#<img\b[^>]*>#i', '', $html );
// Aufräumen
$html = preg_replace( '#<p>\s*( |\xC2\xA0)?\s*</p>#u', '', $html );
return trim( preg_replace( '/\n{3,}/', "\n\n", $html ) );
}
/**
* (Unbenutzt — Fallback-Generator wurde absichtlich deaktiviert.
* Funktion bleibt als Referenz erhalten, wird aber vom Importer nicht mehr aufgerufen.)
*/
function wb_generate_fallback_image( $title, $config ) {
if ( ! function_exists( 'imagecreatetruecolor' ) ) {
return null;
}
$W = (int) $config['fallback_img']['width'];
$H = (int) $config['fallback_img']['height'];
$im = imagecreatetruecolor( $W, $H );
imagealphablending( $im, true );
// Bordeaux-Verlauf (oben dunkel → unten weinrot)
$c_top = array( 42, 10, 18 ); // #2a0a12
$c_mid = array( 74, 15, 29 ); // #4a0f1d
$c_bottom = array( 92, 22, 40 ); // #5c1628
for ( $y = 0; $y < $H; $y++ ) {
$t = $y / ( $H - 1 );
if ( $t < 0.5 ) {
$k = $t * 2;
$r = $c_top[0] + ( $c_mid[0] - $c_top[0] ) * $k;
$g = $c_top[1] + ( $c_mid[1] - $c_top[1] ) * $k;
$b = $c_top[2] + ( $c_mid[2] - $c_top[2] ) * $k;
} else {
$k = ( $t - 0.5 ) * 2;
$r = $c_mid[0] + ( $c_bottom[0] - $c_mid[0] ) * $k;
$g = $c_mid[1] + ( $c_bottom[1] - $c_mid[1] ) * $k;
$b = $c_mid[2] + ( $c_bottom[2] - $c_mid[2] ) * $k;
}
$col = imagecolorallocate( $im, (int) $r, (int) $g, (int) $b );
imageline( $im, 0, $y, $W, $y, $col );
}
// Dezente Diagonale (weiches Editorial-Feeling)
$glow = imagecolorallocatealpha( $im, 200, 154, 132, 100 );
for ( $i = 0; $i < 220; $i += 4 ) {
imageline( $im, $W - 600 - $i, -50, $W + 200 - $i, $H + 50, $glow );
}
// Dünner Markenstreifen
$rose = imagecolorallocate( $im, 200, 154, 132 );
imagefilledrectangle( $im, 0, $H - 6, $W, $H, $rose );
// Typografie: Eyebrow + Titel
$font_path = ABSPATH . WPINC . '/fonts/dashicons.ttf'; // Fallback, falls keine echte Schrift verfügbar
// Schöner: System-Font wenn vorhanden
$candidates = array(
'/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
'/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf',
);
foreach ( $candidates as $cand ) {
if ( file_exists( $cand ) ) { $font_path = $cand; break; }
}
$rose_text = imagecolorallocate( $im, 200, 154, 132 );
$white = imagecolorallocate( $im, 244, 237, 231 );
if ( $font_path && file_exists( $font_path ) ) {
// Eyebrow
imagettftext( $im, 14, 0, 80, 100, $rose_text, $font_path, 'TASCHENMUSCHI-KAUFEN.DE' );
// Titel — auf 3 Zeilen umbrechen
$title_text = wp_strip_all_tags( (string) $title );
if ( $title_text === '' ) $title_text = 'Beitrag';
$size_title = 42;
$max_chars = 32; // grobe Heuristik pro Zeile
$lines = array();
$words = preg_split( '/\s+/u', $title_text );
$cur = '';
foreach ( $words as $w ) {
if ( mb_strlen( $cur . ' ' . $w ) <= $max_chars ) {
$cur = trim( $cur . ' ' . $w );
} else {
if ( $cur !== '' ) $lines[] = $cur;
$cur = $w;
}
if ( count( $lines ) >= 3 ) break;
}
if ( $cur !== '' && count( $lines ) < 4 ) $lines[] = $cur;
$y = 200;
foreach ( $lines as $line ) {
imagettftext( $im, $size_title, 0, 80, $y, $white, $font_path, $line );
$y += $size_title + 16;
}
} else {
// GD-Builtin-Font als Notfall
imagestring( $im, 5, 80, 100, 'TASCHENMUSCHI-KAUFEN.DE', $rose_text );
imagestring( $im, 5, 80, 200, wp_strip_all_tags( $title ), $white );
}
// Ergebnis als JPEG in Tempdir
$tmp = wp_tempnam( $config['fallback_img']['filename'] );
imagejpeg( $im, $tmp, 90 );
imagedestroy( $im );
return $tmp;
}
/* ============================================================================
* 9) Importer: Posts + Bilder
* ============================================================================ */
class Wayback_Importer {
private $cfg;
/** @var Wayback_Client */ private $http;
/** @var Wayback_Logger */ private $log;
private $default_cat_id;
private $stats = array(
'imported' => 0, 'updated' => 0, 'skipped' => 0,
'errors' => 0, 'images' => 0,
);
public function __construct( $cfg, Wayback_Client $http, Wayback_Logger $log ) {
$this->cfg = $cfg;
$this->http = $http;
$this->log = $log;
$this->default_cat_id = $this->ensure_category( $cfg['default_category'] );
}
private function ensure_category( $name ) {
$term = term_exists( $name, 'category' );
if ( ! $term ) {
$term = wp_insert_term( $name, 'category' );
}
return is_array( $term ) ? (int) $term['term_id'] : (int) $term;
}
private function find_existing( $source_url ) {
$q = new WP_Query( array(
'post_type' => 'post',
'post_status' => 'any',
'posts_per_page' => 1,
'no_found_rows' => true,
'fields' => 'ids',
'meta_query' => array( array(
'key' => '_wayback_source_url',
'value' => $source_url,
) ),
) );
return $q->have_posts() ? (int) $q->posts[0] : 0;
}
private function find_attachment_by_source( $source_url ) {
$hash = md5( $source_url );
$q = get_posts( array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'fields' => 'ids',
'meta_query' => array( array(
'key' => '_wayback_source_hash',
'value' => $hash,
) ),
'numberposts' => 1,
) );
return $q ? (int) $q[0] : 0;
}
public function sideload_image( $source_url, $alt = '' ) {
if ( ! $source_url ) return 0;
$existing = $this->find_attachment_by_source( $source_url );
if ( $existing ) return $existing;
// Wayback im_-Modus bauen, um sicher die archivierte Variante zu erhalten.
$wayback_url = 'https://web.archive.org/web/0im_/' . $source_url;
$tmp = $this->http->fetch_file( $wayback_url );
if ( ! $tmp ) {
// Plan B: direkt das Original probieren
$tmp = $this->http->fetch_file( $source_url );
}
if ( ! $tmp ) return 0;
$filename = sanitize_file_name( basename( wp_parse_url( $source_url, PHP_URL_PATH ) ) );
if ( ! $filename || strpos( $filename, '.' ) === false ) {
$filename = 'wayback-' . substr( md5( $source_url ), 0, 8 ) . '.jpg';
}
$file_array = array( 'name' => $filename, 'tmp_name' => $tmp );
$att_id = media_handle_sideload( $file_array, 0, $alt );
if ( is_wp_error( $att_id ) ) {
@unlink( $tmp );
$this->log->warn( 'Bild-Sideload fehlgeschlagen', array( 'url' => $source_url, 'message' => $att_id->get_error_message() ) );
return 0;
}
update_post_meta( $att_id, '_wayback_source_hash', md5( $source_url ) );
update_post_meta( $att_id, '_wayback_source_url', $source_url );
if ( $alt ) update_post_meta( $att_id, '_wp_attachment_image_alt', $alt );
$this->stats['images']++;
return (int) $att_id;
}
public function import_one( $article ) {
$source_url = $article['source_url'];
$existing_id = $this->find_existing( $source_url );
if ( $existing_id && ! $this->cfg['force'] ) {
$this->log->info( 'Übersprungen (existiert)', array( 'url' => $source_url, 'post_id' => $existing_id ) );
$this->stats['skipped']++;
return;
}
$parsed = wb_parse_post_path( $source_url );
if ( ! $parsed ) {
$this->log->error( 'URL-Pfad nicht parsebar', array( 'url' => $source_url ) );
$this->stats['errors']++;
return;
}
if ( $article['title'] === '' ) {
$this->log->error( 'Kein Titel', array( 'url' => $source_url ) );
$this->stats['errors']++;
return;
}
if ( $article['content_html'] === '' ) {
$this->log->error( 'Kein Content', array( 'url' => $source_url ) );
$this->stats['errors']++;
return;
}
$date_gmt = $article['date'] ?: ( $parsed['year'] . '-' . $parsed['month'] . '-01T00:00:00+00:00' );
$ts = strtotime( $date_gmt );
$post_date_gmt = gmdate( 'Y-m-d H:i:s', $ts );
$post_date = get_date_from_gmt( $post_date_gmt );
// Bilder werden bewusst NICHT importiert.
// Inline-<img>-Tags werden aus dem Content komplett entfernt;
// im Frontend springt der theme-seitige no-image.jpg-Fallback ein.
$content = wb_strip_all_images( $article['content_html'] );
$featured_id = 0;
if ( $this->cfg['dry_run'] ) {
$this->log->info( '[DRY] Würde importieren', array(
'url' => $source_url, 'title' => $article['title'], 'date' => $post_date,
'slug' => $parsed['slug'], 'images' => count( $article['inline_images'] ),
'featured_ready' => (bool) ( $article['og_image'] || $article['inline_images'] ),
) );
$this->stats['imported']++;
return;
}
$postarr = array(
'post_type' => 'post',
'post_title' => $article['title'],
'post_content' => $content,
'post_status' => 'publish',
'post_name' => sanitize_title( $parsed['slug'] ),
'post_date' => $post_date,
'post_date_gmt' => $post_date_gmt,
'post_category' => array( $this->default_cat_id ),
'meta_input' => array(
'_wayback_source_url' => $source_url,
'_wayback_imported_at' => current_time( 'mysql' ),
'_wayback_import_hash' => sha1( $article['title'] . $content ),
),
);
if ( $existing_id ) {
$postarr['ID'] = $existing_id;
$post_id = wp_update_post( $postarr, true );
$action = 'updated';
} else {
$post_id = wp_insert_post( $postarr, true );
$action = 'imported';
}
if ( is_wp_error( $post_id ) || ! $post_id ) {
$msg = is_wp_error( $post_id ) ? $post_id->get_error_message() : 'unknown';
$this->log->error( 'Insert/Update fehlgeschlagen', array( 'url' => $source_url, 'message' => $msg ) );
$this->stats['errors']++;
return;
}
if ( $featured_id ) {
set_post_thumbnail( $post_id, $featured_id );
}
$this->log->info( $action, array( 'url' => $source_url, 'post_id' => $post_id, 'slug' => $parsed['slug'] ) );
$this->stats[ $action === 'imported' ? 'imported' : 'updated' ]++;
}
public function stats() { return $this->stats; }
}
/* ============================================================================
* 10) Permalinks
* ============================================================================ */
function wb_ensure_permalinks( Wayback_Logger $log ) {
$required = '/%year%/%monthnum%/%postname%/';
$current = get_option( 'permalink_structure' );
if ( $current !== $required ) {
update_option( 'permalink_structure', $required );
$log->info( 'Permalink-Struktur auf ' . $required . ' gesetzt (vorher: "' . $current . '")' );
}
// Hard-Flush: nur so wird das Date-Postname-Regelset wirklich neu generiert.
delete_option( 'rewrite_rules' );
flush_rewrite_rules( false );
$log->info( 'Rewrite-Regeln neu generiert', array( 'count' => count( get_option( 'rewrite_rules', array() ) ) ) );
}
/* ============================================================================
* 11) Main
* ============================================================================ */
$LOG->info( 'Start', array(
'limit' => $config['limit'],
'dry_run' => (bool) $config['dry_run'],
'force' => (bool) $config['force'],
) );
wb_ensure_permalinks( $LOG );
$urls = wb_collect_article_urls( $config, $HTTP, $LOG );
if ( ! $urls ) {
$LOG->error( 'Keine Artikel-URLs gefunden.' );
exit( 1 );
}
if ( $config['limit'] ) {
$urls = array_slice( $urls, 0, (int) $config['limit'] );
$LOG->info( 'Limit aktiv', array( 'urls' => count( $urls ) ) );
}
$importer = new Wayback_Importer( $config, $HTTP, $LOG );
$start = microtime( true );
foreach ( $urls as $i => $wayback_url ) {
$idx = $i + 1;
$LOG->info( ">>> [$idx/" . count( $urls ) . "] " . $wayback_url );
$html = $HTTP->fetch( $wayback_url );
if ( ! $html ) {
$LOG->error( 'HTML nicht abrufbar', array( 'url' => $wayback_url ) );
continue;
}
$article = wb_parse_article( $html, $wayback_url, $config );
if ( ! $article ) {
$LOG->error( 'Parse fehlgeschlagen', array( 'url' => $wayback_url ) );
continue;
}
$importer->import_one( $article );
}
$dur = number_format( microtime( true ) - $start, 1 );
$LOG->info( "Fertig in ${dur}s", $importer->stats() );
if ( PHP_SAPI !== 'cli' ) {
echo "\n👉 Wenn Permalinks vorher abwichen, im Browser die Seite einmal aufrufen — Rewrite-Regeln sind bereits geflusht.\n";
}