This commit is contained in:
2025-12-21 01:11:30 +01:00
parent d3efe34ff4
commit a97f4f77ba
29 changed files with 986 additions and 622 deletions

39
config/config.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
// Basic error reporting (keep strict in dev)
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
require_once __DIR__ . '/settings.php';
require_once __DIR__ . '/domaindata.php';
// Environment: staging|prod|local (example)
if (!defined('APP_ENV')) {
define('APP_ENV', 'local');
}
// Asset versioning
if (!defined('ASSET_VERSION')) {
define('ASSET_VERSION', 'dev-' . date('Ymd-His'));
}
// Primary domain + URL
if (!defined('APP_DOMAIN_PRIMARY')) {
define('APP_DOMAIN_PRIMARY', APP_DOMAIN_NAME);
}
if (!defined('APP_URL_PRIMARY')) {
define('APP_URL_PRIMARY', 'https://' . APP_DOMAIN_PRIMARY);
}
// API base (example)
if (!defined('APP_API_BASE')) {
define('APP_API_BASE', 'https://api.' . APP_DOMAIN_PRIMARY);
}
// Feature toggles
if (!defined('APP_DB_ENABLED')) {
define('APP_DB_ENABLED', false); // set true to enable DB connection
}

View File

@@ -1,4 +0,0 @@
<?php
define('APP_DOMAIN_NAME', 'usbcheck.it');
define('APP_PREFIX', 'usbcheck');

View File

@@ -1,121 +1,41 @@
<?php
declare(strict_types=1);
// -----------------------------------------------------------
// 0) Umgebung / Domains / Error-Level laden
// → Diese Datei DEFINIERT die Konstanten wie
// APP_COOKIE_PREFIX, APP_COOKIE_DOMAIN, APP_ENV etc.
// -----------------------------------------------------------
require_once __DIR__ . "/config.php";
// 1) Load config (constants, env, domains)
require_once __DIR__ . '/config.php';
// Diese Werte später ins Template schieben:
$GLOBALS['app_env'] = APP_ENV;
$GLOBALS['app_base_url'] = APP_URL_PRIMARY;
$GLOBALS['app_api_base'] = $apiBaseUrl;
// -----------------------------------------------------------
// set cookie / session parameters
// -----------------------------------------------------------
if (!defined('CUSTOM_PREFIX')) {
define('CUSTOM_PREFIX', APP_PREFIX);
}
if(!defined('APP_COOKIE_PREFIX')) {
if(APP_ENV==="staging"){
define('APP_COOKIE_PREFIX', APP_PREFIX.'_stg'.'_');
} else
{
define('APP_COOKIE_PREFIX', APP_PREFIX.'_');
}
}
if (!defined('APP_COOKIE_DOMAIN')) {
// Fallback: aktuelle Domain des Hosts
define('APP_COOKIE_DOMAIN', '.'.APP_DOMAIN_PRIMARY);
define('APP_PRIMARY_DOMAIN', APP_DOMAIN_PRIMARY);
}
if (!defined('APP_CLIENT_COOKIE_LIFETIME')) {
define('APP_CLIENT_COOKIE_LIFETIME', 365 * 24 * 60 * 60); // 1 Jahr
}
// Einheitliche Cookie-Namen (projektübergreifend steuerbar)
$sessionCookieName = APP_COOKIE_PREFIX . 'session';
$clientCookieName = APP_COOKIE_PREFIX . 'client';
// -----------------------------------------------------------
// 1) PHP-Session starten
// -----------------------------------------------------------
if (php_sapi_name() !== 'cli') {
if (session_status() === PHP_SESSION_NONE) {
session_name($sessionCookieName);
session_set_cookie_params([
'lifetime' => 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;

View File

@@ -1,397 +0,0 @@
<?php
// config/i18n.php
// Zentrale Sprachlogik für das gesamte Projekt
if (session_status() !== PHP_SESSION_ACTIVE) {
@session_start();
}
/**
* Liest die meta-Infos einer Sprachdatei.
* Erwartet Struktur:
* {
* "meta": { "code": "de", "label": "Deutsch", "flag": "🇩🇪", "enabled": true },
* ...
* }
*
* Gibt NULL zurück, wenn:
* - Datei nicht lesbar
* - JSON ungültig
* - kein meta vorhanden
* - meta.enabled existiert und NICHT true ist
*/
function app_i18n_load_language_meta_from_file(string $file): ?array
{
$json = @file_get_contents($file);
if ($json === false) {
return null;
}
$data = json_decode($json, true);
if (!is_array($data)) {
return null;
}
$meta = $data['meta'] ?? null;
if (!is_array($meta)) {
return null;
}
// Nur aktive Sprachen (enabled === true)
if (array_key_exists('enabled', $meta) && !$meta['enabled']) {
return null;
}
// Code aus meta, Fallback: Dateiname
$code = '';
if (!empty($meta['code'])) {
$code = strtolower(substr((string)$meta['code'], 0, 5));
} else {
$base = basename($file, '.json');
$code = strtolower($base);
}
// Auf 2-Buchstaben-Codes normalisieren (de-DE → de)
if (strlen($code) > 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. &amp;)
$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;
}

View File

@@ -1,34 +0,0 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . "/domaindata.php";
// Umgebung (optional, aber hilfreich für Debugging / Logik)
define('APP_ENV', 'prod'); // oder 'prod', 'local', ...
if (!defined('ASSET_VERSION')) {
define('ASSET_VERSION', '2024-11-22'); // oder deine aktuelle Version
}
// Domain-Konfiguration (kann pro Umgebung angepasst werden)
if (!defined('APP_DOMAIN_PRIMARY')) {
define('APP_DOMAIN_PRIMARY', APP_DOMAIN_NAME);
}
if (!defined('APP_URL_PRIMARY')) {
define('APP_URL_PRIMARY', 'https://' . APP_DOMAIN_PRIMARY);
}
if (!defined('APP_DOMAIN_FAKECHECK')) {
define('APP_DOMAIN_FAKECHECK', 'ismyusbfake.com');
}
if (!defined('APP_URL_FAKECHECK')) {
define('APP_URL_FAKECHECK', 'https://' . APP_DOMAIN_FAKECHECK);
}
// Matomo Einstellungen
define('MATOMO_URL', 'https://matomo.my-statistics.info/');
define('MATOMO_ENABLED', true);
define('MATOMO_SITE_ID', 7);
$env = 'prod';
$baseUrl = 'https://'.APP_DOMAIN_NAME;
$apiBaseUrl = 'https://api.'.APP_DOMAIN_NAME;

View File

@@ -1,28 +1,93 @@
<?php
// config/db.php
declare(strict_types=1);
$DB_HOST = 'localhost';
$DB_NAME = 'd0444c21';
$DB_USER = 'd0444c21';
$DB_PASS = 'Rnßü7ROxFTxmOazLNhz/'; // anpassen
/**
* config/db.php
*
* - Choose ONE driver below (others stay commented).
* - Each driver has its own config section.
* - The file returns ONE normalized array used by Database::createPdo().
*/
$options = [
PDO::ATTR_ERRMODE => 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);
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
// Example: a single "brand" domain name.
// In real deployments you might derive this from ENV or hostnames.
if (!defined('APP_DOMAIN_NAME')) {
define('APP_DOMAIN_NAME', 'papa-kind-treff.info');
}
if (!defined('APP_PREFIX')) {
define('APP_PREFIX', 'miniapp');
}

8
config/prod/settings.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
define('APP_ENV', 'local');
define('ASSET_VERSION', 'dev-' . date('Ymd-His'));
define('APP_DOMAIN_PRIMARY', APP_DOMAIN_NAME);
define('APP_URL_PRIMARY', 'https://' . APP_DOMAIN_PRIMARY);
define('APP_API_BASE', 'https://api.' . APP_DOMAIN_PRIMARY);
define('APP_DB_ENABLED', false); // set true to enable DB connection

View File

@@ -1,36 +0,0 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . "/domaindata.php";
// Umgebung (optional, aber hilfreich für Debugging / Logik)
define('APP_ENV', 'staging'); // oder 'prod', 'local', ...
if (!defined('ASSET_VERSION')) {
define('ASSET_VERSION', time()); // oder deine aktuelle Version
}
// Domain-Konfiguration (kann pro Umgebung angepasst werden)
if (!defined('APP_DOMAIN_PRIMARY')) {
define('APP_DOMAIN_PRIMARY', 'staging.'.APP_DOMAIN_NAME);
}
if (!defined('APP_URL_PRIMARY')) {
define('APP_URL_PRIMARY', 'https://' . APP_DOMAIN_PRIMARY);
}
if (!defined('APP_DOMAIN_FAKECHECK')) {
define('APP_DOMAIN_FAKECHECK', 'staging.ismyusbfake.com');
}
if (!defined('APP_URL_FAKECHECK')) {
define('APP_URL_FAKECHECK', 'https://' . APP_DOMAIN_FAKECHECK);
}
// Matomo Einstellungen
define('MATOMO_URL', 'https://matomo.my-statistics.info/');
define('MATOMO_ENABLED', false);
define('MATOMO_SITE_ID', 8);
$baseUrl = 'https://'.APP_DOMAIN_PRIMARY;
$apiBaseUrl = 'https://api.'.APP_DOMAIN_PRIMARY;

View File

@@ -1,28 +1,93 @@
<?php
// config/db.php
declare(strict_types=1);
$DB_HOST = 'localhost';
$DB_NAME = 'd0444c25';
$DB_USER = 'd0444c25';
$DB_PASS = '/7ü9+§ÄfkiQvGPr§2Op7'; // anpassen
/**
* config/db.php
*
* - Choose ONE driver below (others stay commented).
* - Each driver has its own config section.
* - The file returns ONE normalized array used by Database::createPdo().
*/
$options = [
PDO::ATTR_ERRMODE => 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);
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
// Example: a single "brand" domain name.
// In real deployments you might derive this from ENV or hostnames.
if (!defined('APP_DOMAIN_NAME')) {
define('APP_DOMAIN_NAME', 'staging.papa-kind-treff.info');
}
if (!defined('APP_PREFIX')) {
define('APP_PREFIX', 'miniapp');
}

View File

@@ -0,0 +1,8 @@
<?php
define('APP_ENV', 'local');
define('ASSET_VERSION', 'dev-' . date('Ymd-His'));
define('APP_DOMAIN_PRIMARY', APP_DOMAIN_NAME);
define('APP_URL_PRIMARY', 'https://' . APP_DOMAIN_PRIMARY);
define('APP_API_BASE', 'https://api.' . APP_DOMAIN_PRIMARY);
define('APP_DB_ENABLED', false); // set true to enable DB connection

View File

@@ -0,0 +1,39 @@
<?php
$app = app();
// Example: register assets from inside a landing template
$app->assets()->addStyle('/assets/app.css', 'early');
$app->assets()->addScript('/assets/app.js', 'footer', true);
$flash = $app->flash()->get();
?>
<div class="card">
<div class="pill">env: <?= htmlspecialchars($app->config()->env, ENT_QUOTES) ?></div>
<h1 style="margin-top: .75rem;"><?= htmlspecialchars(t('common.title'), ENT_QUOTES) ?></h1>
<p class="muted"><?= htmlspecialchars(t('common.intro'), ENT_QUOTES) ?></p>
<?php if ($flash): ?>
<div style="margin: 1rem 0; padding: .75rem 1rem; border: 1px solid #ddd; border-radius: 12px;">
<strong><?= htmlspecialchars($flash['type'], ENT_QUOTES) ?>:</strong>
<?= htmlspecialchars($flash['message'], ENT_QUOTES) ?>
</div>
<?php endif; ?>
<div class="grid" style="margin-top: 1rem;">
<div>
<h3 style="margin: 0 0 .5rem 0;">Runtime</h3>
<div><strong>Current URL:</strong> <?= htmlspecialchars($app->request()->currentUrl(), ENT_QUOTES) ?></div>
<div><strong>Client-ID:</strong> <code><?= htmlspecialchars($GLOBALS['client_id'] ?? '', ENT_QUOTES) ?></code></div>
</div>
<div>
<h3 style="margin: 0 0 .5rem 0;">Actions</h3>
<form method="post" action="/action/flash">
<button type="submit" style="padding:.6rem 1rem; border-radius: 12px; border: 1px solid #ddd; background: white; cursor:pointer;">
Set flash message
</button>
</form>
<p class="muted" style="margin-top:.5rem;">Flash uses SessionManager, no direct globals.</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
<?php asset_scripts('footer'); ?>
</body>
</html>

View File

@@ -0,0 +1,22 @@
<?php
/** @var \App\App $app */
$app = app();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars(t('common.title'), ENT_QUOTES) ?></title>
<?php asset_styles(); ?>
<?php asset_scripts('header'); ?>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin: 2rem; }
.card { border: 1px solid #ddd; border-radius: 12px; padding: 1.25rem; max-width: 820px; }
.muted { color: #555; }
.pill { display: inline-block; padding: .25rem .5rem; border-radius: 999px; border: 1px solid #ddd; font-size: .9rem; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 720px) { .grid { grid-template-columns: 1fr; } }
</style>
</head>
<body>

32
public/..htaccess Normal file
View File

@@ -0,0 +1,32 @@
# -------------------------------------------------
# Apache Front Controller Setup (public/.htaccess)
# -------------------------------------------------
RewriteEngine On
# Sicherheit: keine Directory Listings
Options -Indexes
# -------------------------------------------------
# 1) Assets DIREKT ausliefern
# -------------------------------------------------
RewriteRule ^assets/ - [L]
# -------------------------------------------------
# 2) page/ von außen sperren (nur intern per require nutzbar)
# -------------------------------------------------
RewriteRule ^page/ - [F,L]
# -------------------------------------------------
# 3) Alles andere an den Front Controller
# -------------------------------------------------
RewriteRule ^ index.php [L]
# -------------------------------------------------
# 4) (Optional) Zusätzliche Sicherheits-Header
# -------------------------------------------------
<IfModule mod_headers.c>
Header set X-Frame-Options "SAMEORIGIN"
Header set X-Content-Type-Options "nosniff"
Header set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>

1
public/assets/app.css Normal file
View File

@@ -0,0 +1 @@
/* minimal css placeholder */

1
public/assets/app.js Normal file
View File

@@ -0,0 +1 @@
console.log('mini example loaded');

40
public/index.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/fileload.php';
$uriPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$uriPath = preg_replace('~/{2,}~', '/', $uriPath);
$uriPath = trim($uriPath, '/');
// Sicherheitscheck
if (str_contains($uriPath, '..')) {
http_response_code(400);
exit('Bad request');
}
// Root → page/index.php
if ($uriPath === '' || $uriPath === 'index' || $uriPath === 'index.php') {
$target = __DIR__ . '/page/index.php';
} else {
$base = __DIR__ . '/page/' . $uriPath;
// 1) Verzeichnis mit index.php
if (is_dir($base) && is_file($base . '/index.php')) {
$target = $base . '/index.php';
}
// 2) Datei
elseif (is_file($base . '.php')) {
$target = $base . '.php';
}
// 3) 404
else {
http_response_code(404);
$target = __DIR__ . '/page/404.php';
}
}
// Zentrale Ausgabe
tpl('layout_start', 'structure');
require $target;
tpl('layout_end', 'structure');

3
public/page/index.php Normal file
View File

@@ -0,0 +1,3 @@
<?php
echo "Hello, World!";

50
src/App/App.php Normal file
View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App;
final class App
{
private static ?self $instance = null;
private Request $request;
private SessionManager $session;
private Assets $assets;
private I18n $i18n;
private Flash $flash;
private ?\PDO $pdo;
private function __construct(private Config $config)
{
$this->request = new Request();
$this->session = new SessionManager($config);
$this->assets = new Assets($config);
$this->i18n = new I18n($config, 'en');
$this->flash = new Flash($this->session);
$this->pdo = Database::createPdo($config);
}
public static function init(Config $config): self
{
if (self::$instance === null) {
self::$instance = new self($config);
}
return self::$instance;
}
public static function get(): self
{
if (self::$instance === null) {
throw new \RuntimeException('App not initialized. Call App::init() in bootstrap.');
}
return self::$instance;
}
public function config(): Config { return $this->config; }
public function request(): Request { return $this->request; }
public function session(): SessionManager { return $this->session; }
public function assets(): Assets { return $this->assets; }
public function i18n(): I18n { return $this->i18n; }
public function flash(): Flash { return $this->flash; }
public function pdo(): ?\PDO { return $this->pdo; }
}

44
src/App/Assets.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App;
final class Assets
{
private array $styles = [];
private array $scriptsHeader = [];
private array $scriptsFooter = [];
public function __construct(private Config $config) {}
public function addStyle(string $href, string $priority = 'normal', ?string $version = null): void
{
$version ??= $this->config->assetVersion;
$this->styles[] = [
'href' => $href,
'priority' => $priority,
'version' => $version,
];
}
public function addScript(string $src, string $pos = 'footer', bool $defer = true, bool $async = false, ?string $version = null): void
{
$version ??= $this->config->assetVersion;
$row = [
'src' => $src,
'defer' => $defer,
'async' => $async,
'version' => $version,
];
if ($pos === 'header') {
$this->scriptsHeader[] = $row;
} else {
$this->scriptsFooter[] = $row;
}
}
public function styles(): array { return $this->styles; }
public function headerScripts(): array { return $this->scriptsHeader; }
public function footerScripts(): array { return $this->scriptsFooter; }
}

60
src/App/Config.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App;
final class Config
{
public function __construct(
public readonly string $env,
public readonly string $prefix,
public readonly string $primaryDomain,
public readonly string $primaryUrl,
public readonly string $apiBase,
public readonly string $assetVersion,
public readonly bool $dbEnabled,
public readonly array $db,
) {}
public static function fromPhpConstants(string $configDir): self
{
// config.php defines these constants.
$env = defined('APP_ENV') ? (string) APP_ENV : 'prod';
$prefix = defined('APP_PREFIX') ? (string) APP_PREFIX : 'app';
$primaryDom = defined('APP_DOMAIN_PRIMARY') ? (string) APP_DOMAIN_PRIMARY : 'example.test';
$primaryUrl = defined('APP_URL_PRIMARY') ? (string) APP_URL_PRIMARY : 'https://example.test';
$apiBase = defined('APP_API_BASE') ? (string) APP_API_BASE : ($primaryUrl . '/api');
$assetVersion = defined('ASSET_VERSION') ? (string) ASSET_VERSION : '';
$dbEnabled = defined('APP_DB_ENABLED') ? (bool) APP_DB_ENABLED : false;
$dbFile = rtrim($configDir, '/\\') . DIRECTORY_SEPARATOR . 'db.php';
$db = file_exists($dbFile) ? (array) require $dbFile : [];
return new self(
env: $env,
prefix: $prefix,
primaryDomain: $primaryDom,
primaryUrl: rtrim($primaryUrl, '/'),
apiBase: rtrim($apiBase, '/'),
assetVersion: $assetVersion,
dbEnabled: $dbEnabled,
db: $db
);
}
public function cookiePrefix(): string
{
// Example: add suffix for staging
if ($this->env === 'staging') {
return $this->prefix . '_stg_';
}
return $this->prefix . '_';
}
public function cookieDomain(): string
{
// Leading dot for subdomain-wide cookies
return '.' . ltrim($this->primaryDomain, '.');
}
}

123
src/App/Database.php Normal file
View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace App;
final class Database
{
public static function createPdo(Config $config): ?\PDO
{
if (!$config->dbEnabled) {
return null;
}
$db = $config->db;
$driver = (string)($db['driver'] ?? '');
if ($driver === '') {
throw new \RuntimeException('DB enabled but config/db.php missing "driver"');
}
$dsn = match ($driver) {
'mysql' => self::buildMysqlDsn($db),
'pgsql' => self::buildPgsqlDsn($db),
'sqlite' => self::buildSqliteDsn($db),
default => throw new \RuntimeException('Unsupported PDO driver: ' . $driver),
};
try {
$pdo = new \PDO(
$dsn,
// sqlite braucht user/pass nicht, PDO ignoriert es aber; wir geben leer zurück
(string)($db['user'] ?? ''),
(string)($db['password'] ?? ''),
(array)($db['options'] ?? [])
);
// Optional: PostgreSQL schema/search_path setzen
if ($driver === 'pgsql' && !empty($db['schema'])) {
// Minimaler Schutz gegen Injection über schema
$schema = preg_replace('/[^a-zA-Z0-9_]/', '', (string)$db['schema']);
if ($schema !== '') {
$pdo->exec('SET search_path TO ' . $schema);
}
}
return $pdo;
} catch (\PDOException $e) {
// In Prod würdest du loggen; hier minimal
http_response_code(500);
echo 'Database connection error.';
exit;
}
}
private static function buildMysqlDsn(array $db): string
{
if (empty($db['dbname'])) {
throw new \RuntimeException('MySQL config missing "dbname"');
}
$charset = (string)($db['charset'] ?? 'utf8mb4');
// Unix socket takes precedence
if (!empty($db['unix_socket'])) {
return sprintf(
'mysql:unix_socket=%s;dbname=%s;charset=%s',
(string)$db['unix_socket'],
(string)$db['dbname'],
$charset
);
}
$host = (string)($db['host'] ?? 'localhost');
$port = (int)($db['port'] ?? 3306);
return sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
$host,
$port,
(string)$db['dbname'],
$charset
);
}
private static function buildPgsqlDsn(array $db): string
{
if (empty($db['dbname'])) {
throw new \RuntimeException('PostgreSQL config missing "dbname"');
}
$host = (string)($db['host'] ?? 'localhost');
$port = (int)($db['port'] ?? 5432);
// Hinweis: charset gehört bei pgsql nicht in den DSN
return sprintf(
'pgsql:host=%s;port=%d;dbname=%s',
$host,
$port,
(string)$db['dbname']
);
}
private static function buildSqliteDsn(array $db): string
{
// SQLite kann :memory: oder einen Pfad nutzen
$path = (string)($db['path'] ?? '');
if ($path === '') {
// Default: Memory-DB
$path = ':memory:';
}
// Wenn es ein Pfad ist, stelle sicher, dass das Verzeichnis existiert.
if ($path !== ':memory:') {
$dir = \dirname($path);
if ($dir && !is_dir($dir)) {
@mkdir($dir, 0775, true);
}
}
return 'sqlite:' . $path;
}
}

33
src/App/Flash.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App;
final class Flash
{
public function __construct(private SessionManager $session) {}
public function set(string $type, string $message): void
{
$this->session->start();
$_SESSION['flash'] = [
'type' => $type,
'message' => $message,
];
}
public function get(): ?array
{
$this->session->start();
if (empty($_SESSION['flash']) || !is_array($_SESSION['flash'])) {
return null;
}
$f = $_SESSION['flash'];
unset($_SESSION['flash']);
return [
'type' => (string)($f['type'] ?? 'info'),
'message' => (string)($f['message'] ?? ''),
];
}
}

59
src/App/I18n.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App;
final class I18n
{
private array $current = [];
private array $fallback = [];
public function __construct(private Config $config, private string $lang = 'en')
{
// Minimal example translations (normally load JSON/PHP arrays from disk)
$this->fallback = [
'common' => [
'title' => 'Mini Example Landingpage',
'intro' => 'This is a tiny project showing a clean bootstrap.',
],
'cta' => [
'primary' => 'Continue',
],
];
$this->current = $this->fallback;
}
private function traverse(array $data, string $key): mixed
{
$node = $data;
foreach (explode('.', $key) as $seg) {
if (!is_array($node) || !array_key_exists($seg, $node)) {
return null;
}
$node = $node[$seg];
}
return $node;
}
public function get(string $key, $default = '', array $vars = []): string
{
$val = $this->traverse($this->current, $key);
if ($val === null) {
$val = $this->traverse($this->fallback, $key);
}
if (!is_string($val)) {
$val = (string)($default ?? '');
}
// Built-ins
$val = str_replace('{year}', date('Y'), $val);
$val = str_replace('{{primary_url}}', $this->config->primaryUrl, $val);
foreach ($vars as $k => $v) {
$val = str_replace('{' . $k . '}', (string)$v, $val);
$val = str_replace('{{' . $k . '}}', (string)$v, $val);
}
return $val;
}
}

47
src/App/Request.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App;
final class Request
{
public function scheme(): string
{
// Proxy / LB
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
$proto = strtolower((string)$_SERVER['HTTP_X_FORWARDED_PROTO']);
if ($proto === 'https' || $proto === 'http') {
return $proto;
}
}
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
return 'https';
}
return 'http';
}
public function host(): string
{
return (string)($_SERVER['HTTP_HOST'] ?? 'localhost');
}
public function baseUrl(): string
{
return $this->scheme() . '://' . $this->host();
}
public function path(): string
{
return (string) strtok((string)($_SERVER['REQUEST_URI'] ?? '/'), '?');
}
public function currentUrl(bool $withQuery = true): string
{
$base = $this->baseUrl();
$uri = (string)($_SERVER['REQUEST_URI'] ?? '/');
if ($withQuery) {
return $base . $uri;
}
return $base . (string) strtok($uri, '?');
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App;
final class SessionManager
{
private string $sessionCookieName;
private string $clientCookieName;
public function __construct(private Config $config)
{
$prefix = $config->cookiePrefix();
$this->sessionCookieName = $prefix . 'session';
$this->clientCookieName = $prefix . 'client';
}
public function start(): void
{
if (PHP_SAPI === 'cli') {
return;
}
if (session_status() !== PHP_SESSION_NONE) {
return;
}
session_name($this->sessionCookieName);
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => $this->config->cookieDomain(),
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
public function ensureClientId(int $lifetimeSeconds = 31536000): string
{
if (PHP_SAPI === 'cli') {
return 'cli';
}
$id = $_COOKIE[$this->clientCookieName] ?? null;
if (!is_string($id) || !preg_match('/^[a-f0-9]{64}$/', $id)) {
$id = bin2hex(random_bytes(32));
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
setcookie($this->clientCookieName, $id, [
'expires' => time() + $lifetimeSeconds,
'path' => '/',
'domain' => $this->config->cookieDomain(),
'secure' => $secure,
'httponly' => false, // accessible to JS if needed
'samesite' => 'Lax',
]);
$_COOKIE[$this->clientCookieName] = $id;
}
return $id;
}
}

78
src/helpers.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
use App\App;
function app(): App
{
return App::get();
}
function t(string $key, $default = '', array $vars = []): string
{
return app()->i18n()->get($key, $default, $vars);
}
function tpl(string $file, string $type = 'structure', string $site = 'main'): void
{
$base = __DIR__ . '/../partials/';
// very small validation
foreach ([$file, $type, $site] as $v) {
if (preg_match('/[^a-zA-Z0-9_\-]/', $v)) {
echo "<!-- tpl(): invalid parameter -->";
return;
}
}
if ($type === 'landing') {
$path = $base . "landing/$site/$file.php";
} else {
$path = $base . "structure/$file.php";
}
if (file_exists($path)) {
include $path;
} else {
echo "<!-- tpl(): not found: $path -->";
}
}
function asset_styles(): void
{
$styles = app()->assets()->styles();
// simple priority order
$order = ['early' => 0, 'normal' => 1, 'late' => 2];
usort($styles, fn($a,$b) => ($order[$a['priority']] ?? 1) <=> ($order[$b['priority']] ?? 1));
foreach ($styles as $s) {
$href = $s['href'];
$v = $s['version'];
if ($v !== null && $v !== '') {
$sep = (str_contains($href, '?') ? '&' : '?');
$href = $href . $sep . 'v=' . rawurlencode((string)$v);
}
echo '<link rel="stylesheet" href="' . htmlspecialchars($href, ENT_QUOTES) . '">' . "\n";
}
}
function asset_scripts(string $pos = 'footer'): void
{
$scripts = ($pos === 'header') ? app()->assets()->headerScripts() : app()->assets()->footerScripts();
foreach ($scripts as $s) {
$src = $s['src'];
$v = $s['version'];
if ($v !== null && $v !== '') {
$sep = (str_contains($src, '?') ? '&' : '?');
$src = $src . $sep . 'v=' . rawurlencode((string)$v);
}
$attrs = '';
if (!empty($s['defer'])) $attrs .= ' defer';
if (!empty($s['async'])) $attrs .= ' async';
echo '<script src="' . htmlspecialchars($src, ENT_QUOTES) . '"' . $attrs . '></script>' . "\n";
}
}