Compare commits

...

103 Commits

Author SHA1 Message Date
niklas.ortmann 75b4aa0b88 Update index.php 2026-07-20 00:11:27 +02:00
niklas.ortmann 9146d395a2 Update dataSources.local.xml 2026-07-20 00:05:44 +02:00
niklas.ortmann f957f252d1 Merge branch 'bugs' into devNachCSRF 2026-07-20 00:05:39 +02:00
niklas.ortmann b3f7db2615 Merge branch 'loader' into devNachCSRF 2026-07-19 23:59:13 +02:00
niklas.ortmann 12369a5a00 Merge branch 'visuelles-Feedback' into devNachCSRF 2026-07-19 23:54:44 +02:00
niklas.ortmann 652bcc5474 Merge pull request 'devNachCRSF mit Datenschutz-Nutzungsbedingungen_ueberarbeiten mergen (TEST)' (#65) from Datenschutz-Nutzungsbedingungen_ueberarbeiten into devNachCSRF
Reviewed-on: #65
2026-07-19 23:49:40 +02:00
caroline.slt c22853d802 Merge remote-tracking branch 'origin/bugs' into bugs 2026-07-19 22:29:58 +02:00
niklas.ortmann 4abc5f4167 . 2026-07-19 22:29:52 +02:00
caroline.slt 09f8b11f36 Loader 2026-07-19 21:35:23 +02:00
caroline.slt cc4cebdb45 . 2026-07-19 21:16:54 +02:00
niklas.ortmann 9f4343cc53 Merge branch 'CSRF' into dev 2026-07-19 21:14:01 +02:00
niklas.ortmann a5998cffca Merge pull request 'Deutlichere Effekte bei Nutzerinteraktionen' (#60) from visuelles-Feedback into dev
Reviewed-on: #60
Reviewed-by: niklas.ortmann <ortmann.niklas@yahoo.de>
2026-07-19 21:09:02 +02:00
caroline.slt 153a612ad6 Email aus Kommentar entfernen 2026-07-19 18:21:06 +02:00
caroline.slt 437da98add Email aus Kommentar entfernen 2026-07-19 18:13:41 +02:00
caroline.slt a4b0dfe7ec Email aus Kommentar entfernen 2026-07-19 18:05:23 +02:00
caroline.slt 3a819d6ed2 Email aus Kommentar entfernen 2026-07-19 17:57:34 +02:00
caroline.slt 4f4ad9599d Löschen Button verschönert 2026-07-19 17:51:52 +02:00
caroline.slt d9da018ac2 Löschen Button verschönert 2026-07-19 17:31:49 +02:00
caroline.slt 2148ba75f3 Button hervorheben und Kommentare ändern verschönert 2026-07-19 17:27:35 +02:00
caroline.slt c4ffc1c797 Deutlichere Effekte bei Nutzerinteraktionen 2026-07-19 17:19:57 +02:00
niklas.ortmann 96f38ce72e Merge pull request 'Suche nutzt nun Tags' (#59) from suchergebnisseTags into dev
Reviewed-on: #59
2026-07-19 16:29:40 +02:00
niklas.ortmann b81f0c9e5c Update DatabaseArticleManager.php 2026-07-19 16:28:29 +02:00
niklas.ortmann f8aed4283e Update dataSources.local.xml 2026-07-19 16:28:27 +02:00
niklas.ortmann a785d862d8 Update DatabaseArticleManager.php 2026-07-19 16:10:08 +02:00
niklas.ortmann a728a8a556 Update showArticle-controller.php 2026-07-19 16:08:55 +02:00
niklas.ortmann ae34afda15 Update article-validator.php 2026-07-19 16:08:06 +02:00
niklas.ortmann 59e0b9f111 Update like-controller.php 2026-07-19 16:05:52 +02:00
niklas.ortmann cd50840b19 Update add-comment.php 2026-07-19 16:05:05 +02:00
niklas.ortmann 3bce65a1a5 Update deleteComment-controller.php 2026-07-19 16:01:42 +02:00
niklas.ortmann 50b29c96d6 Update updateComment-controller.php 2026-07-19 16:01:06 +02:00
niklas.ortmann d5fc5f3065 Update updateArticle-controller.php 2026-07-19 16:00:07 +02:00
niklas.ortmann 0427d52a58 Update index.php 2026-07-19 15:58:32 +02:00
niklas.ortmann cc804757ff Update createArticle-controller.php 2026-07-19 15:57:36 +02:00
niklas.ortmann b55a6592f0 Update logout-controller.php 2026-07-19 15:55:54 +02:00
niklas.ortmann e12a98cc98 Update deleteArticle-controller.php 2026-07-19 15:55:09 +02:00
niklas.ortmann 320141855a Update deleteAccount-controller.php 2026-07-19 15:54:15 +02:00
niklas.ortmann 12801344f1 Update profile-controller.php 2026-07-19 15:42:28 +02:00
niklas.ortmann 83387de103 Update createArticle.php 2026-07-19 15:41:37 +02:00
niklas.ortmann dfb8e1dc89 Update updateArticle.php 2026-07-19 15:41:35 +02:00
niklas.ortmann bdbc38a339 Update profile.php 2026-07-19 15:40:16 +02:00
niklas.ortmann e6356525b9 Update comments.js 2026-07-19 15:34:50 +02:00
niklas.ortmann ca74690952 Update showArticle.php 2026-07-19 15:33:13 +02:00
niklas.ortmann 75f9fdbeb8 Update navbar.php 2026-07-19 15:31:02 +02:00
niklas.ortmann 81a27135d5 Update navbar.css 2026-07-19 15:28:08 +02:00
niklas.ortmann a58576637a Update navbar.css 2026-07-19 15:26:53 +02:00
niklas.ortmann 997957a5d3 Update logout-controller.php 2026-07-19 15:25:15 +02:00
niklas.ortmann ec794db706 logout-fix 2026-07-19 15:23:46 +02:00
niklas.ortmann ba749bca68 debugging 2026-07-19 15:21:16 +02:00
niklas.ortmann 12a6af55b0 debugging 2026-07-19 15:19:47 +02:00
niklas.ortmann 400f018104 debugging 2026-07-19 15:17:32 +02:00
niklas.ortmann 80f4b623f9 debugging 2026-07-19 15:16:22 +02:00
niklas.ortmann 6546d264b0 debugging 2026-07-19 15:15:30 +02:00
niklas.ortmann e7e120e5b8 debugging 2026-07-19 15:15:03 +02:00
niklas.ortmann ec66889a1f debugging 2026-07-19 15:13:44 +02:00
niklas.ortmann f1703a476e Update index.php 2026-07-19 15:09:22 +02:00
niklas.ortmann bac296b58a Update logout-controller.php 2026-07-19 15:07:09 +02:00
niklas.ortmann bfb2d2b0d8 navbar logout mit post 2026-07-19 14:59:45 +02:00
niklas.ortmann ad9a00fd49 csrf.php 2026-07-19 14:57:52 +02:00
niklas.ortmann 42faab17e8 Create csrf.php 2026-07-19 14:56:26 +02:00
niklas.ortmann 1da4842847 Update dataSources.local.xml 2026-07-19 14:56:25 +02:00
niklas.ortmann afd90086d8 Update profile.php 2026-07-19 14:20:43 +02:00
niklas.ortmann a7a54f877c Merge pull request 'Beitragseditor funktioniert mit und ohne JS (+Bilder löschen)' (#57) from BeitragseditorOhneJS into dev
Reviewed-on: #57
2026-07-19 13:45:33 +02:00
NOrtmann1 9b9f71d21c Update DatabaseInitializer.php 2026-07-19 13:44:27 +02:00
NOrtmann1 4b84f0ab09 Update updateArticle-controller.php 2026-07-19 13:30:39 +02:00
NOrtmann1 8c8b148bf4 Update updateArticle-controller.php 2026-07-19 13:15:23 +02:00
NOrtmann1 6bf335ce46 Update createArticle.php 2026-07-19 13:15:20 +02:00
NOrtmann1 144a1015f1 Create article-block-helper.php 2026-07-19 13:15:17 +02:00
NOrtmann1 c0a94b2eee Update createArticle-controller.php 2026-07-19 13:15:14 +02:00
NOrtmann1 f1a32120d6 Update editor.js 2026-07-19 12:48:41 +02:00
NOrtmann1 97fa1de999 Update updateArticle-controller.php 2026-07-19 12:48:38 +02:00
NOrtmann1 7341fe7733 Update updateArticle.php 2026-07-19 12:48:33 +02:00
NOrtmann1 5b0528a914 Update index-controller.php 2026-07-19 10:56:05 +02:00
NOrtmann1 ac64e65dd3 Update index-controller.php 2026-07-19 10:54:33 +02:00
NOrtmann1 96ffa3288e Update dataSources.local.xml 2026-07-19 10:52:21 +02:00
NOrtmann1 0c6cf13efa merge-commit 2026-07-19 10:50:22 +02:00
caroline.slt 3867458050 Opt-In & Optimierung für Datenschutz und Nutzungsbedingungen - Korrektur 2026-07-18 20:16:56 +02:00
caroline.slt f752150188 Opt-In & Optimierung für Datenschutz und Nutzungsbedingungen 2026-07-18 20:06:26 +02:00
caroline.slt 325df51812 ReadMe 2026-07-18 19:51:23 +02:00
caroline.slt 97afd1ea06 Kommentare bearbeiten - Fehlerkorrektur 2026-07-18 19:41:39 +02:00
caroline.slt e3903b4f3e Kommentare bearbeiten - Fehlerkorrektur 2026-07-18 19:27:36 +02:00
caroline.slt 031f2afd25 Kommentare bearbeiten 2026-07-18 18:49:48 +02:00
niklas.ortmann d3884d2fc1 Merge branch 'PaginatorOhneJS2' into dev 2026-07-18 18:33:04 +02:00
niklas.ortmann 1575f4fa24 Merge pull request 'Deiteiincludes ($pfad) per whitelist prüfen' (#53) from AllowistDateiincludes into dev
Reviewed-on: #53
2026-07-18 18:31:55 +02:00
caroline.slt 7189133861 Korrektur 2026-07-18 18:09:11 +02:00
caroline.slt 8c961a56ce Korrektur 2026-07-18 17:54:30 +02:00
caroline.slt 4f95a3d423 JavaScript Korrektur 2026-07-18 17:43:45 +02:00
caroline.slt 5310083e11 JavaScript Korrektur 2026-07-18 17:23:52 +02:00
caroline.slt b564306fb3 JavaScript Korrektur 2026-07-18 17:20:14 +02:00
niklas.ortmann 98b088afc0 Update alertMessages.php 2026-07-18 17:00:31 +02:00
niklas.ortmann 51c850d122 Update 404.php 2026-07-18 16:59:47 +02:00
caroline.slt 7b7208ad6b Kommentare löschen 2026-07-18 16:44:05 +02:00
niklas.ortmann 97302f1bb3 Update search-results-controller.php 2026-07-18 16:22:03 +02:00
niklas.ortmann 35d4425086 Update index-controller.php 2026-07-18 16:19:48 +02:00
niklas.ortmann d3435d4cde Update index.php 2026-07-18 16:14:09 +02:00
niklas.ortmann f61c548b9e Update index.php 2026-07-18 16:11:53 +02:00
niklas.ortmann 101a2a247b Update index-controller.php 2026-07-18 16:08:05 +02:00
niklas.ortmann 423941e7ed Update index-controller.php 2026-07-18 16:06:35 +02:00
niklas.ortmann a8e76c782d Update index-controller.php 2026-07-18 15:53:33 +02:00
niklas.ortmann 3f9c048493 Update index.php 2026-07-18 15:53:31 +02:00
niklas.ortmann fee5a701df Update index-controller.php 2026-07-18 15:50:07 +02:00
niklas.ortmann d341312192 Update index-controller.php 2026-07-18 15:47:25 +02:00
niklas.ortmann 4dadfb8863 Update index.php 2026-07-18 15:47:23 +02:00
caroline.slt ada97ec538 Kommentare ohne js verfassen 2026-07-18 15:26:13 +02:00
43 changed files with 2890 additions and 912 deletions
+3
View File
@@ -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
+1 -5
View File
@@ -4,11 +4,7 @@
--> -->
<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"]);
?> ?>
+57 -17
View File
@@ -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); ?>">
<?php if ($blockType === 'text'): ?>
<textarea name="blocks[<?php echo (int)$i; ?>][text]"
placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
<?php else: /* image */ ?>
<?php if (!empty($block['value'])): ?>
<img src="<?php echo htmlspecialchars($block['value']); ?>"
class="block-image-preview"
style="max-width:200px;display:block;margin-top:10px;">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][existing_image]" value="<?php echo htmlspecialchars($block['value']); ?>">
<?php endif; ?>
<input type="file" name="blocks[<?php echo (int)$i; ?>][image]" accept="image/*">
<?php endif; ?>
<button type="submit" name="editor_action" value="delete_block:<?php echo (int)$i; ?>" class="delete-block-btn">✕</button>
</div>
<?php endforeach; ?>
</div>
<!-- Plus-Button und das Pop-up-Menü -->
<div id="add-block-control" class="article-editor-scope add-block-control"> <div id="add-block-control" class="article-editor-scope add-block-control">
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button> <button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
<div id="block-popup" class="article-editor-scope block-popup hidden"> <div id="block-popup" class="article-editor-scope block-popup">
<button type="button" data-type="text">Textblock</button> <button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
<button type="button" data-type="image">Bild einfügen</button> <button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
</div> </div>
</div> </div>
<!-- Unsichtbares Textfeld, das die JSON-Daten hält und an den Controller postet -->
<textarea id="content" name="content" style="display:none;"><?php
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){
echo htmlspecialchars($_SESSION['old_content']);
unset($_SESSION['old_content']);
} else {
echo '[]'; // Standardmäßig ein leeres JSON-Array
}
?></textarea>
</main> </main>
<!-- Seitenleiste --> <!-- Seitenleiste -->
+227 -181
View File
@@ -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&shy;erkl&auml;rung</h1> <main>
<h2>1. Datenschutz auf einen Blick</h2>
<h3>Allgemeine Hinweise</h3> <p>Die folgenden Hinweise geben einen einfachen &Uuml;berblick dar&uuml;ber,
was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind
alle Daten, mit denen Sie pers&ouml;nlich identifiziert werden k&ouml;nnen. Ausf&uuml;hrliche Informationen zum
Thema Datenschutz entnehmen Sie unserer unter diesem Text aufgef&uuml;hrten
Datenschutzerkl&auml;rung.</p>
<h3>Datenerfassung auf dieser Website</h3> <h4>Wer ist verantwortlich f&uuml;r die Datenerfassung auf dieser
Website?</h4> <p>Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen
Kontaktdaten k&ouml;nnen Sie dem Abschnitt &bdquo;Hinweis zur verantwortlichen Stelle&ldquo; in dieser
Datenschutzerkl&auml;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.&nbsp;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.&nbsp;B. Internetbrowser,
Betriebssystem oder Uhrzeit des Seitenaufrufs). Die Erfassung dieser Daten erfolgt automatisch, sobald Sie diese
Website betreten.</p> <h4>Wof&uuml;r nutzen wir Ihre Daten?</h4> <p>Ein Teil der Daten wird erhoben, um eine
fehlerfreie Bereitstellung der Website zu gew&auml;hrleisten. Andere Daten k&ouml;nnen zur Analyse Ihres
Nutzerverhaltens verwendet werden. Sofern &uuml;ber die Website Vertr&auml;ge geschlossen oder angebahnt
werden k&ouml;nnen, werden die &uuml;bermittelten Daten auch f&uuml;r Vertragsangebote, Bestellungen oder
sonstige Auftragsanfragen verarbeitet.</p> <h4>Welche Rechte haben Sie bez&uuml;glich Ihrer Daten?</h4>
<p>Sie haben jederzeit das Recht, unentgeltlich Auskunft &uuml;ber Herkunft, Empf&auml;nger und Zweck Ihrer
gespeicherten personenbezogenen Daten zu erhalten. Sie haben au&szlig;erdem ein Recht, die Berichtigung oder
L&ouml;schung dieser Daten zu verlangen. Wenn Sie eine Einwilligung zur Datenverarbeitung erteilt haben,
k&ouml;nnen Sie diese Einwilligung jederzeit f&uuml;r die Zukunft widerrufen. Au&szlig;erdem haben Sie das Recht,
unter bestimmten Umst&auml;nden die Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu
verlangen. Des Weiteren steht Ihnen ein Beschwerderecht bei der zust&auml;ndigen Aufsichtsbeh&ouml;rde
zu.</p> <p>Hierzu sowie zu weiteren Fragen zum Thema Datenschutz k&ouml;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.&nbsp;a. um IP-Adressen, Kontaktanfragen, Meta- und Kommunikationsdaten, Vertragsdaten, Kontaktdaten,
Namen, Websitezugriffe und sonstige Daten, die &uuml;ber eine Website generiert werden, handeln.</p> <p>Das
externe Hosting erfolgt zum Zwecke der Vertragserf&uuml;llung gegen&uuml;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&szlig;lich auf Grundlage von
Art. 6 Abs. 1 lit. a DSGVO und &sect; 25 Abs. 1 TDDDG, soweit die Einwilligung die Speicherung von Cookies oder
den Zugriff auf Informationen im Endger&auml;t des Nutzers (z.&nbsp;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&uuml;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 &uuml;ber Auftragsverarbeitung (AVV) zur Nutzung
des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen
Vertrag, der gew&auml;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&shy;informationen</h2>
<h3>Datenschutz</h3> <p>Die Betreiber dieser Seiten nehmen den Schutz Ihrer pers&ouml;nlichen Daten sehr
ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen
Datenschutzvorschriften sowie dieser Datenschutzerkl&auml;rung.</p> <p>Wenn Sie diese Website benutzen,
werden verschiedene personenbezogene Daten erhoben. Personenbezogene Daten sind Daten, mit denen Sie
pers&ouml;nlich identifiziert werden k&ouml;nnen. Die vorliegende Datenschutzerkl&auml;rung erl&auml;utert,
welche Daten wir erheben und wof&uuml;r wir sie nutzen. Sie erl&auml;utert auch, wie und zu welchem Zweck das
geschieht.</p> <p>Wir weisen darauf hin, dass die Daten&uuml;bertragung im Internet (z.&nbsp;B. bei der
Kommunikation per E-Mail) Sicherheitsl&uuml;cken aufweisen kann. Ein l&uuml;ckenloser Schutz der Daten vor
dem Zugriff durch Dritte ist nicht m&ouml;glich.</p>
<h3>Hinweis zur verantwortlichen Stelle</h3> <p>Die verantwortliche Stelle f&uuml;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&uuml;rliche oder juristische Person, die allein oder gemeinsam mit anderen
&uuml;ber die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten (z.&nbsp;B. Namen, E-Mail-
Adressen o. &Auml;.) entscheidet.</p>
<h3>Speicherdauer</h3> <p>Soweit innerhalb dieser Datenschutzerkl&auml;rung keine speziellere Speicherdauer
genannt wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck f&uuml;r die Datenverarbeitung
entf&auml;llt. Wenn Sie ein berechtigtes L&ouml;schersuchen geltend machen oder eine Einwilligung zur
Datenverarbeitung widerrufen, werden Ihre Daten gel&ouml;scht, sofern wir keine anderen rechtlich
zul&auml;ssigen Gr&uuml;nde f&uuml;r die Speicherung Ihrer personenbezogenen Daten haben (z.&nbsp;B. steuer-
oder handelsrechtliche Aufbewahrungsfristen); im letztgenannten Fall erfolgt die L&ouml;schung nach Fortfall
dieser Gr&uuml;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&uuml;cklichen Einwilligung in die &Uuml;bertragung personenbezogener
Daten in Drittstaaten erfolgt die Datenverarbeitung au&szlig;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&auml;t (z.&nbsp;B. via
Device-Fingerprinting) eingewilligt haben, erfolgt die Datenverarbeitung zus&auml;tzlich auf Grundlage von &sect;
25 Abs. 1 TDDDG. Die Einwilligung ist jederzeit widerrufbar. Sind Ihre Daten zur Vertragserf&uuml;llung oder zur
Durchf&uuml;hrung vorvertraglicher Ma&szlig;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&uuml;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. &Uuml;ber die
jeweils im Einzelfall einschl&auml;gigen Rechtsgrundlagen wird in den folgenden Abs&auml;tzen dieser
Datenschutzerkl&auml;rung informiert.</p>
<h3>Empf&auml;nger von personenbezogenen Daten</h3> <p>Im Rahmen unserer Gesch&auml;ftst&auml;tigkeit
arbeiten wir mit verschiedenen externen Stellen zusammen. Dabei ist teilweise auch eine &Uuml;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&uuml;llung erforderlich ist, wenn wir gesetzlich
hierzu verpflichtet sind (z.&nbsp;B. Weitergabe von Daten an Steuerbeh&ouml;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&uuml;ltigen Vertrags &uuml;ber Auftragsverarbeitung weiter. Im Falle einer
gemeinsamen Verarbeitung wird ein Vertrag &uuml;ber gemeinsame Verarbeitung geschlossen.</p>
<h3>Widerruf Ihrer Einwilligung zur Datenverarbeitung</h3> <p>Viele Datenverarbeitungsvorg&auml;nge sind
nur mit Ihrer ausdr&uuml;cklichen Einwilligung m&ouml;glich. Sie k&ouml;nnen eine bereits erteilte Einwilligung
jederzeit widerrufen. Die Rechtm&auml;&szlig;igkeit der bis zum Widerruf erfolgten Datenverarbeitung bleibt vom
Widerruf unber&uuml;hrt.</p>
<h3>Widerspruchsrecht gegen die Datenerhebung in besonderen F&auml;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&Uuml;NDEN, DIE SICH AUS IHRER
BESONDEREN SITUATION ERGEBEN, GEGEN DIE VERARBEITUNG IHRER PERSONENBEZOGENEN DATEN
WIDERSPRUCH EINZULEGEN; DIES GILT AUCH F&Uuml;R EIN AUF DIESE BESTIMMUNGEN GEST&Uuml;TZTES
PROFILING. DIE JEWEILIGE RECHTSGRUNDLAGE, AUF DENEN EINE VERARBEITUNG BERUHT, ENTNEHMEN
SIE DIESER DATENSCHUTZERKL&Auml;RUNG. WENN SIE WIDERSPRUCH EINLEGEN, WERDEN WIR IHRE
BETROFFENEN PERSONENBEZOGENEN DATEN NICHT MEHR VERARBEITEN, ES SEI DENN, WIR
K&Ouml;NNEN ZWINGENDE SCHUTZW&Uuml;RDIGE GR&Uuml;NDE F&Uuml;R DIE VERARBEITUNG
NACHWEISEN, DIE IHRE INTERESSEN, RECHTE UND FREIHEITEN &Uuml;BERWIEGEN ODER DIE
VERARBEITUNG DIENT DER GELTENDMACHUNG, AUS&Uuml;BUNG ODER VERTEIDIGUNG VON
RECHTSANSPR&Uuml;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&Uuml;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&shy;recht bei der zust&auml;ndigen Aufsichts&shy;beh&ouml;rde</h3> <p>Im Falle von
Verst&ouml;&szlig;en gegen die DSGVO steht den Betroffenen ein Beschwerderecht bei einer
Aufsichtsbeh&ouml;rde, insbesondere in dem Mitgliedstaat ihres gew&ouml;hnlichen Aufenthalts, ihres
Arbeitsplatzes oder des Orts des mutma&szlig;lichen Versto&szlig;es zu. Das Beschwerderecht besteht
unbeschadet anderweitiger verwaltungsrechtlicher oder gerichtlicher Rechtsbehelfe.</p>
<h3>Recht auf Daten&shy;&uuml;bertrag&shy;barkeit</h3> <p>Sie haben das Recht, Daten, die wir auf Grundlage
Ihrer Einwilligung oder in Erf&uuml;llung eines Vertrags automatisiert verarbeiten, an sich oder an einen Dritten in
einem g&auml;ngigen, maschinenlesbaren Format aush&auml;ndigen zu lassen. Sofern Sie die direkte
&Uuml;bertragung der Daten an einen anderen Verantwortlichen verlangen, erfolgt dies nur, soweit es technisch
machbar ist.</p>
<h3>Auskunft, Berichtigung und L&ouml;schung</h3> <p>Sie haben im Rahmen der geltenden gesetzlichen
Bestimmungen jederzeit das Recht auf unentgeltliche Auskunft &uuml;ber Ihre gespeicherten personenbezogenen
Daten, deren Herkunft und Empf&auml;nger und den Zweck der Datenverarbeitung und ggf. ein Recht auf
Berichtigung oder L&ouml;schung dieser Daten. Hierzu sowie zu weiteren Fragen zum Thema personenbezogene
Daten k&ouml;nnen Sie sich jederzeit an uns wenden.</p>
<h3>Recht auf Einschr&auml;nkung der Verarbeitung</h3> <p>Sie haben das Recht, die Einschr&auml;nkung der
Verarbeitung Ihrer personenbezogenen Daten zu verlangen. Hierzu k&ouml;nnen Sie sich jederzeit an uns wenden.
Das Recht auf Einschr&auml;nkung der Verarbeitung besteht in folgenden F&auml;llen:</p> <ul> <li>Wenn Sie die
Richtigkeit Ihrer bei uns gespeicherten personenbezogenen Daten bestreiten, ben&ouml;tigen wir in der Regel Zeit,
um dies zu &uuml;berpr&uuml;fen. F&uuml;r die Dauer der Pr&uuml;fung haben Sie das Recht, die
Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li> <li>Wenn die
Verarbeitung Ihrer personenbezogenen Daten unrechtm&auml;&szlig;ig geschah/geschieht, k&ouml;nnen Sie statt
der L&ouml;schung die Einschr&auml;nkung der Datenverarbeitung verlangen.</li> <li>Wenn wir Ihre
personenbezogenen Daten nicht mehr ben&ouml;tigen, Sie sie jedoch zur Aus&uuml;bung, Verteidigung oder
Geltendmachung von Rechtsanspr&uuml;chen ben&ouml;tigen, haben Sie das Recht, statt der L&ouml;schung die
Einschr&auml;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&auml;gung zwischen Ihren und unseren
Interessen vorgenommen werden. Solange noch nicht feststeht, wessen Interessen &uuml;berwiegen, haben Sie das
Recht, die Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li> </ul>
<p>Wenn Sie die Verarbeitung Ihrer personenbezogenen Daten eingeschr&auml;nkt haben, d&uuml;rfen diese
Daten &ndash; von ihrer Speicherung abgesehen &ndash; nur mit Ihrer Einwilligung oder zur Geltendmachung,
Aus&uuml;bung oder Verteidigung von Rechtsanspr&uuml;chen oder zum Schutz der Rechte einer anderen
nat&uuml;rlichen oder juristischen Person oder aus Gr&uuml;nden eines wichtigen &ouml;ffentlichen Interesses der
Europ&auml;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 114118<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>
+40 -20
View File
@@ -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,9 +23,7 @@
include_once 'includes/navbar.php'; include_once 'includes/navbar.php';
?> ?>
<main> <main>
<main>
<h1>Nutzungsbedingungen</h1> <h1>Nutzungsbedingungen</h1>
@@ -41,45 +39,67 @@ include_once 'includes/navbar.php';
Die Nutzung der Plattform darf ausschließlich im Rahmen der geltenden Gesetze erfolgen. Die Nutzung der Plattform darf ausschließlich im Rahmen der geltenden Gesetze erfolgen.
</p> </p>
<h2>2. Verhalten der Nutzerinnen und Nutzer</h2> <h2>2. Registrierung und Benutzerkonto</h2>
<p>
Für die Nutzung bestimmter Funktionen ist eine Registrierung erforderlich.
Nutzerinnen und Nutzer verpflichten sich, bei der Registrierung
wahrheitsgemäße Angaben zu machen und ihre Zugangsdaten vertraulich zu behandeln.
Änderungen der Profildaten können jederzeit über das Benutzerprofil vorgenommen werden.
</p>
<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
die bereitgestellte Passwort-Reset-Funktion genutzt werden.
</p> </p>
<h2>6. Änderungen der Nutzungsbedingungen</h2> <h2>7. Moderation und Entfernung von Inhalten</h2>
<p>
Der Betreiber behält sich vor, Beiträge, Kommentare oder Benutzerkonten zu sperren
oder Inhalte zu entfernen, wenn diese gegen geltendes Recht oder diese
Nutzungsbedingungen verstoßen.
</p>
<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>
<h2>9. Änderungen der Nutzungsbedingungen</h2>
<p> <p>
Der Betreiber behält sich vor, diese Nutzungsbedingungen anzupassen, sofern dies Der Betreiber behält sich vor, diese Nutzungsbedingungen anzupassen, sofern dies
für den Betrieb oder die Weiterentwicklung der Plattform erforderlich ist. für den Betrieb oder die Weiterentwicklung der Plattform erforderlich ist.
Über wesentliche Änderungen werden die Nutzerinnen und Nutzer informiert.
</p> </p>
</main> </main>
</main> <?php
include_once 'includes/footer.php';
?>
<?php
include_once 'includes/footer.php';
?>
</body> </body>
</html> </html>
+5 -2
View File
@@ -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()); ?>">
+30
View File
@@ -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"
+11
View File
@@ -137,6 +137,17 @@ $categories = [
<p class="s-res-meta"><span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"</p> <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 if (!empty($results)): ?> <?php if (!empty($results)): ?>
+371 -22
View File
@@ -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
@@ -15,8 +67,6 @@ include_once 'php/controller/showArticle-controller.php';
<?php include_once "includes/alertMessages.php"?> <?php include_once "includes/alertMessages.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"> <div class="category-and-likes-row">
@@ -30,9 +80,14 @@ include_once 'php/controller/showArticle-controller.php';
<span>❤️ <span class="like-count"><?php echo $articleObj->getLikeCount(); ?></span></span> <span>❤️ <span class="like-count"><?php echo $articleObj->getLikeCount(); ?></span></span>
<?php if (isset($_SESSION["user_email"])): ?> <?php if (isset($_SESSION["user_email"])): ?>
<a href="php/controller/like-controller.php?id=<?php echo $articleObj->getId(); ?>" class="like-toggle-btn"> <form method="post"
action="php/controller/like-controller.php?id=<?php echo $articleObj->getId(); ?>"
class="like-toggle-form">
<?php csrf_field(); ?>
<button type="submit" 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>
</form>
<?php else: ?> <?php else: ?>
<span class="login-hint">(Anmelden zum Liken)</span> <span class="login-hint">(Anmelden zum Liken)</span>
<?php endif; ?> <?php endif; ?>
@@ -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> ); ?>">
<?php
$isDeleted = $comment->getContent()
=== "Dieser Kommentar wurde gelöscht.";
?>
<?php if ($isDeleted): ?>
<p class="deleted-comment">
Dieser Kommentar wurde gelöscht.
</p> </p>
<p><?php echo nl2br(htmlspecialchars($comment->getContent())); ?></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"])): ?> <?php if (isset($_SESSION["user_email"])): ?>
<button type="button"
<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" class="reply-button"
data-comment-id="<?php echo htmlspecialchars($comment->getId()); ?>" data-comment-id="<?php echo htmlspecialchars(
data-author="<?php echo htmlspecialchars($comment->getAuthor()); ?>"> (string) $comment->getId()
); ?>"
data-author="<?php echo htmlspecialchars(
$comment->getAuthor()
); ?>">
Antworten Antworten
</button> </a>
<?php endif; ?>
<?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> <?php
<span><?php echo htmlspecialchars($reply->getCreated()); ?></span> $isReplyDeleted = $reply->getContent()
=== "Dieser Kommentar wurde gelöscht.";
?>
<?php if ($isReplyDeleted): ?>
<p class="deleted-comment">
Dieser Kommentar wurde gelöscht.
</p> </p>
<p><?php echo nl2br(htmlspecialchars($reply->getContent())); ?></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">
+11
View File
@@ -113,6 +113,17 @@ $resultCount = count($results);
</p> </p>
</div> </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 --> <!-- Ergebnisliste -->
<div class="s-res-list"> <div class="s-res-list">
<?php if (!empty($results)): ?> <?php if (!empty($results)): ?>
+57 -23
View File
@@ -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); ?>">
<?php if ($blockType === 'text'): ?>
<textarea name="blocks[<?php echo (int)$i; ?>][text]"
placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
<?php else: /* image */ ?>
<?php if (!empty($block['value'])): ?>
<img src="<?php echo htmlspecialchars($block['value']); ?>"
class="block-image-preview"
style="max-width:200px;display:block;margin-top:10px;">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][existing_image]" value="<?php echo htmlspecialchars($block['value']); ?>">
<?php endif; ?>
<input type="file" name="blocks[<?php echo (int)$i; ?>][image]" accept="image/*">
<?php endif; ?>
<button type="submit" name="editor_action" value="delete_block:<?php echo (int)$i; ?>" class="delete-block-btn">✕</button>
</div>
<?php endforeach; ?>
</div>
<!-- Plus-Button und das Pop-up-Menü -->
<div id="add-block-control" class="article-editor-scope add-block-control"> <div id="add-block-control" class="article-editor-scope add-block-control">
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button> <button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
<div id="block-popup" class="article-editor-scope block-popup hidden"> <div id="block-popup" class="article-editor-scope block-popup">
<button type="button" data-type="text">Textblock</button> <button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
<button type="button" data-type="image">Bild einfügen</button> <button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
</div> </div>
</div> </div>
<!-- Unsichtbares Textfeld, das die JSON-Daten hält und an den Controller postet -->
<textarea id="content" name="content" style="display:none;"><?php
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){
echo htmlspecialchars($_SESSION['old_content']);
unset($_SESSION['old_content']);
}elseif (isset($content) && !empty($content)){
echo htmlspecialchars($content);
} else {
echo '[]';
}
?></textarea>
<!-- unsichtbares Input, um die zu löschenden Bilder zu übergeben-->
<input type="hidden" id="deleted-images" name="deleted_images" value="[]">
</main> </main>
<!-- Seitenleiste --> <!-- Seitenleiste -->
+224 -5
View File
@@ -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
View File
@@ -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;
}
+29
View File
@@ -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);
}
}
+6 -5
View File
@@ -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"]);
?> ?>
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* Baut die Blockliste aus den POST-Daten (blocks[i][type], blocks[i][text],
* blocks[i][existing_image]) und ggf. hochgeladenen Dateien (blocks[i][image])
* zusammen. Läuft bei JEDEM Submit (Zwischen-Schritt "Block hinzufügen/löschen"
* UND finales Speichern/Veröffentlichen), damit neu ausgewählte Bilder in jedem
* Fall persistiert werden, bevor PHP die temporäre Upload-Datei nach
* Request-Ende verwirft.
*
* Wird sowohl vom createArticle- als auch vom updateArticle-Controller genutzt.
*
* @param array $postBlocks $_POST['blocks'] ?? []
* @param array $fileBlocks $_FILES['blocks'] ?? []
* @param string $uploadDir absoluter Pfad zum uploads-Verzeichnis (mit trailing slash)
* @return array Liste von ['type' => 'text'|'image', 'value' => string]
*/
function rebuildBlocksFromPost(array $postBlocks, array $fileBlocks, string $uploadDir): array {
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$keys = array_keys($postBlocks);
if (isset($fileBlocks['name']) && is_array($fileBlocks['name'])) {
$keys = array_unique(array_merge($keys, array_keys($fileBlocks['name'])));
}
sort($keys, SORT_NUMERIC);
$blocks = [];
foreach ($keys as $key) {
$type = $postBlocks[$key]['type'] ?? null;
if ($type === 'text') {
$blocks[] = [
'type' => 'text',
'value' => $postBlocks[$key]['text'] ?? '',
];
} elseif ($type === 'image') {
// Vorbelegung: bereits vorhandenes Server-Bild (falls Datei nicht ersetzt wird)
$value = $postBlocks[$key]['existing_image'] ?? '';
$hasUpload = isset($fileBlocks['error'][$key]['image'])
&& $fileBlocks['error'][$key]['image'] === UPLOAD_ERR_OK;
if ($hasUpload) {
$tmpName = $fileBlocks['tmp_name'][$key]['image'];
$originalName = $fileBlocks['name'][$key]['image'];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($extension, $allowedExtensions, true)) {
$extension = 'jpg';
}
$fileName = 'img_' . uniqid() . '.' . $extension;
$destination = $uploadDir . $fileName;
if (move_uploaded_file($tmpName, $destination)) {
$value = 'uploads/' . $fileName;
}
// Bei Fehler: alter Wert (falls vorhanden) bleibt erhalten, Block wird nicht verworfen
}
$blocks[] = [
'type' => 'image',
'value' => $value,
];
}
// unbekannter/fehlender type -> Block wird ignoriert
}
return $blocks;
}
+88
View File
@@ -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();
}
}
+20 -72
View File
@@ -2,57 +2,13 @@
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 === "search-results-controller") {
include_once "php/controller/search-results-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>
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="description" content="EduForge"> <meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann"> <meta name="author" content="Niklas Ortmann">
@@ -76,29 +32,21 @@ if ($pfad === "deleteAccount") {
<title>EduForge</title> <title>EduForge</title>
</head> </head>
<body>
<body>
<?php
include_once 'includes/navbar.php';
/*
Dynamischer Inhalt:
Je nach pfad-Parameter wird die passende Datei aus content geladen.
*/
if (file_exists('content/' . $pfad . '.php')) {
include_once 'content/' . $pfad . '.php';
} else {
include_once 'content/404.php';
}
include_once 'includes/footer.php';
?>
</body>
</html>
<?php <?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>
+235 -25
View File
@@ -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.
* 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"; 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,8 +115,9 @@ 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 = `
@@ -73,40 +125,186 @@ document.addEventListener("DOMContentLoaded", function () {
<strong>${escapeHtml(data.author)}</strong> <strong>${escapeHtml(data.author)}</strong>
<span>${escapeHtml(data.created)}</span> <span>${escapeHtml(data.created)}</span>
</p> </p>
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p> <p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
<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 = document.querySelector( 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 { } else {
commentElement.dataset.commentId = data.commentId; commentsList.prepend(commentElement);
}
} else {
commentElement.innerHTML = ` commentElement.innerHTML = `
<p> <p>
<strong>${escapeHtml(data.author)}</strong> <strong>${escapeHtml(data.author)}</strong>
<span>${escapeHtml(data.created)}</span> <span>${escapeHtml(data.created)}</span>
</p> </p>
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p> <p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
<button type="button" <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" class="reply-button"
data-comment-id="${escapeHtml(data.commentId)}" data-comment-id="${escapeHtml(data.commentId)}"
data-author="${escapeHtml(data.author)}"> data-author="${escapeHtml(data.author)}"
>
Antworten Antworten
</button> </a>
<div class="comment-replies"></div> <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 = "";
if (replyInfo) {
replyInfo.textContent = ""; replyInfo.textContent = "";
replyInfo.style.display = "none"; 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;
} }
}); });
+84 -123
View File
@@ -1,172 +1,133 @@
console.log("Die JavaScript-Datei wurde erfolgreich geladen!"); //console.log("editor.js wurde erfolgreich geladen!");
function initEditor() { function initEditor() {
const form = document.getElementById("editor-form"); const form = document.getElementById("editor-form");
if (!form) { if (!form) {
console.error("Skript abgebrochen: Formular nicht gefunden!"); console.error("editor.js abgebrochen: Formular nicht gefunden!");
return; return;
} else { } else {
console.log("Formular gefunden und Editor initialisiert:", form); console.log("Formular gefunden und editor.js initialisiert:", form);
} }
const container = document.getElementById("block-container"); const container = document.getElementById("block-container");
const plusButton = document.getElementById("plus-button"); const plusButton = document.getElementById("plus-button");
const popup = document.getElementById("block-popup"); const popup = document.getElementById("block-popup");
const hiddenContentInput = document.getElementById("content");
const initialImages = []; // Fortlaufender Zähler für eindeutige Block-Indizes. Wird nie wiederverwendet
// (auch nicht nach dem Löschen eines Blocks), damit sich neue und übrig
// gebliebene Blöcke beim finalen Submit nie einen Namen teilen.
let blockIndex = container.querySelectorAll(".editor-block").length;
// Ohne JS sind Textblock-/Bild-Button im Pop-up immer sichtbar und ganz normale
// Submit-Buttons. Erst mit JS blenden wir das Pop-up standardmäßig aus und
// steuern die Sichtbarkeit über den Plus-Button.
popup.classList.add("hidden");
// Pop-up umschalten bei Klick auf das Plus
plusButton.addEventListener("click", () => { plusButton.addEventListener("click", () => {
popup.classList.toggle("hidden"); popup.classList.toggle("hidden");
}); });
// Klick auf eine Block-Option im Pop-up // Klick auf "Textblock" / "Bild einfügen": lokal im DOM anlegen statt zum
popup.querySelectorAll("button").forEach(btn => { // Server zu submitten (entlastet den Server, kein Page-Reload nötig).
btn.addEventListener("click", function() { popup.querySelectorAll('[name="editor_action"]').forEach(btn => {
btn.addEventListener("click", function (e) {
e.preventDefault();
const type = this.getAttribute("data-type"); const type = this.getAttribute("data-type");
addBlockElement(type, ""); addBlockElement(type, "");
popup.classList.add("hidden"); popup.classList.add("hidden");
}); });
}); });
// Erstellt ein visuelles HTML-Element im Editor // Bereits vom Server gerenderte Blöcke (z.B. beim Bearbeiten eines bestehenden
function addBlockElement(type, value = "") { // Artikels oder nach einem Validierungsfehler) ebenfalls mit JS-Verhalten ausstatten.
const blockDiv = document.createElement("div"); container.querySelectorAll(".editor-block").forEach(blockDiv => {
blockDiv.classList.add("editor-block"); bindDeleteButton(blockDiv);
blockDiv.setAttribute("data-type", type); bindImageInput(blockDiv);
// Wenn es ein existierendes Server-Bild beim Laden ist, Pfad im globalen Array sichern
if (type === "image" && value && typeof value === 'string' && value.startsWith('uploads/')) {
initialImages.push(value);
blockDiv.setAttribute("data-value", value);
}
// Löschen-Button
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.innerHTML = "✕";
deleteBtn.classList.add("delete-block-btn");
deleteBtn.addEventListener("click", () => {
// ANPASSUNG 2B: Logik hier komplett geleert. Das '✕' entfernt den Block jetzt nur noch sicher aus dem HTML.
blockDiv.remove();
}); });
blockDiv.appendChild(deleteBtn);
// Erstellt einen neuen Block inkl. echter, benannter Formularfelder
// (blocks[i][type], blocks[i][text] bzw. blocks[i][image]). Diese Felder werden
// beim finalen Submit ganz normal vom Browser als multipart/form-data verschickt
// es ist kein manuelles Zusammenbauen von JSON beim Absenden mehr nötig.
function addBlockElement(type, value = "") {
const index = blockIndex++;
const blockDiv = document.createElement("div");
blockDiv.classList.add("editor-block", "article-editor-scope");
blockDiv.setAttribute("data-index", String(index));
const typeInput = document.createElement("input");
typeInput.type = "hidden";
typeInput.name = `blocks[${index}][type]`;
typeInput.value = type;
blockDiv.appendChild(typeInput);
if (type === "text") { if (type === "text") {
const textarea = document.createElement("textarea"); const textarea = document.createElement("textarea");
textarea.name = `blocks[${index}][text]`;
textarea.placeholder = "Schreibe deinen Textblock..."; textarea.placeholder = "Schreibe deinen Textblock...";
textarea.value = value; textarea.value = value;
blockDiv.appendChild(textarea); blockDiv.appendChild(textarea);
} else if (type === "image") { } else if (type === "image") {
const fileInput = document.createElement("input"); const fileInput = document.createElement("input");
fileInput.type = "file"; fileInput.type = "file";
fileInput.name = `blocks[${index}][image]`;
fileInput.accept = "image/*"; fileInput.accept = "image/*";
blockDiv.appendChild(fileInput);
}
const imgPreview = document.createElement("img"); const deleteBtn = document.createElement("button");
deleteBtn.type = "submit";
deleteBtn.name = "editor_action";
deleteBtn.value = `delete_block:${index}`;
deleteBtn.classList.add("delete-block-btn");
deleteBtn.innerHTML = "✕";
blockDiv.appendChild(deleteBtn);
container.appendChild(blockDiv);
bindDeleteButton(blockDiv);
bindImageInput(blockDiv);
}
// Löschen-Button eines Blocks lokal abfangen: entfernt den Block nur aus dem DOM,
// statt das Formular zum Server zu senden.
function bindDeleteButton(blockDiv) {
const btn = blockDiv.querySelector(".delete-block-btn");
if (!btn) return;
btn.addEventListener("click", (e) => {
e.preventDefault();
blockDiv.remove();
});
}
// Zeigt bei Auswahl einer Bilddatei sofort eine Vorschau an. Rein optisch
// 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;
let imgPreview = blockDiv.querySelector(".block-image-preview");
if (!imgPreview) {
imgPreview = document.createElement("img");
imgPreview.classList.add("block-image-preview");
imgPreview.style.maxWidth = "200px"; imgPreview.style.maxWidth = "200px";
imgPreview.style.display = "block"; imgPreview.style.display = "none";
imgPreview.style.marginTop = "10px"; imgPreview.style.marginTop = "10px";
blockDiv.insertBefore(imgPreview, fileInput.nextSibling);
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() { fileInput.addEventListener("change", function () {
if (this.files && this.files[0]) { if (this.files && this.files[0]) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function(e) { reader.onload = (e) => {
imgPreview.src = e.target.result; imgPreview.src = e.target.result;
blockDiv.setAttribute("data-value", e.target.result); imgPreview.style.display = "block";
} };
reader.readAsDataURL(this.files[0]); reader.readAsDataURL(this.files[0]);
} }
}); });
blockDiv.appendChild(fileInput);
blockDiv.appendChild(imgPreview);
}
container.appendChild(blockDiv);
}
// beim Abschicken verbleibende Blöcke auslesen UND gelöschte Bilder ermitteln
form.addEventListener("submit", function(e) {
const blocks = [];
const currentImages = [];
// alle aktuell im Formular verbliebenen Blöcke scannen
container.querySelectorAll(".editor-block").forEach(blockDiv => {
const type = blockDiv.getAttribute("data-type");
let value = "";
if (type === "text") {
value = blockDiv.querySelector("textarea").value;
} else if (type === "image") {
const imgTag = blockDiv.querySelector("img");
if (imgTag) {
const srcValue = imgTag.getAttribute("src") || "";
// Wenn es ein neues Bild ist, nutzen wir das data-value (Base64)
if (srcValue.startsWith('data:image/')) {
value = blockDiv.getAttribute("data-value") || "";
} else {
value = srcValue;
}
}
// Pfade sammeln, die der Nutzer NICHT gelöscht hat (für den Abgleich)
if (value && value.startsWith('uploads/')) {
currentImages.push(value);
}
}
blocks.push({ type: type, value: value });
});
// das reguläre unsichtbare Content-Feld befüllen
hiddenContentInput.value = JSON.stringify(blocks);
// Differenz berechnen: Welche Bilder aus 'initialImages' fehlen in 'currentImages' ?
const deletedImages = initialImages.filter(img => !currentImages.includes(img));
// das 'deleted_images'-Feld dynamisch befüllen und an den Controller senden
let deletedInput = document.getElementById("deleted-images");
if (!deletedInput) {
deletedInput = document.createElement("input");
deletedInput.type = "hidden";
deletedInput.id = "deleted-images";
deletedInput.name = "deleted_images";
form.appendChild(deletedInput);
}
deletedInput.value = JSON.stringify(deletedImages);
});
// Existierende Blöcke laden (stellt alte Daten aus der Session wieder her)
try {
const initialBlocks = JSON.parse(hiddenContentInput.value.trim());
if (Array.isArray(initialBlocks)) {
initialBlocks.forEach(b => {
if (b.type === "image" && b.value && typeof b.value === 'string' && !b.value.startsWith('data:image/')) {
let cleanPath = b.value.trim().replace(/\\\//g, '/'); // Verwandelt \/ in /
initialImages.push(cleanPath);
addBlockElement(b.type, cleanPath);
} else {
addBlockElement(b.type, b.value);
}
});
console.log("Erfolgreich registrierte Start-Bilder:", initialImages);
}
} catch(e) {
if (hiddenContentInput.value.trim() !== "") {
addBlockElement("text", hiddenContentInput.value);
}
} }
} }
+23 -4
View File
@@ -25,6 +25,7 @@
let listContainer, sortRadios, categorySelect, limitSelect; let listContainer, sortRadios, categorySelect, limitSelect;
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl; let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
let loadingElement;
function init() { function init() {
listContainer = document.querySelector('.s-res-list'); listContainer = document.querySelector('.s-res-list');
@@ -38,23 +39,41 @@
numbersContainer = document.getElementById('dynamic-page-numbers'); numbersContainer = document.getElementById('dynamic-page-numbers');
jsNav = document.getElementById('js-page-navigation'); jsNav = document.getElementById('js-page-navigation');
resultCountEl = document.getElementById('s-res-result-count'); resultCountEl = document.getElementById('s-res-result-count');
loadingElement = document.getElementById('results-loading');
const checkedRadio = document.querySelector('.sort-radio:checked'); const checkedRadio = document.querySelector('.sort-radio:checked');
state.sort = checkedRadio ? checkedRadio.value : 'alphabet'; state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
state.category = categorySelect ? categorySelect.value : 'all'; state.category = categorySelect ? categorySelect.value : 'all';
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10; state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
if (loadingElement) {
loadingElement.hidden = false;
}
fetchFullDataset() fetchFullDataset()
.then(function (data) { .then(function (data) {
state.allItems = data.results || []; state.allItems = data.results || [];
attachEvents(); attachEvents();
render(); render();
if (jsNav) jsNav.style.display = 'flex';
if (jsNav) {
jsNav.style.display = 'flex';
}
}) })
.catch(function (err) { .catch(function (err) {
// JSON-Endpoint nicht erreichbar: die serverseitig gerenderte /*
// (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter. * Falls das Nachladen nicht funktioniert, bleibt die bereits
console.error('Suchergebnisse konnten nicht nachgeladen werden:', err); * serverseitig ausgegebene Ergebnisliste sichtbar.
*/
console.error(
'Suchergebnisse konnten nicht nachgeladen werden:',
err
);
})
.finally(function () {
if (loadingElement) {
loadingElement.hidden = true;
}
}); });
} }
+23 -4
View File
@@ -21,6 +21,7 @@
let listContainer, sortRadios, searchInput, limitSelect; let listContainer, sortRadios, searchInput, limitSelect;
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl; let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
let loadingElement;
let searchDebounceTimer; let searchDebounceTimer;
function init() { function init() {
@@ -35,23 +36,41 @@
numbersContainer = document.getElementById('dynamic-page-numbers'); numbersContainer = document.getElementById('dynamic-page-numbers');
jsNav = document.getElementById('js-page-navigation'); jsNav = document.getElementById('js-page-navigation');
resultCountEl = document.getElementById('s-res-result-count'); resultCountEl = document.getElementById('s-res-result-count');
loadingElement = document.getElementById('results-loading');
const checkedRadio = document.querySelector('.sort-radio:checked'); const checkedRadio = document.querySelector('.sort-radio:checked');
state.sort = checkedRadio ? checkedRadio.value : 'alphabet'; state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
state.query = searchInput ? searchInput.value : ''; state.query = searchInput ? searchInput.value : '';
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10; state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
if (loadingElement) {
loadingElement.hidden = false;
}
fetchFullDataset() fetchFullDataset()
.then(function (data) { .then(function (data) {
state.allItems = data.results || []; state.allItems = data.results || [];
attachEvents(); attachEvents();
render(); render();
if (jsNav) jsNav.style.display = 'flex';
if (jsNav) {
jsNav.style.display = 'flex';
}
}) })
.catch(function (err) { .catch(function (err) {
// JSON-Endpoint nicht erreichbar: die serverseitig gerenderte /*
// (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter. * Falls das Nachladen nicht funktioniert, bleibt die bereits
console.error('Kategorie-Beiträge konnten nicht nachgeladen werden:', err); * serverseitig ausgegebene Kategorieansicht sichtbar.
*/
console.error(
'Kategorie-Beiträge konnten nicht nachgeladen werden:',
err
);
})
.finally(function () {
if (loadingElement) {
loadingElement.hidden = true;
}
}); });
} }
+218 -26
View File
@@ -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"]);
$authorName = $_SESSION["user_email"];
if ($user !== null) {
$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, "commentId" => $commentId,
"author" => $_SESSION["user_email"], "author" => $authorName,
"content" => $content, "content" => $content,
"created" => date("Y-m-d H:i:s"), "created" => date("Y-m-d H:i:s"),
"parentCommentId" => $parentCommentId "parentCommentId" => $parentCommentId
]); ]
);
} catch (Exception $e) { } catch (Throwable $e) {
echo json_encode([ sendCommentResponse(
"success" => false, false,
"message" => "Kommentar konnte nicht gespeichert werden." "Der Kommentar konnte nicht gespeichert werden.",
]); $articleId
);
} }
+52 -58
View File
@@ -5,24 +5,68 @@ 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") {
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=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_title"] = $_POST["title"] ?? '';
$_SESSION["old_content"] = $_POST["content"] ?? '';
$_SESSION["old_category"] = $_POST["category"] ?? ''; $_SESSION["old_category"] = $_POST["category"] ?? '';
$_SESSION["old_tags"] = $_POST["tags"] ?? ''; $_SESSION["old_tags"] = $_POST["tags"] ?? '';
if(!isset($_POST["title"]) ||!isset($_POST["content"]) || !isset($_POST["category"])){ $blocks = rebuildBlocksFromPost($_POST['blocks'] ?? [], $_FILES['blocks'] ?? [], $uploadDir);
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
// ---------------------------------------------------------------------
// Zwischenspeichern
// ---------------------------------------------------------------------
if (isset($_POST['editor_action']) && $_POST['editor_action'] !== '') {
$action = $_POST['editor_action'];
if ($action === 'add_text') {
$blocks[] = ['type' => 'text', 'value' => ''];
} elseif ($action === 'add_image') {
$blocks[] = ['type' => 'image', 'value' => ''];
} elseif (str_starts_with($action, 'delete_block:')) {
$deleteIndex = (int) substr($action, strlen('delete_block:'));
unset($blocks[$deleteIndex]);
$blocks = array_values($blocks);
}
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
header("location: ../../index.php?pfad=createArticle");
exit();
}
// ---------------------------------------------------------------------
// Echtes Veröffentlichen
// ---------------------------------------------------------------------
if (!isset($_POST["title"]) || !isset($_POST["category"])) {
$_SESSION["message"] = "missing_parameters"; $_SESSION["message"] = "missing_parameters";
header("location: ../../index.php?pfad=createArticle"); header("location: ../../index.php?pfad=createArticle");
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'] ?? '';
@@ -63,67 +107,17 @@ if (!isset($_SESSION["user"])) {
$cleanedTags = implode(',', $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: --------------------------- // ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
// $content enthält bereits die finalen "uploads/..."-Pfade (kein Base64 mehr),
// da rebuildBlocksFromPost() Datei-Uploads sofort verarbeitet.
$articleManager->addArticle($title, $content, $author, $category, $cleanedTags); $articleManager->addArticle($title, $content, $author, $category, $cleanedTags);
// Formulardaten nach erfolgreichem Erstellen aus der Session löschen // Formulardaten nach erfolgreichem Erstellen aus der Session löschen
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"] = "internal_error"; $_SESSION["message"] = "internal_error";
header("location: ../../index.php?pfad=createArticle"); header("location: ../../index.php?pfad=createArticle");
exit(); exit();
@@ -134,5 +128,5 @@ if (!isset($_SESSION["user"])) {
header("location: ../../index.php"); header("location: ../../index.php");
exit(); 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
+12 -3
View File
@@ -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();
+123
View File
@@ -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();
+39 -1
View File
@@ -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";
}
+13
View File
@@ -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);
+4
View File
@@ -1,4 +1,8 @@
<?php <?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION = []; $_SESSION = [];
session_destroy(); session_destroy();
+8 -1
View File
@@ -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";
+40 -11
View File
@@ -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,6 +2,7 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
require_once '../model/ArticleManager.php'; require_once '../model/ArticleManager.php';
require_once '../model/UserManager.php'; require_once '../model/UserManager.php';
require_once '../model/Article.php'; require_once '../model/Article.php';
+9 -4
View File
@@ -7,10 +7,14 @@ require_once 'php/model/Article.php';
require_once 'php/model/ArticleManager.php'; require_once 'php/model/ArticleManager.php';
require_once 'php/model/UserManager.php'; require_once 'php/model/UserManager.php';
require_once 'php/model/CommentManager.php'; require_once 'php/model/CommentManager.php';
require_once 'php/validator/article-validator.php';
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){
@@ -38,7 +42,7 @@ if (isset($_GET["id"]) && !empty($_GET["id"])){
} }
$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()) {
@@ -49,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{
+97 -78
View File
@@ -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,13 +55,53 @@ 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'] ?? '';
@@ -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: -------------------------- $newImagePaths = [];
if (is_array($blocks)) { foreach ($blocks as $block) {
foreach ($blocks as &$block) { if (($block['type'] ?? '') === 'image' && !empty($block['value'])) {
// Prüfen, ob der Block ein Bild ist und ein NEUES Bild (Base64-Format) enthält $newImagePaths[] = $block['value'];
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 $orphanedImages = array_diff($oldImagePaths, $newImagePaths);
$finalContent = json_encode($blocks, JSON_UNESCAPED_UNICODE); 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();
+101
View File
@@ -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();
+5 -5
View File
@@ -13,7 +13,7 @@ class Comment
private int $id; private int $id;
private int $articleId; private int $articleId;
private ?int $parentCommentId; private ?int $parentCommentId;
private string $author; private ?string $author;
private string $content; private string $content;
private string $created; private string $created;
@@ -23,7 +23,7 @@ class Comment
* @param int $id Eindeutige ID des Kommentars * @param int $id Eindeutige ID des Kommentars
* @param int $articleId ID des zugehörigen Beitrags * @param int $articleId ID des zugehörigen Beitrags
* @param int|null $parentCommentId ID des Eltern-Kommentars oder null * @param int|null $parentCommentId ID des Eltern-Kommentars oder null
* @param string $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;
} }
+36
View File
@@ -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;
} }
+10 -41
View File
@@ -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 = [];
@@ -315,32 +290,26 @@ class DatabaseArticleManager implements ArticleManagerDAO {
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'] ?? '',
+191 -1
View File
@@ -17,7 +17,7 @@ class DatabaseCommentManager implements CommentManagerDAO
/** /**
* Erstellt die Kommentartabelle, falls diese noch nicht existiert. * Erstellt die Kommentartabelle, falls diese noch nicht existiert.
*/ */
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");
}
}
} }
+8 -3
View File
@@ -49,11 +49,11 @@ class DatabaseInitializer {
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
); );
"); ");
@@ -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]
+15
View File
@@ -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