Compare commits

..

15 Commits

Author SHA1 Message Date
niklas.ortmann e1529da4b0 Update search-results.php 2026-07-18 15:19:27 +02:00
niklas.ortmann 986b85bed9 search-results.js 2026-07-18 15:05:22 +02:00
niklas.ortmann 39d90c9fe0 revert 2026-07-18 15:00:54 +02:00
niklas.ortmann db62cee09b revert 2026-07-18 14:52:26 +02:00
niklas.ortmann b8d32de25f debugging 2026-07-18 14:34:30 +02:00
niklas.ortmann 470d98940a debugging 2026-07-18 14:30:54 +02:00
niklas.ortmann cab41ade7a debugging 2026-07-18 14:26:48 +02:00
niklas.ortmann cf76f86f09 debugging 2026-07-18 14:25:27 +02:00
niklas.ortmann bf33afb15b debugging 2026-07-18 14:22:05 +02:00
niklas.ortmann 193c523de6 Update paginator.js 2026-07-18 14:19:35 +02:00
niklas.ortmann 36c47bb7a6 Update paginator.js 2026-07-18 14:19:12 +02:00
niklas.ortmann 6031fc0185 Update editor.js 2026-07-18 14:19:09 +02:00
niklas.ortmann e77c60d8c8 js übernimmt Sortierung + Fallback 2026-07-18 14:04:13 +02:00
niklas.ortmann 7f5cea8c1e revert 2026-07-18 13:33:15 +02:00
niklas.ortmann 71c213ef1e js sortiert die Ergebnisse 2026-07-18 13:18:04 +02:00
46 changed files with 1492 additions and 3331 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="dataSourceStorageLocal" created-in="IU-253.32098.101"> <component name="dataSourceStorageLocal" created-in="IU-261.25134.95">
<data-source name="articles" uuid="315cb5c9-2b0f-435b-b602-59823b160908"> <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"> <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>&quot;</identifier-quote-string> <identifier-quote-string>&quot;</identifier-quote-string>
-3
View File
@@ -36,9 +36,6 @@
- JavaScript wird verwendet, um im erweitertem Beitragseditor clientseitig einzelne Content-Boxen erstellen und löschen - JavaScript wird verwendet, um im erweitertem Beitragseditor clientseitig einzelne Content-Boxen erstellen und löschen
zu können. zu können.
- JavaScript wird ebenfalls verwendet, um in die Suchergebnisse clientseitig zu sortieren. - JavaScript wird ebenfalls verwendet, um in die Suchergebnisse clientseitig zu sortieren.
- Die Kommentarfunktion ist zusätzlich auch ohne JavaScript nutzbar. Kommentare und Antworten können auch ohne JavaScript erstellt werden.
- Eigene Kommentare und Antworten können bearbeitet und gelöscht werden. Die Bearbeitung und das Löschen funktionieren ebenfalls ohne JavaScript über normale Formulare und serverseitige Verarbeitung.
## Sonstiges ## Sonstiges
- Das Datenschema befindet sich unter /planung/Datenschema.pdf - Das Datenschema befindet sich unter /planung/Datenschema.pdf
+5 -1
View File
@@ -4,7 +4,11 @@
--> -->
<main> <main>
<?php include_once "includes/alertMessages.php"?> <?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "internal_error"): ?>
<p class="alert-message is-error">
Es ist ein interner Fehler beim Speichern aufgetreten. Bitte versuche es erneut.
</p>
<?php endif; ?>
<?php <?php
unset($_SESSION["message"]); unset($_SESSION["message"]);
?> ?>
+18 -58
View File
@@ -4,30 +4,12 @@ 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" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container"> <form method="post" action="php/controller/createArticle-controller.php" id="editor-form" class="article-editor-scope.editor-container article-editor-scope editor-container">
<?php csrf_field(); ?>
<main class="editor-main"> <main class="editor-main">
<?php include_once "includes/alertMessages.php"?> <?php include_once "includes/alertMessages.php"?>
@@ -36,49 +18,27 @@ $blocks = array_values($blocks); // sequentielle Indizes 0..n-1 sicherstellen
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 -->
Content-Blöcke: werden serverseitig als echte, benannte Formularfelder gerendert <div id="block-container"></div>
(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); ?>">
<?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>
<!-- Plus-Button und das Pop-up-Menü -->
<div id="add-block-control" class="article-editor-scope add-block-control"> <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> <button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
<div id="block-popup" class="article-editor-scope block-popup"> <div id="block-popup" class="article-editor-scope block-popup hidden">
<button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button> <button type="button" data-type="text">Textblock</button>
<button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button> <button type="button" data-type="image">Bild einfügen</button>
</div> </div>
</div> </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>
</main> </main>
<!-- Seitenleiste --> <!-- Seitenleiste -->
@@ -308,4 +268,4 @@ $blocks = array_values($blocks); // sequentielle Indizes 0..n-1 sicherstellen
</aside> </aside>
</form> </form>
+2 -5
View File
@@ -1,4 +1,5 @@
<?php <?php
include_once 'php/controller/profile-controller.php';
$user = $user ?? null; $user = $user ?? null;
$isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error); $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
@@ -17,8 +18,6 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
<form method="post" action="index.php?pfad=profile"> <form method="post" action="index.php?pfad=profile">
<?php csrf_field(); ?>
<label class="input-label">Vorname</label> <label class="input-label">Vorname</label>
<input type="text" <input type="text"
name="vorname" name="vorname"
@@ -83,10 +82,9 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
<br> <br>
<form action="php/controller/deleteAccount-controller.php" method="POST"> <form action="php/controller/deleteAccount-controller.php" method="POST">
<?php csrf_field(); ?>
<button type="submit" <button type="submit"
class="button" class="button"
onclick="return confirm('Möchtest du deinen Account und alle deine Beiträge wirklich unwiderruflich löschen?');"> onclick="return confirm('Möchtest du deinen Account wirklich unwiderruflich löschen?');">
Account löschen Account löschen
</button> </button>
</form> </form>
@@ -157,7 +155,6 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
</a> </a>
<form action="php/controller/deleteArticle-controller.php" method="POST"> <form action="php/controller/deleteArticle-controller.php" method="POST">
<?php csrf_field(); ?>
<input type="hidden" <input type="hidden"
name="id" name="id"
value="<?php echo htmlspecialchars($userArticle->getID()); ?>"> value="<?php echo htmlspecialchars($userArticle->getID()); ?>">
+116 -96
View File
@@ -2,33 +2,46 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
require_once __DIR__ . '/../includes/resultsHelper.php';
$rawResults = $_SESSION["search_results"] ?? []; $all_results = $_SESSION["search_results"] ?? [];
$query = $_SESSION["search_query"] ?? ""; $query = $_SESSION["search_query"] ?? "";
// Sortierung / Kategorie / Limit: GET-Parameter haben Vorrang, sonst // ---------------------------------------------------------------
// Rückfall auf die zuletzt in der Session gemerkten Werte. So bleibt // Parameter aus der URL lesen & validieren
// z.B. ein reiner Pagination-Link (?page=2) bei der aktuellen // ---------------------------------------------------------------
// Sortierung/Filterung. $allowedCategories = [
$currentSort = $_GET['sort'] ?? ($_SESSION['search_sort'] ?? 'alphabet'); 'all', 'Deutsch', 'Englisch', 'Franzoesisch', 'Latein', 'Literatur',
if (!in_array($currentSort, ['alphabet', 'likes', 'newest', 'oldest'])) { 'Mathematik', 'Biologie', 'Informatik', 'Chemie', 'Physik', 'Astronomie',
$currentSort = 'alphabet'; 'Geschichte', 'Erdkunde', 'Sozialkunde', 'Wirtschaftskunde', 'Religion',
'Ethikunterricht', 'Philosophie', 'Psychologie', 'Kunst', 'Musik',
'Theater', 'Technik', 'Werken', 'Hauswirtschaft', 'Sport',
];
$category = $_GET['category'] ?? 'all';
if (!in_array($category, $allowedCategories, true)) {
$category = 'all';
} }
$_SESSION['search_sort'] = $currentSort;
$currentCategory = strtolower($_GET['category'] ?? ($_SESSION['search_category'] ?? 'all')); $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
$_SESSION['search_category'] = $currentCategory; if (!in_array($limit, [10, 20, 50, 100], true)) {
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['search_limit'] ?? 10);
if (!in_array($limit, [10, 20, 50, 100])) {
$limit = 10; $limit = 10;
} }
$_SESSION['search_limit'] = $limit;
// Serverseitig sortieren + filtern $currentSort = $_SESSION['search_sort'] ?? 'alphabet';
$filteredResults = sortSearchResults($rawResults, $currentSort);
$filteredResults = filterSearchResultsByCategory($filteredResults, $currentCategory); // ---------------------------------------------------------------
// Diese Berechnung ist der Server-seitige Fallback für Nutzer ohne
// JavaScript. Sobald JS aktiv ist, übernimmt js/search-results.js
// Filterung, Sortierung und Pagination komplett im Browser (auf
// Basis des unten eingebetteten JSON-Datensatzes) und überschreibt
// diese Ausgabe live, ohne dass eine neue Seite geladen wird.
// ---------------------------------------------------------------
$filteredResults = $all_results;
if ($category !== 'all') {
$filteredResults = array_values(array_filter($filteredResults, function ($item) use ($category) {
return ($item['category'] ?? '') === $category;
}));
}
$totalResultsCount = count($filteredResults); $totalResultsCount = count($filteredResults);
$totalPages = max(1, (int)ceil($totalResultsCount / $limit)); $totalPages = max(1, (int)ceil($totalResultsCount / $limit));
@@ -40,56 +53,65 @@ if ($currentPage < 1) {
$currentPage = $totalPages; $currentPage = $totalPages;
} }
$offset = ($currentPage - 1) * $limit; $offset = ($currentPage - 1) * $limit;
// Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert.
// Bei aktiviertem JS lädt js/results-app.js zusätzlich den kompletten
// Datensatz nach (ein einziger Request) und übernimmt Sortierung, Filter
// und Pagination danach komplett im Browser, ohne weitere Serveranfragen.
$results = array_slice($filteredResults, $offset, $limit); $results = array_slice($filteredResults, $offset, $limit);
$resultCount = count($results); $resultCount = count($results);
// Kategorien für das Auswahlfeld (Wert => Label) // Hilfsfunktion, um Pagination-Links zu bauen, die alle aktuellen
$categories = [ // Parameter (q, sort, limit, category) beibehalten.
'Deutsch' => 'Deutsch', 'Englisch' => 'Englisch', 'Franzoesisch' => 'Französisch', function buildPageUrl(int $page, string $query, string $sort, int $limit, string $category): string
'Latein' => 'Latein', 'Literatur' => 'Literatur', 'Mathematik' => 'Mathematik', {
'Biologie' => 'Biologie', 'Informatik' => 'Informatik', 'Chemie' => 'Chemie', $params = [
'Physik' => 'Physik', 'Astronomie' => 'Astronomie', 'Geschichte' => 'Geschichte', 'pfad' => 'search-results',
'Erdkunde' => 'Erdkunde', 'Sozialkunde' => 'Sozialkunde', 'Wirtschaftskunde' => 'Wirtschaftskunde', 'q' => $query,
'Religion' => 'Religion', 'Ethikunterricht' => 'Ethikunterricht', 'Philosophie' => 'Philosophie', 'sort' => $sort,
'Psychologie' => 'Psychologie', 'Kunst' => 'Kunst', 'Musik' => 'Musik', 'Theater' => 'Theater', 'limit' => $limit,
'Technik' => 'Technik', 'Werken' => 'Werken', 'Hauswirtschaft' => 'Hauswirtschaft', 'Sport' => 'Sport', 'category' => $category,
]; 'page' => $page,
];
return 'index.php?' . http_build_query($params);
}
// ---------------------------------------------------------------
// JSON-Datensatz für die clientseitige Aufbereitung (Filter,
// Sortierung, Pagination) durch js/search-results.js. Enthält ALLE
// Treffer der Suche (unabhängig von Kategorie/Sortierung/Seite),
// damit im Browser ohne weitere Serveranfragen gearbeitet werden kann.
// ---------------------------------------------------------------
$jsonItems = array_map(function ($item) {
$likes = isset($item['likes']) && is_array($item['likes']) ? count($item['likes']) : ($item['likes'] ?? 0);
return [
'id' => $item['id'] ?? null,
'title' => $item['title'] ?? '',
'author' => $item['author'] ?? '',
'category' => $item['category'] ?? '',
'likes' => $likes,
'creationDate'=> $item['creationDate'] ?? '',
];
}, $all_results);
?> ?>
<noscript>
Bitte JavaScript aktivieren, um die volle Funktionalität (Live-Filterung ohne Neuladen) zu nutzen. Die Seite funktioniert auch ohne JavaScript Sortierung, Filterung und Seitenwechsel erfolgen dann über die Formular- bzw. Link-Auswahl.
</noscript>
<!-- <!--
Seite: Suchergebnisse Seite: Suchergebnisse
Inhalt: Zeigt die Ergebnisse einer Suche an. Inhalt: Zeigt die Ergebnisse einer Suche an
Funktioniert ohne JavaScript (serverseitige Sortierung/Filter/Pagination
über echte Links + Formulare) und wird bei aktiviertem JS von
js/search-results.js vollständig client-seitig übernommen.
--> -->
<div class="s-res-layout-grid"> <div class="s-res-layout-grid">
<?php include_once "includes/alertMessages.php" ?> <?php include_once "includes/alertMessages.php" ?>
<!-- Links: Seitenleiste für Filter und Suche --> <!-- Links: Seitenleiste für Filter und Suche -->
<aside class="s-res-sidebar"> <aside class="s-res-sidebar">
<!-- Sortierfuntion Box und Such Box -->
<form action="php/controller/search-results-controller.php" method="GET" id="searchform-id" class="s-res-sidebar-form">
<input type="hidden" id="s-res-page-input" name="page" value="<?php echo (int)$currentPage; ?>">
<!-- Formular 1: startet eine NEUE Suche (löst immer einen Server-Request aus) -->
<form action="php/controller/search-results-controller.php" method="GET" id="search-form-id" class="s-res-sidebar-form">
<div class="s-res-sidebar-box"> <div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Suche anpassen</h3> <h3 class="s-res-sidebar-title">Suche anpassen</h3>
<input type="search" id="site-search" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50" required> <input type="search" id="site-search" name="q" placeholder="Suchen..."
class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50" required>
<button type="submit" class="nav__search-button">Suchen</button> <button type="submit" class="nav__search-button">Suchen</button>
</div> </div>
</form>
<!-- Formular 2: sortiert/filtert die BEREITS gefundenen Ergebnisse.
Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Suche.
Mit JS: search-results.js übernimmt das live im Browser, der Submit-Button
wird dafür nicht benötigt und über <noscript> versteckt. -->
<form action="index.php" method="GET" id="s-res-filter-form" class="s-res-sidebar-form">
<input type="hidden" name="pfad" value="search-results">
<input type="hidden" name="q" value="<?php echo htmlspecialchars($query); ?>">
<div class="s-res-sidebar-box"> <div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Sortierung</h3> <h3 class="s-res-sidebar-title">Sortierung</h3>
@@ -116,33 +138,36 @@ $categories = [
<div class="s-res-sidebar-box"> <div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Kategorie filtern</h3> <h3 class="s-res-sidebar-title">Kategorie filtern</h3>
<select id="category-filter" name="category" class="s-res-limit-select" style="width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #cbd5e1;"> <select id="category-filter" name="category" class="s-res-limit-select" style="width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #cbd5e1;">
<option value="all" <?php echo $currentCategory === 'all' ? 'selected' : ''; ?>>Alle Kategorien</option> <?php foreach ($allowedCategories as $catValue):
<?php foreach ($categories as $value => $label): ?> $label = $catValue === 'all' ? 'Alle Kategorien' : ($catValue === 'Franzoesisch' ? 'Französisch' : $catValue);
<option value="<?php echo htmlspecialchars($value); ?>" <?php echo $currentCategory === strtolower($value) ? 'selected' : ''; ?>><?php echo htmlspecialchars($label); ?></option> ?>
<option value="<?php echo htmlspecialchars($catValue); ?>" <?php echo $category === $catValue ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($label); ?>
</option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<!-- Für Nutzer ohne JavaScript: expliziter Button, um Sortierung/Kategorie anzuwenden -->
<noscript> <noscript>
<button type="submit" class="nav__search-button">Filter anwenden</button> <button type="submit" class="nav__search-button">Filter anwenden</button>
</noscript> </noscript>
</form> </form>
</aside> </aside>
<main class="s-res-main-content"> <main class="s-res-main-content">
<div class="s-res-header"> <div class="s-res-header">
<h1 class="s-res-main-title">Suchergebnisse</h1> <h1 class="s-res-main-title">Suchergebnisse</h1>
<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> <p class="s-res-meta" data-query="<?php echo htmlspecialchars($query); ?>">
<?php echo $totalResultsCount; ?> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"
</p>
</div> </div>
<!-- Ergebnisliste --> <!-- Ergebnisliste: serverseitig gerendert (No-JS-Fallback). Wird bei aktivem JS durch js/search-results.js live ersetzt. -->
<div class="s-res-list"> <div class="s-res-list">
<?php if (!empty($results)): ?> <?php if (!empty($results)): ?>
<?php foreach ($results as $item): <?php foreach ($results as $item):
$likesCount = getLikeCount($item); $likesCount = isset($item['likes']) && is_array($item['likes']) ? count($item['likes']) : ($item['likes'] ?? 0);
?> ?>
<div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>"> <div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
<div class="s-res-content"> <div class="s-res-content">
@@ -153,60 +178,55 @@ $categories = [
</h2> </h2>
<div class="s-res-meta-row"> <div class="s-res-meta-row">
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p> <p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p>
<span class="s-res-likes"> ❤️ <?php echo $likesCount; ?></span>
<span class="s-res-likes">
❤️ <?php echo $likesCount; ?>
</span>
</div> </div>
</div> </div>
<div class="s-res-arrow">&rarr;</div> <div class="s-res-arrow">&rarr;</div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php elseif (isset($_SESSION["search_query"]) && $_SESSION["search_query"] !== "" && $resultCount === 0): ?>
<?php elseif (isset($_SESSION["message"]) && $_SESSION["message"] === "invalid_search_query"): ?>
<p>Unzulässige Suchanfrage</p>
<?php elseif ($query !== "" && $totalResultsCount === 0 && count($rawResults) > 0): ?>
<p>Keine Beiträge in dieser Kategorie gefunden.</p>
<?php elseif ($query !== "" && $totalResultsCount === 0): ?>
<p>Keine Beiträge zu diesem Suchbegriff gefunden.</p> <p>Keine Beiträge zu diesem Suchbegriff gefunden.</p>
<?php elseif (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_search_query"): ?>
<p>Unzulässige Suchanfrage</p>
<?php endif; ?> <?php endif; ?>
<?php unset($_SESSION["message"]); ?> <?php unset($_SESSION["message"]); ?>
</div> </div>
<div class="s-res-pagination-footer"> <div class="s-res-pagination-footer">
<!-- Auswahl der Ergebnisse pro Seite -->
<!-- Auswahl der Ergebnisse pro Seite. Gehört per form="..." logisch
zum Filterformular oben -->
<div class="s-res-limit-selector"> <div class="s-res-limit-selector">
<label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label> <label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label>
<select id="s-res-per-page" name="limit" form="s-res-filter-form" class="s-res-limit-select"> <select id="s-res-per-page" name="limit" form="searchform-id" class="s-res-limit-select">
<option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option> <option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option>
<option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option> <option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option>
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option> <option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option>
<option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100</option> <option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100</option>
</select> </select>
<noscript><button type="submit" form="s-res-filter-form" class="nav__search-button">Übernehmen</button></noscript>
</div> </div>
<nav class="s-res-page-navigation" aria-label="Seitennavigation"> <!-- No-JS-Fallback: echte Links, die serverseitig neu paginieren -->
<!-- No-JS-Fallback: --> <div class="s-res-page-navigation">
<noscript> <?php if ($currentPage > 1): ?>
<?php echo renderNoJsPagination($currentPage, $totalPages, $query, $currentSort, $currentCategory, $limit); ?> <a class="s-res-page-btn" href="<?php echo htmlspecialchars(buildPageUrl($currentPage - 1, $query, $currentSort, $limit, $category)); ?>">&laquo;</a>
</noscript> <?php else: ?>
<span class="s-res-page-btn" aria-disabled="true">&laquo;</span>
<?php endif; ?>
<!-- JS-Version: wird per search-results.js befüllt/eingeblendet: --> <span id="dynamic-page-numbers">Seite <?php echo $currentPage; ?> von <?php echo $totalPages; ?></span>
<div id="js-page-navigation" style="display:none;">
<button type="button" class="s-res-page-btn" id="prev-page-btn">«</button>
<span id="dynamic-page-numbers"></span>
<button type="button" class="s-res-page-btn" id="next-page-btn">»</button>
</div>
</nav>
<?php if ($currentPage < $totalPages): ?>
<a class="s-res-page-btn" href="<?php echo htmlspecialchars(buildPageUrl($currentPage + 1, $query, $currentSort, $limit, $category)); ?>">&raquo;</a>
<?php else: ?>
<span class="s-res-page-btn" aria-disabled="true">&raquo;</span>
<?php endif; ?>
</div>
</div> </div>
</main> </main>
</div>
</div> <!-- Vollständiger, ungefilterter Ergebnis-Datensatz für die clientseitige
Filterung/Sortierung/Pagination (siehe js/search-results.js).
JSON_HEX_* verhindert das Ausbrechen aus dem <script>-Tag bzw. XSS. -->
<script type="application/json" id="s-res-data">
<?php echo json_encode($jsonItems, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>
</script>
+42 -379
View File
@@ -5,58 +5,6 @@ $repliesByParent = [];
$articleObj = null; $articleObj = null;
include_once 'php/controller/showArticle-controller.php'; include_once 'php/controller/showArticle-controller.php';
require_once 'php/model/UserManager.php';
$userManager = UserManager::getInstance();
/**
* Liefert den vollständigen Namen zu einer gespeicherten E-Mail-Adresse.
* Falls kein Benutzer gefunden wird, wird die E-Mail als Ersatz angezeigt.
*/
function getCommentAuthorName($email, $userManager)
{
$email = trim($email);
$user = $userManager->findUser($email);
if ($user === null) {
return $email;
}
$vorname = trim($user["vorname"] ?? "");
$nachname = trim($user["nachname"] ?? "");
$fullName = trim($vorname . " " . $nachname);
return $fullName !== "" ? $fullName : $email;
}
/*
* Ermittelt, ob ohne JavaScript auf einen Kommentar
* geantwortet werden soll.
*/
$replyTo = filter_input(
INPUT_GET,
"reply_to",
FILTER_VALIDATE_INT
);
$replyAuthor = null;
if ($replyTo !== false && $replyTo !== null) {
foreach ($mainComments as $mainComment) {
if ($mainComment->getId() === $replyTo) {
$replyAuthor = $mainComment->getAuthor();
break;
}
}
}
/*
* Eine Antwort darf nur auf einen existierenden
* Hauptkommentar geschrieben werden.
*/
if ($replyAuthor === null) {
$replyTo = null;
}
?> ?>
<!-- <!--
Seite: Anzeige für Beiträge Seite: Anzeige für Beiträge
@@ -69,31 +17,28 @@ if ($replyAuthor === null) {
<!-- Metadaten & Titel --> <!-- Metadaten & Titel -->
<div class="article-view-top-section"> <div class="article-view-top-section">
<div class="category-and-likes-row"> <div class="article-view-top-section">
<?php if (isset($category) && !empty($category)): ?>
<span class="article-view-category"><?php echo htmlspecialchars($category); ?></span>
<?php endif; ?>
<!-- Like-Anzeige und dynamischer Like-Button --> <div class="category-and-likes-row">
<?php if (isset($articleObj) && $articleObj !== null): ?> <?php if (isset($category) && !empty($category)): ?>
<div class="article-view-likes"> <span class="article-view-category"><?php echo htmlspecialchars($category); ?></span>
<span>❤️ <span class="like-count"><?php echo $articleObj->getLikeCount(); ?></span></span> <?php endif; ?>
<?php if (isset($_SESSION["user_email"])): ?> <!-- Like-Anzeige und dynamischer Like-Button -->
<form method="post" <?php if (isset($articleObj) && $articleObj !== null): ?>
action="php/controller/like-controller.php?id=<?php echo $articleObj->getId(); ?>" <div class="article-view-likes">
class="like-toggle-form"> <span>❤️ <span class="like-count"><?php echo $articleObj->getLikeCount(); ?></span></span>
<?php csrf_field(); ?>
<button type="submit" class="like-toggle-btn"> <?php if (isset($_SESSION["user_email"])): ?>
<a href="php/controller/like-controller.php?id=<?php echo $articleObj->getId(); ?>" class="like-toggle-btn">
<?php echo $articleObj->hasLiked($_SESSION["user_email"]) ? '👎 Gefällt mir nicht mehr' : '👍 Gefällt mir'; ?> <?php echo $articleObj->hasLiked($_SESSION["user_email"]) ? '👎 Gefällt mir nicht mehr' : '👍 Gefällt mir'; ?>
</button> </a>
</form> <?php else: ?>
<?php else: ?> <span class="login-hint">(Anmelden zum Liken)</span>
<span class="login-hint">(Anmelden zum Liken)</span> <?php endif; ?>
<?php endif; ?> </div>
</div> <?php endif; ?>
<?php endif; ?> </div>
</div>
<h1 class="article-view-title"> <h1 class="article-view-title">
<?php if (isset($title)) { echo htmlspecialchars($title); } ?> <?php if (isset($title)) { echo htmlspecialchars($title); } ?>
@@ -161,290 +106,39 @@ if ($replyAuthor === null) {
</div> </div>
<?php endif; ?> <?php endif; ?>
<section class="article-comments-section" id="comments"> <section class="article-comments-section">
<h2>Kommentare</h2> <h2>Kommentare</h2>
<?php if (isset($_SESSION["comment_message"])): ?>
<div class="alert-message <?php
echo ($_SESSION["comment_message_type"] ?? "") === "success"
? "is-success"
: "is-error";
?>">
<?php echo htmlspecialchars($_SESSION["comment_message"]); ?>
</div>
<?php
unset($_SESSION["comment_message"]);
unset($_SESSION["comment_message_type"]);
?>
<?php endif; ?>
<div id="comments-list"> <div id="comments-list">
<?php if (!empty($mainComments)): ?> <?php if (!empty($mainComments)): ?>
<?php foreach ($mainComments as $comment): ?> <?php foreach ($mainComments as $comment): ?>
<div class="comment-item" <div class="comment-item" data-comment-id="<?php echo htmlspecialchars($comment->getId()); ?>">
data-comment-id="<?php echo htmlspecialchars( <p>
(string) $comment->getId() <strong><?php echo htmlspecialchars($comment->getAuthor()); ?></strong>
); ?>"> <span><?php echo htmlspecialchars($comment->getCreated()); ?></span>
</p>
<?php <p><?php echo nl2br(htmlspecialchars($comment->getContent())); ?></p>
$isDeleted = $comment->getContent()
=== "Dieser Kommentar wurde gelöscht.";
?>
<?php if ($isDeleted): ?>
<p class="deleted-comment">
Dieser Kommentar wurde gelöscht.
</p>
<?php else: ?>
<p>
<strong>
<?php
echo htmlspecialchars(
getCommentAuthorName(
$comment->getAuthor(),
$userManager
)
);
?>
</strong>
<span>
<?php echo htmlspecialchars($comment->getCreated()); ?>
</span>
</p>
<p>
<?php
echo nl2br(
htmlspecialchars($comment->getContent())
);
?>
</p>
<?php if (
isset($_SESSION["user_email"])
&& $_SESSION["user_email"] === $comment->getAuthor()
): ?>
<details class="edit-comment-details">
<summary class="edit-comment-button">
Kommentar bearbeiten
</summary>
<form method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form">
<?php csrf_field(); ?>
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $comment->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $comment->getArticleId()
); ?>">
<label for="edit-comment-<?php
echo htmlspecialchars((string) $comment->getId());
?>">
Kommentar bearbeiten
</label>
<textarea
id="edit-comment-<?php
echo htmlspecialchars((string) $comment->getId());
?>"
name="content"
required><?php echo htmlspecialchars(
$comment->getContent()
); ?></textarea>
<button type="submit" class="button">
Änderungen speichern
</button>
</form>
</details>
<form method="post"
action="index.php?pfad=deleteComment"
class="delete-comment-form">
<?php csrf_field(); ?>
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $comment->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $comment->getArticleId()
); ?>">
<button type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');">
Kommentar löschen
</button>
</form>
<?php endif; ?>
<?php if (isset($_SESSION["user_email"])): ?>
<a href="index.php?pfad=<?php
echo urlencode($_GET["pfad"] ?? "showArticle");
?>&id=<?php
echo urlencode((string) $comment->getArticleId());
?>&reply_to=<?php
echo urlencode((string) $comment->getId());
?>#comment-form"
class="reply-button"
data-comment-id="<?php echo htmlspecialchars(
(string) $comment->getId()
); ?>"
data-author="<?php echo htmlspecialchars(
$comment->getAuthor()
); ?>">
Antworten
</a>
<?php endif; ?>
<?php if (isset($_SESSION["user_email"])): ?>
<button type="button"
class="reply-button"
data-comment-id="<?php echo htmlspecialchars($comment->getId()); ?>"
data-author="<?php echo htmlspecialchars($comment->getAuthor()); ?>">
Antworten
</button>
<?php endif; ?> <?php endif; ?>
<div class="comment-replies"> <div class="comment-replies">
<?php if (isset($repliesByParent[$comment->getId()])): ?> <?php if (isset($repliesByParent[$comment->getId()])): ?>
<?php foreach ($repliesByParent[$comment->getId()] as $reply): ?> <?php foreach ($repliesByParent[$comment->getId()] as $reply): ?>
<div class="comment-item comment-reply"> <div class="comment-item comment-reply">
<p>
<strong><?php echo htmlspecialchars($reply->getAuthor()); ?></strong>
<span><?php echo htmlspecialchars($reply->getCreated()); ?></span>
</p>
<?php <p><?php echo nl2br(htmlspecialchars($reply->getContent())); ?></p>
$isReplyDeleted = $reply->getContent()
=== "Dieser Kommentar wurde gelöscht.";
?>
<?php if ($isReplyDeleted): ?>
<p class="deleted-comment">
Dieser Kommentar wurde gelöscht.
</p>
<?php else: ?>
<p>
<strong>
<?php
echo htmlspecialchars(
getCommentAuthorName(
$reply->getAuthor(),
$userManager
)
);
?>
</strong>
<span>
<?php echo htmlspecialchars($reply->getCreated()); ?>
</span>
</p>
<p>
<?php
echo nl2br(
htmlspecialchars($reply->getContent())
);
?>
</p>
<?php if (
isset($_SESSION["user_email"])
&& $_SESSION["user_email"] === $reply->getAuthor()
): ?>
<details class="edit-comment-details">
<summary class="edit-comment-button">
Antwort bearbeiten
</summary>
<form method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form">
<?php csrf_field(); ?>
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $reply->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $reply->getArticleId()
); ?>">
<label for="edit-reply-<?php
echo htmlspecialchars((string) $reply->getId());
?>">
Antwort bearbeiten
</label>
<textarea
id="edit-reply-<?php
echo htmlspecialchars((string) $reply->getId());
?>"
name="content"
required><?php echo htmlspecialchars(
$reply->getContent()
); ?></textarea>
<button type="submit" class="button">
Änderungen speichern
</button>
</form>
</details>
<form method="post"
action="index.php?pfad=deleteComment"
class="delete-comment-form">
<?php csrf_field(); ?>
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $reply->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $reply->getArticleId()
); ?>">
<button type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');">
Kommentar löschen
</button>
</form>
<?php endif; ?>
<?php endif; ?>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?> <?php endif; ?>
@@ -459,48 +153,17 @@ if ($replyAuthor === null) {
</div> </div>
<?php if (isset($_SESSION["user_email"])): ?> <?php if (isset($_SESSION["user_email"])): ?>
<form id="comment-form" <form id="comment-form">
method="post"
action="php/ajax/add-comment.php">
<?php csrf_field(); ?>
<input type="hidden" <input type="hidden"
name="article_id" name="article_id"
value="<?php echo htmlspecialchars( value="<?php echo htmlspecialchars($_GET["id"] ?? ""); ?>">
(string) ($_GET["id"] ?? "")
); ?>">
<input type="hidden" <input type="hidden"
name="parent_comment_id" name="parent_comment_id"
id="parent-comment-id" id="parent-comment-id"
value="<?php echo $replyTo !== null value="">
? htmlspecialchars((string) $replyTo)
: "";
?>">
<p id="reply-info" <p id="reply-info" class="reply-info" style="display: none;"></p>
class="reply-info"
<?php if ($replyAuthor === null): ?>
style="display: none;"
<?php endif; ?>>
<?php if ($replyAuthor !== null): ?>
Antwort auf <?php echo htmlspecialchars($replyAuthor); ?>
<a href="index.php?pfad=<?php
echo urlencode($_GET["pfad"] ?? "showArticle");
?>&id=<?php
echo urlencode((string) ($_GET["id"] ?? ""));
?>#comment-form">
Abbrechen
</a>
<?php endif; ?>
</p>
<label for="comment-content">
Kommentar
</label>
<textarea name="content" <textarea name="content"
id="comment-content" id="comment-content"
+87 -160
View File
@@ -1,184 +1,111 @@
<?php <?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
include_once "php/controller/showCategory-controller.php"; include_once "php/controller/showCategory-controller.php";
require_once __DIR__ . '/../includes/resultsHelper.php';
$rawResults = $_SESSION['category_results'] ?? []; $currentSort = isset($sortStyle) ? $sortStyle : 'alphabet';
$category = $_SESSION['category_name'] ?? ($_GET['category'] ?? ''); $currentPage = isset($page) ? $page : 1;
// Sortierung / Suche / Limit: GET-Parameter haben Vorrang, sonst Rückfall auf
// die zuletzt in der Session gemerkten Werte (analog zu search-results.php).
$currentSort = $_GET['sort'] ?? ($_SESSION['cat_sort'] ?? 'alphabet');
if (!in_array($currentSort, ['alphabet', 'likes', 'newest', 'oldest'])) {
$currentSort = 'alphabet';
}
$_SESSION['cat_sort'] = $currentSort;
$query = trim($_GET['q'] ?? ($_SESSION['cat_query'] ?? ''));
$_SESSION['cat_query'] = $query;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['cat_limit'] ?? 10);
if (!in_array($limit, [10, 20, 50, 100])) {
$limit = 10;
}
$_SESSION['cat_limit'] = $limit;
// Serverseitig sortieren + nach Suchbegriff filtern (Kategorie steht bereits fest)
$filteredResults = sortSearchResults($rawResults, $currentSort);
$filteredResults = filterResultsByQuery($filteredResults, $query);
$totalResultsCount = count($filteredResults);
$totalPages = max(1, (int)ceil($totalResultsCount / $limit));
$currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($currentPage < 1) {
$currentPage = 1;
} elseif ($currentPage > $totalPages) {
$currentPage = $totalPages;
}
$offset = ($currentPage - 1) * $limit;
// Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert.
// Bei aktiviertem JS lädt js/showCategory.js zusätzlich den kompletten
// Kategorie-Datensatz nach (ein einziger Request) und übernimmt Sortierung,
// Suche und Pagination danach komplett im Browser, ohne weitere Serveranfragen.
$results = array_slice($filteredResults, $offset, $limit);
$resultCount = count($results);
?> ?>
<!--
Seite: Kategorie-Ansicht
Inhalt: Zeigt alle Beiträge einer Kategorie an, sortierbar und innerhalb
der Kategorie durchsuchbar.
Funktioniert ohne JavaScript (serverseitige Sortierung/Suche/Pagination
über echte Links + Formulare) und wird bei aktiviertem JS von
js/showCategory.js vollständig client-seitig übernommen.
-->
<div class="s-res-layout-grid">
<?php include_once "includes/alertMessages.php" ?>
<!-- Links: Seitenleiste für Suche (innerhalb der Kategorie) und Sortierung --> <noscript>
<aside class="s-res-sidebar"> Bitte JavaScript aktivieren!
</noscript>
<!-- Sortiert/durchsucht die BEREITS geladenen Kategorie-Beiträge. <!-- Das Formular umschließt jetzt das gesamte Layout -->
Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Abfrage. <form id="search-form-id" action="index.php" method="GET" style="display: contents;">
Mit JS: js/showCategory.js übernimmt das live im Browser, der Submit-Button <input type="hidden" name="pfad" value="showCategory">
wird dafür nicht benötigt und über <noscript> versteckt. --> <input type="hidden" name="category" value="<?php echo (isset($category) && !empty($category)) ? htmlspecialchars($category) : ''; ?>">
<form action="index.php" method="GET" id="cat-filter-form" class="s-res-sidebar-form"> <input type="hidden" id="s-res-page-input" name="page" value="<?php echo $currentPage; ?>">
<input type="hidden" name="pfad" value="showCategory">
<input type="hidden" name="category" value="<?php echo htmlspecialchars($category); ?>">
<div class="s-res-sidebar-box"> <div class="cat-view-container">
<h3 class="s-res-sidebar-title">In dieser Kategorie suchen</h3>
<input type="search" id="cat-search-input" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50">
</div>
<div class="s-res-sidebar-box"> <!-- LINKE SPALTE: SEITENLEISTE FÜR SORTIERUNG -->
<h3 class="s-res-sidebar-title">Sortierung</h3> <aside class="cat-sidebar">
<div class="s-res-filter-group"> <div class="cat-sidebar-box">
<label class="s-res-filter-option"> <h3 class="cat-sidebar-title">Sortierung</h3>
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>> <div class="cat-filter-group">
<span>Alphabetisch</span> <label class="cat-filter-option">
<input type="radio" name="sort" value="alphabet" <?php echo ($currentSort === 'alphabet') ? 'checked' : ''; ?> onchange="this.form.submit();">
Alphabetisch
</label> </label>
<label class="s-res-filter-option"> <label class="cat-filter-option">
<input type="radio" name="sort" value="likes" class="sort-radio" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?>> <input type="radio" name="sort" value="likes" <?php echo ($currentSort === 'likes') ? 'checked' : ''; ?> onchange="this.form.submit();">
<span>Beliebtheit (Likes)</span> Beliebtheit (Likes)
</label> </label>
<label class="s-res-filter-option"> <label class="cat-filter-option">
<input type="radio" name="sort" value="newest" class="sort-radio" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?>> <input type="radio" name="sort" value="newest" <?php echo ($currentSort === 'newest') ? 'checked' : ''; ?> onchange="this.form.submit();">
<span>Neueste Beiträge</span> Neueste Beiträge
</label> </label>
<label class="s-res-filter-option"> <label class="cat-filter-option">
<input type="radio" name="sort" value="oldest" class="sort-radio" <?php echo $currentSort === 'oldest' ? 'checked' : ''; ?>> <input type="radio" name="sort" value="oldest" <?php echo ($currentSort === 'oldest') ? 'checked' : ''; ?> onchange="this.form.submit();">
<span>Älteste Beiträge</span> Älteste Beiträge
</label> </label>
</div> </div>
</div> </div>
</aside>
<noscript> <!-- RECHTE SPALTE: INHALT & PAGINATOR -->
<button type="submit" class="nav__search-button">Anwenden</button> <main class="cat-view-main">
</noscript> <div class="cat-view-header">
</form> <h1 class="cat-view-title">Kategorie: <?php echo (isset($category) && !empty($category)) ? htmlspecialchars($category) : 'Unbekannt'; ?></h1>
<p class="cat-view-meta-text">
<?php echo (isset($totalArticles)) ? $totalArticles : 0; ?> Treffer in dieser Kategorie
</p>
</div>
</aside> <!-- Beiträge -->
<div class="cat-view-list">
<?php if (isset($articles) && !empty($articles)): ?>
<?php foreach ($articles as $article): ?>
<div class="cat-view-item">
<div class="cat-view-content">
<h2 class="cat-view-item-title">
<a href="index.php?pfad=showArticle&id=<?php echo htmlspecialchars($article->getID()); ?>" class="cat-view-link">
<?php echo (isset($article) && !empty($article->getTitle())) ? htmlspecialchars($article->getTitle()) : 'Kein Titel'; ?>
</a>
</h2>
<main class="s-res-main-content"> <div class="cat-view-meta-row">
<p class="cat-view-author">
<div class="s-res-header"> von <span class="cat-view-author-name"><?php echo (isset($article) && !empty($article->getAuthor())) ? htmlspecialchars($article->getAuthor()) : 'Anonym'; ?></span>
<h1 class="s-res-main-title">Kategorie: <?php echo htmlspecialchars($category); ?></h1> </p>
<p class="s-res-meta"> <span class="cat-view-likes">
<span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer in dieser Kategorie<?php echo $query !== '' ? ' für "' . htmlspecialchars($query) . '"' : ''; ?> ❤️ <?php echo (isset($article) && !empty($article->getLikes())) ? htmlspecialchars($article->getLikes()) : '0'; ?>
</p> </span>
</div> </div>
<!-- Ergebnisliste -->
<div class="s-res-list">
<?php if (!empty($results)): ?>
<?php foreach ($results as $item):
$likesCount = getLikeCount($item);
?>
<div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
<div class="s-res-content">
<h2 class="s-res-item-title">
<a href="index.php?pfad=showArticle&id=<?php echo $item['id']; ?>" class="s-res-link">
<?php echo htmlspecialchars($item['title']); ?>
</a>
</h2>
<div class="s-res-meta-row">
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p>
<span class="s-res-likes">
❤️ <?php echo $likesCount; ?>
</span>
</div> </div>
<div class="cat-view-arrow">&rarr;</div>
</div> </div>
<div class="s-res-arrow">&rarr;</div> <?php endforeach; ?>
</div> <?php else: ?>
<?php endforeach; ?> <p class="cat-view-meta-text">Keine Artikel in dieser Kategorie gefunden.</p>
<?php endif; ?>
<?php elseif ($query !== ''): ?>
<p>Keine Beiträge in dieser Kategorie zu diesem Suchbegriff gefunden.</p>
<?php else: ?>
<p>Keine Beiträge in dieser Kategorie gefunden.</p>
<?php endif; ?>
<?php unset($_SESSION["message"]); ?>
</div>
<div class="s-res-pagination-footer">
<!-- Auswahl der Ergebnisse pro Seite -->
<div class="s-res-limit-selector">
<label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label>
<select id="s-res-per-page" name="limit" form="cat-filter-form" class="s-res-limit-select">
<option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option>
<option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option>
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option>
<option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100</option>
</select>
<noscript><button type="submit" form="cat-filter-form" class="nav__search-button">Übernehmen</button></noscript>
</div> </div>
<nav class="s-res-page-navigation" aria-label="Seitennavigation"> <!-- Footer Paginator -->
<!-- No-JS-Fallback: --> <?php if (isset($totalPages) && $totalPages > 1): ?>
<noscript> <div class="cat-view-pagination-footer">
<?php echo renderNoJsPaginationCategory($currentPage, $totalPages, $category, $currentSort, $query, $limit); ?> <div></div>
</noscript> <div class="s-res-page-navigation">
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage - 1; ?>" <?php echo ($currentPage <= 1) ? 'disabled' : ''; ?>>
&laquo;
</button>
<!-- JS-Version: wird per js/showCategory.js befüllt/eingeblendet: --> <?php for ($i = 1; $i <= $totalPages; $i++): ?>
<div id="js-page-navigation" style="display:none;"> <button type="button"
<button type="button" class="s-res-page-btn" id="prev-page-btn">«</button> class="s-res-page-btn <?php echo ($i === $currentPage) ? 'cat-active-btn' : ''; ?>"
<span id="dynamic-page-numbers"></span> data-page="<?php echo $i; ?>"
<button type="button" class="s-res-page-btn" id="next-page-btn">»</button> <?php echo ($i === $currentPage) ? 'disabled' : ''; ?>>
<?php echo $i; ?>
</button>
<?php endfor; ?>
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage + 1; ?>" <?php echo ($currentPage >= $totalPages) ? 'disabled' : ''; ?>>
&raquo;
</button>
</div>
</div> </div>
</nav> <?php endif; ?>
</main>
</div> </div>
</main> </form>
</div>
+24 -58
View File
@@ -5,30 +5,13 @@ 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 bearbeiten Seite: Beitrag erstellen
Inhalt: Formular für die Bearbeitung eines Beitrags Inhalt: Formular für die Erstellung eines neuen 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">
<?php csrf_field(); ?>
<main class="editor-main"> <main class="editor-main">
<?php include_once "includes/alertMessages.php"?> <?php include_once "includes/alertMessages.php"?>
@@ -44,49 +27,32 @@ $blocks = array_values($blocks);
?>" ?>"
placeholder="Titel hier eingeben" required> placeholder="Titel hier eingeben" required>
<!-- <!-- Hier werden die dynamischen divs via JavaScript eingefügt -->
Content-Blöcke: werden serverseitig als echte, benannte Formularfelder gerendert <div id="block-container"></div>
(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); ?>">
<?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>
<!-- Plus-Button und das Pop-up-Menü -->
<div id="add-block-control" class="article-editor-scope add-block-control"> <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> <button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
<div id="block-popup" class="article-editor-scope block-popup"> <div id="block-popup" class="article-editor-scope block-popup hidden">
<button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button> <button type="button" data-type="text">Textblock</button>
<button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button> <button type="button" data-type="image">Bild einfügen</button>
</div> </div>
</div> </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']);
}elseif (isset($content) && !empty($content)){
echo htmlspecialchars($content);
} 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 -->
@@ -323,4 +289,4 @@ $blocks = array_values($blocks);
</aside> </aside>
</form> </form>
+5 -194
View File
@@ -154,32 +154,21 @@ h1 {
.button { .button {
width: 100%; width: 100%;
padding: 14px; padding: 12px;
background-color: #2563eb; background-color: #2563eb;
color: white; color: white;
border: 2px solid transparent; border: none;
border-radius: 8px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
transition: transition: background-color 0.2s, transform 0.2s, box-shadow 0.2s;
background-color 0.2s ease,
transform 0.2s ease,
box-shadow 0.2s ease,
border-color 0.2s ease;
} }
.button:hover { .button:hover {
background-color: #1e40af;
border-color: #93c5fd;
transform: translateY(-3px) scale(1.01);
box-shadow: 0 8px 18px rgba(37, 99, 235, 0.35);
}
.button:active {
background-color: #1d4ed8; background-color: #1d4ed8;
transform: translateY(1px) scale(0.99); transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(37, 99, 235, 0.25); box-shadow: 0 4px 10px rgba(0,0,0,0.15);
} }
.register-link { .register-link {
@@ -236,182 +225,4 @@ h1 {
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
box-sizing: border-box; box-sizing: border-box;
}
/* Deutlichere Klickreaktion für Kategorien */
.category-link:active {
transform: translateY(1px);
box-shadow: none;
}
/* Sichtbare Tastatur-Markierung */
.button:focus-visible,
.category-link:focus-visible,
.article-link a:focus-visible,
.register-link a:focus-visible {
outline: 3px solid #fbbf24;
outline-offset: 3px;
}
/* Kommentarbereich */
#comments-list {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 40px;
}
#comments-list > div,
.comment {
background-color: #ffffff;
border: 1px solid #dbe3ec;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
transition:
transform 0.2s ease,
box-shadow 0.2s ease,
border-color 0.2s ease;
}
#comments-list > div:hover,
.comment:hover {
transform: translateY(-3px);
border-color: #93c5fd;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
}
#comments-list textarea,
#comment-content {
width: 100%;
box-sizing: border-box;
padding: 14px;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-size: 1rem;
resize: vertical;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
#comments-list textarea:focus,
#comment-content:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.18);
}
#comments-list a {
color: #2563eb;
font-weight: bold;
text-decoration: none;
border-radius: 4px;
transition:
color 0.2s ease,
background-color 0.2s ease;
}
#comments-list a:hover {
color: #1e40af;
background-color: #dbeafe;
text-decoration: underline;
}
#comments-list button {
cursor: pointer;
}
.delete-comment-button {
display: inline-block;
background: #ffffff;
color: #dc2626;
border: 2px solid #dc2626;
border-radius: 8px;
padding: 10px 18px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition:
background-color 0.25s ease,
color 0.25s ease,
transform 0.2s ease,
box-shadow 0.2s ease;
}
.delete-comment-button:hover {
background: #dc2626;
color: #ffffff;
transform: translateY(-2px);
box-shadow: 0 6px 14px rgba(220,38,38,0.25);
}
.delete-comment-button:active {
transform: translateY(1px);
box-shadow: none;
}
.delete-comment-form {
margin-top: 12px;
margin-bottom: 12px;
}
/* Button zum Öffnen der Kommentarbearbeitung */
.edit-comment-button {
display: inline-block;
width: auto;
padding: 10px 18px;
background-color: #2563eb;
color: #ffffff;
border: 2px solid #2563eb;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
list-style: none;
transition:
background-color 0.2s ease,
border-color 0.2s ease,
transform 0.2s ease,
box-shadow 0.2s ease;
}
/* Entfernt das normale Dreieck in einigen Browsern */
.edit-comment-button::-webkit-details-marker {
display: none;
}
/* Eigenes Symbol vor dem Text */
.edit-comment-button::before {
content: "✏ ";
}
.edit-comment-button:hover {
background-color: #1e40af;
border-color: #1e40af;
transform: translateY(-2px);
box-shadow: 0 5px 12px rgba(37, 99, 235, 0.3);
}
.edit-comment-button:active {
transform: translateY(1px);
box-shadow: none;
}
.edit-comment-button:focus-visible {
outline: 3px solid #fbbf24;
outline-offset: 3px;
}
/* Abstand zwischen Bearbeiten und Löschen */
.edit-comment-details {
margin-bottom: 12px;
}
/* Geöffneter Bearbeitungsbereich */
.edit-comment-details[open] .edit-comment-button {
margin-bottom: 12px;
background-color: #1e40af;
}
/* Geändertes Symbol, wenn der Bereich geöffnet ist */
.edit-comment-details[open] .edit-comment-button::before {
content: "▲ ";
} }
+3 -113
View File
@@ -248,9 +248,9 @@ CSS für die navbar
z-index: 1000; z-index: 1000;
transition: left 0.3s ease; transition: left 0.3s ease;
padding: 2rem 1rem; padding: 2rem 1rem;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5); box-shadow: 2px 0 10px rgba(0,0,0,0.5);
overflow-y: auto; overflow-y: auto;
/* Genug Abstand oben rechts, damit Links nicht hinter dem X liegen */ /* Genug Abstand oben rechts, damit Links nicht hinter dem X liegen */
padding: 4rem 1.5rem 2rem 1.5rem; padding: 4rem 1.5rem 2rem 1.5rem;
} }
@@ -285,32 +285,6 @@ CSS für die navbar
border-bottom: 1px solid #333d43; border-bottom: 1px solid #333d43;
} }
.nav__logout-form {
display: contents;
margin: 0;
}
.nav__logout-form .nav__button {
width: 100%;
height: 100%;
}
.nav__mobile-logout-button {
color: #fff;
text-decoration: none;
font-size: 1.2rem;
font-weight: 600;
display: block;
width: 100%;
text-align: left;
padding: 0.5rem 1rem;
border: none;
border-bottom: 1px solid #333d43;
background: none;
cursor: pointer;
font-family: inherit;
}
.nav__mobile-submenu { .nav__mobile-submenu {
display: block; display: block;
list-style: none; list-style: none;
@@ -328,89 +302,5 @@ CSS für die navbar
padding: 0.8rem 1rem; padding: 0.8rem 1rem;
cursor: pointer; cursor: pointer;
} }
}
/* Deutlichere Hover-Effekte für die Navigation */
.nav__dropdown-toggle, }
.nav__link {
border-radius: 6px;
transition:
background-color 0.2s ease,
color 0.2s ease,
transform 0.2s ease;
}
.nav__dropdown-toggle:hover,
.nav__link:hover {
background-color: #ffffff;
color: #1d4ed8;
transform: translateY(-2px);
}
/* Sichtbare Reaktion beim Anklicken */
.nav__dropdown-toggle:active,
.nav__link:active {
transform: translateY(1px);
}
/* Deutlichere Effekte für Anmelden, Registrieren usw. */
.nav__button {
transition:
background-color 0.2s ease,
color 0.2s ease,
transform 0.2s ease,
box-shadow 0.2s ease;
}
.nav__button:hover {
background-color: #2563eb;
color: #ffffff;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
.nav__button:active {
transform: translateY(1px);
box-shadow: none;
}
/* Deutlichere Hervorhebung der Einträge im Dropdown-Menü */
.nav__dropdown-menu a {
display: block;
transition:
background-color 0.2s ease,
color 0.2s ease,
padding-left 0.2s ease;
}
.nav__dropdown-menu a:hover {
background-color: #dbeafe;
color: #1d4ed8;
padding-left: 1.4rem;
}
/* Effekt für den Suchbutton */
.nav__search-button {
transition:
background-color 0.2s ease,
color 0.2s ease,
transform 0.2s ease;
}
.nav__search-button:hover {
background-color: #2563eb;
color: #ffffff;
}
.nav__search-button:active {
transform: scale(0.95);
}
/* Sichtbare Markierung bei Tastaturbedienung */
.nav a:focus-visible,
.nav button:focus-visible,
.nav input:focus-visible,
.nav label:focus-visible {
outline: 3px solid #fbbf24;
outline-offset: 3px;
}
+6 -7
View File
@@ -18,6 +18,11 @@
Ein Beitrag muss Inhalt besitzen. Text- und Bildelemente dürfen nicht leer sein! Ein Beitrag muss Inhalt besitzen. Text- und Bildelemente dürfen nicht leer sein!
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_category"): ?>
<p class="alert-message is-error">
Die ausgewählte Kategorie ist ungültig.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_tags"): ?> <?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_tags"): ?>
<p class="alert-message is-error"> <p class="alert-message is-error">
Ungültige Schlagworte gefunden. Erlaubt sind nur Buchstaben, Zahlen, Leerzeichen und Bindestriche (2-50 Zeichen). Ungültige Schlagworte gefunden. Erlaubt sind nur Buchstaben, Zahlen, Leerzeichen und Bindestriche (2-50 Zeichen).
@@ -89,12 +94,6 @@
Es ist ein Datenbankfehler aufgetreten. Bitte versuche es erneut. Es ist ein Datenbankfehler aufgetreten. Bitte versuche es erneut.
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_csrf_token"): ?>
<p class="alert-message is-error">
Deine Sitzung ist abgelaufen oder die Anfrage konnte nicht überprüft werden.
Bitte lade die Seite neu und versuche es erneut.
</p>
<?php endif; ?>
<?php <?php
unset($_SESSION["message"]); unset($_SESSION["message"]);
?> ?>
-69
View File
@@ -1,69 +0,0 @@
<?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;
}
-88
View File
@@ -1,88 +0,0 @@
<?php
/**
* CSRF-Schutz nach dem Synchronizer-Token-Pattern.
*
* Pro Session wird ein einziges, zufälliges Token erzeugt,
* das in jedem Formular als verstecktes Feld mitgeschickt und bei jeder
* zustandsändernden Anfrage serverseitig mit dem Session-Token verglichen
* wird.
*
* @author Niklas Ortmann
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
/**
* Gibt das aktuelle CSRF-Token der Session zurück.
*
* Existiert noch kein Token, wird eines erzeugt und in der Session
* gespeichert.
*
* @return string Das CSRF-Token
*/
function csrf_token(): string
{
if (empty($_SESSION["csrf_token"]) || !is_string($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
return $_SESSION["csrf_token"];
}
/**
* Gibt ein verstecktes Formularfeld mit dem aktuellen CSRF-Token aus.
*
* Wird in jedem Formular benötigt, das eine zustandsändernde
* Aktion auslöst.
*
* @return void
*/
function csrf_field(): void
{
echo '<input type="hidden" name="csrf_token" value="'
. htmlspecialchars(csrf_token())
. '">';
}
/**
* Prüft, ob das per POST gesendete CSRF-Token zum Session-Token passt.
*
* Der Vergleich erfolgt zeitkonstant über hash_equals(), um
* Timing-Angriffe auf den Vergleich selbst auszuschließen.
*
* @return bool true, wenn das Token gültig ist
*/
function csrf_verify(): bool
{
$sentToken = $_POST["csrf_token"] ?? "";
$sessionToken = $_SESSION["csrf_token"] ?? "";
if (!is_string($sentToken) || $sentToken === "" || $sessionToken === "") {
return false;
}
return hash_equals($sessionToken, $sentToken);
}
/**
* Bricht die Anfrage ab und leitet mit einer Fehlermeldung um,
* wenn das mitgesendete CSRF-Token ungültig oder nicht vorhanden ist.
*
* Muss am Anfang jeder zustandsändernden POST-Aktion aufgerufen werden,
* bevor irgendeine Änderung an Daten vorgenommen wird.
*
* @param string $redirectTo Ziel-URL, zu der bei ungültigem Token
* weitergeleitet wird
* @return void
*/
function csrf_require_valid(string $redirectTo = "index.php"): void
{
if (!csrf_verify()) {
http_response_code(403);
$_SESSION["message"] = "invalid_csrf_token";
header("Location: " . $redirectTo);
exit();
}
}
+1 -1
View File
@@ -179,4 +179,4 @@ Globales Menü, wird via PHP später in alle Seiten eingebunden
<?php endif; ?> <?php endif; ?>
</div> </div>
</nav> </nav>
-171
View File
@@ -1,171 +0,0 @@
<?php
/**
* Hilfsfunktionen rund um die Suchergebnisse.
*
* Werden sowohl von content/search-results.php (No-JS-Rendering)
* als auch von php/controller/search-results-data.php (JSON für
* die JS-Hydration) genutzt, damit Sortier-/Filterlogik nur an
* einer Stelle gepflegt werden muss
*/
function getLikeCount($item): int
{
if (isset($item['likes']) && is_array($item['likes'])) {
return count($item['likes']);
}
return (int)($item['likes'] ?? 0);
}
/**
* Sortiert die Ergebnisse
*/
function sortSearchResults(array $results, string $sortStyle): array
{
switch ($sortStyle) {
case 'likes':
usort($results, function ($a, $b) {
return getLikeCount($b) <=> getLikeCount($a);
});
break;
case 'newest':
usort($results, function ($a, $b) {
return strcmp($b['creationDate'] ?? '', $a['creationDate'] ?? '');
});
break;
case 'oldest':
usort($results, function ($a, $b) {
return strcmp($a['creationDate'] ?? '', $b['creationDate'] ?? '');
});
break;
case 'alphabet':
default:
usort($results, function ($a, $b) {
return strcasecmp($a['title'] ?? '', $b['title'] ?? '');
});
break;
}
return $results;
}
/**
* Filtert die Ergebnisse ('all' = kein Filter).
*/
function filterSearchResultsByCategory(array $results, string $category): array
{
$category = strtolower(trim($category));
if ($category === '' || $category === 'all') {
return $results;
}
return array_values(array_filter($results, function ($item) use ($category) {
return strtolower($item['category'] ?? '') === $category;
}));
}
/**
* Baut eine Such-Ergebnis-URL für einen bestimmten Seitenwechsel (No-JS-Pagination).
*/
function buildSearchResultsUrl(int $page, string $query, string $sort, string $category, int $limit): string
{
return "index.php?pfad=search-results"
. "&q=" . urlencode($query)
. "&sort=" . urlencode($sort)
. "&category=" . urlencode($category)
. "&limit=" . $limit
. "&page=" . $page;
}
/**
* Rendert eine rein serverseitige Pagination für den No-JS-Fall:
*/
function renderNoJsPagination(int $currentPage, int $totalPages, string $query, string $sort, string $category, int $limit): string
{
$html = '';
if ($currentPage > 1) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($currentPage - 1, $query, $sort, $category, $limit)) . '">&laquo;</a> ';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&laquo;</span> ';
}
for ($i = 1; $i <= $totalPages; $i++) {
if ($i === $currentPage) {
$html .= '<span class="s-res-page-btn s-res-page-btn-active">' . $i . '</span> ';
} else {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($i, $query, $sort, $category, $limit)) . '">' . $i . '</a> ';
}
}
if ($currentPage < $totalPages) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($currentPage + 1, $query, $sort, $category, $limit)) . '">&raquo;</a>';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&raquo;</span>';
}
return $html;
}
/**
* Filtert Ergebnisse anhand eines Suchbegriffs
*
* Wird von content/showCategory.php genutzt, um innerhalb einer bereits
* geladenen Kategorie zu suchen.
*/
function filterResultsByQuery(array $results, string $query): array
{
$query = trim($query);
if ($query === '') {
return $results;
}
$needle = mb_strtolower($query);
return array_values(array_filter($results, function ($item) use ($needle) {
$haystack = mb_strtolower(($item['title'] ?? '') . ' ' . ($item['content'] ?? ''));
return mb_strpos($haystack, $needle) !== false;
}));
}
/**
* Baut eine Kategorie-URL für einen bestimmten Seitenwechsel (No-JS-Pagination).
* Analog zu buildSearchResultsUrl, aber für pfad=showCategory (feste Kategorie,
* dafür mit Freitext-Suche innerhalb der Kategorie statt Kategorie-Filter).
*/
function buildCategoryUrl(int $page, string $category, string $sort, string $query, int $limit): string
{
return "index.php?pfad=showCategory"
. "&category=" . urlencode($category)
. "&sort=" . urlencode($sort)
. "&q=" . urlencode($query)
. "&limit=" . $limit
. "&page=" . $page;
}
/**
* Rendert eine rein serverseitige Pagination für showCategory (No-JS-Fall).
* Analog zu renderNoJsPagination.
*/
function renderNoJsPaginationCategory(int $currentPage, int $totalPages, string $category, string $sort, string $query, int $limit): string
{
$html = '';
if ($currentPage > 1) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($currentPage - 1, $category, $sort, $query, $limit)) . '">&laquo;</a> ';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&laquo;</span> ';
}
for ($i = 1; $i <= $totalPages; $i++) {
if ($i === $currentPage) {
$html .= '<span class="s-res-page-btn s-res-page-btn-active">' . $i . '</span> ';
} else {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($i, $category, $sort, $query, $limit)) . '">' . $i . '</a> ';
}
}
if ($currentPage < $totalPages) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($currentPage + 1, $category, $sort, $query, $limit)) . '">&raquo;</a>';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&raquo;</span>';
}
return $html;
}
+91 -41
View File
@@ -2,50 +2,100 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
include_once "includes/csrf.php"; ob_start();
include_once "php/controller/index-controller.php"; include_once("php/controller/index.php");
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css"> $pfad = $_GET["pfad"] ?? "home";
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<link rel="stylesheet" href="css/search-results.css">
<link rel="stylesheet" href="css/createArticle.css">
<link rel="stylesheet" href="css/profile.css">
<link rel="stylesheet" href="css/showArticle.css">
<link rel="stylesheet" href="css/message.css">
<link rel="stylesheet" href="css/showCategory.css">
<script src="js/comments.js" defer></script> /*
<script src="js/editor.js" async></script> Controller für Aktionen werden vor der HTML-Ausgabe geladen,
<script src="js/search-results.js" async></script> damit Weiterleitungen mit header() funktionieren.
<script src="js/showCategory.js" async></script> */
if ($pfad === "login") {
include_once "php/controller/login-controller.php";
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
// Dynamischer Inhalt
if (isset($pfad) && $pfad !== "404" && file_exists('content/' . $pfad . '.php')) {
include_once 'content/' . $pfad . '.php';
} else {
include_once 'content/404.php';
} }
include_once 'includes/footer.php'; if ($pfad === "register") {
include_once "php/controller/register-controller.php";
}
if ($pfad === "password-forgotten") {
include_once "php/controller/password-forgotten-controller.php";
}
if ($pfad === "confirm-register") {
include_once "php/controller/confirm-register-controller.php";
}
if ($pfad === "confirm-password") {
include_once "php/controller/confirm-password-controller.php";
}
if ($pfad === "logout") {
include_once "php/controller/logout-controller.php";
exit();
}
if ($pfad === "deleteAccount") {
include_once "php/controller/deleteAccount-controller.php";
exit();
}
?> ?>
</body> <!--
</html> Seite: Index der Lernplattform
Funktion: Webseitengerüst, Anzeigen von Content
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<link rel="stylesheet" href="css/search-results.css">
<link rel="stylesheet" href="css/createArticle.css">
<link rel="stylesheet" href="css/profile.css">
<link rel="stylesheet" href="css/showArticle.css">
<link rel="stylesheet" href="css/message.css">
<link rel="stylesheet" href="css/showCategory.css">
<!--<script src="js/paginator.js" async></script>
<script src="js/sorter.js" async></script>-->
<script src="js/comments.js" defer></script>
<script src="js/editor.js" async></script>
<!--<script src="js/filter.js" async></script>-->
<script src="js/search-results.js" async></script>
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
/*
Dynamischer Inhalt:
Je nach pfad-Parameter wird die passende Datei aus content geladen.
*/
if (file_exists('content/' . $pfad . '.php')) {
include_once 'content/' . $pfad . '.php';
} else {
include_once 'content/404.php';
}
include_once 'includes/footer.php';
?>
</body>
</html>
<?php
ob_end_flush();
?>
+43 -216
View File
@@ -11,31 +11,20 @@ document.addEventListener("DOMContentLoaded", function () {
const parentCommentInput = document.getElementById("parent-comment-id"); const parentCommentInput = document.getElementById("parent-comment-id");
const replyInfo = document.getElementById("reply-info"); const replyInfo = document.getElementById("reply-info");
if (!form || !commentsList || !commentContent || !parentCommentInput) { if (!form || !commentsList || !commentContent || !parentCommentInput || !replyInfo) {
return; return;
} }
/** /**
* Aktiviert einen einzelnen Antworten-Link. * Aktiviert einen einzelnen Antworten-Button.
* *
* @param {HTMLAnchorElement} replyLink Antworten-Link * @param {HTMLButtonElement} button Antworten-Button
*/ */
function registerReplyButton(replyLink) { function registerReplyButton(button) {
replyLink.addEventListener("click", function (event) { button.addEventListener("click", function () {
/* parentCommentInput.value = button.dataset.commentId;
* Mit JavaScript wird die Seite nicht neu geladen. replyInfo.textContent = "Antwort auf " + button.dataset.author;
* Ohne JavaScript funktioniert der normale Link. replyInfo.style.display = "block";
*/
event.preventDefault();
parentCommentInput.value = replyLink.dataset.commentId;
if (replyInfo) {
replyInfo.textContent =
"Antwort auf " + replyLink.dataset.author;
replyInfo.style.display = "block";
}
commentContent.focus(); commentContent.focus();
}); });
} }
@@ -56,29 +45,18 @@ document.addEventListener("DOMContentLoaded", function () {
const formData = new FormData(form); const formData = new FormData(form);
const parentCommentId = parentCommentInput.value; const parentCommentId = parentCommentInput.value;
fetch(form.action, { fetch("php/ajax/add-comment.php", {
method: "POST", method: "POST",
body: formData, body: formData
headers: {
"X-Requested-With": "XMLHttpRequest"
}
}) })
.then(function (response) { .then(response => response.json())
if (!response.ok) { .then(data => {
throw new Error("Fehlerhafte Serverantwort.");
}
return response.json();
})
.then(function (data) {
if (!data.success) { if (!data.success) {
alert(data.message); alert(data.message);
return; return;
} }
const emptyMessage = commentsList.querySelector( const emptyMessage = commentsList.querySelector(".no-comments-message");
".no-comments-message"
);
if (emptyMessage) { if (emptyMessage) {
emptyMessage.remove(); emptyMessage.remove();
@@ -86,196 +64,49 @@ document.addEventListener("DOMContentLoaded", function () {
const commentElement = document.createElement("div"); const commentElement = document.createElement("div");
commentElement.classList.add("comment-item"); commentElement.classList.add("comment-item");
commentElement.dataset.commentId = data.commentId;
if (parentCommentId !== "") { if (parentCommentId) {
commentElement.classList.add("comment-reply"); commentElement.classList.add("comment-reply");
commentElement.innerHTML = ` commentElement.innerHTML = `
<p> <p>
<strong>${escapeHtml(data.author)}</strong> <strong>${escapeHtml(data.author)}</strong>
<span>${escapeHtml(data.created)}</span> <span>${escapeHtml(data.created)}</span>
</p> </p>
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
`;
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p> const parentReplies = document.querySelector(
<details class="edit-comment-details">
<summary class="edit-comment-button">
Antwort bearbeiten
</summary>
<form
method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form"
>
<input
type="hidden"
name="csrf_token"
value="${escapeHtml(formData.get("csrf_token"))}"
>
<input
type="hidden"
name="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<textarea
name="content"
required
>${escapeHtml(data.content)}</textarea>
<button type="submit" class="button">
Änderungen speichern
</button>
</form>
</details>
<form
method="post"
action="index.php?pfad=deleteComment"
class="delete-comment-form"
>
<input
type="hidden"
name="csrf_token"
value="${escapeHtml(formData.get("csrf_token"))}"
>
<input
type="hidden"
name="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<button
type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');"
>
Kommentar löschen
</button>
</form>
`;
const parentReplies = commentsList.querySelector(
`.comment-item[data-comment-id="${parentCommentId}"] .comment-replies` `.comment-item[data-comment-id="${parentCommentId}"] .comment-replies`
); );
if (parentReplies) { if (parentReplies) {
parentReplies.appendChild(commentElement); parentReplies.appendChild(commentElement);
} else {
commentsList.prepend(commentElement);
} }
} else { } else {
commentElement.dataset.commentId = data.commentId;
commentElement.innerHTML = ` commentElement.innerHTML = `
<p> <p>
<strong>${escapeHtml(data.author)}</strong> <strong>${escapeHtml(data.author)}</strong>
<span>${escapeHtml(data.created)}</span> <span>${escapeHtml(data.created)}</span>
</p> </p>
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p> <button type="button"
class="reply-button"
data-comment-id="${escapeHtml(data.commentId)}"
data-author="${escapeHtml(data.author)}">
Antworten
</button>
<details class="edit-comment-details"> <div class="comment-replies"></div>
<summary class="edit-comment-button"> `;
Kommentar bearbeiten
</summary>
<form
method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form"
>
<input
type="hidden"
name="csrf_token"
value="${escapeHtml(formData.get("csrf_token"))}"
>
<input
type="hidden"
name="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<textarea
name="content"
required
>${escapeHtml(data.content)}</textarea>
<button type="submit" class="button">
Änderungen speichern
</button>
</form>
</details>
<form
method="post"
action="index.php?pfad=deleteComment"
class="delete-comment-form"
>
<input
type="hidden"
name="csrf_token"
value="${escapeHtml(formData.get("csrf_token"))}"
>
<input
type="hidden"
name="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<button
type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');"
>
Kommentar löschen
</button>
</form>
<a
href="#comment-form"
class="reply-button"
data-comment-id="${escapeHtml(data.commentId)}"
data-author="${escapeHtml(data.author)}"
>
Antworten
</a>
<div class="comment-replies"></div>
`;
commentsList.prepend(commentElement); commentsList.prepend(commentElement);
const newReplyButton = const newReplyButton = commentElement.querySelector(".reply-button");
commentElement.querySelector(".reply-button");
if (newReplyButton) { if (newReplyButton) {
registerReplyButton(newReplyButton); registerReplyButton(newReplyButton);
@@ -284,14 +115,10 @@ document.addEventListener("DOMContentLoaded", function () {
commentContent.value = ""; commentContent.value = "";
parentCommentInput.value = ""; parentCommentInput.value = "";
replyInfo.textContent = "";
if (replyInfo) { replyInfo.style.display = "none";
replyInfo.textContent = "";
replyInfo.style.display = "none";
}
}) })
.catch(function (error) { .catch(() => {
console.error(error);
alert("Kommentar konnte nicht gesendet werden."); alert("Kommentar konnte nicht gesendet werden.");
}); });
}); });
@@ -299,12 +126,12 @@ document.addEventListener("DOMContentLoaded", function () {
/** /**
* Entfernt HTML-Sonderzeichen aus Nutzereingaben. * Entfernt HTML-Sonderzeichen aus Nutzereingaben.
* *
* @param {*} text Zu bereinigender Text * @param {string} text Zu bereinigender Text
* @returns {string} Sicherer Text * @returns {string} Sicherer Text
*/ */
function escapeHtml(text) { function escapeHtml(text) {
const div = document.createElement("div"); const div = document.createElement("div");
div.textContent = String(text ?? ""); div.textContent = text;
return div.innerHTML; return div.innerHTML;
} }
}); });
+122 -83
View File
@@ -1,10 +1,10 @@
//console.log("editor.js 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("editor.js abgebrochen: Formular nicht gefunden!"); console.error("editor.js wurde abgebrochen: Formular nicht gefunden!");
return; return;
} else { } else {
console.log("Formular gefunden und editor.js initialisiert:", form); console.log("Formular gefunden und editor.js initialisiert:", form);
@@ -13,121 +13,160 @@ function initEditor() {
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");
// Fortlaufender Zähler für eindeutige Block-Indizes. Wird nie wiederverwendet const initialImages = [];
// (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 "Textblock" / "Bild einfügen": lokal im DOM anlegen statt zum // Klick auf eine Block-Option im Pop-up
// Server zu submitten (entlastet den Server, kein Page-Reload nötig). popup.querySelectorAll("button").forEach(btn => {
popup.querySelectorAll('[name="editor_action"]').forEach(btn => { btn.addEventListener("click", function() {
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");
}); });
}); });
// Bereits vom Server gerenderte Blöcke (z.B. beim Bearbeiten eines bestehenden // Erstellt ein visuelles HTML-Element im Editor
// 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", "article-editor-scope"); blockDiv.classList.add("editor-block");
blockDiv.setAttribute("data-index", String(index)); blockDiv.setAttribute("data-type", type);
const typeInput = document.createElement("input"); // Wenn es ein existierendes Server-Bild beim Laden ist, Pfad im globalen Array sichern
typeInput.type = "hidden"; if (type === "image" && value && typeof value === 'string' && value.startsWith('uploads/')) {
typeInput.name = `blocks[${index}][type]`; initialImages.push(value);
typeInput.value = type; blockDiv.setAttribute("data-value", value);
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/*";
blockDiv.appendChild(fileInput);
}
const deleteBtn = document.createElement("button"); const imgPreview = document.createElement("img");
deleteBtn.type = "submit"; imgPreview.style.maxWidth = "200px";
deleteBtn.name = "editor_action"; imgPreview.style.display = "block";
deleteBtn.value = `delete_block:${index}`; imgPreview.style.marginTop = "10px";
deleteBtn.classList.add("delete-block-btn");
deleteBtn.innerHTML = "✕"; if (value && typeof value === 'string') {
blockDiv.appendChild(deleteBtn); 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);
}
container.appendChild(blockDiv); container.appendChild(blockDiv);
bindDeleteButton(blockDiv);
bindImageInput(blockDiv);
} }
// Löschen-Button eines Blocks lokal abfangen: entfernt den Block nur aus dem DOM, // beim Abschicken verbleibende Blöcke auslesen UND gelöschte Bilder ermitteln
// statt das Formular zum Server zu senden. form.addEventListener("submit", function(e) {
function bindDeleteButton(blockDiv) { const blocks = [];
const btn = blockDiv.querySelector(".delete-block-btn"); const currentImages = [];
if (!btn) return;
btn.addEventListener("click", (e) => {
e.preventDefault();
blockDiv.remove();
});
}
// Zeigt bei Auswahl einer Bilddatei sofort eine Vorschau an. Rein optisch // alle aktuell im Formular verbliebenen Blöcke scannen
// der eigentliche Datei-Upload läuft nativ über das <input type="file">. container.querySelectorAll(".editor-block").forEach(blockDiv => {
function bindImageInput(blockDiv) { const type = blockDiv.getAttribute("data-type");
const fileInput = blockDiv.querySelector('input[type="file"]'); let value = "";
if (!fileInput) return;
let imgPreview = blockDiv.querySelector(".block-image-preview"); if (type === "text") {
if (!imgPreview) { value = blockDiv.querySelector("textarea").value;
imgPreview = document.createElement("img"); } else if (type === "image") {
imgPreview.classList.add("block-image-preview");
imgPreview.style.maxWidth = "200px";
imgPreview.style.display = "none";
imgPreview.style.marginTop = "10px";
blockDiv.insertBefore(imgPreview, fileInput.nextSibling);
}
fileInput.addEventListener("change", function () { const imgTag = blockDiv.querySelector("img");
if (this.files && this.files[0]) { if (imgTag) {
const reader = new FileReader(); const srcValue = imgTag.getAttribute("src") || "";
reader.onload = (e) => { // Wenn es ein neues Bild ist, nutzen wir das data-value (Base64)
imgPreview.src = e.target.result; if (srcValue.startsWith('data:image/')) {
imgPreview.style.display = "block"; value = blockDiv.getAttribute("data-value") || "";
}; } else {
reader.readAsDataURL(this.files[0]); 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
hiddenContentInput.value = JSON.stringify(blocks);
// 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);
}
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);
}
} }
} }
@@ -137,4 +176,4 @@ if (document.readyState === "loading") {
} else { } else {
// Falls das DOM schon fertig geladen ist, führen wir es direkt aus // Falls das DOM schon fertig geladen ist, führen wir es direkt aus
initEditor(); initEditor();
} }
+116
View File
@@ -0,0 +1,116 @@
let currentClientPage = 1;
let itemsPerPage = 10;
function initFilter() {
const filterSelect = document.getElementById('category-filter');
const listContainer = document.querySelector('.s-res-list');
const limitSelect = document.getElementById('s-res-per-page');
if (!filterSelect || !listContainer) return;
if (limitSelect) {
itemsPerPage = parseInt(limitSelect.value, 10) || 10;
}
updateVisibility();
if (limitSelect) {
limitSelect.addEventListener('change', function() {
//neues limit einlesen
itemsPerPage = parseInt(this.value, 10);
currentClientPage = 1;
updateVisibility();
});
}
filterSelect.addEventListener('change', function() {
currentClientPage = 1;
updateVisibility();
});
const navigationContainer = document.querySelector('.s-res-page-navigation');
if (navigationContainer) {
navigationContainer.addEventListener('click', function(e) {
const button = e.target.closest('.s-res-page-btn');
if (!button || button.disabled) return;
e.preventDefault();
const targetPage = button.getAttribute('data-page');
if (targetPage) {
currentClientPage = parseInt(targetPage, 10);
updateVisibility();
}
});
}
}
function updateVisibility() {
const filterSelect = document.getElementById('category-filter');
const listContainer = document.querySelector('.s-res-list');
const selectedCategory = filterSelect.value.toLowerCase().trim();
const cards = listContainer.querySelectorAll('.s-res-item');
let visibleCards = [];
cards.forEach(card => {
const cardCategory = (card.getAttribute('data-category') || '').toLowerCase().trim();
if (selectedCategory === 'all' || cardCategory.includes(selectedCategory) || selectedCategory.includes(cardCategory)) {
visibleCards.push(card);
} else {
card.style.display = 'none';
}
});
const totalVisible = visibleCards.length;
const totalPages = Math.max(1, Math.ceil(totalVisible / itemsPerPage));
if (currentClientPage < 1) currentClientPage = 1;
if (currentClientPage > totalPages) currentClientPage = totalPages;
const startOffset = (currentClientPage - 1) * itemsPerPage;
const endOffset = startOffset + itemsPerPage;
visibleCards.forEach((card, index) => {
if (index >= startOffset && index < endOffset) {
card.style.display = 'flex';
} else {
card.style.display = 'none';
}
});
updatePaginatorUI(currentClientPage, totalPages);
}
function updatePaginatorUI(currentPage, totalPages) {
const prevBtn = document.getElementById('prev-page-btn');
const nextBtn = document.getElementById('next-page-btn');
const numbersContainer = document.getElementById('dynamic-page-numbers');
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.setAttribute('data-page', currentPage - 1);
prevBtn.disabled = (currentPage <= 1);
nextBtn.setAttribute('data-page', currentPage + 1);
nextBtn.disabled = (currentPage >= totalPages);
let buttonsHTML = '';
for (let i = 1; i <= totalPages; i++) {
const activeClass = (i === currentPage) ? 's-res-page-btn-active' : '';
buttonsHTML += `<button type="button" class="s-res-page-btn ${activeClass}" data-page="${i}">${i}</button> `;
}
numbersContainer.innerHTML = buttonsHTML;
}
// Hilfsfunktion für Math.ceil in JS
function ceil(val) { return Math.ceil(val); }
// ist das DOM bereits vollständig aufgebaut?
if (document.readyState === 'loading') {
// Falls noch geladen wird, auf das Event warten
document.addEventListener('DOMContentLoaded', initFilter);
} else {
// Falls das HTML bereits komplett da ist, sofort ausführen
initFilter();
}
+203
View File
@@ -0,0 +1,203 @@
/**
* Übernimmt Filterung (Kategorie), Sortierung und Pagination der
* Suchergebnisse vollständig clientseitig, ohne Neuladen der Seite.
*
* Voraussetzung: Im HTML liegt ein <script type="application/json" id="s-res-data">
* mit allen Treffern der aktuellen Suche (siehe content/search-results.php).
*
* Ist kein JavaScript aktiv, bleibt die serverseitig gerenderte Ausgabe
* (inkl. echter Pagination-Links und Formular-Submit) unverändert nutzbar.
*/
(function () {
const DEFAULT_PER_PAGE = 10;
const state = {
all: [],
filtered: [],
category: 'all',
sort: 'alphabet',
perPage: DEFAULT_PER_PAGE,
page: 1,
};
const els = {};
function init() {
const dataScript = document.getElementById('s-res-data');
const listContainer = document.querySelector('.s-res-list');
if (!dataScript || !listContainer) {
return; // keine Ergebnisliste auf dieser Seite vorhanden
}
try {
state.all = JSON.parse(dataScript.textContent);
} catch (e) {
console.error('Suchergebnis-Daten konnten nicht gelesen werden.', e);
return;
}
els.list = listContainer;
els.categorySelect = document.getElementById('category-filter');
els.perPageSelect = document.getElementById('s-res-per-page');
els.sortRadios = document.querySelectorAll('.sort-radio');
els.pagerContainer = document.querySelector('.s-res-page-navigation');
els.metaCount = document.querySelector('.s-res-meta');
// Ausgangszustand aus dem serverseitig gerenderten Markup übernehmen,
// damit Browser-zurück / geteilte Links konsistent bleiben.
if (els.categorySelect) state.category = els.categorySelect.value || 'all';
if (els.perPageSelect) state.perPage = parseInt(els.perPageSelect.value, 10) || DEFAULT_PER_PAGE;
const checkedSort = document.querySelector('.sort-radio:checked');
if (checkedSort) state.sort = checkedSort.value;
// Ab hier übernimmt JS: Formular-Steuerelemente lösen keinen
// Seiten-Reload mehr aus, sondern rendern direkt neu.
if (els.categorySelect) {
els.categorySelect.addEventListener('change', function () {
state.category = this.value;
state.page = 1;
render();
});
}
if (els.perPageSelect) {
els.perPageSelect.addEventListener('change', function () {
state.perPage = parseInt(this.value, 10) || DEFAULT_PER_PAGE;
state.page = 1;
render();
});
}
els.sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.page = 1;
render();
});
});
render();
}
function sortItems(items, sortValue) {
const copy = items.slice();
switch (sortValue) {
case 'likes':
copy.sort(function (a, b) { return (b.likes || 0) - (a.likes || 0); });
break;
case 'newest':
copy.sort(function (a, b) { return (b.creationDate || '').localeCompare(a.creationDate || ''); });
break;
case 'oldest':
copy.sort(function (a, b) { return (a.creationDate || '').localeCompare(b.creationDate || ''); });
break;
case 'alphabet':
default:
copy.sort(function (a, b) { return a.title.localeCompare(b.title, 'de', { sensitivity: 'base' }); });
break;
}
return copy;
}
function render() {
let items = state.all;
if (state.category && state.category !== 'all') {
items = items.filter(function (it) {
return (it.category || '').toLowerCase() === state.category.toLowerCase();
});
}
state.filtered = sortItems(items, state.sort);
const totalResults = state.filtered.length;
const totalPages = Math.max(1, Math.ceil(totalResults / state.perPage));
if (state.page > totalPages) state.page = totalPages;
if (state.page < 1) state.page = 1;
const start = (state.page - 1) * state.perPage;
const pageItems = state.filtered.slice(start, start + state.perPage);
renderList(pageItems);
renderPager(totalPages);
renderMeta(totalResults);
}
function renderList(items) {
els.list.innerHTML = '';
if (items.length === 0) {
const p = document.createElement('p');
p.textContent = 'Keine Beiträge zu diesem Suchbegriff gefunden.';
els.list.appendChild(p);
return;
}
items.forEach(function (item) {
const div = document.createElement('div');
div.className = 's-res-item';
div.setAttribute('data-likes', item.likes || 0);
div.setAttribute('data-category', (item.category || '').toLowerCase());
div.innerHTML =
'<div class="s-res-content">' +
'<h2 class="s-res-item-title">' +
'<a class="s-res-link"></a>' +
'</h2>' +
'<div class="s-res-meta-row">' +
'<p class="s-res-author">Von: <span class="s-res-author-name"></span></p>' +
'<span class="s-res-likes"></span>' +
'</div>' +
'</div>' +
'<div class="s-res-arrow">&rarr;</div>';
const link = div.querySelector('.s-res-link');
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title;
div.querySelector('.s-res-author-name').textContent = item.author;
div.querySelector('.s-res-likes').textContent = item.likes || 0;
els.list.appendChild(div);
});
}
function renderPager(totalPages) {
if (!els.pagerContainer) return;
els.pagerContainer.innerHTML = '';
const prevBtn = document.createElement('button');
prevBtn.type = 'button';
prevBtn.className = 's-res-page-btn';
prevBtn.textContent = '\u00AB';
prevBtn.disabled = state.page <= 1;
prevBtn.addEventListener('click', function () { goToPage(state.page - 1); });
els.pagerContainer.appendChild(prevBtn);
const span = document.createElement('span');
span.id = 'dynamic-page-numbers';
span.textContent = 'Seite ' + state.page + ' von ' + totalPages;
els.pagerContainer.appendChild(span);
const nextBtn = document.createElement('button');
nextBtn.type = 'button';
nextBtn.className = 's-res-page-btn';
nextBtn.textContent = '\u00BB';
nextBtn.disabled = state.page >= totalPages;
nextBtn.addEventListener('click', function () { goToPage(state.page + 1); });
els.pagerContainer.appendChild(nextBtn);
}
function renderMeta(totalResults) {
if (!els.metaCount) return;
const query = els.metaCount.getAttribute('data-query') || '';
els.metaCount.textContent = totalResults + ' Treffer für Ihre Suchanfrage "' + query + '"';
}
function goToPage(page) {
state.page = page;
render();
els.list.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
+163 -198
View File
@@ -1,233 +1,198 @@
/** /**
* Übernimmt Sortierung, Kategorie-Filter und Pagination der Suchergebnisse * Übernimmt Filterung (Kategorie), Sortierung und Pagination der
* vollständig clientseitig * Suchergebnisse vollständig clientseitig, ohne Neuladen der Seite.
* *
* Ablauf: * Voraussetzung: Im HTML liegt ein <script type="application/json" id="s-res-data">
* 1. Kompletten Ergebnis-Datensatz einmalig per fetch() nachladen * mit allen Treffern der aktuellen Suche (siehe content/search-results.php).
* (php/controller/search-results-data.php, liest nur die Session-Daten,
* keine erneute Datenbank-Suche).
* 2. Sortieren + filtern (JS-Array).
* 3. Ergebnisliste + Pagination-UI daraus neu rendern.
* *
* Ersetzt sorter.js, filter.js und paginator.js, deren Klick-Handler sich * Ist kein JavaScript aktiv, bleibt die serverseitig gerenderte Ausgabe
* gegenseitig ins Gehege kamen. * (inkl. echter Pagination-Links und Formular-Submit) unverändert nutzbar.
* Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare)
* voll funktionsfähig.
*/ */
(function () { (function () {
const DEFAULT_PER_PAGE = 10;
const state = { const state = {
allItems: [], all: [],
sort: 'alphabet', filtered: [],
category: 'all', category: 'all',
itemsPerPage: 10, sort: 'alphabet',
currentPage: 1, perPage: DEFAULT_PER_PAGE,
page: 1,
}; };
let listContainer, sortRadios, categorySelect, limitSelect; const els = {};
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
function init() { function init() {
listContainer = document.querySelector('.s-res-list'); const dataScript = document.getElementById('s-res-data');
if (!listContainer) return; const listContainer = document.querySelector('.s-res-list');
if (!dataScript || !listContainer) {
sortRadios = document.querySelectorAll('.sort-radio'); return; // keine Ergebnisliste auf dieser Seite vorhanden
categorySelect = document.getElementById('category-filter');
limitSelect = document.getElementById('s-res-per-page');
prevBtn = document.getElementById('prev-page-btn');
nextBtn = document.getElementById('next-page-btn');
numbersContainer = document.getElementById('dynamic-page-numbers');
jsNav = document.getElementById('js-page-navigation');
resultCountEl = document.getElementById('s-res-result-count');
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;
fetchFullDataset()
.then(function (data) {
state.allItems = data.results || [];
attachEvents();
render();
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);
});
}
function fetchFullDataset() {
return fetch('php/controller/search-results-data.php', { credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
});
}
function getFilteredSortedItems() {
let items = state.allItems;
if (state.category !== 'all') {
const wanted = state.category.toLowerCase();
items = items.filter(function (item) {
return (item.category || '').toLowerCase() === wanted;
});
} }
items = items.slice().sort(function (a, b) { try {
if (state.sort === 'likes') { state.all = JSON.parse(dataScript.textContent);
return (b.likes || 0) - (a.likes || 0); } catch (e) {
} console.error('Suchergebnis-Daten konnten nicht gelesen werden.', e);
if (state.sort === 'newest' || state.sort === 'oldest') { return;
return state.sort === 'newest' ? (b.id - a.id) : (a.id - b.id); }
}
// alphabet (Standard) els.list = listContainer;
return (a.title || '').toLowerCase().localeCompare((b.title || '').toLowerCase()); els.categorySelect = document.getElementById('category-filter');
els.perPageSelect = document.getElementById('s-res-per-page');
els.sortRadios = document.querySelectorAll('.sort-radio');
els.pagerContainer = document.querySelector('.s-res-page-navigation');
els.metaCount = document.querySelector('.s-res-meta');
// Ausgangszustand aus dem serverseitig gerenderten Markup übernehmen,
// damit Browser-zurück / geteilte Links konsistent bleiben.
if (els.categorySelect) state.category = els.categorySelect.value || 'all';
if (els.perPageSelect) state.perPage = parseInt(els.perPageSelect.value, 10) || DEFAULT_PER_PAGE;
const checkedSort = document.querySelector('.sort-radio:checked');
if (checkedSort) state.sort = checkedSort.value;
// Ab hier übernimmt JS: Formular-Steuerelemente lösen keinen
// Seiten-Reload mehr aus, sondern rendern direkt neu.
if (els.categorySelect) {
els.categorySelect.addEventListener('change', function () {
state.category = this.value;
state.page = 1;
render();
});
}
if (els.perPageSelect) {
els.perPageSelect.addEventListener('change', function () {
state.perPage = parseInt(this.value, 10) || DEFAULT_PER_PAGE;
state.page = 1;
render();
});
}
els.sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.page = 1;
render();
});
}); });
return items; render();
}
function sortItems(items, sortValue) {
const copy = items.slice();
switch (sortValue) {
case 'likes':
copy.sort(function (a, b) { return (b.likes || 0) - (a.likes || 0); });
break;
case 'newest':
copy.sort(function (a, b) { return (b.creationDate || '').localeCompare(a.creationDate || ''); });
break;
case 'oldest':
copy.sort(function (a, b) { return (a.creationDate || '').localeCompare(b.creationDate || ''); });
break;
case 'alphabet':
default:
copy.sort(function (a, b) { return a.title.localeCompare(b.title, 'de', { sensitivity: 'base' }); });
break;
}
return copy;
} }
function render() { function render() {
const items = getFilteredSortedItems(); let items = state.all;
if (state.category && state.category !== 'all') {
if (resultCountEl) resultCountEl.textContent = items.length; items = items.filter(function (it) {
return (it.category || '').toLowerCase() === state.category.toLowerCase();
const totalPages = Math.max(1, Math.ceil(items.length / state.itemsPerPage));
if (state.currentPage > totalPages) state.currentPage = totalPages;
if (state.currentPage < 1) state.currentPage = 1;
const start = (state.currentPage - 1) * state.itemsPerPage;
const pageItems = items.slice(start, start + state.itemsPerPage);
listContainer.innerHTML = '';
if (pageItems.length === 0) {
const empty = document.createElement('p');
empty.textContent = 'Keine Beiträge gefunden.';
listContainer.appendChild(empty);
} else {
pageItems.forEach(function (item) {
listContainer.appendChild(buildCard(item));
}); });
} }
state.filtered = sortItems(items, state.sort);
updatePaginatorUI(state.currentPage, totalPages); const totalResults = state.filtered.length;
const totalPages = Math.max(1, Math.ceil(totalResults / state.perPage));
if (state.page > totalPages) state.page = totalPages;
if (state.page < 1) state.page = 1;
const start = (state.page - 1) * state.perPage;
const pageItems = state.filtered.slice(start, start + state.perPage);
renderList(pageItems);
renderPager(totalPages);
renderMeta(totalResults);
} }
function buildCard(item) { function renderList(items) {
const card = document.createElement('div'); els.list.innerHTML = '';
card.className = 's-res-item';
card.setAttribute('data-likes', item.likes);
card.setAttribute('data-category', (item.category || '').toLowerCase());
const link = document.createElement('a'); if (items.length === 0) {
link.className = 's-res-link'; const p = document.createElement('p');
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id); p.textContent = 'Keine Beiträge zu diesem Suchbegriff gefunden.';
link.textContent = item.title; els.list.appendChild(p);
return;
const title = document.createElement('h2');
title.className = 's-res-item-title';
title.appendChild(link);
const authorName = document.createElement('span');
authorName.className = 's-res-author-name';
authorName.textContent = item.author;
const author = document.createElement('p');
author.className = 's-res-author';
author.append('Von: ', authorName);
const likes = document.createElement('span');
likes.className = 's-res-likes';
likes.textContent = '❤️ ' + item.likes;
const metaRow = document.createElement('div');
metaRow.className = 's-res-meta-row';
metaRow.append(author, likes);
const content = document.createElement('div');
content.className = 's-res-content';
content.append(title, metaRow);
const arrow = document.createElement('div');
arrow.className = 's-res-arrow';
arrow.textContent = '→';
card.append(content, arrow);
return card;
}
function updatePaginatorUI(currentPage, totalPages) {
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
numbersContainer.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 's-res-page-btn' + (i === currentPage ? ' s-res-page-btn-active' : '');
btn.textContent = i;
btn.addEventListener('click', function () {
state.currentPage = i;
render();
});
numbersContainer.appendChild(btn);
} }
}
function attachEvents() { items.forEach(function (item) {
sortRadios.forEach(function (radio) { const div = document.createElement('div');
radio.addEventListener('change', function () { div.className = 's-res-item';
state.sort = this.value; div.setAttribute('data-likes', item.likes || 0);
state.currentPage = 1; div.setAttribute('data-category', (item.category || '').toLowerCase());
render();
}); div.innerHTML =
'<div class="s-res-content">' +
'<h2 class="s-res-item-title">' +
'<a class="s-res-link"></a>' +
'</h2>' +
'<div class="s-res-meta-row">' +
'<p class="s-res-author">Von: <span class="s-res-author-name"></span></p>' +
'<span class="s-res-likes"></span>' +
'</div>' +
'</div>' +
'<div class="s-res-arrow">&rarr;</div>';
const link = div.querySelector('.s-res-link');
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title;
div.querySelector('.s-res-author-name').textContent = item.author;
div.querySelector('.s-res-likes').textContent = item.likes || 0;
els.list.appendChild(div);
}); });
}
if (categorySelect) { function renderPager(totalPages) {
categorySelect.addEventListener('change', function () { if (!els.pagerContainer) return;
state.category = this.value; els.pagerContainer.innerHTML = '';
state.currentPage = 1;
render();
});
}
if (limitSelect) { const prevBtn = document.createElement('button');
limitSelect.addEventListener('change', function () { prevBtn.type = 'button';
state.itemsPerPage = parseInt(this.value, 10) || 10; prevBtn.className = 's-res-page-btn';
state.currentPage = 1; prevBtn.textContent = '\u00AB';
render(); prevBtn.disabled = state.page <= 1;
}); prevBtn.addEventListener('click', function () { goToPage(state.page - 1); });
} els.pagerContainer.appendChild(prevBtn);
if (prevBtn) { const span = document.createElement('span');
prevBtn.addEventListener('click', function () { span.id = 'dynamic-page-numbers';
if (state.currentPage > 1) { span.textContent = 'Seite ' + state.page + ' von ' + totalPages;
state.currentPage -= 1; els.pagerContainer.appendChild(span);
render();
}
});
}
if (nextBtn) { const nextBtn = document.createElement('button');
nextBtn.addEventListener('click', function () { nextBtn.type = 'button';
state.currentPage += 1; nextBtn.className = 's-res-page-btn';
render(); nextBtn.textContent = '\u00BB';
}); nextBtn.disabled = state.page >= totalPages;
} nextBtn.addEventListener('click', function () { goToPage(state.page + 1); });
els.pagerContainer.appendChild(nextBtn);
}
// Mit aktivem JS übernimmt render() Sortierung/Filter live function renderMeta(totalResults) {
// ein echtes Absenden des Formulars (No-JS-Fallback) ist dann nicht mehr nötig. if (!els.metaCount) return;
const filterForm = document.getElementById('s-res-filter-form'); const query = els.metaCount.getAttribute('data-query') || '';
if (filterForm) { els.metaCount.textContent = totalResults + ' Treffer für Ihre Suchanfrage "' + query + '"';
filterForm.addEventListener('submit', function (e) { }
e.preventDefault();
}); function goToPage(page) {
} state.page = page;
render();
els.list.scrollIntoView({ behavior: 'smooth', block: 'start' });
} }
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
-242
View File
@@ -1,242 +0,0 @@
/**
* Übernimmt Sortierung, Suche (innerhalb der Kategorie) und Pagination der
* Kategorie-Ansicht vollständig clientseitig.
*
* Ablauf:
* 1. Kompletten Kategorie-Datensatz einmalig per fetch() nachladen
* 2. Sortieren + nach Suchbegriff filtern (JS-Array).
* 3. Ergebnisliste + Pagination-UI daraus neu rendern.
*
* Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare)
* voll funktionsfähig
*/
(function () {
const state = {
allItems: [],
sort: 'alphabet',
query: '',
itemsPerPage: 10,
currentPage: 1,
};
let listContainer, sortRadios, searchInput, limitSelect;
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
let searchDebounceTimer;
function init() {
listContainer = document.querySelector('.s-res-list');
if (!listContainer) return;
sortRadios = document.querySelectorAll('.sort-radio');
searchInput = document.getElementById('cat-search-input');
limitSelect = document.getElementById('s-res-per-page');
prevBtn = document.getElementById('prev-page-btn');
nextBtn = document.getElementById('next-page-btn');
numbersContainer = document.getElementById('dynamic-page-numbers');
jsNav = document.getElementById('js-page-navigation');
resultCountEl = document.getElementById('s-res-result-count');
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;
fetchFullDataset()
.then(function (data) {
state.allItems = data.results || [];
attachEvents();
render();
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);
});
}
function fetchFullDataset() {
return fetch('php/controller/showCategory-data.php', { credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
});
}
function getFilteredSortedItems() {
let items = state.allItems;
const needle = state.query.trim().toLowerCase();
if (needle !== '') {
items = items.filter(function (item) {
const haystack = ((item.title || '') + ' ' + (item.content || '')).toLowerCase();
return haystack.indexOf(needle) !== -1;
});
}
items = items.slice().sort(function (a, b) {
if (state.sort === 'likes') {
return (b.likes || 0) - (a.likes || 0);
}
if (state.sort === 'newest' || state.sort === 'oldest') {
return state.sort === 'newest' ? (b.id - a.id) : (a.id - b.id);
}
// alphabet (Standard)
return (a.title || '').toLowerCase().localeCompare((b.title || '').toLowerCase());
});
return items;
}
function render() {
const items = getFilteredSortedItems();
if (resultCountEl) resultCountEl.textContent = items.length;
const totalPages = Math.max(1, Math.ceil(items.length / state.itemsPerPage));
if (state.currentPage > totalPages) state.currentPage = totalPages;
if (state.currentPage < 1) state.currentPage = 1;
const start = (state.currentPage - 1) * state.itemsPerPage;
const pageItems = items.slice(start, start + state.itemsPerPage);
listContainer.innerHTML = '';
if (pageItems.length === 0) {
const empty = document.createElement('p');
empty.textContent = 'Keine Beiträge gefunden.';
listContainer.appendChild(empty);
} else {
pageItems.forEach(function (item) {
listContainer.appendChild(buildCard(item));
});
}
updatePaginatorUI(state.currentPage, totalPages);
}
function buildCard(item) {
const card = document.createElement('div');
card.className = 's-res-item';
card.setAttribute('data-likes', item.likes);
card.setAttribute('data-category', (item.category || '').toLowerCase());
const link = document.createElement('a');
link.className = 's-res-link';
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title;
const title = document.createElement('h2');
title.className = 's-res-item-title';
title.appendChild(link);
const authorName = document.createElement('span');
authorName.className = 's-res-author-name';
authorName.textContent = item.author;
const author = document.createElement('p');
author.className = 's-res-author';
author.append('Von: ', authorName);
const likes = document.createElement('span');
likes.className = 's-res-likes';
likes.textContent = '❤️ ' + item.likes;
const metaRow = document.createElement('div');
metaRow.className = 's-res-meta-row';
metaRow.append(author, likes);
const content = document.createElement('div');
content.className = 's-res-content';
content.append(title, metaRow);
const arrow = document.createElement('div');
arrow.className = 's-res-arrow';
arrow.textContent = '→';
card.append(content, arrow);
return card;
}
function updatePaginatorUI(currentPage, totalPages) {
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
numbersContainer.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 's-res-page-btn' + (i === currentPage ? ' s-res-page-btn-active' : '');
btn.textContent = i;
btn.addEventListener('click', function () {
state.currentPage = i;
render();
});
numbersContainer.appendChild(btn);
}
}
function attachEvents() {
sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.currentPage = 1;
render();
});
});
if (searchInput) {
// Live-Filterung während des Tippens (leicht entprellt), da der
// Datensatz bereits vollständig im Browser liegt.
searchInput.addEventListener('input', function () {
clearTimeout(searchDebounceTimer);
const value = this.value;
searchDebounceTimer = setTimeout(function () {
state.query = value;
state.currentPage = 1;
render();
}, 150);
});
}
if (limitSelect) {
limitSelect.addEventListener('change', function () {
state.itemsPerPage = parseInt(this.value, 10) || 10;
state.currentPage = 1;
render();
});
}
if (prevBtn) {
prevBtn.addEventListener('click', function () {
if (state.currentPage > 1) {
state.currentPage -= 1;
render();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
state.currentPage += 1;
render();
});
}
// Mit aktivem JS übernimmt render() Sortierung/Suche live
// ein echtes Absenden des Formulars (No-JS-Fallback) ist dann nicht mehr nötig.
const filterForm = document.getElementById('cat-filter-form');
if (filterForm) {
filterForm.addEventListener('submit', function (e) {
e.preventDefault();
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
+50
View File
@@ -0,0 +1,50 @@
function initSorter() {
const listContainer = document.querySelector('.s-res-list');
const sortRadios = document.querySelectorAll('.sort-radio');
// wenn keine liste vorhanden, abbrechen
if (!listContainer || sortRadios.length === 0) return;
sortRadios.forEach(radio => {
radio.addEventListener('change', function() {
const cards = Array.from(listContainer.querySelectorAll('.s-res-item'));
const sortValue = this.value;
cards.sort((a, b) => {
if (sortValue === 'likes') {
const likesA = parseInt(a.getAttribute('data-likes') || '0', 10);
const likesB = parseInt(b.getAttribute('data-likes') || '0', 10);
return likesB - likesA;
}
else if (sortValue === 'alphabet') {
// alphabetische sortierung
const titleA = a.querySelector('.s-res-link').textContent.trim().toLowerCase();
const titleB = b.querySelector('.s-res-link').textContent.trim().toLowerCase();
return titleA.localeCompare(titleB);
}
else if (sortValue === 'newest' || sortValue === 'oldest') {
// hoehere ID wird als neuer gesehen
const urlA = a.querySelector('.s-res-link').getAttribute('href');
const urlB = b.querySelector('.s-res-link').getAttribute('href');
const idA = parseInt(urlA.match(/id=(\d+)/)[1], 10);
const idB = parseInt(urlB.match(/id=(\d+)/)[1], 10);
return sortValue === 'newest' ? idB - idA : idA - idB;
}
return 0;
});
listContainer.innerHTML = '';
cards.forEach(card => listContainer.appendChild(card));
});
});
}
// ist das DOM bereits vollständig aufgebaut?
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSorter);
} else {
initSorter();
}
+29 -221
View File
@@ -3,203 +3,32 @@ if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
header("Content-Type: application/json");
require_once "../model/CommentManager.php"; require_once "../model/CommentManager.php";
require_once "../model/UserManager.php";
require_once "../model/ArticleManager.php";
require_once "../../includes/csrf.php";
/**
* Prüft, ob die Anfrage durch JavaScript per AJAX gesendet wurde.
*/
$isAjaxRequest = isset($_SERVER["HTTP_X_REQUESTED_WITH"])
&& strtolower($_SERVER["HTTP_X_REQUESTED_WITH"]) === "xmlhttprequest";
/**
* Gibt das Ergebnis entweder als JSON zurück oder leitet
* bei einem normalen Formularaufruf wieder zum Beitrag zurück.
*
* @param bool $success War das Speichern erfolgreich?
* @param string $message Rückmeldung für den Benutzer
* @param int|null $articleId ID des Beitrags
* @param array $additionalData Zusätzliche Daten für AJAX
*/
function sendCommentResponse(
$success,
$message,
$articleId,
$additionalData = []
) {
global $isAjaxRequest;
if ($isAjaxRequest) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode(
array_merge(
[
"success" => $success,
"message" => $message
],
$additionalData
)
);
exit();
}
/*
* Bei deaktiviertem JavaScript wird die Rückmeldung
* in der Session gespeichert und die Beitragsseite neu geladen.
*/
$_SESSION["comment_message"] = $message;
$_SESSION["comment_message_type"] = $success ? "success" : "error";
if ($articleId !== null) {
header(
"Location: ../../index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
} else {
header("Location: ../../index.php");
}
if (!isset($_SESSION["user_email"])) {
echo json_encode([
"success" => false,
"message" => "Du musst angemeldet sein, um zu kommentieren."
]);
exit(); exit();
} }
/* $articleId = $_POST["article_id"] ?? null;
* Nur POST-Anfragen dürfen Kommentare erstellen.
*/
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
sendCommentResponse(
false,
"Ungültige Anfrage.",
null
);
}
/*
* Die Beitrags-ID wird zuerst eingelesen,
* damit bei Fehlern wieder zum Beitrag zurückgeleitet werden kann.
*/
$articleId = filter_input(
INPUT_POST,
"article_id",
FILTER_VALIDATE_INT
);
/*
* Ein Benutzer muss angemeldet sein.
*/
if (!isset($_SESSION["user_email"])) {
sendCommentResponse(
false,
"Du musst angemeldet sein, um zu kommentieren.",
$articleId !== false ? $articleId : null
);
}
/*
* CSRF-Token prüfen, bevor irgendeine Änderung vorgenommen wird.
*/
if (!csrf_verify()) {
sendCommentResponse(
false,
"Deine Sitzung ist abgelaufen. Bitte lade die Seite neu und versuche es erneut.",
$articleId !== false ? $articleId : null
);
}
/*
* Weitere Formulardaten einlesen.
*/
$content = trim($_POST["content"] ?? ""); $content = trim($_POST["content"] ?? "");
$parentCommentId = $_POST["parent_comment_id"] ?? null;
$parentCommentId = filter_input( if ($parentCommentId === "" || $parentCommentId === "0") {
INPUT_POST,
"parent_comment_id",
FILTER_VALIDATE_INT
);
/*
* Ein leerer Wert bedeutet, dass es sich um einen
* normalen Hauptkommentar handelt.
*/
if (
!isset($_POST["parent_comment_id"])
|| $_POST["parent_comment_id"] === ""
|| $_POST["parent_comment_id"] === "0"
) {
$parentCommentId = null; $parentCommentId = null;
} }
if ($articleId === false || $articleId === null) { if (empty($articleId) || empty($content)) {
sendCommentResponse( echo json_encode([
false, "success" => false,
"Der zugehörige Beitrag ist ungültig.", "message" => "Kommentar darf nicht leer sein."
null ]);
); exit();
}
/*
* Der Beitrag muss tatsächlich existieren.
*/
$existingArticle = ArticleManager::getInstance()->getArticle($articleId);
if ($existingArticle === null) {
sendCommentResponse(
false,
"Der zugehörige Beitrag wurde nicht gefunden.",
null
);
}
if ($content === "") {
sendCommentResponse(
false,
"Der Kommentar darf nicht leer sein.",
$articleId
);
}
/*
* Eine ungültige Eltern-ID darf nicht gespeichert werden.
*/
if (
isset($_POST["parent_comment_id"])
&& $_POST["parent_comment_id"] !== ""
&& $_POST["parent_comment_id"] !== "0"
&& $parentCommentId === false
) {
sendCommentResponse(
false,
"Der ausgewählte Kommentar ist ungültig.",
$articleId
);
}
/*
* Falls eine Eltern-ID angegeben wurde, muss dieser Kommentar
* tatsächlich existieren und zum selben Beitrag gehören.
*/
if ($parentCommentId !== null) {
$existingComments = CommentManager::getInstance()->getCommentsByArticle($articleId);
$parentExists = false;
foreach ($existingComments as $existingComment) {
if ($existingComment->getId() === $parentCommentId) {
$parentExists = true;
break;
}
}
if (!$parentExists) {
sendCommentResponse(
false,
"Der ausgewählte Kommentar wurde nicht gefunden.",
$articleId
);
}
} }
try { try {
@@ -212,39 +41,18 @@ try {
$parentCommentId $parentCommentId
); );
$userManager = UserManager::getInstance(); echo json_encode([
$user = $userManager->findUser($_SESSION["user_email"]); "success" => true,
"commentId" => $commentId,
"author" => $_SESSION["user_email"],
"content" => $content,
"created" => date("Y-m-d H:i:s"),
"parentCommentId" => $parentCommentId
]);
$authorName = $_SESSION["user_email"]; } catch (Exception $e) {
echo json_encode([
if ($user !== null) { "success" => false,
$vorname = trim($user["vorname"] ?? ""); "message" => "Kommentar konnte nicht gespeichert werden."
$nachname = trim($user["nachname"] ?? ""); ]);
$fullName = trim($vorname . " " . $nachname);
if ($fullName !== "") {
$authorName = $fullName;
}
}
sendCommentResponse(
true,
"Der Kommentar wurde erfolgreich gespeichert.",
$articleId,
[
"commentId" => $commentId,
"author" => $authorName,
"content" => $content,
"created" => date("Y-m-d H:i:s"),
"parentCommentId" => $parentCommentId
]
);
} catch (Throwable $e) {
sendCommentResponse(
false,
"Der Kommentar konnte nicht gespeichert werden.",
$articleId
);
} }
+114 -108
View File
@@ -5,128 +5,134 @@ 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';
require_once '../../includes/csrf.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 ($_SERVER["REQUEST_METHOD"] === "POST") { if(!isset($_POST["title"]) ||!isset($_POST["content"]) || !isset($_POST["category"])){
$_SESSION["message"] = "missing_parameters";
// CSRF-Token prüfen, bevor irgendeine Änderung vorgenommen wird
if (!csrf_verify()) {
$_SESSION["message"] = "invalid_csrf_token";
header("location: ../../index.php?pfad=createArticle");
exit();
}
$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 {
$cleanedTags = []; $title = $_POST["title"];
$rawTags = explode(',', $tags); $content = $_POST["content"];
foreach ($rawTags as $rawTag) { $author = $_SESSION["user_email"];
// Leerzeichen am Anfang/Ende des einzelnen Tags entfernen: $category = $_POST["category"];
$tag = trim($rawTag); $tags = $_POST['tags'] ?? '';
$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);
}
// ----------------- Übertragung der validierten Daten in ArticleManager: --------------------------- if (!articleContentValidator($content)) {
try { $_SESSION["message"] = "invalid_content";
$articleManager = ArticleManager::getInstance(); header("location: ../../index.php?pfad=createArticle");
// $content enthält bereits die finalen "uploads/..."-Pfade (kein Base64 mehr), exit();
// da rebuildBlocksFromPost() Datei-Uploads sofort verarbeitet. }
$articleManager->addArticle($title, $content, $author, $category, $cleanedTags);
// Formulardaten nach erfolgreichem Erstellen aus der Session löschen if (!articleCategoryValidator($category)) {
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]); $_SESSION["message"] = "invalid_category";
header("location: ../../index.php?pfad=createArticle");
exit();
}
} catch (\Throwable $e) { if (!articleTagValidator($tags)) {
$_SESSION["message"] = "internal_error"; $_SESSION["message"] = "invalid_tags";
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();
} }
}
?> ?>
@@ -5,24 +5,12 @@ if (session_status() === PHP_SESSION_NONE) {
require_once __DIR__ . "/../model/UserManager.php"; require_once __DIR__ . "/../model/UserManager.php";
require_once __DIR__ . "/../model/ArticleManager.php"; require_once __DIR__ . "/../model/ArticleManager.php";
require_once __DIR__ . "/../../includes/csrf.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") {
header("Location: ../../index.php?pfad=profile");
exit();
}
if (!csrf_verify()) {
$_SESSION["message"] = "invalid_csrf_token";
header("Location: ../../index.php?pfad=profile");
exit();
}
/* /*
Deregistrierung Deregistrierung
Funktion: Entfernt User aus der Datenbank und beendet die Session Funktion: Entfernt User aus der Datenbank und beendet die Session
+4 -13
View File
@@ -4,7 +4,6 @@ if (session_status() === PHP_SESSION_NONE) {
} }
require_once __DIR__ . "/../model/ArticleManager.php"; require_once __DIR__ . "/../model/ArticleManager.php";
require_once __DIR__ . "/../../includes/csrf.php";
if (!isset($_SESSION["user"])) { if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login"); header("Location: index.php?pfad=login");
@@ -13,13 +12,6 @@ if (!isset($_SESSION["user"])) {
if ($_SERVER["REQUEST_METHOD"] === "POST") { if ($_SERVER["REQUEST_METHOD"] === "POST") {
// CSRF-Token prüfen, bevor irgendeine Änderung vorgenommen wird
if (!csrf_verify()) {
$_SESSION["message"] = "invalid_csrf_token";
header("location: ../../index.php?pfad=profile");
exit();
}
if (isset($_SESSION["user_email"])) { if (isset($_SESSION["user_email"])) {
$user = $_SESSION["user_email"]; $user = $_SESSION["user_email"];
} else { } else {
@@ -30,10 +22,9 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
exit(); exit();
} }
// Die Beitrags-ID muss eine gültige numerische ID sein. if (isset($_POST["id"]) && !empty($_POST["id"])) {
$id = filter_input(INPUT_POST, "id", FILTER_VALIDATE_INT); $id = $_POST["id"];
} else {
if ($id === false || $id === null) {
$_SESSION["message"] = "missing_id"; $_SESSION["message"] = "missing_id";
header("location: ../../index.php?pfad=profile"); header("location: ../../index.php?pfad=profile");
exit(); exit();
@@ -53,4 +44,4 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$_SESSION["message"] = "article_deleted"; $_SESSION["message"] = "article_deleted";
header("location: ../../index.php?pfad=profile"); header("location: ../../index.php?pfad=profile");
exit(); exit();
} }
-123
View File
@@ -1,123 +0,0 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . "/../model/CommentManager.php";
require_once __DIR__ . "/../../includes/csrf.php";
/*
* Kommentare dürfen nur über ein POST-Formular gelöscht werden.
*/
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: index.php");
exit();
}
$commentId = filter_input(
INPUT_POST,
"comment_id",
FILTER_VALIDATE_INT
);
$articleId = filter_input(
INPUT_POST,
"article_id",
FILTER_VALIDATE_INT
);
/*
* Nur angemeldete Nutzer dürfen Kommentare löschen.
*/
if (!isset($_SESSION["user_email"])) {
$_SESSION["comment_message"] = "Du musst angemeldet sein.";
$_SESSION["comment_message_type"] = "error";
if ($articleId !== false && $articleId !== null) {
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
} else {
header("Location: index.php");
}
exit();
}
// CSRF-Token prüfen, bevor irgendeine Änderung vorgenommen wird
if (!csrf_verify()) {
$_SESSION["comment_message"] = "Deine Sitzung ist abgelaufen. Bitte lade die Seite neu.";
$_SESSION["comment_message_type"] = "error";
if ($articleId !== false && $articleId !== null) {
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
} else {
header("Location: index.php");
}
exit();
}
/*
* Kommentar-ID und Beitrags-ID müssen gültige Zahlen sein.
*/
if (
$commentId === false
|| $commentId === null
|| $articleId === false
|| $articleId === null
) {
$_SESSION["comment_message"] =
"Der Kommentar konnte nicht gelöscht werden.";
$_SESSION["comment_message_type"] = "error";
header("Location: index.php");
exit();
}
try {
$commentManager = CommentManager::getInstance();
/*
* Die E-Mail-Adresse aus der Session wird mitgegeben.
* Dadurch kann der Nutzer nur eigene Kommentare löschen.
*/
$deleted = $commentManager->deleteComment(
$commentId,
$_SESSION["user_email"]
);
if ($deleted) {
$_SESSION["comment_message"] =
"Der Kommentar wurde gelöscht.";
$_SESSION["comment_message_type"] = "success";
} else {
$_SESSION["comment_message"] =
"Der Kommentar wurde nicht gefunden oder gehört nicht dir.";
$_SESSION["comment_message_type"] = "error";
}
} catch (Throwable $e) {
$_SESSION["comment_message"] =
"Der Kommentar konnte nicht gelöscht werden.";
$_SESSION["comment_message_type"] = "error";
}
/*
* Anschließend wird wieder zum Beitrag und zu den Kommentaren geleitet.
*/
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
exit();
+1 -39
View File
@@ -1,41 +1,3 @@
<?php <?php
// Standardpfad
$pfad = $_GET["pfad"] ?? "home";
if ($pfad === "logout") { ?>
include_once "php/controller/logout-controller.php";
exit();
} elseif ($pfad === "deleteAccount") {
include_once "php/controller/deleteAccount-controller.php";
exit();
}
if ($pfad === "login") {
include_once "php/controller/login-controller.php";
} elseif ($pfad === "register") {
include_once "php/controller/register-controller.php";
} elseif ($pfad === "password-forgotten") {
include_once "php/controller/password-forgotten-controller.php";
} elseif ($pfad === "confirm-register") {
include_once "php/controller/confirm-register-controller.php";
} elseif ($pfad === "confirm-password") {
include_once "php/controller/confirm-password-controller.php";
} elseif ($pfad === "profile") {
include_once "php/controller/profile-controller.php";
} elseif ($pfad === "updateComment") {
include_once "php/controller/updateComment-controller.php";
} elseif($pfad === "deleteComment") {
include_once "php/controller/deleteComment-controller.php";
}
// Whitelist
$erlaubte_content_seiten = [
"accessibility", "confirm-password", "confirm-register", "createArticle",
"datenschutz", "home", "impressum", "login", "nutzungsbedingungen",
"password-forgotten", "profile", "register", "search-results",
"show-mail", "showArticle", "showCategory", "updateArticle"
];
if (!in_array($pfad, $erlaubte_content_seiten)) {
$pfad = "404";
}
-13
View File
@@ -5,12 +5,6 @@ if (session_status() === PHP_SESSION_NONE) {
require_once __DIR__ . '/../model/Article.php'; require_once __DIR__ . '/../model/Article.php';
require_once __DIR__ . '/../model/ArticleManager.php'; require_once __DIR__ . '/../model/ArticleManager.php';
require_once __DIR__ . '/../../includes/csrf.php';
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: ../../index.php");
exit();
}
// 2. Prüfen, ob eine gültige Artikel-ID übergeben wurde // 2. Prüfen, ob eine gültige Artikel-ID übergeben wurde
if (isset($_GET["id"]) && !empty($_GET["id"])) { if (isset($_GET["id"]) && !empty($_GET["id"])) {
@@ -23,13 +17,6 @@ if (isset($_GET["id"]) && !empty($_GET["id"])) {
exit(); exit();
} }
// CSRF-Token prüfen, bevor der Like-Status verändert wird
if (!csrf_verify()) {
$_SESSION["message"] = "invalid_csrf_token";
header("Location: ../../index.php?pfad=showArticle&id=" . $articleId);
exit();
}
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$articleManager->toggleLike($articleId, $userEmail); $articleManager->toggleLike($articleId, $userEmail);
-4
View File
@@ -1,8 +1,4 @@
<?php <?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION = []; $_SESSION = [];
session_destroy(); session_destroy();
+1 -8
View File
@@ -5,7 +5,6 @@ require_once "php/model/Article.php";
require_once "php/model/ArticleManager.php"; require_once "php/model/ArticleManager.php";
require_once "php/model/CommentManager.php"; require_once "php/model/CommentManager.php";
require_once "php/validator/user-validator.php"; require_once "php/validator/user-validator.php";
require_once "includes/csrf.php";
$error = null; $error = null;
@@ -28,7 +27,6 @@ try {
} }
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["saveProfile"])) { if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["saveProfile"])) {
$oldEmail = $_SESSION["user_email"]; $oldEmail = $_SESSION["user_email"];
$newEmail = trim($_POST["email"] ?? ""); $newEmail = trim($_POST["email"] ?? "");
@@ -36,12 +34,7 @@ try {
$nachname = trim($_POST["nachname"] ?? ""); $nachname = trim($_POST["nachname"] ?? "");
$password = $_POST["password"] ?? ""; $password = $_POST["password"] ?? "";
if (!csrf_verify()) { if (!userEmailValidator($newEmail)) {
// CSRF-Token prüfen, bevor irgendeine Änderung vorgenommen wird
$error = "Deine Sitzung ist abgelaufen. Bitte lade die Seite neu und versuche es erneut.";
$_GET["edit"] = "1";
} elseif (!userEmailValidator($newEmail)) {
$error = "Bitte gib eine gültige E-Mail-Adresse ein."; $error = "Bitte gib eine gültige E-Mail-Adresse ein.";
$_GET["edit"] = "1"; $_GET["edit"] = "1";
+48 -24
View File
@@ -2,64 +2,88 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
require_once '../model/ArticleManager.php'; require_once '../model/ArticleManager.php';
require_once '../model/UserManager.php'; require_once '../model/UserManager.php';
require_once '../model/Article.php'; require_once '../model/Article.php';
require_once '../validator/search-validator.php'; require_once '../validator/search-validator.php';
if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) { if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
$search = trim($_GET["q"]); $search = trim($_GET["q"]);
if (!searchQueryValidator($search)) { if (!searchQueryValidator($search)) {
$_SESSION["search_results"] = []; $_SESSION["search_results"] = [];
$_SESSION["search_query"] = ""; $_SESSION["search_query"] = "";
$_SESSION["message"] = "invalid_search_query"; $_SESSION["message"] = "invalid_search_query";
} else { } else {
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$userManager = UserManager::getInstance(); $userManager = UserManager::getInstance();
$results = $articleManager->search($search);
$results = $articleManager->search($search); $sortStyle = $_GET['sort'] ?? 'alphabet';
$_SESSION['search_sort'] = $sortStyle;
if ($sortStyle === 'alphabet') {
// Titel aufsteigend alphabetisch sortiert
usort($results, function ($a, $b) {
return strcasecmp($a->getTitle(), $b->getTitle());
});
} elseif ($sortStyle === 'likes') {
usort($results, function ($a, $b) {
return $b->getLikeCount() <=> $a->getLikeCount();
});
} elseif ($sortStyle === 'newest') {
// Datum neu zu alt sortiert
usort($results, function ($a, $b) {
return strcmp($b->getCreationDate(), $a->getCreationDate());
});
} elseif ($sortStyle === 'oldest') {
// Datum alt zu neu sortiert
usort($results, function ($a, $b) {
return strcmp($a->getCreationDate(), $b->getCreationDate());
});
}
// Ergebnisse werden in ein flaches Array umgewandelt, da sonst incomplete-PHP-Fehler im Ergebnis
$safeArrayResults = []; $safeArrayResults = [];
foreach ($results as $obj) { foreach ($results as $obj) {
$safeArrayResults[] = [ $safeArrayResults[] = [
"id" => $obj->getId(), "id" => $obj->getId(),
"title" => $obj->getTitle(), "title" => $obj->getTitle(),
"content" => $obj->getContent(), "content" => $obj->getContent(),
"author" => $userManager->findUser($obj->getAuthor())["vorname"] . " " . $userManager->findUser($obj->getAuthor())["nachname"], "author" => $userManager->findUser($obj->getAuthor())["vorname"] . " " . $userManager->findUser($obj->getAuthor())["nachname"],
"category" => $obj->getCategory(), "category" => $obj->getCategory(),
"tags" => $obj->getTags(), "tags" => $obj->getTags(),
"creationDate" => $obj->getCreationDate(), "creationDate" => $obj->getCreationDate(),
"likes" => $obj->getLikes(), "likes" => $obj->getLikes(),
]; ];
} }
$_SESSION["search_results"] = $safeArrayResults; $_SESSION["search_results"] = $safeArrayResults;
$_SESSION["search_query"] = $search; $_SESSION["search_query"] = $search;
$_SESSION["message"] = "new_search_results"; $_SESSION["message"] = "new_search_results";
} catch (Exception $e) { } catch (Exception $e) {
$_SESSION["message"] = "internal_error"; $_SESSION["message"] = "internal_error";
} }
} }
$sort = $_GET['sort'] ?? 'alphabet'; $sort = $_GET['sort'] ?? 'alphabet';
$_SESSION['search_sort'] = $sort;
$category = $_GET['category'] ?? 'all';
$_SESSION['search_category'] = $category;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10; $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
if (!searchLimitValidator($limit)) { if (!searchLimitValidator($limit)) {
$limit = 10; $limit = 10;
} }
$_SESSION['search_limit'] = $limit; $page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
// Neue Suche -> immer wieder bei Seite 1 beginnen // Kategorie-Filter durchreichen (Whitelist-Prüfung erfolgt in der View)
header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&category=" . urlencode($category) . "&limit=" . $limit . "&page=1"); $category = isset($_GET['category']) ? $_GET['category'] : 'all';
header(
"Location: ../../index.php?pfad=search-results&q=" . urlencode($search) .
"&sort=" . urlencode($sort) .
"&limit=" . $limit .
"&category=" . urlencode($category) .
"&page=" . $page
);
exit(); exit();
}
} ?>
-32
View File
@@ -1,32 +0,0 @@
<?php
/**
* Liefert den vollständigen, zuletzt gefundenen Ergebnis-Datensatz als JSON.
*
* Wird von js/search-results.js EINMALIG per fetch() geladen, damit Sortierung,
* Filterung und Pagination danach komplett im Browser laufen können, ohne
* weitere Serveranfragen oder eine erneute Datenbank-Suche auszulösen.
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../includes/resultsHelper.php';
$rawResults = $_SESSION['search_results'] ?? [];
$payload = array_map(function ($item) {
return [
'id' => $item['id'],
'title' => $item['title'],
'author' => $item['author'],
'category' => $item['category'] ?? '',
'likes' => getLikeCount($item),
];
}, $rawResults);
echo json_encode([
'query' => $_SESSION['search_query'] ?? '',
'results' => array_values($payload),
]);
+4 -9
View File
@@ -7,14 +7,10 @@ require_once 'php/model/Article.php';
require_once 'php/model/ArticleManager.php'; require_once 'php/model/ArticleManager.php';
require_once 'php/model/UserManager.php'; require_once 'php/model/UserManager.php';
require_once 'php/model/CommentManager.php'; require_once 'php/model/CommentManager.php';
require_once 'php/validator/article-validator.php';
// Die übergebene ID muss eine gültige, positive Zahl sein, bevor sie if (isset($_GET["id"]) && !empty($_GET["id"])){
// weiterverwendet wird. Vorher wurde jeder nicht-leere Wert akzeptiert.
$id = isset($_GET["id"]) ? articleIdValidator($_GET["id"]) : false;
if ($id !== false) {
try { try {
$id = $_GET["id"];
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$article = $articleManager->getArticle($id); $article = $articleManager->getArticle($id);
if($article != null){ if($article != null){
@@ -42,7 +38,7 @@ if ($id !== false) {
} }
$commentManager = CommentManager::getInstance(); $commentManager = CommentManager::getInstance();
$comments = $commentManager->getCommentsByArticle($id); // NEU: validierte ID statt rohem $_GET["id"] $comments = $commentManager->getCommentsByArticle($_GET["id"]);
foreach ($comments as $comment) { foreach ($comments as $comment) {
if ($comment->isReply()) { if ($comment->isReply()) {
@@ -53,9 +49,8 @@ if ($id !== false) {
} }
} }
} catch (Throwable $e) { } catch (Exception $e){
$_SESSION["message"] = "internal_error"; $_SESSION["message"] = "internal_error";
header("Location: index.php");
exit(); exit();
} }
}else{ }else{
+39 -24
View File
@@ -2,51 +2,66 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
require_once 'php/model/Article.php'; require_once 'php/model/Article.php';
require_once 'php/model/ArticleManager.php'; require_once 'php/model/ArticleManager.php';
require_once 'php/model/UserManager.php'; require_once 'php/model/UserManager.php';
require_once 'php/validator/article-validator.php'; require_once 'php/validator/article-validator.php';
require_once __DIR__ . '/../../includes/resultsHelper.php';
if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_GET["category"])) {
if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_GET["category"])){
$category = $_GET["category"]; $category = $_GET["category"];
// Sortierung auslesen (Standard: alphabet)
$sortStyle = isset($_GET['sort']) ? trim($_GET['sort']) : 'alphabet';
// Aktuelle Seite auslesen
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) { $page = 1; }
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$userManager = UserManager::getInstance(); $userManager = UserManager::getInstance();
$allArticles = $articleManager->getArticlesByCategory($category); $allArticles = $articleManager->getArticlesByCategory($category);
$safeArrayResults = [];
foreach ($allArticles as $article) { foreach ($allArticles as $article) {
$authorName = $userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"]; $article->setAuthor($userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"]);
$safeArrayResults[] = [ }
"id" => $article->getID(), $totalArticles = count($allArticles);
"title" => $article->getTitle(),
"content" => $article->getContent(), // --- SORTIERUNG LOGIK ---
"author" => $authorName, if ($sortStyle === 'alphabet') {
"category" => $article->getCategory(), usort($allArticles, function($a, $b) {
"tags" => $article->getTags(), return strcasecmp($a->getTitle(), $b->getTitle());
"creationDate" => $article->getCreationDate(), });
"likes" => $article->getLikes(), } elseif ($sortStyle === 'likes') {
]; usort($allArticles, function($a, $b) {
return $b->getLikes() <=> $a->getLikes(); // Absteigend nach Likes
});
} elseif ($sortStyle === 'newest') {
usort($allArticles, function($a, $b) {
return strtotime($b->getCreationDate()) <=> strtotime($a->getCreationDate()); // Neueste zuerst
});
} elseif ($sortStyle === 'oldest') {
usort($allArticles, function($a, $b) {
return strtotime($a->getCreationDate()) <=> strtotime($b->getCreationDate()); // Älteste zuerst
});
} }
// Session dient hier (wie bei search-results) als Zwischenspeicher, damit // Paginierung konfigurieren
// js/showCategory.js den kompletten Datensatz per showCategory-data.php $limit = 10;
// nachladen kann, ohne die Kategorie-Abfrage ein zweites Mal auszuführen. $totalPages = ceil($totalArticles / $limit);
$_SESSION['category_results'] = $safeArrayResults; if ($page > $totalPages && $totalPages > 0) { $page = $totalPages; }
$_SESSION['category_name'] = $category;
$offset = ($page - 1) * $limit;
$articles = array_slice($allArticles, $offset, $limit);
} catch (Exception $e) { } catch (Exception $e) {
$_SESSION["message"] = "internal_error"; $_SESSION["message"] = "internal_error";
include_once "content/404.php"; include_once "content/404.php";
exit(); exit();
} }
} else { } else {
$_SESSION["message"] = "invalid_category"; $_SESSION["message"] = "invalid_category";
include_once "content/404.php"; include_once "content/404.php";
exit(); exit();
} }
?>
-35
View File
@@ -1,35 +0,0 @@
<?php
/**
* Liefert den vollständigen, zuletzt geladenen Kategorie-Datensatz als JSON.
*
* Wird von js/showCategory.js EINMALIG per fetch() geladen, damit Sortierung,
* Suche und Pagination danach komplett im Browser laufen können, ohne
* weitere Serveranfragen oder eine erneute Datenbank-Abfrage auszulösen
*
* Analog zu php/controller/search-results-data.php
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../includes/resultsHelper.php';
$rawResults = $_SESSION['category_results'] ?? [];
$payload = array_map(function ($item) {
return [
'id' => $item['id'],
'title' => $item['title'],
'content' => $item['content'] ?? '',
'author' => $item['author'],
'category' => $item['category'] ?? '',
'likes' => getLikeCount($item),
];
}, $rawResults);
echo json_encode([
'category' => $_SESSION['category_name'] ?? '',
'results' => array_values($payload),
]);
+84 -103
View File
@@ -7,27 +7,21 @@ 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';
require_once '../../includes/csrf.php'; // NEU: CSRF-Schutz
if (!isset($_SESSION["user"])) { if (!isset($_SESSION["user_email"])) {
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"] ?? '';
// CSRF-Token prüfen, bevor irgendeine Änderung vorgenommen wird if (isset($_GET["id"]) && !empty($_GET["id"])) {
if (!csrf_verify()) { $id = $_GET["id"];
$_SESSION["message"] = "invalid_csrf_token"; } else {
header("location: ../../index.php?pfad=updateArticle");
exit();
}
// Die Beitrags-ID muss eine gültige numerische ID sein
$id = filter_input(INPUT_GET, "id", FILTER_VALIDATE_INT);
if ($id === false || $id === null) {
$_SESSION["message"] = "missing_id"; $_SESSION["message"] = "missing_id";
header("location: ../../index.php?pfad=updateArticle"); header("location: ../../index.php?pfad=updateArticle");
exit(); exit();
@@ -36,14 +30,6 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$article = $articleManager->getArticle($id); $article = $articleManager->getArticle($id);
// Existenz des Beitrags prüfen, bevor auf $article zugegriffen wird.
if ($article === null) {
$_SESSION["message"] = "missing_id";
header("location: ../../index.php?pfad=updateArticle");
exit();
}
if ($article->getAuthor() != $_SESSION["user_email"]) { if ($article->getAuthor() != $_SESSION["user_email"]) {
$_SESSION["message"] = "unauthorized_access"; $_SESSION["message"] = "unauthorized_access";
header("location: ../../index.php"); header("location: ../../index.php");
@@ -55,56 +41,16 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
exit(); exit();
} }
$uploadDir = __DIR__ . '/../../uploads/'; if (!isset($_POST["title"]) ||!isset($_POST["content"]) || !isset($_POST["category"])){
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 = json_encode($blocks, JSON_UNESCAPED_UNICODE); $content = $_POST["content"];
$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)) {
@@ -142,56 +88,91 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$cleanedTags = implode(',', $cleanedTags); $cleanedTags = implode(',', $cleanedTags);
} }
// ----------------- Verwaiste Bilder aufräumen ----------------- // --------------------------------------- Base64-Bilder speichern ---------------------------------------------
// Bilder, die im alten (gespeicherten) Content vorkamen, im neuen aber nicht $blocks = json_decode($content, true);
// mehr referenziert werden, wurden vom Nutzer entfernt oder ersetzt -> löschen. $uploadDir = __DIR__ . '/../../uploads/';
// Hinweis/TODO: Bilder, die innerhalb derselben Bearbeitungs-Sitzung neu
// hochgeladen und noch vor dem finalen Speichern wieder entfernt wurden, if (!file_exists($uploadDir)) {
// werden hierüber nicht erfasst (sie tauchten nie im alten Content auf) und mkdir($uploadDir, 0755, true);
// 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. // ----------------- Gelöschte Bilder über die JS-Löschliste entfernen ----------------- TODO: Gelöschte Bilder über die JS-Löschliste entfernen
$oldBlocks = json_decode($article->getContent(), true); /*if (isset($_POST['deleted_images'])) {
$oldImagePaths = []; $deletedImages = json_decode($_POST['deleted_images'], true);
if (is_array($oldBlocks)) {
foreach ($oldBlocks as $oldBlock) { // Wir ermitteln den physisch echten, absoluten Pfad zum uploads-Ordner auf der Festplatte
if (($oldBlock['type'] ?? '') === 'image' $uploadDir = realpath(__DIR__ . '/../../uploads') . DIRECTORY_SEPARATOR;
&& !empty($oldBlock['value'])
&& is_string($oldBlock['value']) if (is_array($deletedImages)) {
&& str_starts_with($oldBlock['value'], 'uploads/')) { foreach ($deletedImages as $imagePath) {
$oldImagePaths[] = $oldBlock['value']; // 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);
}
} }
} }
}*/
// ----------------------- 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);
} }
$newImagePaths = []; // Aktualisiertes Array wieder in JSON konvertieren
foreach ($blocks as $block) { $finalContent = json_encode($blocks, JSON_UNESCAPED_UNICODE);
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($content); $article->setContent($finalContent);
$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();
-101
View File
@@ -1,101 +0,0 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once "php/model/CommentManager.php";
require_once "includes/csrf.php";
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: index.php");
exit();
}
if (!isset($_SESSION["user_email"])) {
header("Location: index.php?pfad=login");
exit();
}
// CSRF-Token prüfen, bevor irgendeine Änderung vorgenommen wird
if (!csrf_verify()) {
$_SESSION["comment_message"] = "Deine Sitzung ist abgelaufen. Bitte lade die Seite neu.";
$_SESSION["comment_message_type"] = "error";
header("Location: index.php");
exit();
}
$commentId = filter_input(
INPUT_POST,
"comment_id",
FILTER_VALIDATE_INT
);
$articleId = filter_input(
INPUT_POST,
"article_id",
FILTER_VALIDATE_INT
);
$content = trim($_POST["content"] ?? "");
/*
* Ohne gültige Beitrags-ID kann nicht sicher
* zum ursprünglichen Beitrag zurückgeleitet werden.
*/
if (!$articleId) {
header("Location: index.php");
exit();
}
/*
* Kommentar-ID und Inhalt werden geprüft.
*/
if (!$commentId || $content === "") {
$_SESSION["comment_message"] =
"Der Kommentar darf nicht leer sein.";
$_SESSION["comment_message_type"] = "error";
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
exit();
}
try {
$commentManager = CommentManager::getInstance();
$updated = $commentManager->updateComment(
$commentId,
$_SESSION["user_email"],
$content
);
if ($updated) {
$_SESSION["comment_message"] =
"Der Kommentar wurde erfolgreich bearbeitet.";
$_SESSION["comment_message_type"] = "success";
} else {
$_SESSION["comment_message"] =
"Der Kommentar konnte nicht bearbeitet werden.";
$_SESSION["comment_message_type"] = "error";
}
} catch (Throwable $e) {
$_SESSION["comment_message"] =
"Beim Bearbeiten des Kommentars ist ein Fehler aufgetreten.";
$_SESSION["comment_message_type"] = "error";
}
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
exit();
+5 -5
View File
@@ -13,7 +13,7 @@ class Comment
private int $id; private int $id;
private int $articleId; private int $articleId;
private ?int $parentCommentId; private ?int $parentCommentId;
private ?string $author; private string $author;
private string $content; private string $content;
private string $created; private string $created;
@@ -23,7 +23,7 @@ class Comment
* @param int $id Eindeutige ID des Kommentars * @param int $id Eindeutige ID des Kommentars
* @param int $articleId ID des zugehörigen Beitrags * @param int $articleId ID des zugehörigen Beitrags
* @param int|null $parentCommentId ID des Eltern-Kommentars oder null * @param int|null $parentCommentId ID des Eltern-Kommentars oder null
* @param string|null $author Autor des Kommentars oder null bei gelöschten Kommentaren * @param string $author Autor des Kommentars
* @param string $content Inhalt des Kommentars * @param string $content Inhalt des Kommentars
* @param string $created Erstellungsdatum des Kommentars * @param string $created Erstellungsdatum des Kommentars
*/ */
@@ -31,7 +31,7 @@ class Comment
int $id, int $id,
int $articleId, int $articleId,
?int $parentCommentId, ?int $parentCommentId,
?string $author, string $author,
string $content, string $content,
string $created string $created
) { ) {
@@ -86,9 +86,9 @@ class Comment
/** /**
* Gibt den Autor des Kommentars zurück. * Gibt den Autor des Kommentars zurück.
* *
* @return string|null Autor oder null bei gelöschten Kommentaren * @return string Autor
*/ */
public function getAuthor(): ?string public function getAuthor(): string
{ {
return $this->author; return $this->author;
} }
-36
View File
@@ -51,40 +51,4 @@ interface CommentManagerDAO
* @return Comment[] Liste der Kommentare * @return Comment[] Liste der Kommentare
*/ */
public function getCommentsByAuthor($author); public function getCommentsByAuthor($author);
/**
* Löscht einen einzelnen Kommentar des angemeldeten Nutzers.
*
* Kommentare ohne Antworten werden vollständig entfernt.
* Kommentare mit Antworten bleiben als anonymer Platzhalter erhalten.
*
* @param int $commentId ID des Kommentars
* @param string $author E-Mail-Adresse des Autors
* @return bool true, wenn der Kommentar gefunden und gelöscht wurde
*/
public function deleteComment(int $commentId, string $author): bool;
/**
* Löscht beziehungsweise anonymisiert alle Kommentare eines Nutzers.
*
* Diese Methode wird bei der Löschung eines Benutzerkontos verwendet.
*
* @param string $author E-Mail-Adresse des Nutzers
* @return void
*/
public function deleteCommentsByAuthor(string $author): void;
/**
* Bearbeitet einen Kommentar des angemeldeten Nutzers.
*
* Nur der Autor des Kommentars darf den Inhalt ändern.
*
* @param int $commentId ID des Kommentars
* @param string $author E-Mail-Adresse des Autors
* @param string $content Neuer Kommentarinhalt
* @return bool true, wenn der Kommentar bearbeitet wurde
*/
public function updateComment(
int $commentId,
string $author,
string $content
): bool;
} }
+51 -13
View File
@@ -63,9 +63,12 @@ class DatabaseArticleManager implements ArticleManagerDAO {
VALUES (:title, :content, :author, :category, :tags);"; VALUES (:title, :content, :author, :category, :tags);";
$command = $db->prepare($sql); $command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
// Verknüpft die übergebenen Parameter exakt mit den SQL-Platzhaltern // Verknüpft die übergebenen Parameter exakt mit den SQL-Platzhaltern
$command->execute([ $success = $command->execute([
":title" => $title, ":title" => $title,
":content" => $content, ":content" => $content,
":author" => $author, ":author" => $author,
@@ -73,10 +76,14 @@ class DatabaseArticleManager implements ArticleManagerDAO {
":tags" => $tags ":tags" => $tags
]); ]);
if (!$success) {
throw new InternalServerErrorException("internal_error");
}
return intval($db->lastInsertId()); return intval($db->lastInsertId());
} catch (PDOException $e) { } catch (PDOException $e) {
throw new InternalServerErrorException("internal_error"); throw new InternalServerErrorException($e->getMessage());
} }
} }
@@ -99,8 +106,11 @@ class DatabaseArticleManager implements ArticleManagerDAO {
WHERE id = :id;"; WHERE id = :id;";
$command = $db->prepare($sql); $command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
$command->execute([ $success = $command->execute([
":id" => $id, ":id" => $id,
":title" => $article->getTitle(), ":title" => $article->getTitle(),
":content" => $article->getContent(), ":content" => $article->getContent(),
@@ -110,7 +120,7 @@ class DatabaseArticleManager implements ArticleManagerDAO {
]); ]);
// rowCount() prüft, ob eine Zeile mit dieser ID existierte und geändert werden konnte // rowCount() prüft, ob eine Zeile mit dieser ID existierte und geändert werden konnte
if ($command->rowCount() === 0) { if (!$success || $command->rowCount() === 0) {
// Falls die ID nicht existiert, prüfen wir, ob sie überhaupt da ist // Falls die ID nicht existiert, prüfen wir, ob sie überhaupt da ist
if (!$this->getArticle($id)) { if (!$this->getArticle($id)) {
throw new NotFoundException("missing_id"); throw new NotFoundException("missing_id");
@@ -138,7 +148,13 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$sql = "DELETE FROM articles WHERE id = :id;"; $sql = "DELETE FROM articles WHERE id = :id;";
$command = $db->prepare($sql); $command = $db->prepare($sql);
$command->execute([":id" => $id]); if (!$command) {
throw new InternalServerErrorException("internal_error");
}
if (!$command->execute([":id" => $id])) {
throw new InternalServerErrorException("internal_error");
}
} catch (PDOException $exc) { } catch (PDOException $exc) {
throw new InternalServerErrorException("internal_error"); throw new InternalServerErrorException("internal_error");
} }
@@ -151,6 +167,10 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$sql = "SELECT * FROM articles WHERE id = :id;"; $sql = "SELECT * FROM articles WHERE id = :id;";
$command = $db->prepare($sql); $command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
$command->execute([":id" => $id]); $command->execute([":id" => $id]);
$row = $command->fetch(PDO::FETCH_ASSOC); $row = $command->fetch(PDO::FETCH_ASSOC);
@@ -182,6 +202,10 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$sql = "SELECT * FROM articles;"; $sql = "SELECT * FROM articles;";
$command = $db->query($sql); $command = $db->query($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
$rows = $command->fetchAll(PDO::FETCH_ASSOC); $rows = $command->fetchAll(PDO::FETCH_ASSOC);
$articles = []; $articles = [];
@@ -210,6 +234,10 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$sql = "SELECT * FROM articles WHERE author = :author;"; $sql = "SELECT * FROM articles WHERE author = :author;";
$command = $db->prepare($sql); $command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
$command->execute([":author" => $author]); $command->execute([":author" => $author]);
$rows = $command->fetchAll(PDO::FETCH_ASSOC); $rows = $command->fetchAll(PDO::FETCH_ASSOC);
$filteredArticles = []; $filteredArticles = [];
@@ -242,6 +270,10 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$sql = "SELECT * FROM articles WHERE category = :category;"; $sql = "SELECT * FROM articles WHERE category = :category;";
$command = $db->prepare($sql); $command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
$command->execute([":category" => $category]); $command->execute([":category" => $category]);
$rows = $command->fetchAll(PDO::FETCH_ASSOC); $rows = $command->fetchAll(PDO::FETCH_ASSOC);
$filteredArticles = []; $filteredArticles = [];
@@ -280,29 +312,35 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$db = $this->getConnection(); $db = $this->getConnection();
$sql = "SELECT id, title, content, author, category, tags, created $sql = "SELECT id, title, content, author, category, tags, created
FROM articles FROM articles
WHERE title LIKE :keyword WHERE title LIKE :keyword
OR content LIKE :keyword OR content LIKE :keyword
OR tags LIKE :keyword;"; OR tags LIKE :keyword";
$command = $db->prepare($sql); $command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
// Wildcards für die Suche hinzufügen // Wildcards für die Suche hinzufügen
$searchParam = '%' . $cleankeyword . '%'; $searchParam = '%' . $cleankeyword . '%';
$command->execute([ $success = $command->execute([
":keyword" => $searchParam ":keyword" => $searchParam
]); ]);
if (!$success) {
throw new InternalServerErrorException("internal_error");
}
$rows = $command->fetchAll(PDO::FETCH_ASSOC); $rows = $command->fetchAll(PDO::FETCH_ASSOC);
$filteredArticles = []; $filteredArticles = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$articleId = intval($row['id']); $likes = $this->getLikesForArticle(intval($row['id']));
$likes = $this->getLikesForArticle($articleId);
$filteredArticles[] = new Article( $filteredArticles[] = new Article(
$articleId, intval($row['id']),
$row['title'] ?? '', $row['title'] ?? '',
$row['content'] ?? '', $row['content'] ?? '',
$row['author'] ?? '', $row['author'] ?? '',
+1 -191
View File
@@ -17,7 +17,7 @@ class DatabaseCommentManager implements CommentManagerDAO
/** /**
* Erstellt die Kommentartabelle, falls diese noch nicht existiert. * Erstellt die Kommentartabelle, falls diese noch nicht existiert.
*/ */
private function __construct() public function __construct()
{ {
$this->dbPath = __DIR__ . '/../../db/eduforgeDB.db'; $this->dbPath = __DIR__ . '/../../db/eduforgeDB.db';
DatabaseInitializer::initialize($this->dbPath); DatabaseInitializer::initialize($this->dbPath);
@@ -223,194 +223,4 @@ class DatabaseCommentManager implements CommentManagerDAO
return $comments; return $comments;
} }
/**
* Bearbeitet einen eigenen Kommentar.
*
* Der Kommentar wird nur geändert, wenn er dem
* angemeldeten Nutzer gehört.
*
* @param int $commentId ID des Kommentars
* @param string $author E-Mail-Adresse des Autors
* @param string $content Neuer Kommentarinhalt
* @return bool true, wenn der Kommentar bearbeitet wurde
*/
public function updateComment(
int $commentId,
string $author,
string $content
): bool {
try {
$db = $this->getConnection();
$command = $db->prepare("
UPDATE comments
SET content = :content
WHERE id = :commentId
AND author = :author
");
$command->execute([
":content" => $content,
":commentId" => $commentId,
":author" => $author
]);
return $command->rowCount() > 0;
} catch (PDOException $e) {
throw new RuntimeException("internal_error");
}
}
/**
* Löscht einen eigenen Kommentar.
*
* Hat der Kommentar Antworten, wird er anonymisiert.
* Hat er keine Antworten, wird er vollständig gelöscht.
*
* @param int $commentId ID des Kommentars
* @param string $author E-Mail-Adresse des Autors
* @return bool true, wenn der Kommentar gelöscht wurde
*/
public function deleteComment(int $commentId, string $author): bool
{
try {
$db = $this->getConnection();
/*
* Zuerst wird geprüft, ob der Kommentar existiert
* und wirklich dem angemeldeten Nutzer gehört.
*/
$checkCommand = $db->prepare("
SELECT id
FROM comments
WHERE id = :commentId
AND author = :author
");
$checkCommand->execute([
":commentId" => $commentId,
":author" => $author
]);
if ($checkCommand->fetch() === false) {
return false;
}
/*
* Danach wird geprüft, ob Antworten auf den Kommentar existieren.
*/
$replyCommand = $db->prepare("
SELECT COUNT(*)
FROM comments
WHERE parent_comment_id = :commentId
");
$replyCommand->execute([
":commentId" => $commentId
]);
$hasReplies = (int) $replyCommand->fetchColumn() > 0;
if ($hasReplies) {
/*
* Der Kommentar wird für den Kommentarbaum benötigt.
* Deshalb bleibt er als anonymer Platzhalter erhalten.
*/
$deleteCommand = $db->prepare("
UPDATE comments
SET author = NULL,
content = 'Dieser Kommentar wurde gelöscht.'
WHERE id = :commentId
AND author = :author
");
} else {
/*
* Ohne Antworten kann der Kommentar vollständig
* aus der Datenbank entfernt werden.
*/
$deleteCommand = $db->prepare("
DELETE FROM comments
WHERE id = :commentId
AND author = :author
");
}
$deleteCommand->execute([
":commentId" => $commentId,
":author" => $author
]);
return $deleteCommand->rowCount() > 0;
} catch (PDOException $e) {
throw new RuntimeException("internal_error");
}
}
/**
* Löscht beziehungsweise anonymisiert alle Kommentare eines Nutzers.
*
* Kommentare ohne Antworten werden vollständig gelöscht.
* Kommentare mit Antworten bleiben als anonyme Platzhalter erhalten.
*
* @param string $author E-Mail-Adresse des Nutzers
* @return void
*/
public function deleteCommentsByAuthor(string $author): void
{
$db = $this->getConnection();
try {
$db->beginTransaction();
/*
* Zuerst werden alle Kommentare ohne Antworten gelöscht.
*
* Die Schleife ist wichtig, weil durch das Löschen einer Antwort
* eventuell auch der darüberliegende Kommentar keine Antworten
* mehr besitzt und anschließend ebenfalls gelöscht werden kann.
*/
do {
$deleteCommand = $db->prepare("
DELETE FROM comments
WHERE author = :author
AND NOT EXISTS (
SELECT 1
FROM comments AS replies
WHERE replies.parent_comment_id = comments.id
)
");
$deleteCommand->execute([
":author" => $author
]);
$deletedRows = $deleteCommand->rowCount();
} while ($deletedRows > 0);
/*
* Kommentare, auf die noch Antworten anderer Nutzer folgen,
* müssen für den Kommentarbaum erhalten bleiben.
*/
$placeholderCommand = $db->prepare("
UPDATE comments
SET author = NULL,
content = 'Dieser Kommentar wurde gelöscht.'
WHERE author = :author
");
$placeholderCommand->execute([
":author" => $author
]);
$db->commit();
} catch (PDOException $e) {
if ($db->inTransaction()) {
$db->rollBack();
}
throw new RuntimeException("internal_error");
}
}
} }
+13 -18
View File
@@ -46,17 +46,17 @@ class DatabaseInitializer {
$db->exec(" $db->exec("
CREATE TABLE IF NOT EXISTS comments ( CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id INTEGER NOT NULL, article_id INTEGER NOT NULL,
parent_comment_id INTEGER NULL, parent_comment_id INTEGER NULL,
author TEXT NULL, author TEXT NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE, FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
FOREIGN KEY (author) REFERENCES users(email) ON DELETE SET NULL, FOREIGN KEY (author) REFERENCES users(email) ON DELETE CASCADE,
FOREIGN KEY (parent_comment_id) REFERENCES comments(id) ON DELETE CASCADE FOREIGN KEY (parent_comment_id) REFERENCES comments(id) ON DELETE CASCADE
); );
"); ");
$initializer = new self(); $initializer = new self();
$availableEmails = $initializer->seedDummyUsers($db); $availableEmails = $initializer->seedDummyUsers($db);
@@ -234,18 +234,13 @@ 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' => $content, ':content' => $article[1],
':author' => $randomAuthor, ':author' => $randomAuthor,
':category' => $article[2], ':category' => $article[2],
':tags' => $article[3] ':tags' => $article[3]
]); ]);
} }
} }
} }
-15
View File
@@ -1,20 +1,5 @@
<?php <?php
/**
* NEU: Prüft, ob ein übergebener Wert eine gültige, positive
* Beitrags-ID ist. Wird überall dort verwendet, wo eine Artikel-ID
* aus $_GET oder $_POST entgegengenommen wird
*
* @param mixed $id
* @return int|false Die validierte ID als int, oder false bei Ungültigkeit
*/
function articleIdValidator($id)
{
$options = ["options" => ["min_range" => 1]];
return filter_var($id, FILTER_VALIDATE_INT, $options);
}
/** /**
* Prüft, ob der Titel die folgenden Bedingungen erfüllt: * Prüft, ob der Titel die folgenden Bedingungen erfüllt:
* Buchstaben von a-z; A-Z * Buchstaben von a-z; A-Z