false, 'message' => 'Bitte anmelden.'], 403); } } function sfExportRequireToken(): void { $sessionToken = isset($_SESSION['sonnenfinsternis_export_token']) && is_string($_SESSION['sonnenfinsternis_export_token']) ? $_SESSION['sonnenfinsternis_export_token'] : ''; $submittedToken = isset($_REQUEST['token']) ? (string) $_REQUEST['token'] : ''; if ($sessionToken === '' || $submittedToken === '' || !hash_equals($sessionToken, $submittedToken)) { sfExportJson(['success' => false, 'message' => 'Sicherheitsprüfung fehlgeschlagen. Bitte Seite neu laden.'], 403); } } function sfExportDir(string $exportId): string { return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'sfexport_' . $exportId; } function sfExportRequireOwnedExportId(): string { $exportId = isset($_REQUEST['exportId']) ? (string) $_REQUEST['exportId'] : ''; if (!preg_match('/^[a-f0-9]{32}$/', $exportId) || !isset($_SESSION['sonnenfinsternis_export_ids'][$exportId])) { sfExportJson(['success' => false, 'message' => 'Unbekannte oder abgelaufene Export-ID.'], 403); } return $exportId; } function sfExportDeleteDir(string $dir): void { if (!is_dir($dir)) { return; } foreach (glob($dir . DIRECTORY_SEPARATOR . '*') ?: [] as $file) { @unlink($file); } @rmdir($dir); } function sfExportSweepStale(): void { $base = sys_get_temp_dir(); foreach (glob($base . DIRECTORY_SEPARATOR . 'sfexport_*', GLOB_ONLYDIR) ?: [] as $dir) { $mtime = @filemtime($dir); if ($mtime !== false && (time() - $mtime) > SF_EXPORT_TTL_SECONDS) { sfExportDeleteDir($dir); } } } // ─── Store-Format-ZIP-Writer (kein ZipArchive-Modul vorausgesetzt) ───────── function sfZipLocalHeader(string $name, int $crc, int $size, int $dosTime, int $dosDate): string { return pack( 'VvvvvvVVVvv', 0x04034b50, 20, 0, 0, $dosTime, $dosDate, $crc, $size, $size, strlen($name), 0 ) . $name; } function sfZipCentralHeader(string $name, int $crc, int $size, int $dosTime, int $dosDate, int $offset): string { return pack( 'VvvvvvvVVVvvvvvVV', 0x02014b50, 20, 20, 0, 0, $dosTime, $dosDate, $crc, $size, $size, strlen($name), 0, 0, 0, 0, 0, $offset ) . $name; } function sfBuildZip(array $sourceFiles, string $zipPath): void { $fh = fopen($zipPath, 'wb'); if ($fh === false) { throw new RuntimeException('ZIP-Datei konnte nicht angelegt werden.'); } $dt = getdate(); $dosTime = ($dt['hours'] << 11) | ($dt['minutes'] << 5) | intdiv($dt['seconds'], 2); $dosDate = (($dt['year'] - 1980) << 9) | ($dt['mon'] << 5) | $dt['mday']; $centralBlob = ''; $offset = 0; foreach ($sourceFiles as $name => $path) { $data = file_get_contents($path); if ($data === false) { fclose($fh); @unlink($zipPath); throw new RuntimeException('Frame-Datei konnte nicht gelesen werden: ' . $path); } $crc = crc32($data); $size = strlen($data); fwrite($fh, sfZipLocalHeader($name, $crc, $size, $dosTime, $dosDate)); fwrite($fh, $data); unset($data); $centralBlob .= sfZipCentralHeader($name, $crc, $size, $dosTime, $dosDate, $offset); $offset += 30 + strlen($name) + $size; } $centralStart = $offset; fwrite($fh, $centralBlob); fwrite($fh, pack( 'VvvvvVVv', 0x06054b50, 0, 0, count($sourceFiles), count($sourceFiles), strlen($centralBlob), $centralStart, 0 )); fclose($fh); } // ─── Routing ──────────────────────────────────────────────────────────────── sfExportRequireLogin(); sfExportRequireToken(); $action = isset($_REQUEST['action']) ? (string) $_REQUEST['action'] : ''; if ($action === 'start') { sfExportSweepStale(); $exportId = bin2hex(random_bytes(16)); $dir = sfExportDir($exportId); if (!mkdir($dir, 0700, true) && !is_dir($dir)) { sfExportJson(['success' => false, 'message' => 'Temp-Verzeichnis konnte nicht angelegt werden.'], 500); } $_SESSION['sonnenfinsternis_export_ids'][$exportId] = time(); sfExportJson(['success' => true, 'exportId' => $exportId]); } if ($action === 'frame') { $exportId = sfExportRequireOwnedExportId(); $index = isset($_REQUEST['index']) ? (string) $_REQUEST['index'] : ''; if (!ctype_digit($index) || (int) $index > SF_EXPORT_MAX_INDEX) { sfExportJson(['success' => false, 'message' => 'Ungültiger Frame-Index.'], 400); } $raw = file_get_contents('php://input', false, null, 0, SF_EXPORT_MAX_FRAME_BYTES + 1); if ($raw === false || $raw === '' || strlen($raw) > SF_EXPORT_MAX_FRAME_BYTES) { sfExportJson(['success' => false, 'message' => 'Ungültige oder zu große Bilddaten.'], 400); } $filename = sfExportDir($exportId) . DIRECTORY_SEPARATOR . sprintf('frame_%05d.png', (int) $index); if (file_put_contents($filename, $raw) === false) { sfExportJson(['success' => false, 'message' => 'Frame konnte nicht gespeichert werden.'], 500); } sfExportJson(['success' => true]); } if ($action === 'cancel') { $exportId = sfExportRequireOwnedExportId(); sfExportDeleteDir(sfExportDir($exportId)); unset($_SESSION['sonnenfinsternis_export_ids'][$exportId]); sfExportJson(['success' => true]); } if ($action === 'finish') { $exportId = sfExportRequireOwnedExportId(); $dir = sfExportDir($exportId); $frameFiles = glob($dir . DIRECTORY_SEPARATOR . 'frame_*.png') ?: []; sort($frameFiles); if ($frameFiles === []) { sfExportJson(['success' => false, 'message' => 'Keine Frames für diesen Export gefunden.'], 404); } $sourceFiles = []; foreach ($frameFiles as $path) { $sourceFiles[basename($path)] = $path; } // Das Zippen tausender Frames dauert laenger als PHPs Standard-Zeitlimit // (30s) - hier ist es ein legitimer, einmaliger Vorgang fuer einen // eingeloggten Nutzer, daher wird das Limit fuer diesen Request aufgehoben. set_time_limit(0); $zipPath = $dir . DIRECTORY_SEPARATOR . 'export.zip'; try { sfBuildZip($sourceFiles, $zipPath); } catch (Throwable $e) { sfExportDeleteDir($dir); unset($_SESSION['sonnenfinsternis_export_ids'][$exportId]); sfExportJson(['success' => false, 'message' => 'ZIP konnte nicht erstellt werden: ' . $e->getMessage()], 500); } header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="sonnenfinsternis_export_' . substr($exportId, 0, 8) . '.zip"'); header('Content-Length: ' . (string) filesize($zipPath)); // Aktive Output-Buffer wuerden die komplette (potenziell mehrere GB grosse) // ZIP-Datei im Speicher aufsammeln statt sie durchzureichen - das war die // Ursache des Speicherfehlers. Deshalb Buffer abschalten und die Datei in // festen Bloecken direkt ausgeben, statt readfile() das ini-abhaengige // Verhalten zu ueberlassen. while (ob_get_level() > 0) { ob_end_clean(); } $out = fopen($zipPath, 'rb'); if ($out !== false) { while (!feof($out)) { echo fread($out, 1024 * 1024); flush(); } fclose($out); } sfExportDeleteDir($dir); unset($_SESSION['sonnenfinsternis_export_ids'][$exportId]); exit; } sfExportJson(['success' => false, 'message' => 'Unbekannte Aktion.'], 400);