403Webshell
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 :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/clients/client2/web32/web/tools/import-wayback-blog.php
<?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, &nbsp; bereinigen
    $out = preg_replace( '#<p>\s*(&nbsp;)?\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*(&nbsp;|\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";
}

Youez - 2016 - github.com/yon3zu
LinuXploit