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 @@
+
+
+
+
+
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);