refactor: support portable PHP hosting without Docker

This commit is contained in:
Marco0300
2026-09-01 19:22:48 +02:00
parent 9f5142b071
commit 5b2c64ff39
9 changed files with 128 additions and 159 deletions
+32 -49
View File
@@ -1,6 +1,26 @@
<?php
declare(strict_types=1);
function load_dotenv(string $path): void
{
if (!is_file($path) || !is_readable($path)) return;
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#')) continue;
$separator = strpos($line, '=');
if ($separator === false) continue;
$name = trim(substr($line, 0, $separator));
$value = trim(substr($line, $separator + 1));
if ($name === '' || getenv($name) !== false) continue;
if (strlen($value) >= 2 && (($value[0] === '"' && $value[-1] === '"') || ($value[0] === "'" && $value[-1] === "'"))) {
$value = substr($value, 1, -1);
}
putenv("{$name}={$value}");
}
}
load_dotenv(dirname(__DIR__) . '/.env');
function env_required(string $name): string
{
$value = getenv($name);
@@ -13,10 +33,7 @@ function env_required(string $name): string
function db(): PDO
{
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
if ($pdo instanceof PDO) return $pdo;
$dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',
env_required('DB_HOST'), getenv('DB_PORT') ?: '3306', env_required('DB_DATABASE'));
$pdo = new PDO($dsn, env_required('DB_USERNAME'), env_required('DB_PASSWORD'), [
@@ -29,9 +46,7 @@ function db(): PDO
function csrf_token(): string
{
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(32));
return $_SESSION['csrf'];
}
@@ -52,44 +67,25 @@ function verify_csrf(): void
function ensure_initial_administrator(): void
{
static $checked = false;
if ($checked) {
return;
}
if ($checked) return;
$checked = true;
$count = (int)db()->query('SELECT COUNT(*) FROM users')->fetchColumn();
if ($count !== 0) {
return;
}
if ((int)db()->query('SELECT COUNT(*) FROM users')->fetchColumn() !== 0) return;
$email = strtolower(trim(env_required('ADMIN_EMAIL')));
$password = env_required('ADMIN_PASSWORD');
if (strlen($password) < 12) {
throw new RuntimeException('ADMIN_PASSWORD must be at least 12 characters');
}
if (strlen($password) < 12) throw new RuntimeException('ADMIN_PASSWORD must be at least 12 characters');
$roleId = (int)db()->query("SELECT id FROM roles WHERE name = 'Administrator'")->fetchColumn();
if ($roleId < 1) {
throw new RuntimeException('Administrator role is missing from the database');
}
if ($roleId < 1) throw new RuntimeException('Administrator role is missing from the database');
$stmt = db()->prepare('INSERT INTO users (role_id, email, name, password_hash) VALUES (:role, :email, :name, :hash)');
$stmt->execute([
'role' => $roleId,
'email' => $email,
'name' => 'System Administrator',
'hash' => password_hash($password, PASSWORD_DEFAULT),
]);
$stmt->execute(['role' => $roleId, 'email' => $email, 'name' => 'System Administrator', 'hash' => password_hash($password, PASSWORD_DEFAULT)]);
}
function current_user(): ?array
{
ensure_initial_administrator();
static $user = false;
if ($user !== false) {
return $user;
}
if ($user !== false) return $user;
$id = $_SESSION['user_id'] ?? null;
if (!$id) {
return $user = null;
}
if (!$id) return $user = null;
$stmt = db()->prepare('SELECT u.*, r.name AS role_name FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = :id AND u.is_active = 1');
$stmt->execute(['id' => $id]);
return $user = ($stmt->fetch() ?: null);
@@ -98,10 +94,7 @@ function current_user(): ?array
function require_login(): array
{
$user = current_user();
if (!$user) {
header('Location: /?route=login');
exit;
}
if (!$user) { header('Location: /?route=login'); exit; }
return $user;
}
@@ -120,24 +113,14 @@ function can(string $permission): bool
function require_permission(string $permission): void
{
if (!can($permission)) {
http_response_code(403);
exit('Forbidden');
}
if (!can($permission)) { http_response_code(403); exit('Forbidden'); }
}
function audit(string $action, string $entityType, ?int $entityId = null, array $metadata = []): void
{
$user = current_user();
$stmt = db()->prepare('INSERT INTO audit_events (user_id, action, entity_type, entity_id, metadata, ip_address) VALUES (:user_id, :action, :entity_type, :entity_id, :metadata, :ip)');
$stmt->execute([
'user_id' => $user['id'] ?? null,
'action' => $action,
'entity_type' => $entityType,
'entity_id' => $entityId,
'metadata' => $metadata ? json_encode($metadata, JSON_THROW_ON_ERROR) : null,
'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
]);
$stmt->execute(['user_id' => $user['id'] ?? null, 'action' => $action, 'entity_type' => $entityType, 'entity_id' => $entityId, 'metadata' => $metadata ? json_encode($metadata, JSON_THROW_ON_ERROR) : null, 'ip' => $_SERVER['REMOTE_ADDR'] ?? null]);
}
function e(string $value): string