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

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