From d49c326c901a5f6ab1ee196a24901eeb2feb3cf1 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Wed, 14 Jan 2026 01:50:50 +0100 Subject: [PATCH] ssdfdsf --- partials/landingpage/accountsetup/bridge.php | 27 ++++ .../assets/fonts/KidsHandwriting-Regular.ttf | Bin 0 -> 15584 bytes .../assets/fonts/KidsHandwriting-Regular.woff | Bin 0 -> 10508 bytes .../fonts/KidsHandwriting-Regular.woff2 | Bin 0 -> 8936 bytes public/assets/js/bridge-setup-page.js | 27 +++- public/assets/js/bridge/blocks-api.js | 6 +- public/editor/bridge-core.js | 149 ++++++++++++++---- public/editor/editor-core.php | 34 ++++ src/ApiKernel.php | 137 ++++++++++++++++ 9 files changed, 343 insertions(+), 37 deletions(-) create mode 100644 public/assets/fonts/KidsHandwriting-Regular.ttf create mode 100644 public/assets/fonts/KidsHandwriting-Regular.woff create mode 100644 public/assets/fonts/KidsHandwriting-Regular.woff2 diff --git a/partials/landingpage/accountsetup/bridge.php b/partials/landingpage/accountsetup/bridge.php index 648f3b7..d046eab 100644 --- a/partials/landingpage/accountsetup/bridge.php +++ b/partials/landingpage/accountsetup/bridge.php @@ -116,6 +116,33 @@ require dirname(__DIR__) . '/../structure/layout_start.php';
Noch nicht gespeichert.
+ +
+

Schriftarten (optional)

+

+ Wenn du eigene Fonts vom Hoster nutzen willst, trage hier entweder einen lokalen Schriftarten-Ordner + (inkl. oeffentlicher URL) ein oder gib direkte Font-URLs an. Das System erzeugt automatisch @font-face + und stellt die Fonts im Editor bereit. +

+
+ + + +

+ Hinweis: woff2/woff ist empfohlen, ttf/otf funktioniert meist, wird aber nicht von allen Mail-Clients geladen. +

+
+
diff --git a/public/assets/fonts/KidsHandwriting-Regular.ttf b/public/assets/fonts/KidsHandwriting-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a9c8ba98e2f08180f89f087d3c80b89e9cd8f701 GIT binary patch literal 15584 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
i|E;#Z^;32{ZyP9acpR%Tb=a{? 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 P5;>Xx=xM-xRwe!)6A%2- literal 0 HcmV?d00001 diff --git a/public/assets/fonts/KidsHandwriting-Regular.woff2 b/public/assets/fonts/KidsHandwriting-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..2bb56ad54a50e2f02ead9975c70160ef32e69460 GIT binary patch literal 8936 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 C1{@;* literal 0 HcmV?d00001 diff --git a/public/assets/js/bridge-setup-page.js b/public/assets/js/bridge-setup-page.js index fe6abcd..d9a8f98 100644 --- a/public/assets/js/bridge-setup-page.js +++ b/public/assets/js/bridge-setup-page.js @@ -86,6 +86,11 @@ function defaultSetup() { password_key: '', charset_key: '', }, + fonts: { + dir: '', + url_base: '', + urls: '', + }, }; } @@ -148,6 +153,19 @@ function fillForm(setup, options = {}) { if (input) input.value = value; }); } + + const fontFields = document.getElementById('bridgeFontsForm'); + if (fontFields) { + const fontMap = { + fonts_dir: data.fonts.dir || '', + fonts_url_base: data.fonts.url_base || '', + fonts_urls: data.fonts.urls || '', + }; + Object.entries(fontMap).forEach(([name, value]) => { + const input = fontFields.querySelector(`[name="${name}"]`); + if (input) input.value = value; + }); + } } function applyModeVisibility(mode) { @@ -224,6 +242,9 @@ async function submitBridgeSetup(ev) { config_user_key: form.config_user_key?.value.trim() || '', config_password_key: form.config_password_key?.value.trim() || '', config_charset_key: form.config_charset_key?.value.trim() || '', + fonts_dir: document.querySelector('[name="fonts_dir"]')?.value.trim() || '', + fonts_url_base: document.querySelector('[name="fonts_url_base"]')?.value.trim() || '', + fonts_urls: document.querySelector('[name="fonts_urls"]')?.value || '', }; try { @@ -324,5 +345,9 @@ function normalizeSetupInput(input) { direct.port = Number(direct.port || 3306) || 3306; direct.charset = direct.charset || 'utf8mb4'; const config = { ...base.config, ...(input.config || {}) }; - return { tables, mode: validMode, direct, config }; + const fonts = { ...base.fonts, ...(input.fonts || {}) }; + fonts.dir = String(fonts.dir || '').trim(); + fonts.url_base = String(fonts.url_base || '').trim(); + fonts.urls = String(fonts.urls || ''); + return { tables, mode: validMode, direct, config, fonts }; } diff --git a/public/assets/js/bridge/blocks-api.js b/public/assets/js/bridge/blocks-api.js index 32c5488..7e81510 100644 --- a/public/assets/js/bridge/blocks-api.js +++ b/public/assets/js/bridge/blocks-api.js @@ -464,7 +464,11 @@ } // 1. Daten extrahieren - const htmlContent = editor.getHtml() + ''; + const fontCss = (B && typeof B.RTE_FONT_FACE_CSS === 'string' && B.RTE_FONT_FACE_CSS.trim()) + ? B.RTE_FONT_FACE_CSS.trim() + : ''; + const cssPayload = (fontCss ? fontCss + '\n' : '') + editor.getCss(); + const htmlContent = editor.getHtml() + ''; // 2. KRITISCH: Holt die JSON-Repräsentation des Editors let jsonProjectDataRaw = ''; try { diff --git a/public/editor/bridge-core.js b/public/editor/bridge-core.js index febbb96..6082a9d 100644 --- a/public/editor/bridge-core.js +++ b/public/editor/bridge-core.js @@ -263,7 +263,7 @@ autosave: false, }; - const execRteCommand = (rte, cmd, value) => { + const execRteCommand = (rte, cmd, value, docOverride) => { try { if (rte && typeof rte.exec === 'function') { rte.exec(cmd, value); @@ -271,9 +271,16 @@ } } catch {} try { - const ok = document.execCommand(cmd, false, value); + const doc = docOverride + || rte?.doc + || rte?.el?.ownerDocument + || document; + if (rte?.el && typeof rte.el.focus === 'function') { + rte.el.focus(); + } + const ok = doc.execCommand(cmd, false, value); if (ok === false && cmd === 'insertText') { - document.execCommand('insertHTML', false, String(value || '').replace(//g, '>')); + doc.execCommand('insertHTML', false, String(value || '').replace(//g, '>')); } return true; } catch {} @@ -344,10 +351,10 @@ || ''; content.innerHTML = initialHtml; - const addButton = (label, title, cmd, valueGetter) => { + const addButton = (labelHtml, title, cmd, valueGetter) => { const btn = doc.createElement('button'); btn.type = 'button'; - btn.textContent = label; + btn.innerHTML = labelHtml; btn.title = title; btn.style.padding = '4px 8px'; btn.style.border = '1px solid #cbd5f5'; @@ -359,7 +366,7 @@ const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter; if (value === null || value === undefined) return; if (cmd === 'createLink' && !value) return; - execRteCommand(null, cmd, value); + execRteCommand(null, cmd, value, content.ownerDocument); }); toolbar.appendChild(btn); }; @@ -393,32 +400,41 @@ } }; - addButton('B', 'Fett', 'bold'); - addButton('I', 'Kursiv', 'italic'); - addButton('U', 'Unterstrichen', 'underline'); - addButton('S', 'Durchgestrichen', 'strikethrough'); + const icon = (path) => ``; + addButton('B', 'Fett', 'bold'); + addButton('I', 'Kursiv', 'italic'); + addButton('U', 'Unterstrichen', 'underline'); + addButton('S', 'Durchgestrichen', 'strikethrough'); + addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList'); + addButton(icon('M4 7h14v2H4zM4 11h14v2H4zM4 15h14v2H4z') + '1.', 'Liste (geordnet)', 'insertOrderedList'); + addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h12v2H4z'), 'Linksbundig', 'justifyLeft'); + addButton(icon('M5 7h14v2H5zM4 11h16v2H4zM5 15h14v2H5z'), 'Zentriert', 'justifyCenter'); + addButton(icon('M10 7h10v2H10zM4 11h16v2H4zM8 15h12v2H8z'), 'Rechtsbundig', 'justifyRight'); + addButton(icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull'); addButton('Link', 'Link einfuegen', 'createLink', () => prompt('Link-URL eingeben', 'https://')); addButton('Unlink', 'Link entfernen', 'unlink'); - addButton('UL', 'Liste (ungeordnet)', 'insertUnorderedList'); - addButton('OL', 'Liste (geordnet)', 'insertOrderedList'); - addButton('L', 'Linksbundig', 'justifyLeft'); - addButton('C', 'Zentriert', 'justifyCenter'); - addButton('R', 'Rechtsbundig', 'justifyRight'); - addButton('J', 'Blocksatz', 'justifyFull'); addButton('Sub', 'Tiefgestellt', 'subscript'); addButton('Sup', 'Hochgestellt', 'superscript'); addButton('Einr.', 'Einzug', 'indent'); addButton('Aus.', 'Ausruecken', 'outdent'); addButton('Clear', 'Formatierung entfernen', 'removeFormat'); + const fontOptions = (B.RTE_FONTS && Array.isArray(B.RTE_FONTS) && B.RTE_FONTS.length) + ? B.RTE_FONTS + : [ + { label: 'Arial', value: 'Arial, sans-serif' }, + { label: 'Calibri', value: 'Calibri, sans-serif' }, + { label: 'Cambria', value: 'Cambria, serif' }, + { label: 'Georgia', value: 'Georgia, serif' }, + { label: 'Tahoma', value: 'Tahoma, sans-serif' }, + { label: 'Times New Roman', value: 'Times New Roman, serif' }, + { label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' }, + { label: 'Verdana', value: 'Verdana, sans-serif' }, + ]; addSelect([ { label: 'Schriftart', value: '' }, - { label: 'Arial', value: 'Arial, sans-serif' }, - { label: 'Georgia', value: 'Georgia, serif' }, - { label: 'Tahoma', value: 'Tahoma, sans-serif' }, - { label: 'Times New Roman', value: 'Times New Roman, serif' }, - { label: 'Verdana', value: 'Verdana, sans-serif' }, - ], 'Schriftart', (value) => execRteCommand(null, 'fontName', value)); + ...fontOptions, + ], 'Schriftart', (value) => execRteCommand(null, 'fontName', value, content.ownerDocument)); addSelect([ { label: 'Groesse', value: '' }, @@ -429,7 +445,7 @@ { label: '18px', value: '5' }, { label: '24px', value: '6' }, { label: '32px', value: '7' }, - ], 'Schriftgroesse', (value) => execRteCommand(null, 'fontSize', value)); + ], 'Schriftgroesse', (value) => execRteCommand(null, 'fontSize', value, content.ownerDocument)); const emojiBtn = doc.createElement('button'); emojiBtn.type = 'button'; @@ -498,6 +514,19 @@ const setupRichTextEditor = (editor) => { if (!editor || !editor.RichTextEditor) return; const rte = editor.RichTextEditor; + const icon = (path) => ``; + const resolveFontOptions = () => (B.RTE_FONTS && Array.isArray(B.RTE_FONTS) && B.RTE_FONTS.length) + ? B.RTE_FONTS + : [ + { label: 'Arial', value: 'Arial, sans-serif' }, + { label: 'Calibri', value: 'Calibri, sans-serif' }, + { label: 'Cambria', value: 'Cambria, serif' }, + { label: 'Georgia', value: 'Georgia, serif' }, + { label: 'Tahoma', value: 'Tahoma, sans-serif' }, + { label: 'Times New Roman', value: 'Times New Roman, serif' }, + { label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' }, + { label: 'Verdana', value: 'Verdana, sans-serif' }, + ]; const addAction = (name, icon, title, command, valueGetter) => { if (rte.get && rte.get(name)) return; @@ -515,14 +544,18 @@ }); }; - addAction('bridge-align-left', 'L', 'Linksbundig', 'justifyLeft'); - addAction('bridge-align-center', 'C', 'Zentriert', 'justifyCenter'); - addAction('bridge-align-right', 'R', 'Rechtsbundig', 'justifyRight'); - addAction('bridge-align-justify', 'J', 'Blocksatz', 'justifyFull'); - addAction('bridge-ul', 'UL', 'Liste (ungeordnet)', 'insertUnorderedList'); - addAction('bridge-ol', 'OL', 'Liste (geordnet)', 'insertOrderedList'); + addAction('bridge-align-left', icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h12v2H4z'), 'Linksbundig', 'justifyLeft'); + addAction('bridge-align-center', icon('M5 7h14v2H5zM4 11h16v2H4zM5 15h14v2H5z'), 'Zentriert', 'justifyCenter'); + addAction('bridge-align-right', icon('M10 7h10v2H10zM4 11h16v2H4zM8 15h12v2H8z'), 'Rechtsbundig', 'justifyRight'); + addAction('bridge-align-justify', icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull'); + addAction('bridge-ul', icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList'); + addAction('bridge-ol', icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Liste (geordnet)', 'insertOrderedList'); addAction('bridge-emoji', ':-)', 'Emoticon einfuegen', 'insertText', () => prompt('Emoticon eingeben', ':)')); - addAction('bridge-font-family', 'F', 'Schriftart', 'fontName', () => prompt('Schriftart (z.B. Arial, Georgia)', 'Arial')); + addAction('bridge-font-family', 'F', 'Schriftart', 'fontName', () => { + const fonts = resolveFontOptions(); + const example = fonts.map((f) => f.label).slice(0, 5).join(', '); + return prompt(`Schriftart (z.B. ${example})`, fonts[0]?.label || 'Arial'); + }); addAction('bridge-font-size', 'Px', 'Schriftgroesse', 'fontSize', () => { const raw = prompt('Schriftgroesse in px (10-32)', '14'); const val = Number(raw || 14); @@ -542,11 +575,16 @@ }); return best.cmd; }); - addAction('bridge-open-richtext', 'RTE', 'Richtext Editor', 'execCommand', () => { - const component = editor.getSelected && editor.getSelected(); - openRichTextModal(editor, component); - return null; - }); + if (!(rte.get && rte.get('bridge-open-richtext'))) { + rte.add('bridge-open-richtext', { + icon: 'RTE', + attributes: { title: 'Richtext Editor' }, + result: () => { + const component = editor.getSelected && editor.getSelected(); + openRichTextModal(editor, component); + }, + }); + } if (!editor.Commands.get('bridge-open-richtext')) { editor.Commands.add('bridge-open-richtext', { @@ -576,6 +614,46 @@ editor.on('component:add', (model) => ensureTextToolbarButton(editor, model)); }; + const loadDynamicFonts = async () => { + try { + const base = B.API_KERNEL_URL || '/api.php'; + const sep = base.includes('?') ? '&' : '?'; + const res = await fetch(`${base}${sep}action=account.fonts.list`, { credentials: 'include' }); + if (!res.ok) return; + const data = await res.json(); + if (!data || !data.ok) return; + const incoming = Array.isArray(data.fonts) ? data.fonts : []; + if (incoming.length) { + const merged = []; + const seen = new Set(); + const addFont = (item) => { + if (!item || !item.label || !item.value) return; + const key = String(item.label).toLowerCase(); + if (seen.has(key)) return; + seen.add(key); + merged.push({ label: String(item.label), value: String(item.value) }); + }; + (B.RTE_FONTS || []).forEach(addFont); + incoming.forEach(addFont); + B.RTE_FONTS = merged; + } + if (typeof data.font_face_css === 'string' && data.font_face_css.trim()) { + const existing = typeof B.RTE_FONT_FACE_CSS === 'string' ? B.RTE_FONT_FACE_CSS.trim() : ''; + B.RTE_FONT_FACE_CSS = existing + ? `${existing}\n${data.font_face_css.trim()}` + : data.font_face_css.trim(); + const styleId = 'bridge-font-faces'; + let styleEl = document.getElementById(styleId); + if (!styleEl) { + styleEl = document.createElement('style'); + styleEl.id = styleId; + document.head.appendChild(styleEl); + } + styleEl.textContent = B.RTE_FONT_FACE_CSS; + } + } catch {} + }; + var ed = grapesjs.init({ container: '#gjs', height: '100vh', @@ -623,6 +701,7 @@ } setupRichTextEditor(ed); + loadDynamicFonts(); // Entfernt: jegliche Blur/RTE-Handler, die Inhalte verändern. diff --git a/public/editor/editor-core.php b/public/editor/editor-core.php index 164805d..63d0246 100644 --- a/public/editor/editor-core.php +++ b/public/editor/editor-core.php @@ -2,6 +2,27 @@ $mode = strtolower($_GET['mode'] ?? 'templates'); $id = (int)($_GET['id'] ?? 0); $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time(); +$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; +$host = $_SERVER['HTTP_HOST'] ?? 'localhost'; +$appBaseUrl = rtrim($GLOBALS['app_base_url'] ?? ($scheme . '://' . $host), '/'); +$fontDir = __DIR__ . '/../assets/fonts'; +$fontBase = $appBaseUrl . '/assets/fonts'; +$customFontName = 'Kids Handwriting'; +$customFontFiles = [ + 'woff2' => 'KidsHandwriting-Regular.woff2', + 'woff' => 'KidsHandwriting-Regular.woff', + 'ttf' => 'KidsHandwriting-Regular.ttf', +]; +$fontSources = []; +foreach ($customFontFiles as $format => $file) { + if (is_file($fontDir . '/' . $file)) { + $fontSources[] = "url('" . htmlspecialchars($fontBase . '/' . $file, ENT_QUOTES) . "') format('{$format}')"; + } +} +$fontFaceCss = ''; +if ($fontSources) { + $fontFaceCss = "@font-face{font-family:'{$customFontName}';font-style:normal;font-weight:400;src:" . implode(',', $fontSources) . ";}"; +} ?> @@ -15,6 +36,7 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time(); .gjs-one-bg{background-color:#fff!important}.gjs-two-color{color:#0f172a!important} .gjs-three-bg{background-color:#f8fafc!important}.gjs-four-color{color:#334155!important} #badge{position:fixed;right:8px;top:8px;background:#eef2ff;color:#1e3a8a;border:1px solid #c7d2fe;border-radius:999px;padding:4px 10px;font:12px system-ui;z-index:2147483647;opacity:.9} + @@ -31,6 +53,18 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time(); window.BridgeParts.API_KERNEL_URL = window.BridgeParts.API_KERNEL_URL || '/api.php'; window.BridgeParts.API_BASE = window.BridgeParts.API_BASE || window.BridgeParts.API_KERNEL_URL; window.BridgeParts.STORAGE_URL_BASE = window.BridgeParts.STORAGE_URL_BASE || window.BridgeParts.API_BASE; + window.BridgeParts.RTE_FONTS = window.BridgeParts.RTE_FONTS || [ + { label: 'Kids Handwriting', value: "'Kids Handwriting', 'Comic Sans MS', cursive" }, + { label: 'Arial', value: 'Arial, sans-serif' }, + { label: 'Calibri', value: 'Calibri, sans-serif' }, + { label: 'Cambria', value: 'Cambria, serif' }, + { label: 'Georgia', value: 'Georgia, serif' }, + { label: 'Tahoma', value: 'Tahoma, sans-serif' }, + { label: 'Times New Roman', value: 'Times New Roman, serif' }, + { label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' }, + { label: 'Verdana', value: 'Verdana, sans-serif' }, + ]; + window.BridgeParts.RTE_FONT_FACE_CSS = window.BridgeParts.RTE_FONT_FACE_CSS || ; function logToParent(type, detail){ try{ parent.postMessage({source:'editor-core',type:type,detail:String(detail||'')},'*'); }catch(e){} } window.addEventListener('error', function(e){ diff --git a/src/ApiKernel.php b/src/ApiKernel.php index 49bf361..6442a77 100644 --- a/src/ApiKernel.php +++ b/src/ApiKernel.php @@ -1081,6 +1081,9 @@ class ApiKernel case 'account.bridge.test': $this->handleAccountBridgeTest(); break; + case 'account.fonts.list': + $this->handleAccountFontsList(); + break; case 'placeholders.status': $this->handlePlaceholderStatus(); break; @@ -2072,12 +2075,18 @@ class ApiKernel 'password_key' => (string)($this->in['config_password_key'] ?? ''), 'charset_key' => (string)($this->in['config_charset_key'] ?? ''), ]; + $fonts = [ + 'dir' => (string)($this->in['fonts_dir'] ?? ''), + 'url_base' => (string)($this->in['fonts_url_base'] ?? ''), + 'urls' => (string)($this->in['fonts_urls'] ?? ''), + ]; $setup = $this->sanitizeBridgeSetup([ 'tables' => $tables, 'mode' => $mode, 'direct' => $direct, 'config' => $config, + 'fonts' => $fonts, ]); $stored = $this->saveBridgeSetupData($customerId, $setup); @@ -2114,6 +2123,20 @@ class ApiKernel ]); } + private function handleAccountFontsList(): void + { + $user = $this->requireAuth(); + $customerId = (int)($user['customer_id'] ?? 0); + $setup = $this->getBridgeSetupData($customerId); + $fonts = $setup['fonts'] ?? []; + $payload = $this->buildFontCatalog($fonts); + $this->respond([ + 'ok' => true, + 'fonts' => $payload['fonts'], + 'font_face_css' => $payload['font_face_css'], + ]); + } + private function handleDebugPhpInfo(): void { $user = $this->requireAuth(); @@ -2315,6 +2338,11 @@ class ApiKernel 'password_key' => '', 'charset_key' => '', ], + 'fonts' => [ + 'dir' => '', + 'url_base' => '', + 'urls' => '', + ], ]; } @@ -2331,11 +2359,28 @@ class ApiKernel $tables = $this->normalizeBridgeTables($input['tables'] ?? []); $direct = $input['direct'] ?? []; $config = $input['config'] ?? []; + $fonts = $input['fonts'] ?? []; $sanitizePath = function ($value) { $value = trim((string)$value); if ($value === '') return ''; return preg_replace('/[^a-zA-Z0-9_\.\-]/', '', $value) ?: ''; }; + $sanitizeDir = function ($value) { + $value = trim((string)$value); + if ($value === '') return ''; + return trim(preg_replace('/[^a-zA-Z0-9_\.\-\/\\\\:\s]/', '', $value)) ?: ''; + }; + $sanitizeUrl = function ($value) { + $value = trim((string)$value); + if ($value === '') return ''; + $value = preg_replace('/[\x00-\x1f]/', '', $value); + return $value; + }; + $sanitizeText = function ($value) { + $value = (string)$value; + $value = preg_replace('/[\x00-\x08\x0b\x0c\x0e-\x1f]/', '', $value); + return $value; + }; $result = [ 'tables' => $tables, 'mode' => $mode, @@ -2357,6 +2402,11 @@ class ApiKernel 'password_key' => $sanitizePath($config['password_key'] ?? ''), 'charset_key' => $sanitizePath($config['charset_key'] ?? ''), ], + 'fonts' => [ + 'dir' => $sanitizeDir($fonts['dir'] ?? ''), + 'url_base' => $sanitizeUrl($fonts['url_base'] ?? ''), + 'urls' => $sanitizeText($fonts['urls'] ?? ''), + ], ]; if ($result['direct']['port'] <= 0) { $result['direct']['port'] = 3306; @@ -2364,6 +2414,93 @@ class ApiKernel return $result; } + private function buildFontCatalog(array $fonts): array + { + $items = []; + $faces = []; + $seen = []; + $groups = []; + $allowed = ['woff2', 'woff', 'ttf', 'otf']; + + $addGroup = function (string $family, array $sources) use (&$items, &$faces, &$seen, $allowed) { + $family = trim($family); + if ($family === '') return; + if (!empty($seen[strtolower($family)])) return; + $srcParts = []; + foreach ($allowed as $ext) { + if (!empty($sources[$ext])) { + $url = $sources[$ext]; + $srcParts[] = "url('{$url}') format('{$ext}')"; + } + } + if (!$srcParts) return; + $safeFamily = str_replace("'", "\\'", $family); + $faces[] = "@font-face{font-family:'{$safeFamily}';font-style:normal;font-weight:400;src:" . implode(',', $srcParts) . ";}"; + $items[] = [ + 'label' => $family, + 'value' => "'" . $safeFamily . "', sans-serif", + ]; + $seen[strtolower($family)] = true; + }; + + $inferFamily = function (string $name): string { + $name = preg_replace('/[-_]+/', ' ', $name); + $name = preg_replace('/\s+/', ' ', $name); + return trim($name); + }; + + $dir = trim((string)($fonts['dir'] ?? '')); + $base = trim((string)($fonts['url_base'] ?? '')); + if ($dir !== '' && $base !== '' && is_dir($dir)) { + $base = rtrim($base, '/'); + $pattern = $dir . '/*.{woff2,woff,ttf,otf,WOFF2,WOFF,TTF,OTF}'; + $files = glob($pattern, GLOB_BRACE) ?: []; + foreach ($files as $file) { + $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + if (!in_array($ext, $allowed, true)) continue; + $name = pathinfo($file, PATHINFO_FILENAME); + $groups[$name][$ext] = $base . '/' . basename($file); + } + } + + $rawUrls = (string)($fonts['urls'] ?? ''); + if ($rawUrls !== '') { + $lines = preg_split('/\r?\n/', $rawUrls) ?: []; + foreach ($lines as $line) { + $line = trim($line); + if ($line === '') continue; + $family = ''; + $url = ''; + if (strpos($line, '|') !== false) { + [$family, $url] = array_map('trim', explode('|', $line, 2)); + } elseif (strpos($line, '=') !== false) { + [$family, $url] = array_map('trim', explode('=', $line, 2)); + } else { + $url = $line; + } + if ($url === '') continue; + $path = parse_url($url, PHP_URL_PATH); + if (!$path) continue; + $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + if (!in_array($ext, $allowed, true)) continue; + $name = pathinfo($path, PATHINFO_FILENAME); + $family = $family !== '' ? $family : $inferFamily($name); + if ($family === '') continue; + $groups[$family][$ext] = $url; + } + } + + foreach ($groups as $family => $sources) { + $display = $inferFamily($family); + $addGroup($display, $sources); + } + + return [ + 'fonts' => $items, + 'font_face_css' => implode("\n", $faces), + ]; + } + private function customerSettingsTable(): string { return 'emailtemplate_customer_settings';