Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75b4aa0b88 | |||
| 9146d395a2 | |||
| f957f252d1 | |||
| b3f7db2615 | |||
| 12369a5a00 | |||
| 652bcc5474 | |||
| c22853d802 | |||
| 4abc5f4167 | |||
| 09f8b11f36 | |||
| cc4cebdb45 | |||
| 9f4343cc53 | |||
| a5998cffca | |||
| 153a612ad6 | |||
| 437da98add | |||
| a4b0dfe7ec | |||
| 3a819d6ed2 | |||
| 4f4ad9599d | |||
| d9da018ac2 | |||
| 2148ba75f3 | |||
| c4ffc1c797 | |||
| 96f38ce72e | |||
| b81f0c9e5c | |||
| f8aed4283e | |||
| a785d862d8 | |||
| a728a8a556 | |||
| ae34afda15 | |||
| 59e0b9f111 | |||
| cd50840b19 | |||
| 3bce65a1a5 | |||
| 50b29c96d6 | |||
| d5fc5f3065 | |||
| 0427d52a58 | |||
| cc804757ff | |||
| b55a6592f0 | |||
| e12a98cc98 | |||
| 320141855a | |||
| 12801344f1 | |||
| 83387de103 | |||
| dfb8e1dc89 | |||
| bdbc38a339 | |||
| e6356525b9 | |||
| ca74690952 | |||
| 75f9fdbeb8 | |||
| 81a27135d5 | |||
| a58576637a | |||
| 997957a5d3 | |||
| ec794db706 | |||
| ba749bca68 | |||
| 12a6af55b0 | |||
| 400f018104 | |||
| 80f4b623f9 | |||
| 6546d264b0 | |||
| e7e120e5b8 | |||
| ec66889a1f | |||
| f1703a476e | |||
| bac296b58a | |||
| bfb2d2b0d8 | |||
| ad9a00fd49 | |||
| 42faab17e8 | |||
| 1da4842847 | |||
| afd90086d8 | |||
| a7a54f877c | |||
| 9b9f71d21c | |||
| 4b84f0ab09 | |||
| 8c8b148bf4 | |||
| 6bf335ce46 | |||
| 144a1015f1 | |||
| c0a94b2eee | |||
| f1a32120d6 | |||
| 97fa1de999 | |||
| 7341fe7733 | |||
| 5b0528a914 | |||
| ac64e65dd3 | |||
| 96ffa3288e | |||
| 0c6cf13efa | |||
| 3867458050 | |||
| f752150188 | |||
| 325df51812 | |||
| 97afd1ea06 | |||
| e3903b4f3e | |||
| 031f2afd25 | |||
| d3884d2fc1 | |||
| 1575f4fa24 | |||
| 0aca76d2f4 | |||
| eca6646071 | |||
| 7189133861 | |||
| 8c961a56ce | |||
| 4f95a3d423 | |||
| 35e5006063 | |||
| fcb9289d80 | |||
| 6595a3d78d | |||
| 41d8ac6721 | |||
| 63c213bae7 | |||
| 61d72436c9 | |||
| 721bc8d833 | |||
| 2666d2d14f | |||
| de1ba88860 | |||
| e0fb04794d | |||
| 5310083e11 | |||
| 6897a29574 | |||
| b564306fb3 | |||
| 8836034670 | |||
| 98b088afc0 | |||
| 51c850d122 | |||
| d590561ade | |||
| ba11c216bd | |||
| 71a1b029cb | |||
| 2126d076d5 | |||
| 7b7208ad6b | |||
| 2ecd446dd0 | |||
| 97302f1bb3 | |||
| 35d4425086 | |||
| d3435d4cde | |||
| f61c548b9e | |||
| 101a2a247b | |||
| 423941e7ed | |||
| a8e76c782d | |||
| 3f9c048493 | |||
| fee5a701df | |||
| d341312192 | |||
| 4dadfb8863 | |||
| ada97ec538 | |||
| 0b4c5f988d | |||
| 59eeb05e4b | |||
| 5d8239822a | |||
| 31585511d0 | |||
| 2ec1b70c91 | |||
| 8f8b25249c | |||
| 8be6e6d61c | |||
| bad0c08e6f | |||
| 55695f3f82 | |||
| 74cbc60cbe | |||
| c09c7ecfa3 | |||
| 944e332b49 | |||
| 4306785be9 | |||
| 266b70a094 | |||
| bd8593d212 | |||
| 0d009262b6 | |||
| bb2c368b1f | |||
| a712f5a993 | |||
| 53bee08833 | |||
| 7a2a3ac1e9 | |||
| a5f02c9a6c | |||
| f77f09ec6f | |||
| 535df85cff | |||
| 82e8c8a1a3 | |||
| c0d8fdede6 | |||
| 958ee1c1f5 | |||
| 1dd128d480 | |||
| dca716ddc5 | |||
| c1dd4fce2b | |||
| 16cfa92721 | |||
| 3b2ed78c1c | |||
| cebbe8869a | |||
| e07051b356 | |||
| 2d2bbeb39f | |||
| c00443b6fa | |||
| 1d541e285c |
@@ -36,6 +36,9 @@
|
|||||||
- 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
|
||||||
|
|||||||
+2
-7
@@ -4,17 +4,12 @@
|
|||||||
-->
|
-->
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "internal_error"): ?>
|
<?php include_once "includes/alertMessages.php"?>
|
||||||
<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"]);
|
||||||
?>
|
?>
|
||||||
<h1>404 - Seite nicht vorhanden</h1>
|
<h1>404 - Seite nicht vorhanden</h1>
|
||||||
<p>
|
<p>
|
||||||
Später im Projekt sollen über index.php?pfad= ... der Inhalt der index.php dynamisch gesetzt werden.
|
Die Seite, nach der du suchst, existiert hier leider nicht. Entweder haben wir sie verlegt, ein hungriger Bug hat sie gefressen, oder du hast dich beim Tippen vertippt. (Wir schieben die Schuld einfach mal ganz unauffällig auf dich.)
|
||||||
Alle Content-Links der Navbar führen zunächst hier her. Exemplarisch wurden für die Fächer Informatik, Physik, Mathematik der Content als Seite erstellt und kann über die Navbar ausgekundschaftet werden.
|
|
||||||
</p>
|
</p>
|
||||||
</main>
|
</main>
|
||||||
+59
-19
@@ -4,12 +4,30 @@ if (!isset($_SESSION["user"])) {
|
|||||||
header("Location: index.php?pfad=login");
|
header("Location: index.php?pfad=login");
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Aktuelle Blockliste ermitteln: Session-Fallback (nach Validierungsfehler
|
||||||
|
// oder Zwischen-Submit "add_block"/"delete_block") oder leeres Array (Start).
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
$rawContent = '[]';
|
||||||
|
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])) {
|
||||||
|
$rawContent = $_SESSION['old_content'];
|
||||||
|
unset($_SESSION['old_content']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$blocks = json_decode($rawContent, true);
|
||||||
|
if (!is_array($blocks)) {
|
||||||
|
$blocks = [];
|
||||||
|
}
|
||||||
|
$blocks = array_values($blocks); // sequentielle Indizes 0..n-1 sicherstellen
|
||||||
?>
|
?>
|
||||||
<!--
|
<!--
|
||||||
Seite: Beitrag erstellen
|
Seite: Beitrag erstellen
|
||||||
Inhalt: Formular für die Erstellung eines neuen Beitrags
|
Inhalt: Formular für die Erstellung eines neuen Beitrags
|
||||||
-->
|
-->
|
||||||
<form method="post" action="php/controller/createArticle-controller.php" id="editor-form" class="article-editor-scope.editor-container article-editor-scope editor-container">
|
<form method="post" action="php/controller/createArticle-controller.php" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container">
|
||||||
|
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
|
||||||
<main class="editor-main">
|
<main class="editor-main">
|
||||||
<?php include_once "includes/alertMessages.php"?>
|
<?php include_once "includes/alertMessages.php"?>
|
||||||
@@ -18,27 +36,49 @@ if (!isset($_SESSION["user"])) {
|
|||||||
value="<?php echo htmlspecialchars($_SESSION['old_title'] ?? ''); unset($_SESSION['old_title']); ?>"
|
value="<?php echo htmlspecialchars($_SESSION['old_title'] ?? ''); unset($_SESSION['old_title']); ?>"
|
||||||
placeholder="Titel hier eingeben" required>
|
placeholder="Titel hier eingeben" required>
|
||||||
|
|
||||||
<!-- Hier werden die dynamischen divs via JavaScript eingefügt -->
|
<!--
|
||||||
<div id="block-container"></div>
|
Content-Blöcke: werden serverseitig als echte, benannte Formularfelder gerendert
|
||||||
|
(blocks[i][type], blocks[i][text] bzw. blocks[i][image]). Dadurch funktioniert das
|
||||||
|
Hinzufügen/Entfernen von Blöcken und der Bild-Upload auch ganz ohne JavaScript über
|
||||||
|
einen normalen Formular-Submit. Ist JavaScript aktiv, fängt js/editor.js diese
|
||||||
|
Submits ab und erledigt dieselbe Änderung lokal im DOM, ohne den Server zu belasten.
|
||||||
|
-->
|
||||||
|
<div id="block-container">
|
||||||
|
<?php foreach ($blocks as $i => $block): ?>
|
||||||
|
<?php
|
||||||
|
$blockType = $block['type'] ?? '';
|
||||||
|
if ($blockType !== 'text' && $blockType !== 'image') {
|
||||||
|
continue; // unbekannter/kaputter Block wird übersprungen
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="editor-block article-editor-scope" data-index="<?php echo (int)$i; ?>">
|
||||||
|
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][type]" value="<?php echo htmlspecialchars($blockType); ?>">
|
||||||
|
|
||||||
<!-- Plus-Button und das Pop-up-Menü -->
|
<?php if ($blockType === 'text'): ?>
|
||||||
<div id="add-block-control" class="article-editor-scope add-block-control">
|
<textarea name="blocks[<?php echo (int)$i; ?>][text]"
|
||||||
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
|
placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
|
||||||
<div id="block-popup" class="article-editor-scope block-popup hidden">
|
<?php else: /* image */ ?>
|
||||||
<button type="button" data-type="text">Textblock</button>
|
<?php if (!empty($block['value'])): ?>
|
||||||
<button type="button" data-type="image">Bild einfügen</button>
|
<img src="<?php echo htmlspecialchars($block['value']); ?>"
|
||||||
</div>
|
class="block-image-preview"
|
||||||
|
style="max-width:200px;display:block;margin-top:10px;">
|
||||||
|
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][existing_image]" value="<?php echo htmlspecialchars($block['value']); ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
<input type="file" name="blocks[<?php echo (int)$i; ?>][image]" accept="image/*">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<button type="submit" name="editor_action" value="delete_block:<?php echo (int)$i; ?>" class="delete-block-btn">✕</button>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Unsichtbares Textfeld, das die JSON-Daten hält und an den Controller postet -->
|
<div id="add-block-control" class="article-editor-scope add-block-control">
|
||||||
<textarea id="content" name="content" style="display:none;"><?php
|
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
|
||||||
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){
|
<div id="block-popup" class="article-editor-scope block-popup">
|
||||||
echo htmlspecialchars($_SESSION['old_content']);
|
<button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
|
||||||
unset($_SESSION['old_content']);
|
<button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
|
||||||
} else {
|
</div>
|
||||||
echo '[]'; // Standardmäßig ein leeres JSON-Array
|
</div>
|
||||||
}
|
|
||||||
?></textarea>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Seitenleiste -->
|
<!-- Seitenleiste -->
|
||||||
|
|||||||
+227
-181
@@ -2,188 +2,234 @@
|
|||||||
Seite: Datenschutzerklärung
|
Seite: Datenschutzerklärung
|
||||||
Inhalt: Informationen zur Datenverarbeitung gemäß DSGVO
|
Inhalt: Informationen zur Datenverarbeitung gemäß DSGVO
|
||||||
-->
|
-->
|
||||||
<main>
|
|
||||||
|
|
||||||
<h1>Datenschutz­erklärung</h1>
|
<main>
|
||||||
<h2>1. Datenschutz auf einen Blick</h2>
|
|
||||||
<h3>Allgemeine Hinweise</h3> <p>Die folgenden Hinweise geben einen einfachen Überblick darüber,
|
|
||||||
was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind
|
|
||||||
alle Daten, mit denen Sie persönlich identifiziert werden können. Ausführliche Informationen zum
|
|
||||||
Thema Datenschutz entnehmen Sie unserer unter diesem Text aufgeführten
|
|
||||||
Datenschutzerklärung.</p>
|
|
||||||
<h3>Datenerfassung auf dieser Website</h3> <h4>Wer ist verantwortlich für die Datenerfassung auf dieser
|
|
||||||
Website?</h4> <p>Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen
|
|
||||||
Kontaktdaten können Sie dem Abschnitt „Hinweis zur verantwortlichen Stelle“ in dieser
|
|
||||||
Datenschutzerklärung entnehmen.</p> <h4>Wie erfassen wir Ihre Daten?</h4> <p>Ihre Daten werden zum
|
|
||||||
einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z. B. um Daten handeln, die Sie in ein
|
|
||||||
Kontaktformular eingeben.</p> <p>Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch
|
|
||||||
der Website durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z. B. Internetbrowser,
|
|
||||||
Betriebssystem oder Uhrzeit des Seitenaufrufs). Die Erfassung dieser Daten erfolgt automatisch, sobald Sie diese
|
|
||||||
Website betreten.</p> <h4>Wofür nutzen wir Ihre Daten?</h4> <p>Ein Teil der Daten wird erhoben, um eine
|
|
||||||
fehlerfreie Bereitstellung der Website zu gewährleisten. Andere Daten können zur Analyse Ihres
|
|
||||||
Nutzerverhaltens verwendet werden. Sofern über die Website Verträge geschlossen oder angebahnt
|
|
||||||
werden können, werden die übermittelten Daten auch für Vertragsangebote, Bestellungen oder
|
|
||||||
sonstige Auftragsanfragen verarbeitet.</p> <h4>Welche Rechte haben Sie bezüglich Ihrer Daten?</h4>
|
|
||||||
<p>Sie haben jederzeit das Recht, unentgeltlich Auskunft über Herkunft, Empfänger und Zweck Ihrer
|
|
||||||
gespeicherten personenbezogenen Daten zu erhalten. Sie haben außerdem ein Recht, die Berichtigung oder
|
|
||||||
Löschung dieser Daten zu verlangen. Wenn Sie eine Einwilligung zur Datenverarbeitung erteilt haben,
|
|
||||||
können Sie diese Einwilligung jederzeit für die Zukunft widerrufen. Außerdem haben Sie das Recht,
|
|
||||||
unter bestimmten Umständen die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu
|
|
||||||
verlangen. Des Weiteren steht Ihnen ein Beschwerderecht bei der zuständigen Aufsichtsbehörde
|
|
||||||
zu.</p> <p>Hierzu sowie zu weiteren Fragen zum Thema Datenschutz können Sie sich jederzeit an uns
|
|
||||||
wenden.</p>
|
|
||||||
<h2>2. Hosting</h2>
|
|
||||||
<p>Wir hosten die Inhalte unserer Website bei folgendem Anbieter:</p>
|
|
||||||
<h3>Externes Hosting</h3> <p>Diese Website wird extern gehostet. Die personenbezogenen Daten, die auf dieser
|
|
||||||
Website erfasst werden, werden auf den Servern des Hosters / der Hoster gespeichert. Hierbei kann es sich
|
|
||||||
v. a. um IP-Adressen, Kontaktanfragen, Meta- und Kommunikationsdaten, Vertragsdaten, Kontaktdaten,
|
|
||||||
Namen, Websitezugriffe und sonstige Daten, die über eine Website generiert werden, handeln.</p> <p>Das
|
|
||||||
externe Hosting erfolgt zum Zwecke der Vertragserfüllung gegenüber unseren potenziellen und
|
|
||||||
bestehenden Kunden (Art. 6 Abs. 1 lit. b DSGVO) und im Interesse einer sicheren, schnellen und effizienten
|
|
||||||
Bereitstellung unseres Online-Angebots durch einen professionellen Anbieter (Art. 6 Abs. 1 lit. f DSGVO). Sofern
|
|
||||||
eine entsprechende Einwilligung abgefragt wurde, erfolgt die Verarbeitung ausschließlich auf Grundlage von
|
|
||||||
Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TDDDG, soweit die Einwilligung die Speicherung von Cookies oder
|
|
||||||
den Zugriff auf Informationen im Endgerät des Nutzers (z. B. Device-Fingerprinting) im Sinne des
|
|
||||||
TDDDG umfasst. Die Einwilligung ist jederzeit widerrufbar.</p> <p>Unser(e) Hoster wird bzw. werden Ihre Daten
|
|
||||||
nur insoweit verarbeiten, wie dies zur Erfüllung seiner Leistungspflichten erforderlich ist und unsere
|
|
||||||
Weisungen in Bezug auf diese Daten befolgen.</p> <p>Wir setzen folgende(n) Hoster ein:</p>
|
|
||||||
<p>-Platzhalter-</p>
|
|
||||||
<h4>Auftragsverarbeitung</h4> <p>Wir haben einen Vertrag über Auftragsverarbeitung (AVV) zur Nutzung
|
|
||||||
des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen
|
|
||||||
Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nach
|
|
||||||
unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.</p>
|
|
||||||
<h2>3. Allgemeine Hinweise und Pflicht­informationen</h2>
|
|
||||||
<h3>Datenschutz</h3> <p>Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr
|
|
||||||
ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen
|
|
||||||
Datenschutzvorschriften sowie dieser Datenschutzerklärung.</p> <p>Wenn Sie diese Website benutzen,
|
|
||||||
werden verschiedene personenbezogene Daten erhoben. Personenbezogene Daten sind Daten, mit denen Sie
|
|
||||||
persönlich identifiziert werden können. Die vorliegende Datenschutzerklärung erläutert,
|
|
||||||
welche Daten wir erheben und wofür wir sie nutzen. Sie erläutert auch, wie und zu welchem Zweck das
|
|
||||||
geschieht.</p> <p>Wir weisen darauf hin, dass die Datenübertragung im Internet (z. B. bei der
|
|
||||||
Kommunikation per E-Mail) Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor
|
|
||||||
dem Zugriff durch Dritte ist nicht möglich.</p>
|
|
||||||
<h3>Hinweis zur verantwortlichen Stelle</h3> <p>Die verantwortliche Stelle für die Datenverarbeitung auf
|
|
||||||
dieser Website ist:</p> <p>Ammerlaender Heerstrasse, 114-118</p>
|
|
||||||
<p>Telefon: 0123456789<br />
|
|
||||||
E-Mail: GruppeC@uol.de</p>
|
|
||||||
<p>Verantwortliche Stelle ist die natürliche oder juristische Person, die allein oder gemeinsam mit anderen
|
|
||||||
über die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten (z. B. Namen, E-Mail-
|
|
||||||
Adressen o. Ä.) entscheidet.</p>
|
|
||||||
<h3>Speicherdauer</h3> <p>Soweit innerhalb dieser Datenschutzerklärung keine speziellere Speicherdauer
|
|
||||||
genannt wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck für die Datenverarbeitung
|
|
||||||
entfällt. Wenn Sie ein berechtigtes Löschersuchen geltend machen oder eine Einwilligung zur
|
|
||||||
Datenverarbeitung widerrufen, werden Ihre Daten gelöscht, sofern wir keine anderen rechtlich
|
|
||||||
zulässigen Gründe für die Speicherung Ihrer personenbezogenen Daten haben (z. B. steuer-
|
|
||||||
oder handelsrechtliche Aufbewahrungsfristen); im letztgenannten Fall erfolgt die Löschung nach Fortfall
|
|
||||||
dieser Gründe.</p>
|
|
||||||
<h3>Allgemeine Hinweise zu den Rechtsgrundlagen der Datenverarbeitung auf dieser Website</h3> <p>Sofern Sie
|
|
||||||
in die Datenverarbeitung eingewilligt haben, verarbeiten wir Ihre personenbezogenen Daten auf Grundlage von Art.
|
|
||||||
6 Abs. 1 lit. a DSGVO bzw. Art. 9 Abs. 2 lit. a DSGVO, sofern besondere Datenkategorien nach Art. 9 Abs. 1 DSGVO
|
|
||||||
verarbeitet werden. Im Falle einer ausdrücklichen Einwilligung in die Übertragung personenbezogener
|
|
||||||
Daten in Drittstaaten erfolgt die Datenverarbeitung außerdem auf Grundlage von Art. 49 Abs. 1 lit. a DSGVO.
|
|
||||||
Sofern Sie in die Speicherung von Cookies oder in den Zugriff auf Informationen in Ihr Endgerät (z. B. via
|
|
||||||
Device-Fingerprinting) eingewilligt haben, erfolgt die Datenverarbeitung zusätzlich auf Grundlage von §
|
|
||||||
25 Abs. 1 TDDDG. Die Einwilligung ist jederzeit widerrufbar. Sind Ihre Daten zur Vertragserfüllung oder zur
|
|
||||||
Durchführung vorvertraglicher Maßnahmen erforderlich, verarbeiten wir Ihre Daten auf Grundlage des
|
|
||||||
Art. 6 Abs. 1 lit. b DSGVO. Des Weiteren verarbeiten wir Ihre Daten, sofern diese zur Erfüllung einer
|
|
||||||
rechtlichen Verpflichtung erforderlich sind, auf Grundlage von Art. 6 Abs. 1 lit. c DSGVO. Die Datenverarbeitung
|
|
||||||
kann ferner auf Grundlage unseres berechtigten Interesses nach Art. 6 Abs. 1 lit. f DSGVO erfolgen. Über die
|
|
||||||
jeweils im Einzelfall einschlägigen Rechtsgrundlagen wird in den folgenden Absätzen dieser
|
|
||||||
Datenschutzerklärung informiert.</p>
|
|
||||||
<h3>Empfänger von personenbezogenen Daten</h3> <p>Im Rahmen unserer Geschäftstätigkeit
|
|
||||||
arbeiten wir mit verschiedenen externen Stellen zusammen. Dabei ist teilweise auch eine Übermittlung von
|
|
||||||
personenbezogenen Daten an diese externen Stellen erforderlich. Wir geben personenbezogene Daten nur dann an
|
|
||||||
externe Stellen weiter, wenn dies im Rahmen einer Vertragserfüllung erforderlich ist, wenn wir gesetzlich
|
|
||||||
hierzu verpflichtet sind (z. B. Weitergabe von Daten an Steuerbehörden), wenn wir ein berechtigtes
|
|
||||||
Interesse nach Art. 6 Abs. 1 lit. f DSGVO an der Weitergabe haben oder wenn eine sonstige Rechtsgrundlage die
|
|
||||||
Datenweitergabe erlaubt. Beim Einsatz von Auftragsverarbeitern geben wir personenbezogene Daten unserer
|
|
||||||
Kunden nur auf Grundlage eines gültigen Vertrags über Auftragsverarbeitung weiter. Im Falle einer
|
|
||||||
gemeinsamen Verarbeitung wird ein Vertrag über gemeinsame Verarbeitung geschlossen.</p>
|
|
||||||
<h3>Widerruf Ihrer Einwilligung zur Datenverarbeitung</h3> <p>Viele Datenverarbeitungsvorgänge sind
|
|
||||||
nur mit Ihrer ausdrücklichen Einwilligung möglich. Sie können eine bereits erteilte Einwilligung
|
|
||||||
jederzeit widerrufen. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Datenverarbeitung bleibt vom
|
|
||||||
Widerruf unberührt.</p>
|
|
||||||
<h3>Widerspruchsrecht gegen die Datenerhebung in besonderen Fällen sowie gegen Direktwerbung (Art. 21
|
|
||||||
DSGVO)</h3> <p>WENN DIE DATENVERARBEITUNG AUF GRUNDLAGE VON ART. 6 ABS. 1 LIT. E ODER F
|
|
||||||
DSGVO ERFOLGT, HABEN SIE JEDERZEIT DAS RECHT, AUS GRÜNDEN, DIE SICH AUS IHRER
|
|
||||||
BESONDEREN SITUATION ERGEBEN, GEGEN DIE VERARBEITUNG IHRER PERSONENBEZOGENEN DATEN
|
|
||||||
WIDERSPRUCH EINZULEGEN; DIES GILT AUCH FÜR EIN AUF DIESE BESTIMMUNGEN GESTÜTZTES
|
|
||||||
PROFILING. DIE JEWEILIGE RECHTSGRUNDLAGE, AUF DENEN EINE VERARBEITUNG BERUHT, ENTNEHMEN
|
|
||||||
SIE DIESER DATENSCHUTZERKLÄRUNG. WENN SIE WIDERSPRUCH EINLEGEN, WERDEN WIR IHRE
|
|
||||||
BETROFFENEN PERSONENBEZOGENEN DATEN NICHT MEHR VERARBEITEN, ES SEI DENN, WIR
|
|
||||||
KÖNNEN ZWINGENDE SCHUTZWÜRDIGE GRÜNDE FÜR DIE VERARBEITUNG
|
|
||||||
NACHWEISEN, DIE IHRE INTERESSEN, RECHTE UND FREIHEITEN ÜBERWIEGEN ODER DIE
|
|
||||||
VERARBEITUNG DIENT DER GELTENDMACHUNG, AUSÜBUNG ODER VERTEIDIGUNG VON
|
|
||||||
RECHTSANSPRÜCHEN (WIDERSPRUCH NACH ART. 21 ABS. 1 DSGVO).</p> <p>WERDEN IHRE
|
|
||||||
PERSONENBEZOGENEN DATEN VERARBEITET, UM DIREKTWERBUNG ZU BETREIBEN, SO HABEN SIE DAS
|
|
||||||
RECHT, JEDERZEIT WIDERSPRUCH GEGEN DIE VERARBEITUNG SIE BETREFFENDER PERSONENBEZOGENER
|
|
||||||
DATEN ZUM ZWECKE DERARTIGER WERBUNG EINZULEGEN; DIES GILT AUCH FÜR DAS PROFILING,
|
|
||||||
SOWEIT ES MIT SOLCHER DIREKTWERBUNG IN VERBINDUNG STEHT. WENN SIE WIDERSPRECHEN,
|
|
||||||
WERDEN IHRE PERSONENBEZOGENEN DATEN ANSCHLIESSEND NICHT MEHR ZUM ZWECKE DER
|
|
||||||
DIREKTWERBUNG VERWENDET (WIDERSPRUCH NACH ART. 21 ABS. 2 DSGVO).</p>
|
|
||||||
<h3>Beschwerde­recht bei der zuständigen Aufsichts­behörde</h3> <p>Im Falle von
|
|
||||||
Verstößen gegen die DSGVO steht den Betroffenen ein Beschwerderecht bei einer
|
|
||||||
Aufsichtsbehörde, insbesondere in dem Mitgliedstaat ihres gewöhnlichen Aufenthalts, ihres
|
|
||||||
Arbeitsplatzes oder des Orts des mutmaßlichen Verstoßes zu. Das Beschwerderecht besteht
|
|
||||||
unbeschadet anderweitiger verwaltungsrechtlicher oder gerichtlicher Rechtsbehelfe.</p>
|
|
||||||
<h3>Recht auf Daten­übertrag­barkeit</h3> <p>Sie haben das Recht, Daten, die wir auf Grundlage
|
|
||||||
Ihrer Einwilligung oder in Erfüllung eines Vertrags automatisiert verarbeiten, an sich oder an einen Dritten in
|
|
||||||
einem gängigen, maschinenlesbaren Format aushändigen zu lassen. Sofern Sie die direkte
|
|
||||||
Übertragung der Daten an einen anderen Verantwortlichen verlangen, erfolgt dies nur, soweit es technisch
|
|
||||||
machbar ist.</p>
|
|
||||||
<h3>Auskunft, Berichtigung und Löschung</h3> <p>Sie haben im Rahmen der geltenden gesetzlichen
|
|
||||||
Bestimmungen jederzeit das Recht auf unentgeltliche Auskunft über Ihre gespeicherten personenbezogenen
|
|
||||||
Daten, deren Herkunft und Empfänger und den Zweck der Datenverarbeitung und ggf. ein Recht auf
|
|
||||||
Berichtigung oder Löschung dieser Daten. Hierzu sowie zu weiteren Fragen zum Thema personenbezogene
|
|
||||||
Daten können Sie sich jederzeit an uns wenden.</p>
|
|
||||||
<h3>Recht auf Einschränkung der Verarbeitung</h3> <p>Sie haben das Recht, die Einschränkung der
|
|
||||||
Verarbeitung Ihrer personenbezogenen Daten zu verlangen. Hierzu können Sie sich jederzeit an uns wenden.
|
|
||||||
Das Recht auf Einschränkung der Verarbeitung besteht in folgenden Fällen:</p> <ul> <li>Wenn Sie die
|
|
||||||
Richtigkeit Ihrer bei uns gespeicherten personenbezogenen Daten bestreiten, benötigen wir in der Regel Zeit,
|
|
||||||
um dies zu überprüfen. Für die Dauer der Prüfung haben Sie das Recht, die
|
|
||||||
Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li> <li>Wenn die
|
|
||||||
Verarbeitung Ihrer personenbezogenen Daten unrechtmäßig geschah/geschieht, können Sie statt
|
|
||||||
der Löschung die Einschränkung der Datenverarbeitung verlangen.</li> <li>Wenn wir Ihre
|
|
||||||
personenbezogenen Daten nicht mehr benötigen, Sie sie jedoch zur Ausübung, Verteidigung oder
|
|
||||||
Geltendmachung von Rechtsansprüchen benötigen, haben Sie das Recht, statt der Löschung die
|
|
||||||
Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li> <li>Wenn Sie einen
|
|
||||||
Widerspruch nach Art. 21 Abs. 1 DSGVO eingelegt haben, muss eine Abwägung zwischen Ihren und unseren
|
|
||||||
Interessen vorgenommen werden. Solange noch nicht feststeht, wessen Interessen überwiegen, haben Sie das
|
|
||||||
Recht, die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li> </ul>
|
|
||||||
<p>Wenn Sie die Verarbeitung Ihrer personenbezogenen Daten eingeschränkt haben, dürfen diese
|
|
||||||
Daten – von ihrer Speicherung abgesehen – nur mit Ihrer Einwilligung oder zur Geltendmachung,
|
|
||||||
Ausübung oder Verteidigung von Rechtsansprüchen oder zum Schutz der Rechte einer anderen
|
|
||||||
natürlichen oder juristischen Person oder aus Gründen eines wichtigen öffentlichen Interesses der
|
|
||||||
Europäischen Union oder eines Mitgliedstaats verarbeitet werden.</p>
|
|
||||||
|
|
||||||
<h4>Cookies</h4>
|
<h1>Datenschutzerklärung</h1>
|
||||||
<p>
|
|
||||||
Unsere Internetseiten verwenden so genannte „Cookies“. Cookies sind kleine Datenpakete und richten auf
|
|
||||||
Ihrem Endgerät keinen Schaden an. Sie werden entweder vorübergehend für die Dauer einer Sitzung
|
|
||||||
(Session-Cookies) oder dauerhaft (permanente Cookies) auf Ihrem Endgerät gespeichert. Session-Cookies
|
|
||||||
werden nach Ende Ihres Besuchs automatisch gelöscht. Permanente Cookies bleiben auf Ihrem Endgerät
|
|
||||||
gespeichert, bis Sie diese selbst löschen oder eine automatische Löschung durch Ihren Webbrowser erfolgt.
|
|
||||||
Cookies haben verschiedene Funktionen. Zahlreiche Cookies sind technisch notwendig, da bestimmte
|
|
||||||
Webseitenfunktionen ohne diese nicht funktionieren würden (z. B. die Anmeldung). Andere Cookies können zur
|
|
||||||
Auswertung des Nutzerverhaltens oder zu Werbezwecken verwendet werden.
|
|
||||||
Cookies, die zur Durchführung des elektronischen Kommunikationsvorgangs, zur Bereitstellung
|
|
||||||
bestimmter, von Ihnen erwünschter Funktionen oder zur Optimierung der
|
|
||||||
Website (z. B. Cookies zur Messung des Webpublikums) erforderlich sind (notwendige Cookies), werden auf
|
|
||||||
Grundlage von Art. 6 Abs. 1 lit. f DSGVO gespeichert, sofern keine andere Rechtsgrundlage angegeben wird.
|
|
||||||
Der Websitebetreiber hat ein berechtigtes Interesse an der Speicherung von notwendigen Cookies zur
|
|
||||||
technisch fehlerfreien und optimierten Bereitstellung seiner Dienste. Sofern eine Einwilligung zur
|
|
||||||
Speicherung von Cookies und vergleichbaren Wiedererkennungstechnologien abgefragt wurde, erfolgt die
|
|
||||||
Verarbeitung ausschließlich auf Grundlage dieser Einwilligung (Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1
|
|
||||||
TDDDG); die Einwilligung ist jederzeit widerrufbar.
|
|
||||||
Sie können Ihren Browser so einstellen, dass Sie über das Setzen von Cookies informiert werden und
|
|
||||||
Cookies nur im Einzelfall erlauben, die Annahme von Cookies für bestimmte Fälle oder generell ausschließen
|
|
||||||
sowie das automatische Löschen der Cookies beim Schließen des Browsers aktivieren. Bei der
|
|
||||||
Deaktivierung von Cookies kann die Funktionalität dieser Website eingeschränkt sein.
|
|
||||||
Sofern weitere Cookies und Dienste auf dieser Website eingesetzt werden, können Sie dies dieser
|
|
||||||
Datenschutzerklärung entnehmen.
|
|
||||||
|
|
||||||
Auf dieser Webseite werden jedoch nur technisch notwendige Cookies verwendet.
|
<h2>1. Datenschutz auf einen Blick</h2>
|
||||||
</p>
|
|
||||||
<p>Quelle: <a href="https://www.e-recht24.de">https://www.e-recht24.de</a></p>
|
|
||||||
|
|
||||||
</main>
|
<h3>Allgemeine Hinweise</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Die folgenden Hinweise geben einen einfachen Überblick darüber,
|
||||||
|
was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen.
|
||||||
|
Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können.
|
||||||
|
Ausführliche Informationen zum Datenschutz finden Sie in dieser Datenschutzerklärung.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Datenerfassung auf dieser Website</h3>
|
||||||
|
|
||||||
|
<h4>Wer ist verantwortlich für die Datenerfassung?</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Die Datenverarbeitung erfolgt durch den Betreiber dieser Website.
|
||||||
|
Die Kontaktdaten finden Sie im Abschnitt
|
||||||
|
„Hinweis zur verantwortlichen Stelle“.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Wie erfassen wir Ihre Daten?</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Personenbezogene Daten werden erhoben, wenn Sie diese freiwillig
|
||||||
|
im Rahmen der Nutzung der Plattform angeben.
|
||||||
|
Dies betrifft insbesondere die Registrierung eines Benutzerkontos,
|
||||||
|
Änderungen am Benutzerprofil sowie das Veröffentlichen von Beiträgen
|
||||||
|
und Kommentaren.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Darüber hinaus werden beim Besuch der Website automatisch technische
|
||||||
|
Informationen wie Browsertyp, Betriebssystem oder Zeitpunkt des Seitenaufrufs
|
||||||
|
verarbeitet. Diese Daten dienen ausschließlich der technischen Bereitstellung
|
||||||
|
und Sicherheit der Website.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Wofür nutzen wir Ihre Daten?</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Die erhobenen Daten werden verwendet, um Ihr Benutzerkonto bereitzustellen,
|
||||||
|
die Funktionen der Plattform zu ermöglichen sowie Beiträge und Kommentare
|
||||||
|
den jeweiligen Benutzerkonten zuzuordnen.
|
||||||
|
Außerdem werden technische Daten verarbeitet, um einen sicheren und
|
||||||
|
fehlerfreien Betrieb der Website zu gewährleisten.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Welche Rechte haben Sie bezüglich Ihrer Daten?</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Sie haben jederzeit das Recht auf Auskunft über Ihre gespeicherten
|
||||||
|
personenbezogenen Daten sowie auf Berichtigung oder Löschung dieser Daten.
|
||||||
|
Darüber hinaus können Sie eine erteilte Einwilligung jederzeit mit Wirkung
|
||||||
|
für die Zukunft widerrufen. Außerdem steht Ihnen ein Beschwerderecht bei der
|
||||||
|
zuständigen Datenschutzaufsichtsbehörde zu.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>2. Hosting</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Diese Website wird auf einem Server der Carl von Ossietzky Universität
|
||||||
|
Oldenburg im Rahmen eines Studienprojekts betrieben.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Hosting</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Zur Bereitstellung der Website werden technisch notwendige Daten,
|
||||||
|
beispielsweise IP-Adressen oder Server-Logdaten, verarbeitet.
|
||||||
|
Diese Daten dienen ausschließlich dem sicheren Betrieb der Website.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>3. Allgemeine Hinweise und Pflichtinformationen</h2>
|
||||||
|
|
||||||
|
<h3>Datenschutz</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Die Betreiber dieser Website behandeln personenbezogene Daten vertraulich
|
||||||
|
und entsprechend der Datenschutz-Grundverordnung (DSGVO) sowie den
|
||||||
|
geltenden gesetzlichen Datenschutzvorschriften.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Personenbezogene Daten werden nur in dem Umfang verarbeitet,
|
||||||
|
wie dies für die Bereitstellung der Plattform und ihrer Funktionen
|
||||||
|
erforderlich ist.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Registrierung und Benutzerkonto</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Bei der Registrierung werden personenbezogene Daten wie
|
||||||
|
Vorname, Nachname, E-Mail-Adresse und ein verschlüsseltes Passwort
|
||||||
|
verarbeitet.
|
||||||
|
Diese Daten dienen ausschließlich der Bereitstellung und Verwaltung
|
||||||
|
Ihres Benutzerkontos.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Beiträge und Kommentare</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Wenn Sie Beiträge oder Kommentare veröffentlichen,
|
||||||
|
werden diese gemeinsam mit Ihrem Benutzerkonto gespeichert
|
||||||
|
und auf der Plattform angezeigt.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Hinweis zur verantwortlichen Stelle</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Verantwortlich für die Datenverarbeitung ist:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Carl von Ossietzky Universität Oldenburg<br>
|
||||||
|
Ammerländer Heerstraße 114–118<br>
|
||||||
|
E-Mail: GruppeC@uol.de
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Verantwortliche Stelle ist die natürliche oder juristische Person,
|
||||||
|
die über Zwecke und Mittel der Verarbeitung personenbezogener Daten entscheidet.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Speicherdauer</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Personenbezogene Daten werden nur solange gespeichert,
|
||||||
|
wie dies zur Erfüllung des jeweiligen Zwecks erforderlich ist
|
||||||
|
oder gesetzliche Aufbewahrungspflichten bestehen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Allgemeine Hinweise zu den Rechtsgrundlagen der Datenverarbeitung</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Die Verarbeitung personenbezogener Daten erfolgt entsprechend den
|
||||||
|
gesetzlichen Vorgaben der DSGVO.
|
||||||
|
Je nach Verarbeitung erfolgt diese insbesondere auf Grundlage
|
||||||
|
von Art. 6 Abs. 1 lit. a, b, c oder f DSGVO.
|
||||||
|
</p>
|
||||||
|
<h3>Empfänger von personenbezogenen Daten</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Personenbezogene Daten werden grundsätzlich nicht an Dritte weitergegeben,
|
||||||
|
sofern hierfür keine gesetzliche Verpflichtung besteht oder dies zur
|
||||||
|
Bereitstellung der Plattform erforderlich ist.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Widerruf Ihrer Einwilligung zur Datenverarbeitung</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Sofern die Verarbeitung personenbezogener Daten auf Ihrer Einwilligung
|
||||||
|
beruht, können Sie diese Einwilligung jederzeit mit Wirkung für die Zukunft
|
||||||
|
widerrufen. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Verarbeitung
|
||||||
|
bleibt hiervon unberührt.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Widerspruchsrecht nach Art. 21 DSGVO</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Werden personenbezogene Daten auf Grundlage von Art. 6 Abs. 1 lit. e oder
|
||||||
|
lit. f DSGVO verarbeitet, haben Sie das Recht, aus Gründen, die sich aus
|
||||||
|
Ihrer besonderen Situation ergeben, jederzeit Widerspruch gegen diese
|
||||||
|
Verarbeitung einzulegen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Beschwerderecht bei der zuständigen Aufsichtsbehörde</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Im Falle datenschutzrechtlicher Verstöße haben Betroffene das Recht,
|
||||||
|
Beschwerde bei einer zuständigen Datenschutzaufsichtsbehörde einzulegen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Recht auf Datenübertragbarkeit</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Sie haben das Recht, personenbezogene Daten, die wir auf Grundlage Ihrer
|
||||||
|
Einwilligung oder zur Vertragserfüllung automatisiert verarbeiten,
|
||||||
|
in einem gängigen, maschinenlesbaren Format zu erhalten oder an einen
|
||||||
|
anderen Verantwortlichen übertragen zu lassen, soweit dies technisch
|
||||||
|
möglich ist.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Auskunft, Berichtigung und Löschung</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Sie haben jederzeit das Recht auf unentgeltliche Auskunft über Ihre
|
||||||
|
gespeicherten personenbezogenen Daten sowie auf Berichtigung,
|
||||||
|
Einschränkung der Verarbeitung oder Löschung dieser Daten,
|
||||||
|
soweit dem keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Recht auf Einschränkung der Verarbeitung</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Unter den gesetzlichen Voraussetzungen können Sie die Einschränkung der
|
||||||
|
Verarbeitung Ihrer personenbezogenen Daten verlangen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Cookies</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Diese Website verwendet ausschließlich technisch notwendige Cookies.
|
||||||
|
Diese dienen beispielsweise der Sitzungsverwaltung nach der Anmeldung
|
||||||
|
sowie der sicheren und fehlerfreien Bereitstellung der Website.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Es werden keine Analyse-, Marketing- oder Tracking-Cookies eingesetzt.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Sie können Ihren Browser so konfigurieren, dass Sie über das Setzen von
|
||||||
|
Cookies informiert werden oder Cookies vollständig ablehnen.
|
||||||
|
Dadurch können einzelne Funktionen der Website eingeschränkt sein.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Änderungen dieser Datenschutzerklärung</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Der Betreiber behält sich vor, diese Datenschutzerklärung anzupassen,
|
||||||
|
wenn dies aufgrund technischer oder rechtlicher Änderungen erforderlich
|
||||||
|
wird.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|||||||
+3
-6
@@ -11,12 +11,9 @@ include_once 'php/controller/home-controller.php';
|
|||||||
|
|
||||||
<h1>Home</h1>
|
<h1>Home</h1>
|
||||||
<p>
|
<p>
|
||||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
|
Schön, dass du da bist. EduForge ist deine digitale Werkstatt für Lernerfolg. Wir unterstützen Schüler und
|
||||||
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
|
Studierende dabei, die passenden Lernmaterialien zu finden, Wissenslücken zu schließen und Gelerntes gezielt
|
||||||
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
|
zu vertiefen.
|
||||||
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
|
|
||||||
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
|
|
||||||
sea takimata sanctus est Lorem ipsum dolor sit amet.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Flexbox für aktive Kategorien -->
|
<!-- Flexbox für aktive Kategorien -->
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!--
|
<!--
|
||||||
Seite: Nutzungsbedingungen
|
Seite: Nutzungsbedingungen
|
||||||
Inhalt: Regelungen für die Nutzung der Plattform (AGBs und so...)
|
Inhalt: Regelungen für die Nutzung der Plattform
|
||||||
-->
|
-->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
@@ -23,63 +23,83 @@
|
|||||||
include_once 'includes/navbar.php';
|
include_once 'includes/navbar.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
<main>
|
<h1>Nutzungsbedingungen</h1>
|
||||||
|
|
||||||
<h1>Nutzungsbedingungen</h1>
|
<p>
|
||||||
|
Diese Nutzungsbedingungen regeln die Nutzung der Plattform EduForge.
|
||||||
|
Mit der Registrierung und Nutzung der Website erkennen Nutzerinnen und Nutzer
|
||||||
|
die nachfolgenden Bedingungen an.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<h2>1. Nutzung der Plattform</h2>
|
||||||
Diese Nutzungsbedingungen regeln die Nutzung der Plattform EduForge.
|
<p>
|
||||||
Mit der Registrierung und Nutzung der Website erkennen Nutzerinnen und Nutzer
|
EduForge dient dem Austausch von Lerninhalten, Beiträgen und Kommentaren.
|
||||||
die nachfolgenden Bedingungen an.
|
Die Nutzung der Plattform darf ausschließlich im Rahmen der geltenden Gesetze erfolgen.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>1. Nutzung der Plattform</h2>
|
<h2>2. Registrierung und Benutzerkonto</h2>
|
||||||
<p>
|
<p>
|
||||||
EduForge dient dem Austausch von Lerninhalten, Beiträgen und Kommentaren.
|
Für die Nutzung bestimmter Funktionen ist eine Registrierung erforderlich.
|
||||||
Die Nutzung der Plattform darf ausschließlich im Rahmen der geltenden Gesetze erfolgen.
|
Nutzerinnen und Nutzer verpflichten sich, bei der Registrierung
|
||||||
</p>
|
wahrheitsgemäße Angaben zu machen und ihre Zugangsdaten vertraulich zu behandeln.
|
||||||
|
Änderungen der Profildaten können jederzeit über das Benutzerprofil vorgenommen werden.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>2. Verhalten der Nutzerinnen und Nutzer</h2>
|
<h2>3. Verhalten der Nutzerinnen und Nutzer</h2>
|
||||||
<p>
|
<p>
|
||||||
Nutzerinnen und Nutzer verpflichten sich zu einem respektvollen Umgang miteinander.
|
Nutzerinnen und Nutzer verpflichten sich zu einem respektvollen Umgang miteinander.
|
||||||
Das Veröffentlichen von beleidigenden, diskriminierenden, rechtswidrigen oder
|
Das Veröffentlichen von beleidigenden, diskriminierenden, rechtswidrigen oder
|
||||||
anderweitig unangemessenen Inhalten ist untersagt.
|
anderweitig unangemessenen Inhalten ist untersagt.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>3. Veröffentlichung von Inhalten</h2>
|
<h2>4. Veröffentlichung von Inhalten</h2>
|
||||||
<p>
|
<p>
|
||||||
Nutzerinnen und Nutzer sind für die von ihnen veröffentlichten Inhalte selbst verantwortlich.
|
Nutzerinnen und Nutzer sind für die von ihnen veröffentlichten Inhalte selbst verantwortlich.
|
||||||
Dies gilt insbesondere für Beiträge, Kommentare, Bilder und sonstige hochgeladene Dateien.
|
Dies gilt insbesondere für Beiträge, Kommentare, Bilder und sonstige hochgeladene Dateien.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>4. Urheberrechte</h2>
|
<h2>5. Benutzerkonto löschen</h2>
|
||||||
<p>
|
<p>
|
||||||
Es dürfen ausschließlich Inhalte veröffentlicht werden, an denen die Nutzerinnen und Nutzer
|
Nutzerinnen und Nutzer können ihr Benutzerkonto jederzeit über die entsprechende
|
||||||
die erforderlichen Rechte besitzen. Durch das Einstellen von Inhalten versichern die
|
Funktion der Plattform löschen. Personenbezogene Daten werden anschließend gemäß
|
||||||
Nutzerinnen und Nutzer, keine Rechte Dritter zu verletzen.
|
der Datenschutzerklärung verarbeitet.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>5. Entfernung von Inhalten</h2>
|
<h2>6. Passwort und Passwort-Reset</h2>
|
||||||
<p>
|
<p>
|
||||||
Inhalte, die gegen diese Nutzungsbedingungen oder geltendes Recht verstoßen,
|
Nutzerinnen und Nutzer sind verpflichtet, ihr Passwort geheim zu halten und
|
||||||
können vom Betreiber der Plattform entfernt werden.
|
nicht an Dritte weiterzugeben. Bei Verlust oder Vergessen des Passworts kann
|
||||||
</p>
|
die bereitgestellte Passwort-Reset-Funktion genutzt werden.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>6. Änderungen der Nutzungsbedingungen</h2>
|
<h2>7. Moderation und Entfernung von Inhalten</h2>
|
||||||
<p>
|
<p>
|
||||||
Der Betreiber behält sich vor, diese Nutzungsbedingungen anzupassen, sofern dies
|
Der Betreiber behält sich vor, Beiträge, Kommentare oder Benutzerkonten zu sperren
|
||||||
für den Betrieb oder die Weiterentwicklung der Plattform erforderlich ist.
|
oder Inhalte zu entfernen, wenn diese gegen geltendes Recht oder diese
|
||||||
</p>
|
Nutzungsbedingungen verstoßen.
|
||||||
|
</p>
|
||||||
|
|
||||||
</main>
|
<h2>8. Urheberrechte</h2>
|
||||||
|
<p>
|
||||||
|
Es dürfen ausschließlich Inhalte veröffentlicht werden, an denen die Nutzerinnen
|
||||||
|
und Nutzer die erforderlichen Rechte besitzen. Durch das Einstellen von Inhalten
|
||||||
|
versichern sie, keine Rechte Dritter zu verletzen.
|
||||||
|
</p>
|
||||||
|
|
||||||
</main>
|
<h2>9. Änderungen der Nutzungsbedingungen</h2>
|
||||||
|
<p>
|
||||||
|
Der Betreiber behält sich vor, diese Nutzungsbedingungen anzupassen, sofern dies
|
||||||
|
für den Betrieb oder die Weiterentwicklung der Plattform erforderlich ist.
|
||||||
|
Über wesentliche Änderungen werden die Nutzerinnen und Nutzer informiert.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
include_once 'includes/footer.php';
|
||||||
|
?>
|
||||||
|
|
||||||
<?php
|
|
||||||
include_once 'includes/footer.php';
|
|
||||||
?>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
+5
-2
@@ -1,5 +1,4 @@
|
|||||||
<?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);
|
||||||
@@ -18,6 +17,8 @@ $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"
|
||||||
@@ -82,9 +83,10 @@ $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 wirklich unwiderruflich löschen?');">
|
onclick="return confirm('Möchtest du deinen Account und alle deine Beiträge wirklich unwiderruflich löschen?');">
|
||||||
Account löschen
|
Account löschen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -155,6 +157,7 @@ $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()); ?>">
|
||||||
|
|||||||
@@ -57,6 +57,36 @@ $success = $success ?? null;
|
|||||||
placeholder="Passwort"
|
placeholder="Passwort"
|
||||||
required>
|
required>
|
||||||
|
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="termsAccepted"
|
||||||
|
value="1"
|
||||||
|
<?php echo isset($_POST["termsAccepted"]) ? "checked" : ""; ?>
|
||||||
|
required
|
||||||
|
>
|
||||||
|
|
||||||
|
Ich habe die
|
||||||
|
<a
|
||||||
|
href="index.php?pfad=datenschutz"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Datenschutzerklärung
|
||||||
|
</a>
|
||||||
|
und die
|
||||||
|
<a
|
||||||
|
href="index.php?pfad=nutzungsbedingungen"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Nutzungsbedingungen
|
||||||
|
</a>
|
||||||
|
gelesen und akzeptiere sie.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
value="register"
|
value="register"
|
||||||
name="registerSubmit"
|
name="registerSubmit"
|
||||||
|
|||||||
+102
-70
@@ -2,20 +2,37 @@
|
|||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
require_once __DIR__ . '/../includes/resultsHelper.php';
|
||||||
|
|
||||||
$all_results = $_SESSION["search_results"] ?? [];
|
$rawResults = $_SESSION["search_results"] ?? [];
|
||||||
$query = $_SESSION["search_query"] ?? "";
|
$query = $_SESSION["search_query"] ?? "";
|
||||||
$totalResultsCount = count($all_results);
|
|
||||||
|
|
||||||
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
|
// Sortierung / Kategorie / Limit: GET-Parameter haben Vorrang, sonst
|
||||||
|
// Rückfall auf die zuletzt in der Session gemerkten Werte. So bleibt
|
||||||
|
// z.B. ein reiner Pagination-Link (?page=2) bei der aktuellen
|
||||||
|
// Sortierung/Filterung.
|
||||||
|
$currentSort = $_GET['sort'] ?? ($_SESSION['search_sort'] ?? 'alphabet');
|
||||||
|
if (!in_array($currentSort, ['alphabet', 'likes', 'newest', 'oldest'])) {
|
||||||
|
$currentSort = 'alphabet';
|
||||||
|
}
|
||||||
|
$_SESSION['search_sort'] = $currentSort;
|
||||||
|
|
||||||
|
$currentCategory = strtolower($_GET['category'] ?? ($_SESSION['search_category'] ?? 'all'));
|
||||||
|
$_SESSION['search_category'] = $currentCategory;
|
||||||
|
|
||||||
|
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['search_limit'] ?? 10);
|
||||||
if (!in_array($limit, [10, 20, 50, 100])) {
|
if (!in_array($limit, [10, 20, 50, 100])) {
|
||||||
$limit = 10;
|
$limit = 10;
|
||||||
}
|
}
|
||||||
|
$_SESSION['search_limit'] = $limit;
|
||||||
|
|
||||||
// Gesamtseitenzahl
|
// Serverseitig sortieren + filtern
|
||||||
$totalPages = max(1, ceil($totalResultsCount / $limit));
|
$filteredResults = sortSearchResults($rawResults, $currentSort);
|
||||||
|
$filteredResults = filterSearchResultsByCategory($filteredResults, $currentCategory);
|
||||||
|
|
||||||
|
$totalResultsCount = count($filteredResults);
|
||||||
|
$totalPages = max(1, (int)ceil($totalResultsCount / $limit));
|
||||||
|
|
||||||
// Aktuelle Seite auslesen und validieren
|
|
||||||
$currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
$currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||||
if ($currentPage < 1) {
|
if ($currentPage < 1) {
|
||||||
$currentPage = 1;
|
$currentPage = 1;
|
||||||
@@ -23,42 +40,59 @@ if ($currentPage < 1) {
|
|||||||
$currentPage = $totalPages;
|
$currentPage = $totalPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Startpunkt im Array berechnen (Offset)
|
|
||||||
$offset = ($currentPage - 1) * $limit;
|
$offset = ($currentPage - 1) * $limit;
|
||||||
|
|
||||||
// Nur die Ergebnisse für die aktuelle Seite ausschneiden
|
// Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert.
|
||||||
//$results = array_slice($all_results, $offset, $limit);
|
// Bei aktiviertem JS lädt js/results-app.js zusätzlich den kompletten
|
||||||
$results = $all_results;
|
// Datensatz nach (ein einziger Request) und übernimmt Sortierung, Filter
|
||||||
|
// und Pagination danach komplett im Browser, ohne weitere Serveranfragen.
|
||||||
|
$results = array_slice($filteredResults, $offset, $limit);
|
||||||
$resultCount = count($results);
|
$resultCount = count($results);
|
||||||
|
|
||||||
|
// Kategorien für das Auswahlfeld (Wert => Label)
|
||||||
|
$categories = [
|
||||||
|
'Deutsch' => 'Deutsch', 'Englisch' => 'Englisch', 'Franzoesisch' => 'Französisch',
|
||||||
|
'Latein' => 'Latein', 'Literatur' => 'Literatur', 'Mathematik' => 'Mathematik',
|
||||||
|
'Biologie' => 'Biologie', 'Informatik' => 'Informatik', 'Chemie' => 'Chemie',
|
||||||
|
'Physik' => 'Physik', 'Astronomie' => 'Astronomie', 'Geschichte' => 'Geschichte',
|
||||||
|
'Erdkunde' => 'Erdkunde', 'Sozialkunde' => 'Sozialkunde', 'Wirtschaftskunde' => 'Wirtschaftskunde',
|
||||||
|
'Religion' => 'Religion', 'Ethikunterricht' => 'Ethikunterricht', 'Philosophie' => 'Philosophie',
|
||||||
|
'Psychologie' => 'Psychologie', 'Kunst' => 'Kunst', 'Musik' => 'Musik', 'Theater' => 'Theater',
|
||||||
|
'Technik' => 'Technik', 'Werken' => 'Werken', 'Hauswirtschaft' => 'Hauswirtschaft', 'Sport' => 'Sport',
|
||||||
|
];
|
||||||
?>
|
?>
|
||||||
<noscript>
|
|
||||||
Bitte JavaScript aktivieren!
|
|
||||||
</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-->
|
<!-- 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">
|
<form action="php/controller/search-results-controller.php" method="GET" id="search-form-id" class="s-res-sidebar-form">
|
||||||
|
|
||||||
<input type="hidden" id="s-res-page-input" name="page" value="<?php echo $_GET['page'] ?? 1; ?>">
|
|
||||||
|
|
||||||
<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>
|
||||||
<?php $currentSort = $_SESSION['search_sort'] ?? 'alphabet'; ?>
|
|
||||||
<div class="s-res-filter-group">
|
<div class="s-res-filter-group">
|
||||||
<label class="s-res-filter-option">
|
<label class="s-res-filter-option">
|
||||||
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
|
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
|
||||||
@@ -81,37 +115,17 @@ $resultCount = count($results);
|
|||||||
|
|
||||||
<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" 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">Alle Kategorien</option>
|
<option value="all" <?php echo $currentCategory === 'all' ? 'selected' : ''; ?>>Alle Kategorien</option>
|
||||||
<option value="Deutsch">Deutsch</option>
|
<?php foreach ($categories as $value => $label): ?>
|
||||||
<option value="Englisch">Englisch</option>
|
<option value="<?php echo htmlspecialchars($value); ?>" <?php echo $currentCategory === strtolower($value) ? 'selected' : ''; ?>><?php echo htmlspecialchars($label); ?></option>
|
||||||
<option value="Franzoesisch">Französisch</option>
|
<?php endforeach; ?>
|
||||||
<option value="Latein">Latein</option>
|
|
||||||
<option value="Literatur">Literatur</option>
|
|
||||||
<option value="Mathematik">Mathematik</option>
|
|
||||||
<option value="Biologie">Biologie</option>
|
|
||||||
<option value="Informatik">Informatik</option>
|
|
||||||
<option value="Chemie">Chemie</option>
|
|
||||||
<option value="Physik">Physik</option>
|
|
||||||
<option value="Astronomie">Astronomie</option>
|
|
||||||
<option value="Geschichte">Geschichte</option>
|
|
||||||
<option value="Erdkunde">Erdkunde</option>
|
|
||||||
<option value="Sozialkunde">Sozialkunde</option>
|
|
||||||
<option value="Wirtschaftskunde">Wirtschaftskunde</option>
|
|
||||||
<option value="Religion">Religion</option>
|
|
||||||
<option value="Ethikunterricht">Ethikunterricht</option>
|
|
||||||
<option value="Philosophie">Philosophie</option>
|
|
||||||
<option value="Psychologie">Psychologie</option>
|
|
||||||
<option value="Kunst">Kunst</option>
|
|
||||||
<option value="Musik">Musik</option>
|
|
||||||
<option value="Theater">Theater</option>
|
|
||||||
<option value="Technik">Technik</option>
|
|
||||||
<option value="Werken">Werken</option>
|
|
||||||
<option value="Hauswirtschaft">Hauswirtschaft</option>
|
|
||||||
<option value="Sport">Sport</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<button type="submit" class="nav__search-button">Filter anwenden</button>
|
||||||
|
</noscript>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
@@ -120,18 +134,27 @@ $resultCount = count($results);
|
|||||||
|
|
||||||
<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"><?php echo $totalResultsCount; ?> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"</p>
|
<p class="s-res-meta"><span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p
|
||||||
|
id="results-loading"
|
||||||
|
class="results-loading"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<span class="results-spinner" aria-hidden="true"></span>
|
||||||
|
Suchergebnisse werden geladen …
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- Ergebnisliste -->
|
<!-- Ergebnisliste -->
|
||||||
<div class="s-res-list">
|
<div class="s-res-list">
|
||||||
<?php
|
<?php if (!empty($results)): ?>
|
||||||
if (!empty($results)): ?>
|
|
||||||
|
|
||||||
<?php foreach ($results as $item):
|
<?php foreach ($results as $item):
|
||||||
// Anzahl der Likes ermitteln (falls es ein Array ist, zählen; falls Zahl, direkt nutzen)
|
$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">
|
||||||
<h2 class="s-res-item-title">
|
<h2 class="s-res-item-title">
|
||||||
@@ -151,39 +174,48 @@ $resultCount = count($results);
|
|||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<?php
|
<?php elseif (isset($_SESSION["message"]) && $_SESSION["message"] === "invalid_search_query"): ?>
|
||||||
elseif (isset($_SESSION["search_query"]) && $_SESSION["search_query"] !== "" && $resultCount === 0): ?>
|
|
||||||
<p>Keine Beiträge zu diesem Suchbegriff gefunden.</p>
|
|
||||||
<?php
|
|
||||||
elseif (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_search_query"): ?>
|
|
||||||
<p>Unzulässige Suchanfrage</p>
|
<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>
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php
|
<?php unset($_SESSION["message"]); ?>
|
||||||
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" class="s-res-limit-select">
|
<select id="s-res-per-page" name="limit" form="s-res-filter-form" 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>
|
||||||
|
|
||||||
<div class="s-res-page-navigation">
|
<nav class="s-res-page-navigation" aria-label="Seitennavigation">
|
||||||
<button type="button" class="s-res-page-btn" id="prev-page-btn" data-page="0">«</button>
|
<!-- No-JS-Fallback: -->
|
||||||
|
<noscript>
|
||||||
|
<?php echo renderNoJsPagination($currentPage, $totalPages, $query, $currentSort, $currentCategory, $limit); ?>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
<span id="dynamic-page-numbers"></span>
|
<!-- JS-Version: wird per search-results.js befüllt/eingeblendet: -->
|
||||||
|
<div id="js-page-navigation" style="display:none;">
|
||||||
<button type="button" class="s-res-page-btn" id="next-page-btn" data-page="2">»</button>
|
<button type="button" class="s-res-page-btn" id="prev-page-btn">«</button>
|
||||||
</div>
|
<span id="dynamic-page-numbers"></span>
|
||||||
|
<button type="button" class="s-res-page-btn" id="next-page-btn">»</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+393
-44
@@ -5,6 +5,58 @@ $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
|
||||||
@@ -17,36 +69,39 @@ include_once 'php/controller/showArticle-controller.php';
|
|||||||
<!-- Metadaten & Titel -->
|
<!-- Metadaten & Titel -->
|
||||||
<div class="article-view-top-section">
|
<div class="article-view-top-section">
|
||||||
|
|
||||||
<div class="article-view-top-section">
|
<div class="category-and-likes-row">
|
||||||
|
<?php if (isset($category) && !empty($category)): ?>
|
||||||
|
<span class="article-view-category"><?php echo htmlspecialchars($category); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="category-and-likes-row">
|
<!-- Like-Anzeige und dynamischer Like-Button -->
|
||||||
<?php if (isset($category) && !empty($category)): ?>
|
<?php if (isset($articleObj) && $articleObj !== null): ?>
|
||||||
<span class="article-view-category"><?php echo htmlspecialchars($category); ?></span>
|
<div class="article-view-likes">
|
||||||
<?php endif; ?>
|
<span>❤️ <span class="like-count"><?php echo $articleObj->getLikeCount(); ?></span></span>
|
||||||
|
|
||||||
<!-- Like-Anzeige und dynamischer Like-Button -->
|
<?php if (isset($_SESSION["user_email"])): ?>
|
||||||
<?php if (isset($articleObj) && $articleObj !== null): ?>
|
<form method="post"
|
||||||
<div class="article-view-likes">
|
action="php/controller/like-controller.php?id=<?php echo $articleObj->getId(); ?>"
|
||||||
<span>❤️ <span class="like-count"><?php echo $articleObj->getLikeCount(); ?></span></span>
|
class="like-toggle-form">
|
||||||
|
<?php csrf_field(); ?>
|
||||||
<?php if (isset($_SESSION["user_email"])): ?>
|
<button type="submit" class="like-toggle-btn">
|
||||||
<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'; ?>
|
||||||
</a>
|
</button>
|
||||||
<?php else: ?>
|
</form>
|
||||||
<span class="login-hint">(Anmelden zum Liken)</span>
|
<?php else: ?>
|
||||||
<?php endif; ?>
|
<span class="login-hint">(Anmelden zum Liken)</span>
|
||||||
</div>
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
</div>
|
||||||
</div>
|
<?php endif; ?>
|
||||||
|
</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); } ?>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="article-view-meta">
|
<div class="article-view-meta">
|
||||||
<?php if (isset($author) && !empty($author)): ?>
|
<?php if (isset($name) && !empty($name)): ?>
|
||||||
<span class="article-view-author">Von: <strong><?php echo htmlspecialchars($author); ?></strong></span>
|
<span class="article-view-author">Von: <strong><?php echo htmlspecialchars($name); ?></strong></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -106,39 +161,290 @@ include_once 'php/controller/showArticle-controller.php';
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<section class="article-comments-section">
|
<section class="article-comments-section" id="comments">
|
||||||
<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" data-comment-id="<?php echo htmlspecialchars($comment->getId()); ?>">
|
<div class="comment-item"
|
||||||
<p>
|
data-comment-id="<?php echo htmlspecialchars(
|
||||||
<strong><?php echo htmlspecialchars($comment->getAuthor()); ?></strong>
|
(string) $comment->getId()
|
||||||
<span><?php echo htmlspecialchars($comment->getCreated()); ?></span>
|
); ?>">
|
||||||
</p>
|
|
||||||
|
|
||||||
<p><?php echo nl2br(htmlspecialchars($comment->getContent())); ?></p>
|
<?php
|
||||||
|
$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>
|
|
||||||
|
|
||||||
<p><?php echo nl2br(htmlspecialchars($reply->getContent())); ?></p>
|
<?php
|
||||||
|
$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; ?>
|
||||||
@@ -153,17 +459,48 @@ include_once 'php/controller/showArticle-controller.php';
|
|||||||
</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($_GET["id"] ?? ""); ?>">
|
value="<?php echo htmlspecialchars(
|
||||||
|
(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="">
|
value="<?php echo $replyTo !== null
|
||||||
|
? htmlspecialchars((string) $replyTo)
|
||||||
|
: "";
|
||||||
|
?>">
|
||||||
|
|
||||||
<p id="reply-info" class="reply-info" style="display: none;"></p>
|
<p id="reply-info"
|
||||||
|
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"
|
||||||
@@ -173,6 +510,18 @@ include_once 'php/controller/showArticle-controller.php';
|
|||||||
<button type="submit" class="button">
|
<button type="submit" class="button">
|
||||||
Kommentar senden
|
Kommentar senden
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<p
|
||||||
|
id="comment-loading"
|
||||||
|
class="comment-loading"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<span class="comment-spinner" aria-hidden="true"></span>
|
||||||
|
Kommentar wird gesendet …
|
||||||
|
</p>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="comment-login-hint">
|
<div class="comment-login-hint">
|
||||||
|
|||||||
+187
-24
@@ -1,32 +1,195 @@
|
|||||||
<?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'] ?? [];
|
||||||
|
$category = $_SESSION['category_name'] ?? ($_GET['category'] ?? '');
|
||||||
|
|
||||||
|
// 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" ?>
|
||||||
|
|
||||||
<main>
|
<!-- Links: Seitenleiste für Suche (innerhalb der Kategorie) und Sortierung -->
|
||||||
<?php include_once "includes/alertMessages.php"?>
|
<aside class="s-res-sidebar">
|
||||||
|
|
||||||
<h1><?php if (isset($category) && !empty($category)){ echo htmlspecialchars($category); } ?></h1>
|
<!-- Sortiert/durchsucht die BEREITS geladenen Kategorie-Beiträge.
|
||||||
|
Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Abfrage.
|
||||||
|
Mit JS: js/showCategory.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="cat-filter-form" class="s-res-sidebar-form">
|
||||||
|
<input type="hidden" name="pfad" value="showCategory">
|
||||||
|
<input type="hidden" name="category" value="<?php echo htmlspecialchars($category); ?>">
|
||||||
|
|
||||||
<div class="s-res-list">
|
<div class="s-res-sidebar-box">
|
||||||
<?php
|
<h3 class="s-res-sidebar-title">In dieser Kategorie suchen</h3>
|
||||||
if (!empty($articles)): ?>
|
<input type="search" id="cat-search-input" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50">
|
||||||
<?php foreach ($articles as $article): ?>
|
</div>
|
||||||
<div class="s-res-item">
|
|
||||||
<div class="s-res-content">
|
<div class="s-res-sidebar-box">
|
||||||
<h2 class="s-res-item-title">
|
<h3 class="s-res-sidebar-title">Sortierung</h3>
|
||||||
<a href="index.php?pfad=showArticle&id=<?php echo $article->getId(); ?>" class="s-res-link">
|
<div class="s-res-filter-group">
|
||||||
<?php echo htmlspecialchars($article->getTitle()); ?>
|
<label class="s-res-filter-option">
|
||||||
</a>
|
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
|
||||||
</h2>
|
<span>Alphabetisch</span>
|
||||||
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($article->getAuthor()); ?></span></p>
|
</label>
|
||||||
</div>
|
<label class="s-res-filter-option">
|
||||||
<div class="s-res-arrow">→</div>
|
<input type="radio" name="sort" value="likes" class="sort-radio" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?>>
|
||||||
|
<span>Beliebtheit (Likes)</span>
|
||||||
|
</label>
|
||||||
|
<label class="s-res-filter-option">
|
||||||
|
<input type="radio" name="sort" value="newest" class="sort-radio" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?>>
|
||||||
|
<span>Neueste Beiträge</span>
|
||||||
|
</label>
|
||||||
|
<label class="s-res-filter-option">
|
||||||
|
<input type="radio" name="sort" value="oldest" class="sort-radio" <?php echo $currentSort === 'oldest' ? 'checked' : ''; ?>>
|
||||||
|
<span>Älteste Beiträge</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
</div>
|
||||||
<?php
|
|
||||||
else: ?>
|
|
||||||
<p> Es sind noch keine Beiträge in dieser Kategorie enthalten.</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
<noscript>
|
||||||
|
<button type="submit" class="nav__search-button">Anwenden</button>
|
||||||
|
</noscript>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="s-res-main-content">
|
||||||
|
|
||||||
|
<div class="s-res-header">
|
||||||
|
<h1 class="s-res-main-title">Kategorie: <?php echo htmlspecialchars($category); ?></h1>
|
||||||
|
<p class="s-res-meta">
|
||||||
|
<span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer in dieser Kategorie<?php echo $query !== '' ? ' für "' . htmlspecialchars($query) . '"' : ''; ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p
|
||||||
|
id="results-loading"
|
||||||
|
class="results-loading"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<span class="results-spinner" aria-hidden="true"></span>
|
||||||
|
Kategoriebeiträge werden geladen …
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 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="s-res-arrow">→</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?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>
|
||||||
|
|
||||||
|
<nav class="s-res-page-navigation" aria-label="Seitennavigation">
|
||||||
|
<!-- No-JS-Fallback: -->
|
||||||
|
<noscript>
|
||||||
|
<?php echo renderNoJsPaginationCategory($currentPage, $totalPages, $category, $currentSort, $query, $limit); ?>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<!-- JS-Version: wird per js/showCategory.js befüllt/eingeblendet: -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
+59
-25
@@ -5,13 +5,30 @@ if (!isset($_SESSION["user"])) {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
include_once 'php/controller/showArticle-controller.php';
|
include_once 'php/controller/showArticle-controller.php';
|
||||||
|
|
||||||
|
// Aktuelle Blockliste ermitteln:
|
||||||
|
$rawContent = '[]';
|
||||||
|
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])) {
|
||||||
|
$rawContent = $_SESSION['old_content'];
|
||||||
|
unset($_SESSION['old_content']);
|
||||||
|
} elseif (isset($content) && !empty($content)) {
|
||||||
|
$rawContent = $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
$blocks = json_decode($rawContent, true);
|
||||||
|
if (!is_array($blocks)) {
|
||||||
|
$blocks = [];
|
||||||
|
}
|
||||||
|
$blocks = array_values($blocks);
|
||||||
?>
|
?>
|
||||||
<!--
|
<!--
|
||||||
Seite: Beitrag erstellen
|
Seite: Beitrag bearbeiten
|
||||||
Inhalt: Formular für die Erstellung eines neuen Beitrags
|
Inhalt: Formular für die Bearbeitung eines Beitrags
|
||||||
-->
|
-->
|
||||||
<form method="post" action="php/controller/updateArticle-controller.php?id=<?php if(isset($id) && !empty($id)){echo htmlspecialchars($id);}else{$_SESSION["message"] = "missing_id";} ?>" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container">
|
<form method="post" action="php/controller/updateArticle-controller.php?id=<?php if(isset($id) && !empty($id)){echo htmlspecialchars($id);}else{$_SESSION["message"] = "missing_id";} ?>" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container">
|
||||||
|
|
||||||
|
<?php csrf_field(); ?>
|
||||||
|
|
||||||
<main class="editor-main">
|
<main class="editor-main">
|
||||||
<?php include_once "includes/alertMessages.php"?>
|
<?php include_once "includes/alertMessages.php"?>
|
||||||
|
|
||||||
@@ -27,32 +44,49 @@ include_once 'php/controller/showArticle-controller.php';
|
|||||||
?>"
|
?>"
|
||||||
placeholder="Titel hier eingeben" required>
|
placeholder="Titel hier eingeben" required>
|
||||||
|
|
||||||
<!-- Hier werden die dynamischen divs via JavaScript eingefügt -->
|
<!--
|
||||||
<div id="block-container"></div>
|
Content-Blöcke: werden serverseitig als echte, benannte Formularfelder gerendert
|
||||||
|
(blocks[i][type]...). Dadurch funktioniert das Hinzufügen/Entfernen von Blöcken und
|
||||||
|
der Bild-Upload auch ganz ohne JavaScript über einen normalen Formular-Submit.
|
||||||
|
Ist JavaScript aktiv, fängt js/editor.js diese Submits ab und erledigt dieselbe
|
||||||
|
Änderung lokal im DOM, ohne den Server zu belasten.
|
||||||
|
-->
|
||||||
|
<div id="block-container">
|
||||||
|
<?php foreach ($blocks as $i => $block): ?>
|
||||||
|
<?php
|
||||||
|
$blockType = $block['type'] ?? '';
|
||||||
|
if ($blockType !== 'text' && $blockType !== 'image') {
|
||||||
|
continue; // unbekannter/kaputter Block wird übersprungen
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="editor-block article-editor-scope" data-index="<?php echo (int)$i; ?>">
|
||||||
|
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][type]" value="<?php echo htmlspecialchars($blockType); ?>">
|
||||||
|
|
||||||
<!-- Plus-Button und das Pop-up-Menü -->
|
<?php if ($blockType === 'text'): ?>
|
||||||
<div id="add-block-control" class="article-editor-scope add-block-control">
|
<textarea name="blocks[<?php echo (int)$i; ?>][text]"
|
||||||
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
|
placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
|
||||||
<div id="block-popup" class="article-editor-scope block-popup hidden">
|
<?php else: /* image */ ?>
|
||||||
<button type="button" data-type="text">Textblock</button>
|
<?php if (!empty($block['value'])): ?>
|
||||||
<button type="button" data-type="image">Bild einfügen</button>
|
<img src="<?php echo htmlspecialchars($block['value']); ?>"
|
||||||
</div>
|
class="block-image-preview"
|
||||||
|
style="max-width:200px;display:block;margin-top:10px;">
|
||||||
|
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][existing_image]" value="<?php echo htmlspecialchars($block['value']); ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
<input type="file" name="blocks[<?php echo (int)$i; ?>][image]" accept="image/*">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<button type="submit" name="editor_action" value="delete_block:<?php echo (int)$i; ?>" class="delete-block-btn">✕</button>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Unsichtbares Textfeld, das die JSON-Daten hält und an den Controller postet -->
|
<div id="add-block-control" class="article-editor-scope add-block-control">
|
||||||
<textarea id="content" name="content" style="display:none;"><?php
|
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
|
||||||
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){
|
<div id="block-popup" class="article-editor-scope block-popup">
|
||||||
echo htmlspecialchars($_SESSION['old_content']);
|
<button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
|
||||||
unset($_SESSION['old_content']);
|
<button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
|
||||||
}elseif (isset($content) && !empty($content)){
|
</div>
|
||||||
echo htmlspecialchars($content);
|
</div>
|
||||||
} else {
|
|
||||||
echo '[]';
|
|
||||||
}
|
|
||||||
?></textarea>
|
|
||||||
|
|
||||||
<!-- unsichtbares Input, um die zu löschenden Bilder zu übergeben-->
|
|
||||||
<input type="hidden" id="deleted-images" name="deleted_images" value="[]">
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Seitenleiste -->
|
<!-- Seitenleiste -->
|
||||||
|
|||||||
+224
-5
@@ -154,21 +154,32 @@ h1 {
|
|||||||
|
|
||||||
.button {
|
.button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 14px;
|
||||||
background-color: #2563eb;
|
background-color: #2563eb;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: 2px solid transparent;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s, transform 0.2s, box-shadow 0.2s;
|
transition:
|
||||||
|
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(-2px);
|
transform: translateY(1px) scale(0.99);
|
||||||
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
|
box-shadow: 0 2px 5px rgba(37, 99, 235, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.register-link {
|
.register-link {
|
||||||
@@ -226,3 +237,211 @@ h1 {
|
|||||||
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: "▲ ";
|
||||||
|
}
|
||||||
|
.comment-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-loading[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-spinner {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid #cbd5e1;
|
||||||
|
border-top-color: #1f2937;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: comment-spinner-rotation 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes comment-spinner-rotation {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#comment-form button[type="submit"]:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
+112
-2
@@ -248,7 +248,7 @@ 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 */
|
||||||
@@ -285,6 +285,32 @@ 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;
|
||||||
@@ -302,5 +328,89 @@ 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -271,4 +271,33 @@ CSS für die Suchergebnis-Seite
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Ladeanzeige beim Nachladen von Such- und Kategorieergebnissen */
|
||||||
|
.results-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 15px 0;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-loading[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-spinner {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 2px solid #cbd5e1;
|
||||||
|
border-top-color: #3182ce;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: results-spinner-rotation 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes results-spinner-rotation {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
/* --- ZWEISPALTIIGES GRID-LAYOUT --- */
|
||||||
|
.cat-view-container {
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 95%; /* 95% Monitorbreite wie das Original */
|
||||||
|
width: 95%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
color: #212529;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
/* Grid für die Zweispaltigkeit */
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 320px 1fr; /* 320px Sidebar, Rest für Content */
|
||||||
|
gap: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-container * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- SEITENLEISTE (SORTIERUNG) --- */
|
||||||
|
.cat-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-sidebar-box {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-sidebar-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a202c;
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-filter-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-filter-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-filter-option input[type="radio"] {
|
||||||
|
margin: 0;
|
||||||
|
accent-color: #3182ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- HAUPTINHALT --- */
|
||||||
|
.cat-view-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #1a202c;
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-meta-text {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- BEITRÄGE LISTE --- */
|
||||||
|
.cat-view-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-item {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-item-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-link {
|
||||||
|
color: #3182ce;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: #2b6cb0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-meta-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-author {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #4a5568;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-author-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-likes {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-arrow {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #a0aec0;
|
||||||
|
padding-left: 1rem;
|
||||||
|
transition: color 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-item:hover .cat-view-arrow {
|
||||||
|
color: #3182ce;
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- FOOTER PAGINATION --- */
|
||||||
|
.cat-view-pagination-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-pagination-footer .s-res-page-navigation {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-pagination-footer .s-res-page-btn {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
color: #4a5568;
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-pagination-footer .s-res-page-btn:hover:not(:disabled) {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-pagination-footer .cat-active-btn {
|
||||||
|
background-color: #3182ce;
|
||||||
|
border-color: #3182ce;
|
||||||
|
color: #ffffff;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-view-pagination-footer .s-res-page-btn:disabled:not(.cat-active-btn) {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
color: #94a3b8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RESPONSIVE ANPASSUNG (Unter 768px) */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cat-view-container {
|
||||||
|
grid-template-columns: 1fr; /* Stapelt Sidebar und Inhalt untereinander */
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin: 1rem auto;
|
||||||
|
}
|
||||||
|
.cat-view-pagination-footer {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,11 +18,6 @@
|
|||||||
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).
|
||||||
@@ -94,6 +89,12 @@
|
|||||||
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"]);
|
||||||
?>
|
?>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Baut die Blockliste aus den POST-Daten (blocks[i][type], blocks[i][text],
|
||||||
|
* blocks[i][existing_image]) und ggf. hochgeladenen Dateien (blocks[i][image])
|
||||||
|
* zusammen. Läuft bei JEDEM Submit (Zwischen-Schritt "Block hinzufügen/löschen"
|
||||||
|
* UND finales Speichern/Veröffentlichen), damit neu ausgewählte Bilder in jedem
|
||||||
|
* Fall persistiert werden, bevor PHP die temporäre Upload-Datei nach
|
||||||
|
* Request-Ende verwirft.
|
||||||
|
*
|
||||||
|
* Wird sowohl vom createArticle- als auch vom updateArticle-Controller genutzt.
|
||||||
|
*
|
||||||
|
* @param array $postBlocks $_POST['blocks'] ?? []
|
||||||
|
* @param array $fileBlocks $_FILES['blocks'] ?? []
|
||||||
|
* @param string $uploadDir absoluter Pfad zum uploads-Verzeichnis (mit trailing slash)
|
||||||
|
* @return array Liste von ['type' => 'text'|'image', 'value' => string]
|
||||||
|
*/
|
||||||
|
function rebuildBlocksFromPost(array $postBlocks, array $fileBlocks, string $uploadDir): array {
|
||||||
|
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||||
|
|
||||||
|
$keys = array_keys($postBlocks);
|
||||||
|
if (isset($fileBlocks['name']) && is_array($fileBlocks['name'])) {
|
||||||
|
$keys = array_unique(array_merge($keys, array_keys($fileBlocks['name'])));
|
||||||
|
}
|
||||||
|
sort($keys, SORT_NUMERIC);
|
||||||
|
|
||||||
|
$blocks = [];
|
||||||
|
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$type = $postBlocks[$key]['type'] ?? null;
|
||||||
|
|
||||||
|
if ($type === 'text') {
|
||||||
|
$blocks[] = [
|
||||||
|
'type' => 'text',
|
||||||
|
'value' => $postBlocks[$key]['text'] ?? '',
|
||||||
|
];
|
||||||
|
} elseif ($type === 'image') {
|
||||||
|
// Vorbelegung: bereits vorhandenes Server-Bild (falls Datei nicht ersetzt wird)
|
||||||
|
$value = $postBlocks[$key]['existing_image'] ?? '';
|
||||||
|
|
||||||
|
$hasUpload = isset($fileBlocks['error'][$key]['image'])
|
||||||
|
&& $fileBlocks['error'][$key]['image'] === UPLOAD_ERR_OK;
|
||||||
|
|
||||||
|
if ($hasUpload) {
|
||||||
|
$tmpName = $fileBlocks['tmp_name'][$key]['image'];
|
||||||
|
$originalName = $fileBlocks['name'][$key]['image'];
|
||||||
|
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||||
|
if (!in_array($extension, $allowedExtensions, true)) {
|
||||||
|
$extension = 'jpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = 'img_' . uniqid() . '.' . $extension;
|
||||||
|
$destination = $uploadDir . $fileName;
|
||||||
|
|
||||||
|
if (move_uploaded_file($tmpName, $destination)) {
|
||||||
|
$value = 'uploads/' . $fileName;
|
||||||
|
}
|
||||||
|
// Bei Fehler: alter Wert (falls vorhanden) bleibt erhalten, Block wird nicht verworfen
|
||||||
|
}
|
||||||
|
|
||||||
|
$blocks[] = [
|
||||||
|
'type' => 'image',
|
||||||
|
'value' => $value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// unbekannter/fehlender type -> Block wird ignoriert
|
||||||
|
}
|
||||||
|
|
||||||
|
return $blocks;
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<?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)) . '">«</a> ';
|
||||||
|
} else {
|
||||||
|
$html .= '<span class="s-res-page-btn" aria-disabled="true">«</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)) . '">»</a>';
|
||||||
|
} else {
|
||||||
|
$html .= '<span class="s-res-page-btn" aria-disabled="true">»</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)) . '">«</a> ';
|
||||||
|
} else {
|
||||||
|
$html .= '<span class="s-res-page-btn" aria-disabled="true">«</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)) . '">»</a>';
|
||||||
|
} else {
|
||||||
|
$html .= '<span class="s-res-page-btn" aria-disabled="true">»</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
@@ -2,98 +2,51 @@
|
|||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
ob_start();
|
include_once "includes/csrf.php";
|
||||||
include_once("php/controller/index.php");
|
include_once "php/controller/index-controller.php";
|
||||||
|
|
||||||
$pfad = $_GET["pfad"] ?? "home";
|
|
||||||
|
|
||||||
/*
|
|
||||||
Controller für Aktionen werden vor der HTML-Ausgabe geladen,
|
|
||||||
damit Weiterleitungen mit header() funktionieren.
|
|
||||||
*/
|
|
||||||
if ($pfad === "login") {
|
|
||||||
include_once "php/controller/login-controller.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();
|
|
||||||
}
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!--
|
<!DOCTYPE html>
|
||||||
Seite: Index der Lernplattform
|
<html lang="de">
|
||||||
Funktion: Webseitengerüst, Anzeigen von Content
|
<head>
|
||||||
-->
|
<meta charset="utf-8">
|
||||||
<!DOCTYPE html>
|
<meta name="description" content="EduForge">
|
||||||
<html lang="de">
|
<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">
|
||||||
|
|
||||||
<head>
|
<link rel="stylesheet" href="css/main.css">
|
||||||
<meta charset="utf-8">
|
<link rel="stylesheet" href="css/navbar.css">
|
||||||
<meta name="description" content="EduForge">
|
<link rel="stylesheet" href="css/footer.css">
|
||||||
<meta name="author" content="Niklas Ortmann">
|
<link rel="stylesheet" href="css/search-results.css">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<link rel="stylesheet" href="css/createArticle.css">
|
||||||
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
|
<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">
|
||||||
|
|
||||||
<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">
|
|
||||||
|
|
||||||
<script src="js/paginator.js" async></script>
|
|
||||||
<script src="js/sorter.js" async></script>
|
|
||||||
<script src="js/comments.js" defer></script>
|
<script src="js/comments.js" defer></script>
|
||||||
<script src="js/editor.js" async></script>
|
<script src="js/editor.js" async></script>
|
||||||
<script src="js/filter.js" async></script>
|
<script src="js/search-results.js" async></script>
|
||||||
|
<script src="js/showCategory.js" async></script>
|
||||||
|
|
||||||
<title>EduForge</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<title>EduForge</title>
|
||||||
|
</head>
|
||||||
<?php
|
<body>
|
||||||
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
|
<?php
|
||||||
ob_end_flush();
|
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';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
+253
-43
@@ -11,20 +11,43 @@ 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 || !replyInfo) {
|
const submitButton = form
|
||||||
|
? form.querySelector('button[type="submit"]')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const loadingMessage = document.getElementById("comment-loading");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!form
|
||||||
|
|| !commentsList
|
||||||
|
|| !commentContent
|
||||||
|
|| !parentCommentInput
|
||||||
|
|| !submitButton
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aktiviert einen einzelnen Antworten-Button.
|
* Aktiviert einen einzelnen Antworten-Link.
|
||||||
*
|
*
|
||||||
* @param {HTMLButtonElement} button Antworten-Button
|
* @param {HTMLAnchorElement} replyLink Antworten-Link
|
||||||
*/
|
*/
|
||||||
function registerReplyButton(button) {
|
function registerReplyButton(replyLink) {
|
||||||
button.addEventListener("click", function () {
|
replyLink.addEventListener("click", function (event) {
|
||||||
parentCommentInput.value = button.dataset.commentId;
|
/*
|
||||||
replyInfo.textContent = "Antwort auf " + button.dataset.author;
|
* Mit JavaScript wird die Seite nicht neu geladen.
|
||||||
replyInfo.style.display = "block";
|
* Ohne JavaScript funktioniert der normale Link.
|
||||||
|
*/
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
parentCommentInput.value = replyLink.dataset.commentId;
|
||||||
|
|
||||||
|
if (replyInfo) {
|
||||||
|
replyInfo.textContent =
|
||||||
|
"Antwort auf " + replyLink.dataset.author;
|
||||||
|
replyInfo.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
commentContent.focus();
|
commentContent.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -42,21 +65,49 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
form.addEventListener("submit", function (event) {
|
form.addEventListener("submit", function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Verhindert einen erneuten Submit, während der vorherige
|
||||||
|
* Kommentar noch gespeichert wird.
|
||||||
|
*/
|
||||||
|
if (submitButton.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalButtonText = submitButton.textContent;
|
||||||
|
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.textContent = "Wird gesendet …";
|
||||||
|
|
||||||
|
if (loadingMessage) {
|
||||||
|
loadingMessage.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const parentCommentId = parentCommentInput.value;
|
const parentCommentId = parentCommentInput.value;
|
||||||
|
|
||||||
fetch("php/ajax/add-comment.php", {
|
fetch(form.action, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
"X-Requested-With": "XMLHttpRequest"
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(function (response) {
|
||||||
.then(data => {
|
if (!response.ok) {
|
||||||
|
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(".no-comments-message");
|
const emptyMessage = commentsList.querySelector(
|
||||||
|
".no-comments-message"
|
||||||
|
);
|
||||||
|
|
||||||
if (emptyMessage) {
|
if (emptyMessage) {
|
||||||
emptyMessage.remove();
|
emptyMessage.remove();
|
||||||
@@ -64,49 +115,196 @@ 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>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const parentReplies = document.querySelector(
|
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
|
||||||
|
|
||||||
|
<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>
|
|
||||||
|
|
||||||
<button type="button"
|
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
|
||||||
class="reply-button"
|
|
||||||
data-comment-id="${escapeHtml(data.commentId)}"
|
|
||||||
data-author="${escapeHtml(data.author)}">
|
|
||||||
Antworten
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="comment-replies"></div>
|
<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"
|
||||||
|
>
|
||||||
|
<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 = commentElement.querySelector(".reply-button");
|
const newReplyButton =
|
||||||
|
commentElement.querySelector(".reply-button");
|
||||||
|
|
||||||
if (newReplyButton) {
|
if (newReplyButton) {
|
||||||
registerReplyButton(newReplyButton);
|
registerReplyButton(newReplyButton);
|
||||||
@@ -115,23 +313,35 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
|
|
||||||
commentContent.value = "";
|
commentContent.value = "";
|
||||||
parentCommentInput.value = "";
|
parentCommentInput.value = "";
|
||||||
replyInfo.textContent = "";
|
|
||||||
replyInfo.style.display = "none";
|
if (replyInfo) {
|
||||||
|
replyInfo.textContent = "";
|
||||||
|
replyInfo.style.display = "none";
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(function (error) {
|
||||||
|
console.error(error);
|
||||||
alert("Kommentar konnte nicht gesendet werden.");
|
alert("Kommentar konnte nicht gesendet werden.");
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.textContent = originalButtonText;
|
||||||
|
|
||||||
|
if (loadingMessage) {
|
||||||
|
loadingMessage.hidden = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entfernt HTML-Sonderzeichen aus Nutzereingaben.
|
* Entfernt HTML-Sonderzeichen aus Nutzereingaben.
|
||||||
*
|
*
|
||||||
* @param {string} text Zu bereinigender Text
|
* @param {*} 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 = text;
|
div.textContent = String(text ?? "");
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
+82
-121
@@ -1,172 +1,133 @@
|
|||||||
console.log("Die JavaScript-Datei wurde erfolgreich geladen!");
|
//console.log("editor.js wurde erfolgreich geladen!");
|
||||||
|
|
||||||
function initEditor() {
|
function initEditor() {
|
||||||
const form = document.getElementById("editor-form");
|
const form = document.getElementById("editor-form");
|
||||||
|
|
||||||
if (!form) {
|
if (!form) {
|
||||||
console.error("Skript abgebrochen: Formular nicht gefunden!");
|
console.error("editor.js abgebrochen: Formular nicht gefunden!");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
console.log("Formular gefunden und Editor initialisiert:", form);
|
console.log("Formular gefunden und editor.js initialisiert:", form);
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = document.getElementById("block-container");
|
const container = document.getElementById("block-container");
|
||||||
const plusButton = document.getElementById("plus-button");
|
const plusButton = document.getElementById("plus-button");
|
||||||
const popup = document.getElementById("block-popup");
|
const popup = document.getElementById("block-popup");
|
||||||
const hiddenContentInput = document.getElementById("content");
|
|
||||||
|
|
||||||
const initialImages = [];
|
// Fortlaufender Zähler für eindeutige Block-Indizes. Wird nie wiederverwendet
|
||||||
|
// (auch nicht nach dem Löschen eines Blocks), damit sich neue und übrig
|
||||||
|
// gebliebene Blöcke beim finalen Submit nie einen Namen teilen.
|
||||||
|
let blockIndex = container.querySelectorAll(".editor-block").length;
|
||||||
|
|
||||||
|
// Ohne JS sind Textblock-/Bild-Button im Pop-up immer sichtbar und ganz normale
|
||||||
|
// Submit-Buttons. Erst mit JS blenden wir das Pop-up standardmäßig aus und
|
||||||
|
// steuern die Sichtbarkeit über den Plus-Button.
|
||||||
|
popup.classList.add("hidden");
|
||||||
|
|
||||||
// Pop-up umschalten bei Klick auf das Plus
|
|
||||||
plusButton.addEventListener("click", () => {
|
plusButton.addEventListener("click", () => {
|
||||||
popup.classList.toggle("hidden");
|
popup.classList.toggle("hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Klick auf eine Block-Option im Pop-up
|
// Klick auf "Textblock" / "Bild einfügen": lokal im DOM anlegen statt zum
|
||||||
popup.querySelectorAll("button").forEach(btn => {
|
// Server zu submitten (entlastet den Server, kein Page-Reload nötig).
|
||||||
btn.addEventListener("click", function() {
|
popup.querySelectorAll('[name="editor_action"]').forEach(btn => {
|
||||||
|
btn.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
const type = this.getAttribute("data-type");
|
const type = this.getAttribute("data-type");
|
||||||
addBlockElement(type, "");
|
addBlockElement(type, "");
|
||||||
popup.classList.add("hidden");
|
popup.classList.add("hidden");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Erstellt ein visuelles HTML-Element im Editor
|
// Bereits vom Server gerenderte Blöcke (z.B. beim Bearbeiten eines bestehenden
|
||||||
|
// Artikels oder nach einem Validierungsfehler) ebenfalls mit JS-Verhalten ausstatten.
|
||||||
|
container.querySelectorAll(".editor-block").forEach(blockDiv => {
|
||||||
|
bindDeleteButton(blockDiv);
|
||||||
|
bindImageInput(blockDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Erstellt einen neuen Block inkl. echter, benannter Formularfelder
|
||||||
|
// (blocks[i][type], blocks[i][text] bzw. blocks[i][image]). Diese Felder werden
|
||||||
|
// beim finalen Submit ganz normal vom Browser als multipart/form-data verschickt –
|
||||||
|
// es ist kein manuelles Zusammenbauen von JSON beim Absenden mehr nötig.
|
||||||
function addBlockElement(type, value = "") {
|
function addBlockElement(type, value = "") {
|
||||||
|
const index = blockIndex++;
|
||||||
|
|
||||||
const blockDiv = document.createElement("div");
|
const blockDiv = document.createElement("div");
|
||||||
blockDiv.classList.add("editor-block");
|
blockDiv.classList.add("editor-block", "article-editor-scope");
|
||||||
blockDiv.setAttribute("data-type", type);
|
blockDiv.setAttribute("data-index", String(index));
|
||||||
|
|
||||||
// Wenn es ein existierendes Server-Bild beim Laden ist, Pfad im globalen Array sichern
|
const typeInput = document.createElement("input");
|
||||||
if (type === "image" && value && typeof value === 'string' && value.startsWith('uploads/')) {
|
typeInput.type = "hidden";
|
||||||
initialImages.push(value);
|
typeInput.name = `blocks[${index}][type]`;
|
||||||
blockDiv.setAttribute("data-value", value);
|
typeInput.value = type;
|
||||||
}
|
blockDiv.appendChild(typeInput);
|
||||||
|
|
||||||
// Löschen-Button
|
|
||||||
const deleteBtn = document.createElement("button");
|
|
||||||
deleteBtn.type = "button";
|
|
||||||
deleteBtn.innerHTML = "✕";
|
|
||||||
deleteBtn.classList.add("delete-block-btn");
|
|
||||||
deleteBtn.addEventListener("click", () => {
|
|
||||||
// ANPASSUNG 2B: Logik hier komplett geleert. Das '✕' entfernt den Block jetzt nur noch sicher aus dem HTML.
|
|
||||||
blockDiv.remove();
|
|
||||||
});
|
|
||||||
blockDiv.appendChild(deleteBtn);
|
|
||||||
|
|
||||||
if (type === "text") {
|
if (type === "text") {
|
||||||
const textarea = document.createElement("textarea");
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.name = `blocks[${index}][text]`;
|
||||||
textarea.placeholder = "Schreibe deinen Textblock...";
|
textarea.placeholder = "Schreibe deinen Textblock...";
|
||||||
textarea.value = value;
|
textarea.value = value;
|
||||||
blockDiv.appendChild(textarea);
|
blockDiv.appendChild(textarea);
|
||||||
} else if (type === "image") {
|
} else if (type === "image") {
|
||||||
const fileInput = document.createElement("input");
|
const fileInput = document.createElement("input");
|
||||||
fileInput.type = "file";
|
fileInput.type = "file";
|
||||||
|
fileInput.name = `blocks[${index}][image]`;
|
||||||
fileInput.accept = "image/*";
|
fileInput.accept = "image/*";
|
||||||
|
|
||||||
const imgPreview = document.createElement("img");
|
|
||||||
imgPreview.style.maxWidth = "200px";
|
|
||||||
imgPreview.style.display = "block";
|
|
||||||
imgPreview.style.marginTop = "10px";
|
|
||||||
|
|
||||||
if (value && typeof value === 'string') {
|
|
||||||
if (value.startsWith('uploads/') || value.startsWith('data:image/')) {
|
|
||||||
imgPreview.src = value;
|
|
||||||
blockDiv.setAttribute("data-value", value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInput.addEventListener("change", function() {
|
|
||||||
if (this.files && this.files[0]) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function(e) {
|
|
||||||
imgPreview.src = e.target.result;
|
|
||||||
blockDiv.setAttribute("data-value", e.target.result);
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(this.files[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
blockDiv.appendChild(fileInput);
|
blockDiv.appendChild(fileInput);
|
||||||
blockDiv.appendChild(imgPreview);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteBtn = document.createElement("button");
|
||||||
|
deleteBtn.type = "submit";
|
||||||
|
deleteBtn.name = "editor_action";
|
||||||
|
deleteBtn.value = `delete_block:${index}`;
|
||||||
|
deleteBtn.classList.add("delete-block-btn");
|
||||||
|
deleteBtn.innerHTML = "✕";
|
||||||
|
blockDiv.appendChild(deleteBtn);
|
||||||
|
|
||||||
container.appendChild(blockDiv);
|
container.appendChild(blockDiv);
|
||||||
|
|
||||||
|
bindDeleteButton(blockDiv);
|
||||||
|
bindImageInput(blockDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
// beim Abschicken verbleibende Blöcke auslesen UND gelöschte Bilder ermitteln
|
// Löschen-Button eines Blocks lokal abfangen: entfernt den Block nur aus dem DOM,
|
||||||
form.addEventListener("submit", function(e) {
|
// statt das Formular zum Server zu senden.
|
||||||
const blocks = [];
|
function bindDeleteButton(blockDiv) {
|
||||||
const currentImages = [];
|
const btn = blockDiv.querySelector(".delete-block-btn");
|
||||||
|
if (!btn) return;
|
||||||
// alle aktuell im Formular verbliebenen Blöcke scannen
|
btn.addEventListener("click", (e) => {
|
||||||
container.querySelectorAll(".editor-block").forEach(blockDiv => {
|
e.preventDefault();
|
||||||
const type = blockDiv.getAttribute("data-type");
|
blockDiv.remove();
|
||||||
let value = "";
|
|
||||||
|
|
||||||
if (type === "text") {
|
|
||||||
value = blockDiv.querySelector("textarea").value;
|
|
||||||
} else if (type === "image") {
|
|
||||||
|
|
||||||
const imgTag = blockDiv.querySelector("img");
|
|
||||||
if (imgTag) {
|
|
||||||
const srcValue = imgTag.getAttribute("src") || "";
|
|
||||||
// Wenn es ein neues Bild ist, nutzen wir das data-value (Base64)
|
|
||||||
if (srcValue.startsWith('data:image/')) {
|
|
||||||
value = blockDiv.getAttribute("data-value") || "";
|
|
||||||
} else {
|
|
||||||
value = srcValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pfade sammeln, die der Nutzer NICHT gelöscht hat (für den Abgleich)
|
|
||||||
if (value && value.startsWith('uploads/')) {
|
|
||||||
currentImages.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blocks.push({ type: type, value: value });
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// das reguläre unsichtbare Content-Feld befüllen
|
// Zeigt bei Auswahl einer Bilddatei sofort eine Vorschau an. Rein optisch –
|
||||||
hiddenContentInput.value = JSON.stringify(blocks);
|
// der eigentliche Datei-Upload läuft nativ über das <input type="file">.
|
||||||
|
function bindImageInput(blockDiv) {
|
||||||
|
const fileInput = blockDiv.querySelector('input[type="file"]');
|
||||||
|
if (!fileInput) return;
|
||||||
|
|
||||||
// Differenz berechnen: Welche Bilder aus 'initialImages' fehlen in 'currentImages' ?
|
let imgPreview = blockDiv.querySelector(".block-image-preview");
|
||||||
const deletedImages = initialImages.filter(img => !currentImages.includes(img));
|
if (!imgPreview) {
|
||||||
|
imgPreview = document.createElement("img");
|
||||||
// das 'deleted_images'-Feld dynamisch befüllen und an den Controller senden
|
imgPreview.classList.add("block-image-preview");
|
||||||
let deletedInput = document.getElementById("deleted-images");
|
imgPreview.style.maxWidth = "200px";
|
||||||
if (!deletedInput) {
|
imgPreview.style.display = "none";
|
||||||
deletedInput = document.createElement("input");
|
imgPreview.style.marginTop = "10px";
|
||||||
deletedInput.type = "hidden";
|
blockDiv.insertBefore(imgPreview, fileInput.nextSibling);
|
||||||
deletedInput.id = "deleted-images";
|
|
||||||
deletedInput.name = "deleted_images";
|
|
||||||
form.appendChild(deletedInput);
|
|
||||||
}
|
}
|
||||||
deletedInput.value = JSON.stringify(deletedImages);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Existierende Blöcke laden (stellt alte Daten aus der Session wieder her)
|
fileInput.addEventListener("change", function () {
|
||||||
try {
|
if (this.files && this.files[0]) {
|
||||||
const initialBlocks = JSON.parse(hiddenContentInput.value.trim());
|
const reader = new FileReader();
|
||||||
if (Array.isArray(initialBlocks)) {
|
reader.onload = (e) => {
|
||||||
initialBlocks.forEach(b => {
|
imgPreview.src = e.target.result;
|
||||||
if (b.type === "image" && b.value && typeof b.value === 'string' && !b.value.startsWith('data:image/')) {
|
imgPreview.style.display = "block";
|
||||||
let cleanPath = b.value.trim().replace(/\\\//g, '/'); // Verwandelt \/ in /
|
};
|
||||||
|
reader.readAsDataURL(this.files[0]);
|
||||||
initialImages.push(cleanPath);
|
}
|
||||||
addBlockElement(b.type, cleanPath);
|
});
|
||||||
|
|
||||||
} else {
|
|
||||||
addBlockElement(b.type, b.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log("Erfolgreich registrierte Start-Bilder:", initialImages);
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
if (hiddenContentInput.value.trim() !== "") {
|
|
||||||
addBlockElement("text", hiddenContentInput.value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-116
@@ -1,116 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
function initPaginator() {
|
|
||||||
const form = document.getElementById('search-form-id');
|
|
||||||
const pageInput = document.getElementById('s-res-page-input');
|
|
||||||
const pageButtons = document.querySelectorAll('.s-res-page-navigation .s-res-page-btn');
|
|
||||||
|
|
||||||
pageButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', function() {
|
|
||||||
if (this.disabled) return;
|
|
||||||
|
|
||||||
const targetPage = this.getAttribute('data-page');
|
|
||||||
|
|
||||||
if (targetPage && form && pageInput) {
|
|
||||||
pageInput.value = targetPage;
|
|
||||||
form.submit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ist das DOM bereits vollständig aufgebaut?
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
// Falls noch geladen wird, auf das Event warten
|
|
||||||
document.addEventListener('DOMContentLoaded', initPaginator);
|
|
||||||
} else {
|
|
||||||
// Falls das HTML bereits komplett da ist, sofort ausführen
|
|
||||||
initPaginator();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
/**
|
||||||
|
* Übernimmt Sortierung, Kategorie-Filter und Pagination der Suchergebnisse
|
||||||
|
* vollständig clientseitig
|
||||||
|
*
|
||||||
|
* Ablauf:
|
||||||
|
* 1. Kompletten Ergebnis-Datensatz einmalig per fetch() nachladen
|
||||||
|
* (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
|
||||||
|
* gegenseitig ins Gehege kamen.
|
||||||
|
* Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare)
|
||||||
|
* voll funktionsfähig.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
const state = {
|
||||||
|
allItems: [],
|
||||||
|
sort: 'alphabet',
|
||||||
|
category: 'all',
|
||||||
|
itemsPerPage: 10,
|
||||||
|
currentPage: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let listContainer, sortRadios, categorySelect, limitSelect;
|
||||||
|
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
|
||||||
|
let loadingElement;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
listContainer = document.querySelector('.s-res-list');
|
||||||
|
if (!listContainer) return;
|
||||||
|
|
||||||
|
sortRadios = document.querySelectorAll('.sort-radio');
|
||||||
|
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');
|
||||||
|
loadingElement = document.getElementById('results-loading');
|
||||||
|
|
||||||
|
const checkedRadio = document.querySelector('.sort-radio:checked');
|
||||||
|
state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
|
||||||
|
state.category = categorySelect ? categorySelect.value : 'all';
|
||||||
|
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
|
||||||
|
|
||||||
|
if (loadingElement) {
|
||||||
|
loadingElement.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchFullDataset()
|
||||||
|
.then(function (data) {
|
||||||
|
state.allItems = data.results || [];
|
||||||
|
attachEvents();
|
||||||
|
render();
|
||||||
|
|
||||||
|
if (jsNav) {
|
||||||
|
jsNav.style.display = 'flex';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
/*
|
||||||
|
* Falls das Nachladen nicht funktioniert, bleibt die bereits
|
||||||
|
* serverseitig ausgegebene Ergebnisliste sichtbar.
|
||||||
|
*/
|
||||||
|
console.error(
|
||||||
|
'Suchergebnisse konnten nicht nachgeladen werden:',
|
||||||
|
err
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
if (loadingElement) {
|
||||||
|
loadingElement.hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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 (categorySelect) {
|
||||||
|
categorySelect.addEventListener('change', function () {
|
||||||
|
state.category = this.value;
|
||||||
|
state.currentPage = 1;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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/Filter live
|
||||||
|
// ein echtes Absenden des Formulars (No-JS-Fallback) ist dann nicht mehr nötig.
|
||||||
|
const filterForm = document.getElementById('s-res-filter-form');
|
||||||
|
if (filterForm) {
|
||||||
|
filterForm.addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
/**
|
||||||
|
* Ü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 loadingElement;
|
||||||
|
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');
|
||||||
|
loadingElement = document.getElementById('results-loading');
|
||||||
|
|
||||||
|
const checkedRadio = document.querySelector('.sort-radio:checked');
|
||||||
|
state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
|
||||||
|
state.query = searchInput ? searchInput.value : '';
|
||||||
|
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
|
||||||
|
|
||||||
|
if (loadingElement) {
|
||||||
|
loadingElement.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchFullDataset()
|
||||||
|
.then(function (data) {
|
||||||
|
state.allItems = data.results || [];
|
||||||
|
attachEvents();
|
||||||
|
render();
|
||||||
|
|
||||||
|
if (jsNav) {
|
||||||
|
jsNav.style.display = 'flex';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
/*
|
||||||
|
* Falls das Nachladen nicht funktioniert, bleibt die bereits
|
||||||
|
* serverseitig ausgegebene Kategorieansicht sichtbar.
|
||||||
|
*/
|
||||||
|
console.error(
|
||||||
|
'Kategorie-Beiträge konnten nicht nachgeladen werden:',
|
||||||
|
err
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
if (loadingElement) {
|
||||||
|
loadingElement.hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
+222
-30
@@ -3,32 +3,203 @@ 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;
|
/*
|
||||||
$content = trim($_POST["content"] ?? "");
|
* Nur POST-Anfragen dürfen Kommentare erstellen.
|
||||||
$parentCommentId = $_POST["parent_comment_id"] ?? null;
|
*/
|
||||||
|
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
|
||||||
|
sendCommentResponse(
|
||||||
|
false,
|
||||||
|
"Ungültige Anfrage.",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ($parentCommentId === "" || $parentCommentId === "0") {
|
/*
|
||||||
|
* 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"] ?? "");
|
||||||
|
|
||||||
|
$parentCommentId = filter_input(
|
||||||
|
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 (empty($articleId) || empty($content)) {
|
if ($articleId === false || $articleId === null) {
|
||||||
echo json_encode([
|
sendCommentResponse(
|
||||||
"success" => false,
|
false,
|
||||||
"message" => "Kommentar darf nicht leer sein."
|
"Der zugehörige Beitrag ist ungültig.",
|
||||||
]);
|
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 {
|
||||||
@@ -41,18 +212,39 @@ try {
|
|||||||
$parentCommentId
|
$parentCommentId
|
||||||
);
|
);
|
||||||
|
|
||||||
echo json_encode([
|
$userManager = UserManager::getInstance();
|
||||||
"success" => true,
|
$user = $userManager->findUser($_SESSION["user_email"]);
|
||||||
"commentId" => $commentId,
|
|
||||||
"author" => $_SESSION["user_email"],
|
|
||||||
"content" => $content,
|
|
||||||
"created" => date("Y-m-d H:i:s"),
|
|
||||||
"parentCommentId" => $parentCommentId
|
|
||||||
]);
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
$authorName = $_SESSION["user_email"];
|
||||||
echo json_encode([
|
|
||||||
"success" => false,
|
if ($user !== null) {
|
||||||
"message" => "Kommentar konnte nicht gespeichert werden."
|
$vorname = trim($user["vorname"] ?? "");
|
||||||
]);
|
$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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -5,134 +5,128 @@ 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(!isset($_POST["title"]) ||!isset($_POST["content"]) || !isset($_POST["category"])){
|
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||||
$_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 {
|
||||||
$title = $_POST["title"];
|
$cleanedTags = [];
|
||||||
$content = $_POST["content"];
|
$rawTags = explode(',', $tags);
|
||||||
$author = $_SESSION["user_email"];
|
foreach ($rawTags as $rawTag) {
|
||||||
$category = $_POST["category"];
|
// Leerzeichen am Anfang/Ende des einzelnen Tags entfernen:
|
||||||
$tags = $_POST['tags'] ?? '';
|
$tag = trim($rawTag);
|
||||||
|
$cleanedTags[] = $tag;
|
||||||
// -------------------------------- Validierung der Daten: -------------------------
|
|
||||||
if (!articleTitleValidator($title)) {
|
|
||||||
$_SESSION["message"] = "invalid_title";
|
|
||||||
header("location: ../../index.php?pfad=createArticle");
|
|
||||||
exit();
|
|
||||||
}
|
}
|
||||||
|
// Duplikate entfernen:
|
||||||
|
$cleanedTags = array_unique($cleanedTags);
|
||||||
|
$cleanedTags = implode(',', $cleanedTags);
|
||||||
|
}
|
||||||
|
|
||||||
if (!articleContentValidator($content)) {
|
// ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
|
||||||
$_SESSION["message"] = "invalid_content";
|
try {
|
||||||
header("location: ../../index.php?pfad=createArticle");
|
$articleManager = ArticleManager::getInstance();
|
||||||
exit();
|
// $content enthält bereits die finalen "uploads/..."-Pfade (kein Base64 mehr),
|
||||||
}
|
// da rebuildBlocksFromPost() Datei-Uploads sofort verarbeitet.
|
||||||
|
$articleManager->addArticle($title, $content, $author, $category, $cleanedTags);
|
||||||
|
|
||||||
if (!articleCategoryValidator($category)) {
|
// Formulardaten nach erfolgreichem Erstellen aus der Session löschen
|
||||||
$_SESSION["message"] = "invalid_category";
|
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
|
||||||
header("location: ../../index.php?pfad=createArticle");
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!articleTagValidator($tags)) {
|
} catch (\Throwable $e) {
|
||||||
$_SESSION["message"] = "invalid_tags";
|
$_SESSION["message"] = "internal_error";
|
||||||
header("location: ../../index.php?pfad=createArticle");
|
header("location: ../../index.php?pfad=createArticle");
|
||||||
exit();
|
|
||||||
} else {
|
|
||||||
$cleanedTags = [];
|
|
||||||
$rawTags = explode(',', $tags);
|
|
||||||
foreach ($rawTags as $rawTag) {
|
|
||||||
// Leerzeichen am Anfang/Ende des einzelnen Tags entfernen:
|
|
||||||
$tag = trim($rawTag);
|
|
||||||
$cleanedTags[] = $tag;
|
|
||||||
}
|
|
||||||
// Duplikate entfernen:
|
|
||||||
$cleanedTags = array_unique($cleanedTags);
|
|
||||||
$cleanedTags = implode(',', $cleanedTags);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------- Base64-Bilder verarbeiten und auf Server speichern -----------------
|
|
||||||
$blocks = json_decode($content, true);
|
|
||||||
$uploadDir = __DIR__ . '/../../uploads/';
|
|
||||||
|
|
||||||
if (!file_exists($uploadDir)) {
|
|
||||||
mkdir($uploadDir, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($blocks)) {
|
|
||||||
foreach ($blocks as &$block) {
|
|
||||||
// sicherstellen, dass 'type' und 'value' existieren:
|
|
||||||
if (isset($block['type']) && isset($block['value']) && $block['type'] === 'image' && str_starts_with($block['value'], 'data:image/')) {
|
|
||||||
|
|
||||||
// Base64-String zerlegen
|
|
||||||
$parts = explode(',', $block['value']);
|
|
||||||
|
|
||||||
// falls der String korrupt ist und kein Komma hat
|
|
||||||
if (count($parts) < 2) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$metadata = $parts[0];
|
|
||||||
$base64Data = $parts[1];
|
|
||||||
|
|
||||||
// Dateiendung ermitteln
|
|
||||||
preg_match('/data:image\/(?<extension>.*?);/', $metadata, $matches);
|
|
||||||
$extension = $matches['extension'] ?? 'jpg';
|
|
||||||
if ($extension === 'jpeg') {
|
|
||||||
$extension = 'jpg';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eindeutigen Dateinamen generieren
|
|
||||||
$fileName = 'img_' . uniqid() . '.' . $extension;
|
|
||||||
$filePath = $uploadDir . $fileName;
|
|
||||||
|
|
||||||
// Datei im /uploads speichern:
|
|
||||||
if (file_put_contents($filePath, base64_decode($base64Data)) !== false) {
|
|
||||||
// temporären Base64-String durch den echten Pfad ersetzen
|
|
||||||
$block['value'] = 'uploads/' . $fileName;
|
|
||||||
} else {
|
|
||||||
$_SESSION["message"] = "image_upload_error";
|
|
||||||
header("location: ../../index.php?pfad=createArticle");
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unset($block);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiertes Array wieder in JSON konvertieren
|
|
||||||
$finalContent = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
|
||||||
|
|
||||||
// ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
|
|
||||||
try {
|
|
||||||
$articleManager = ArticleManager::getInstance();
|
|
||||||
$articleManager->addArticle($title, $content, $author, $category, $cleanedTags);
|
|
||||||
|
|
||||||
// Formulardaten nach erfolgreichem Erstellen aus der Session löschen
|
|
||||||
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
|
|
||||||
|
|
||||||
} catch (\Throwable $e){
|
|
||||||
$_SESSION["message"] = "internal_error";
|
|
||||||
header("location: ../../index.php?pfad=createArticle");
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$_SESSION["message"] = "new_article";
|
|
||||||
// Weiterleitung zur Homepage
|
|
||||||
header("location: ../../index.php");
|
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$_SESSION["message"] = "new_article";
|
||||||
|
// Weiterleitung zur Homepage
|
||||||
|
header("location: ../../index.php");
|
||||||
|
exit();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
@@ -5,12 +5,24 @@ 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,6 +4,7 @@ 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");
|
||||||
@@ -12,6 +13,13 @@ 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 {
|
||||||
@@ -22,9 +30,10 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($_POST["id"]) && !empty($_POST["id"])) {
|
// Die Beitrags-ID muss eine gültige numerische ID sein.
|
||||||
$id = $_POST["id"];
|
$id = filter_input(INPUT_POST, "id", FILTER_VALIDATE_INT);
|
||||||
} 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();
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<?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,3 +1,41 @@
|
|||||||
<?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";
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ 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"])) {
|
||||||
@@ -17,6 +23,13 @@ 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);
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
$_SESSION = [];
|
$_SESSION = [];
|
||||||
session_destroy();
|
session_destroy();
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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;
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ 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"] ?? "");
|
||||||
@@ -34,7 +36,12 @@ try {
|
|||||||
$nachname = trim($_POST["nachname"] ?? "");
|
$nachname = trim($_POST["nachname"] ?? "");
|
||||||
$password = $_POST["password"] ?? "";
|
$password = $_POST["password"] ?? "";
|
||||||
|
|
||||||
if (!userEmailValidator($newEmail)) {
|
if (!csrf_verify()) {
|
||||||
|
// 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";
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
$vorname = trim($_POST["vorname"] ?? "");
|
$vorname = trim($_POST["vorname"] ?? "");
|
||||||
$nachname = trim($_POST["nachname"] ?? "");
|
$nachname = trim($_POST["nachname"] ?? "");
|
||||||
$plainPassword = $_POST["password"] ?? "";
|
$plainPassword = $_POST["password"] ?? "";
|
||||||
|
$termsAccepted = $_POST["termsAccepted"] ?? null;
|
||||||
|
|
||||||
if (!userEmailValidator($email)) {
|
if (!userEmailValidator($email)) {
|
||||||
$error = "Bitte gib eine gültige E-Mail-Adresse ein.";
|
$error = "Bitte gib eine gültige E-Mail-Adresse ein.";
|
||||||
@@ -21,6 +22,8 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
$error = "Der Nachname muss 2 bis 20 Zeichen lang sein und darf nur Buchstaben, Umlaute, Leerzeichen und Bindestriche enthalten.";
|
$error = "Der Nachname muss 2 bis 20 Zeichen lang sein und darf nur Buchstaben, Umlaute, Leerzeichen und Bindestriche enthalten.";
|
||||||
} elseif (!userPasswordValidator($plainPassword)) {
|
} elseif (!userPasswordValidator($plainPassword)) {
|
||||||
$error = "Das Passwort muss 5 bis 12 Zeichen lang sein.";
|
$error = "Das Passwort muss 5 bis 12 Zeichen lang sein.";
|
||||||
|
} elseif ($termsAccepted !== "1") {
|
||||||
|
$error = "Bitte akzeptiere die Datenschutzerklärung und die Nutzungsbedingungen.";
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
$dao = UserManager::getInstance();
|
$dao = UserManager::getInstance();
|
||||||
@@ -30,23 +33,34 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
$existingUser = $dao->findUser($email);
|
$existingUser = $dao->findUser($email);
|
||||||
|
|
||||||
if (!is_dir("data/mails") && !mkdir("data/mails", 0777, true)) {
|
if (!is_dir("data/mails") && !mkdir("data/mails", 0777, true)) {
|
||||||
throw new RuntimeException("Ordner data/mails konnte nicht erstellt werden.");
|
throw new RuntimeException(
|
||||||
|
"Ordner data/mails konnte nicht erstellt werden."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_dir("data/pending") && !mkdir("data/pending", 0777, true)) {
|
if (!is_dir("data/pending") && !mkdir("data/pending", 0777, true)) {
|
||||||
throw new RuntimeException("Ordner data/pending konnte nicht erstellt werden.");
|
throw new RuntimeException(
|
||||||
|
"Ordner data/pending konnte nicht erstellt werden."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_writable("data/mails")) {
|
if (!is_writable("data/mails")) {
|
||||||
throw new RuntimeException("Ordner data/mails ist nicht beschreibbar.");
|
throw new RuntimeException(
|
||||||
|
"Ordner data/mails ist nicht beschreibbar."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_writable("data/pending")) {
|
if (!is_writable("data/pending")) {
|
||||||
throw new RuntimeException("Ordner data/pending ist nicht beschreibbar.");
|
throw new RuntimeException(
|
||||||
|
"Ordner data/pending ist nicht beschreibbar."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($existingUser === null) {
|
if ($existingUser === null) {
|
||||||
$password = password_hash($plainPassword, PASSWORD_DEFAULT);
|
$password = password_hash(
|
||||||
|
$plainPassword,
|
||||||
|
PASSWORD_DEFAULT
|
||||||
|
);
|
||||||
|
|
||||||
$pendingData = [
|
$pendingData = [
|
||||||
"email" => $email,
|
"email" => $email,
|
||||||
@@ -57,12 +71,18 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
|
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
"data/pending/" . $token . ".json",
|
"data/pending/" . $token . ".json",
|
||||||
json_encode($pendingData, JSON_PRETTY_PRINT)
|
json_encode(
|
||||||
|
$pendingData,
|
||||||
|
JSON_PRETTY_PRINT
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$mailContent = "
|
$mailContent = "
|
||||||
<h2>Registrierung bestätigen</h2>
|
<h2>Registrierung bestätigen</h2>
|
||||||
<p>Bitte ignorieren Sie diese Nachricht, wenn Sie sich nicht registrieren wollten.</p>
|
<p>
|
||||||
|
Bitte ignorieren Sie diese Nachricht,
|
||||||
|
wenn Sie sich nicht registrieren wollten.
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href='index.php?pfad=confirm-register&token=$token'>
|
<a href='index.php?pfad=confirm-register&token=$token'>
|
||||||
Registrierung bestätigen
|
Registrierung bestätigen
|
||||||
@@ -72,7 +92,10 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
} else {
|
} else {
|
||||||
$mailContent = "
|
$mailContent = "
|
||||||
<h2>Registrierung</h2>
|
<h2>Registrierung</h2>
|
||||||
<p>Bitte ignorieren Sie diese Nachricht, wenn Sie sich nicht registrieren wollten.</p>
|
<p>
|
||||||
|
Bitte ignorieren Sie diese Nachricht,
|
||||||
|
wenn Sie sich nicht registrieren wollten.
|
||||||
|
</p>
|
||||||
<p>Sie sind bereits registriert.</p>
|
<p>Sie sind bereits registriert.</p>
|
||||||
<p>
|
<p>
|
||||||
<a href='index.php?pfad=password-forgotten'>
|
<a href='index.php?pfad=password-forgotten'>
|
||||||
@@ -82,11 +105,17 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
";
|
";
|
||||||
}
|
}
|
||||||
|
|
||||||
file_put_contents("data/mails/" . $token . ".html", $mailContent);
|
file_put_contents(
|
||||||
|
"data/mails/" . $token . ".html",
|
||||||
|
$mailContent
|
||||||
|
);
|
||||||
|
|
||||||
// Neutrale Meldung, damit nicht sichtbar wird, ob die E-Mail bereits registriert ist.
|
// Neutrale Meldung, damit nicht sichtbar wird,
|
||||||
|
// ob die E-Mail bereits registriert ist.
|
||||||
$success = 'Weitere Infos finden Sie in der Datei
|
$success = 'Weitere Infos finden Sie in der Datei
|
||||||
<a href="index.php?pfad=show-mail&token=' . htmlspecialchars($token) . '" target="_blank">xy</a>.';
|
<a href="index.php?pfad=show-mail&token='
|
||||||
|
. htmlspecialchars($token)
|
||||||
|
. '" target="_blank">xy</a>.';
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$error = "Die Registrierung konnte nicht verarbeitet werden.";
|
$error = "Die Registrierung konnte nicht verarbeitet werden.";
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
require_once '../model/LocalArticleManager.php';
|
|
||||||
require_once '../model/ArticleManager.php';
|
require_once '../model/ArticleManager.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';
|
||||||
|
|
||||||
@@ -18,41 +19,17 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
|
|||||||
try {
|
try {
|
||||||
|
|
||||||
$articleManager = ArticleManager::getInstance();
|
$articleManager = ArticleManager::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 alphabetiisch 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 sont incomplete-PHP error 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" => $obj->getAuthor(),
|
"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(),
|
||||||
@@ -64,20 +41,25 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
|
|||||||
$_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;
|
||||||
}
|
}
|
||||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
$_SESSION['search_limit'] = $limit;
|
||||||
header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&limit=" . $limit . "&page=" . $page);
|
|
||||||
|
// Neue Suche -> immer wieder bei Seite 1 beginnen
|
||||||
|
header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&category=" . urlencode($category) . "&limit=" . $limit . "&page=1");
|
||||||
exit();
|
exit();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?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),
|
||||||
|
]);
|
||||||
@@ -5,11 +5,16 @@ if (session_status() === PHP_SESSION_NONE) {
|
|||||||
|
|
||||||
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/CommentManager.php';
|
require_once 'php/model/CommentManager.php';
|
||||||
|
require_once 'php/validator/article-validator.php';
|
||||||
|
|
||||||
if (isset($_GET["id"]) && !empty($_GET["id"])){
|
// Die übergebene ID muss eine gültige, positive Zahl sein, bevor sie
|
||||||
|
// 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){
|
||||||
@@ -19,6 +24,9 @@ if (isset($_GET["id"]) && !empty($_GET["id"])){
|
|||||||
$author = $article->getAuthor();
|
$author = $article->getAuthor();
|
||||||
$tags = $article->getTags();
|
$tags = $article->getTags();
|
||||||
$articleObj = $article; // Objekt für die Like-Abfagen sichern
|
$articleObj = $article; // Objekt für die Like-Abfagen sichern
|
||||||
|
|
||||||
|
$userManager = UserManager::getInstance();
|
||||||
|
$name = $userManager->findUser($author)["vorname"]." ".$userManager->findUser($author)["nachname"];
|
||||||
}else{
|
}else{
|
||||||
//header("location: index.php?pfad=404");
|
//header("location: index.php?pfad=404");
|
||||||
include_once "content/404.php";
|
include_once "content/404.php";
|
||||||
@@ -26,15 +34,15 @@ if (isset($_GET["id"]) && !empty($_GET["id"])){
|
|||||||
}
|
}
|
||||||
|
|
||||||
if($_GET["pfad"] == "updateArticle"){
|
if($_GET["pfad"] == "updateArticle"){
|
||||||
if($article->getAuthor() != $_SESSION["user"]){
|
if($article->getAuthor() != $_SESSION["user_email"]){
|
||||||
$_SESSION["message"] = "unauthorized_access";
|
$_SESSION["message"] = "unauthorized_access";
|
||||||
header("location: ../../index.php");
|
header("location: index.php");
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$commentManager = CommentManager::getInstance();
|
$commentManager = CommentManager::getInstance();
|
||||||
$comments = $commentManager->getCommentsByArticle($_GET["id"]);
|
$comments = $commentManager->getCommentsByArticle($id); // NEU: validierte ID statt rohem $_GET["id"]
|
||||||
|
|
||||||
foreach ($comments as $comment) {
|
foreach ($comments as $comment) {
|
||||||
if ($comment->isReply()) {
|
if ($comment->isReply()) {
|
||||||
@@ -45,8 +53,9 @@ if (isset($_GET["id"]) && !empty($_GET["id"])){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception $e){
|
} catch (Throwable $e) {
|
||||||
$_SESSION["message"] = "internal_error";
|
$_SESSION["message"] = "internal_error";
|
||||||
|
header("Location: index.php");
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
|
|||||||
@@ -1,21 +1,52 @@
|
|||||||
<?php
|
<?php
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
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/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"];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$articleManager = ArticleManager::getInstance();
|
$articleManager = ArticleManager::getInstance();
|
||||||
$articles = $articleManager->getArticlesByCategory($category);
|
$userManager = UserManager::getInstance();
|
||||||
|
|
||||||
|
$allArticles = $articleManager->getArticlesByCategory($category);
|
||||||
|
|
||||||
|
$safeArrayResults = [];
|
||||||
|
foreach ($allArticles as $article) {
|
||||||
|
$authorName = $userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"];
|
||||||
|
$safeArrayResults[] = [
|
||||||
|
"id" => $article->getID(),
|
||||||
|
"title" => $article->getTitle(),
|
||||||
|
"content" => $article->getContent(),
|
||||||
|
"author" => $authorName,
|
||||||
|
"category" => $article->getCategory(),
|
||||||
|
"tags" => $article->getTags(),
|
||||||
|
"creationDate" => $article->getCreationDate(),
|
||||||
|
"likes" => $article->getLikes(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session dient hier (wie bei search-results) als Zwischenspeicher, damit
|
||||||
|
// js/showCategory.js den kompletten Datensatz per showCategory-data.php
|
||||||
|
// nachladen kann, ohne die Kategorie-Abfrage ein zweites Mal auszuführen.
|
||||||
|
$_SESSION['category_results'] = $safeArrayResults;
|
||||||
|
$_SESSION['category_name'] = $category;
|
||||||
|
|
||||||
} 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();
|
||||||
}
|
}
|
||||||
?>
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?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),
|
||||||
|
]);
|
||||||
@@ -7,21 +7,27 @@ 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_email"])) {
|
if (!isset($_SESSION["user"])) {
|
||||||
header("Location: index.php?pfad=login");
|
header("Location: index.php?pfad=login");
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||||
$_SESSION["old_title"] = $_POST["title"] ?? '';
|
|
||||||
$_SESSION["old_content"] = $_POST["content"] ?? '';
|
|
||||||
$_SESSION["old_category"] = $_POST["category"] ?? '';
|
|
||||||
$_SESSION["old_tags"] = $_POST["tags"] ?? '';
|
|
||||||
|
|
||||||
if (isset($_GET["id"]) && !empty($_GET["id"])) {
|
// CSRF-Token prüfen, bevor irgendeine Änderung vorgenommen wird
|
||||||
$id = $_GET["id"];
|
if (!csrf_verify()) {
|
||||||
} else {
|
$_SESSION["message"] = "invalid_csrf_token";
|
||||||
|
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();
|
||||||
@@ -30,6 +36,14 @@ 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");
|
||||||
@@ -41,16 +55,56 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($_POST["title"]) ||!isset($_POST["content"]) || !isset($_POST["category"])){
|
$uploadDir = __DIR__ . '/../../uploads/';
|
||||||
|
if (!file_exists($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formularzustand (Titel/Tags/Kategorie/Blöcke) immer sichern, damit er nach einem
|
||||||
|
// Redirect (PRG-Pattern oder Validierungsfehler) wieder angezeigt werden kann.
|
||||||
|
$_SESSION["old_title"] = $_POST["title"] ?? '';
|
||||||
|
$_SESSION["old_tags"] = $_POST["tags"] ?? '';
|
||||||
|
$_SESSION["old_category"] = $_POST["category"] ?? '';
|
||||||
|
|
||||||
|
$blocks = rebuildBlocksFromPost($_POST['blocks'] ?? [], $_FILES['blocks'] ?? [], $uploadDir);
|
||||||
|
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Zwischen-Schritt: Block hinzufügen oder entfernen (kein echtes Speichern).
|
||||||
|
// Wird bei aktivem JavaScript per preventDefault() abgefangen und lokal im
|
||||||
|
// DOM erledigt (js/editor.js) – ohne JS läuft dieser Server-Roundtrip.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
if (isset($_POST['editor_action']) && $_POST['editor_action'] !== '') {
|
||||||
|
$action = $_POST['editor_action'];
|
||||||
|
|
||||||
|
if ($action === 'add_text') {
|
||||||
|
$blocks[] = ['type' => 'text', 'value' => ''];
|
||||||
|
} elseif ($action === 'add_image') {
|
||||||
|
$blocks[] = ['type' => 'image', 'value' => ''];
|
||||||
|
} elseif (str_starts_with($action, 'delete_block:')) {
|
||||||
|
$deleteIndex = (int) substr($action, strlen('delete_block:'));
|
||||||
|
unset($blocks[$deleteIndex]);
|
||||||
|
$blocks = array_values($blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
||||||
|
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Echtes Speichern
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
if (!isset($_POST["title"]) || !isset($_POST["category"])) {
|
||||||
$_SESSION["message"] = "missing_parameters";
|
$_SESSION["message"] = "missing_parameters";
|
||||||
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
||||||
exit();
|
exit();
|
||||||
}else{
|
} else {
|
||||||
$title = $_POST["title"];
|
$title = $_POST["title"];
|
||||||
$content = $_POST["content"];
|
$content = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
||||||
$author = $_SESSION["user_email"];
|
$author = $_SESSION["user_email"];
|
||||||
$category = $_POST["category"];
|
$category = $_POST["category"];
|
||||||
$tags = $_POST['tags'] ?? '';
|
$tags = $_POST['tags'] ?? '';
|
||||||
|
|
||||||
// -------------------------------- Validierung der Daten: -------------------------
|
// -------------------------------- Validierung der Daten: -------------------------
|
||||||
if (!articleTitleValidator($title)) {
|
if (!articleTitleValidator($title)) {
|
||||||
@@ -88,91 +142,56 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
$cleanedTags = implode(',', $cleanedTags);
|
$cleanedTags = implode(',', $cleanedTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------- Base64-Bilder speichern ---------------------------------------------
|
// ----------------- Verwaiste Bilder aufräumen -----------------
|
||||||
$blocks = json_decode($content, true);
|
// Bilder, die im alten (gespeicherten) Content vorkamen, im neuen aber nicht
|
||||||
$uploadDir = __DIR__ . '/../../uploads/';
|
// mehr referenziert werden, wurden vom Nutzer entfernt oder ersetzt -> löschen.
|
||||||
|
// Hinweis/TODO: Bilder, die innerhalb derselben Bearbeitungs-Sitzung neu
|
||||||
if (!file_exists($uploadDir)) {
|
// hochgeladen und noch vor dem finalen Speichern wieder entfernt wurden,
|
||||||
mkdir($uploadDir, 0755, true);
|
// werden hierüber nicht erfasst (sie tauchten nie im alten Content auf) und
|
||||||
}
|
// bleiben als Datei liegen. Für eine vollständige Bereinigung würde sich ein
|
||||||
|
// regelmäßiger Cleanup-Job anbieten, der verwaiste Dateien im uploads/-Ordner
|
||||||
// ----------------- Gelöschte Bilder über die JS-Löschliste entfernen ----------------- TODO: Gelöschte Bilder über die JS-Löschliste entfernen
|
// mit den in der Datenbank referenzierten Pfaden abgleicht.
|
||||||
/*if (isset($_POST['deleted_images'])) {
|
$oldBlocks = json_decode($article->getContent(), true);
|
||||||
$deletedImages = json_decode($_POST['deleted_images'], true);
|
$oldImagePaths = [];
|
||||||
|
if (is_array($oldBlocks)) {
|
||||||
// Wir ermitteln den physisch echten, absoluten Pfad zum uploads-Ordner auf der Festplatte
|
foreach ($oldBlocks as $oldBlock) {
|
||||||
$uploadDir = realpath(__DIR__ . '/../../uploads') . DIRECTORY_SEPARATOR;
|
if (($oldBlock['type'] ?? '') === 'image'
|
||||||
|
&& !empty($oldBlock['value'])
|
||||||
if (is_array($deletedImages)) {
|
&& is_string($oldBlock['value'])
|
||||||
foreach ($deletedImages as $imagePath) {
|
&& str_starts_with($oldBlock['value'], 'uploads/')) {
|
||||||
// Nur den reinen Dateinamen heraustrennen (z.B. img_65a123.jpg)
|
$oldImagePaths[] = $oldBlock['value'];
|
||||||
$filename = basename($imagePath);
|
|
||||||
$fullDeletePath = $uploadDir . $filename;
|
|
||||||
|
|
||||||
// Debugging & Löschen:
|
|
||||||
if (file_exists($fullDeletePath)) {
|
|
||||||
// Versuchen zu löschen. Wenn es fehlschlägt, Fehlermeldung erzwingen
|
|
||||||
if (!@unlink($fullDeletePath)) {
|
|
||||||
$error = error_get_last();
|
|
||||||
die("Datei existiert, aber PHP darf sie nicht löschen! Grund: " . $error['message']);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Wenn PHP die Datei an diesem Pfad nicht findet, brechen wir zum Debuggen ab
|
|
||||||
// die("PHP findet die Datei nicht unter dem Pfad: " . $fullDeletePath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}*/
|
|
||||||
|
|
||||||
// ----------------------- NEU hinzugefügte Base64-Bilder: --------------------------
|
|
||||||
if (is_array($blocks)) {
|
|
||||||
foreach ($blocks as &$block) {
|
|
||||||
// Prüfen, ob der Block ein Bild ist und ein NEUES Bild (Base64-Format) enthält
|
|
||||||
if (isset($block['type']) && isset($block['value']) && $block['type'] === 'image' && is_string($block['value'])) {
|
|
||||||
|
|
||||||
if (str_starts_with($block['value'], 'data:image/')) {
|
|
||||||
$parts = explode(',', $block['value']);
|
|
||||||
if (count($parts) >= 2) {
|
|
||||||
$metadata = $parts[0];
|
|
||||||
$base64Data = $parts[1];
|
|
||||||
|
|
||||||
preg_match('/data:image\/(?<extension>.*?);/', $metadata, $matches);
|
|
||||||
$extension = $matches['extension'] ?? 'jpg';
|
|
||||||
if ($extension === 'jpeg') { $extension = 'jpg'; }
|
|
||||||
|
|
||||||
$fileName = 'img_' . uniqid() . '.' . $extension;
|
|
||||||
$filePath = $uploadDir . $fileName;
|
|
||||||
|
|
||||||
if (file_put_contents($filePath, base64_decode($base64Data)) !== false) {
|
|
||||||
$block['value'] = 'uploads/' . $fileName;
|
|
||||||
} else {
|
|
||||||
$_SESSION["message"] = "image_upload_error";
|
|
||||||
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unset($block);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aktualisiertes Array wieder in JSON konvertieren
|
$newImagePaths = [];
|
||||||
$finalContent = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
foreach ($blocks as $block) {
|
||||||
|
if (($block['type'] ?? '') === 'image' && !empty($block['value'])) {
|
||||||
|
$newImagePaths[] = $block['value'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$orphanedImages = array_diff($oldImagePaths, $newImagePaths);
|
||||||
|
foreach ($orphanedImages as $orphanedImage) {
|
||||||
|
$absolutePath = __DIR__ . '/../../' . $orphanedImage;
|
||||||
|
if (is_file($absolutePath)) {
|
||||||
|
@unlink($absolutePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
|
// ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
|
||||||
try {
|
try {
|
||||||
$articleManager = ArticleManager::getInstance();
|
$articleManager = ArticleManager::getInstance();
|
||||||
$article = $articleManager->getArticle($id);
|
$article = $articleManager->getArticle($id);
|
||||||
$article->setTitle($title);
|
$article->setTitle($title);
|
||||||
$article->setContent($finalContent);
|
$article->setContent($content);
|
||||||
$article->setCategory($category);
|
$article->setCategory($category);
|
||||||
$article->setTags($cleanedTags);
|
$article->setTags($cleanedTags);
|
||||||
$articleManager->updateArticle($id ,$article, $author);
|
$articleManager->updateArticle($id, $article, $author);
|
||||||
|
|
||||||
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
|
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
|
||||||
|
|
||||||
} catch (\Throwable $e){
|
} catch (\Throwable $e) {
|
||||||
$_SESSION["message"] = $e->getMessage();
|
$_SESSION["message"] = $e->getMessage();
|
||||||
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
||||||
exit();
|
exit();
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?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();
|
||||||
@@ -97,6 +97,16 @@ class Article
|
|||||||
return $this->author;
|
return $this->author;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt einen neuen Autor eines Beitrages.
|
||||||
|
* @param string $author
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setAuthor(string $author)
|
||||||
|
{
|
||||||
|
$this->author = $author;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt das Veröffentlichungsdatum des Beitrags zurück.
|
* Gibt das Veröffentlichungsdatum des Beitrags zurück.
|
||||||
* @return string
|
* @return string
|
||||||
|
|||||||
@@ -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 $author Autor des Kommentars
|
* @param string|null $author Autor des Kommentars oder null bei gelöschten Kommentaren
|
||||||
* @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 Autor
|
* @return string|null Autor oder null bei gelöschten Kommentaren
|
||||||
*/
|
*/
|
||||||
public function getAuthor(): string
|
public function getAuthor(): ?string
|
||||||
{
|
{
|
||||||
return $this->author;
|
return $this->author;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,4 +51,40 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ class DatabaseArticleManager implements ArticleManagerDAO {
|
|||||||
return intval($db->lastInsertId());
|
return intval($db->lastInsertId());
|
||||||
|
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
throw new InternalServerErrorException($e->getMessage());
|
throw new InternalServerErrorException("internal_error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,11 +106,8 @@ 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
$success = $command->execute([
|
$command->execute([
|
||||||
":id" => $id,
|
":id" => $id,
|
||||||
":title" => $article->getTitle(),
|
":title" => $article->getTitle(),
|
||||||
":content" => $article->getContent(),
|
":content" => $article->getContent(),
|
||||||
@@ -120,7 +117,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 (!$success || $command->rowCount() === 0) {
|
if ($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");
|
||||||
@@ -148,13 +145,7 @@ 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);
|
||||||
if (!$command) {
|
$command->execute([":id" => $id]);
|
||||||
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");
|
||||||
}
|
}
|
||||||
@@ -167,10 +158,6 @@ 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);
|
||||||
|
|
||||||
@@ -202,10 +189,6 @@ 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 = [];
|
||||||
|
|
||||||
@@ -234,10 +217,6 @@ 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 = [];
|
||||||
@@ -270,10 +249,6 @@ 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 = [];
|
||||||
@@ -312,35 +287,29 @@ 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 SQL-Suche hinzufügen
|
||||||
$searchParam = '%' . $cleankeyword . '%';
|
$searchParam = '%' . $cleankeyword . '%';
|
||||||
|
|
||||||
$success = $command->execute([
|
$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) {
|
||||||
$likes = $this->getLikesForArticle(intval($row['id']));
|
$articleId = intval($row['id']);
|
||||||
|
$likes = $this->getLikesForArticle($articleId);
|
||||||
|
|
||||||
$filteredArticles[] = new Article(
|
$filteredArticles[] = new Article(
|
||||||
intval($row['id']),
|
$articleId,
|
||||||
$row['title'] ?? '',
|
$row['title'] ?? '',
|
||||||
$row['content'] ?? '',
|
$row['content'] ?? '',
|
||||||
$row['author'] ?? '',
|
$row['author'] ?? '',
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class DatabaseCommentManager implements CommentManagerDAO
|
|||||||
/**
|
/**
|
||||||
* Erstellt die Kommentartabelle, falls diese noch nicht existiert.
|
* Erstellt die Kommentartabelle, falls diese noch nicht existiert.
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
private function __construct()
|
||||||
{
|
{
|
||||||
$this->dbPath = __DIR__ . '/../../db/eduforgeDB.db';
|
$this->dbPath = __DIR__ . '/../../db/eduforgeDB.db';
|
||||||
DatabaseInitializer::initialize($this->dbPath);
|
DatabaseInitializer::initialize($this->dbPath);
|
||||||
@@ -223,4 +223,194 @@ 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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 NOT NULL,
|
author TEXT 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 CASCADE,
|
FOREIGN KEY (author) REFERENCES users(email) ON DELETE SET NULL,
|
||||||
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,9 +234,14 @@ class DatabaseInitializer {
|
|||||||
// Bestimmt per Zufall einen Autor aus dem Pool der gültigen E-Mails
|
// Bestimmt per Zufall einen Autor aus dem Pool der gültigen E-Mails
|
||||||
$randomAuthor = $availableEmails[array_rand($availableEmails)];
|
$randomAuthor = $availableEmails[array_rand($availableEmails)];
|
||||||
|
|
||||||
|
// Text in (blocks[i][type]/[text]/[image])-Format umwandeln:
|
||||||
|
$content = json_encode([
|
||||||
|
['type' => 'text', 'value' => $article[1]]
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
$articleInsertStmt->execute([
|
$articleInsertStmt->execute([
|
||||||
':title' => $article[0],
|
':title' => $article[0],
|
||||||
':content' => $article[1],
|
':content' => $content,
|
||||||
':author' => $randomAuthor,
|
':author' => $randomAuthor,
|
||||||
':category' => $article[2],
|
':category' => $article[2],
|
||||||
':tags' => $article[3]
|
':tags' => $article[3]
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
<?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
|
||||||
|
|||||||
Reference in New Issue
Block a user