start
This commit is contained in:
39
config/config.php
Normal file
39
config/config.php
Normal 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
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<?php
|
||||
|
||||
define('APP_DOMAIN_NAME', 'usbcheck.it');
|
||||
define('APP_PREFIX', 'usbcheck');
|
||||
@@ -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;
|
||||
|
||||
397
config/i18n.php
397
config/i18n.php
@@ -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. &)
|
||||
$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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [
|
||||
// ------------------------------------------------------------
|
||||
// 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,
|
||||
],
|
||||
];
|
||||
|
||||
// ---- 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,
|
||||
],
|
||||
];
|
||||
|
||||
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;
|
||||
// ---- 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);
|
||||
}
|
||||
|
||||
12
config/prod/domaindata.php
Normal file
12
config/prod/domaindata.php
Normal 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
8
config/prod/settings.php
Normal 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [
|
||||
// ------------------------------------------------------------
|
||||
// 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,
|
||||
],
|
||||
];
|
||||
|
||||
// ---- 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,
|
||||
],
|
||||
];
|
||||
|
||||
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;
|
||||
// ---- 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);
|
||||
}
|
||||
|
||||
12
config/staging/domaindata.php
Normal file
12
config/staging/domaindata.php
Normal 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');
|
||||
}
|
||||
8
config/staging/settings.php
Normal file
8
config/staging/settings.php
Normal 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
|
||||
|
||||
39
partials/landing/main/home.php
Normal file
39
partials/landing/main/home.php
Normal 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>
|
||||
3
partials/structure/layout_end.php
Normal file
3
partials/structure/layout_end.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php asset_scripts('footer'); ?>
|
||||
</body>
|
||||
</html>
|
||||
22
partials/structure/layout_start.php
Normal file
22
partials/structure/layout_start.php
Normal 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
32
public/..htaccess
Normal 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
1
public/assets/app.css
Normal file
@@ -0,0 +1 @@
|
||||
/* minimal css placeholder */
|
||||
1
public/assets/app.js
Normal file
1
public/assets/app.js
Normal file
@@ -0,0 +1 @@
|
||||
console.log('mini example loaded');
|
||||
40
public/index.php
Normal file
40
public/index.php
Normal 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
3
public/page/index.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
echo "Hello, World!";
|
||||
50
src/App/App.php
Normal file
50
src/App/App.php
Normal 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
44
src/App/Assets.php
Normal 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
60
src/App/Config.php
Normal 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
123
src/App/Database.php
Normal 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
33
src/App/Flash.php
Normal 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
59
src/App/I18n.php
Normal 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
47
src/App/Request.php
Normal 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, '?');
|
||||
}
|
||||
}
|
||||
71
src/App/SessionManager.php
Normal file
71
src/App/SessionManager.php
Normal 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
78
src/helpers.php
Normal 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";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user