From 30d0ce3a235a8f1eb36ee6cef6d5d5e2bcd995c2 Mon Sep 17 00:00:00 2001 From: Friederich Loheide Date: Wed, 23 Sep 2026 06:27:30 +0000 Subject: [PATCH] Branding: Farben systemweit einstellbar + Bild-Upload statt nur URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- admin/settings.php | 201 +++++++++++++++++++++++++++++++++++++++----- assets/global.css | 39 ++++++++- forgot_password.php | 2 +- includes/Ui.php | 20 ++++- includes/Upload.php | 133 +++++++++++++++++++++++++++++ index.php | 2 +- lang/de.php | 26 +++++- lang/en.php | 26 +++++- login.php | 6 +- login_simple.php | 2 +- reset_password.php | 2 +- uploads/.gitignore | 4 + uploads/.htaccess | 6 ++ 13 files changed, 437 insertions(+), 32 deletions(-) create mode 100644 includes/Upload.php create mode 100644 uploads/.gitignore create mode 100644 uploads/.htaccess diff --git a/admin/settings.php b/admin/settings.php index 42a4b98..1a50778 100644 --- a/admin/settings.php +++ b/admin/settings.php @@ -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', '

{APP_TITLE}

WLAN Code

{VOUCHER_CODE}

Gültig bis: {EXPIRY_DATE} {EXPIRY_TIME}

Site: {SITE_NAME}

Geräte: {MAX_USES}


{INSTRUCTIONS}
'), + '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, '../'); + ?> +
+ +
+
+ + + + + +
+
+ + + + + +
+
+
+
+ @@ -253,6 +328,7 @@ $adminBase = '';
+ @@ -265,13 +341,13 @@ $adminBase = '';

-
+
-
-
+ +

@@ -317,11 +393,80 @@ $adminBase = '';
+ +
+

+

+
+ + + +
+
+ +
+ + +
+
+
+
+ +
+ + +
+
+
+
+ +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ + +
+
+ +
+

+
+ + + + + + +
+
+ + +
+
+

-
+ @@ -336,11 +481,7 @@ $adminBase = '';
-
- - -
-
+

@@ -365,11 +506,7 @@ $adminBase = '';
-
- - -
-
+
@@ -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(); }); diff --git a/assets/global.css b/assets/global.css index d54c3e2..7ef44d6 100644 --- a/assets/global.css +++ b/assets/global.css @@ -562,7 +562,28 @@ input:focus, select:focus, textarea:focus, .input:focus { } input:disabled, select:disabled, textarea:disabled { background: var(--bg-hover); color: var(--text-muted); cursor: not-allowed; } input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); width: 16px; height: 16px; cursor: pointer; } -input[type="file"] { font-size: 13px; color: var(--text-secondary); } +input[type="file"] { + width: 100%; + font-size: 13px; + color: var(--text-secondary); +} +input[type="file"]::file-selector-button { + margin-right: 10px; + padding: 7px 13px; + border: 1px solid var(--border-color); + border-radius: var(--r-sm); + background: var(--bg-card); + color: var(--text-primary); + font: inherit; + font-size: 12.5px; + font-weight: 550; + cursor: pointer; + transition: background-color .15s, border-color .15s; +} +input[type="file"]::file-selector-button:hover { + background: var(--bg-hover); + border-color: var(--border-hover); +} .chk, label.chk { display: flex; align-items: center; gap: 10px; @@ -1060,6 +1081,22 @@ input[type="file"] { font-size: 13px; color: var(--text-secondary); } border-radius: var(--r-xl); box-shadow: var(--shadow-xl); } +/* Bildfeld: Vorschau, Upload und URL nebeneinander */ +.image-field { display: flex; gap: 14px; align-items: flex-start; } +.image-preview { + width: 84px; height: 64px; flex-shrink: 0; + display: flex; align-items: center; justify-content: center; + padding: 6px; + background: var(--bg-subtle); + border: 1px solid var(--border-color); + border-radius: var(--r-md); + color: var(--text-muted); + overflow: hidden; +} +.image-preview img { max-width: 100%; max-height: 100%; object-fit: contain; } +.image-field-controls { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; } +.image-field-controls .chk { margin: 0; font-size: 12.5px; color: var(--text-secondary); } + /* Farbwähler mit Hex-Eingabe */ .color-field { display: flex; align-items: center; gap: 8px; } .color-field input[type="color"] { diff --git a/forgot_password.php b/forgot_password.php index 53526db..9a19fec 100644 --- a/forgot_password.php +++ b/forgot_password.php @@ -89,7 +89,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- +

diff --git a/includes/Ui.php b/includes/Ui.php index 5517aaa..8f3b3d0 100644 --- a/includes/Ui.php +++ b/includes/Ui.php @@ -116,6 +116,24 @@ class Ui . ''; } + /** + * URL eines Bildes aus den Einstellungen. + * Hochgeladene Dateien liegen relativ zur Projektwurzel (uploads/…), + * externe Adressen bleiben unveraendert. + */ + public static function mediaUrl(string $value, string $base = ''): string + { + $value = trim($value); + if ($value === '') { + return ''; + } + if (preg_match('#^(https?:)?//#i', $value) || strncmp($value, 'data:', 5) === 0 || $value[0] === '/') { + return $value; + } + + return $base . $value; + } + /** * Kompletter Standard-Kopf: Favicon, Schrift, Icons, Design-System, * Theme-Bootstrap und Branding. @@ -124,7 +142,7 @@ class Ui { $out = []; - $favicon = $db ? trim((string)$db->getSetting('favicon_url', '')) : ''; + $favicon = $db ? self::mediaUrl((string)$db->getSetting('favicon_url', ''), $base) : ''; if ($favicon !== '') { $out[] = ''; } diff --git a/includes/Upload.php b/includes/Upload.php new file mode 100644 index 0000000..88c2271 --- /dev/null +++ b/includes/Upload.php @@ -0,0 +1,133 @@ + ['png', 'jpg', 'jpeg', 'webp', 'gif', 'svg'], + 'favicon' => ['ico', 'png', 'svg'], + ]; + + private static function dir(): string + { + return dirname(__DIR__) . '/uploads'; + } + + /** Legt das Upload-Verzeichnis inkl. Schutzdatei an. */ + public static function ensureDir(): bool + { + $dir = self::dir(); + if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) { + return false; + } + + $htaccess = $dir . '/.htaccess'; + if (!file_exists($htaccess)) { + @file_put_contents($htaccess, "php_flag engine off\nOptions -ExecCGI\n\n Require all denied\n\n"); + } + + return is_writable($dir); + } + + /** Ist der Pfad eine von uns gespeicherte Datei? */ + public static function isLocal(string $path): bool + { + return $path !== '' && strncmp($path, 'uploads/', 8) === 0 && strpos($path, '..') === false; + } + + /** Loescht eine zuvor hochgeladene Datei (externe URLs bleiben unberuehrt). */ + public static function delete(string $path): void + { + if (!self::isLocal($path)) { + return; + } + $file = dirname(__DIR__) . '/' . $path; + if (is_file($file)) { + @unlink($file); + } + } + + /** + * Nimmt einen Upload entgegen und gibt den relativen Pfad zurueck. + * + * @param array $file Eintrag aus $_FILES + * @param string $kind 'image' oder 'favicon' + * @throws RuntimeException bei ungueltigen Dateien + */ + public static function store(array $file, string $kind = 'image'): string + { + if (!isset($file['error']) || $file['error'] === UPLOAD_ERR_NO_FILE) { + return ''; + } + if ($file['error'] !== UPLOAD_ERR_OK) { + throw new RuntimeException(__('upload_error_generic')); + } + if (!is_uploaded_file($file['tmp_name'])) { + throw new RuntimeException(__('upload_error_generic')); + } + if ($file['size'] > self::MAX_BYTES) { + throw new RuntimeException(__('upload_error_size')); + } + + $allowed = self::ALLOWED[$kind] ?? self::ALLOWED['image']; + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if ($ext === 'jpeg') { + $ext = 'jpg'; + } + if (!in_array($ext, $allowed, true)) { + throw new RuntimeException(__('upload_error_type')); + } + + $data = (string)file_get_contents($file['tmp_name']); + + if ($ext === 'svg') { + $data = self::sanitizeSvg($data); + } elseif ($ext !== 'ico') { + // Raster: muss als Bild lesbar sein + if (@getimagesize($file['tmp_name']) === false) { + throw new RuntimeException(__('upload_error_type')); + } + } + + if (!self::ensureDir()) { + throw new RuntimeException(__('upload_error_dir')); + } + + $name = bin2hex(random_bytes(8)) . '.' . $ext; + $dest = self::dir() . '/' . $name; + if (file_put_contents($dest, $data) === false) { + throw new RuntimeException(__('upload_error_dir')); + } + @chmod($dest, 0644); + + return 'uploads/' . $name; + } + + /** + * Entfernt aktive Inhalte aus SVG-Dateien (Skripte, Event-Handler, + * externe Verweise). Lieber eine Grafik verlieren als eine XSS-Luecke. + */ + private static function sanitizeSvg(string $svg): string + { + if (stripos($svg, ']*>.*?<\s*/\s*\1\s*>#is', '', $svg); + $svg = preg_replace('#<\s*(script|foreignObject|iframe|embed|object|animate|set)\b[^>]*/?>#i', '', $svg); + $svg = preg_replace('#\son[a-z]+\s*=\s*"[^"]*"#i', '', $svg); + $svg = preg_replace("#\son[a-z]+\s*=\s*'[^']*'#i", '', $svg); + $svg = preg_replace('#(href|xlink:href)\s*=\s*([\'"])\s*(javascript|data):[^\'"]*\2#i', '', $svg); + $svg = preg_replace('#]*>#i', '', $svg); + + return (string)$svg; + } +} diff --git a/index.php b/index.php index b873249..2cbcded 100644 --- a/index.php +++ b/index.php @@ -346,7 +346,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
- + diff --git a/lang/de.php b/lang/de.php index af979e8..efa47ec 100644 --- a/lang/de.php +++ b/lang/de.php @@ -220,6 +220,28 @@ return [ // Settings 'settings_title' => 'Einstellungen', 'settings_subtitle' => 'System-Konfiguration und Personalisierung', + 'settings_tab_branding' => 'Design', + 'settings_branding_intro' => 'Farben und Formen der gesamten Oberfläche – Frontend wie Administration.', + 'settings_brand_accent' => 'Akzentfarbe (hell)', + 'settings_brand_accent_hint' => 'Buttons, aktive Navigation, Links. Abgeleitete Töne werden automatisch berechnet.', + 'settings_brand_accent_dark' => 'Akzentfarbe (Dark Mode)', + 'settings_brand_accent_dark_hint' => 'Im Dark Mode meist eine hellere Variante der Grundfarbe.', + 'settings_brand_gradient_from'=> 'Markenverlauf: Start', + 'settings_brand_gradient_to' => 'Markenverlauf: Ende', + 'settings_brand_gradient_hint'=> 'Für Logo-Kachel, Avatare und die Voucher-Karte.', + 'settings_brand_radius' => 'Eckenradius', + 'settings_brand_radius_sharp' => 'Kantig', + 'settings_brand_radius_default'=> 'Standard', + 'settings_brand_radius_round' => 'Rund', + 'settings_brand_preview' => 'Vorschau', + 'settings_brand_link' => 'Beispiel-Link', + 'settings_image_url_placeholder' => 'https://… oder Datei hochladen', + 'settings_image_remove' => 'Bild entfernen', + 'settings_upload_hint' => 'PNG, JPG, WEBP, GIF oder SVG – maximal 3 MB.', + 'upload_error_generic' => 'Die Datei konnte nicht hochgeladen werden.', + 'upload_error_size' => 'Die Datei ist zu groß (maximal 3 MB).', + 'upload_error_type' => 'Dieser Dateityp wird nicht unterstützt.', + 'upload_error_dir' => 'Der Ordner uploads/ ist nicht beschreibbar.', 'settings_tab_login' => 'Login-Seite', 'settings_login_intro' => 'Aussehen und Texte der Anmeldeseite. Leere Felder verwenden die Standardwerte.', 'settings_login_panel' => 'Linke Bildspalte (Split-Screen) anzeigen', @@ -249,8 +271,8 @@ return [ 'settings_tab_password' => 'Passwort', 'settings_saved' => 'Einstellungen erfolgreich gespeichert!', 'settings_app_title' => 'Anwendungs-Titel *', - 'settings_logo_url' => 'Logo-URL', - 'settings_favicon_url' => 'Favicon-URL', + 'settings_logo_url' => 'Logo', + 'settings_favicon_url' => 'Favicon', 'settings_favicon_hint' => 'Icon im Browser-Tab (.ico, .png, .svg)', 'settings_instr_header' => 'Anleitung - Überschrift', 'settings_instr_text' => 'Anleitung - Text', diff --git a/lang/en.php b/lang/en.php index 33c423c..be5f5b0 100644 --- a/lang/en.php +++ b/lang/en.php @@ -220,6 +220,28 @@ return [ // Settings 'settings_title' => 'Settings', 'settings_subtitle' => 'System configuration and customization', + 'settings_tab_branding' => 'Design', + 'settings_branding_intro' => 'Colours and shapes for the whole interface – front end and administration.', + 'settings_brand_accent' => 'Accent colour (light)', + 'settings_brand_accent_hint' => 'Buttons, active navigation, links. Derived shades are calculated automatically.', + 'settings_brand_accent_dark' => 'Accent colour (dark mode)', + 'settings_brand_accent_dark_hint' => 'Usually a lighter variant of the base colour for dark mode.', + 'settings_brand_gradient_from'=> 'Brand gradient: start', + 'settings_brand_gradient_to' => 'Brand gradient: end', + 'settings_brand_gradient_hint'=> 'Used for the logo tile, avatars and the voucher card.', + 'settings_brand_radius' => 'Corner radius', + 'settings_brand_radius_sharp' => 'Sharp', + 'settings_brand_radius_default'=> 'Default', + 'settings_brand_radius_round' => 'Round', + 'settings_brand_preview' => 'Preview', + 'settings_brand_link' => 'Example link', + 'settings_image_url_placeholder' => 'https://… or upload a file', + 'settings_image_remove' => 'Remove image', + 'settings_upload_hint' => 'PNG, JPG, WEBP, GIF or SVG – 3 MB maximum.', + 'upload_error_generic' => 'The file could not be uploaded.', + 'upload_error_size' => 'The file is too large (3 MB maximum).', + 'upload_error_type' => 'This file type is not supported.', + 'upload_error_dir' => 'The uploads/ directory is not writable.', 'settings_tab_login' => 'Login page', 'settings_login_intro' => 'Appearance and wording of the sign-in page. Empty fields fall back to the defaults.', 'settings_login_panel' => 'Show left image column (split screen)', @@ -249,8 +271,8 @@ return [ 'settings_tab_password' => 'Password', 'settings_saved' => 'Settings saved successfully!', 'settings_app_title' => 'Application Title *', - 'settings_logo_url' => 'Logo URL', - 'settings_favicon_url' => 'Favicon URL', + 'settings_logo_url' => 'Logo', + 'settings_favicon_url' => 'Favicon', 'settings_favicon_hint' => 'Browser tab icon (.ico, .png, .svg)', 'settings_instr_header' => 'Instructions - Headline', 'settings_instr_text' => 'Instructions - Text', diff --git a/login.php b/login.php index e890f95..3531633 100644 --- a/login.php +++ b/login.php @@ -146,7 +146,7 @@ try { . ';--login-to:' . htmlspecialchars($loginBgTo, ENT_QUOTES) . ';--login-overlay:' . ($loginOverlay / 100); if ($loginBgImage !== '') { - $visualStyle .= ";--login-image:url('" . htmlspecialchars($loginBgImage, ENT_QUOTES) . "')"; + $visualStyle .= ";--login-image:url('" . htmlspecialchars(Ui::mediaUrl($loginBgImage), ENT_QUOTES) . "')"; } } catch (Exception $e) { @@ -167,7 +167,7 @@ try {
- + @@ -204,7 +204,7 @@ try {