diff --git a/public/ajax/sonnenfinsternis_export.php b/public/ajax/sonnenfinsternis_export.php new file mode 100644 index 0000000..6c81fae --- /dev/null +++ b/public/ajax/sonnenfinsternis_export.php @@ -0,0 +1,239 @@ + 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); diff --git a/public/css/style.css b/public/css/style.css index ce79b0a..c044885 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -3880,12 +3880,14 @@ body.solarsystem-page:not(.jupitersystem-page) .solarsystem-stage-card { } .solarsystem-input-row input[type="date"], +.solarsystem-input-row input[type="time"], .solarsystem-input-row select, .solarsystem-sidebar input[type="range"] { width: 100%; } .solarsystem-input-row input[type="date"], +.solarsystem-input-row input[type="time"], .solarsystem-input-row select, .solarsystem-multiselect { min-height: 2.6rem; @@ -3899,6 +3901,7 @@ body.solarsystem-page:not(.jupitersystem-page) .solarsystem-stage-card { } .solarsystem-input-row input[type="date"], +.solarsystem-input-row input[type="time"], .solarsystem-input-row select { padding: 0.55rem 0.8rem; } diff --git a/public/sonnenfinsternis.php b/public/sonnenfinsternis.php index 4af6571..dd2cab9 100644 --- a/public/sonnenfinsternis.php +++ b/public/sonnenfinsternis.php @@ -18,6 +18,11 @@ session_start(); $loggedIn = isset($_SESSION['user_id']); $pageTitle = 'Sonnenfinsternis – AstroTools'; $bodyClass = 'solarsystem-page'; + +if (!isset($_SESSION['sonnenfinsternis_export_token']) || !is_string($_SESSION['sonnenfinsternis_export_token'])) { + $_SESSION['sonnenfinsternis_export_token'] = bin2hex(random_bytes(16)); +} +$sonnenfinsternisExportToken = $_SESSION['sonnenfinsternis_export_token']; ?> Kontaktzeiten (UTC)