This commit is contained in:
2026-01-14 01:50:50 +01:00
parent 62da2a67af
commit d49c326c90
9 changed files with 343 additions and 37 deletions

View File

@@ -116,6 +116,33 @@ require dirname(__DIR__) . '/../structure/layout_start.php';
<div id="setupStatus" class="text-xs text-slate-500">Noch nicht gespeichert.</div> <div id="setupStatus" class="text-xs text-slate-500">Noch nicht gespeichert.</div>
</form> </form>
</section> </section>
<section class="section-card">
<h4>Schriftarten (optional)</h4>
<p class="text-sm text-slate-600 mb-3">
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.
</p>
<div id="bridgeFontsForm" class="space-y-3">
<label class="block text-sm text-slate-600">Schriftarten-Ordner (Serverpfad, optional)
<input type="text" name="fonts_dir" class="input mt-1" placeholder="/var/www/site/public/fonts">
</label>
<label class="block text-sm text-slate-600">Oeffentliche Basis-URL zum Ordner
<input type="text" name="fonts_url_base" class="input mt-1" placeholder="https://deine-seite.de/fonts">
<p class="text-xs text-slate-500 mt-1">Wird genutzt, um die Dateien im Editor/Email zu laden.</p>
</label>
<label class="block text-sm text-slate-600">Direkte Font-URLs (eine pro Zeile, optional)
<textarea name="fonts_urls" class="input mt-1 h-28" placeholder="Family|https://deine-seite.de/fonts/MyFont.woff2"></textarea>
<p class="text-xs text-slate-500 mt-1">
Format: <code>Family|URL</code> oder nur <code>URL</code>. Erlaubte Endungen: woff2, woff, ttf, otf.
</p>
</label>
<p class="text-xs text-slate-500">
Hinweis: woff2/woff ist empfohlen, ttf/otf funktioniert meist, wird aber nicht von allen Mail-Clients geladen.
</p>
</div>
</section>
</main> </main>
<div id="toast-root"></div> <div id="toast-root"></div>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -86,6 +86,11 @@ function defaultSetup() {
password_key: '', password_key: '',
charset_key: '', charset_key: '',
}, },
fonts: {
dir: '',
url_base: '',
urls: '',
},
}; };
} }
@@ -148,6 +153,19 @@ function fillForm(setup, options = {}) {
if (input) input.value = value; 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) { function applyModeVisibility(mode) {
@@ -224,6 +242,9 @@ async function submitBridgeSetup(ev) {
config_user_key: form.config_user_key?.value.trim() || '', config_user_key: form.config_user_key?.value.trim() || '',
config_password_key: form.config_password_key?.value.trim() || '', config_password_key: form.config_password_key?.value.trim() || '',
config_charset_key: form.config_charset_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 { try {
@@ -324,5 +345,9 @@ function normalizeSetupInput(input) {
direct.port = Number(direct.port || 3306) || 3306; direct.port = Number(direct.port || 3306) || 3306;
direct.charset = direct.charset || 'utf8mb4'; direct.charset = direct.charset || 'utf8mb4';
const config = { ...base.config, ...(input.config || {}) }; 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 };
} }

View File

@@ -464,7 +464,11 @@
} }
// 1. Daten extrahieren // 1. Daten extrahieren
const htmlContent = editor.getHtml() + '<style>' + editor.getCss() + '</style>'; 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() + '<style>' + cssPayload + '</style>';
// 2. KRITISCH: Holt die JSON-Repräsentation des Editors // 2. KRITISCH: Holt die JSON-Repräsentation des Editors
let jsonProjectDataRaw = ''; let jsonProjectDataRaw = '';
try { try {

View File

@@ -263,7 +263,7 @@
autosave: false, autosave: false,
}; };
const execRteCommand = (rte, cmd, value) => { const execRteCommand = (rte, cmd, value, docOverride) => {
try { try {
if (rte && typeof rte.exec === 'function') { if (rte && typeof rte.exec === 'function') {
rte.exec(cmd, value); rte.exec(cmd, value);
@@ -271,9 +271,16 @@
} }
} catch {} } catch {}
try { 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') { if (ok === false && cmd === 'insertText') {
document.execCommand('insertHTML', false, String(value || '').replace(/</g, '&lt;').replace(/>/g, '&gt;')); doc.execCommand('insertHTML', false, String(value || '').replace(/</g, '&lt;').replace(/>/g, '&gt;'));
} }
return true; return true;
} catch {} } catch {}
@@ -344,10 +351,10 @@
|| ''; || '';
content.innerHTML = initialHtml; content.innerHTML = initialHtml;
const addButton = (label, title, cmd, valueGetter) => { const addButton = (labelHtml, title, cmd, valueGetter) => {
const btn = doc.createElement('button'); const btn = doc.createElement('button');
btn.type = 'button'; btn.type = 'button';
btn.textContent = label; btn.innerHTML = labelHtml;
btn.title = title; btn.title = title;
btn.style.padding = '4px 8px'; btn.style.padding = '4px 8px';
btn.style.border = '1px solid #cbd5f5'; btn.style.border = '1px solid #cbd5f5';
@@ -359,7 +366,7 @@
const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter; const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter;
if (value === null || value === undefined) return; if (value === null || value === undefined) return;
if (cmd === 'createLink' && !value) return; if (cmd === 'createLink' && !value) return;
execRteCommand(null, cmd, value); execRteCommand(null, cmd, value, content.ownerDocument);
}); });
toolbar.appendChild(btn); toolbar.appendChild(btn);
}; };
@@ -393,32 +400,41 @@
} }
}; };
addButton('B', 'Fett', 'bold'); const icon = (path) => `<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="${path}" fill="currentColor"/></svg>`;
addButton('I', 'Kursiv', 'italic'); addButton('<strong>B</strong>', 'Fett', 'bold');
addButton('U', 'Unterstrichen', 'underline'); addButton('<em>I</em>', 'Kursiv', 'italic');
addButton('S', 'Durchgestrichen', 'strikethrough'); addButton('<span style="text-decoration:underline">U</span>', 'Unterstrichen', 'underline');
addButton('<span style="text-decoration:line-through">S</span>', 'Durchgestrichen', 'strikethrough');
addButton(icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList');
addButton(icon('M4 7h14v2H4zM4 11h14v2H4zM4 15h14v2H4z') + '<span style="font-size:10px;margin-left:4px">1.</span>', '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('Link', 'Link einfuegen', 'createLink', () => prompt('Link-URL eingeben', 'https://'));
addButton('Unlink', 'Link entfernen', 'unlink'); 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('Sub', 'Tiefgestellt', 'subscript');
addButton('Sup', 'Hochgestellt', 'superscript'); addButton('Sup', 'Hochgestellt', 'superscript');
addButton('Einr.', 'Einzug', 'indent'); addButton('Einr.', 'Einzug', 'indent');
addButton('Aus.', 'Ausruecken', 'outdent'); addButton('Aus.', 'Ausruecken', 'outdent');
addButton('Clear', 'Formatierung entfernen', 'removeFormat'); addButton('Clear', 'Formatierung entfernen', 'removeFormat');
addSelect([ const fontOptions = (B.RTE_FONTS && Array.isArray(B.RTE_FONTS) && B.RTE_FONTS.length)
{ label: 'Schriftart', value: '' }, ? B.RTE_FONTS
: [
{ label: 'Arial', value: 'Arial, sans-serif' }, { label: 'Arial', value: 'Arial, sans-serif' },
{ label: 'Calibri', value: 'Calibri, sans-serif' },
{ label: 'Cambria', value: 'Cambria, serif' },
{ label: 'Georgia', value: 'Georgia, serif' }, { label: 'Georgia', value: 'Georgia, serif' },
{ label: 'Tahoma', value: 'Tahoma, sans-serif' }, { label: 'Tahoma', value: 'Tahoma, sans-serif' },
{ label: 'Times New Roman', value: 'Times New Roman, serif' }, { label: 'Times New Roman', value: 'Times New Roman, serif' },
{ label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
{ label: 'Verdana', value: 'Verdana, sans-serif' }, { label: 'Verdana', value: 'Verdana, sans-serif' },
], 'Schriftart', (value) => execRteCommand(null, 'fontName', value)); ];
addSelect([
{ label: 'Schriftart', value: '' },
...fontOptions,
], 'Schriftart', (value) => execRteCommand(null, 'fontName', value, content.ownerDocument));
addSelect([ addSelect([
{ label: 'Groesse', value: '' }, { label: 'Groesse', value: '' },
@@ -429,7 +445,7 @@
{ label: '18px', value: '5' }, { label: '18px', value: '5' },
{ label: '24px', value: '6' }, { label: '24px', value: '6' },
{ label: '32px', value: '7' }, { label: '32px', value: '7' },
], 'Schriftgroesse', (value) => execRteCommand(null, 'fontSize', value)); ], 'Schriftgroesse', (value) => execRteCommand(null, 'fontSize', value, content.ownerDocument));
const emojiBtn = doc.createElement('button'); const emojiBtn = doc.createElement('button');
emojiBtn.type = 'button'; emojiBtn.type = 'button';
@@ -498,6 +514,19 @@
const setupRichTextEditor = (editor) => { const setupRichTextEditor = (editor) => {
if (!editor || !editor.RichTextEditor) return; if (!editor || !editor.RichTextEditor) return;
const rte = editor.RichTextEditor; const rte = editor.RichTextEditor;
const icon = (path) => `<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="${path}" fill="currentColor"/></svg>`;
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) => { const addAction = (name, icon, title, command, valueGetter) => {
if (rte.get && rte.get(name)) return; if (rte.get && rte.get(name)) return;
@@ -515,14 +544,18 @@
}); });
}; };
addAction('bridge-align-left', 'L', 'Linksbundig', 'justifyLeft'); addAction('bridge-align-left', icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h12v2H4z'), 'Linksbundig', 'justifyLeft');
addAction('bridge-align-center', 'C', 'Zentriert', 'justifyCenter'); addAction('bridge-align-center', icon('M5 7h14v2H5zM4 11h16v2H4zM5 15h14v2H5z'), 'Zentriert', 'justifyCenter');
addAction('bridge-align-right', 'R', 'Rechtsbundig', 'justifyRight'); addAction('bridge-align-right', icon('M10 7h10v2H10zM4 11h16v2H4zM8 15h12v2H8z'), 'Rechtsbundig', 'justifyRight');
addAction('bridge-align-justify', 'J', 'Blocksatz', 'justifyFull'); addAction('bridge-align-justify', icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull');
addAction('bridge-ul', 'UL', 'Liste (ungeordnet)', 'insertUnorderedList'); addAction('bridge-ul', icon('M4 7h10v2H4zM4 11h16v2H4zM4 15h10v2H4z'), 'Liste (ungeordnet)', 'insertUnorderedList');
addAction('bridge-ol', 'OL', 'Liste (geordnet)', 'insertOrderedList'); addAction('bridge-ol', icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Liste (geordnet)', 'insertOrderedList');
addAction('bridge-emoji', ':-)', 'Emoticon einfuegen', 'insertText', () => prompt('Emoticon eingeben', ':)')); 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', () => { addAction('bridge-font-size', 'Px', 'Schriftgroesse', 'fontSize', () => {
const raw = prompt('Schriftgroesse in px (10-32)', '14'); const raw = prompt('Schriftgroesse in px (10-32)', '14');
const val = Number(raw || 14); const val = Number(raw || 14);
@@ -542,11 +575,16 @@
}); });
return best.cmd; return best.cmd;
}); });
addAction('bridge-open-richtext', 'RTE', 'Richtext Editor', 'execCommand', () => { 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(); const component = editor.getSelected && editor.getSelected();
openRichTextModal(editor, component); openRichTextModal(editor, component);
return null; },
}); });
}
if (!editor.Commands.get('bridge-open-richtext')) { if (!editor.Commands.get('bridge-open-richtext')) {
editor.Commands.add('bridge-open-richtext', { editor.Commands.add('bridge-open-richtext', {
@@ -576,6 +614,46 @@
editor.on('component:add', (model) => ensureTextToolbarButton(editor, model)); 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({ var ed = grapesjs.init({
container: '#gjs', container: '#gjs',
height: '100vh', height: '100vh',
@@ -623,6 +701,7 @@
} }
setupRichTextEditor(ed); setupRichTextEditor(ed);
loadDynamicFonts();
// Entfernt: jegliche Blur/RTE-Handler, die Inhalte verändern. // Entfernt: jegliche Blur/RTE-Handler, die Inhalte verändern.

View File

@@ -2,6 +2,27 @@
$mode = strtolower($_GET['mode'] ?? 'templates'); $mode = strtolower($_GET['mode'] ?? 'templates');
$id = (int)($_GET['id'] ?? 0); $id = (int)($_GET['id'] ?? 0);
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time(); $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) . ";}";
}
?><!doctype html> ?><!doctype html>
<html lang="de"> <html lang="de">
<head> <head>
@@ -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-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} .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} #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}
<?= $fontFaceCss ?>
</style> </style>
</head> </head>
<body> <body>
@@ -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_KERNEL_URL = window.BridgeParts.API_KERNEL_URL || '/api.php';
window.BridgeParts.API_BASE = window.BridgeParts.API_BASE || window.BridgeParts.API_KERNEL_URL; 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.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 || <?= json_encode($fontFaceCss) ?>;
function logToParent(type, detail){ try{ parent.postMessage({source:'editor-core',type:type,detail:String(detail||'')},'*'); }catch(e){} } function logToParent(type, detail){ try{ parent.postMessage({source:'editor-core',type:type,detail:String(detail||'')},'*'); }catch(e){} }
window.addEventListener('error', function(e){ window.addEventListener('error', function(e){

View File

@@ -1081,6 +1081,9 @@ class ApiKernel
case 'account.bridge.test': case 'account.bridge.test':
$this->handleAccountBridgeTest(); $this->handleAccountBridgeTest();
break; break;
case 'account.fonts.list':
$this->handleAccountFontsList();
break;
case 'placeholders.status': case 'placeholders.status':
$this->handlePlaceholderStatus(); $this->handlePlaceholderStatus();
break; break;
@@ -2072,12 +2075,18 @@ class ApiKernel
'password_key' => (string)($this->in['config_password_key'] ?? ''), 'password_key' => (string)($this->in['config_password_key'] ?? ''),
'charset_key' => (string)($this->in['config_charset_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([ $setup = $this->sanitizeBridgeSetup([
'tables' => $tables, 'tables' => $tables,
'mode' => $mode, 'mode' => $mode,
'direct' => $direct, 'direct' => $direct,
'config' => $config, 'config' => $config,
'fonts' => $fonts,
]); ]);
$stored = $this->saveBridgeSetupData($customerId, $setup); $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 private function handleDebugPhpInfo(): void
{ {
$user = $this->requireAuth(); $user = $this->requireAuth();
@@ -2315,6 +2338,11 @@ class ApiKernel
'password_key' => '', 'password_key' => '',
'charset_key' => '', 'charset_key' => '',
], ],
'fonts' => [
'dir' => '',
'url_base' => '',
'urls' => '',
],
]; ];
} }
@@ -2331,11 +2359,28 @@ class ApiKernel
$tables = $this->normalizeBridgeTables($input['tables'] ?? []); $tables = $this->normalizeBridgeTables($input['tables'] ?? []);
$direct = $input['direct'] ?? []; $direct = $input['direct'] ?? [];
$config = $input['config'] ?? []; $config = $input['config'] ?? [];
$fonts = $input['fonts'] ?? [];
$sanitizePath = function ($value) { $sanitizePath = function ($value) {
$value = trim((string)$value); $value = trim((string)$value);
if ($value === '') return ''; if ($value === '') return '';
return preg_replace('/[^a-zA-Z0-9_\.\-]/', '', $value) ?: ''; 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 = [ $result = [
'tables' => $tables, 'tables' => $tables,
'mode' => $mode, 'mode' => $mode,
@@ -2357,6 +2402,11 @@ class ApiKernel
'password_key' => $sanitizePath($config['password_key'] ?? ''), 'password_key' => $sanitizePath($config['password_key'] ?? ''),
'charset_key' => $sanitizePath($config['charset_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) { if ($result['direct']['port'] <= 0) {
$result['direct']['port'] = 3306; $result['direct']['port'] = 3306;
@@ -2364,6 +2414,93 @@ class ApiKernel
return $result; 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 private function customerSettingsTable(): string
{ {
return 'emailtemplate_customer_settings'; return 'emailtemplate_customer_settings';