Merge pull request 'develop - explain local setup' (#3) from dev into main
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
# Intranet lokal aufsetzen
|
||||
|
||||
## 1. Docker installieren
|
||||
|
||||
### Windows + WSL 2 — empfohlen
|
||||
|
||||
PowerShell als Administrator:
|
||||
|
||||
```powershell
|
||||
wsl --install -d Ubuntu-24.04
|
||||
wsl --update
|
||||
```
|
||||
|
||||
Windows neu starten und prüfen:
|
||||
|
||||
```powershell
|
||||
wsl -l -v
|
||||
```
|
||||
|
||||
Ubuntu muss mit **WSL-Version 2** laufen.
|
||||
|
||||
Falls `Ubuntu-24.04` über `wsl --install` nicht installiert werden kann, Ubuntu 24.04 alternativ über den Microsoft Store installieren.
|
||||
|
||||
Danach **Docker Desktop for Windows** installieren und das WSL-2-Backend verwenden.
|
||||
|
||||
In Docker Desktop:
|
||||
|
||||
```text
|
||||
Settings → Resources → WSL Integration
|
||||
```
|
||||
|
||||
Integration für Ubuntu aktivieren und in WSL testen:
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
docker run --rm hello-world
|
||||
```
|
||||
|
||||
> Docker Engine nicht zusätzlich innerhalb von WSL installieren, wenn Docker Desktop verwendet wird.
|
||||
>
|
||||
> Für bessere Dateisystem-Performance das Repository möglichst im WSL-Dateisystem ablegen, z. B. `~/src/intranet`, nicht unter `/mnt/c/...`.
|
||||
|
||||
---
|
||||
|
||||
### Linux — Ubuntu 24.04 LTS
|
||||
|
||||
**1. Konfliktierende Pakete entfernen**
|
||||
|
||||
```bash
|
||||
sudo apt remove docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc
|
||||
```
|
||||
|
||||
**2. Voraussetzungen installieren**
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install ca-certificates curl -y
|
||||
```
|
||||
|
||||
**3. Docker GPG-Key hinzufügen**
|
||||
|
||||
```bash
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
|
||||
-o /etc/apt/keyrings/docker.asc
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||
```
|
||||
|
||||
**4. Docker-Repository einrichten**
|
||||
|
||||
```bash
|
||||
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
|
||||
Types: deb
|
||||
URIs: https://download.docker.com/linux/ubuntu
|
||||
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
|
||||
Components: stable
|
||||
Architectures: $(dpkg --print-architecture)
|
||||
Signed-By: /etc/apt/keyrings/docker.asc
|
||||
EOF
|
||||
|
||||
sudo apt update
|
||||
```
|
||||
|
||||
**5. Docker Engine und Compose installieren**
|
||||
|
||||
```bash
|
||||
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
|
||||
```
|
||||
|
||||
**6. Docker aktivieren und testen**
|
||||
|
||||
```bash
|
||||
sudo systemctl enable --now docker
|
||||
sudo docker run --rm hello-world
|
||||
docker compose version
|
||||
```
|
||||
|
||||
**7. Optional: Docker ohne `sudo` verwenden**
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
Danach ab- und wieder anmelden.
|
||||
|
||||
> **Debian 13:** Das Projekt funktioniert ebenfalls unter Debian. Dafür das Docker-Repository entsprechend auf `linux/debian` umstellen:
|
||||
> <https://docs.docker.com/engine/install/debian/>
|
||||
|
||||
---
|
||||
|
||||
### macOS
|
||||
|
||||
1. **Docker Desktop for Mac** passend für Apple Silicon oder Intel installieren.
|
||||
2. Docker Desktop starten.
|
||||
3. Im Terminal testen:
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
docker run --rm hello-world
|
||||
```
|
||||
|
||||
Dokumentation:
|
||||
|
||||
<https://docs.docker.com/desktop/setup/install/mac-install/>
|
||||
|
||||
---
|
||||
|
||||
## 2. Lokale Docker-Konfiguration
|
||||
|
||||
Im Projektroot müssen folgende Dateien liegen:
|
||||
|
||||
```text
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
docker-compose.local.yml
|
||||
web/
|
||||
```
|
||||
|
||||
Die produktive `docker-compose.yml` wird lokal **nicht** verwendet. Dadurch werden insbesondere folgende Produktionsbestandteile nicht gestartet:
|
||||
|
||||
- `scheduler`
|
||||
- `dokploy-network`
|
||||
- Strato-SSH
|
||||
- produktive Scheduler-Secrets
|
||||
|
||||
Die lokale Compose-Datei startet ausschließlich:
|
||||
|
||||
- `web`
|
||||
- `db` (PostgreSQL)
|
||||
|
||||
Sie verwendet denselben `Dockerfile` wie Produktion.
|
||||
|
||||
Der Web-Container ist lokal unter `127.0.0.1:8080` erreichbar. PostgreSQL wird für pgAdmin unter `127.0.0.1:5432` freigegeben.
|
||||
|
||||
Durch den Bind-Mount
|
||||
|
||||
```text
|
||||
./web → /var/www/html
|
||||
```
|
||||
|
||||
werden Änderungen an PHP, CSS und JavaScript ohne erneuten Image-Build sichtbar. Laufzeitdaten wie Profilbilder und Aushänge liegen weiterhin in lokalen Docker-Volumes.
|
||||
|
||||
---
|
||||
|
||||
## 3. Environment-Variablen anlegen
|
||||
|
||||
Im Projektroot eine `.env` anlegen:
|
||||
|
||||
```dotenv
|
||||
# PostgreSQL
|
||||
POSTGRES_VERSION=16
|
||||
DB_NAME=intranet
|
||||
DB_USER=intranet
|
||||
DB_PASSWORD=local-dev-password
|
||||
|
||||
# Intranet
|
||||
INTRANET_FQDN=localhost:8080
|
||||
|
||||
# Lokale Dummy-Werte für externe Dienste
|
||||
SMTP_HOST=127.0.0.1
|
||||
SMTP_PORT=1025
|
||||
SMTP_USER=local
|
||||
SMTP_PASSWORD=local
|
||||
ADMIN_EMAIL=local@example.invalid
|
||||
|
||||
SMS_GATEWAY_URL=http://127.0.0.1:9
|
||||
HA_BASE_URL=http://127.0.0.1:9
|
||||
HA_TOKEN=local
|
||||
OPENWEATHER_API_KEY=local
|
||||
```
|
||||
|
||||
### PostgreSQL-Version prüfen
|
||||
|
||||
Lokal sollte möglichst dieselbe PostgreSQL-Major-Version wie in Produktion verwendet werden. Wir verwenden **PostgreSQL 18**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Produktivdatenbank mit pgAdmin sichern
|
||||
|
||||
In pgAdmin:
|
||||
|
||||
1. Produktivserver öffnen.
|
||||
2. Unter **Databases** die Intranet-Datenbank rechtsklicken.
|
||||
3. **Backup…** auswählen.
|
||||
4. Datei außerhalb des Repositorys speichern, z. B. `intranet.dump`.
|
||||
5. **Format: Custom** wählen.
|
||||
6. Unter **Data Options → Do not save**:
|
||||
- **Owner: Yes**
|
||||
- **Privileges: Yes**
|
||||
7. **Include CREATE DATABASE statement: No** lassen.
|
||||
8. **Backup** starten.
|
||||
|
||||
> Der Dump enthält Produktivdaten. Nicht committen oder weitergeben.
|
||||
|
||||
---
|
||||
|
||||
## 5. Lokale Datenbank starten
|
||||
|
||||
Im Projektroot:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml up -d db
|
||||
```
|
||||
|
||||
Status prüfen:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml ps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Dump mit pgAdmin importieren
|
||||
|
||||
In pgAdmin einen neuen Server registrieren:
|
||||
|
||||
```text
|
||||
Host: localhost
|
||||
Port: 5432
|
||||
Maintenance database: intranet
|
||||
Username: intranet
|
||||
Password: local-dev-password
|
||||
```
|
||||
|
||||
Die Werte müssen den Angaben in `.env` entsprechen.
|
||||
|
||||
Danach:
|
||||
|
||||
1. Lokale Datenbank `intranet` rechtsklicken.
|
||||
2. **Restore…** auswählen.
|
||||
3. **Format: Custom**.
|
||||
4. `intranet.dump` auswählen.
|
||||
5. Unter **Data Options → Do not save**:
|
||||
- **Owner: Yes**
|
||||
- **Privileges: Yes**
|
||||
6. **Include CREATE DATABASE statement: No**
|
||||
7. **Clean before restore: No** beim ersten Import.
|
||||
8. **Restore** starten.
|
||||
|
||||
Soll die lokale Datenbank später vollständig durch einen neuen Dump ersetzt werden:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml down -v
|
||||
docker compose -f docker-compose.local.yml up -d db
|
||||
```
|
||||
|
||||
Danach den Dump erneut importieren.
|
||||
|
||||
> `down -v` löscht neben PostgreSQL auch die übrigen lokalen Intranet-Volumes.
|
||||
|
||||
---
|
||||
|
||||
## 7. Intranet starten
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml up -d --build
|
||||
```
|
||||
|
||||
Aufrufen:
|
||||
|
||||
<http://localhost:8080>
|
||||
|
||||
Die Benutzer aus dem importierten Dump können anschließend für den Login verwendet werden.
|
||||
|
||||
Die lokale Compose-Datei setzt `session.cookie_secure=0`, da lokal HTTP statt HTTPS verwendet wird.
|
||||
|
||||
---
|
||||
|
||||
## Entwicklung
|
||||
|
||||
Status:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml ps
|
||||
```
|
||||
|
||||
Logs:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml logs -f web
|
||||
```
|
||||
|
||||
Nach Änderungen am `Dockerfile` neu bauen:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml up -d --build
|
||||
```
|
||||
|
||||
Normale Änderungen unter `web/` benötigen durch den Bind-Mount keinen Rebuild.
|
||||
|
||||
Stoppen:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml down
|
||||
```
|
||||
|
||||
Komplett zurücksetzen:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml down -v
|
||||
```
|
||||
@@ -0,0 +1,63 @@
|
||||
# LOCAL DEVELOPMENT ONLY
|
||||
|
||||
name: intranet-local
|
||||
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "127.0.0.1:8080:80"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
TZ: Europe/Berlin
|
||||
DB_HOST: db
|
||||
DB_PORT: "5432"
|
||||
CKAL_XLSX_DIR: /data/ckal-xlsx
|
||||
STRATO_SYNC_ENABLED: "false"
|
||||
BRANDSCHUTZ_MAIL_ENABLED: "false"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
command: >
|
||||
sh -c "printf '%s\n' 'session.cookie_secure = 0'
|
||||
> /usr/local/etc/php/conf.d/zz-local.ini
|
||||
&& exec apache2-foreground"
|
||||
volumes:
|
||||
- ./web:/var/www/html
|
||||
- profile_pictures:/var/www/html/intranet_repo/general_files/profile_pictures
|
||||
- content_news:/var/www/html/intranet_repo/projects/contentpanel/insertion/news
|
||||
- developer_work:/var/www/html/intranet_repo/projects/developerpanel/work
|
||||
- seelsorge_data:/var/www/html/intranet_repo/projects/seelsorge/data
|
||||
- dashboard_month:/var/www/html/intranet_repo/projects/dashboard/month
|
||||
- ckal_xlsx:/data/ckal-xlsx
|
||||
- calendar_pdf:/var/www/html/pdf
|
||||
|
||||
db:
|
||||
image: postgres:${POSTGRES_VERSION:-16}
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
environment:
|
||||
TZ: Europe/Berlin
|
||||
POSTGRES_DB: ${DB_NAME:?set DB_NAME in .env}
|
||||
POSTGRES_USER: ${DB_USER:?set DB_USER in .env}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
profile_pictures:
|
||||
content_news:
|
||||
developer_work:
|
||||
seelsorge_data:
|
||||
dashboard_month:
|
||||
ckal_xlsx:
|
||||
calendar_pdf:
|
||||
@@ -8,7 +8,7 @@ if (!isset($groups["Allgemein"]) || $groups["Allgemein"] != 1) {
|
||||
exit();
|
||||
}
|
||||
|
||||
// Abgelaufene News samt PDF und PNGs entfernen
|
||||
// Abgelaufene Aushänge entfernen
|
||||
cleanupExpiredNews();
|
||||
|
||||
|
||||
@@ -22,11 +22,6 @@ function sanitizeFileName($fileName) {
|
||||
return preg_replace('/[^A-Za-z0-9_\-]/', '_', str_replace(' ', '_', $fileName));
|
||||
}
|
||||
|
||||
// Hilfsfunktion, um den Dateinamen zu generieren
|
||||
function generateFileName($date, $name, $extension) {
|
||||
return $date . '-' . $name . '.' . $extension;
|
||||
}
|
||||
|
||||
/** PDF -> Text per pdftotext (Poppler) */
|
||||
function pdfToText($pdfFullPath) {
|
||||
$bin = trim(shell_exec('command -v pdftotext'));
|
||||
@@ -186,23 +181,18 @@ if (
|
||||
exit();
|
||||
}
|
||||
|
||||
// Neuen Aushang als PDF hochladen und in PNG-Seiten umwandeln
|
||||
// Neuen Aushang hochladen und direkt in PNG-Seiten umwandeln
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['fileToUpload'])) {
|
||||
$date = (string) ($_POST['date'] ?? '');
|
||||
$dateObject = DateTimeImmutable::createFromFormat('!Y-m-d', $date);
|
||||
|
||||
if (
|
||||
$dateObject === false ||
|
||||
$dateObject->format('Y-m-d') !== $date
|
||||
) {
|
||||
if ($dateObject === false || $dateObject->format('Y-m-d') !== $date) {
|
||||
$_SESSION['meldung_dateien'] = 'Ungültiges Verfallsdatum.';
|
||||
header('Location: insert.php#dateien');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Bereinige den eingegebenen Namen.
|
||||
$name = sanitizeFileName((string) ($_POST['name'] ?? ''));
|
||||
$name = trim($name, '_-');
|
||||
$name = trim(sanitizeFileName((string) ($_POST['name'] ?? '')), '_-');
|
||||
|
||||
if ($name === '') {
|
||||
$_SESSION['meldung_dateien'] = 'Bitte einen gültigen Titel eingeben.';
|
||||
@@ -210,68 +200,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['fileToUpload'])) {
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($_FILES['fileToUpload']['error'] !== UPLOAD_ERR_OK) {
|
||||
$_SESSION['meldung_dateien'] = 'Fehler beim Upload der PDF-Datei.';
|
||||
header('Location: insert.php#dateien');
|
||||
exit();
|
||||
}
|
||||
|
||||
$fileExtension = strtolower(
|
||||
pathinfo($_FILES['fileToUpload']['name'], PATHINFO_EXTENSION)
|
||||
);
|
||||
|
||||
if ($fileExtension !== 'pdf') {
|
||||
$_SESSION['meldung_dateien'] = 'Nur PDF-Dateien sind erlaubt.';
|
||||
header('Location: insert.php#dateien');
|
||||
exit();
|
||||
}
|
||||
|
||||
$directoryPath =
|
||||
'/var/www/html/intranet_repo/projects/contentpanel/insertion/news/';
|
||||
$imageOutputDirectory = $directoryPath . 'png/';
|
||||
|
||||
if (
|
||||
!is_dir($imageOutputDirectory) &&
|
||||
!mkdir($imageOutputDirectory, 0755, true) &&
|
||||
!is_dir($imageOutputDirectory)
|
||||
$_FILES['fileToUpload']['error'] !== UPLOAD_ERR_OK ||
|
||||
strtolower(pathinfo($_FILES['fileToUpload']['name'], PATHINFO_EXTENSION)) !== 'pdf'
|
||||
) {
|
||||
$_SESSION['meldung_dateien'] =
|
||||
'Das Aushang-Verzeichnis konnte nicht angelegt werden.';
|
||||
$_SESSION['meldung_dateien'] = 'Bitte eine gültige PDF-Datei hochladen.';
|
||||
header('Location: insert.php#dateien');
|
||||
exit();
|
||||
}
|
||||
|
||||
$fileName = generateFileName($date, $name, 'pdf');
|
||||
$targetFilePath = $directoryPath . $fileName;
|
||||
$baseFileName = pathinfo($fileName, PATHINFO_FILENAME);
|
||||
$baseFileName = $date . '-' . $name;
|
||||
|
||||
if (!isValidNewsFileBase($baseFileName)) {
|
||||
$_SESSION['meldung_dateien'] = 'Ungültiger Dateiname.';
|
||||
header('Location: insert.php#dateien');
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!move_uploaded_file(
|
||||
$_FILES['fileToUpload']['tmp_name'],
|
||||
$targetFilePath
|
||||
)) {
|
||||
$_SESSION['meldung_dateien'] = 'Fehler beim Speichern der PDF-Datei.';
|
||||
if (!is_dir(NEWS_PNG_DIRECTORY) && !mkdir(NEWS_PNG_DIRECTORY, 0755, true)) {
|
||||
$_SESSION['meldung_dateien'] = 'Das Aushang-Verzeichnis konnte nicht angelegt werden.';
|
||||
header('Location: insert.php#dateien');
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!convertPDFtoPNG(
|
||||
$targetFilePath,
|
||||
$imageOutputDirectory,
|
||||
$_FILES['fileToUpload']['tmp_name'],
|
||||
NEWS_PNG_DIRECTORY,
|
||||
$baseFileName
|
||||
)) {
|
||||
// Fehlgeschlagenen Upload vollständig aufräumen.
|
||||
deleteNewsFiles($baseFileName);
|
||||
|
||||
$_SESSION['meldung_dateien'] =
|
||||
'Fehler bei der Umwandlung der PDF. '
|
||||
. 'Die unvollständigen Dateien wurden entfernt.';
|
||||
|
||||
$_SESSION['meldung_dateien'] = 'Fehler bei der Umwandlung der PDF.';
|
||||
header('Location: insert.php#dateien');
|
||||
exit();
|
||||
}
|
||||
@@ -281,6 +233,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['fileToUpload'])) {
|
||||
exit();
|
||||
}
|
||||
|
||||
|
||||
// Löschen eines Gottesdienstes
|
||||
if (isset($_POST['delete_id'])) {
|
||||
$delete_id = $_POST['delete_id'];
|
||||
@@ -898,52 +851,30 @@ $pdo = null;
|
||||
</div>
|
||||
<div class="sb-aushang-grid">
|
||||
<?php
|
||||
$pngDirectoryPath = '/var/www/html/intranet_repo/projects/contentpanel/insertion/news/png/';
|
||||
if (is_dir($pngDirectoryPath)) {
|
||||
$pngFiles = scandir($pngDirectoryPath);
|
||||
$aushangData = [];
|
||||
$aushangFiles = glob(NEWS_PNG_DIRECTORY . '/*_page_1.png') ?: [];
|
||||
|
||||
foreach ($pngFiles as $file) {
|
||||
if (strpos($file, '_page_1.png') !== false) {
|
||||
$baseFileName = substr($file, 0, strpos($file, '_page_1.png'));
|
||||
$fileParts = explode('-', $baseFileName);
|
||||
$expiryDate = implode('-', array_slice($fileParts, 0, 3));
|
||||
$title = implode('-', array_slice($fileParts, 3));
|
||||
$date = DateTime::createFromFormat('Y-m-d', $expiryDate);
|
||||
$formattedExpiryDate = $date !== false ? $date->format('d.m.Y') : $expiryDate;
|
||||
|
||||
$numPages = 0;
|
||||
foreach ($pngFiles as $png) {
|
||||
if (strpos($png, $baseFileName . '_page_') !== false) $numPages++;
|
||||
}
|
||||
|
||||
$aushangData[] = [
|
||||
'fileBase' => $baseFileName,
|
||||
'formattedExpiry' => $formattedExpiryDate,
|
||||
'title' => $title,
|
||||
'numPages' => $numPages,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($aushangData)) {
|
||||
if (!$aushangFiles) {
|
||||
echo '<p style="color:var(--gray-500);font-size:var(--font-size-sm);grid-column:1/-1;text-align:center;padding:var(--space-lg) 0;">Keine Aushänge vorhanden.</p>';
|
||||
} else {
|
||||
foreach ($aushangData as $aushang) {
|
||||
foreach ($aushangFiles as $file) {
|
||||
$baseFileName = basename($file, '_page_1.png');
|
||||
$expiryDate = substr($baseFileName, 0, 10);
|
||||
$title = substr($baseFileName, 11);
|
||||
$numPages = count(glob(NEWS_PNG_DIRECTORY . '/' . $baseFileName . '_page_*.png') ?: []);
|
||||
$formattedExpiryDate = DateTime::createFromFormat('!Y-m-d', $expiryDate)?->format('d.m.Y') ?? $expiryDate;
|
||||
$safeBase = htmlspecialchars($baseFileName, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
echo '<div class="sb-aushang-card">';
|
||||
echo '<img src="/intranet_repo/projects/contentpanel/insertion/news/png/' . $aushang['fileBase'] . '_page_1.png" alt="Aushang">';
|
||||
echo '<div class="sb-aushang-title">' . htmlspecialchars($aushang['title']) . '</div>';
|
||||
echo '<div class="sb-aushang-meta">' . $aushang['numPages'] . ($aushang['numPages'] == 1 ? ' Seite' : ' Seiten') . ' · Verfällt ' . $aushang['formattedExpiry'] . '</div>';
|
||||
echo '<img src="/intranet_repo/projects/contentpanel/insertion/news/png/' . rawurlencode($baseFileName) . '_page_1.png" alt="Aushang">';
|
||||
echo '<div class="sb-aushang-title">' . htmlspecialchars($title) . '</div>';
|
||||
echo '<div class="sb-aushang-meta">' . $numPages . ($numPages === 1 ? ' Seite' : ' Seiten') . ' · Verfällt ' . $formattedExpiryDate . '</div>';
|
||||
echo '<form method="POST" action="" style="width:100%;text-align:center;">';
|
||||
echo '<input type="hidden" name="delete_file_base" value="' . $aushang['fileBase'] . '">';
|
||||
echo '<input type="hidden" name="delete_file_base" value="' . $safeBase . '">';
|
||||
echo '<button type="submit" class="sb-btn sb-btn-danger" onclick="return confirm(\'Aushang wirklich löschen?\')">Löschen</button>';
|
||||
echo '</form>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo '<p style="color:var(--gray-500);font-size:var(--font-size-sm);grid-column:1/-1;text-align:center;">Verzeichnis nicht gefunden.</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,133 +1,49 @@
|
||||
<?php
|
||||
|
||||
// Prüft, ob ein Basisname dem von der Anwendung erzeugten Format entspricht.
|
||||
const NEWS_PNG_DIRECTORY = __DIR__ . '/news/png';
|
||||
|
||||
function isValidNewsFileBase(string $fileBase): bool
|
||||
{
|
||||
return preg_match(
|
||||
'/^\d{4}-\d{2}-\d{2}-[A-Za-z0-9_-]+$/D',
|
||||
$fileBase
|
||||
) === 1;
|
||||
return preg_match('/^\d{4}-\d{2}-\d{2}-[A-Za-z0-9_-]+$/D', $fileBase) === 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Löscht eine News vollständig:
|
||||
*
|
||||
* - Original-PDF
|
||||
* - alle daraus erzeugten PNG-Seiten
|
||||
*/
|
||||
function deleteNewsFiles(string $fileBase): bool
|
||||
{
|
||||
if (!isValidNewsFileBase($fileBase)) {
|
||||
error_log('Ungültiger News-Dateiname beim Löschen: ' . $fileBase);
|
||||
return false;
|
||||
}
|
||||
|
||||
$newsDirectory = __DIR__ . '/news';
|
||||
$pngDirectory = $newsDirectory . '/png';
|
||||
|
||||
$success = true;
|
||||
|
||||
// Alle erzeugten PNG-Seiten löschen
|
||||
$pngFiles = glob(
|
||||
$pngDirectory . '/' . $fileBase . '_page_*.png'
|
||||
) ?: [];
|
||||
|
||||
foreach ($pngFiles as $pngFile) {
|
||||
if (is_file($pngFile) && !unlink($pngFile)) {
|
||||
error_log('News-PNG konnte nicht gelöscht werden: ' . $pngFile);
|
||||
foreach (glob(NEWS_PNG_DIRECTORY . '/' . $fileBase . '_page_*.png') ?: [] as $file) {
|
||||
if (!unlink($file)) {
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Original-PDF löschen
|
||||
$pdfFile = $newsDirectory . '/' . $fileBase . '.pdf';
|
||||
|
||||
if (is_file($pdfFile) && !unlink($pdfFile)) {
|
||||
error_log('News-PDF konnte nicht gelöscht werden: ' . $pdfFile);
|
||||
$success = false;
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Löscht alle abgelaufenen News.
|
||||
*
|
||||
* Wichtig:
|
||||
* Es werden sowohl vorhandene PDFs als auch page_1-PNGs betrachtet.
|
||||
* Dadurch werden auch verwaiste PDFs ohne PNG zuverlässig gefunden.
|
||||
*/
|
||||
function cleanupExpiredNews(): void
|
||||
{
|
||||
$newsDirectory = __DIR__ . '/news';
|
||||
$pngDirectory = $newsDirectory . '/png';
|
||||
$today = new DateTimeImmutable('today');
|
||||
|
||||
$fileBases = [];
|
||||
foreach (glob(NEWS_PNG_DIRECTORY . '/*_page_1.png') ?: [] as $file) {
|
||||
$fileBase = basename($file, '_page_1.png');
|
||||
|
||||
// PDFs erfassen
|
||||
foreach (glob($newsDirectory . '/*.pdf') ?: [] as $pdfFile) {
|
||||
$fileBase = pathinfo($pdfFile, PATHINFO_FILENAME);
|
||||
|
||||
if (isValidNewsFileBase($fileBase)) {
|
||||
$fileBases[$fileBase] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Zusätzlich PNGs erfassen.
|
||||
// Damit funktioniert die Bereinigung auch, falls die PDF bereits fehlt.
|
||||
foreach (glob($pngDirectory . '/*_page_1.png') ?: [] as $pngFile) {
|
||||
$fileName = basename($pngFile);
|
||||
$suffix = '_page_1.png';
|
||||
|
||||
if (!str_ends_with($fileName, $suffix)) {
|
||||
if (!isValidNewsFileBase($fileBase)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fileBase = substr($fileName, 0, -strlen($suffix));
|
||||
$dateString = substr($fileBase, 0, 10);
|
||||
$expiryDate = DateTimeImmutable::createFromFormat('!Y-m-d', $dateString);
|
||||
|
||||
if (isValidNewsFileBase($fileBase)) {
|
||||
$fileBases[$fileBase] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$now = new DateTimeImmutable();
|
||||
|
||||
foreach (array_keys($fileBases) as $fileBase) {
|
||||
// YYYY-MM-DD aus dem Dateinamen holen
|
||||
$expiryDateString = substr($fileBase, 0, 10);
|
||||
|
||||
$expiryDate = DateTimeImmutable::createFromFormat(
|
||||
'!Y-m-d',
|
||||
$expiryDateString
|
||||
);
|
||||
|
||||
// Zusätzlich prüfen, ob wirklich ein gültiges Datum entstanden ist
|
||||
if (
|
||||
$expiryDate === false ||
|
||||
$expiryDate->format('Y-m-d') !== $expiryDateString
|
||||
$expiryDate !== false &&
|
||||
$expiryDate->format('Y-m-d') === $dateString &&
|
||||
$today > $expiryDate
|
||||
) {
|
||||
error_log(
|
||||
'Ungültiges Ablaufdatum bei News-Datei: ' . $fileBase
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Aushang bleibt den gesamten angegebenen Tag sichtbar.
|
||||
* Beispiel Ablaufdatum 2026-09-05:
|
||||
* Löschung ab 2026-09-06 00:00 Uhr.
|
||||
*/
|
||||
$deleteFrom = $expiryDate->modify('+1 day');
|
||||
|
||||
if ($now >= $deleteFrom) {
|
||||
if (!deleteNewsFiles($fileBase)) {
|
||||
error_log(
|
||||
'Abgelaufene News konnte nicht vollständig gelöscht werden: '
|
||||
. $fileBase
|
||||
);
|
||||
}
|
||||
deleteNewsFiles($fileBase);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user