PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); } return $pdo; } function pnpaas_security_headers(): void { if (headers_sent()) return; header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); header('Referrer-Policy: no-referrer'); header('Permissions-Policy: camera=(), microphone=(), geolocation=()'); header('Cross-Origin-Opener-Policy: same-origin'); header("Content-Security-Policy: default-src 'self'; style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; script-src 'none'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; connect-src 'self'"); if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); } } function pnpaas_session_start(): void { pnpaas_security_headers(); if (session_status() === PHP_SESSION_ACTIVE) { return; } session_start([ 'use_strict_mode' => true, 'use_only_cookies' => true, 'cookie_httponly' => true, 'cookie_samesite' => 'Lax', 'cookie_secure' => !empty(pnpaas_config()['PNPAAS_FORCE_SECURE_COOKIES']) || (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), ]); } function pnpaas_csrf_token(): string { pnpaas_session_start(); if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return (string)$_SESSION['csrf_token']; } function pnpaas_valid_csrf(string $token): bool { pnpaas_session_start(); return $token !== '' && hash_equals((string)($_SESSION['csrf_token'] ?? ''), $token); } function pnpaas_registration_documents(): array { $config = pnpaas_config(); return [ 'terms' => ['title' => 'AGB', 'url' => trim((string)($config['PNPAAS_TERMS_URL'] ?? '')), 'version' => trim((string)($config['PNPAAS_TERMS_VERSION'] ?? ''))], 'privacy' => ['title' => 'Datenschutzerklärung', 'url' => trim((string)($config['PNPAAS_PRIVACY_URL'] ?? '')), 'version' => trim((string)($config['PNPAAS_PRIVACY_VERSION'] ?? ''))], 'withdrawal' => ['title' => 'Widerrufsbelehrung', 'url' => trim((string)($config['PNPAAS_WITHDRAWAL_URL'] ?? '')), 'version' => trim((string)($config['PNPAAS_WITHDRAWAL_VERSION'] ?? ''))], ]; } function pnpaas_app_url(): string { $config = pnpaas_config(); if (!empty($config['PNPAAS_APP_URL'])) { return rtrim($config['PNPAAS_APP_URL'], '/'); } $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; return $scheme . '://' . $host . '/pnpaas'; } function pnpaas_send_account_activation(string $recipient, string $token): bool { $config = pnpaas_config(); $from = $config['PNPAAS_MAIL_FROM'] ?? ('no-reply@' . ($_SERVER['HTTP_HOST'] ?? 'localhost')); $link = pnpaas_app_url() . '/activate-account.php?token=' . rawurlencode($token); $subject = 'PnPaaS – Konto aktivieren'; $body = "Hallo,\n\n" . "vielen Dank für Ihre Registrierung bei PnPaaS.\n\n" . "Aktivieren Sie Ihr Konto innerhalb der nächsten 24 Stunden über diesen Link:\n" . $link . "\n\n" . "Wenn Sie diese Registrierung nicht angefordert haben, können Sie diese E-Mail ignorieren.\n\n" . "Viele Grüße\nPnPaaS"; $headers = [ 'From: ' . $from, 'Content-Type: text/plain; charset=UTF-8', 'X-Mailer: PnPaaS', ]; return mail($recipient, $subject, $body, implode("\r\n", $headers)); } function pnpaas_send_password_reset(string $recipient, string $token): bool { $config = pnpaas_config(); $from = $config['PNPAAS_MAIL_FROM'] ?? ('no-reply@' . ($_SERVER['HTTP_HOST'] ?? 'localhost')); $link = pnpaas_app_url() . '/reset-password.php?token=' . rawurlencode($token); $subject = 'PnPaaS – Passwort zurücksetzen'; $body = "Hallo,\n\n" . "für Ihr PnPaaS-Konto wurde eine Passwort-Zurücksetzung angefordert.\n\n" . "Öffnen Sie innerhalb der nächsten Stunde diesen Link:\n" . $link . "\n\n" . "Wenn Sie die Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren.\n\n" . "Viele Grüße\nPnPaaS"; $headers = [ 'From: ' . $from, 'Content-Type: text/plain; charset=UTF-8', 'X-Mailer: PnPaaS', ]; return mail($recipient, $subject, $body, implode("\r\n", $headers)); } function pnpaas_portainer_request(string $path, string $method = 'GET', ?array $payload = null): array { $config = pnpaas_config(); foreach (['PORTAINER_URL', 'PORTAINER_API_KEY'] as $key) { if (empty($config[$key])) { throw new RuntimeException('Unvollständige Portainer-Konfiguration.'); } } $url = rtrim($config['PORTAINER_URL'], '/') . '/' . ltrim($path, '/'); $handle = curl_init($url); if ($handle === false) { throw new RuntimeException('Portainer-Anfrage konnte nicht initialisiert werden.'); } curl_setopt_array($handle, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 15, CURLOPT_HTTPHEADER => [ 'Accept: application/json', 'Content-Type: application/json', 'X-API-Key: ' . $config['PORTAINER_API_KEY'], ], CURLOPT_POSTFIELDS => $payload === null ? null : json_encode($payload, JSON_THROW_ON_ERROR), CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]); $body = curl_exec($handle); $error = curl_error($handle); $status = (int)curl_getinfo($handle, CURLINFO_HTTP_CODE); curl_close($handle); if ($body === false || $error !== '') { throw new RuntimeException('Portainer ist nicht erreichbar.'); } if ($status < 200 || ($status >= 300 && $status !== 304)) { $detail = trim(preg_replace('/\s+/', ' ', (string)$body) ?? ''); $detail = preg_replace('/("?(?:token|password|api[_-]?key|secret)"?\s*:\s*)"[^"]*"/i', '$1"[redacted]"', $detail) ?? $detail; $detail = substr($detail, 0, 600); throw new RuntimeException('Portainer HTTP ' . $status . ($detail !== '' ? ': ' . $detail : '.')); } if ($body === '' || $body === null) { return []; } $data = json_decode($body, true); if (!is_array($data)) { throw new RuntimeException('Portainer hat eine ungültige Antwort geliefert.'); } return $data; } function pnpaas_portainer_endpoint_id(): int { $config = pnpaas_config(); $id = (int)($config['PNPAAS_PORTAINER_ENDPOINT_ID'] ?? 3); if ($id < 1) { throw new RuntimeException('Ungültige Portainer-Umgebungs-ID.'); } return $id; }