From 24c7c215eee1ccef7a10ec8ef631de723374b0e8 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Sun, 7 Dec 2025 02:18:06 +0100 Subject: [PATCH] download --- download/emailtemplate_bridge.php | 163 ++++++++++++++++ download/emailtemplate_sender.php | 308 ++++++++++++++++++++++++++++++ 2 files changed, 471 insertions(+) create mode 100644 download/emailtemplate_bridge.php create mode 100644 download/emailtemplate_sender.php diff --git a/download/emailtemplate_bridge.php b/download/emailtemplate_bridge.php new file mode 100644 index 0000000..0a18b87 --- /dev/null +++ b/download/emailtemplate_bridge.php @@ -0,0 +1,163 @@ + getenv('EMAILTEMPLATE_BRIDGE_TOKEN') ?: 'REPLACE_WITH_SHARED_TOKEN', + 'db' => [ + 'dsn' => getenv('EMAILTEMPLATE_BRIDGE_DSN') ?: 'mysql:host=127.0.0.1;dbname=example;charset=utf8mb4', + 'user' => getenv('EMAILTEMPLATE_BRIDGE_DB_USER') ?: 'root', + 'pass' => getenv('EMAILTEMPLATE_BRIDGE_DB_PASS') ?: '', + 'options' => [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ], + ], +]; + +$localOverride = __DIR__ . '/emailtemplate.bridge.conf.php'; +if (is_file($localOverride)) { + $override = include $localOverride; + if (is_array($override)) { + $bridgeConfig = array_replace_recursive($bridgeConfig, $override); + } +} + +function bridgeRespond($payload, int $status = 200): void +{ + http_response_code($status); + header('Content-Type: application/json; charset=utf-8'); + header('Cache-Control: no-store, max-age=0'); + echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +function bridgeRequireToken(array $config): void +{ + $expected = (string)($config['token'] ?? ''); + if ($expected === '') { + bridgeRespond(['ok' => false, 'error' => 'Bridge token not configured'], 500); + } + + $provided = null; + if (!empty($_SERVER['HTTP_AUTHORIZATION']) && stripos($_SERVER['HTTP_AUTHORIZATION'], 'Bearer ') === 0) { + $provided = trim(substr($_SERVER['HTTP_AUTHORIZATION'], 7)); + } elseif (!empty($_SERVER['HTTP_X_EMAILTEMPLATE_TOKEN'])) { + $provided = trim($_SERVER['HTTP_X_EMAILTEMPLATE_TOKEN']); + } elseif (isset($_GET['token'])) { + $provided = (string)$_GET['token']; + } elseif (isset($_POST['token'])) { + $provided = (string)$_POST['token']; + } + + if (!$provided || !hash_equals($expected, $provided)) { + bridgeRespond(['ok' => false, 'error' => 'Unauthorized'], 403); + } +} + +function bridgeDb(array $config): PDO +{ + static $pdo = null; + if ($pdo instanceof PDO) { + return $pdo; + } + + try { + $pdo = new PDO( + $config['db']['dsn'], + $config['db']['user'], + $config['db']['pass'], + $config['db']['options'] + ); + } catch (Throwable $e) { + bridgeRespond(['ok' => false, 'error' => 'DB connection failed', 'detail' => $e->getMessage()], 500); + } + + return $pdo; +} + +bridgeRequireToken($bridgeConfig); + +$action = strtolower((string)($_GET['action'] ?? $_POST['action'] ?? 'schema')); + +if ($action === 'ping') { + bridgeRespond(['ok' => true, 'time' => date(DATE_ATOM)]); +} + +if ($action !== 'schema') { + bridgeRespond(['ok' => false, 'error' => 'Unknown action'], 404); +} + +$pdo = bridgeDb($bridgeConfig); + +try { + $dbName = ''; + if (preg_match('/dbname=([^;]+)/i', $bridgeConfig['db']['dsn'], $m)) { + $dbName = $m[1]; + } + + $tablesStmt = $pdo->query('SHOW FULL TABLES'); + $tables = []; + while ($row = $tablesStmt->fetch(PDO::FETCH_NUM)) { + $tableName = $row[0]; + if ($tableName === null) { + continue; + } + + $columnsStmt = $pdo->prepare( + 'SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY, EXTRA + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table + ORDER BY ORDINAL_POSITION' + ); + $columnsStmt->execute([ + ':schema' => $dbName ?: $pdo->query('SELECT DATABASE()')->fetchColumn(), + ':table' => $tableName, + ]); + + $columns = []; + foreach ($columnsStmt as $col) { + $columns[] = [ + 'name' => $col['COLUMN_NAME'], + 'type' => $col['DATA_TYPE'], + 'nullable' => ($col['IS_NULLABLE'] === 'YES'), + 'default' => $col['COLUMN_DEFAULT'], + 'key' => $col['COLUMN_KEY'], + 'extra' => $col['EXTRA'], + 'placeholder'=> strtoupper($tableName) . '__' . strtoupper($col['COLUMN_NAME']), + ]; + } + + $tables[] = [ + 'name' => $tableName, + 'columns' => $columns, + ]; + } + + bridgeRespond([ + 'ok' => true, + 'tables' => $tables, + 'fetched' => date(DATE_ATOM), + ]); +} catch (Throwable $e) { + bridgeRespond(['ok' => false, 'error' => 'Schema fetch failed', 'detail' => $e->getMessage()], 500); +} diff --git a/download/emailtemplate_sender.php b/download/emailtemplate_sender.php new file mode 100644 index 0000000..c8d55eb --- /dev/null +++ b/download/emailtemplate_sender.php @@ -0,0 +1,308 @@ + getenv('EMAILTEMPLATE_SENDER_TOKEN') ?: 'REPLACE_WITH_SHARED_TOKEN', + 'template_api' => [ + 'base_url' => getenv('EMAILTEMPLATE_API_BASE') ?: 'https://api.emailtemplate.it/external/render', + 'token' => getenv('EMAILTEMPLATE_API_TOKEN') ?: 'REPLACE_WITH_TEMPLATE_API_TOKEN', + 'timeout' => 15, + ], + 'db' => [ + 'dsn' => getenv('EMAILTEMPLATE_LOCAL_DSN') ?: 'mysql:host=127.0.0.1;dbname=example;charset=utf8mb4', + 'user' => getenv('EMAILTEMPLATE_LOCAL_DB_USER') ?: 'root', + 'pass' => getenv('EMAILTEMPLATE_LOCAL_DB_PASS') ?: '', + 'options' => [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ], + 'enabled' => true, + ], + 'mail' => [ + 'from_email' => 'no-reply@example.com', + 'from_name' => 'EmailTemplate Sender', + 'transport' => 'mail', // aktuell nur mail() + ], +]; + +$localSenderOverride = __DIR__ . '/emailtemplate.sender.conf.php'; +if (is_file($localSenderOverride)) { + $override = include $localSenderOverride; + if (is_array($override)) { + $senderConfig = array_replace_recursive($senderConfig, $override); + } +} + +function senderRespond($payload, int $status = 200): void +{ + http_response_code($status); + header('Content-Type: application/json; charset=utf-8'); + header('Cache-Control: no-store, max-age=0'); + echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +function senderRequireToken(array $config): void +{ + $expected = (string)($config['token'] ?? ''); + if ($expected === '') { + senderRespond(['ok' => false, 'error' => 'Sender token not configured'], 500); + } + + $provided = null; + if (!empty($_SERVER['HTTP_AUTHORIZATION']) && stripos($_SERVER['HTTP_AUTHORIZATION'], 'Bearer ') === 0) { + $provided = trim(substr($_SERVER['HTTP_AUTHORIZATION'], 7)); + } elseif (!empty($_SERVER['HTTP_X_EMAILTEMPLATE_TOKEN'])) { + $provided = trim($_SERVER['HTTP_X_EMAILTEMPLATE_TOKEN']); + } elseif (isset($_POST['token'])) { + $provided = (string)$_POST['token']; + } + + if (!$provided || !hash_equals($expected, $provided)) { + senderRespond(['ok' => false, 'error' => 'Unauthorized'], 403); + } +} + +function senderInput(): array +{ + $raw = file_get_contents('php://input'); + if ($raw !== false && trim($raw) !== '') { + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + return $decoded; + } + } + + if (!empty($_POST)) { + return $_POST; + } + + return []; +} + +function senderDb(array $config): ?PDO +{ + if (empty($config['db']['enabled'])) { + return null; + } + + static $pdo = null; + if ($pdo instanceof PDO) { + return $pdo; + } + + try { + $pdo = new PDO( + $config['db']['dsn'], + $config['db']['user'], + $config['db']['pass'], + $config['db']['options'] + ); + } catch (Throwable $e) { + senderRespond(['ok' => false, 'error' => 'DB connection failed', 'detail' => $e->getMessage()], 500); + } + + return $pdo; +} + +function resolvePlaceholderValue($definition, ?PDO $pdo) +{ + if (is_scalar($definition)) { + return $definition; + } + if (!is_array($definition)) { + return null; + } + + if (array_key_exists('value', $definition)) { + return $definition['value']; + } + + if (($definition['source'] ?? '') === 'db') { + if (!$pdo) { + throw new RuntimeException('Database access disabled'); + } + $table = $definition['table'] ?? null; + $column = $definition['column'] ?? null; + $where = $definition['where'] ?? []; + if (!$table || !$column || !is_array($where) || !$where) { + throw new InvalidArgumentException('Invalid DB placeholder definition'); + } + $conditions = []; + $params = []; + foreach ($where as $key => $value) { + $param = ':w_' . preg_replace('/[^a-z0-9_]/i', '_', $key); + $conditions[] = sprintf('`%s` = %s', str_replace('`', '', $key), $param); + $params[$param] = $value; + } + $sql = sprintf( + 'SELECT `%s` FROM `%s` WHERE %s LIMIT 1', + str_replace('`', '', $column), + str_replace('`', '', $table), + implode(' AND ', $conditions) + ); + $stmt = $pdo->prepare($sql); + foreach ($params as $param => $value) { + $stmt->bindValue($param, $value); + } + $stmt->execute(); + return $stmt->fetchColumn(); + } + + return null; +} + +function fetchTemplateHtml(array $config, array $payload): array +{ + $apiUrl = $config['template_api']['base_url']; + $apiToken = $config['template_api']['token']; + if (!$apiUrl || !$apiToken) { + senderRespond(['ok' => false, 'error' => 'Template API not configured'], 500); + } + + $body = $payload; + $body['token'] = $apiToken; + + $context = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'timeout' => (int)($config['template_api']['timeout'] ?? 15), + 'content' => json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + ], + ]); + + $response = @file_get_contents($apiUrl, false, $context); + if ($response === false) { + senderRespond(['ok' => false, 'error' => 'Template API unreachable'], 502); + } + + $decoded = json_decode($response, true); + if (!is_array($decoded) || !empty($decoded['ok']) === false) { + senderRespond(['ok' => false, 'error' => 'Template API error', 'detail' => $decoded], 502); + } + + if (empty($decoded['html'])) { + senderRespond(['ok' => false, 'error' => 'Template API returned no HTML'], 502); + } + + return $decoded; +} + +function senderMail(array $config, string $to, string $subject, string $html): bool +{ + $headers = [ + 'MIME-Version: 1.0', + 'Content-Type: text/html; charset=UTF-8', + ]; + $from = $config['mail']['from_email'] ?? null; + if ($from) { + $fromName = $config['mail']['from_name'] ?? ''; + if ($fromName !== '') { + $fromName = function_exists('mb_encode_mimeheader') + ? mb_encode_mimeheader($fromName, 'UTF-8') + : $fromName; + $headers[] = sprintf('From: %s <%s>', $fromName, $from); + } else { + $headers[] = 'From: ' . $from; + } + } + $encodedSubject = function_exists('mb_encode_mimeheader') + ? mb_encode_mimeheader($subject, 'UTF-8') + : $subject; + return @mail($to, $encodedSubject, $html, implode("\r\n", $headers)); +} + +senderRequireToken($senderConfig); +$input = senderInput(); + +$templateId = isset($input['template_id']) ? (int)$input['template_id'] : null; +$templateName = $input['template_name'] ?? null; +$recipient = isset($input['to']) ? filter_var($input['to'], FILTER_VALIDATE_EMAIL) : false; +$subject = trim((string)($input['subject'] ?? 'EmailTemplate Versand')); +$previewOnly = !empty($input['preview_only']); + +if (!$templateId && !$templateName) { + senderRespond(['ok' => false, 'error' => 'template_id or template_name required'], 422); +} +if (!$recipient && !$previewOnly) { + senderRespond(['ok' => false, 'error' => 'Valid recipient required'], 422); +} + +$placeholdersSpec = $input['placeholders'] ?? []; +if (is_string($placeholdersSpec)) { + $decoded = json_decode($placeholdersSpec, true); + if (is_array($decoded)) { + $placeholdersSpec = $decoded; + } +} +if (!is_array($placeholdersSpec)) { + $placeholdersSpec = []; +} + +$pdo = senderDb($senderConfig); +$resolvedPlaceholders = []; +foreach ($placeholdersSpec as $key => $definition) { + try { + $resolvedPlaceholders[$key] = resolvePlaceholderValue($definition, $pdo); + } catch (Throwable $e) { + senderRespond(['ok' => false, 'error' => 'Placeholder resolution failed', 'detail' => $e->getMessage()], 422); + } +} + +$templatePayload = [ + 'placeholders' => $resolvedPlaceholders, +]; +if ($templateId) { + $templatePayload['template_id'] = $templateId; +} +if ($templateName) { + $templatePayload['template_name'] = $templateName; +} + +$rendered = fetchTemplateHtml($senderConfig, $templatePayload); +$finalHtml = (string)$rendered['html']; +if (isset($rendered['subject']) && !empty($rendered['subject'])) { + $subject = (string)$rendered['subject']; +} + +if ($previewOnly) { + senderRespond([ + 'ok' => true, + 'mode' => 'preview', + 'subject' => $subject, + 'html' => $finalHtml, + 'placeholders' => $resolvedPlaceholders, + ]); +} + +$sent = senderMail($senderConfig, $recipient, $subject, $finalHtml); +if (!$sent) { + senderRespond(['ok' => false, 'error' => 'Mail dispatch failed'], 500); +} + +senderRespond([ + 'ok' => true, + 'mode' => 'send', + 'recipient' => $recipient, + 'subject' => $subject, + 'placeholders' => $resolvedPlaceholders, +]);