This commit is contained in:
2025-12-09 00:21:01 +01:00
parent 9488fe1ea4
commit ad89392ff1
8 changed files with 788 additions and 124 deletions

View File

@@ -35,6 +35,8 @@ $bridgeConfig = [
'tables_allow' => [], // optional whitelist: ['customers', 'orders'] 'tables_allow' => [], // optional whitelist: ['customers', 'orders']
]; ];
// {{BRIDGE_DB_SETUP}}
$localOverride = __DIR__ . '/emailtemplate.bridge.conf.php'; $localOverride = __DIR__ . '/emailtemplate.bridge.conf.php';
if (is_file($localOverride)) { if (is_file($localOverride)) {
$override = include $localOverride; $override = include $localOverride;
@@ -96,6 +98,35 @@ function bridgeDb(array $config): PDO
return $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); bridgeRequireToken($bridgeConfig);
$action = strtolower((string)($_GET['action'] ?? $_POST['action'] ?? 'schema')); $action = strtolower((string)($_GET['action'] ?? $_POST['action'] ?? 'schema'));

View File

@@ -0,0 +1,158 @@
<?php
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
$appBaseUrl = rtrim($GLOBALS['app_base_url'] ?? '', '/');
$assetBase = $appBaseUrl !== '' ? $appBaseUrl : '';
$appApiBase = rtrim($GLOBALS['app_api_base'] ?? '', '/');
$debugRedirect = isset($_GET['debug_redirect']);
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Email Template System Bridge Setup</title>
<script>document.documentElement.classList.add('auth-pending');</script>
<style>html.auth-pending body{visibility:hidden;}</style>
<script>
window.APP_BASE_URL = <?= json_encode($appBaseUrl, JSON_UNESCAPED_SLASHES) ?>;
window.APP_API_BASE = <?= json_encode($appApiBase, JSON_UNESCAPED_SLASHES) ?>;
<?php if ($debugRedirect): ?>
window.DISABLE_AUTH_REDIRECT = true;
<?php endif; ?>
</script>
<script src="https://cdn.tailwindcss.com"></script>
<?php if ($debugRedirect): ?>
<script src="<?= $assetBase ?>/assets/js/debug-location.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
<?php endif; ?>
<link rel="stylesheet" href="<?= $assetBase ?>/assets/css/admin.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
<link rel="stylesheet" href="<?= $assetBase ?>/assets/css/toast.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
<style>
:root { color-scheme: light; }
.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.35rem .7rem;border-radius:.7rem;border:1px solid #e5e7eb;background:#fff;font-size:.9rem;cursor:pointer;}
.btn:hover{background:#f8fafc}
.btn-avatar{padding:.35rem;border-radius:999px;width:42px;height:42px;justify-content:center;font-weight:600;background:#0ea5e9;color:#fff;border:none}
.btn-avatar:hover{background:#0284c7}
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
.section-card h4{margin:0 0 1rem;font-size:1rem;font-weight:600;color:#0f172a}
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
.badge{display:inline-flex;align-items:center;padding:.1rem .5rem;border-radius:999px;font-size:.75rem;background:#e2e8f0;color:#0f172a}
.chip{display:inline-flex;align-items:center;padding:.15rem .55rem;border-radius:999px;background:#f1f5f9;color:#0f172a;border:1px solid #e2e8f0;font-size:.8rem}
</style>
</head>
<body class="bg-slate-50 text-slate-800" data-page="bridge-setup">
<header class="sticky top-0 z-30 bg-white/90 border-b backdrop-blur">
<div class="max-w-4xl mx-auto px-4 py-4 flex items-center gap-3">
<a href="<?= $appBaseUrl ?>/admin/settings.php" class="btn" title="Zurück zur Administration">← Administration</a>
<h1 class="font-semibold text-lg">Bridge Setup</h1>
<div class="ms-auto flex gap-2 items-center">
<div class="relative" id="userMenu">
<button id="btn-user" type="button" class="btn-avatar" aria-haspopup="true" aria-expanded="false">
<span id="userAvatar">U</span>
</button>
<div id="userMenuPanel" class="user-menu hidden" role="menu">
<a href="<?= $appBaseUrl ?>/admin/profile.php" class="user-menu-item" data-menu="profile">Profil</a>
<a href="<?= $appBaseUrl ?>/admin/dashboard.php" class="user-menu-item" data-role="admin">Dashboard</a>
<a href="<?= $appBaseUrl ?>/admin/settings.php" class="user-menu-item" data-role="admin">Administration</a>
<button id="btn-logout" type="button" class="user-menu-item text-red-600">Logout</button>
</div>
</div>
</div>
</div>
</header>
<main class="max-w-4xl mx-auto p-4 md:p-6 space-y-6">
<section class="section-card">
<h4>Bridge-Datei vorbereiten</h4>
<p class="text-sm text-slate-600 mb-3">
Diese Angaben werden nur verwendet, um die <strong>emailtemplate_bridge.php</strong> zu generieren. Das EmailTemplate-System selbst behält Zugriff auf alle Tabellen; die hier definierten Whitelists greifen ausschließlich in der Bridge-Datei.
</p>
<form id="bridgeSetupForm" class="space-y-4">
<div>
<label class="block text-sm text-slate-600">Tabellen-Whitelist (optional)</label>
<textarea name="tables" class="input mt-1" rows="3" placeholder="z.B. customers, orders"></textarea>
<p class="text-xs text-slate-500 mt-1">Kommagetrennt oder je Zeile eine Tabelle. Leer lassen = keine Einschränkung.</p>
<div id="selectedTables" class="flex flex-wrap gap-2 text-sm text-slate-600 mt-2">Noch keine Tabellen angegeben.</div>
</div>
<fieldset class="border border-slate-200 rounded-xl p-4">
<legend class="px-2 text-sm font-semibold">Datenbankquelle</legend>
<div class="flex flex-wrap gap-4 text-sm text-slate-600 mb-3">
<label class="inline-flex items-center gap-2">
<input type="radio" name="db_mode" value="direct" checked> Direkte Angaben (Host, DB, Benutzer …)
</label>
<label class="inline-flex items-center gap-2">
<input type="radio" name="db_mode" value="config"> Aus bestehender Konfigurationsdatei laden
</label>
</div>
<div id="directFields" class="grid md:grid-cols-2 gap-3">
<label class="block text-sm text-slate-600">Server / Host
<input type="text" name="direct_host" class="input mt-1" placeholder="127.0.0.1">
</label>
<label class="block text-sm text-slate-600">Port
<input type="number" name="direct_port" class="input mt-1" placeholder="3306">
</label>
<label class="block text-sm text-slate-600">Datenbankname
<input type="text" name="direct_database" class="input mt-1" placeholder="kunden_db">
</label>
<label class="block text-sm text-slate-600">Zeichensatz
<input type="text" name="direct_charset" class="input mt-1" value="utf8mb4">
</label>
<label class="block text-sm text-slate-600">Benutzername
<input type="text" name="direct_user" class="input mt-1" placeholder="db_user">
</label>
<label class="block text-sm text-slate-600">Passwort
<input type="text" name="direct_password" class="input mt-1" placeholder="••••">
</label>
</div>
<div id="configFields" class="hidden space-y-3">
<div>
<label class="block text-sm text-slate-600">Pfad zur Konfigurationsdatei</label>
<input type="text" name="config_file" class="input mt-1" placeholder="../config/database.php">
<p class="text-xs text-slate-500 mt-1">Relativ zur Bridge-Datei oder absolut. Die Datei sollte ein Array oder Objekt mit den Zugangsdaten liefern.</p>
</div>
<div>
<label class="block text-sm text-slate-600">Basis-Pfad im Array (optional)</label>
<input type="text" name="config_base" class="input mt-1" placeholder="database.connections.mysql">
<p class="text-xs text-slate-500 mt-1">Dot-Notation, um in verschachtelte Arrays zu springen.</p>
</div>
<div class="grid md:grid-cols-2 gap-3">
<label class="block text-sm text-slate-600">Host-Key
<input type="text" name="config_host_key" class="input mt-1" placeholder="host">
</label>
<label class="block text-sm text-slate-600">Port-Key
<input type="text" name="config_port_key" class="input mt-1" placeholder="port">
</label>
<label class="block text-sm text-slate-600">DB-Name-Key
<input type="text" name="config_database_key" class="input mt-1" placeholder="database">
</label>
<label class="block text-sm text-slate-600">Charset-Key
<input type="text" name="config_charset_key" class="input mt-1" placeholder="charset">
</label>
<label class="block text-sm text-slate-600">User-Key
<input type="text" name="config_user_key" class="input mt-1" placeholder="username">
</label>
<label class="block text-sm text-slate-600">Passwort-Key
<input type="text" name="config_password_key" class="input mt-1" placeholder="password">
</label>
</div>
<p class="text-xs text-slate-500">Alle Keys nutzen Dot-Notation relativ zum Basis-Pfad.</p>
</div>
</fieldset>
<div class="flex flex-wrap gap-2">
<button type="button" id="btn-load-remote" class="btn" data-role="admin">Tabellen vom Bridge-Endpunkt laden</button>
<button type="submit" class="btn">Bridge-Setup speichern</button>
</div>
<div id="setupStatus" class="text-xs text-slate-500">Noch nicht gespeichert.</div>
</form>
</section>
</main>
<div id="toast-root"></div>
<script src="<?= $assetBase ?>/assets/js/toast.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
<script type="module" src="<?= $assetBase ?>/assets/js/bridge-setup.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
</body>
</html>

View File

@@ -170,14 +170,9 @@ $debugRedirect = isset($_GET['debug_redirect']);
<button type="button" class="btn" data-rotate="external">Neu erstellen</button> <button type="button" class="btn" data-rotate="external">Neu erstellen</button>
</div> </div>
</div> </div>
<div>
<label class="block text-sm text-slate-600">Verfügbare Tabellen (kommagetrennt oder Zeilen)</label>
<textarea name="bridge_tables" class="input mt-1" rows="3" placeholder="z.B. customers, orders"></textarea>
<p class="text-xs text-slate-500 mt-1">Nur diese Tabellen werden als Platzhalter angeboten. Leer = alle.</p>
</div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<button type="button" id="btn-validate-bridge" class="btn w-max" data-role="admin">Verbindung prüfen & Tabellen laden</button> <a href="<?= $appBaseUrl ?>/admin/bridge.php" class="btn w-max" data-role="admin">Bridge-Setup & Tabellen öffnen</a>
<div id="bridgeTablesPreview" class="flex flex-wrap gap-2 text-sm text-slate-600">Noch nicht geprüft.</div> <p class="text-xs text-slate-500">Dort kannst du Tabellen-Filter sowie DB-Quellen für die Bridge-Datei konfigurieren.</p>
</div> </div>
<div class="flex justify-between gap-2 flex-wrap pt-2"> <div class="flex justify-between gap-2 flex-wrap pt-2">
<div class="flex gap-2" data-role="admin"> <div class="flex gap-2" data-role="admin">

3
public/admin/bridge.php Normal file
View File

@@ -0,0 +1,3 @@
<?php
require_once $_SERVER['DOCUMENT_ROOT'] . '../config/fileload.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '../partials/landingpage/admin/bridge.php';

View File

@@ -0,0 +1,242 @@
import { apiAction, toast } from './api.js';
const state = {
setup: null,
loading: false,
};
let form;
let tablesInput;
let tablesPreview;
let modeInputs;
let directFields;
let configFields;
let statusLabel;
let loadBtn;
export function initBridgeSetupPage() {
form = document.getElementById('bridgeSetupForm');
if (!form) return;
tablesInput = form.elements.tables;
tablesPreview = document.getElementById('selectedTables');
directFields = document.getElementById('directFields');
configFields = document.getElementById('configFields');
statusLabel = document.getElementById('setupStatus');
loadBtn = document.getElementById('btn-load-remote');
modeInputs = Array.from(form.querySelectorAll('input[name="db_mode"]'));
form.addEventListener('submit', submitBridgeSetup);
tablesInput?.addEventListener('input', () => 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 = '<span class="text-xs text-slate-500">Noch keine Tabellen angegeben.</span>';
return;
}
tablesPreview.innerHTML = list.map(name => `<span class="chip">${escapeHtml(name)}</span>`).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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

View File

@@ -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' });
});

View File

@@ -7,7 +7,6 @@ const state = {
userMap: new Map(), userMap: new Map(),
senders: [], senders: [],
senderMap: new Map(), senderMap: new Map(),
bridgeTables: [],
currentTab: 'profile', currentTab: 'profile',
loading: false, loading: false,
}; };
@@ -25,8 +24,6 @@ let teamTable;
let userForm; let userForm;
let senderTable; let senderTable;
let senderForm; let senderForm;
let bridgePreview;
let validateBridgeBtn;
let menuInitialized = false; let menuInitialized = false;
let menuOpen = false; let menuOpen = false;
let debugButton; let debugButton;
@@ -64,8 +61,6 @@ export function initAccountPage() {
userForm = document.getElementById('userForm'); userForm = document.getElementById('userForm');
senderTable = document.getElementById('senderTable'); senderTable = document.getElementById('senderTable');
senderForm = document.getElementById('senderForm'); senderForm = document.getElementById('senderForm');
bridgePreview = document.getElementById('bridgeTablesPreview');
validateBridgeBtn = document.getElementById('btn-validate-bridge');
document.getElementById('btn-user-add')?.addEventListener('click', () => openUserForm()); document.getElementById('btn-user-add')?.addEventListener('click', () => openUserForm());
document.getElementById('userFormCancel')?.addEventListener('click', () => closeUserForm()); document.getElementById('userFormCancel')?.addEventListener('click', () => closeUserForm());
@@ -80,8 +75,6 @@ export function initAccountPage() {
settingsForm?.addEventListener('submit', submitSettingsForm); settingsForm?.addEventListener('submit', submitSettingsForm);
teamTable?.addEventListener('click', handleTeamTableClick); teamTable?.addEventListener('click', handleTeamTableClick);
senderTable?.addEventListener('click', handleSenderTableClick); senderTable?.addEventListener('click', handleSenderTableClick);
validateBridgeBtn?.addEventListener('click', validateBridgeSettings);
document.querySelectorAll('[data-user-tab]').forEach(btn => { document.querySelectorAll('[data-user-tab]').forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab'))); btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
}); });
@@ -250,11 +243,6 @@ function fillSettingsForm(settings) {
settingsForm.bridge_token.value = settings.bridge_token || ''; settingsForm.bridge_token.value = settings.bridge_token || '';
settingsForm.sender_token.value = settings.sender_token || ''; settingsForm.sender_token.value = settings.sender_token || '';
settingsForm.external_api_token.value = settings.external_api_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 }; 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_bridge_token: state.rotate.bridge ? 1 : 0,
rotate_sender_token: state.rotate.sender ? 1 : 0, rotate_sender_token: state.rotate.sender ? 1 : 0,
rotate_external_token: state.rotate.external ? 1 : 0, rotate_external_token: state.rotate.external ? 1 : 0,
bridge_tables: parseBridgeTablesInput(),
}; };
try { try {
const res = await apiAction('account.settings.update', { method: 'POST', data }); 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 = '<span class="text-xs text-slate-500">Keine Einschränkung alle Tabellen erlaubt.</span>';
return;
}
bridgePreview.innerHTML = state.bridgeTables.map(name => `<span class="chip">${escapeHtml(name)}</span>`).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() { async function loadUsers() {
try { try {
const res = await apiAction('account.users.list', { method: 'GET' }); const res = await apiAction('account.users.list', { method: 'GET' });

View File

@@ -876,6 +876,12 @@ class ApiKernel
case 'downloads.sender': case 'downloads.sender':
$this->handleDownloadFile('sender'); $this->handleDownloadFile('sender');
break; break;
case 'account.bridge.setup.get':
$this->handleAccountBridgeSetupGet();
break;
case 'account.bridge.setup.save':
$this->handleAccountBridgeSetupSave();
break;
case 'account.bridge.test': case 'account.bridge.test':
$this->handleAccountBridgeTest(); $this->handleAccountBridgeTest();
break; break;
@@ -1272,15 +1278,9 @@ class ApiKernel
return; return;
} }
$settings = $this->getCustomerSettings($customerId);
$tables = $schema['tables'] ?? [];
if (!empty($settings['bridge_tables'])) {
$tables = $this->filterSchemaTables($tables, $settings['bridge_tables']);
}
$this->respond([ $this->respond([
'ok' => true, 'ok' => true,
'tables' => $tables, 'tables' => $schema['tables'] ?? [],
'fetched' => $schema['fetched'] ?? date(DATE_ATOM), 'fetched' => $schema['fetched'] ?? date(DATE_ATOM),
]); ]);
} }
@@ -1466,8 +1466,6 @@ class ApiKernel
$bridgeToken = trim((string)($this->in['bridge_token'] ?? '')); $bridgeToken = trim((string)($this->in['bridge_token'] ?? ''));
$senderToken = trim((string)($this->in['sender_token'] ?? '')); $senderToken = trim((string)($this->in['sender_token'] ?? ''));
$externalToken = trim((string)($this->in['external_api_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']); $rotateBridge = !empty($this->in['rotate_bridge_token']);
$rotateSender = !empty($this->in['rotate_sender_token']); $rotateSender = !empty($this->in['rotate_sender_token']);
$rotateExternal = !empty($this->in['rotate_external_token']); $rotateExternal = !empty($this->in['rotate_external_token']);
@@ -1486,7 +1484,6 @@ class ApiKernel
'bridge_token' => $bridgeToken, 'bridge_token' => $bridgeToken,
'sender_token' => $senderToken, 'sender_token' => $senderToken,
'external_api_token' => $externalToken, 'external_api_token' => $externalToken,
'bridge_tables' => $bridgeTables,
]); ]);
$this->respond(['ok' => true, 'settings' => $settings]); $this->respond(['ok' => true, 'settings' => $settings]);
@@ -1809,9 +1806,10 @@ class ApiKernel
if ($customerId <= 0) $this->fail('Customer context missing', null, 500); if ($customerId <= 0) $this->fail('Customer context missing', null, 500);
$settings = $this->ensureSettingsTokens($customerId, $this->getCustomerSettings($customerId)); $settings = $this->ensureSettingsTokens($customerId, $this->getCustomerSettings($customerId));
$bridgeSetup = $this->getBridgeSetupData($customerId);
$content = $this->loadDownloadTemplate($type); $content = $this->loadDownloadTemplate($type);
if ($type === 'bridge') { if ($type === 'bridge') {
$content = $this->populateBridgeDownload($content, $settings); $content = $this->populateBridgeDownload($content, $settings, $bridgeSetup);
} else { } else {
$content = $this->populateSenderDownload($content, $settings); $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 private function handleAccountBridgeTest(): void
{ {
$user = $this->requireAuth(); $user = $this->requireAuth();
@@ -1838,20 +1886,15 @@ class ApiKernel
if ($bridgeUrl === '' || $bridgeToken === '') { if ($bridgeUrl === '' || $bridgeToken === '') {
$this->fail('Bridge nicht konfiguriert', null, 422); $this->fail('Bridge nicht konfiguriert', null, 422);
} }
$settings = $this->getCustomerSettings($customerId);
try { try {
$schema = $this->fetchPlaceholderSchema($bridgeUrl, $bridgeToken, 0); $schema = $this->fetchPlaceholderSchema($bridgeUrl, $bridgeToken, 0);
} catch (Throwable $e) { } catch (Throwable $e) {
$this->fail('Bridge request failed', $e->getMessage(), 502); $this->fail('Bridge request failed', $e->getMessage(), 502);
return; return;
} }
$tables = $schema['tables'] ?? [];
if (!empty($settings['bridge_tables'])) {
$tables = $this->filterSchemaTables($tables, $settings['bridge_tables']);
}
$this->respond([ $this->respond([
'ok' => true, 'ok' => true,
'tables' => $tables, 'tables' => $schema['tables'] ?? [],
'fetched' => $schema['fetched'] ?? date(DATE_ATOM), 'fetched' => $schema['fetched'] ?? date(DATE_ATOM),
]); ]);
} }
@@ -1887,17 +1930,26 @@ class ApiKernel
return $row ? $this->formatCustomerSettingsRow($row) : []; 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 private function saveCustomerSettings(int $customerId, array $data): array
{ {
if ($customerId <= 0) return []; if ($customerId <= 0) return [];
$this->ensureCustomerSettingsTableExists(); $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)); $fields = array_intersect_key($data, array_flip($allowed));
if (!$fields) return $this->getCustomerSettings($customerId); if (!$fields) return $this->getCustomerSettings($customerId);
if (array_key_exists('bridge_tables', $fields)) { if (array_key_exists('bridge_tables', $fields)) {
$normalized = $this->normalizeBridgeTables($fields['bridge_tables']); $normalized = $this->normalizeBridgeTables($fields['bridge_tables']);
$fields['bridge_tables'] = $normalized ? $this->encodeBridgeTables($normalized) : null; $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; $fields['customer_id'] = $customerId;
$columns = array_keys($fields); $columns = array_keys($fields);
$insertCols = implode(',', array_map(fn($c) => "`$c`", $columns)); $insertCols = implode(',', array_map(fn($c) => "`$c`", $columns));
@@ -1917,6 +1969,12 @@ class ApiKernel
return $this->getCustomerSettings($customerId); 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 private function ensureSettingsTokens(int $customerId, array $settings): array
{ {
if ($customerId <= 0) return $settings; if ($customerId <= 0) return $settings;
@@ -1940,6 +1998,11 @@ class ApiKernel
} else { } else {
$row['bridge_tables'] = []; $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; return $row;
} }
@@ -1982,25 +2045,104 @@ class ApiKernel
return $this->normalizeBridgeTables($str); return $this->normalizeBridgeTables($str);
} }
private function filterSchemaTables(array $tables, array $allowed): array private function encodeBridgeSetup($setup)
{ {
if (empty($allowed)) return $tables; if (is_array($setup)) {
$allowedLower = array_map('strtolower', $allowed); $setup = $this->sanitizeBridgeSetup($setup);
$filtered = []; return json_encode($setup, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
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;
}
}
} }
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 private function customerSettingsTable(): string
@@ -2056,6 +2198,9 @@ SQL;
if (!in_array('bridge_tables', $columns, true)) { if (!in_array('bridge_tables', $columns, true)) {
$missing[] = 'ADD COLUMN `bridge_tables` text DEFAULT NULL'; $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) { if (!$missing) {
return; return;
@@ -2380,13 +2525,13 @@ SQL;
return ''; return '';
} }
private function populateBridgeDownload(string $content, array $settings): string private function populateBridgeDownload(string $content, array $settings, array $setup): string
{ {
$token = (string)($settings['bridge_token'] ?? ''); $token = (string)($settings['bridge_token'] ?? '');
$content = str_replace('REPLACE_WITH_SHARED_TOKEN', $token, $content); $content = str_replace('REPLACE_WITH_SHARED_TOKEN', $token, $content);
$tables = []; $tables = array_values(array_filter(array_map('strval', $setup['tables'] ?? [])));
if (!empty($settings['bridge_tables']) && is_array($settings['bridge_tables'])) { if (!$tables && !empty($settings['bridge_tables']) && is_array($settings['bridge_tables'])) {
$tables = array_values(array_filter(array_map('strval', $settings['bridge_tables']))); $tables = array_values(array_filter(array_map('strval', $settings['bridge_tables'])));
} }
$tablesExport = $this->exportPhpArray($tables); $tablesExport = $this->exportPhpArray($tables);
@@ -2397,9 +2542,148 @@ SQL;
1 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; 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 private function populateSenderDownload(string $content, array $settings): string
{ {
$content = str_replace('REPLACE_WITH_SHARED_TOKEN', (string)($settings['sender_token'] ?? ''), $content); $content = str_replace('REPLACE_WITH_SHARED_TOKEN', (string)($settings['sender_token'] ?? ''), $content);