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.
|
||||
|
||||
### Paket vorbereiten
|
||||
|
||||
Wenn du das Projekt aus dem Repository nutzt und Composer lokal (oder per Docker) hast:
|
||||
### Upload-Paket erzeugen
|
||||
|
||||
```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)
|
||||
|
||||
@@ -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 –
|
||||
> 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
|
||||
|
||||
- **`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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "3486c41b4f3b5ad75dcd8b39db645306",
|
||||
"content-hash": "f1b5bd346bb675500290d6e5402ce9ab",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
@@ -1847,7 +1847,8 @@
|
||||
"php": ">=8.2",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-gd": "*"
|
||||
"ext-gd": "*",
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
|
||||
+11
-1
@@ -28,6 +28,7 @@ use App\Security\RateLimiter;
|
||||
use App\Security\Session;
|
||||
use App\Security\SetupToken;
|
||||
use App\Service\DomainChecker;
|
||||
use App\Service\SystemCheck;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
@@ -137,9 +138,18 @@ final class App
|
||||
$this->auth,
|
||||
new SetupToken($this->config->path($this->config->getString('DATA_DIR', 'data'))),
|
||||
$this->rateLimiter,
|
||||
new SystemCheck($this->config),
|
||||
);
|
||||
$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);
|
||||
$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);
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Repository\LinkRepository;
|
||||
use App\Repository\VisitRepository;
|
||||
use App\Security\Auth;
|
||||
use App\Security\Session;
|
||||
use App\Service\SystemCheck;
|
||||
use App\View;
|
||||
|
||||
final class DashboardController extends Controller
|
||||
@@ -23,6 +24,7 @@ final class DashboardController extends Controller
|
||||
private readonly VisitRepository $visits,
|
||||
Config $config,
|
||||
private readonly DomainRepository $domains,
|
||||
private readonly SystemCheck $systemCheck,
|
||||
) {
|
||||
parent::__construct($config, $view);
|
||||
}
|
||||
@@ -46,6 +48,9 @@ final class DashboardController extends Controller
|
||||
'topLinks' => $this->links->all('', 'clicks_desc', 5),
|
||||
'series' => $this->visits->clicksPerDayGlobal(30),
|
||||
'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',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Security\Csrf;
|
||||
use App\Security\RateLimiter;
|
||||
use App\Security\Session;
|
||||
use App\Security\SetupToken;
|
||||
use App\Service\SystemCheck;
|
||||
use App\View;
|
||||
|
||||
/**
|
||||
@@ -37,6 +38,7 @@ final class SetupController extends Controller
|
||||
private readonly Auth $auth,
|
||||
private readonly SetupToken $setupToken,
|
||||
private readonly RateLimiter $rateLimiter,
|
||||
private readonly SystemCheck $systemCheck,
|
||||
) {
|
||||
parent::__construct($config, $view);
|
||||
}
|
||||
@@ -126,6 +128,8 @@ final class SetupController extends Controller
|
||||
'errors' => $errors,
|
||||
'minLength' => self::MIN_PASSWORD_LENGTH,
|
||||
'tokenPath' => $this->setupToken->path(),
|
||||
'systemChecks' => $this->systemCheck->all(),
|
||||
'systemStatus' => $this->systemCheck->worstStatus(),
|
||||
], $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\Service\SystemCheck;
|
||||
use App\Support\ShortDomain;
|
||||
|
||||
$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>
|
||||
<h1>Übersicht</h1>
|
||||
|
||||
@@ -6,8 +6,22 @@
|
||||
* @var array<string,string> $errors
|
||||
* @var int $minLength
|
||||
* @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__head">
|
||||
@@ -83,3 +97,29 @@
|
||||
(<code>ADMIN_USER</code> und <code>ADMIN_PASSWORD_HASH</code>). Details stehen in der README.
|
||||
</p>
|
||||
</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