format('Y-m-d') === $dateString && $today > $expiryDate) { contentpanel_delete_news_files($fileBase); } } } function contentpanel_render_news( string $pdfPath, string $outputDirectory, string $fileBase, string $mode, ?string $date = null ): array { $timeout = $mode === 'template' ? filter_var(getenv('CONTENTPANEL_JOB_TIMEOUT_SECONDS') ?: '3600', FILTER_VALIDATE_INT, ['options' => ['min_range' => 60, 'max_range' => 86400]]) : 600; if ($timeout === false) throw new RuntimeException('Ungültiges Verarbeitungstimeout.'); $output = contentpanel_run([CONTENTPANEL_PYTHON, CONTENTPANEL_AUSHANG_WORKER, '--pdf', $pdfPath, '--template', CONTENTPANEL_AUSHANG_TEMPLATE, '--output-dir', $outputDirectory, '--base-name', $fileBase, '--date', $date ?? date('Y-m-d'), '--mode', $mode], $timeout, [ 2 => 'Die KI-Ausgabe enthält eine nicht belegte Zahl oder Kontaktangabe. Bitte erneut versuchen oder die Original-PDF-Darstellung verwenden.', 3 => 'Die KI-Verarbeitung ist fehlgeschlagen. Für Bild-PDFs muss das konfigurierte Modell Bilder lesen können. Bitte erneut versuchen oder die Original-PDF-Darstellung verwenden.', 4 => 'Die PDF konnte nicht vollständig gelesen werden. Bitte prüfen Sie die Datei und ihre Lesbarkeit.', 124 => 'Die PDF-Verarbeitung hat zu lange gedauert. Bitte verwenden Sie eine kleinere PDF oder die Original-PDF-Darstellung.', ]); $result = json_decode($output, true, 32, JSON_THROW_ON_ERROR); if (($result['ok'] ?? false) !== true || !is_int($result['pages'] ?? null) || $result['pages'] < 1) { throw new RuntimeException('Der Aushang-Worker meldet ein unvollständiges Ergebnis.'); } return $result; } function contentpanel_publish_news_pages(array $generatedFiles, string $fileBase): void { if (!$generatedFiles || !contentpanel_is_valid_news_file_base($fileBase)) { throw new RuntimeException('Es wurden keine gültigen Aushangseiten erzeugt.'); } foreach (array_values($generatedFiles) as $index => $source) { if (!is_file($source) || basename($source) !== $fileBase . '_page_' . ($index + 1) . '.png') { throw new RuntimeException('Die erzeugten Aushangseiten sind unvollständig.'); } } contentpanel_news_locked(function () use ($generatedFiles, $fileBase) { $token = bin2hex(random_bytes(6)); $backups = $published = []; try { foreach (glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/' . $fileBase . '_page_*.png') ?: [] as $existing) { $backup = $existing . '.bak-' . $token; if (!rename($existing, $backup)) throw new RuntimeException('Aushang konnte nicht gesichert werden.'); $backups[$backup] = $existing; } foreach ($generatedFiles as $source) { $target = CONTENTPANEL_NEWS_PNG_DIRECTORY . '/' . basename($source); if (!rename($source, $target)) throw new RuntimeException('Aushangseite konnte nicht veröffentlicht werden.'); $published[] = $target; } } catch (Throwable $exception) { foreach ($published as $file) @unlink($file); $restored = true; foreach ($backups as $backup => $original) $restored = @rename($backup, $original) && $restored; if (!$restored) { // Failed backups are retained as the last copy of the original announcement. throw new RuntimeException('Veröffentlichung fehlgeschlagen. Sicherungsdateien konnten nicht vollständig zurückgespielt werden.', 0, $exception); } throw $exception; } foreach ($backups as $backup => $original) @unlink($backup); }); } function contentpanel_upload_news(array $input, array $file, PDO $pdo, array $user): string { $expiryDate = contentpanel_date($input['date'] ?? '', 'Ungültiges Verfallsdatum.'); $title = contentpanel_sanitize_news_title(trim((string) ($input['name'] ?? ''))); if ($title === '') { throw new InvalidArgumentException('Bitte einen gültigen Titel eingeben.'); } $mode = trim((string) ($input['format_mode'] ?? 'original')); if (!in_array($mode, ['original', 'template'], true)) { throw new InvalidArgumentException('Ungültiger Aushang-Modus.'); } $pdfPath = contentpanel_validate_pdf_upload( $file, 'Bitte eine gültige PDF-Datei hochladen.', 'Bitte eine gültige PDF-Datei hochladen.' ); $job = ['base' => $expiryDate->format('Y-m-d') . '-' . $title, 'title' => trim((string) $input['name']), 'mode' => $mode, 'date' => date('Y-m-d')]; if ($mode === 'template') { return contentpanel_enqueue_news($pdo, $user, $pdfPath, $job); } contentpanel_process_news($pdfPath, $job); return 'Aushang wurde veröffentlicht.'; } function contentpanel_process_news(string $pdfPath, array $job, ?callable $beforePublish = null): array { if (!is_dir(CONTENTPANEL_NEWS_PNG_DIRECTORY) && !mkdir(CONTENTPANEL_NEWS_PNG_DIRECTORY, 0755, true)) { throw new RuntimeException('Das Aushang-Verzeichnis konnte nicht angelegt werden.'); } $fileBase = $job['base']; // Same filesystem as the final images: publishing only needs atomic renames. $tempDirectory = CONTENTPANEL_NEWS_PNG_DIRECTORY . '/.upload-' . (isset($job['id']) ? 'job-' . (int) $job['id'] : bin2hex(random_bytes(8))); if (!is_dir($tempDirectory) && !mkdir($tempDirectory, 0700, true)) { throw new RuntimeException('Temporäres Aushang-Verzeichnis konnte nicht angelegt werden.'); } try { // A restarted queue job may have left incomplete render output. foreach (glob($tempDirectory . '/*') ?: [] as $path) if (is_file($path)) unlink($path); $result = contentpanel_render_news($pdfPath, $tempDirectory, $fileBase, $job['mode'], $job['date']); $generatedFiles = glob($tempDirectory . '/' . $fileBase . '_page_*.png') ?: []; natsort($generatedFiles); if (count($generatedFiles) !== $result['pages']) { throw new RuntimeException('Die erzeugte Seitenzahl ist unvollständig.'); } if ($beforePublish) $beforePublish(); contentpanel_publish_news_pages($generatedFiles, $fileBase); return $result; } finally { foreach (glob($tempDirectory . '/*') ?: [] as $filePath) { if (is_file($filePath)) { @unlink($filePath); } } @rmdir($tempDirectory); } } function contentpanel_list_news(): array { $items = []; foreach (glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/*_page_1.png') ?: [] as $file) { $base = basename($file, '_page_1.png'); if (!is_file($file) || !contentpanel_is_valid_news_file_base($base)) { continue; } $items[] = [ 'base' => $base, 'title' => substr($base, 11), 'pages' => count(glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/' . $base . '_page_[0-9]*.png') ?: []), 'expiry' => contentpanel_format_display_date(substr($base, 0, 10)), 'image_url' => '/intranet_repo/projects/contentpanel/insertion/news/png/' . rawurlencode(basename($file)) . '?v=' . (int) filemtime($file), ]; } return $items; }