diff --git a/content/createArticle.php b/content/createArticle.php index 959583e..c682836 100644 --- a/content/createArticle.php +++ b/content/createArticle.php @@ -4,12 +4,28 @@ if (!isset($_SESSION["user"])) { header("Location: index.php?pfad=login"); exit(); } + +// ------------------------------------------------------------------------- +// Aktuelle Blockliste ermitteln: Session-Fallback (nach Validierungsfehler +// oder Zwischen-Submit "add_block"/"delete_block") oder leeres Array (Start). +// ------------------------------------------------------------------------- +$rawContent = '[]'; +if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])) { + $rawContent = $_SESSION['old_content']; + unset($_SESSION['old_content']); +} + +$blocks = json_decode($rawContent, true); +if (!is_array($blocks)) { + $blocks = []; +} +$blocks = array_values($blocks); // sequentielle Indizes 0..n-1 sicherstellen ?> -
+
@@ -18,27 +34,49 @@ if (!isset($_SESSION["user"])) { value="" placeholder="Titel hier eingeben" required> - -
+ +
+ $block): ?> + +
+ - -
- - + + + + + + + + + + + +
+
- - +
+ +
+ + +
+
@@ -268,4 +306,4 @@ if (!isset($_SESSION["user"])) { -
+ \ No newline at end of file diff --git a/content/updateArticle.php b/content/updateArticle.php index 8555072..cc6fb00 100644 --- a/content/updateArticle.php +++ b/content/updateArticle.php @@ -5,10 +5,25 @@ if (!isset($_SESSION["user"])) { exit(); } include_once 'php/controller/showArticle-controller.php'; + +// Aktuelle Blockliste ermitteln: +$rawContent = '[]'; +if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])) { + $rawContent = $_SESSION['old_content']; + unset($_SESSION['old_content']); +} elseif (isset($content) && !empty($content)) { + $rawContent = $content; +} + +$blocks = json_decode($rawContent, true); +if (!is_array($blocks)) { + $blocks = []; +} +$blocks = array_values($blocks); ?>
" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container"> @@ -27,32 +42,49 @@ include_once 'php/controller/showArticle-controller.php'; ?>" placeholder="Titel hier eingeben" required> - -
+ +
+ $block): ?> + +
+ - -
- - + + + + + + + + + + + +
+
- - - - - +
+ +
+ + +
+
@@ -289,4 +321,4 @@ include_once 'php/controller/showArticle-controller.php'; - + \ No newline at end of file diff --git a/includes/article-block-helper.php b/includes/article-block-helper.php new file mode 100644 index 0000000..98e5646 --- /dev/null +++ b/includes/article-block-helper.php @@ -0,0 +1,69 @@ + 'text'|'image', 'value' => string] + */ +function rebuildBlocksFromPost(array $postBlocks, array $fileBlocks, string $uploadDir): array { + $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + + $keys = array_keys($postBlocks); + if (isset($fileBlocks['name']) && is_array($fileBlocks['name'])) { + $keys = array_unique(array_merge($keys, array_keys($fileBlocks['name']))); + } + sort($keys, SORT_NUMERIC); + + $blocks = []; + + foreach ($keys as $key) { + $type = $postBlocks[$key]['type'] ?? null; + + if ($type === 'text') { + $blocks[] = [ + 'type' => 'text', + 'value' => $postBlocks[$key]['text'] ?? '', + ]; + } elseif ($type === 'image') { + // Vorbelegung: bereits vorhandenes Server-Bild (falls Datei nicht ersetzt wird) + $value = $postBlocks[$key]['existing_image'] ?? ''; + + $hasUpload = isset($fileBlocks['error'][$key]['image']) + && $fileBlocks['error'][$key]['image'] === UPLOAD_ERR_OK; + + if ($hasUpload) { + $tmpName = $fileBlocks['tmp_name'][$key]['image']; + $originalName = $fileBlocks['name'][$key]['image']; + $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); + if (!in_array($extension, $allowedExtensions, true)) { + $extension = 'jpg'; + } + + $fileName = 'img_' . uniqid() . '.' . $extension; + $destination = $uploadDir . $fileName; + + if (move_uploaded_file($tmpName, $destination)) { + $value = 'uploads/' . $fileName; + } + // Bei Fehler: alter Wert (falls vorhanden) bleibt erhalten, Block wird nicht verworfen + } + + $blocks[] = [ + 'type' => 'image', + 'value' => $value, + ]; + } + // unbekannter/fehlender type -> Block wird ignoriert + } + + return $blocks; +} \ No newline at end of file diff --git a/js/editor.js b/js/editor.js index e666f40..01ee67f 100644 --- a/js/editor.js +++ b/js/editor.js @@ -1,172 +1,133 @@ -console.log("Die JavaScript-Datei wurde erfolgreich geladen!"); +//console.log("editor.js wurde erfolgreich geladen!"); function initEditor() { const form = document.getElementById("editor-form"); if (!form) { - console.error("Skript abgebrochen: Formular nicht gefunden!"); + console.error("editor.js abgebrochen: Formular nicht gefunden!"); return; } else { - console.log("Formular gefunden und Editor initialisiert:", form); + console.log("Formular gefunden und editor.js initialisiert:", form); } const container = document.getElementById("block-container"); const plusButton = document.getElementById("plus-button"); const popup = document.getElementById("block-popup"); - const hiddenContentInput = document.getElementById("content"); - const initialImages = []; + // Fortlaufender Zähler für eindeutige Block-Indizes. Wird nie wiederverwendet + // (auch nicht nach dem Löschen eines Blocks), damit sich neue und übrig + // gebliebene Blöcke beim finalen Submit nie einen Namen teilen. + let blockIndex = container.querySelectorAll(".editor-block").length; + + // Ohne JS sind Textblock-/Bild-Button im Pop-up immer sichtbar und ganz normale + // Submit-Buttons. Erst mit JS blenden wir das Pop-up standardmäßig aus und + // steuern die Sichtbarkeit über den Plus-Button. + popup.classList.add("hidden"); - // Pop-up umschalten bei Klick auf das Plus plusButton.addEventListener("click", () => { popup.classList.toggle("hidden"); }); - // Klick auf eine Block-Option im Pop-up - popup.querySelectorAll("button").forEach(btn => { - btn.addEventListener("click", function() { + // Klick auf "Textblock" / "Bild einfügen": lokal im DOM anlegen statt zum + // Server zu submitten (entlastet den Server, kein Page-Reload nötig). + popup.querySelectorAll('[name="editor_action"]').forEach(btn => { + btn.addEventListener("click", function (e) { + e.preventDefault(); const type = this.getAttribute("data-type"); addBlockElement(type, ""); popup.classList.add("hidden"); }); }); - // Erstellt ein visuelles HTML-Element im Editor + // Bereits vom Server gerenderte Blöcke (z.B. beim Bearbeiten eines bestehenden + // Artikels oder nach einem Validierungsfehler) ebenfalls mit JS-Verhalten ausstatten. + container.querySelectorAll(".editor-block").forEach(blockDiv => { + bindDeleteButton(blockDiv); + bindImageInput(blockDiv); + }); + + // Erstellt einen neuen Block inkl. echter, benannter Formularfelder + // (blocks[i][type], blocks[i][text] bzw. blocks[i][image]). Diese Felder werden + // beim finalen Submit ganz normal vom Browser als multipart/form-data verschickt – + // es ist kein manuelles Zusammenbauen von JSON beim Absenden mehr nötig. function addBlockElement(type, value = "") { + const index = blockIndex++; + const blockDiv = document.createElement("div"); - blockDiv.classList.add("editor-block"); - blockDiv.setAttribute("data-type", type); + blockDiv.classList.add("editor-block", "article-editor-scope"); + blockDiv.setAttribute("data-index", String(index)); - // Wenn es ein existierendes Server-Bild beim Laden ist, Pfad im globalen Array sichern - if (type === "image" && value && typeof value === 'string' && value.startsWith('uploads/')) { - initialImages.push(value); - blockDiv.setAttribute("data-value", value); - } - - // Löschen-Button - const deleteBtn = document.createElement("button"); - deleteBtn.type = "button"; - deleteBtn.innerHTML = "✕"; - deleteBtn.classList.add("delete-block-btn"); - deleteBtn.addEventListener("click", () => { - // ANPASSUNG 2B: Logik hier komplett geleert. Das '✕' entfernt den Block jetzt nur noch sicher aus dem HTML. - blockDiv.remove(); - }); - blockDiv.appendChild(deleteBtn); + const typeInput = document.createElement("input"); + typeInput.type = "hidden"; + typeInput.name = `blocks[${index}][type]`; + typeInput.value = type; + blockDiv.appendChild(typeInput); if (type === "text") { const textarea = document.createElement("textarea"); + textarea.name = `blocks[${index}][text]`; textarea.placeholder = "Schreibe deinen Textblock..."; textarea.value = value; blockDiv.appendChild(textarea); } else if (type === "image") { const fileInput = document.createElement("input"); fileInput.type = "file"; + fileInput.name = `blocks[${index}][image]`; fileInput.accept = "image/*"; - - const imgPreview = document.createElement("img"); - imgPreview.style.maxWidth = "200px"; - imgPreview.style.display = "block"; - imgPreview.style.marginTop = "10px"; - - if (value && typeof value === 'string') { - if (value.startsWith('uploads/') || value.startsWith('data:image/')) { - imgPreview.src = value; - blockDiv.setAttribute("data-value", value); - } - } - - fileInput.addEventListener("change", function() { - if (this.files && this.files[0]) { - const reader = new FileReader(); - reader.onload = function(e) { - imgPreview.src = e.target.result; - blockDiv.setAttribute("data-value", e.target.result); - } - reader.readAsDataURL(this.files[0]); - } - }); - blockDiv.appendChild(fileInput); - blockDiv.appendChild(imgPreview); } + const deleteBtn = document.createElement("button"); + deleteBtn.type = "submit"; + deleteBtn.name = "editor_action"; + deleteBtn.value = `delete_block:${index}`; + deleteBtn.classList.add("delete-block-btn"); + deleteBtn.innerHTML = "✕"; + blockDiv.appendChild(deleteBtn); + container.appendChild(blockDiv); + + bindDeleteButton(blockDiv); + bindImageInput(blockDiv); } - // beim Abschicken verbleibende Blöcke auslesen UND gelöschte Bilder ermitteln - form.addEventListener("submit", function(e) { - const blocks = []; - const currentImages = []; - - // alle aktuell im Formular verbliebenen Blöcke scannen - container.querySelectorAll(".editor-block").forEach(blockDiv => { - const type = blockDiv.getAttribute("data-type"); - let value = ""; - - if (type === "text") { - value = blockDiv.querySelector("textarea").value; - } else if (type === "image") { - - const imgTag = blockDiv.querySelector("img"); - if (imgTag) { - const srcValue = imgTag.getAttribute("src") || ""; - // Wenn es ein neues Bild ist, nutzen wir das data-value (Base64) - if (srcValue.startsWith('data:image/')) { - value = blockDiv.getAttribute("data-value") || ""; - } else { - value = srcValue; - } - } - - // Pfade sammeln, die der Nutzer NICHT gelöscht hat (für den Abgleich) - if (value && value.startsWith('uploads/')) { - currentImages.push(value); - } - } - - blocks.push({ type: type, value: value }); + // Löschen-Button eines Blocks lokal abfangen: entfernt den Block nur aus dem DOM, + // statt das Formular zum Server zu senden. + function bindDeleteButton(blockDiv) { + const btn = blockDiv.querySelector(".delete-block-btn"); + if (!btn) return; + btn.addEventListener("click", (e) => { + e.preventDefault(); + blockDiv.remove(); }); + } - // das reguläre unsichtbare Content-Feld befüllen - hiddenContentInput.value = JSON.stringify(blocks); + // Zeigt bei Auswahl einer Bilddatei sofort eine Vorschau an. Rein optisch – + // der eigentliche Datei-Upload läuft nativ über das . + function bindImageInput(blockDiv) { + const fileInput = blockDiv.querySelector('input[type="file"]'); + if (!fileInput) return; - // Differenz berechnen: Welche Bilder aus 'initialImages' fehlen in 'currentImages' ? - const deletedImages = initialImages.filter(img => !currentImages.includes(img)); - - // das 'deleted_images'-Feld dynamisch befüllen und an den Controller senden - let deletedInput = document.getElementById("deleted-images"); - if (!deletedInput) { - deletedInput = document.createElement("input"); - deletedInput.type = "hidden"; - deletedInput.id = "deleted-images"; - deletedInput.name = "deleted_images"; - form.appendChild(deletedInput); + let imgPreview = blockDiv.querySelector(".block-image-preview"); + if (!imgPreview) { + imgPreview = document.createElement("img"); + imgPreview.classList.add("block-image-preview"); + imgPreview.style.maxWidth = "200px"; + imgPreview.style.display = "none"; + imgPreview.style.marginTop = "10px"; + blockDiv.insertBefore(imgPreview, fileInput.nextSibling); } - deletedInput.value = JSON.stringify(deletedImages); - }); - // Existierende Blöcke laden (stellt alte Daten aus der Session wieder her) - try { - const initialBlocks = JSON.parse(hiddenContentInput.value.trim()); - if (Array.isArray(initialBlocks)) { - initialBlocks.forEach(b => { - if (b.type === "image" && b.value && typeof b.value === 'string' && !b.value.startsWith('data:image/')) { - let cleanPath = b.value.trim().replace(/\\\//g, '/'); // Verwandelt \/ in / - - initialImages.push(cleanPath); - addBlockElement(b.type, cleanPath); - - } else { - addBlockElement(b.type, b.value); - } - }); - console.log("Erfolgreich registrierte Start-Bilder:", initialImages); - } - } catch(e) { - if (hiddenContentInput.value.trim() !== "") { - addBlockElement("text", hiddenContentInput.value); - } + fileInput.addEventListener("change", function () { + if (this.files && this.files[0]) { + const reader = new FileReader(); + reader.onload = (e) => { + imgPreview.src = e.target.result; + imgPreview.style.display = "block"; + }; + reader.readAsDataURL(this.files[0]); + } + }); } } @@ -176,4 +137,4 @@ if (document.readyState === "loading") { } else { // Falls das DOM schon fertig geladen ist, führen wir es direkt aus initEditor(); -} +} \ No newline at end of file diff --git a/php/controller/createArticle-controller.php b/php/controller/createArticle-controller.php index a48a98a..de3ed9d 100644 --- a/php/controller/createArticle-controller.php +++ b/php/controller/createArticle-controller.php @@ -5,134 +5,120 @@ if (session_status() === PHP_SESSION_NONE) { require_once '../model/LocalArticleManager.php'; require_once '../model/ArticleManager.php'; require_once '../validator/article-validator.php'; +require_once '../../includes/article-block-helper.php'; if (!isset($_SESSION["user"])) { header("Location: index.php?pfad=login"); exit(); } - if ($_SERVER["REQUEST_METHOD"] === "POST") { - $_SESSION["old_title"] = $_POST["title"] ?? ''; - $_SESSION["old_content"] = $_POST["content"] ?? ''; - $_SESSION["old_category"] = $_POST["category"] ?? ''; - $_SESSION["old_tags"] = $_POST["tags"] ?? ''; - if(!isset($_POST["title"]) ||!isset($_POST["content"]) || !isset($_POST["category"])){ - $_SESSION["message"] = "missing_parameters"; +if ($_SERVER["REQUEST_METHOD"] === "POST") { + + $uploadDir = __DIR__ . '/../../uploads/'; + if (!file_exists($uploadDir)) { + mkdir($uploadDir, 0755, true); + } + + // Formularzustand (Titel/Tags/Kategorie/Blöcke) immer sichern, damit er nach einem + // Redirect (PRG-Pattern oder Validierungsfehler) wieder angezeigt werden kann. + $_SESSION["old_title"] = $_POST["title"] ?? ''; + $_SESSION["old_category"] = $_POST["category"] ?? ''; + $_SESSION["old_tags"] = $_POST["tags"] ?? ''; + + $blocks = rebuildBlocksFromPost($_POST['blocks'] ?? [], $_FILES['blocks'] ?? [], $uploadDir); + $_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE); + + // --------------------------------------------------------------------- + // Zwischenspeichern + // --------------------------------------------------------------------- + if (isset($_POST['editor_action']) && $_POST['editor_action'] !== '') { + $action = $_POST['editor_action']; + + if ($action === 'add_text') { + $blocks[] = ['type' => 'text', 'value' => '']; + } elseif ($action === 'add_image') { + $blocks[] = ['type' => 'image', 'value' => '']; + } elseif (str_starts_with($action, 'delete_block:')) { + $deleteIndex = (int) substr($action, strlen('delete_block:')); + unset($blocks[$deleteIndex]); + $blocks = array_values($blocks); + } + + $_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE); + header("location: ../../index.php?pfad=createArticle"); + exit(); + } + + // --------------------------------------------------------------------- + // Echtes Veröffentlichen + // --------------------------------------------------------------------- + if (!isset($_POST["title"]) || !isset($_POST["category"])) { + $_SESSION["message"] = "missing_parameters"; + header("location: ../../index.php?pfad=createArticle"); + exit(); + } else { + $title = $_POST["title"]; + $content = json_encode($blocks, JSON_UNESCAPED_UNICODE); + $author = $_SESSION["user_email"]; + $category = $_POST["category"]; + $tags = $_POST['tags'] ?? ''; + + // -------------------------------- Validierung der Daten: ------------------------- + if (!articleTitleValidator($title)) { + $_SESSION["message"] = "invalid_title"; + header("location: ../../index.php?pfad=createArticle"); + exit(); + } + + if (!articleContentValidator($content)) { + $_SESSION["message"] = "invalid_content"; + header("location: ../../index.php?pfad=createArticle"); + exit(); + } + + if (!articleCategoryValidator($category)) { + $_SESSION["message"] = "invalid_category"; + header("location: ../../index.php?pfad=createArticle"); + exit(); + } + + if (!articleTagValidator($tags)) { + $_SESSION["message"] = "invalid_tags"; header("location: ../../index.php?pfad=createArticle"); exit(); } else { - $title = $_POST["title"]; - $content = $_POST["content"]; - $author = $_SESSION["user_email"]; - $category = $_POST["category"]; - $tags = $_POST['tags'] ?? ''; - - // -------------------------------- Validierung der Daten: ------------------------- - if (!articleTitleValidator($title)) { - $_SESSION["message"] = "invalid_title"; - header("location: ../../index.php?pfad=createArticle"); - exit(); + $cleanedTags = []; + $rawTags = explode(',', $tags); + foreach ($rawTags as $rawTag) { + // Leerzeichen am Anfang/Ende des einzelnen Tags entfernen: + $tag = trim($rawTag); + $cleanedTags[] = $tag; } + // Duplikate entfernen: + $cleanedTags = array_unique($cleanedTags); + $cleanedTags = implode(',', $cleanedTags); + } - if (!articleContentValidator($content)) { - $_SESSION["message"] = "invalid_content"; - header("location: ../../index.php?pfad=createArticle"); - exit(); - } + // ----------------- Übertragung der validierten Daten in ArticleManager: --------------------------- + try { + $articleManager = ArticleManager::getInstance(); + // $content enthält bereits die finalen "uploads/..."-Pfade (kein Base64 mehr), + // da rebuildBlocksFromPost() Datei-Uploads sofort verarbeitet. + $articleManager->addArticle($title, $content, $author, $category, $cleanedTags); - if (!articleCategoryValidator($category)) { - $_SESSION["message"] = "invalid_category"; - header("location: ../../index.php?pfad=createArticle"); - exit(); - } + // Formulardaten nach erfolgreichem Erstellen aus der Session löschen + unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]); - if (!articleTagValidator($tags)) { - $_SESSION["message"] = "invalid_tags"; - header("location: ../../index.php?pfad=createArticle"); - exit(); - } else { - $cleanedTags = []; - $rawTags = explode(',', $tags); - foreach ($rawTags as $rawTag) { - // Leerzeichen am Anfang/Ende des einzelnen Tags entfernen: - $tag = trim($rawTag); - $cleanedTags[] = $tag; - } - // Duplikate entfernen: - $cleanedTags = array_unique($cleanedTags); - $cleanedTags = implode(',', $cleanedTags); - } - - // ----------------- Base64-Bilder verarbeiten und auf Server speichern ----------------- - $blocks = json_decode($content, true); - $uploadDir = __DIR__ . '/../../uploads/'; - - if (!file_exists($uploadDir)) { - mkdir($uploadDir, 0755, true); - } - - if (is_array($blocks)) { - foreach ($blocks as &$block) { - // sicherstellen, dass 'type' und 'value' existieren: - if (isset($block['type']) && isset($block['value']) && $block['type'] === 'image' && str_starts_with($block['value'], 'data:image/')) { - - // Base64-String zerlegen - $parts = explode(',', $block['value']); - - // falls der String korrupt ist und kein Komma hat - if (count($parts) < 2) { - continue; - } - - $metadata = $parts[0]; - $base64Data = $parts[1]; - - // Dateiendung ermitteln - preg_match('/data:image\/(?.*?);/', $metadata, $matches); - $extension = $matches['extension'] ?? 'jpg'; - if ($extension === 'jpeg') { - $extension = 'jpg'; - } - - // Eindeutigen Dateinamen generieren - $fileName = 'img_' . uniqid() . '.' . $extension; - $filePath = $uploadDir . $fileName; - - // Datei im /uploads speichern: - if (file_put_contents($filePath, base64_decode($base64Data)) !== false) { - // temporären Base64-String durch den echten Pfad ersetzen - $block['value'] = 'uploads/' . $fileName; - } else { - $_SESSION["message"] = "image_upload_error"; - header("location: ../../index.php?pfad=createArticle"); - exit(); - } - } - } - unset($block); - } - - // Aktualisiertes Array wieder in JSON konvertieren - $finalContent = json_encode($blocks, JSON_UNESCAPED_UNICODE); - - // ----------------- Übertragung der validierten Daten in ArticleManager: --------------------------- - try { - $articleManager = ArticleManager::getInstance(); - $articleManager->addArticle($title, $content, $author, $category, $cleanedTags); - - // Formulardaten nach erfolgreichem Erstellen aus der Session löschen - unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]); - - } catch (\Throwable $e){ - $_SESSION["message"] = "internal_error"; - header("location: ../../index.php?pfad=createArticle"); - exit(); - } - - $_SESSION["message"] = "new_article"; - // Weiterleitung zur Homepage - header("location: ../../index.php"); + } catch (\Throwable $e) { + $_SESSION["message"] = "internal_error"; + header("location: ../../index.php?pfad=createArticle"); exit(); } + + $_SESSION["message"] = "new_article"; + // Weiterleitung zur Homepage + header("location: ../../index.php"); + exit(); } +} ?> \ No newline at end of file diff --git a/php/controller/updateArticle-controller.php b/php/controller/updateArticle-controller.php index c5ef8f6..cb58617 100644 --- a/php/controller/updateArticle-controller.php +++ b/php/controller/updateArticle-controller.php @@ -7,17 +7,14 @@ require_once '../model/LocalArticleManager.php'; require_once '../model/ArticleManager.php'; require_once '../model/Article.php'; require_once '../validator/article-validator.php'; +require_once '../../includes/article-block-helper.php'; -if (!isset($_SESSION["user_email"])) { +if (!isset($_SESSION["user"])) { header("Location: index.php?pfad=login"); exit(); } if ($_SERVER["REQUEST_METHOD"] === "POST") { - $_SESSION["old_title"] = $_POST["title"] ?? ''; - $_SESSION["old_content"] = $_POST["content"] ?? ''; - $_SESSION["old_category"] = $_POST["category"] ?? ''; - $_SESSION["old_tags"] = $_POST["tags"] ?? ''; if (isset($_GET["id"]) && !empty($_GET["id"])) { $id = $_GET["id"]; @@ -41,16 +38,56 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") { exit(); } - if (!isset($_POST["title"]) ||!isset($_POST["content"]) || !isset($_POST["category"])){ + $uploadDir = __DIR__ . '/../../uploads/'; + if (!file_exists($uploadDir)) { + mkdir($uploadDir, 0755, true); + } + + // Formularzustand (Titel/Tags/Kategorie/Blöcke) immer sichern, damit er nach einem + // Redirect (PRG-Pattern oder Validierungsfehler) wieder angezeigt werden kann. + $_SESSION["old_title"] = $_POST["title"] ?? ''; + $_SESSION["old_tags"] = $_POST["tags"] ?? ''; + $_SESSION["old_category"] = $_POST["category"] ?? ''; + + $blocks = rebuildBlocksFromPost($_POST['blocks'] ?? [], $_FILES['blocks'] ?? [], $uploadDir); + $_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE); + + // --------------------------------------------------------------------- + // Zwischen-Schritt: Block hinzufügen oder entfernen (kein echtes Speichern). + // Wird bei aktivem JavaScript per preventDefault() abgefangen und lokal im + // DOM erledigt (js/editor.js) – ohne JS läuft dieser Server-Roundtrip. + // --------------------------------------------------------------------- + if (isset($_POST['editor_action']) && $_POST['editor_action'] !== '') { + $action = $_POST['editor_action']; + + if ($action === 'add_text') { + $blocks[] = ['type' => 'text', 'value' => '']; + } elseif ($action === 'add_image') { + $blocks[] = ['type' => 'image', 'value' => '']; + } elseif (str_starts_with($action, 'delete_block:')) { + $deleteIndex = (int) substr($action, strlen('delete_block:')); + unset($blocks[$deleteIndex]); + $blocks = array_values($blocks); + } + + $_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE); + header("location: ../../index.php?pfad=updateArticle&id=$id"); + exit(); + } + + // --------------------------------------------------------------------- + // Echtes Speichern + // --------------------------------------------------------------------- + if (!isset($_POST["title"]) || !isset($_POST["category"])) { $_SESSION["message"] = "missing_parameters"; header("location: ../../index.php?pfad=updateArticle&id=$id"); exit(); - }else{ - $title = $_POST["title"]; - $content = $_POST["content"]; - $author = $_SESSION["user_email"]; + } else { + $title = $_POST["title"]; + $content = json_encode($blocks, JSON_UNESCAPED_UNICODE); + $author = $_SESSION["user_email"]; $category = $_POST["category"]; - $tags = $_POST['tags'] ?? ''; + $tags = $_POST['tags'] ?? ''; // -------------------------------- Validierung der Daten: ------------------------- if (!articleTitleValidator($title)) { @@ -88,91 +125,56 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") { $cleanedTags = implode(',', $cleanedTags); } - // --------------------------------------- Base64-Bilder speichern --------------------------------------------- - $blocks = json_decode($content, true); - $uploadDir = __DIR__ . '/../../uploads/'; - - if (!file_exists($uploadDir)) { - mkdir($uploadDir, 0755, true); - } - - // ----------------- Gelöschte Bilder über die JS-Löschliste entfernen ----------------- TODO: Gelöschte Bilder über die JS-Löschliste entfernen - /*if (isset($_POST['deleted_images'])) { - $deletedImages = json_decode($_POST['deleted_images'], true); - - // Wir ermitteln den physisch echten, absoluten Pfad zum uploads-Ordner auf der Festplatte - $uploadDir = realpath(__DIR__ . '/../../uploads') . DIRECTORY_SEPARATOR; - - if (is_array($deletedImages)) { - foreach ($deletedImages as $imagePath) { - // Nur den reinen Dateinamen heraustrennen (z.B. img_65a123.jpg) - $filename = basename($imagePath); - $fullDeletePath = $uploadDir . $filename; - - // Debugging & Löschen: - if (file_exists($fullDeletePath)) { - // Versuchen zu löschen. Wenn es fehlschlägt, Fehlermeldung erzwingen - if (!@unlink($fullDeletePath)) { - $error = error_get_last(); - die("Datei existiert, aber PHP darf sie nicht löschen! Grund: " . $error['message']); - } - } else { - // Wenn PHP die Datei an diesem Pfad nicht findet, brechen wir zum Debuggen ab - // die("PHP findet die Datei nicht unter dem Pfad: " . $fullDeletePath); - } + // ----------------- Verwaiste Bilder aufräumen ----------------- + // Bilder, die im alten (gespeicherten) Content vorkamen, im neuen aber nicht + // mehr referenziert werden, wurden vom Nutzer entfernt oder ersetzt -> löschen. + // Hinweis/TODO: Bilder, die innerhalb derselben Bearbeitungs-Sitzung neu + // hochgeladen und noch vor dem finalen Speichern wieder entfernt wurden, + // werden hierüber nicht erfasst (sie tauchten nie im alten Content auf) und + // bleiben als Datei liegen. Für eine vollständige Bereinigung würde sich ein + // regelmäßiger Cleanup-Job anbieten, der verwaiste Dateien im uploads/-Ordner + // mit den in der Datenbank referenzierten Pfaden abgleicht. + $oldBlocks = json_decode($article->getContent(), true); + $oldImagePaths = []; + if (is_array($oldBlocks)) { + foreach ($oldBlocks as $oldBlock) { + if (($oldBlock['type'] ?? '') === 'image' + && !empty($oldBlock['value']) + && is_string($oldBlock['value']) + && str_starts_with($oldBlock['value'], 'uploads/')) { + $oldImagePaths[] = $oldBlock['value']; } } - }*/ - - // ----------------------- NEU hinzugefügte Base64-Bilder: -------------------------- - if (is_array($blocks)) { - foreach ($blocks as &$block) { - // Prüfen, ob der Block ein Bild ist und ein NEUES Bild (Base64-Format) enthält - if (isset($block['type']) && isset($block['value']) && $block['type'] === 'image' && is_string($block['value'])) { - - if (str_starts_with($block['value'], 'data:image/')) { - $parts = explode(',', $block['value']); - if (count($parts) >= 2) { - $metadata = $parts[0]; - $base64Data = $parts[1]; - - preg_match('/data:image\/(?.*?);/', $metadata, $matches); - $extension = $matches['extension'] ?? 'jpg'; - if ($extension === 'jpeg') { $extension = 'jpg'; } - - $fileName = 'img_' . uniqid() . '.' . $extension; - $filePath = $uploadDir . $fileName; - - if (file_put_contents($filePath, base64_decode($base64Data)) !== false) { - $block['value'] = 'uploads/' . $fileName; - } else { - $_SESSION["message"] = "image_upload_error"; - header("location: ../../index.php?pfad=updateArticle&id=$id"); - exit(); - } - } - } - } - } - unset($block); } - // Aktualisiertes Array wieder in JSON konvertieren - $finalContent = json_encode($blocks, JSON_UNESCAPED_UNICODE); + $newImagePaths = []; + foreach ($blocks as $block) { + if (($block['type'] ?? '') === 'image' && !empty($block['value'])) { + $newImagePaths[] = $block['value']; + } + } + + $orphanedImages = array_diff($oldImagePaths, $newImagePaths); + foreach ($orphanedImages as $orphanedImage) { + $absolutePath = __DIR__ . '/../../' . $orphanedImage; + if (is_file($absolutePath)) { + @unlink($absolutePath); + } + } // ----------------- Übertragung der validierten Daten in ArticleManager: --------------------------- try { $articleManager = ArticleManager::getInstance(); $article = $articleManager->getArticle($id); $article->setTitle($title); - $article->setContent($finalContent); + $article->setContent($content); $article->setCategory($category); $article->setTags($cleanedTags); - $articleManager->updateArticle($id ,$article, $author); + $articleManager->updateArticle($id, $article, $author); unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]); - } catch (\Throwable $e){ + } catch (\Throwable $e) { $_SESSION["message"] = $e->getMessage(); header("location: ../../index.php?pfad=updateArticle&id=$id"); exit(); diff --git a/php/model/DatabaseInitializer.php b/php/model/DatabaseInitializer.php index f928be1..5e09de9 100644 --- a/php/model/DatabaseInitializer.php +++ b/php/model/DatabaseInitializer.php @@ -234,13 +234,18 @@ class DatabaseInitializer { // Bestimmt per Zufall einen Autor aus dem Pool der gültigen E-Mails $randomAuthor = $availableEmails[array_rand($availableEmails)]; + // Text in (blocks[i][type]/[text]/[image])-Format umwandeln: + $content = json_encode([ + ['type' => 'text', 'value' => $article[1]] + ], JSON_UNESCAPED_UNICODE); + $articleInsertStmt->execute([ ':title' => $article[0], - ':content' => $article[1], + ':content' => $content, ':author' => $randomAuthor, ':category' => $article[2], ':tags' => $article[3] ]); } } -} +} \ No newline at end of file