Compare commits
13 Commits
f1a32120d6
...
loader
| Author | SHA1 | Date | |
|---|---|---|---|
| 09f8b11f36 | |||
| cc4cebdb45 | |||
| 96f38ce72e | |||
| b81f0c9e5c | |||
| f8aed4283e | |||
| afd90086d8 | |||
| a7a54f877c | |||
| 9b9f71d21c | |||
| 4b84f0ab09 | |||
| 8c8b148bf4 | |||
| 6bf335ce46 | |||
| 144a1015f1 | |||
| c0a94b2eee |
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="dataSourceStorageLocal" created-in="IU-261.24374.151">
|
||||
<component name="dataSourceStorageLocal" created-in="IU-253.32098.101">
|
||||
<data-source name="articles" uuid="315cb5c9-2b0f-435b-b602-59823b160908">
|
||||
<database-info product="SQLite" version="3.51.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.51.1.0" dbms="SQLITE" exact-version="3.51.1" exact-driver-version="3.51">
|
||||
<identifier-quote-string>"</identifier-quote-string>
|
||||
|
||||
+58
-20
@@ -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
|
||||
?>
|
||||
<!--
|
||||
Seite: Beitrag erstellen
|
||||
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">
|
||||
<?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']); ?>"
|
||||
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ü -->
|
||||
<div id="add-block-control" class="article-editor-scope add-block-control">
|
||||
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
|
||||
<div id="block-popup" class="article-editor-scope block-popup hidden">
|
||||
<button type="button" data-type="text">Textblock</button>
|
||||
<button type="button" data-type="image">Bild einfügen</button>
|
||||
</div>
|
||||
<?php if ($blockType === 'text'): ?>
|
||||
<textarea name="blocks[<?php echo (int)$i; ?>][text]"
|
||||
placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
|
||||
<?php else: /* image */ ?>
|
||||
<?php if (!empty($block['value'])): ?>
|
||||
<img src="<?php echo htmlspecialchars($block['value']); ?>"
|
||||
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>
|
||||
|
||||
<!-- Unsichtbares Textfeld, das die JSON-Daten hält und an den Controller postet -->
|
||||
<textarea id="content" name="content" style="display:none;"><?php
|
||||
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){
|
||||
echo htmlspecialchars($_SESSION['old_content']);
|
||||
unset($_SESSION['old_content']);
|
||||
} else {
|
||||
echo '[]'; // Standardmäßig ein leeres JSON-Array
|
||||
}
|
||||
?></textarea>
|
||||
<div id="add-block-control" class="article-editor-scope add-block-control">
|
||||
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
|
||||
<div id="block-popup" class="article-editor-scope block-popup">
|
||||
<button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
|
||||
<button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Seitenleiste -->
|
||||
@@ -268,4 +306,4 @@ if (!isset($_SESSION["user"])) {
|
||||
|
||||
</aside>
|
||||
|
||||
</form>
|
||||
</form>
|
||||
+1
-1
@@ -84,7 +84,7 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
|
||||
<form action="php/controller/deleteAccount-controller.php" method="POST">
|
||||
<button type="submit"
|
||||
class="button"
|
||||
onclick="return confirm('Möchtest du deinen Account wirklich unwiderruflich löschen?');">
|
||||
onclick="return confirm('Möchtest du deinen Account und alle deine Beiträge wirklich unwiderruflich löschen?');">
|
||||
Account löschen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -137,6 +137,17 @@ $categories = [
|
||||
<p class="s-res-meta"><span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"</p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
id="results-loading"
|
||||
class="results-loading"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
hidden
|
||||
>
|
||||
<span class="results-spinner" aria-hidden="true"></span>
|
||||
Suchergebnisse werden geladen …
|
||||
</p>
|
||||
|
||||
<!-- Ergebnisliste -->
|
||||
<div class="s-res-list">
|
||||
<?php if (!empty($results)): ?>
|
||||
|
||||
@@ -457,6 +457,18 @@ if ($replyAuthor === null) {
|
||||
<button type="submit" class="button">
|
||||
Kommentar senden
|
||||
</button>
|
||||
|
||||
<p
|
||||
id="comment-loading"
|
||||
class="comment-loading"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
hidden
|
||||
>
|
||||
<span class="comment-spinner" aria-hidden="true"></span>
|
||||
Kommentar wird gesendet …
|
||||
</p>
|
||||
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<div class="comment-login-hint">
|
||||
|
||||
@@ -113,6 +113,17 @@ $resultCount = count($results);
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
id="results-loading"
|
||||
class="results-loading"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
hidden
|
||||
>
|
||||
<span class="results-spinner" aria-hidden="true"></span>
|
||||
Kategoriebeiträge werden geladen …
|
||||
</p>
|
||||
|
||||
<!-- Ergebnisliste -->
|
||||
<div class="s-res-list">
|
||||
<?php if (!empty($results)): ?>
|
||||
|
||||
@@ -225,4 +225,34 @@ h1 {
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.comment-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.comment-loading[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #cbd5e1;
|
||||
border-top-color: #1f2937;
|
||||
border-radius: 50%;
|
||||
animation: comment-spinner-rotation 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes comment-spinner-rotation {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#comment-form button[type="submit"]:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.65;
|
||||
}
|
||||
@@ -271,4 +271,33 @@ CSS für die Suchergebnis-Seite
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
/* Ladeanzeige beim Nachladen von Such- und Kategorieergebnissen */
|
||||
.results-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 15px 0;
|
||||
color: #4a5568;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.results-loading[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.results-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid #cbd5e1;
|
||||
border-top-color: #3182ce;
|
||||
border-radius: 50%;
|
||||
animation: results-spinner-rotation 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes results-spinner-rotation {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+38
-1
@@ -11,7 +11,19 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
const parentCommentInput = document.getElementById("parent-comment-id");
|
||||
const replyInfo = document.getElementById("reply-info");
|
||||
|
||||
if (!form || !commentsList || !commentContent || !parentCommentInput) {
|
||||
const submitButton = form
|
||||
? form.querySelector('button[type="submit"]')
|
||||
: null;
|
||||
|
||||
const loadingMessage = document.getElementById("comment-loading");
|
||||
|
||||
if (
|
||||
!form
|
||||
|| !commentsList
|
||||
|| !commentContent
|
||||
|| !parentCommentInput
|
||||
|| !submitButton
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,6 +65,23 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
form.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
/*
|
||||
* Verhindert einen erneuten Submit, während der vorherige
|
||||
* Kommentar noch gespeichert wird.
|
||||
*/
|
||||
if (submitButton.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalButtonText = submitButton.textContent;
|
||||
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = "Wird gesendet …";
|
||||
|
||||
if (loadingMessage) {
|
||||
loadingMessage.hidden = false;
|
||||
}
|
||||
|
||||
const formData = new FormData(form);
|
||||
const parentCommentId = parentCommentInput.value;
|
||||
|
||||
@@ -269,6 +298,14 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
.catch(function (error) {
|
||||
console.error(error);
|
||||
alert("Kommentar konnte nicht gesendet werden.");
|
||||
})
|
||||
.finally(function () {
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = originalButtonText;
|
||||
|
||||
if (loadingMessage) {
|
||||
loadingMessage.hidden = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+23
-4
@@ -25,6 +25,7 @@
|
||||
|
||||
let listContainer, sortRadios, categorySelect, limitSelect;
|
||||
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
|
||||
let loadingElement;
|
||||
|
||||
function init() {
|
||||
listContainer = document.querySelector('.s-res-list');
|
||||
@@ -38,23 +39,41 @@
|
||||
numbersContainer = document.getElementById('dynamic-page-numbers');
|
||||
jsNav = document.getElementById('js-page-navigation');
|
||||
resultCountEl = document.getElementById('s-res-result-count');
|
||||
loadingElement = document.getElementById('results-loading');
|
||||
|
||||
const checkedRadio = document.querySelector('.sort-radio:checked');
|
||||
state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
|
||||
state.category = categorySelect ? categorySelect.value : 'all';
|
||||
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
|
||||
|
||||
if (loadingElement) {
|
||||
loadingElement.hidden = false;
|
||||
}
|
||||
|
||||
fetchFullDataset()
|
||||
.then(function (data) {
|
||||
state.allItems = data.results || [];
|
||||
attachEvents();
|
||||
render();
|
||||
if (jsNav) jsNav.style.display = 'flex';
|
||||
|
||||
if (jsNav) {
|
||||
jsNav.style.display = 'flex';
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
// JSON-Endpoint nicht erreichbar: die serverseitig gerenderte
|
||||
// (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter.
|
||||
console.error('Suchergebnisse konnten nicht nachgeladen werden:', err);
|
||||
/*
|
||||
* Falls das Nachladen nicht funktioniert, bleibt die bereits
|
||||
* serverseitig ausgegebene Ergebnisliste sichtbar.
|
||||
*/
|
||||
console.error(
|
||||
'Suchergebnisse konnten nicht nachgeladen werden:',
|
||||
err
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
if (loadingElement) {
|
||||
loadingElement.hidden = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+23
-4
@@ -21,6 +21,7 @@
|
||||
|
||||
let listContainer, sortRadios, searchInput, limitSelect;
|
||||
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
|
||||
let loadingElement;
|
||||
let searchDebounceTimer;
|
||||
|
||||
function init() {
|
||||
@@ -35,23 +36,41 @@
|
||||
numbersContainer = document.getElementById('dynamic-page-numbers');
|
||||
jsNav = document.getElementById('js-page-navigation');
|
||||
resultCountEl = document.getElementById('s-res-result-count');
|
||||
loadingElement = document.getElementById('results-loading');
|
||||
|
||||
const checkedRadio = document.querySelector('.sort-radio:checked');
|
||||
state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
|
||||
state.query = searchInput ? searchInput.value : '';
|
||||
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
|
||||
|
||||
if (loadingElement) {
|
||||
loadingElement.hidden = false;
|
||||
}
|
||||
|
||||
fetchFullDataset()
|
||||
.then(function (data) {
|
||||
state.allItems = data.results || [];
|
||||
attachEvents();
|
||||
render();
|
||||
if (jsNav) jsNav.style.display = 'flex';
|
||||
|
||||
if (jsNav) {
|
||||
jsNav.style.display = 'flex';
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
// JSON-Endpoint nicht erreichbar: die serverseitig gerenderte
|
||||
// (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter.
|
||||
console.error('Kategorie-Beiträge konnten nicht nachgeladen werden:', err);
|
||||
/*
|
||||
* Falls das Nachladen nicht funktioniert, bleibt die bereits
|
||||
* serverseitig ausgegebene Kategorieansicht sichtbar.
|
||||
*/
|
||||
console.error(
|
||||
'Kategorie-Beiträge konnten nicht nachgeladen werden:',
|
||||
err
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
if (loadingElement) {
|
||||
loadingElement.hidden = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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\/(?<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");
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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"])) {
|
||||
if (!isset($_SESSION["user"])) {
|
||||
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'];
|
||||
@@ -134,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();
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -310,21 +310,20 @@ class DatabaseArticleManager implements ArticleManagerDAO {
|
||||
|
||||
try {
|
||||
$db = $this->getConnection();
|
||||
|
||||
|
||||
$sql = "SELECT id, title, content, author, category, tags, created
|
||||
FROM articles
|
||||
WHERE title LIKE :keyword
|
||||
OR content LIKE :keyword
|
||||
OR tags LIKE :keyword";
|
||||
FROM articles
|
||||
WHERE title LIKE :keyword
|
||||
OR content LIKE :keyword
|
||||
OR tags LIKE :keyword;";
|
||||
|
||||
$command = $db->prepare($sql);
|
||||
if (!$command) {
|
||||
throw new InternalServerErrorException("internal_error");
|
||||
}
|
||||
|
||||
// Wildcards für die Suche hinzufügen
|
||||
// Wildcards für die SQL-Suche hinzufügen
|
||||
$searchParam = '%' . $cleankeyword . '%';
|
||||
|
||||
$success = $command->execute([
|
||||
":keyword" => $searchParam
|
||||
]);
|
||||
@@ -337,10 +336,11 @@ class DatabaseArticleManager implements ArticleManagerDAO {
|
||||
$filteredArticles = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$likes = $this->getLikesForArticle(intval($row['id']));
|
||||
$articleId = intval($row['id']);
|
||||
$likes = $this->getLikesForArticle($articleId);
|
||||
|
||||
$filteredArticles[] = new Article(
|
||||
intval($row['id']),
|
||||
$articleId,
|
||||
$row['title'] ?? '',
|
||||
$row['content'] ?? '',
|
||||
$row['author'] ?? '',
|
||||
@@ -358,6 +358,7 @@ class DatabaseArticleManager implements ArticleManagerDAO {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Holt alle User-IDs, die einen bestimmten Beitrag geliked haben.
|
||||
*
|
||||
|
||||
@@ -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]
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user