|
|
@@ -268,6 +268,152 @@ function saveProducts($products) {
|
|
|
writeJsonFile(PRODUCTS_FILE, $data);
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Resolve FAQ file path and force storage inside DATA_DIR.
|
|
|
+ */
|
|
|
+function getFaqFilePath(): string {
|
|
|
+ $dataDir = defined('DATA_DIR') && is_string(DATA_DIR) ? DATA_DIR : (dirname(__DIR__) . '/data/');
|
|
|
+ $defaultPath = rtrim($dataDir, '/\\') . '/faq.json';
|
|
|
+
|
|
|
+ if (!defined('FAQ_FILE') || !is_string(FAQ_FILE) || FAQ_FILE === '') {
|
|
|
+ return $defaultPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ $configuredPath = FAQ_FILE;
|
|
|
+ $normalizedDataDir = str_replace('\\', '/', rtrim($dataDir, '/\\')) . '/';
|
|
|
+ $normalizedConfigured = str_replace('\\', '/', $configuredPath);
|
|
|
+
|
|
|
+ if (strpos($normalizedConfigured, $normalizedDataDir) !== 0) {
|
|
|
+ return $defaultPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $configuredPath;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Get FAQ markdown content from JSON store.
|
|
|
+ */
|
|
|
+function getFaqContent(): string {
|
|
|
+ $defaultContent = "# FAQ\n\nHier kann der FAQ-Inhalt im Admin-Bereich bearbeitet werden.";
|
|
|
+ $data = readJsonFile(getFaqFilePath());
|
|
|
+ if (!is_array($data)) {
|
|
|
+ return $defaultContent;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isset($data['content']) || !is_string($data['content'])) {
|
|
|
+ return $defaultContent;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $data['content'];
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Save FAQ markdown content to JSON store.
|
|
|
+ */
|
|
|
+function saveFaqContent(string $markdown): void {
|
|
|
+ writeJsonFile(getFaqFilePath(), ['content' => (string) $markdown]);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Render inline markdown safely (bold + italic).
|
|
|
+ */
|
|
|
+function renderFaqInlineMarkdown(string $text): string {
|
|
|
+ $escaped = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
|
|
+
|
|
|
+ // Apply markdown after escaping so raw HTML is never executed.
|
|
|
+ $escaped = preg_replace('/\*\*(.+?)\*\*/s', '<strong>$1</strong>', $escaped);
|
|
|
+ $escaped = preg_replace('/(?<!\*)\*(?!\s)(.+?)(?<!\s)\*(?!\*)/s', '<em>$1</em>', $escaped);
|
|
|
+
|
|
|
+ return $escaped;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Render a minimal safe markdown subset for FAQ content.
|
|
|
+ */
|
|
|
+function renderFaqMarkdown(string $markdown): string {
|
|
|
+ $normalized = str_replace(["\r\n", "\r"], "\n", $markdown);
|
|
|
+ $lines = explode("\n", $normalized);
|
|
|
+ $htmlParts = [];
|
|
|
+ $paragraphLines = [];
|
|
|
+ $listType = '';
|
|
|
+
|
|
|
+ $flushParagraph = function () use (&$paragraphLines, &$htmlParts): void {
|
|
|
+ if (empty($paragraphLines)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $renderedLines = [];
|
|
|
+ foreach ($paragraphLines as $line) {
|
|
|
+ $renderedLines[] = renderFaqInlineMarkdown($line);
|
|
|
+ }
|
|
|
+
|
|
|
+ $htmlParts[] = '<p>' . implode("<br>\n", $renderedLines) . '</p>';
|
|
|
+ $paragraphLines = [];
|
|
|
+ };
|
|
|
+
|
|
|
+ $closeList = function () use (&$listType, &$htmlParts): void {
|
|
|
+ if ($listType === '') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $htmlParts[] = '</' . $listType . '>';
|
|
|
+ $listType = '';
|
|
|
+ };
|
|
|
+
|
|
|
+ foreach ($lines as $line) {
|
|
|
+ $line = rtrim($line);
|
|
|
+ $trimmed = trim($line);
|
|
|
+
|
|
|
+ if ($trimmed === '') {
|
|
|
+ $flushParagraph();
|
|
|
+ $closeList();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (preg_match('/^(#{1,3})\s+(.+)$/', $trimmed, $matches) === 1) {
|
|
|
+ $flushParagraph();
|
|
|
+ $closeList();
|
|
|
+ $level = strlen($matches[1]);
|
|
|
+ $htmlParts[] = '<h' . $level . '>' . renderFaqInlineMarkdown($matches[2]) . '</h' . $level . '>';
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (preg_match('/^\s*[-*]\s+(.+)$/', $line, $matches) === 1) {
|
|
|
+ $flushParagraph();
|
|
|
+ if ($listType !== 'ul') {
|
|
|
+ $closeList();
|
|
|
+ $listType = 'ul';
|
|
|
+ $htmlParts[] = '<ul>';
|
|
|
+ }
|
|
|
+ $htmlParts[] = '<li>' . renderFaqInlineMarkdown($matches[1]) . '</li>';
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (preg_match('/^\s*\d+\.\s+(.+)$/', $line, $matches) === 1) {
|
|
|
+ $flushParagraph();
|
|
|
+ if ($listType !== 'ol') {
|
|
|
+ $closeList();
|
|
|
+ $listType = 'ol';
|
|
|
+ $htmlParts[] = '<ol>';
|
|
|
+ }
|
|
|
+ $htmlParts[] = '<li>' . renderFaqInlineMarkdown($matches[1]) . '</li>';
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $closeList();
|
|
|
+ $paragraphLines[] = $trimmed;
|
|
|
+ }
|
|
|
+
|
|
|
+ $flushParagraph();
|
|
|
+ $closeList();
|
|
|
+
|
|
|
+ if (empty($htmlParts)) {
|
|
|
+ return '<p>Keine FAQ-Inhalte vorhanden.</p>';
|
|
|
+ }
|
|
|
+
|
|
|
+ return implode("\n", $htmlParts);
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Get all reservations
|
|
|
*/
|