From ad89392ff1f46a0595963a467fef183636e26181 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Tue, 9 Dec 2025 00:21:01 +0100 Subject: [PATCH] up --- download/emailtemplate_bridge.php | 31 ++ partials/landingpage/admin/bridge.php | 158 +++++++++++ partials/landingpage/admin/settings.php | 9 +- public/admin/bridge.php | 3 + public/assets/js/bridge-setup-page.js | 242 ++++++++++++++++ public/assets/js/bridge-setup.js | 30 ++ public/assets/js/ui-user.js | 79 ------ src/ApiKernel.php | 360 +++++++++++++++++++++--- 8 files changed, 788 insertions(+), 124 deletions(-) create mode 100644 partials/landingpage/admin/bridge.php create mode 100644 public/admin/bridge.php create mode 100644 public/assets/js/bridge-setup-page.js create mode 100644 public/assets/js/bridge-setup.js diff --git a/download/emailtemplate_bridge.php b/download/emailtemplate_bridge.php index 97ac755..634c185 100644 --- a/download/emailtemplate_bridge.php +++ b/download/emailtemplate_bridge.php @@ -35,6 +35,8 @@ $bridgeConfig = [ 'tables_allow' => [], // optional whitelist: ['customers', 'orders'] ]; +// {{BRIDGE_DB_SETUP}} + $localOverride = __DIR__ . '/emailtemplate.bridge.conf.php'; if (is_file($localOverride)) { $override = include $localOverride; @@ -96,6 +98,35 @@ function bridgeDb(array $config): PDO return $pdo; } +function bridge_array_get($data, string $path, $default = null) +{ + if (is_object($data)) { + $data = (array)$data; + } + if (!is_array($data)) { + return $default; + } + $path = trim($path); + if ($path === '') { + return $data; + } + $segments = array_values(array_filter(array_map('trim', explode('.', $path)), static function ($segment) { + return $segment !== ''; + })); + foreach ($segments as $segment) { + if (is_array($data) && array_key_exists($segment, $data)) { + $value = $data[$segment]; + if (is_object($value)) { + $value = (array)$value; + } + $data = $value; + } else { + return $default; + } + } + return $data; +} + bridgeRequireToken($bridgeConfig); $action = strtolower((string)($_GET['action'] ?? $_POST['action'] ?? 'schema')); diff --git a/partials/landingpage/admin/bridge.php b/partials/landingpage/admin/bridge.php new file mode 100644 index 0000000..f6fd5a6 --- /dev/null +++ b/partials/landingpage/admin/bridge.php @@ -0,0 +1,158 @@ + + + + + + + Email Template System – Bridge Setup + + + + + + + + + + + + +
+
+ ← Administration +

Bridge Setup

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

Bridge-Datei vorbereiten

+

+ Diese Angaben werden nur verwendet, um die emailtemplate_bridge.php zu generieren. Das EmailTemplate-System selbst behält Zugriff auf alle Tabellen; die hier definierten Whitelists greifen ausschließlich in der Bridge-Datei. +

+
+
+ + +

Kommagetrennt oder je Zeile eine Tabelle. Leer lassen = keine Einschränkung.

+
Noch keine Tabellen angegeben.
+
+ +
+ Datenbankquelle +
+ + +
+ +
+ + + + + + +
+ + +
+ +
+ + +
+
Noch nicht gespeichert.
+
+
+
+ +
+ + + + + diff --git a/partials/landingpage/admin/settings.php b/partials/landingpage/admin/settings.php index 05f7be5..db09fd2 100644 --- a/partials/landingpage/admin/settings.php +++ b/partials/landingpage/admin/settings.php @@ -170,14 +170,9 @@ $debugRedirect = isset($_GET['debug_redirect']); -
- - -

Nur diese Tabellen werden als Platzhalter angeboten. Leer = alle.

-
- -
Noch nicht geprüft.
+ Bridge-Setup & Tabellen öffnen +

Dort kannst du Tabellen-Filter sowie DB-Quellen für die Bridge-Datei konfigurieren.

diff --git a/public/admin/bridge.php b/public/admin/bridge.php new file mode 100644 index 0000000..a288e87 --- /dev/null +++ b/public/admin/bridge.php @@ -0,0 +1,3 @@ + updateTablesPreview(parseTablesInput())); + loadBtn?.addEventListener('click', loadTablesFromBridge); + modeInputs.forEach(input => { + input.addEventListener('change', () => applyModeVisibility(input.value)); + }); + + loadBridgeSetup(); +} + +function defaultSetup() { + return { + tables: [], + mode: 'direct', + direct: { + host: '', + port: 3306, + database: '', + user: '', + password: '', + charset: 'utf8mb4', + }, + config: { + file: '', + base: '', + host_key: '', + port_key: '', + database_key: '', + user_key: '', + password_key: '', + charset_key: '', + }, + }; +} + +async function loadBridgeSetup() { + state.loading = true; + try { + const res = await apiAction('account.bridge.setup.get', { method: 'GET' }); + if (!res?.ok) throw new Error(res?.error || 'Bridge-Setup konnte nicht geladen werden'); + state.setup = res.setup || defaultSetup(); + fillForm(state.setup); + updateStatus('Daten geladen.'); + } catch (err) { + console.error(err); + toast(err.message || 'Bridge-Setup konnte nicht geladen werden', false); + } finally { + state.loading = false; + } +} + +function fillForm(setup) { + const data = { ...defaultSetup(), ...(setup || {}) }; + if (tablesInput) { + tablesInput.value = (data.tables || []).join(', '); + updateTablesPreview(parseTablesInput()); + } + const activeMode = (data.mode || 'direct').toLowerCase(); + modeInputs.forEach(input => { + input.checked = input.value === activeMode; + }); + applyModeVisibility(activeMode); + + if (directFields) { + const directMap = { + direct_host: data.direct.host || '', + direct_port: String(data.direct.port || 3306), + direct_database: data.direct.database || '', + direct_charset: data.direct.charset || 'utf8mb4', + direct_user: data.direct.user || '', + direct_password: data.direct.password || '', + }; + Object.entries(directMap).forEach(([name, value]) => { + const input = directFields.querySelector(`[name="${name}"]`); + if (input) input.value = value; + }); + } + + if (configFields) { + const configMap = { + config_file: data.config.file || '', + config_base: data.config.base || '', + config_host_key: data.config.host_key || '', + config_port_key: data.config.port_key || '', + config_database_key: data.config.database_key || '', + config_user_key: data.config.user_key || '', + config_password_key: data.config.password_key || '', + config_charset_key: data.config.charset_key || '', + }; + Object.entries(configMap).forEach(([name, value]) => { + const input = configFields.querySelector(`[name="${name}"]`); + if (input) input.value = value; + }); + } +} + +function applyModeVisibility(mode) { + const direct = mode === 'config' ? 'add' : 'remove'; + const config = mode === 'config' ? 'remove' : 'add'; + directFields?.classList[direct]('hidden'); + configFields?.classList[config]('hidden'); +} + +function parseTablesInput() { + if (!tablesInput) return []; + return tablesInput.value + .split(/[\s,]+/) + .map(part => part.trim()) + .filter(Boolean) + .filter((value, index, arr) => arr.indexOf(value) === index); +} + +function updateTablesPreview(list) { + if (!tablesPreview) return; + if (!list.length) { + tablesPreview.innerHTML = 'Noch keine Tabellen angegeben.'; + return; + } + tablesPreview.innerHTML = list.map(name => `${escapeHtml(name)}`).join(''); +} + +async function submitBridgeSetup(ev) { + ev.preventDefault(); + if (!form) return; + const mode = form.querySelector('input[name="db_mode"]:checked')?.value || 'direct'; + const payload = { + tables: parseTablesInput(), + mode, + direct_host: form.direct_host?.value.trim() || '', + direct_port: Number(form.direct_port?.value || 0) || 3306, + direct_database: form.direct_database?.value.trim() || '', + direct_charset: form.direct_charset?.value.trim() || 'utf8mb4', + direct_user: form.direct_user?.value.trim() || '', + direct_password: form.direct_password?.value || '', + config_file: form.config_file?.value.trim() || '', + config_base: form.config_base?.value.trim() || '', + config_host_key: form.config_host_key?.value.trim() || '', + config_port_key: form.config_port_key?.value.trim() || '', + config_database_key: form.config_database_key?.value.trim() || '', + 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() || '', + }; + + try { + const res = await apiAction('account.bridge.setup.save', { method: 'POST', data: payload }); + if (!res?.ok) throw new Error(res?.error || 'Bridge-Setup konnte nicht gespeichert werden'); + state.setup = res.setup || payload; + fillForm(state.setup); + updateStatus('Bridge-Setup gespeichert.'); + toast('Bridge-Setup gespeichert', true); + } catch (err) { + console.error(err); + toast(err.message || 'Bridge-Setup konnte nicht gespeichert werden', false); + } +} + +async function loadTablesFromBridge(ev) { + ev?.preventDefault(); + if (!loadBtn) return; + loadBtn.disabled = true; + try { + const res = await apiAction('account.bridge.test', { method: 'POST', data: {} }); + if (!res?.ok) throw new Error(res?.error || 'Bridge konnte nicht abgefragt werden'); + const tables = normalizeTableNames(res.tables); + if (tablesInput) { + tablesInput.value = tables.join(', '); + updateTablesPreview(tables); + } + updateStatus(`Tabellen geladen (${tables.length}).`); + toast('Tabellen erfolgreich geladen', true); + } catch (err) { + console.error(err); + toast(err.message || 'Bridge konnte nicht geprüft werden', false); + } finally { + loadBtn.disabled = false; + } +} + +function normalizeTableNames(list) { + if (!Array.isArray(list)) return []; + const result = []; + const seen = new Set(); + for (const entry of list) { + let name = ''; + if (typeof entry === 'string') { + name = entry; + } else if (entry && typeof entry === 'object') { + name = entry.name || entry.table || entry.label || ''; + } + if (typeof name === 'string') { + const trimmed = name.trim(); + if (trimmed && !seen.has(trimmed)) { + seen.add(trimmed); + result.push(trimmed); + } + } + } + return result; +} + +function updateStatus(msg) { + if (!statusLabel) return; + const time = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + statusLabel.textContent = `${msg} (${time})`; +} + +function escapeHtml(str) { + return String(str || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/public/assets/js/bridge-setup.js b/public/assets/js/bridge-setup.js new file mode 100644 index 0000000..553e4e0 --- /dev/null +++ b/public/assets/js/bridge-setup.js @@ -0,0 +1,30 @@ +import { apiAction } from './api.js'; +import { initUserPanel } from './ui-user.js'; +import { mountLogoutButton, ensureFloatingLogout } from './ui-auth.js'; +import { initBridgeSetupPage } from './bridge-setup-page.js'; + +async function ensureAuthenticated() { + try { + const me = await apiAction('auth.me', { method: 'GET' }); + if (!me?.ok || !me?.user) { + if (!window.DISABLE_AUTH_REDIRECT) { + window.location.href = '/login.php'; + } + return false; + } + window.__currentUser = me.user; + document.documentElement.classList.remove('auth-pending'); + return true; + } catch { + return false; + } +} + +document.addEventListener('DOMContentLoaded', async () => { + const ok = await ensureAuthenticated(); + if (!ok) return; + initUserPanel(); + initBridgeSetupPage(); + mountLogoutButton('#btn-logout', { redirect: '/login.php' }); + ensureFloatingLogout({ redirect: '/login.php' }); +}); diff --git a/public/assets/js/ui-user.js b/public/assets/js/ui-user.js index 231249e..9397817 100644 --- a/public/assets/js/ui-user.js +++ b/public/assets/js/ui-user.js @@ -7,7 +7,6 @@ const state = { userMap: new Map(), senders: [], senderMap: new Map(), - bridgeTables: [], currentTab: 'profile', loading: false, }; @@ -25,8 +24,6 @@ let teamTable; let userForm; let senderTable; let senderForm; -let bridgePreview; -let validateBridgeBtn; let menuInitialized = false; let menuOpen = false; let debugButton; @@ -64,8 +61,6 @@ export function initAccountPage() { userForm = document.getElementById('userForm'); senderTable = document.getElementById('senderTable'); senderForm = document.getElementById('senderForm'); - bridgePreview = document.getElementById('bridgeTablesPreview'); - validateBridgeBtn = document.getElementById('btn-validate-bridge'); document.getElementById('btn-user-add')?.addEventListener('click', () => openUserForm()); document.getElementById('userFormCancel')?.addEventListener('click', () => closeUserForm()); @@ -80,8 +75,6 @@ export function initAccountPage() { settingsForm?.addEventListener('submit', submitSettingsForm); teamTable?.addEventListener('click', handleTeamTableClick); senderTable?.addEventListener('click', handleSenderTableClick); - validateBridgeBtn?.addEventListener('click', validateBridgeSettings); - document.querySelectorAll('[data-user-tab]').forEach(btn => { btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab'))); }); @@ -250,11 +243,6 @@ function fillSettingsForm(settings) { settingsForm.bridge_token.value = settings.bridge_token || ''; settingsForm.sender_token.value = settings.sender_token || ''; settingsForm.external_api_token.value = settings.external_api_token || ''; - const tables = normalizeTableNames(settings.bridge_tables); - if (settingsForm.bridge_tables) { - settingsForm.bridge_tables.value = tables.join(', '); - } - applyBridgePreview(tables); state.rotate = { bridge: false, sender: false, external: false }; } @@ -301,7 +289,6 @@ async function submitSettingsForm(ev) { rotate_bridge_token: state.rotate.bridge ? 1 : 0, rotate_sender_token: state.rotate.sender ? 1 : 0, rotate_external_token: state.rotate.external ? 1 : 0, - bridge_tables: parseBridgeTablesInput(), }; try { const res = await apiAction('account.settings.update', { method: 'POST', data }); @@ -335,72 +322,6 @@ async function downloadFile(type) { } } -function parseBridgeTablesInput() { - if (!settingsForm) return []; - const raw = settingsForm.bridge_tables?.value || ''; - return raw - .split(/[\s,]+/) - .map(part => part.trim()) - .filter(Boolean); -} - -function applyBridgePreview(tables) { - state.bridgeTables = normalizeTableNames(tables); - if (!bridgePreview) return; - if (!state.bridgeTables.length) { - bridgePreview.innerHTML = 'Keine Einschränkung – alle Tabellen erlaubt.'; - return; - } - bridgePreview.innerHTML = state.bridgeTables.map(name => `${escapeHtml(name)}`).join(''); -} - -async function validateBridgeSettings(ev) { - ev?.preventDefault(); - if (!settingsForm) return; - const data = { - bridge_url: settingsForm.bridge_url.value.trim(), - bridge_token: settingsForm.bridge_token.value.trim(), - }; - if (!data.bridge_url || !data.bridge_token) { - toast('Bitte Bridge-URL und Token angeben', false); - return; - } - try { - const res = await apiAction('account.bridge.test', { method: 'POST', data }); - if (!res?.ok) throw new Error(res?.error || 'Prüfung fehlgeschlagen'); - const tables = normalizeTableNames(res.tables); - applyBridgePreview(tables); - if (settingsForm.bridge_tables) { - settingsForm.bridge_tables.value = tables.join(', '); - } - toast('Bridge erfolgreich geprüft', true); - } catch (err) { - toast(err.message || 'Prüfung fehlgeschlagen', false); - } -} - -function normalizeTableNames(list) { - if (!Array.isArray(list)) return []; - const seen = new Set(); - const result = []; - for (const entry of list) { - let name = ''; - if (typeof entry === 'string') { - name = entry; - } else if (entry && typeof entry === 'object') { - name = entry.name || entry.table || entry.label || ''; - } - if (typeof name === 'string') { - const trimmed = name.trim(); - if (trimmed && !seen.has(trimmed)) { - seen.add(trimmed); - result.push(trimmed); - } - } - } - return result; -} - async function loadUsers() { try { const res = await apiAction('account.users.list', { method: 'GET' }); diff --git a/src/ApiKernel.php b/src/ApiKernel.php index aaf34c6..174b1b3 100644 --- a/src/ApiKernel.php +++ b/src/ApiKernel.php @@ -876,6 +876,12 @@ class ApiKernel case 'downloads.sender': $this->handleDownloadFile('sender'); break; + case 'account.bridge.setup.get': + $this->handleAccountBridgeSetupGet(); + break; + case 'account.bridge.setup.save': + $this->handleAccountBridgeSetupSave(); + break; case 'account.bridge.test': $this->handleAccountBridgeTest(); break; @@ -1272,15 +1278,9 @@ class ApiKernel return; } - $settings = $this->getCustomerSettings($customerId); - $tables = $schema['tables'] ?? []; - if (!empty($settings['bridge_tables'])) { - $tables = $this->filterSchemaTables($tables, $settings['bridge_tables']); - } - $this->respond([ 'ok' => true, - 'tables' => $tables, + 'tables' => $schema['tables'] ?? [], 'fetched' => $schema['fetched'] ?? date(DATE_ATOM), ]); } @@ -1466,8 +1466,6 @@ class ApiKernel $bridgeToken = trim((string)($this->in['bridge_token'] ?? '')); $senderToken = trim((string)($this->in['sender_token'] ?? '')); $externalToken = trim((string)($this->in['external_api_token'] ?? '')); - $bridgeTablesInput = $this->in['bridge_tables'] ?? null; - $bridgeTables = $this->normalizeBridgeTables($bridgeTablesInput); $rotateBridge = !empty($this->in['rotate_bridge_token']); $rotateSender = !empty($this->in['rotate_sender_token']); $rotateExternal = !empty($this->in['rotate_external_token']); @@ -1486,7 +1484,6 @@ class ApiKernel 'bridge_token' => $bridgeToken, 'sender_token' => $senderToken, 'external_api_token' => $externalToken, - 'bridge_tables' => $bridgeTables, ]); $this->respond(['ok' => true, 'settings' => $settings]); @@ -1809,9 +1806,10 @@ class ApiKernel if ($customerId <= 0) $this->fail('Customer context missing', null, 500); $settings = $this->ensureSettingsTokens($customerId, $this->getCustomerSettings($customerId)); + $bridgeSetup = $this->getBridgeSetupData($customerId); $content = $this->loadDownloadTemplate($type); if ($type === 'bridge') { - $content = $this->populateBridgeDownload($content, $settings); + $content = $this->populateBridgeDownload($content, $settings, $bridgeSetup); } else { $content = $this->populateSenderDownload($content, $settings); } @@ -1823,6 +1821,56 @@ class ApiKernel ]); } + private function handleAccountBridgeSetupGet(): void + { + $user = $this->requireAuth(); + $this->ensureRole($user, ['owner', 'admin']); + $customerId = (int)($user['customer_id'] ?? 0); + if ($customerId <= 0) $this->fail('Customer context missing', null, 500); + + $setup = $this->getBridgeSetupData($customerId); + $this->respond(['ok' => true, 'setup' => $setup]); + } + + private function handleAccountBridgeSetupSave(): void + { + $user = $this->requireAuth(); + $this->ensureRole($user, ['owner', 'admin']); + $customerId = (int)($user['customer_id'] ?? 0); + if ($customerId <= 0) $this->fail('Customer context missing', null, 500); + + $tables = $this->normalizeBridgeTables($this->in['tables'] ?? $this->in['bridge_tables'] ?? []); + $mode = strtolower((string)($this->in['mode'] ?? $this->in['db_mode'] ?? 'direct')); + $direct = [ + 'host' => trim((string)($this->in['direct_host'] ?? '')), + 'port' => (int)($this->in['direct_port'] ?? 3306), + 'database' => trim((string)($this->in['direct_database'] ?? $this->in['direct_db'] ?? '')), + 'user' => trim((string)($this->in['direct_user'] ?? '')), + 'password' => (string)($this->in['direct_password'] ?? ''), + 'charset' => trim((string)($this->in['direct_charset'] ?? '')) ?: 'utf8mb4', + ]; + $config = [ + 'file' => trim((string)($this->in['config_file'] ?? '')), + 'base' => (string)($this->in['config_base'] ?? ''), + 'host_key' => (string)($this->in['config_host_key'] ?? ''), + 'port_key' => (string)($this->in['config_port_key'] ?? ''), + 'database_key' => (string)($this->in['config_database_key'] ?? ''), + 'user_key' => (string)($this->in['config_user_key'] ?? ''), + 'password_key' => (string)($this->in['config_password_key'] ?? ''), + 'charset_key' => (string)($this->in['config_charset_key'] ?? ''), + ]; + + $setup = $this->sanitizeBridgeSetup([ + 'tables' => $tables, + 'mode' => $mode, + 'direct' => $direct, + 'config' => $config, + ]); + + $stored = $this->saveBridgeSetupData($customerId, $setup); + $this->respond(['ok' => true, 'setup' => $stored]); + } + private function handleAccountBridgeTest(): void { $user = $this->requireAuth(); @@ -1838,20 +1886,15 @@ class ApiKernel if ($bridgeUrl === '' || $bridgeToken === '') { $this->fail('Bridge nicht konfiguriert', null, 422); } - $settings = $this->getCustomerSettings($customerId); try { $schema = $this->fetchPlaceholderSchema($bridgeUrl, $bridgeToken, 0); } catch (Throwable $e) { $this->fail('Bridge request failed', $e->getMessage(), 502); return; } - $tables = $schema['tables'] ?? []; - if (!empty($settings['bridge_tables'])) { - $tables = $this->filterSchemaTables($tables, $settings['bridge_tables']); - } $this->respond([ 'ok' => true, - 'tables' => $tables, + 'tables' => $schema['tables'] ?? [], 'fetched' => $schema['fetched'] ?? date(DATE_ATOM), ]); } @@ -1887,17 +1930,26 @@ class ApiKernel return $row ? $this->formatCustomerSettingsRow($row) : []; } + private function getBridgeSetupData(int $customerId): array + { + $settings = $this->getCustomerSettings($customerId); + return $settings['bridge_setup'] ?? $this->defaultBridgeSetup(); + } + private function saveCustomerSettings(int $customerId, array $data): array { if ($customerId <= 0) return []; $this->ensureCustomerSettingsTableExists(); - $allowed = ['bridge_url', 'bridge_token', 'sender_token', 'external_api_token', 'bridge_tables']; + $allowed = ['bridge_url', 'bridge_token', 'sender_token', 'external_api_token', 'bridge_tables', 'bridge_setup']; $fields = array_intersect_key($data, array_flip($allowed)); if (!$fields) return $this->getCustomerSettings($customerId); if (array_key_exists('bridge_tables', $fields)) { $normalized = $this->normalizeBridgeTables($fields['bridge_tables']); $fields['bridge_tables'] = $normalized ? $this->encodeBridgeTables($normalized) : null; } + if (array_key_exists('bridge_setup', $fields)) { + $fields['bridge_setup'] = $this->encodeBridgeSetup($fields['bridge_setup']); + } $fields['customer_id'] = $customerId; $columns = array_keys($fields); $insertCols = implode(',', array_map(fn($c) => "`$c`", $columns)); @@ -1917,6 +1969,12 @@ class ApiKernel return $this->getCustomerSettings($customerId); } + private function saveBridgeSetupData(int $customerId, array $setup): array + { + $settings = $this->saveCustomerSettings($customerId, ['bridge_setup' => $setup]); + return $settings['bridge_setup'] ?? $this->defaultBridgeSetup(); + } + private function ensureSettingsTokens(int $customerId, array $settings): array { if ($customerId <= 0) return $settings; @@ -1940,6 +1998,11 @@ class ApiKernel } else { $row['bridge_tables'] = []; } + if (array_key_exists('bridge_setup', $row)) { + $row['bridge_setup'] = $this->decodeBridgeSetup($row['bridge_setup']); + } else { + $row['bridge_setup'] = $this->defaultBridgeSetup(); + } return $row; } @@ -1982,25 +2045,104 @@ class ApiKernel return $this->normalizeBridgeTables($str); } - private function filterSchemaTables(array $tables, array $allowed): array + private function encodeBridgeSetup($setup) { - if (empty($allowed)) return $tables; - $allowedLower = array_map('strtolower', $allowed); - $filtered = []; - foreach ($tables as $entry) { - if (is_array($entry)) { - $name = strtolower((string)($entry['name'] ?? $entry['table'] ?? $entry['label'] ?? '')); - if ($name !== '' && in_array($name, $allowedLower, true)) { - $filtered[] = $entry; - } - } else { - $name = strtolower((string)$entry); - if ($name !== '' && in_array($name, $allowedLower, true)) { - $filtered[] = $entry; - } - } + if (is_array($setup)) { + $setup = $this->sanitizeBridgeSetup($setup); + return json_encode($setup, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } - return $filtered; + if (is_string($setup)) { + return $setup; + } + return null; + } + + private function decodeBridgeSetup($stored): array + { + if (is_array($stored)) { + return $this->sanitizeBridgeSetup($stored); + } + $str = (string)$stored; + if ($str === '') { + return $this->defaultBridgeSetup(); + } + $decoded = json_decode($str, true); + if (is_array($decoded)) { + return $this->sanitizeBridgeSetup($decoded); + } + return $this->defaultBridgeSetup(); + } + + private function defaultBridgeSetup(): array + { + return [ + 'tables' => [], + 'mode' => 'direct', + 'direct' => [ + 'host' => '', + 'port' => 3306, + 'database' => '', + 'user' => '', + 'password' => '', + 'charset' => 'utf8mb4', + ], + 'config' => [ + 'file' => '', + 'base' => '', + 'host_key' => '', + 'port_key' => '', + 'database_key' => '', + 'user_key' => '', + 'password_key' => '', + 'charset_key' => '', + ], + ]; + } + + private function sanitizeBridgeSetup(?array $input): array + { + $defaults = $this->defaultBridgeSetup(); + if (!is_array($input)) { + return $defaults; + } + $mode = strtolower((string)($input['mode'] ?? 'direct')); + if (!in_array($mode, ['direct', 'config'], true)) { + $mode = 'direct'; + } + $tables = $this->normalizeBridgeTables($input['tables'] ?? []); + $direct = $input['direct'] ?? []; + $config = $input['config'] ?? []; + $sanitizePath = function ($value) { + $value = trim((string)$value); + if ($value === '') return ''; + return preg_replace('/[^a-zA-Z0-9_\.\-]/', '', $value) ?: ''; + }; + $result = [ + 'tables' => $tables, + 'mode' => $mode, + 'direct' => [ + 'host' => trim((string)($direct['host'] ?? $defaults['direct']['host'])), + 'port' => (int)($direct['port'] ?? $defaults['direct']['port']), + 'database' => trim((string)($direct['database'] ?? $defaults['direct']['database'])), + 'user' => trim((string)($direct['user'] ?? $defaults['direct']['user'])), + 'password' => (string)($direct['password'] ?? $defaults['direct']['password']), + 'charset' => trim((string)($direct['charset'] ?? $defaults['direct']['charset'])) ?: 'utf8mb4', + ], + 'config' => [ + 'file' => trim((string)($config['file'] ?? '')), + 'base' => $sanitizePath($config['base'] ?? ''), + 'host_key' => $sanitizePath($config['host_key'] ?? ''), + 'port_key' => $sanitizePath($config['port_key'] ?? ''), + 'database_key' => $sanitizePath($config['database_key'] ?? ''), + 'user_key' => $sanitizePath($config['user_key'] ?? ''), + 'password_key' => $sanitizePath($config['password_key'] ?? ''), + 'charset_key' => $sanitizePath($config['charset_key'] ?? ''), + ], + ]; + if ($result['direct']['port'] <= 0) { + $result['direct']['port'] = 3306; + } + return $result; } private function customerSettingsTable(): string @@ -2056,6 +2198,9 @@ SQL; if (!in_array('bridge_tables', $columns, true)) { $missing[] = 'ADD COLUMN `bridge_tables` text DEFAULT NULL'; } + if (!in_array('bridge_setup', $columns, true)) { + $missing[] = 'ADD COLUMN `bridge_setup` longtext DEFAULT NULL'; + } if (!$missing) { return; @@ -2380,13 +2525,13 @@ SQL; return ''; } - private function populateBridgeDownload(string $content, array $settings): string + private function populateBridgeDownload(string $content, array $settings, array $setup): string { $token = (string)($settings['bridge_token'] ?? ''); $content = str_replace('REPLACE_WITH_SHARED_TOKEN', $token, $content); - $tables = []; - if (!empty($settings['bridge_tables']) && is_array($settings['bridge_tables'])) { + $tables = array_values(array_filter(array_map('strval', $setup['tables'] ?? []))); + if (!$tables && !empty($settings['bridge_tables']) && is_array($settings['bridge_tables'])) { $tables = array_values(array_filter(array_map('strval', $settings['bridge_tables']))); } $tablesExport = $this->exportPhpArray($tables); @@ -2397,9 +2542,148 @@ SQL; 1 ); + $mode = strtolower((string)($setup['mode'] ?? 'direct')); + if ($mode === 'direct') { + $dsn = $this->buildBridgeDsn($setup['direct'] ?? []); + if ($dsn !== '') { + $dsnValue = var_export($dsn, true); + $content = preg_replace("/'dsn'\\s*=>\\s*'[^']*',/", "'dsn' => {$dsnValue},", $content, 1); + } + $userValue = var_export((string)($setup['direct']['user'] ?? ''), true); + $passValue = var_export((string)($setup['direct']['password'] ?? ''), true); + $content = preg_replace("/'user'\\s*=>\\s*'[^']*',/", "'user' => {$userValue},", $content, 1); + $content = preg_replace("/'pass'\\s*=>\\s*'[^']*',/", "'pass' => {$passValue},", $content, 1); + } + + $snippet = $this->buildBridgeSetupSnippet($setup); + if (strpos($content, '// {{BRIDGE_DB_SETUP}}') !== false) { + $content = str_replace('// {{BRIDGE_DB_SETUP}}', $snippet, $content); + } else { + $content .= "\n" . $snippet; + } + return $content; } + private function buildBridgeDsn(array $direct): string + { + $host = trim((string)($direct['host'] ?? '')); + $db = trim((string)($direct['database'] ?? '')); + if ($host === '' || $db === '') { + return ''; + } + $port = (int)($direct['port'] ?? 3306); + $charset = trim((string)($direct['charset'] ?? 'utf8mb4')) ?: 'utf8mb4'; + $parts = ["mysql:host={$host}"]; + if ($port > 0) { + $parts[] = 'port=' . $port; + } + $parts[] = 'dbname=' . $db; + $parts[] = 'charset=' . $charset; + return implode(';', $parts); + } + + private function buildBridgeSetupSnippet(array $setup): string + { + $mode = strtolower((string)($setup['mode'] ?? 'direct')); + if ($mode !== 'config') { + return "// Bridge DB Setup: direkte Angaben aus dem EmailTemplate-Backend.\n"; + } + $config = $setup['config'] ?? []; + $file = trim((string)($config['file'] ?? '')); + if ($file === '') { + return "// Bridge DB Setup: Bitte im EmailTemplate-Backend eine Konfigurationsdatei angeben.\n"; + } + + $base = trim((string)($config['base'] ?? '')); + $paths = [ + 'host' => $this->bridgeCombinePath($base, $config['host_key'] ?? ''), + 'port' => $this->bridgeCombinePath($base, $config['port_key'] ?? ''), + 'database' => $this->bridgeCombinePath($base, $config['database_key'] ?? ''), + 'user' => $this->bridgeCombinePath($base, $config['user_key'] ?? ''), + 'password' => $this->bridgeCombinePath($base, $config['password_key'] ?? ''), + 'charset' => $this->bridgeCombinePath($base, $config['charset_key'] ?? ''), + ]; + $defaults = [ + 'host' => $this->bridgeCombinePath($base, 'host'), + 'port' => $this->bridgeCombinePath($base, 'port'), + 'database' => $this->bridgeCombinePath($base, 'database'), + 'user' => $this->bridgeCombinePath($base, 'user'), + 'password' => $this->bridgeCombinePath($base, 'password'), + 'charset' => $this->bridgeCombinePath($base, 'charset'), + ]; + foreach ($paths as $key => $value) { + if ($value === '') { + $paths[$key] = $defaults[$key]; + } + } + + $fileExpr = $this->bridgeConfigFileExpression($file); + $baseExport = var_export($base, true); + $lines = []; + $lines[] = '/** Bridge DB Setup: automatisch generiertes Mapping */'; + $lines[] = '$bridgeConfigFile = ' . $fileExpr . ';'; + $lines[] = '$bridgeConfigSource = is_file($bridgeConfigFile) ? include $bridgeConfigFile : null;'; + $lines[] = 'if (is_array($bridgeConfigSource)) {'; + $lines[] = ' $bridgeConfigData = $bridgeConfigSource;'; + $lines[] = " if ({$baseExport} !== '') {"; + $lines[] = " $bridgeConfigData = bridge_array_get($bridgeConfigSource, {$baseExport}, $bridgeConfigData);"; + $lines[] = ' }'; + $lines[] = ' if (!is_array($bridgeConfigData)) {'; + $lines[] = ' $bridgeConfigData = (array)$bridgeConfigData;'; + $lines[] = ' }'; + $lines[] = ' $bridgeDbHost = (string)bridge_array_get($bridgeConfigData, ' . var_export($paths['host'], true) . ', \'\');'; + $lines[] = ' $bridgeDbName = (string)bridge_array_get($bridgeConfigData, ' . var_export($paths['database'], true) . ', \'\');'; + $lines[] = ' $bridgeDbUser = (string)bridge_array_get($bridgeConfigData, ' . var_export($paths['user'], true) . ', \'\');'; + $lines[] = ' $bridgeDbPass = (string)bridge_array_get($bridgeConfigData, ' . var_export($paths['password'], true) . ', \'\');'; + $lines[] = ' $bridgeDbCharset = (string)bridge_array_get($bridgeConfigData, ' . var_export($paths['charset'], true) . ', \'utf8mb4\');'; + $lines[] = ' $bridgeDbPort = (int)bridge_array_get($bridgeConfigData, ' . var_export($paths['port'], true) . ', 3306);'; + $lines[] = ' if ($bridgeDbHost !== \'\' && $bridgeDbName !== \'\') {'; + $lines[] = ' $bridgeDsnParts = ["mysql:host={$bridgeDbHost}"];'; + $lines[] = ' if ($bridgeDbPort > 0) {'; + $lines[] = ' $bridgeDsnParts[] = "port={$bridgeDbPort}";'; + $lines[] = ' }'; + $lines[] = ' $bridgeDbCharset = $bridgeDbCharset ?: \'utf8mb4\';'; + $lines[] = ' $bridgeDsnParts[] = "dbname={$bridgeDbName}";'; + $lines[] = ' $bridgeDsnParts[] = "charset={$bridgeDbCharset}";'; + $lines[] = ' $bridgeConfig[\'db\'][\'dsn\'] = implode(\';\', $bridgeDsnParts);'; + $lines[] = ' }'; + $lines[] = ' if ($bridgeDbUser !== \'\') {'; + $lines[] = ' $bridgeConfig[\'db\'][\'user\'] = $bridgeDbUser;'; + $lines[] = ' }'; + $lines[] = ' if ($bridgeDbPass !== \'\') {'; + $lines[] = ' $bridgeConfig[\'db\'][\'pass\'] = $bridgeDbPass;'; + $lines[] = ' }'; + $lines[] = '}'; + + return implode("\n", $lines) . "\n"; + } + + private function bridgeConfigFileExpression(string $file): string + { + if ($file === '') { + return var_export('', true); + } + if ($file[0] === '/' || preg_match('~^[A-Za-z]:\\\\~', $file)) { + return var_export($file, true); + } + $normalized = '/' . ltrim($file, '/'); + return '__DIR__ . ' . var_export($normalized, true); + } + + private function bridgeCombinePath(string $base, string $key): string + { + $base = trim($base, '.'); + $key = trim($key, '.'); + if ($base !== '' && $key !== '') { + return $base . '.' . $key; + } + if ($base !== '') { + return $base; + } + return $key; + } + private function populateSenderDownload(string $content, array $settings): string { $content = str_replace('REPLACE_WITH_SHARED_TOKEN', (string)($settings['sender_token'] ?? ''), $content);