Branding: Farben systemweit einstellbar + Bild-Upload statt nur URLs
Design-Tab (Administration → Einstellungen → Design): - Akzentfarbe für Hell- und Dark-Mode, Markenverlauf und Eckenradius - abgeleitete Töne (Hover, weiche Flächen, Fokusring) werden per color-mix aus der Grundfarbe berechnet – eine Farbe genügt - Live-Vorschau mit Button, Badge, Chip, Logo-Kachel und Link - Ausgabe als schlanker :root-Override über Ui::brandingStyle(), greift auf allen Seiten inklusive Login und Installer Uploads (includes/Upload.php): - Logo, Favicon, Login-Logo und Login-Hintergrund lassen sich jetzt hochladen; das URL-Feld bleibt als Alternative bestehen - Whitelist nach Endung, 3-MB-Grenze, getimagesize-Prüfung für Raster, SVGs werden von Skripten, Event-Handlern und externen Verweisen befreit - Zufällige Dateinamen in uploads/, dort sperrt eine .htaccess die Ausführung von PHP; beim Ersetzen wird die alte Datei gelöscht Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6da46f040a
commit
30d0ce3a23
13 changed files with 437 additions and 32 deletions
|
|
@ -9,6 +9,7 @@ require_once __DIR__ . '/../includes/Auth.php';
|
|||
require_once __DIR__ . '/../includes/Mailer.php';
|
||||
require_once __DIR__ . '/../includes/I18n.php';
|
||||
require_once __DIR__ . '/../includes/Ui.php';
|
||||
require_once __DIR__ . '/../includes/Upload.php';
|
||||
|
||||
$auth = new Auth();
|
||||
$auth->requireAdmin();
|
||||
|
|
@ -41,6 +42,33 @@ if (isset($_POST['ajax_smtp_test'])) {
|
|||
$error = '';
|
||||
$success = '';
|
||||
|
||||
/**
|
||||
* Liefert den neuen Wert eines Bildfeldes: Upload schlaegt URL, und ein
|
||||
* gesetzter Entfernen-Schalter loescht die bisherige Datei.
|
||||
*/
|
||||
function resolveImageField(string $name, Database $db, string $kind): string
|
||||
{
|
||||
$current = (string)$db->getSetting($name, '');
|
||||
|
||||
$uploaded = Upload::store($_FILES[$name . '_file'] ?? [], $kind);
|
||||
if ($uploaded !== '') {
|
||||
Upload::delete($current);
|
||||
return $uploaded;
|
||||
}
|
||||
|
||||
if (!empty($_POST[$name . '_remove'])) {
|
||||
Upload::delete($current);
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = trim($_POST[$name] ?? '');
|
||||
if ($value !== $current && Upload::isLocal($current) && !Upload::isLocal($value)) {
|
||||
Upload::delete($current);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Einstellungen speichern
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
|
||||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
|
|
@ -52,22 +80,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
|
|||
|
||||
if ($formType === 'general') {
|
||||
$settings['app_title'] = trim($_POST['app_title'] ?? '');
|
||||
$settings['logo_url'] = trim($_POST['logo_url'] ?? '');
|
||||
$settings['favicon_url'] = trim($_POST['favicon_url'] ?? '');
|
||||
$settings['logo_url'] = resolveImageField('logo_url', $db, 'image');
|
||||
$settings['favicon_url'] = resolveImageField('favicon_url', $db, 'favicon');
|
||||
$settings['instruction_header'] = trim($_POST['instruction_header'] ?? '');
|
||||
$settings['instruction_text'] = $_POST['instruction_text'] ?? '';
|
||||
$settings['public_access'] = isset($_POST['public_access']) ? '1' : '0';
|
||||
}
|
||||
|
||||
if ($formType === 'branding') {
|
||||
foreach (['brand_accent', 'brand_accent_dark', 'brand_gradient_from', 'brand_gradient_to'] as $key) {
|
||||
$value = strtolower(trim($_POST[$key] ?? ''));
|
||||
$settings[$key] = preg_match('/^#[0-9a-f]{6}$/', $value) ? $value : '';
|
||||
}
|
||||
$radius = (int)($_POST['brand_radius'] ?? Ui::DEFAULT_RADIUS);
|
||||
$settings['brand_radius'] = (string)max(0, min(28, $radius));
|
||||
}
|
||||
|
||||
if ($formType === 'login') {
|
||||
$settings['login_panel_enabled'] = isset($_POST['login_panel_enabled']) ? '1' : '0';
|
||||
$settings['login_brand_name'] = trim($_POST['login_brand_name'] ?? '');
|
||||
$settings['login_logo_url'] = trim($_POST['login_logo_url'] ?? '');
|
||||
$settings['login_logo_url'] = resolveImageField('login_logo_url', $db, 'image');
|
||||
$settings['login_claim_title'] = trim($_POST['login_claim_title'] ?? '');
|
||||
$settings['login_claim_text'] = trim($_POST['login_claim_text'] ?? '');
|
||||
$settings['login_features'] = trim($_POST['login_features'] ?? '');
|
||||
$settings['login_footer'] = trim($_POST['login_footer'] ?? '');
|
||||
$settings['login_bg_image'] = trim($_POST['login_bg_image'] ?? '');
|
||||
$settings['login_bg_image'] = resolveImageField('login_bg_image', $db, 'image');
|
||||
$settings['login_bg_from'] = trim($_POST['login_bg_from'] ?? '');
|
||||
$settings['login_bg_to'] = trim($_POST['login_bg_to'] ?? '');
|
||||
$overlay = (int)($_POST['login_bg_overlay'] ?? 40);
|
||||
|
|
@ -122,6 +159,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
|
|||
}
|
||||
|
||||
$success = __('settings_saved');
|
||||
} catch (RuntimeException $e) {
|
||||
$error = $e->getMessage();
|
||||
} catch (Exception $e) {
|
||||
$error = 'Fehler: ' . $e->getMessage();
|
||||
}
|
||||
|
|
@ -204,6 +243,11 @@ $cs = [
|
|||
'email_user_notification_subject' => $db->getSetting('email_user_notification_subject', '{APP_TITLE} - Berechtigungen geändert'),
|
||||
'email_user_notification_body' => $db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\n{CHANGES}"),
|
||||
'print_template' => $db->getSetting('print_template', '<div style="text-align:center;padding:40px"><h1>{APP_TITLE}</h1><h2>WLAN Code</h2><div style="font-size:48px;font-weight:bold;margin:30px 0;font-family:monospace">{VOUCHER_CODE}</div><p><strong>Gültig bis:</strong> {EXPIRY_DATE} {EXPIRY_TIME}</p><p><strong>Site:</strong> {SITE_NAME}</p><p><strong>Geräte:</strong> {MAX_USES}</p><hr style="margin:30px 0"><div>{INSTRUCTIONS}</div></div>'),
|
||||
'brand_accent' => $db->getSetting('brand_accent', '') ?: Ui::DEFAULT_ACCENT,
|
||||
'brand_accent_dark' => $db->getSetting('brand_accent_dark', '') ?: Ui::DEFAULT_ACCENT_DARK,
|
||||
'brand_gradient_from' => $db->getSetting('brand_gradient_from', '') ?: Ui::DEFAULT_GRADIENT_FROM,
|
||||
'brand_gradient_to' => $db->getSetting('brand_gradient_to', '') ?: Ui::DEFAULT_GRADIENT_TO,
|
||||
'brand_radius' => $db->getSetting('brand_radius', (string)Ui::DEFAULT_RADIUS),
|
||||
'login_panel_enabled' => $db->getSetting('login_panel_enabled', '1'),
|
||||
'login_brand_name' => $db->getSetting('login_brand_name', ''),
|
||||
'login_logo_url' => $db->getSetting('login_logo_url', ''),
|
||||
|
|
@ -219,6 +263,37 @@ $cs = [
|
|||
'last_cron_sync' => $db->getSetting('last_cron_sync', ''),
|
||||
];
|
||||
|
||||
/**
|
||||
* Bildfeld: Vorschau, Upload, alternativ URL – plus Entfernen-Schalter.
|
||||
*/
|
||||
function imageField(string $name, string $label, string $value, string $hint = '', string $accept = 'image/*'): void
|
||||
{
|
||||
$preview = Ui::mediaUrl($value, '../');
|
||||
?>
|
||||
<div class="form-group">
|
||||
<label><?= htmlspecialchars($label) ?></label>
|
||||
<div class="image-field">
|
||||
<div class="image-preview">
|
||||
<?php if ($preview !== ''): ?>
|
||||
<img src="<?= htmlspecialchars($preview) ?>" alt="">
|
||||
<?php else: ?>
|
||||
<i class="fas fa-image"></i>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="image-field-controls">
|
||||
<input type="file" name="<?= $name ?>_file" accept="<?= htmlspecialchars($accept) ?>">
|
||||
<input type="text" name="<?= $name ?>" value="<?= htmlspecialchars($value) ?>"
|
||||
placeholder="<?= htmlspecialchars(__('settings_image_url_placeholder')) ?>">
|
||||
<?php if ($value !== ''): ?>
|
||||
<label class="chk"><input type="checkbox" name="<?= $name ?>_remove" value="1"> <?= __('settings_image_remove') ?></label>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($hint !== ''): ?><div class="help-text"><?= htmlspecialchars($hint) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
$currentPage = 'settings';
|
||||
$adminBase = '';
|
||||
?>
|
||||
|
|
@ -253,6 +328,7 @@ $adminBase = '';
|
|||
<div class="tab-navigation" id="tabNav">
|
||||
<button class="tab-button active" data-tab="general"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_general') ?></button>
|
||||
<button class="tab-button" data-tab="defaults"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_defaults') ?></button>
|
||||
<button class="tab-button" data-tab="branding"><i class="fas fa-palette"></i> <?= __('settings_tab_branding') ?></button>
|
||||
<button class="tab-button" data-tab="login"><i class="fas fa-right-to-bracket"></i> <?= __('settings_tab_login') ?></button>
|
||||
<button class="tab-button" data-tab="cron"><i class="fas fa-clock"></i> <?= __('settings_tab_cron') ?></button>
|
||||
<button class="tab-button" data-tab="m365"><i class="fab fa-microsoft"></i> <?= __('settings_tab_m365') ?></button>
|
||||
|
|
@ -265,13 +341,13 @@ $adminBase = '';
|
|||
<!-- Allgemein -->
|
||||
<div id="tab-general" class="tab-content active">
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_general') ?></h2>
|
||||
<form method="post">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="form_type" value="general">
|
||||
<div class="form-group"><label><?= __('settings_app_title') ?></label><input type="text" name="app_title" value="<?= htmlspecialchars($cs['app_title']) ?>" required></div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label><?= __('settings_logo_url') ?></label><input type="url" name="logo_url" value="<?= htmlspecialchars($cs['logo_url']) ?>" placeholder="https://example.com/logo.png"></div>
|
||||
<div class="form-group"><label><?= __('settings_favicon_url') ?></label><input type="url" name="favicon_url" value="<?= htmlspecialchars($cs['favicon_url']) ?>" placeholder="https://example.com/favicon.ico"><div class="help-text"><?= __('settings_favicon_hint') ?></div></div>
|
||||
<?php imageField('logo_url', __('settings_logo_url'), $cs['logo_url'], __('settings_upload_hint')); ?>
|
||||
<?php imageField('favicon_url', __('settings_favicon_url'), $cs['favicon_url'], __('settings_favicon_hint'), 'image/x-icon,image/png,image/svg+xml'); ?>
|
||||
</div>
|
||||
<hr class="section-divider">
|
||||
<div class="form-group"><label><?= __('settings_instr_header') ?></label><input type="text" name="instruction_header" value="<?= htmlspecialchars($cs['instruction_header']) ?>"></div>
|
||||
|
|
@ -317,11 +393,80 @@ $adminBase = '';
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Design & Branding -->
|
||||
<div id="tab-branding" class="tab-content">
|
||||
<h2 style="margin-bottom: 8px; color: var(--text-primary);"><i class="fas fa-palette"></i> <?= __('settings_tab_branding') ?></h2>
|
||||
<p style="color: var(--text-muted); font-size: 14px; margin-bottom: 24px;"><?= __('settings_branding_intro') ?></p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="form_type" value="branding">
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label><?= __('settings_brand_accent') ?></label>
|
||||
<div class="color-field">
|
||||
<input type="color" class="color-swatch" data-target="brand_accent" value="<?= htmlspecialchars($cs['brand_accent']) ?>">
|
||||
<input type="text" id="brand_accent" name="brand_accent" value="<?= htmlspecialchars($cs['brand_accent']) ?>" placeholder="<?= Ui::DEFAULT_ACCENT ?>">
|
||||
</div>
|
||||
<div class="help-text"><?= __('settings_brand_accent_hint') ?></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><?= __('settings_brand_accent_dark') ?></label>
|
||||
<div class="color-field">
|
||||
<input type="color" class="color-swatch" data-target="brand_accent_dark" value="<?= htmlspecialchars($cs['brand_accent_dark']) ?>">
|
||||
<input type="text" id="brand_accent_dark" name="brand_accent_dark" value="<?= htmlspecialchars($cs['brand_accent_dark']) ?>" placeholder="<?= Ui::DEFAULT_ACCENT_DARK ?>">
|
||||
</div>
|
||||
<div class="help-text"><?= __('settings_brand_accent_dark_hint') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label><?= __('settings_brand_gradient_from') ?></label>
|
||||
<div class="color-field">
|
||||
<input type="color" class="color-swatch" data-target="brand_gradient_from" value="<?= htmlspecialchars($cs['brand_gradient_from']) ?>">
|
||||
<input type="text" id="brand_gradient_from" name="brand_gradient_from" value="<?= htmlspecialchars($cs['brand_gradient_from']) ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><?= __('settings_brand_gradient_to') ?></label>
|
||||
<div class="color-field">
|
||||
<input type="color" class="color-swatch" data-target="brand_gradient_to" value="<?= htmlspecialchars($cs['brand_gradient_to']) ?>">
|
||||
<input type="text" id="brand_gradient_to" name="brand_gradient_to" value="<?= htmlspecialchars($cs['brand_gradient_to']) ?>">
|
||||
</div>
|
||||
<div class="help-text"><?= __('settings_brand_gradient_hint') ?></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><?= __('settings_brand_radius') ?></label>
|
||||
<select id="brand_radius" name="brand_radius">
|
||||
<?php foreach ([6 => __('settings_brand_radius_sharp'), 14 => __('settings_brand_radius_default'), 20 => __('settings_brand_radius_round')] as $value => $label): ?>
|
||||
<option value="<?= $value ?>" <?= (int)$cs['brand_radius'] === $value ? 'selected' : '' ?>><?= htmlspecialchars($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="brandPreview" style="margin-top: 8px;">
|
||||
<h3><?= __('settings_brand_preview') ?></h3>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin-top:14px;">
|
||||
<span class="brand-mark" style="width:38px;height:38px;"><i class="fas fa-wifi"></i></span>
|
||||
<button type="button" class="btn btn-primary"><i class="fas fa-ticket"></i> <?= __('voucher_create_btn') ?></button>
|
||||
<button type="button" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
|
||||
<span class="badge badge-success"><?= __('status_valid') ?></span>
|
||||
<span class="chip"><?= __('nav_vouchers') ?></span>
|
||||
<a href="#" onclick="return false;"><?= __('settings_brand_link') ?></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="save_settings" class="btn btn-primary" style="margin-top:16px;"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Login-Seite -->
|
||||
<div id="tab-login" class="tab-content">
|
||||
<h2 style="margin-bottom: 8px; color: var(--text-primary);"><i class="fas fa-right-to-bracket"></i> <?= __('settings_tab_login') ?></h2>
|
||||
<p style="color: var(--text-muted); font-size: 14px; margin-bottom: 24px;"><?= __('settings_login_intro') ?></p>
|
||||
<form method="post">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="form_type" value="login">
|
||||
|
||||
|
|
@ -336,11 +481,7 @@ $adminBase = '';
|
|||
<input type="text" name="login_brand_name" value="<?= htmlspecialchars($cs['login_brand_name']) ?>" placeholder="<?= htmlspecialchars($cs['app_title']) ?>">
|
||||
<div class="help-text"><?= __('settings_login_brand_hint') ?></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><?= __('settings_login_logo') ?></label>
|
||||
<input type="url" name="login_logo_url" value="<?= htmlspecialchars($cs['login_logo_url']) ?>" placeholder="https://example.com/logo.svg">
|
||||
<div class="help-text"><?= __('settings_login_logo_hint') ?></div>
|
||||
</div>
|
||||
<?php imageField('login_logo_url', __('settings_login_logo'), $cs['login_logo_url'], __('settings_login_logo_hint')); ?>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
|
@ -365,11 +506,7 @@ $adminBase = '';
|
|||
|
||||
<hr class="section-divider">
|
||||
|
||||
<div class="form-group">
|
||||
<label><?= __('settings_login_bg_image') ?></label>
|
||||
<input type="url" name="login_bg_image" value="<?= htmlspecialchars($cs['login_bg_image']) ?>" placeholder="https://example.com/empfang.jpg">
|
||||
<div class="help-text"><?= __('settings_login_bg_image_hint') ?></div>
|
||||
</div>
|
||||
<?php imageField('login_bg_image', __('settings_login_bg_image'), $cs['login_bg_image'], __('settings_login_bg_image_hint')); ?>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label><?= __('settings_login_bg_from') ?></label>
|
||||
|
|
@ -574,11 +711,37 @@ document.querySelectorAll('.tab-button').forEach(btn => {
|
|||
});
|
||||
});
|
||||
|
||||
// Branding-Vorschau live faerben
|
||||
function updateBrandPreview() {
|
||||
const preview = document.getElementById('brandPreview');
|
||||
if (!preview) return;
|
||||
const accent = (document.getElementById('brand_accent') || {}).value || '';
|
||||
const from = (document.getElementById('brand_gradient_from') || {}).value || '';
|
||||
const to = (document.getElementById('brand_gradient_to') || {}).value || '';
|
||||
const radius = (document.getElementById('brand_radius') || {}).value || '14';
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(accent)) {
|
||||
preview.style.setProperty('--accent', accent);
|
||||
preview.style.setProperty('--accent-hover', `color-mix(in srgb, ${accent} 84%, #000)`);
|
||||
preview.style.setProperty('--accent-soft', `color-mix(in srgb, ${accent} 12%, #fff)`);
|
||||
preview.style.setProperty('--accent-border', `color-mix(in srgb, ${accent} 32%, #fff)`);
|
||||
}
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(from) && /^#[0-9a-fA-F]{6}$/.test(to)) {
|
||||
preview.style.setProperty('--brand-gradient', `linear-gradient(135deg, ${from} 0%, ${to} 100%)`);
|
||||
}
|
||||
preview.style.setProperty('--r-lg', radius + 'px');
|
||||
}
|
||||
['brand_accent', 'brand_gradient_from', 'brand_gradient_to', 'brand_radius'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.addEventListener('input', updateBrandPreview);
|
||||
if (el) el.addEventListener('change', updateBrandPreview);
|
||||
});
|
||||
updateBrandPreview();
|
||||
|
||||
// Farbwähler und Hex-Feld synchron halten
|
||||
document.querySelectorAll('.color-swatch').forEach(swatch => {
|
||||
const field = document.getElementById(swatch.dataset.target);
|
||||
if (!field) return;
|
||||
swatch.addEventListener('input', () => { field.value = swatch.value; });
|
||||
swatch.addEventListener('input', () => { field.value = swatch.value; updateBrandPreview(); });
|
||||
field.addEventListener('input', () => {
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(field.value.trim())) swatch.value = field.value.trim();
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue