This commit is contained in:
115
.gitea/workflows/deploy.yml
Normal file
115
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,115 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
|
||||
env:
|
||||
BASE_DIRS: "src public api partials tools"
|
||||
CONFIG_BASE_DIR: "config"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: private-server
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install lftp
|
||||
shell: sh
|
||||
run: |
|
||||
if command -v lftp >/dev/null 2>&1; then
|
||||
echo "✅ lftp bereits installiert"
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache lftp ca-certificates
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update
|
||||
apt-get install -y lftp ca-certificates
|
||||
else
|
||||
echo "❌ Kein unterstützter Paketmanager gefunden"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Set environment
|
||||
run: |
|
||||
if [ "${{ gitea.ref_name }}" = "main" ]; then
|
||||
echo "TARGET_PATH=${{ vars.FTP_PATH_PROD }}" >> "$GITHUB_ENV"
|
||||
echo "CONFIG_ENV_DIR=config/prod" >> "$GITHUB_ENV"
|
||||
elif [ "${{ gitea.ref_name }}" = "develop" ]; then
|
||||
echo "TARGET_PATH=${{ vars.FTP_PATH_STAGING }}" >> "$GITHUB_ENV"
|
||||
echo "CONFIG_ENV_DIR=config/staging" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "Unsupported branch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Debug workspace
|
||||
run: |
|
||||
echo "📂 CI Workspace:"
|
||||
pwd
|
||||
ls -la
|
||||
|
||||
- name: Deploy via FTPS
|
||||
run: |
|
||||
set -e
|
||||
|
||||
echo "🚀 Deploy to ${TARGET_PATH}"
|
||||
|
||||
VALID_DIRS=""
|
||||
|
||||
for d in $BASE_DIRS; do
|
||||
if [ -d "$d" ]; then
|
||||
VALID_DIRS="$VALID_DIRS $d"
|
||||
else
|
||||
echo "⚠️ Überspringe fehlendes Verzeichnis: $d"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$VALID_DIRS" ]; then
|
||||
echo "❌ Kein deploybares Verzeichnis gefunden."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for d in $VALID_DIRS; do
|
||||
echo "🔁 ${d}/ → ${TARGET_PATH}${d}/"
|
||||
lftp -u "${{ secrets.FTP_USER }}","${{ secrets.FTP_PASSWORD }}" "${{ vars.FTP_HOST }}" -e "
|
||||
set ftp:ssl-force true;
|
||||
set ftp:passive-mode true;
|
||||
set ftp:ssl-protect-data true;
|
||||
set ssl:verify-certificate no;
|
||||
mirror -R --delete --exclude .gitkeep ${d}/ ${TARGET_PATH}${d}/;
|
||||
bye
|
||||
" || exit 1
|
||||
done
|
||||
|
||||
if [ -d "$CONFIG_BASE_DIR" ] && [ -d "$CONFIG_ENV_DIR" ]; then
|
||||
echo "🧩 Baue gemischtes Config-Verzeichnis"
|
||||
|
||||
rm -rf .ci_config_deploy
|
||||
mkdir -p .ci_config_deploy
|
||||
|
||||
for f in ${CONFIG_BASE_DIR}/*.php; do
|
||||
[ -f "$f" ] && cp "$f" .ci_config_deploy/
|
||||
done
|
||||
|
||||
cp -R ${CONFIG_ENV_DIR}/. .ci_config_deploy/
|
||||
|
||||
echo "🔁 config → ${TARGET_PATH}${CONFIG_BASE_DIR}/"
|
||||
|
||||
lftp -u "${{ secrets.FTP_USER }}","${{ secrets.FTP_PASSWORD }}" "${{ vars.FTP_HOST }}" -e "
|
||||
set ftp:ssl-force true;
|
||||
set ftp:passive-mode true;
|
||||
set ftp:ssl-protect-data true;
|
||||
set ssl:verify-certificate no;
|
||||
lcd .ci_config_deploy;
|
||||
mirror -R --delete --exclude .gitkeep ./ ${TARGET_PATH}${CONFIG_BASE_DIR}/;
|
||||
bye
|
||||
" || exit 1
|
||||
else
|
||||
echo "⚠️ Config-Deploy übersprungen: ${CONFIG_BASE_DIR} oder ${CONFIG_ENV_DIR} fehlt"
|
||||
fi
|
||||
|
||||
echo "✅ Deploy abgeschlossen"
|
||||
0
.gitlab-ci.yml
Normal file → Executable file
0
.gitlab-ci.yml
Normal file → Executable file
0
api/.gitkeep
Normal file → Executable file
0
api/.gitkeep
Normal file → Executable file
0
config/.gitkeep
Normal file → Executable file
0
config/.gitkeep
Normal file → Executable file
4
config/domaindata.php
Executable file
4
config/domaindata.php
Executable file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
define('APP_DOMAIN_NAME', 'c0de.it');
|
||||
define('APP_PREFIX', 'c0de');
|
||||
121
config/fileload.php
Executable file
121
config/fileload.php
Executable file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// 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";
|
||||
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
$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',
|
||||
];
|
||||
|
||||
if (!empty(APP_COOKIE_DOMAIN)) {
|
||||
$cookieOpts['domain'] = APP_COOKIE_DOMAIN;
|
||||
}
|
||||
|
||||
setcookie($clientCookieName, $clientId, $cookieOpts);
|
||||
$_COOKIE[$clientCookieName] = $clientId;
|
||||
}
|
||||
|
||||
// global verfügbar machen (NEUER NAME!)
|
||||
$GLOBALS['cookie_client_id'] = $clientId;
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// 3) Sprachlogik laden (bleibt sinnvoll zentral)
|
||||
// -----------------------------------------------------------
|
||||
require_once __DIR__ . '/i18n.php';
|
||||
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// 4) Rest des Systems laden (DB, Funktionen, Hilfs-Libs)
|
||||
// -----------------------------------------------------------
|
||||
require_once __DIR__ . "/db.php";
|
||||
require_once __DIR__ . '/../src/functions.php';
|
||||
397
config/i18n.php
Executable file
397
config/i18n.php
Executable file
@@ -0,0 +1,397 @@
|
||||
<?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;
|
||||
}
|
||||
0
config/prod/.gitkeep
Normal file → Executable file
0
config/prod/.gitkeep
Normal file → Executable file
34
config/prod/config.php
Executable file
34
config/prod/config.php
Executable file
@@ -0,0 +1,34 @@
|
||||
<?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;
|
||||
|
||||
28
config/prod/db.php
Executable file
28
config/prod/db.php
Executable file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
// config/db.php
|
||||
declare(strict_types=1);
|
||||
|
||||
$DB_HOST = 'localhost';
|
||||
$DB_NAME = 'd0455ede';
|
||||
$DB_USER = 'd0455ede';
|
||||
$DB_PASS = 'fF8PhxfCibdLBrSxowIo'; // anpassen
|
||||
|
||||
$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;
|
||||
}
|
||||
0
config/staging/.gitkeep
Normal file → Executable file
0
config/staging/.gitkeep
Normal file → Executable file
36
config/staging/config.php
Executable file
36
config/staging/config.php
Executable file
@@ -0,0 +1,36 @@
|
||||
<?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;
|
||||
|
||||
28
config/staging/db.php
Executable file
28
config/staging/db.php
Executable file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
// config/db.php
|
||||
declare(strict_types=1);
|
||||
|
||||
$DB_HOST = 'localhost';
|
||||
$DB_NAME = 'd0455edf';
|
||||
$DB_USER = 'd0455edf';
|
||||
$DB_PASS = 'fF8PhxfCibdLBrSxowIo'; // anpassen
|
||||
|
||||
$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;
|
||||
}
|
||||
155
inc/helpers.php
Executable file
155
inc/helpers.php
Executable file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
function helper_asset_version(): ?string
|
||||
{
|
||||
if (isset($GLOBALS['layoutContext']['asset_version'])) {
|
||||
return (string)$GLOBALS['layoutContext']['asset_version'];
|
||||
}
|
||||
if (defined('ASSET_VERSION')) {
|
||||
return (string)ASSET_VERSION;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function helper_append_version(string $url, ?string $version = null): string
|
||||
{
|
||||
if ($version === null) {
|
||||
$version = helper_asset_version();
|
||||
}
|
||||
if ($version === null || $version === '') {
|
||||
return $url;
|
||||
}
|
||||
if (preg_match('/[?&]v=/', $url)) {
|
||||
return $url;
|
||||
}
|
||||
return $url . (strpos($url, '?') === false ? '?' : '&') . 'v=' . rawurlencode($version);
|
||||
}
|
||||
|
||||
function tpl_add_script(
|
||||
string $src,
|
||||
string $pos = 'footer',
|
||||
bool $defer = false,
|
||||
bool $async = false,
|
||||
string $type = '',
|
||||
?string $version = null,
|
||||
bool $module = false
|
||||
): void {
|
||||
$data = [
|
||||
'src' => helper_append_version($src, $version),
|
||||
'defer' => $defer,
|
||||
'async' => $async,
|
||||
'type' => $type,
|
||||
'module' => $module,
|
||||
];
|
||||
|
||||
if ($pos === 'header') {
|
||||
$GLOBALS['page_header_scripts'][] = $data;
|
||||
} else {
|
||||
$GLOBALS['page_footer_scripts'][] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
function tpl_add_style(
|
||||
string $href,
|
||||
string $pos = 'header',
|
||||
string $priority = 'normal',
|
||||
?string $version = null,
|
||||
string $media = 'all'
|
||||
): void {
|
||||
$GLOBALS['page_styles'][] = [
|
||||
'href' => helper_append_version($href, $version),
|
||||
'pos' => $pos,
|
||||
'priority' => $priority,
|
||||
'media' => $media,
|
||||
];
|
||||
}
|
||||
|
||||
function tpl(string $file, string $type = 'structure', string $site = 'admin'): void
|
||||
{
|
||||
$base = __DIR__ . '/../partials/';
|
||||
|
||||
$safe = static function ($value) {
|
||||
return preg_match('/^[a-zA-Z0-9_\-]+$/', $value) === 1;
|
||||
};
|
||||
|
||||
if (!$safe($file) || !$safe($type) || !$safe($site)) {
|
||||
echo "<!-- tpl(): Ungültiger Parameter -->";
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $type === 'landing'
|
||||
? $base . "landingpage/{$site}/{$file}.php"
|
||||
: $base . "structure/{$file}.php";
|
||||
|
||||
extract($GLOBALS, EXTR_SKIP);
|
||||
|
||||
if (is_file($path)) {
|
||||
include $path;
|
||||
} else {
|
||||
echo "<!-- tpl(): Datei nicht gefunden: {$path} -->";
|
||||
}
|
||||
}
|
||||
|
||||
function tpl_render_scripts(?array $scripts = null, string $pos = 'footer'): void
|
||||
{
|
||||
if ($scripts === null) {
|
||||
$scripts = $pos === 'header'
|
||||
? ($GLOBALS['page_header_scripts'] ?? [])
|
||||
: ($GLOBALS['page_footer_scripts'] ?? []);
|
||||
}
|
||||
|
||||
foreach ($scripts as $script) {
|
||||
if (empty($script['src'])) {
|
||||
continue;
|
||||
}
|
||||
$attrs = [];
|
||||
if (!empty($script['module'])) {
|
||||
$attrs[] = 'type="module"';
|
||||
} elseif (!empty($script['type'])) {
|
||||
$attrs[] = 'type="' . htmlspecialchars((string)$script['type'], ENT_QUOTES) . '"';
|
||||
}
|
||||
if (!empty($script['defer'])) {
|
||||
$attrs[] = 'defer';
|
||||
}
|
||||
if (!empty($script['async'])) {
|
||||
$attrs[] = 'async';
|
||||
}
|
||||
$attrString = '';
|
||||
if (!empty($attrs)) {
|
||||
$attrString = ' ' . implode(' ', $attrs);
|
||||
}
|
||||
echo '<script src="' . htmlspecialchars($script['src'], ENT_QUOTES) . '"' . $attrString . '></script>' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
function tpl_render_styles(?array $styles = null, string $pos = 'header'): void
|
||||
{
|
||||
if ($styles === null) {
|
||||
$styles = array_filter(
|
||||
$GLOBALS['page_styles'] ?? [],
|
||||
static fn ($style) => ($style['pos'] ?? 'header') === $pos
|
||||
);
|
||||
}
|
||||
|
||||
if (!$styles) {
|
||||
return;
|
||||
}
|
||||
|
||||
$priorityOrder = ['critical' => 0, 'high' => 1, 'normal' => 2, 'low' => 3];
|
||||
usort($styles, static function ($a, $b) use ($priorityOrder) {
|
||||
$aPriority = strtolower($a['priority'] ?? 'normal');
|
||||
$bPriority = strtolower($b['priority'] ?? 'normal');
|
||||
$aScore = $priorityOrder[$aPriority] ?? $priorityOrder['normal'];
|
||||
$bScore = $priorityOrder[$bPriority] ?? $priorityOrder['normal'];
|
||||
return $aScore <=> $bScore;
|
||||
});
|
||||
|
||||
foreach ($styles as $style) {
|
||||
if (empty($style['href'])) {
|
||||
continue;
|
||||
}
|
||||
$media = trim((string)($style['media'] ?? ''));
|
||||
$mediaAttr = $media && strtolower($media) !== 'all' ? ' media="' . htmlspecialchars($media, ENT_QUOTES) . '"' : '';
|
||||
echo '<link rel="stylesheet" href="' . htmlspecialchars($style['href'], ENT_QUOTES) . '"' . $mediaAttr . '>' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
0
partials/.gitkeep
Normal file → Executable file
0
partials/.gitkeep
Normal file → Executable file
0
public/.gitkeep
Normal file → Executable file
0
public/.gitkeep
Normal file → Executable file
0
src/.gitkeep
Normal file → Executable file
0
src/.gitkeep
Normal file → Executable file
0
tools/.gitkeep
Normal file → Executable file
0
tools/.gitkeep
Normal file → Executable file
Reference in New Issue
Block a user