diff --git a/config/config.php b/config/config.php new file mode 100644 index 0000000..c0a9546 --- /dev/null +++ b/config/config.php @@ -0,0 +1,39 @@ + 0, - 'path' => '/', - 'domain' => APP_COOKIE_DOMAIN ?: '', - 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), - 'httponly' => true, - 'samesite' => 'Lax', - ]); - - session_start(); - } -} - - -// ----------------------------------------------------------- -// 2) Persistente Client-ID (für Tracking über Besuche hinweg) -// ----------------------------------------------------------- -if (php_sapi_name() !== 'cli') { - $clientId = $_COOKIE[$clientCookieName] ?? null; - - // Erwartet wird: 64 Hex-Zeichen (32 Bytes) - if ( - !is_string($clientId) || - $clientId === '' || - !preg_match('/^[a-f0-9]{64}$/', $clientId) - ) { - // neue ID erzeugen - try { - $clientId = bin2hex(random_bytes(32)); // 32 bytes → 64 hex - } catch (Throwable $e) { - $clientId = bin2hex(openssl_random_pseudo_bytes(32)); +// 2) Composer Autoloader (falls vorhanden) +$composerAutoload = __DIR__ . '/../vendor/autoload.php'; +if (file_exists($composerAutoload)) { + require_once $composerAutoload; +} else { + // 2b) Fallback: minimaler Autoloader + spl_autoload_register(function (string $class): void { + $prefix = 'App\\'; + if (!str_starts_with($class, $prefix)) { + return; } - $cookieOpts = [ - 'expires' => time() + APP_CLIENT_COOKIE_LIFETIME, - 'path' => '/', - 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), - 'httponly' => false, // JS darf es lesen, wenn erwünscht - 'samesite' => 'Lax', - ]; + $rel = substr($class, strlen($prefix)); + $path = __DIR__ . '/../src/' . str_replace('\\', '/', $rel) . '.php'; - if (!empty(APP_COOKIE_DOMAIN)) { - $cookieOpts['domain'] = APP_COOKIE_DOMAIN; + if (file_exists($path)) { + require_once $path; } - - setcookie($clientCookieName, $clientId, $cookieOpts); - $_COOKIE[$clientCookieName] = $clientId; - } - - // global verfügbar machen (NEUER NAME!) - $GLOBALS['cookie_client_id'] = $clientId; + }); } +// 3) Global helper functions (tpl(), t(), asset_*()) +require_once __DIR__ . '/../src/helpers.php'; -// ----------------------------------------------------------- -// 3) Sprachlogik laden (bleibt sinnvoll zentral) -// ----------------------------------------------------------- -require_once __DIR__ . '/i18n.php'; +// 4) Initialize App (services) +$config = \App\Config::fromPhpConstants(__DIR__ . '/../config'); +\App\App::init($config); +// 5) Start session + create client-id cookie +$app = \App\App::get(); +$app->session()->start(); +$clientId = $app->session()->ensureClientId(); -// ----------------------------------------------------------- -// 4) Rest des Systems laden (DB, Funktionen, Hilfs-Libs) -// ----------------------------------------------------------- -require_once __DIR__ . "/db.php"; -require_once __DIR__ . '/../src/functions.php'; +// Optionally expose a single global for templates if desired +$GLOBALS['client_id'] = $clientId; diff --git a/config/i18n.php b/config/i18n.php deleted file mode 100644 index 2621bc8..0000000 --- a/config/i18n.php +++ /dev/null @@ -1,397 +0,0 @@ - 2) { - $code = substr($code, 0, 2); - } - - if ($code === '') { - return null; - } - - $label = isset($meta['label']) && $meta['label'] !== '' - ? (string)$meta['label'] - : strtoupper($code); - - $flag = isset($meta['flag']) ? (string)$meta['flag'] : ''; - - return [ - 'code' => $code, - 'label' => $label, - 'flag' => $flag, - ]; -} - -/** - * Alle verfügbaren Sprachen aus /public/assets/i18n/*.json ermitteln. - * Verfügbar = JSON mit meta.enabled === true. - * EN wird garantiert hinzugefügt (Fallback), falls nicht gefunden. - */ -function app_i18n_detect_available_languages(): array -{ - $baseDir = realpath(__DIR__ . '/../public/assets/i18n'); - if ($baseDir === false) { - // Wenn gar kein Verzeichnis da ist: minimaler EN-Fallback - return [ - 'en' => [ - 'code' => 'en', - 'label' => 'English', - 'flag' => '', - ], - ]; - } - - $files = glob($baseDir . '/*.json') ?: []; - $langs = []; - - foreach ($files as $file) { - $meta = app_i18n_load_language_meta_from_file($file); - if ($meta === null) { - continue; - } - - $code = $meta['code']; - - // Erste gültige Definition pro Code gewinnt - if (!isset($langs[$code])) { - $langs[$code] = $meta; - } - } - - // EN muss immer vorhanden sein (laut deiner Vorgabe) - if (!isset($langs['en'])) { - // Versuch: gibt es eine en.json, auch wenn enabled=false? - foreach ($files as $file) { - $base = strtolower(basename($file, '.json')); - if ($base === 'en') { - $json = @file_get_contents($file); - $data = json_decode($json, true); - $meta = is_array($data['meta'] ?? null) ? $data['meta'] : []; - - $label = isset($meta['label']) && $meta['label'] !== '' - ? (string)$meta['label'] - : 'English'; - $flag = isset($meta['flag']) ? (string)$meta['flag'] : ''; - - $langs['en'] = [ - 'code' => 'en', - 'label' => $label, - 'flag' => $flag, - ]; - break; - } - } - } - - // Wenn immer noch kein EN → minimaler Stub - if (!isset($langs['en'])) { - $langs['en'] = [ - 'code' => 'en', - 'label' => 'English', - 'flag' => '', - ]; - } - - ksort($langs); - - return $langs; -} - -/** - * Browsersprache aus HTTP_ACCEPT_LANGUAGE extrahieren (2-Buchstaben), - * aber nur, wenn sie in $available existiert. - */ -function app_i18n_detect_browser_lang(array $available): ?string -{ - $header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''; - if ($header === '') { - return null; - } - - $parts = explode(',', $header); - foreach ($parts as $part) { - $part = trim($part); - if ($part === '') { - continue; - } - - $code = strtolower(substr($part, 0, 2)); // "de-DE" → "de" - if (isset($available[$code])) { - return $code; - } - } - - return null; -} - -/** - * Aktuelle Sprache bestimmen: - * 1) ?lang=xx (wenn in $available) - * 2) Browsersprache (wenn in $available) - * 3) Fallback "en" - */ -function app_i18n_resolve_current_lang(array $available): string -{ - // 1) URL-Parameter ?lang=xx - if (!empty($_GET['lang'])) { - $param = strtolower(substr($_GET['lang'], 0, 2)); - if (isset($available[$param])) { - return $param; - } - } - - // 2) Browsersprache - $browser = app_i18n_detect_browser_lang($available); - if ($browser !== null) { - return $browser; - } - - // 3) Standard EN - if (isset($available['en'])) { - return 'en'; - } - - // Sicherheitsfallback: erste verfügbare Sprache - $keys = array_keys($available); - return $keys[0] ?? 'en'; -} - -// ----------------------------------------------------- -// Bootstrap ausführen -// ----------------------------------------------------- - -$availableLangs = app_i18n_detect_available_languages(); -$currentLang = app_i18n_resolve_current_lang($availableLangs); - -// Global bereitstellen -$GLOBALS['availableLangs'] = $availableLangs; -$GLOBALS['lang'] = $currentLang; - -// Optional in Session merken (muss nicht, schadet aber auch nicht) -$_SESSION['lang'] = $currentLang; - -/** - * Frontend-Config für JS (usbConfig.i18n) - * → benutzt direkt die Meta-Daten aus den JSONs (code, label, flag) - */ -function app_i18n_get_frontend_config(): array -{ - return [ - 'current' => $GLOBALS['lang'] ?? 'en', - 'available' => $GLOBALS['availableLangs'] ?? [], - ]; -} - -/** - * Komplette JSON-Struktur für eine Sprache laden. - * Nutzt einfachen Request-Cache, damit pro Sprache nur einmal von Platte gelesen wird. - */ -function app_i18n_load_lang_json(string $lang): array -{ - static $cache = []; - - $lang = strtolower(substr($lang, 0, 5)); - - if (isset($cache[$lang])) { - return $cache[$lang]; - } - - $baseDir = realpath(__DIR__ . '/../public/assets/i18n'); - if ($baseDir === false) { - $cache[$lang] = []; - return $cache[$lang]; - } - - $path = $baseDir . '/' . $lang . '.json'; - if (!is_file($path)) { - // Fallback: en.json, falls vorhanden - $fallback = $baseDir . '/en.json'; - if (is_file($fallback)) { - $json = @file_get_contents($fallback); - $data = json_decode($json, true); - $cache[$lang] = is_array($data) ? $data : []; - return $cache[$lang]; - } - - $cache[$lang] = []; - return $cache[$lang]; - } - - $json = @file_get_contents($path); - $data = json_decode($json, true); - $cache[$lang] = is_array($data) ? $data : []; - - return $cache[$lang]; -} - -/** - * Aus einem Label einen stabilen i18n-Key für Nav-Anker bauen. - * Beispiel: "So funktioniert USBCheck!" -> "nav_so_funktioniert_usbcheck" - */ -function app_i18n_make_anchor_key(string $label): string -{ - // HTML-Entities entfernen (z. B. &) - $decoded = html_entity_decode($label, ENT_QUOTES | ENT_HTML5, 'UTF-8'); - - // Kleinbuchstaben - $decoded = mb_strtolower($decoded, 'UTF-8'); - - // Alles, was kein a-z oder 0-9 ist, durch Unterstrich ersetzen - $key = preg_replace('/[^a-z0-9]+/u', '_', $decoded); - - // Mehrfache Unterstriche trimmen - $key = trim($key, '_'); - - if ($key === '') { - $key = 'item'; - } - - // Prefix, damit klar ist, dass es Navigationskeys sind - return 'nav_' . $key; -} - -/** - * Nav-Anker für eine Seite aus der Sprachdatei holen. - * - * Haupt-Variante im JSON: - * - * "pages": { - * "landing": { - * "anchors": { - * "how": "So funktioniert USBCheck", - * "problem": "Warum gefälschte USB-Sticks gefährlich sind", - * "features": "Funktionen", - * "security": "Sicherheit", - * "faq": "FAQ" - * } - * } - * } - * - * Optional explizit: - * "anchors": { - * "how": { "label": "So funktioniert USBCheck", "i18n": "nav_how" }, - * "faq": { "i18n": "nav_faq" } - * } - * - * Rückgabe-Format: - * [ - * [ 'href' => '#how', 'label' => 'So funktioniert USBCheck', 'i18n' => 'nav_so_funktioniert_usbcheck' ], - * [ 'href' => '#faq', 'label' => '', 'i18n' => 'nav_faq' ], - * ] - */ -function app_get_nav_anchors(string $pageKey): array -{ - $lang = $GLOBALS['lang'] ?? 'en'; - $data = app_i18n_load_lang_json($lang); - - $cfg = $data['pages'][$pageKey]['anchors'] ?? null; - if (!is_array($cfg)) { - return []; - } - - $anchors = []; - - foreach ($cfg as $id => $value) { - $id = trim((string)$id); - if ($id === '') { - continue; - } - - $href = '#' . $id; - $label = ''; - $i18n = ''; - - if (is_string($value)) { - // String IMMER als Label übernehmen - $labelTrim = trim($value); - if ($labelTrim === '') { - continue; - } - - $label = $labelTrim; - // i18n-Key automatisch aus dem Label ableiten - $i18n = app_i18n_make_anchor_key($labelTrim); - - } elseif (is_array($value)) { - // Explizite Variante: - // "how": { "label": "...", "i18n": "nav_how" } - if (!empty($value['label'])) { - $label = trim((string)$value['label']); - } - if (!empty($value['i18n'])) { - $i18n = trim((string)$value['i18n']); - } - - if ($label === '' && $i18n === '') { - continue; - } - - // Wenn Label gesetzt, aber kein i18n: automatisch generieren - if ($label !== '' && $i18n === '') { - $i18n = app_i18n_make_anchor_key($label); - } - } else { - // Weder String noch Array → ignorieren - continue; - } - - $anchors[] = [ - 'href' => $href, - 'label' => $label, - 'i18n' => $i18n, - ]; - } - - return $anchors; -} diff --git a/config/prod/config.php b/config/prod/config.php deleted file mode 100644 index b11bace..0000000 --- a/config/prod/config.php +++ /dev/null @@ -1,34 +0,0 @@ - PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => false, +// ------------------------------------------------------------ +// 1) Driver selection (choose one) +// ------------------------------------------------------------ +$driver = 'pgsql'; +// $driver = 'mysql'; +// $driver = 'sqlite'; + +// ------------------------------------------------------------ +// 2) Driver-specific configuration sections +// ------------------------------------------------------------ + +// ---- PostgreSQL (PDO driver: pgsql) ------------------------- +$pgsql = [ + 'driver' => 'pgsql', + 'host' => 'localhost', + 'port' => 5432, + 'dbname' => 'mydb', + + // optional: schema/search_path (commonly "public") + 'schema' => 'public', + + 'user' => 'myuser', + 'password' => 'secret', + + 'options' => [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ], ]; -try { - $pdo = new PDO( - "mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4", - $DB_USER, - $DB_PASS, - $options - ); -} catch (PDOException $e) { - // In Produktion Logging, keine Details ausgeben - http_response_code(500); - echo 'Database connection error.'; - exit; +// ---- MySQL / MariaDB (PDO driver: mysql) ------------------- +$mysql = [ + 'driver' => 'mysql', + 'host' => 'localhost', + 'port' => 3306, + 'dbname' => 'mydb', + 'charset' => 'utf8mb4', + + // Alternative to host/port: + // 'unix_socket' => '/var/run/mysqld/mysqld.sock', + + 'user' => 'myuser', + 'password' => 'secret', + + 'options' => [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ], +]; + +// ---- SQLite (PDO driver: sqlite) --------------------------- +$sqlite = [ + 'driver' => 'sqlite', + + // Use an absolute path in production, e.g. /var/app/data/app.sqlite + // For demo/dev you can use a relative path. + 'path' => __DIR__ . '/../var/app.sqlite', + + // SQLite ignores host/port/user/pass + 'options' => [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ], +]; + +// ------------------------------------------------------------ +// 3) Select and return config +// ------------------------------------------------------------ +switch ($driver) { + case 'pgsql': + return $pgsql; + + case 'mysql': + return $mysql; + + case 'sqlite': + return $sqlite; + + default: + throw new RuntimeException('Unsupported DB driver in config/db.php: ' . $driver); } diff --git a/config/prod/domaindata.php b/config/prod/domaindata.php new file mode 100644 index 0000000..9a70f61 --- /dev/null +++ b/config/prod/domaindata.php @@ -0,0 +1,12 @@ + PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => false, +// ------------------------------------------------------------ +// 1) Driver selection (choose one) +// ------------------------------------------------------------ +$driver = 'pgsql'; +// $driver = 'mysql'; +// $driver = 'sqlite'; + +// ------------------------------------------------------------ +// 2) Driver-specific configuration sections +// ------------------------------------------------------------ + +// ---- PostgreSQL (PDO driver: pgsql) ------------------------- +$pgsql = [ + 'driver' => 'pgsql', + 'host' => 'localhost', + 'port' => 5432, + 'dbname' => 'mydb', + + // optional: schema/search_path (commonly "public") + 'schema' => 'public', + + 'user' => 'myuser', + 'password' => 'secret', + + 'options' => [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ], ]; -try { - $pdo = new PDO( - "mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4", - $DB_USER, - $DB_PASS, - $options - ); -} catch (PDOException $e) { - // In Produktion Logging, keine Details ausgeben - http_response_code(500); - echo 'Database connection error.'; - exit; +// ---- MySQL / MariaDB (PDO driver: mysql) ------------------- +$mysql = [ + 'driver' => 'mysql', + 'host' => 'localhost', + 'port' => 3306, + 'dbname' => 'mydb', + 'charset' => 'utf8mb4', + + // Alternative to host/port: + // 'unix_socket' => '/var/run/mysqld/mysqld.sock', + + 'user' => 'myuser', + 'password' => 'secret', + + 'options' => [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ], +]; + +// ---- SQLite (PDO driver: sqlite) --------------------------- +$sqlite = [ + 'driver' => 'sqlite', + + // Use an absolute path in production, e.g. /var/app/data/app.sqlite + // For demo/dev you can use a relative path. + 'path' => __DIR__ . '/../var/app.sqlite', + + // SQLite ignores host/port/user/pass + 'options' => [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ], +]; + +// ------------------------------------------------------------ +// 3) Select and return config +// ------------------------------------------------------------ +switch ($driver) { + case 'pgsql': + return $pgsql; + + case 'mysql': + return $mysql; + + case 'sqlite': + return $sqlite; + + default: + throw new RuntimeException('Unsupported DB driver in config/db.php: ' . $driver); } diff --git a/config/staging/domaindata.php b/config/staging/domaindata.php new file mode 100644 index 0000000..09426f6 --- /dev/null +++ b/config/staging/domaindata.php @@ -0,0 +1,12 @@ +assets()->addStyle('/assets/app.css', 'early'); +$app->assets()->addScript('/assets/app.js', 'footer', true); + +$flash = $app->flash()->get(); +?> +
= htmlspecialchars(t('common.intro'), ENT_QUOTES) ?>
+ + += htmlspecialchars($GLOBALS['client_id'] ?? '', ENT_QUOTES) ?>Flash uses SessionManager, no direct globals.
+