feat(setup): Systemcheck und Upload-Paket fuer klassisches Webhosting
Auf Shared Hosting gibt es keine Kommandozeile - ohne Anzeige laesst sich dort nicht feststellen, ob etwa gd fehlt, die PHP-Version zu alt ist oder data/ nicht beschreibbar ist. Der Setup-Assistent prueft das jetzt und listet es auf; im Dashboard meldet sich die Pruefung spaeter nur noch, wenn etwas nicht stimmt. Fehlende Pflichtbausteine sind Fehler, fehlende Kuer (intl, imagick, cURL) nur Hinweise mit Angabe, was dadurch entfaellt. build-release.sh erzeugt das Upload-Paket: Abhaengigkeiten ohne Entwicklungspakete, ohne Tests und Docker-Dateien. Dabei faellt eine 16 MB grosse Schriftart aus endroid/qr-code weg, die ausschliesslich fuer Beschriftungen unter dem QR-Code gebraucht wird - die erzeugt diese Anwendung nicht. Das Paket schrumpft damit von 18 MB auf 1 MB. Sollte spaeter doch eine Beschriftung dazukommen, erkennt das Skript das und behaelt die Datei. Die Variante ohne eigenen Webroot (alles liegt im Domainverzeichnis, die .htaccess im Stammverzeichnis leitet nach public/ um) war bisher gebaut, aber nie erprobt. Sie ist jetzt gegen eine Hoster-Simulation getestet: Ersteinrichtung, Kurzlinks, Weiterleitung, QR-Codes, Statistik und die Sperren fuer src/, vendor/, data/ und .env.
This commit is contained in:
@@ -82,15 +82,41 @@ Benutzername und Passwort.
|
|||||||
|
|
||||||
Es wird **keine Shell** benötigt. Der `vendor/`-Ordner ist im Release-Paket enthalten.
|
Es wird **keine Shell** benötigt. Der `vendor/`-Ordner ist im Release-Paket enthalten.
|
||||||
|
|
||||||
### Paket vorbereiten
|
### Upload-Paket erzeugen
|
||||||
|
|
||||||
Wenn du das Projekt aus dem Repository nutzt und Composer lokal (oder per Docker) hast:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
composer install --no-dev --optimize-autoloader
|
sh build-release.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Danach enthält der Ordner alles, was hochgeladen werden muss.
|
Ohne lokales PHP über das Anwendungs-Image (der schlanke `composer:2` hat kein `gd`
|
||||||
|
und bricht an der Plattformprüfung ab):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose build
|
||||||
|
docker run --rm -v "$PWD:/app" -w /app urlshort-qr-gen:latest sh build-release.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis ist `build/urlshort-qr-gen-release/` – rund **1 MB, 217 Dateien**. Das Skript
|
||||||
|
installiert die Abhängigkeiten ohne Entwicklungspakete und entfernt eine 16 MB große
|
||||||
|
Schriftart aus `endroid/qr-code`, die nur für Beschriftungen *unter* dem QR-Code
|
||||||
|
gebraucht wird (die diese Anwendung nicht erzeugt). Sollte sich das einmal ändern,
|
||||||
|
merkt das Skript es und behält die Datei.
|
||||||
|
|
||||||
|
### Was hochgeladen wird
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| `public/` | Webroot: `index.php`, `.htaccess`, `assets/` |
|
||||||
|
| `src/` | Anwendungscode |
|
||||||
|
| `templates/` | Seiten |
|
||||||
|
| `migrations/` | Datenbankschema |
|
||||||
|
| `vendor/` | Abhängigkeiten (ohne Entwicklungspakete) |
|
||||||
|
| `data/` | leer, muss **beschreibbar** sein – hier entstehen Datenbank und Logos |
|
||||||
|
| `.htaccess` | nur für Variante A2 nötig (siehe unten) |
|
||||||
|
| `.env` | selbst anlegen, Vorlage ist `.env.example` |
|
||||||
|
|
||||||
|
**Nicht** hochgeladen werden `tests/`, `docker/`, `Dockerfile`, `docker-compose.yml`,
|
||||||
|
`phpunit.xml`, `build-release.sh` und `.git/` – das Release-Paket enthält sie erst gar nicht.
|
||||||
|
|
||||||
### Variante A1 – Webroot kann auf `public/` gezeigt werden (empfohlen)
|
### Variante A1 – Webroot kann auf `public/` gezeigt werden (empfohlen)
|
||||||
|
|
||||||
@@ -131,6 +157,46 @@ Zusätzlich liegt in `data/` eine eigene `.htaccess` als zweite Absicherung.
|
|||||||
> liefern. Falls nicht, unterstützt der Hoster `AllowOverride` nicht ausreichend –
|
> liefern. Falls nicht, unterstützt der Hoster `AllowOverride` nicht ausreichend –
|
||||||
> dann bitte Variante A1 verwenden.
|
> dann bitte Variante A1 verwenden.
|
||||||
|
|
||||||
|
### Beispiel: Hetzner Webhosting (konsoleH)
|
||||||
|
|
||||||
|
Stellvertretend für Anbieter mit eigenem Panel – bei IONOS, Strato, All-Inkl oder Netcup
|
||||||
|
heißen die Menüpunkte anders, die Schritte sind dieselben.
|
||||||
|
|
||||||
|
1. **PHP-Version prüfen/setzen** – in konsoleH unter *Einstellungen → PHP-Version*
|
||||||
|
mindestens **PHP 8.2** wählen. Steht dort noch 7.x oder 8.0, läuft die Anwendung nicht.
|
||||||
|
2. **Verzeichnis wählen.** Der Webspace hat pro Domain ein Verzeichnis unterhalb von
|
||||||
|
`/public_html`. Lässt sich das Zielverzeichnis der Domain frei setzen, nimm
|
||||||
|
**Variante A1** und zeige damit auf `…/kurzlinks/public`. Geht das nicht, nimm
|
||||||
|
**Variante A2** und lade alles direkt in das Domainverzeichnis.
|
||||||
|
3. **Hochladen** per SFTP (Zugangsdaten stehen in konsoleH unter *Benutzer*). Den
|
||||||
|
**Inhalt** von `build/urlshort-qr-gen-release/` übertragen, nicht den Ordner selbst.
|
||||||
|
4. **Schreibrechte** für `data/` setzen – im FTP-Programm Rechte `755` (bei manchen
|
||||||
|
Tarifen `775`). Ohne das legt die Anwendung keine Datenbank an.
|
||||||
|
5. **`.env` anlegen:** `.env.example` in `.env` umbenennen und mindestens eintragen:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
APP_BASE_URL=https://kurz.deine-domain.de
|
||||||
|
SESSION_SECURE=true
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **HTTPS aktivieren** – Hetzner stellt kostenlose Let’s-Encrypt-Zertifikate aus
|
||||||
|
(*Einstellungen → SSL*). Vorher `APP_BASE_URL` auf `http://` lassen, sonst zeigen
|
||||||
|
Kurzlinks auf eine Adresse, die der Browser nicht öffnet.
|
||||||
|
7. **Domain aufrufen.** Der Setup-Assistent prüft zuerst die Systemvoraussetzungen und
|
||||||
|
zeigt an, falls etwas fehlt. Er verlangt außerdem das Einmal-Token aus
|
||||||
|
`data/setup-token.txt` – diese Datei per FTP öffnen, den 16-stelligen Wert eintragen.
|
||||||
|
Danach wird sie automatisch gelöscht.
|
||||||
|
|
||||||
|
**Datenbank:** SQLite genügt und ist voreingestellt – die bei Hetzner enthaltenen
|
||||||
|
MySQL-Datenbanken werden nicht gebraucht. Wer sie trotzdem nutzen will, trägt in der
|
||||||
|
`.env` `DB_DRIVER=mysql` samt Zugangsdaten aus konsoleH ein.
|
||||||
|
|
||||||
|
**Was bei Hetzner erfahrungsgemäß passt:** `mod_rewrite`, `gd`, `mbstring`, `fileinfo`
|
||||||
|
und `pdo_sqlite` sind vorhanden. `imagick` ist es meist nicht – dann erscheinen
|
||||||
|
SVG-Logos nur im SVG-Download, PNG-Logos funktionieren normal. Der Setup-Assistent
|
||||||
|
zeigt den tatsächlichen Stand deines Tarifs an; verlass dich darauf statt auf diese
|
||||||
|
Aufzählung.
|
||||||
|
|
||||||
### Wichtige Hinweise zu Shared Hosting
|
### Wichtige Hinweise zu Shared Hosting
|
||||||
|
|
||||||
- **`mod_rewrite` muss aktiv sein.** Ohne Rewrites funktionieren die Kurzlinks nicht.
|
- **`mod_rewrite` muss aktiv sein.** Ohne Rewrites funktionieren die Kurzlinks nicht.
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# Erzeugt das Upload-Paket fuer klassisches Webhosting.
|
||||||
|
#
|
||||||
|
# Ergebnis: build/urlshort-qr-gen-release/ und build/urlshort-qr-gen.zip
|
||||||
|
# Enthalten ist alles, was auf dem Server gebraucht wird - inklusive
|
||||||
|
# vendor/ ohne Entwicklungspakete. Nicht enthalten sind Tests, Docker,
|
||||||
|
# Git-Dateien und die eigene .env.
|
||||||
|
#
|
||||||
|
# Aufruf mit lokalem PHP und Composer (im Projektverzeichnis):
|
||||||
|
# sh build-release.sh
|
||||||
|
#
|
||||||
|
# Ohne lokales PHP ueber das Anwendungs-Image - dieses bringt PHP samt
|
||||||
|
# gd, intl und Composer mit (der schlanke composer:2 hat kein gd und
|
||||||
|
# bricht deshalb an der Plattformpruefung ab):
|
||||||
|
# docker compose build
|
||||||
|
# docker run --rm -v "$PWD:/app" -w /app urlshort-qr-gen:latest sh build-release.sh
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(pwd)"
|
||||||
|
BUILD="$ROOT/build"
|
||||||
|
TARGET="$BUILD/urlshort-qr-gen-release"
|
||||||
|
|
||||||
|
echo "==> Aufraeumen"
|
||||||
|
rm -rf "$BUILD"
|
||||||
|
mkdir -p "$TARGET"
|
||||||
|
|
||||||
|
echo "==> Dateien kopieren"
|
||||||
|
for item in public src templates migrations composer.json composer.lock .env.example README.md .htaccess; do
|
||||||
|
cp -R "$ROOT/$item" "$TARGET/"
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p "$TARGET/data/uploads"
|
||||||
|
cp "$ROOT/data/.htaccess" "$TARGET/data/.htaccess"
|
||||||
|
: > "$TARGET/data/.gitkeep"
|
||||||
|
|
||||||
|
echo "==> Abhaengigkeiten ohne Entwicklungspakete installieren"
|
||||||
|
cd "$TARGET"
|
||||||
|
composer install --no-dev --optimize-autoloader --no-interaction --no-progress --quiet
|
||||||
|
|
||||||
|
# composer.lock wird auf dem Server nicht gebraucht, composer.json auch
|
||||||
|
# nicht - beide sind ueber die .htaccess ohnehin gesperrt. Sie bleiben
|
||||||
|
# liegen, damit sich das Paket spaeter aktualisieren laesst.
|
||||||
|
|
||||||
|
# endroid bringt eine 16 MB grosse Schriftart mit. Gebraucht wird sie
|
||||||
|
# ausschliesslich fuer Beschriftungen UNTER dem QR-Code (Label) - diese
|
||||||
|
# Anwendung erzeugt keine, der QrRenderer uebergibt dort immer null.
|
||||||
|
# Ohne die Datei schrumpft der Upload von rund 18 MB auf gut 2 MB.
|
||||||
|
# Sollte spaeter doch eine Beschriftung dazukommen, faellt das hier auf
|
||||||
|
# und die Schriftart bleibt im Paket.
|
||||||
|
echo "==> Ungenutzte Schriftart pruefen"
|
||||||
|
if grep -rqE 'Endroid\\\\QrCode\\\\Label|new Label\(' "$TARGET/src" 2>/dev/null; then
|
||||||
|
echo " Label wird verwendet - Schriftart bleibt erhalten."
|
||||||
|
else
|
||||||
|
rm -f "$TARGET/vendor/endroid/qr-code/assets/noto_sans.otf"
|
||||||
|
echo " noto_sans.otf entfernt (wird nicht verwendet)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Archiv erzeugen"
|
||||||
|
cd "$BUILD"
|
||||||
|
if command -v zip >/dev/null 2>&1; then
|
||||||
|
zip -rq urlshort-qr-gen.zip urlshort-qr-gen-release
|
||||||
|
echo " build/urlshort-qr-gen.zip"
|
||||||
|
else
|
||||||
|
tar -czf urlshort-qr-gen.tar.gz urlshort-qr-gen-release
|
||||||
|
echo " build/urlshort-qr-gen.tar.gz (kein zip vorhanden)"
|
||||||
|
echo " Unter Windows laesst sich stattdessen ein ZIP erzeugen mit:"
|
||||||
|
echo " Compress-Archive build/urlshort-qr-gen-release build/urlshort-qr-gen.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Fertig"
|
||||||
|
echo " Verzeichnis: build/urlshort-qr-gen-release"
|
||||||
|
echo " Dateien: $(find "$TARGET" -type f | wc -l)"
|
||||||
|
echo " Groesse: $(du -sh "$TARGET" | cut -f1)"
|
||||||
Generated
+3
-2
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "3486c41b4f3b5ad75dcd8b39db645306",
|
"content-hash": "f1b5bd346bb675500290d6e5402ce9ab",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@@ -1847,7 +1847,8 @@
|
|||||||
"php": ">=8.2",
|
"php": ">=8.2",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"ext-gd": "*"
|
"ext-gd": "*",
|
||||||
|
"ext-mbstring": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.9.0"
|
"plugin-api-version": "2.9.0"
|
||||||
|
|||||||
+11
-1
@@ -28,6 +28,7 @@ use App\Security\RateLimiter;
|
|||||||
use App\Security\Session;
|
use App\Security\Session;
|
||||||
use App\Security\SetupToken;
|
use App\Security\SetupToken;
|
||||||
use App\Service\DomainChecker;
|
use App\Service\DomainChecker;
|
||||||
|
use App\Service\SystemCheck;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,9 +138,18 @@ final class App
|
|||||||
$this->auth,
|
$this->auth,
|
||||||
new SetupToken($this->config->path($this->config->getString('DATA_DIR', 'data'))),
|
new SetupToken($this->config->path($this->config->getString('DATA_DIR', 'data'))),
|
||||||
$this->rateLimiter,
|
$this->rateLimiter,
|
||||||
|
new SystemCheck($this->config),
|
||||||
);
|
);
|
||||||
$login = new AuthController($this->config, $this->view, $this->session, $this->csrf, $this->auth, $this->rateLimiter);
|
$login = new AuthController($this->config, $this->view, $this->session, $this->csrf, $this->auth, $this->rateLimiter);
|
||||||
$dashboard = new DashboardController($this->view, $this->auth, $this->links, $this->visits, $this->config, $this->domains);
|
$dashboard = new DashboardController(
|
||||||
|
$this->view,
|
||||||
|
$this->auth,
|
||||||
|
$this->links,
|
||||||
|
$this->visits,
|
||||||
|
$this->config,
|
||||||
|
$this->domains,
|
||||||
|
new SystemCheck($this->config),
|
||||||
|
);
|
||||||
$linkController = new LinkController($this->config, $this->view, $this->session, $this->csrf, $this->auth, $this->links, $this->designs, $this->logos, $this->domains);
|
$linkController = new LinkController($this->config, $this->view, $this->session, $this->csrf, $this->auth, $this->links, $this->designs, $this->logos, $this->domains);
|
||||||
$stats = new StatsController($this->config, $this->view, $this->auth, $this->links, $this->visits);
|
$stats = new StatsController($this->config, $this->view, $this->auth, $this->links, $this->visits);
|
||||||
$qrController = new QrController($this->config, $this->view, $this->session, $this->csrf, $this->auth, $this->links, $this->designs, $this->logos, $this->qr);
|
$qrController = new QrController($this->config, $this->view, $this->session, $this->csrf, $this->auth, $this->links, $this->designs, $this->logos, $this->qr);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use App\Repository\LinkRepository;
|
|||||||
use App\Repository\VisitRepository;
|
use App\Repository\VisitRepository;
|
||||||
use App\Security\Auth;
|
use App\Security\Auth;
|
||||||
use App\Security\Session;
|
use App\Security\Session;
|
||||||
|
use App\Service\SystemCheck;
|
||||||
use App\View;
|
use App\View;
|
||||||
|
|
||||||
final class DashboardController extends Controller
|
final class DashboardController extends Controller
|
||||||
@@ -23,6 +24,7 @@ final class DashboardController extends Controller
|
|||||||
private readonly VisitRepository $visits,
|
private readonly VisitRepository $visits,
|
||||||
Config $config,
|
Config $config,
|
||||||
private readonly DomainRepository $domains,
|
private readonly DomainRepository $domains,
|
||||||
|
private readonly SystemCheck $systemCheck,
|
||||||
) {
|
) {
|
||||||
parent::__construct($config, $view);
|
parent::__construct($config, $view);
|
||||||
}
|
}
|
||||||
@@ -46,6 +48,9 @@ final class DashboardController extends Controller
|
|||||||
'topLinks' => $this->links->all('', 'clicks_desc', 5),
|
'topLinks' => $this->links->all('', 'clicks_desc', 5),
|
||||||
'series' => $this->visits->clicksPerDayGlobal(30),
|
'series' => $this->visits->clicksPerDayGlobal(30),
|
||||||
'domainCount' => $this->domains->count(),
|
'domainCount' => $this->domains->count(),
|
||||||
|
// Nur melden, wenn etwas nicht stimmt - im Normalfall
|
||||||
|
// soll die Uebersicht nicht mit Technik zugestellt werden.
|
||||||
|
'systemProblems' => $this->systemCheck->problems(),
|
||||||
'pageTitle' => 'Übersicht',
|
'pageTitle' => 'Übersicht',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use App\Security\Csrf;
|
|||||||
use App\Security\RateLimiter;
|
use App\Security\RateLimiter;
|
||||||
use App\Security\Session;
|
use App\Security\Session;
|
||||||
use App\Security\SetupToken;
|
use App\Security\SetupToken;
|
||||||
|
use App\Service\SystemCheck;
|
||||||
use App\View;
|
use App\View;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,6 +38,7 @@ final class SetupController extends Controller
|
|||||||
private readonly Auth $auth,
|
private readonly Auth $auth,
|
||||||
private readonly SetupToken $setupToken,
|
private readonly SetupToken $setupToken,
|
||||||
private readonly RateLimiter $rateLimiter,
|
private readonly RateLimiter $rateLimiter,
|
||||||
|
private readonly SystemCheck $systemCheck,
|
||||||
) {
|
) {
|
||||||
parent::__construct($config, $view);
|
parent::__construct($config, $view);
|
||||||
}
|
}
|
||||||
@@ -126,6 +128,8 @@ final class SetupController extends Controller
|
|||||||
'errors' => $errors,
|
'errors' => $errors,
|
||||||
'minLength' => self::MIN_PASSWORD_LENGTH,
|
'minLength' => self::MIN_PASSWORD_LENGTH,
|
||||||
'tokenPath' => $this->setupToken->path(),
|
'tokenPath' => $this->setupToken->path(),
|
||||||
|
'systemChecks' => $this->systemCheck->all(),
|
||||||
|
'systemStatus' => $this->systemCheck->worstStatus(),
|
||||||
], $status, 'layout_plain');
|
], $status, 'layout_plain');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prueft, ob der Server alles mitbringt.
|
||||||
|
*
|
||||||
|
* Auf klassischem Webhosting gibt es keine Kommandozeile - ohne diese
|
||||||
|
* Anzeige liesse sich nicht feststellen, ob etwa gd fehlt oder das
|
||||||
|
* Datenverzeichnis nicht beschreibbar ist. Die Pruefung laeuft deshalb
|
||||||
|
* im Setup-Assistenten und meldet sich spaeter im Dashboard nur dann,
|
||||||
|
* wenn etwas nicht stimmt.
|
||||||
|
*/
|
||||||
|
final class SystemCheck
|
||||||
|
{
|
||||||
|
public const OK = 'ok';
|
||||||
|
public const WARNING = 'warning';
|
||||||
|
public const ERROR = 'error';
|
||||||
|
|
||||||
|
public const MIN_PHP = '8.2.0';
|
||||||
|
|
||||||
|
public function __construct(private readonly Config $config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{label:string,status:string,text:string,required:bool}>
|
||||||
|
*/
|
||||||
|
public function all(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
$this->php(),
|
||||||
|
$this->database(),
|
||||||
|
$this->extension('gd', 'Bilderzeugung (gd)', true, 'Ohne gd lassen sich keine PNG-QR-Codes erzeugen; SVG funktioniert weiterhin.'),
|
||||||
|
$this->extension('mbstring', 'Zeichenketten (mbstring)', true, 'Wird für Umlaute in Slugs, URLs und Domains gebraucht.'),
|
||||||
|
$this->extension('fileinfo', 'Dateityperkennung (fileinfo)', true, 'Wird beim Prüfen hochgeladener Logos gebraucht.'),
|
||||||
|
$this->dataDirectory(),
|
||||||
|
$this->uploadLimit(),
|
||||||
|
$this->extension('intl', 'Umlaut-Domains (intl)', false, 'Ohne intl greift ein eingebauter Punycode-Rückfallweg – Umlaut-Domains funktionieren trotzdem.'),
|
||||||
|
$this->extension('imagick', 'SVG-Logos in PNG (imagick)', false, 'Ohne imagick erscheinen SVG-Logos nur im SVG-Download. PNG-Logos sind davon nicht betroffen.'),
|
||||||
|
$this->outgoingRequests(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fasst zusammen, ob etwas zu tun ist.
|
||||||
|
*/
|
||||||
|
public function worstStatus(): string
|
||||||
|
{
|
||||||
|
$status = self::OK;
|
||||||
|
|
||||||
|
foreach ($this->all() as $check) {
|
||||||
|
if ($check['status'] === self::ERROR) {
|
||||||
|
return self::ERROR;
|
||||||
|
}
|
||||||
|
if ($check['status'] === self::WARNING) {
|
||||||
|
$status = self::WARNING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{label:string,status:string,text:string,required:bool}>
|
||||||
|
*/
|
||||||
|
public function problems(): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(
|
||||||
|
$this->all(),
|
||||||
|
static fn (array $check): bool => $check['status'] !== self::OK,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{label:string,status:string,text:string,required:bool}
|
||||||
|
*/
|
||||||
|
private function php(): array
|
||||||
|
{
|
||||||
|
$ok = version_compare(PHP_VERSION, self::MIN_PHP, '>=');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => 'PHP-Version',
|
||||||
|
'status' => $ok ? self::OK : self::ERROR,
|
||||||
|
'text' => $ok
|
||||||
|
? PHP_VERSION
|
||||||
|
: sprintf('%s – benötigt wird mindestens %s. Im Hosting-Panel umstellen.', PHP_VERSION, self::MIN_PHP),
|
||||||
|
'required' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{label:string,status:string,text:string,required:bool}
|
||||||
|
*/
|
||||||
|
private function database(): array
|
||||||
|
{
|
||||||
|
$driver = strtolower($this->config->getString('DB_DRIVER', 'sqlite'));
|
||||||
|
$needed = $driver === 'mysql' || $driver === 'mariadb' ? 'pdo_mysql' : 'pdo_sqlite';
|
||||||
|
$ok = extension_loaded('pdo') && extension_loaded($needed);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => 'Datenbank',
|
||||||
|
'status' => $ok ? self::OK : self::ERROR,
|
||||||
|
'text' => $ok
|
||||||
|
? sprintf('%s über %s', $driver === 'sqlite' ? 'SQLite' : 'MySQL/MariaDB', $needed)
|
||||||
|
: sprintf('Die PHP-Erweiterung %s fehlt.', $needed),
|
||||||
|
'required' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{label:string,status:string,text:string,required:bool}
|
||||||
|
*/
|
||||||
|
private function extension(string $name, string $label, bool $required, string $hint): array
|
||||||
|
{
|
||||||
|
$loaded = extension_loaded($name);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => $label,
|
||||||
|
'status' => $loaded ? self::OK : ($required ? self::ERROR : self::WARNING),
|
||||||
|
'text' => $loaded ? 'vorhanden' : ('nicht vorhanden – ' . $hint),
|
||||||
|
'required' => $required,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{label:string,status:string,text:string,required:bool}
|
||||||
|
*/
|
||||||
|
private function dataDirectory(): array
|
||||||
|
{
|
||||||
|
$path = $this->config->path($this->config->getString('DATA_DIR', 'data'));
|
||||||
|
$writable = is_dir($path) && is_writable($path);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => 'Datenverzeichnis',
|
||||||
|
'status' => $writable ? self::OK : self::ERROR,
|
||||||
|
'text' => $writable
|
||||||
|
? $path . ' ist beschreibbar'
|
||||||
|
: sprintf('%s ist nicht beschreibbar. Rechte auf 755 bzw. 775 setzen.', $path),
|
||||||
|
'required' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{label:string,status:string,text:string,required:bool}
|
||||||
|
*/
|
||||||
|
private function uploadLimit(): array
|
||||||
|
{
|
||||||
|
$wanted = $this->config->getInt('UPLOAD_MAX_BYTES', 2097152);
|
||||||
|
$limit = min(
|
||||||
|
self::toBytes((string) ini_get('upload_max_filesize')),
|
||||||
|
self::toBytes((string) ini_get('post_max_size')),
|
||||||
|
);
|
||||||
|
|
||||||
|
$ok = $limit <= 0 || $limit >= $wanted;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => 'Upload-Größe',
|
||||||
|
'status' => $ok ? self::OK : self::WARNING,
|
||||||
|
'text' => $ok
|
||||||
|
? sprintf('bis %s je Logo', self::format($limit > 0 ? $limit : $wanted))
|
||||||
|
: sprintf(
|
||||||
|
'Der Server erlaubt nur %s, eingestellt sind %s. Logos werden entsprechend früher abgelehnt.',
|
||||||
|
self::format($limit),
|
||||||
|
self::format($wanted),
|
||||||
|
),
|
||||||
|
'required' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{label:string,status:string,text:string,required:bool}
|
||||||
|
*/
|
||||||
|
private function outgoingRequests(): array
|
||||||
|
{
|
||||||
|
$curl = function_exists('curl_init');
|
||||||
|
$fopen = (bool) ini_get('allow_url_fopen');
|
||||||
|
$ok = $curl || $fopen;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => 'Domain-Prüfung',
|
||||||
|
'status' => $ok ? self::OK : self::WARNING,
|
||||||
|
'text' => $ok
|
||||||
|
? ($curl ? 'über cURL möglich' : 'über allow_url_fopen möglich')
|
||||||
|
: 'Weder cURL noch allow_url_fopen verfügbar – die Domain-Prüfung kann die Zuordnung nicht testen. '
|
||||||
|
. 'Alles Übrige funktioniert davon unberührt.',
|
||||||
|
'required' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function toBytes(string $value): int
|
||||||
|
{
|
||||||
|
$value = trim($value);
|
||||||
|
if ($value === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$unit = strtolower(substr($value, -1));
|
||||||
|
$number = (int) $value;
|
||||||
|
|
||||||
|
return match ($unit) {
|
||||||
|
'g' => $number * 1024 * 1024 * 1024,
|
||||||
|
'm' => $number * 1024 * 1024,
|
||||||
|
'k' => $number * 1024,
|
||||||
|
default => $number,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function format(int $bytes): string
|
||||||
|
{
|
||||||
|
if ($bytes >= 1024 * 1024) {
|
||||||
|
return round($bytes / 1024 / 1024, 1) . ' MB';
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(1, intdiv($bytes, 1024)) . ' KB';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,10 +10,42 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use App\Repository\LinkRepository;
|
use App\Repository\LinkRepository;
|
||||||
|
use App\Service\SystemCheck;
|
||||||
use App\Support\ShortDomain;
|
use App\Support\ShortDomain;
|
||||||
|
|
||||||
$fallbackBase = rtrim($config->baseUrl(), '/');
|
$fallbackBase = rtrim($config->baseUrl(), '/');
|
||||||
?>
|
?>
|
||||||
|
<?php if (($systemProblems ?? []) !== []):
|
||||||
|
$hatFehler = false;
|
||||||
|
foreach ($systemProblems as $problem) {
|
||||||
|
if ($problem['status'] === SystemCheck::ERROR) {
|
||||||
|
$hatFehler = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<section class="card check-result check-result--<?= $hatFehler ? 'error' : 'warn' ?>">
|
||||||
|
<div class="check-result__head">
|
||||||
|
<span class="check-result__icon" aria-hidden="true"><?= $hatFehler ? '×' : '!' ?></span>
|
||||||
|
<div>
|
||||||
|
<h2>Hinweise zum Server</h2>
|
||||||
|
<p class="check-result__summary">
|
||||||
|
<?= $hatFehler
|
||||||
|
? 'Es fehlt etwas, das die Anwendung braucht.'
|
||||||
|
: 'Alles läuft – einzelne Zusatzfunktionen stehen aber nicht zur Verfügung.' ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="check-list">
|
||||||
|
<?php foreach ($systemProblems as $problem): ?>
|
||||||
|
<li class="check-list__item check-list__item--<?= $problem['status'] === SystemCheck::ERROR ? 'error' : 'warn' ?>">
|
||||||
|
<span class="check-list__icon" aria-hidden="true"><?= $problem['status'] === SystemCheck::ERROR ? '×' : '!' ?></span>
|
||||||
|
<span class="check-list__label"><?= $e($problem['label']) ?></span>
|
||||||
|
<span class="check-list__text"><?= $e($problem['text']) ?></span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>Übersicht</h1>
|
<h1>Übersicht</h1>
|
||||||
|
|||||||
@@ -6,8 +6,22 @@
|
|||||||
* @var array<string,string> $errors
|
* @var array<string,string> $errors
|
||||||
* @var int $minLength
|
* @var int $minLength
|
||||||
* @var string $tokenPath
|
* @var string $tokenPath
|
||||||
|
* @var list<array{label:string,status:string,text:string,required:bool}> $systemChecks
|
||||||
|
* @var string $systemStatus
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use App\Service\SystemCheck;
|
||||||
|
|
||||||
|
$symbol = static fn (string $status): string => match ($status) {
|
||||||
|
SystemCheck::OK => '✓',
|
||||||
|
SystemCheck::WARNING => '!',
|
||||||
|
default => '×',
|
||||||
|
};
|
||||||
|
$cssClass = static fn (string $status): string => match ($status) {
|
||||||
|
SystemCheck::OK => 'ok',
|
||||||
|
SystemCheck::WARNING => 'warn',
|
||||||
|
default => 'error',
|
||||||
|
};
|
||||||
?>
|
?>
|
||||||
<div class="card card--narrow">
|
<div class="card card--narrow">
|
||||||
<div class="card__head">
|
<div class="card__head">
|
||||||
@@ -83,3 +97,29 @@
|
|||||||
(<code>ADMIN_USER</code> und <code>ADMIN_PASSWORD_HASH</code>). Details stehen in der README.
|
(<code>ADMIN_USER</code> und <code>ADMIN_PASSWORD_HASH</code>). Details stehen in der README.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="card check-result check-result--<?= $e($cssClass($systemStatus)) ?>">
|
||||||
|
<details <?= $systemStatus === SystemCheck::OK ? '' : 'open' ?>>
|
||||||
|
<summary>
|
||||||
|
Systemvoraussetzungen –
|
||||||
|
<?= $systemStatus === SystemCheck::OK
|
||||||
|
? 'alles vorhanden'
|
||||||
|
: ($systemStatus === SystemCheck::WARNING ? 'mit Einschränkungen nutzbar' : 'es fehlt etwas') ?>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<ul class="check-list">
|
||||||
|
<?php foreach ($systemChecks as $check): ?>
|
||||||
|
<li class="check-list__item check-list__item--<?= $e($cssClass($check['status'])) ?>">
|
||||||
|
<span class="check-list__icon" aria-hidden="true"><?= $e($symbol($check['status'])) ?></span>
|
||||||
|
<span class="check-list__label"><?= $e($check['label']) ?></span>
|
||||||
|
<span class="check-list__text"><?= $e($check['text']) ?></span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="muted small">
|
||||||
|
Dass diese Seite überhaupt erscheint, belegt bereits, dass <code>mod_rewrite</code> arbeitet
|
||||||
|
und die <code>.htaccess</code> ausgewertet wird.
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user