From 7341fe77335ebf8706d0eed3118dcdcfe74cb8f8 Mon Sep 17 00:00:00 2001 From: NOrtmann1 <145041949+NOrtmann1@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:48:33 +0200 Subject: [PATCH 1/9] Update updateArticle.php --- content/updateArticle.php | 84 +++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 26 deletions(-) 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 From 97fa1de99956f85b2b3d3d5f1d46cabb3a880033 Mon Sep 17 00:00:00 2001 From: NOrtmann1 <145041949+NOrtmann1@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:48:38 +0200 Subject: [PATCH 2/9] Update updateArticle-controller.php --- php/controller/updateArticle-controller.php | 220 +++++++++++++------- 1 file changed, 140 insertions(+), 80 deletions(-) diff --git a/php/controller/updateArticle-controller.php b/php/controller/updateArticle-controller.php index c5ef8f6..bcb2093 100644 --- a/php/controller/updateArticle-controller.php +++ b/php/controller/updateArticle-controller.php @@ -13,11 +13,73 @@ if (!isset($_SESSION["user_email"])) { exit(); } +/** + * Baut die Blockliste aus den POST-Daten (blocks[i][type], blocks[i][text], + * blocks[i][existing_image]) und ggf. hochgeladenen Dateien (blocks[i][image]) + * zusammen. Läuft bei JEDEM Submit (Zwischen-Schritt "Block hinzufügen/löschen" + * UND finales Speichern), damit neu ausgewählte Bilder in jedem Fall persistiert + * werden, bevor PHP die temporäre Upload-Datei nach Request-Ende verwirft. + * + * @param array $postBlocks $_POST['blocks'] ?? [] + * @param array $fileBlocks $_FILES['blocks'] ?? [] + * @param string $uploadDir absoluter Pfad zum uploads-Verzeichnis (mit trailing slash) + * @return array Liste von ['type' => '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; +} + 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 +103,54 @@ 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); + + // --------------------------------------------------------------------- + // 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: ../../content/updateArticle.php?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 +188,51 @@ 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. + // 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 + $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(); From f1a32120d696459b10548855fa1fb13771ad9fe9 Mon Sep 17 00:00:00 2001 From: NOrtmann1 <145041949+NOrtmann1@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:48:41 +0200 Subject: [PATCH 3/9] Update editor.js --- js/editor.js | 205 +++++++++++++++++++++------------------------------ 1 file changed, 83 insertions(+), 122 deletions(-) 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 From c0a94b2eeee8ce4e510f8a87cc8f01d45460ef2f Mon Sep 17 00:00:00 2001 From: NOrtmann1 <145041949+NOrtmann1@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:15:14 +0200 Subject: [PATCH 4/9] Update createArticle-controller.php --- php/controller/createArticle-controller.php | 214 +++++++++----------- 1 file changed, 100 insertions(+), 114 deletions(-) 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 From 144a1015f1d20576604313f2e196ac97ec96e3ad Mon Sep 17 00:00:00 2001 From: NOrtmann1 <145041949+NOrtmann1@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:15:17 +0200 Subject: [PATCH 5/9] Create article-block-helper.php --- includes/article-block-helper.php | 69 +++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 includes/article-block-helper.php 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 From 6bf335ce461b1ed13572f43bfd7d274bfd70456d Mon Sep 17 00:00:00 2001 From: NOrtmann1 <145041949+NOrtmann1@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:15:20 +0200 Subject: [PATCH 6/9] Update createArticle.php --- content/createArticle.php | 78 +++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 20 deletions(-) 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 From 8c8b148bf4efc698faff43b1353c34f64bc97a48 Mon Sep 17 00:00:00 2001 From: NOrtmann1 <145041949+NOrtmann1@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:15:23 +0200 Subject: [PATCH 7/9] Update updateArticle-controller.php --- php/controller/updateArticle-controller.php | 78 +++------------------ 1 file changed, 10 insertions(+), 68 deletions(-) diff --git a/php/controller/updateArticle-controller.php b/php/controller/updateArticle-controller.php index bcb2093..9052fcd 100644 --- a/php/controller/updateArticle-controller.php +++ b/php/controller/updateArticle-controller.php @@ -7,78 +7,13 @@ 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"])) { header("Location: index.php?pfad=login"); exit(); } -/** - * Baut die Blockliste aus den POST-Daten (blocks[i][type], blocks[i][text], - * blocks[i][existing_image]) und ggf. hochgeladenen Dateien (blocks[i][image]) - * zusammen. Läuft bei JEDEM Submit (Zwischen-Schritt "Block hinzufügen/löschen" - * UND finales Speichern), damit neu ausgewählte Bilder in jedem Fall persistiert - * werden, bevor PHP die temporäre Upload-Datei nach Request-Ende verwirft. - * - * @param array $postBlocks $_POST['blocks'] ?? [] - * @param array $fileBlocks $_FILES['blocks'] ?? [] - * @param string $uploadDir absoluter Pfad zum uploads-Verzeichnis (mit trailing slash) - * @return array Liste von ['type' => '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; -} - if ($_SERVER["REQUEST_METHOD"] === "POST") { if (isset($_GET["id"]) && !empty($_GET["id"])) { @@ -118,7 +53,9 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") { $_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE); // --------------------------------------------------------------------- - // Zwischenspeichern + // 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']; @@ -191,7 +128,12 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") { // ----------------- 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. - // 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 + // 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)) { From 4b84f0ab09e0df0a9205bb874bf87e838bdf1e17 Mon Sep 17 00:00:00 2001 From: NOrtmann1 <145041949+NOrtmann1@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:30:39 +0200 Subject: [PATCH 8/9] Update updateArticle-controller.php --- php/controller/updateArticle-controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/php/controller/updateArticle-controller.php b/php/controller/updateArticle-controller.php index 9052fcd..cb58617 100644 --- a/php/controller/updateArticle-controller.php +++ b/php/controller/updateArticle-controller.php @@ -9,7 +9,7 @@ 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(); } @@ -71,7 +71,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") { } $_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE); - header("location: ../../content/updateArticle.php?id=$id"); + header("location: ../../index.php?pfad=updateArticle&id=$id"); exit(); } From 9b9f71d21c466a1419bf6a82dc2a477109938548 Mon Sep 17 00:00:00 2001 From: NOrtmann1 <145041949+NOrtmann1@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:44:27 +0200 Subject: [PATCH 9/9] Update DatabaseInitializer.php --- php/model/DatabaseInitializer.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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