Merge pull request 'Beitragseditor funktioniert mit und ohne JS (+Bilder löschen)' (#57) from BeitragseditorOhneJS into dev

Reviewed-on: #57
This commit was merged in pull request #57.
This commit is contained in:
2026-07-19 13:45:33 +02:00
7 changed files with 458 additions and 365 deletions
+57 -19
View File
@@ -4,12 +4,28 @@ if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login"); header("Location: index.php?pfad=login");
exit(); 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
?> ?>
<!-- <!--
Seite: Beitrag erstellen Seite: Beitrag erstellen
Inhalt: Formular für die Erstellung eines neuen Beitrags Inhalt: Formular für die Erstellung eines neuen Beitrags
--> -->
<form method="post" action="php/controller/createArticle-controller.php" id="editor-form" class="article-editor-scope.editor-container article-editor-scope editor-container"> <form method="post" action="php/controller/createArticle-controller.php" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container">
<main class="editor-main"> <main class="editor-main">
<?php include_once "includes/alertMessages.php"?> <?php include_once "includes/alertMessages.php"?>
@@ -18,27 +34,49 @@ if (!isset($_SESSION["user"])) {
value="<?php echo htmlspecialchars($_SESSION['old_title'] ?? ''); unset($_SESSION['old_title']); ?>" value="<?php echo htmlspecialchars($_SESSION['old_title'] ?? ''); unset($_SESSION['old_title']); ?>"
placeholder="Titel hier eingeben" required> placeholder="Titel hier eingeben" required>
<!-- Hier werden die dynamischen divs via JavaScript eingefügt --> <!--
<div id="block-container"></div> Content-Blöcke: werden serverseitig als echte, benannte Formularfelder gerendert
(blocks[i][type], blocks[i][text] bzw. blocks[i][image]). Dadurch funktioniert das
Hinzufügen/Entfernen von Blöcken und der Bild-Upload auch ganz ohne JavaScript über
einen normalen Formular-Submit. Ist JavaScript aktiv, fängt js/editor.js diese
Submits ab und erledigt dieselbe Änderung lokal im DOM, ohne den Server zu belasten.
-->
<div id="block-container">
<?php foreach ($blocks as $i => $block): ?>
<?php
$blockType = $block['type'] ?? '';
if ($blockType !== 'text' && $blockType !== 'image') {
continue; // unbekannter/kaputter Block wird übersprungen
}
?>
<div class="editor-block article-editor-scope" data-index="<?php echo (int)$i; ?>">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][type]" value="<?php echo htmlspecialchars($blockType); ?>">
<!-- Plus-Button und das Pop-up-Menü --> <?php if ($blockType === 'text'): ?>
<div id="add-block-control" class="article-editor-scope add-block-control"> <textarea name="blocks[<?php echo (int)$i; ?>][text]"
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button> placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
<div id="block-popup" class="article-editor-scope block-popup hidden"> <?php else: /* image */ ?>
<button type="button" data-type="text">Textblock</button> <?php if (!empty($block['value'])): ?>
<button type="button" data-type="image">Bild einfügen</button> <img src="<?php echo htmlspecialchars($block['value']); ?>"
</div> class="block-image-preview"
style="max-width:200px;display:block;margin-top:10px;">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][existing_image]" value="<?php echo htmlspecialchars($block['value']); ?>">
<?php endif; ?>
<input type="file" name="blocks[<?php echo (int)$i; ?>][image]" accept="image/*">
<?php endif; ?>
<button type="submit" name="editor_action" value="delete_block:<?php echo (int)$i; ?>" class="delete-block-btn">✕</button>
</div>
<?php endforeach; ?>
</div> </div>
<!-- Unsichtbares Textfeld, das die JSON-Daten hält und an den Controller postet --> <div id="add-block-control" class="article-editor-scope add-block-control">
<textarea id="content" name="content" style="display:none;"><?php <button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){ <div id="block-popup" class="article-editor-scope block-popup">
echo htmlspecialchars($_SESSION['old_content']); <button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
unset($_SESSION['old_content']); <button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
} else { </div>
echo '[]'; // Standardmäßig ein leeres JSON-Array </div>
}
?></textarea>
</main> </main>
<!-- Seitenleiste --> <!-- Seitenleiste -->
+57 -25
View File
@@ -5,10 +5,25 @@ if (!isset($_SESSION["user"])) {
exit(); exit();
} }
include_once 'php/controller/showArticle-controller.php'; 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);
?> ?>
<!-- <!--
Seite: Beitrag erstellen Seite: Beitrag bearbeiten
Inhalt: Formular für die Erstellung eines neuen Beitrags Inhalt: Formular für die Bearbeitung eines Beitrags
--> -->
<form method="post" action="php/controller/updateArticle-controller.php?id=<?php if(isset($id) && !empty($id)){echo htmlspecialchars($id);}else{$_SESSION["message"] = "missing_id";} ?>" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container"> <form method="post" action="php/controller/updateArticle-controller.php?id=<?php if(isset($id) && !empty($id)){echo htmlspecialchars($id);}else{$_SESSION["message"] = "missing_id";} ?>" 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> placeholder="Titel hier eingeben" required>
<!-- Hier werden die dynamischen divs via JavaScript eingefügt --> <!--
<div id="block-container"></div> Content-Blöcke: werden serverseitig als echte, benannte Formularfelder gerendert
(blocks[i][type]...). Dadurch funktioniert das Hinzufügen/Entfernen von Blöcken und
der Bild-Upload auch ganz ohne JavaScript über einen normalen Formular-Submit.
Ist JavaScript aktiv, fängt js/editor.js diese Submits ab und erledigt dieselbe
Änderung lokal im DOM, ohne den Server zu belasten.
-->
<div id="block-container">
<?php foreach ($blocks as $i => $block): ?>
<?php
$blockType = $block['type'] ?? '';
if ($blockType !== 'text' && $blockType !== 'image') {
continue; // unbekannter/kaputter Block wird übersprungen
}
?>
<div class="editor-block article-editor-scope" data-index="<?php echo (int)$i; ?>">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][type]" value="<?php echo htmlspecialchars($blockType); ?>">
<!-- Plus-Button und das Pop-up-Menü --> <?php if ($blockType === 'text'): ?>
<div id="add-block-control" class="article-editor-scope add-block-control"> <textarea name="blocks[<?php echo (int)$i; ?>][text]"
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button> placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
<div id="block-popup" class="article-editor-scope block-popup hidden"> <?php else: /* image */ ?>
<button type="button" data-type="text">Textblock</button> <?php if (!empty($block['value'])): ?>
<button type="button" data-type="image">Bild einfügen</button> <img src="<?php echo htmlspecialchars($block['value']); ?>"
</div> class="block-image-preview"
style="max-width:200px;display:block;margin-top:10px;">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][existing_image]" value="<?php echo htmlspecialchars($block['value']); ?>">
<?php endif; ?>
<input type="file" name="blocks[<?php echo (int)$i; ?>][image]" accept="image/*">
<?php endif; ?>
<button type="submit" name="editor_action" value="delete_block:<?php echo (int)$i; ?>" class="delete-block-btn">✕</button>
</div>
<?php endforeach; ?>
</div> </div>
<!-- Unsichtbares Textfeld, das die JSON-Daten hält und an den Controller postet --> <div id="add-block-control" class="article-editor-scope add-block-control">
<textarea id="content" name="content" style="display:none;"><?php <button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){ <div id="block-popup" class="article-editor-scope block-popup">
echo htmlspecialchars($_SESSION['old_content']); <button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
unset($_SESSION['old_content']); <button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
}elseif (isset($content) && !empty($content)){ </div>
echo htmlspecialchars($content); </div>
} else {
echo '[]';
}
?></textarea>
<!-- unsichtbares Input, um die zu löschenden Bilder zu übergeben-->
<input type="hidden" id="deleted-images" name="deleted_images" value="[]">
</main> </main>
<!-- Seitenleiste --> <!-- Seitenleiste -->
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* 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/Veröffentlichen), damit neu ausgewählte Bilder in jedem
* Fall persistiert werden, bevor PHP die temporäre Upload-Datei nach
* Request-Ende verwirft.
*
* Wird sowohl vom createArticle- als auch vom updateArticle-Controller genutzt.
*
* @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;
}
+82 -121
View File
@@ -1,172 +1,133 @@
console.log("Die JavaScript-Datei wurde erfolgreich geladen!"); //console.log("editor.js wurde erfolgreich geladen!");
function initEditor() { function initEditor() {
const form = document.getElementById("editor-form"); const form = document.getElementById("editor-form");
if (!form) { if (!form) {
console.error("Skript abgebrochen: Formular nicht gefunden!"); console.error("editor.js abgebrochen: Formular nicht gefunden!");
return; return;
} else { } 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 container = document.getElementById("block-container");
const plusButton = document.getElementById("plus-button"); const plusButton = document.getElementById("plus-button");
const popup = document.getElementById("block-popup"); 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", () => { plusButton.addEventListener("click", () => {
popup.classList.toggle("hidden"); popup.classList.toggle("hidden");
}); });
// Klick auf eine Block-Option im Pop-up // Klick auf "Textblock" / "Bild einfügen": lokal im DOM anlegen statt zum
popup.querySelectorAll("button").forEach(btn => { // Server zu submitten (entlastet den Server, kein Page-Reload nötig).
btn.addEventListener("click", function() { popup.querySelectorAll('[name="editor_action"]').forEach(btn => {
btn.addEventListener("click", function (e) {
e.preventDefault();
const type = this.getAttribute("data-type"); const type = this.getAttribute("data-type");
addBlockElement(type, ""); addBlockElement(type, "");
popup.classList.add("hidden"); 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 = "") { function addBlockElement(type, value = "") {
const index = blockIndex++;
const blockDiv = document.createElement("div"); const blockDiv = document.createElement("div");
blockDiv.classList.add("editor-block"); blockDiv.classList.add("editor-block", "article-editor-scope");
blockDiv.setAttribute("data-type", type); blockDiv.setAttribute("data-index", String(index));
// Wenn es ein existierendes Server-Bild beim Laden ist, Pfad im globalen Array sichern const typeInput = document.createElement("input");
if (type === "image" && value && typeof value === 'string' && value.startsWith('uploads/')) { typeInput.type = "hidden";
initialImages.push(value); typeInput.name = `blocks[${index}][type]`;
blockDiv.setAttribute("data-value", value); typeInput.value = type;
} blockDiv.appendChild(typeInput);
// 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);
if (type === "text") { if (type === "text") {
const textarea = document.createElement("textarea"); const textarea = document.createElement("textarea");
textarea.name = `blocks[${index}][text]`;
textarea.placeholder = "Schreibe deinen Textblock..."; textarea.placeholder = "Schreibe deinen Textblock...";
textarea.value = value; textarea.value = value;
blockDiv.appendChild(textarea); blockDiv.appendChild(textarea);
} else if (type === "image") { } else if (type === "image") {
const fileInput = document.createElement("input"); const fileInput = document.createElement("input");
fileInput.type = "file"; fileInput.type = "file";
fileInput.name = `blocks[${index}][image]`;
fileInput.accept = "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(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); container.appendChild(blockDiv);
bindDeleteButton(blockDiv);
bindImageInput(blockDiv);
} }
// beim Abschicken verbleibende Blöcke auslesen UND gelöschte Bilder ermitteln // Löschen-Button eines Blocks lokal abfangen: entfernt den Block nur aus dem DOM,
form.addEventListener("submit", function(e) { // statt das Formular zum Server zu senden.
const blocks = []; function bindDeleteButton(blockDiv) {
const currentImages = []; const btn = blockDiv.querySelector(".delete-block-btn");
if (!btn) return;
// alle aktuell im Formular verbliebenen Blöcke scannen btn.addEventListener("click", (e) => {
container.querySelectorAll(".editor-block").forEach(blockDiv => { e.preventDefault();
const type = blockDiv.getAttribute("data-type"); blockDiv.remove();
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 });
}); });
}
// das reguläre unsichtbare Content-Feld befüllen // Zeigt bei Auswahl einer Bilddatei sofort eine Vorschau an. Rein optisch
hiddenContentInput.value = JSON.stringify(blocks); // der eigentliche Datei-Upload läuft nativ über das <input type="file">.
function bindImageInput(blockDiv) {
const fileInput = blockDiv.querySelector('input[type="file"]');
if (!fileInput) return;
// Differenz berechnen: Welche Bilder aus 'initialImages' fehlen in 'currentImages' ? let imgPreview = blockDiv.querySelector(".block-image-preview");
const deletedImages = initialImages.filter(img => !currentImages.includes(img)); if (!imgPreview) {
imgPreview = document.createElement("img");
// das 'deleted_images'-Feld dynamisch befüllen und an den Controller senden imgPreview.classList.add("block-image-preview");
let deletedInput = document.getElementById("deleted-images"); imgPreview.style.maxWidth = "200px";
if (!deletedInput) { imgPreview.style.display = "none";
deletedInput = document.createElement("input"); imgPreview.style.marginTop = "10px";
deletedInput.type = "hidden"; blockDiv.insertBefore(imgPreview, fileInput.nextSibling);
deletedInput.id = "deleted-images";
deletedInput.name = "deleted_images";
form.appendChild(deletedInput);
} }
deletedInput.value = JSON.stringify(deletedImages);
});
// Existierende Blöcke laden (stellt alte Daten aus der Session wieder her) fileInput.addEventListener("change", function () {
try { if (this.files && this.files[0]) {
const initialBlocks = JSON.parse(hiddenContentInput.value.trim()); const reader = new FileReader();
if (Array.isArray(initialBlocks)) { reader.onload = (e) => {
initialBlocks.forEach(b => { imgPreview.src = e.target.result;
if (b.type === "image" && b.value && typeof b.value === 'string' && !b.value.startsWith('data:image/')) { imgPreview.style.display = "block";
let cleanPath = b.value.trim().replace(/\\\//g, '/'); // Verwandelt \/ in / };
reader.readAsDataURL(this.files[0]);
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);
}
} }
} }
+100 -114
View File
@@ -5,134 +5,120 @@ if (session_status() === PHP_SESSION_NONE) {
require_once '../model/LocalArticleManager.php'; require_once '../model/LocalArticleManager.php';
require_once '../model/ArticleManager.php'; require_once '../model/ArticleManager.php';
require_once '../validator/article-validator.php'; require_once '../validator/article-validator.php';
require_once '../../includes/article-block-helper.php';
if (!isset($_SESSION["user"])) { if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login"); header("Location: index.php?pfad=login");
exit(); 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"])){ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$_SESSION["message"] = "missing_parameters";
$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"); header("location: ../../index.php?pfad=createArticle");
exit(); exit();
} else { } else {
$title = $_POST["title"]; $cleanedTags = [];
$content = $_POST["content"]; $rawTags = explode(',', $tags);
$author = $_SESSION["user_email"]; foreach ($rawTags as $rawTag) {
$category = $_POST["category"]; // Leerzeichen am Anfang/Ende des einzelnen Tags entfernen:
$tags = $_POST['tags'] ?? ''; $tag = trim($rawTag);
$cleanedTags[] = $tag;
// -------------------------------- Validierung der Daten: -------------------------
if (!articleTitleValidator($title)) {
$_SESSION["message"] = "invalid_title";
header("location: ../../index.php?pfad=createArticle");
exit();
} }
// Duplikate entfernen:
$cleanedTags = array_unique($cleanedTags);
$cleanedTags = implode(',', $cleanedTags);
}
if (!articleContentValidator($content)) { // ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
$_SESSION["message"] = "invalid_content"; try {
header("location: ../../index.php?pfad=createArticle"); $articleManager = ArticleManager::getInstance();
exit(); // $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)) { // Formulardaten nach erfolgreichem Erstellen aus der Session löschen
$_SESSION["message"] = "invalid_category"; unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
header("location: ../../index.php?pfad=createArticle");
exit();
}
if (!articleTagValidator($tags)) { } catch (\Throwable $e) {
$_SESSION["message"] = "invalid_tags"; $_SESSION["message"] = "internal_error";
header("location: ../../index.php?pfad=createArticle"); 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\/(?<extension>.*?);/', $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");
exit(); exit();
} }
$_SESSION["message"] = "new_article";
// Weiterleitung zur Homepage
header("location: ../../index.php");
exit();
} }
}
?> ?>
+83 -81
View File
@@ -7,17 +7,14 @@ require_once '../model/LocalArticleManager.php';
require_once '../model/ArticleManager.php'; require_once '../model/ArticleManager.php';
require_once '../model/Article.php'; require_once '../model/Article.php';
require_once '../validator/article-validator.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"); header("Location: index.php?pfad=login");
exit(); exit();
} }
if ($_SERVER["REQUEST_METHOD"] === "POST") { 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"])) { if (isset($_GET["id"]) && !empty($_GET["id"])) {
$id = $_GET["id"]; $id = $_GET["id"];
@@ -41,16 +38,56 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
exit(); 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"; $_SESSION["message"] = "missing_parameters";
header("location: ../../index.php?pfad=updateArticle&id=$id"); header("location: ../../index.php?pfad=updateArticle&id=$id");
exit(); exit();
}else{ } else {
$title = $_POST["title"]; $title = $_POST["title"];
$content = $_POST["content"]; $content = json_encode($blocks, JSON_UNESCAPED_UNICODE);
$author = $_SESSION["user_email"]; $author = $_SESSION["user_email"];
$category = $_POST["category"]; $category = $_POST["category"];
$tags = $_POST['tags'] ?? ''; $tags = $_POST['tags'] ?? '';
// -------------------------------- Validierung der Daten: ------------------------- // -------------------------------- Validierung der Daten: -------------------------
if (!articleTitleValidator($title)) { if (!articleTitleValidator($title)) {
@@ -88,91 +125,56 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$cleanedTags = implode(',', $cleanedTags); $cleanedTags = implode(',', $cleanedTags);
} }
// --------------------------------------- Base64-Bilder speichern --------------------------------------------- // ----------------- Verwaiste Bilder aufräumen -----------------
$blocks = json_decode($content, true); // Bilder, die im alten (gespeicherten) Content vorkamen, im neuen aber nicht
$uploadDir = __DIR__ . '/../../uploads/'; // mehr referenziert werden, wurden vom Nutzer entfernt oder ersetzt -> löschen.
// Hinweis/TODO: Bilder, die innerhalb derselben Bearbeitungs-Sitzung neu
if (!file_exists($uploadDir)) { // hochgeladen und noch vor dem finalen Speichern wieder entfernt wurden,
mkdir($uploadDir, 0755, true); // 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
// ----------------- Gelöschte Bilder über die JS-Löschliste entfernen ----------------- TODO: Gelöschte Bilder über die JS-Löschliste entfernen // mit den in der Datenbank referenzierten Pfaden abgleicht.
/*if (isset($_POST['deleted_images'])) { $oldBlocks = json_decode($article->getContent(), true);
$deletedImages = json_decode($_POST['deleted_images'], true); $oldImagePaths = [];
if (is_array($oldBlocks)) {
// Wir ermitteln den physisch echten, absoluten Pfad zum uploads-Ordner auf der Festplatte foreach ($oldBlocks as $oldBlock) {
$uploadDir = realpath(__DIR__ . '/../../uploads') . DIRECTORY_SEPARATOR; if (($oldBlock['type'] ?? '') === 'image'
&& !empty($oldBlock['value'])
if (is_array($deletedImages)) { && is_string($oldBlock['value'])
foreach ($deletedImages as $imagePath) { && str_starts_with($oldBlock['value'], 'uploads/')) {
// Nur den reinen Dateinamen heraustrennen (z.B. img_65a123.jpg) $oldImagePaths[] = $oldBlock['value'];
$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);
}
} }
} }
}*/
// ----------------------- 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\/(?<extension>.*?);/', $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 $newImagePaths = [];
$finalContent = json_encode($blocks, JSON_UNESCAPED_UNICODE); 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: --------------------------- // ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$article = $articleManager->getArticle($id); $article = $articleManager->getArticle($id);
$article->setTitle($title); $article->setTitle($title);
$article->setContent($finalContent); $article->setContent($content);
$article->setCategory($category); $article->setCategory($category);
$article->setTags($cleanedTags); $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"]); unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
} catch (\Throwable $e){ } catch (\Throwable $e) {
$_SESSION["message"] = $e->getMessage(); $_SESSION["message"] = $e->getMessage();
header("location: ../../index.php?pfad=updateArticle&id=$id"); header("location: ../../index.php?pfad=updateArticle&id=$id");
exit(); exit();
+6 -1
View File
@@ -234,9 +234,14 @@ class DatabaseInitializer {
// Bestimmt per Zufall einen Autor aus dem Pool der gültigen E-Mails // Bestimmt per Zufall einen Autor aus dem Pool der gültigen E-Mails
$randomAuthor = $availableEmails[array_rand($availableEmails)]; $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([ $articleInsertStmt->execute([
':title' => $article[0], ':title' => $article[0],
':content' => $article[1], ':content' => $content,
':author' => $randomAuthor, ':author' => $randomAuthor,
':category' => $article[2], ':category' => $article[2],
':tags' => $article[3] ':tags' => $article[3]