Files
intranet/web/intranet_repo/projects/contentpanel/insertion/util/news.php
T

206 lines
9.4 KiB
PHP

<?php
const CONTENTPANEL_NEWS_PNG_DIRECTORY = __DIR__ . '/../news/png';
const CONTENTPANEL_AUSHANG_TEMPLATE = __DIR__ . '/../template/aushang-template.docx';
const CONTENTPANEL_AUSHANG_WORKER = '/opt/intranet/worker/contentpanel_aushang/process.py';
const CONTENTPANEL_PYTHON = '/opt/intranet-venv/bin/python';
function contentpanel_sanitize_news_title(string $name): string
{
return trim(preg_replace('/[^A-Za-z0-9_-]/', '_', $name), '_-');
}
function contentpanel_is_valid_news_file_base(string $fileBase): bool
{
return preg_match('/^\d{4}-\d{2}-\d{2}-[A-Za-z0-9_-]+$/D', $fileBase) === 1;
}
function contentpanel_delete_news_files(string $fileBase): bool
{
if (!contentpanel_is_valid_news_file_base($fileBase)) {
return false;
}
return contentpanel_news_locked(function () use ($fileBase) {
$success = true;
foreach (glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/' . $fileBase . '_page_*.png') ?: [] as $file) {
$success = @unlink($file) && $success;
}
return $success;
});
}
function contentpanel_news_locked(callable $operation)
{
$lock = fopen(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/.publish.lock', 'c');
if (!$lock) throw new RuntimeException('Aushang-Verzeichnis konnte nicht gesperrt werden.');
try {
if (!flock($lock, LOCK_EX)) throw new RuntimeException('Aushang-Verzeichnis konnte nicht gesperrt werden.');
return $operation();
} finally {
fclose($lock);
}
}
function contentpanel_cleanup_expired_news(): void
{
$today = new DateTimeImmutable('today');
foreach (glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/*_page_1.png') ?: [] as $file) {
$fileBase = basename($file, '_page_1.png');
if (!contentpanel_is_valid_news_file_base($fileBase)) {
continue;
}
$dateString = substr($fileBase, 0, 10);
$expiryDate = DateTimeImmutable::createFromFormat('!Y-m-d', $dateString);
if ($expiryDate !== false && $expiryDate->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;
}