206 lines
9.4 KiB
PHP
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;
|
|
}
|
|
|