diff --git a/README.md b/README.md index e69de29..bd170f2 100644 --- a/README.md +++ b/README.md @@ -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 < **Debian 13:** Das Projekt funktioniert ebenfalls unter Debian. Dafür das Docker-Repository entsprechend auf `linux/debian` umstellen: +> + +--- + +### 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: + + + +--- + +## 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: + + + +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 +``` \ No newline at end of file diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..744a116 --- /dev/null +++ b/docker-compose.local.yml @@ -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: diff --git a/web/intranet_repo/projects/contentpanel/insertion/insert.php b/web/intranet_repo/projects/contentpanel/insertion/insert.php index 53c01d9..83866ab 100644 --- a/web/intranet_repo/projects/contentpanel/insertion/insert.php +++ b/web/intranet_repo/projects/contentpanel/insertion/insert.php @@ -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,51 +851,29 @@ $pdo = null;
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)) { - echo '

Keine Aushänge vorhanden.

'; - } else { - foreach ($aushangData as $aushang) { - echo '
'; - echo 'Aushang'; - echo '
' . htmlspecialchars($aushang['title']) . '
'; - echo '
' . $aushang['numPages'] . ($aushang['numPages'] == 1 ? ' Seite' : ' Seiten') . ' · Verfällt ' . $aushang['formattedExpiry'] . '
'; - echo '
'; - echo ''; - echo ''; - echo '
'; - echo '
'; - } - } + if (!$aushangFiles) { + echo '

Keine Aushänge vorhanden.

'; } else { - echo '

Verzeichnis nicht gefunden.

'; + 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 '
'; + echo 'Aushang'; + echo '
' . htmlspecialchars($title) . '
'; + echo '
' . $numPages . ($numPages === 1 ? ' Seite' : ' Seiten') . ' · Verfällt ' . $formattedExpiryDate . '
'; + echo '
'; + echo ''; + echo ''; + echo '
'; + echo '
'; + } } ?>
diff --git a/web/intranet_repo/projects/contentpanel/insertion/news_cleanup.php b/web/intranet_repo/projects/contentpanel/insertion/news_cleanup.php index d9b301f..dc7fe84 100644 --- a/web/intranet_repo/projects/contentpanel/insertion/news_cleanup.php +++ b/web/intranet_repo/projects/contentpanel/insertion/news_cleanup.php @@ -1,133 +1,49 @@ 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); } } -} \ No newline at end of file +}