merge-commit

This commit is contained in:
NOrtmann1
2026-07-19 10:50:22 +02:00
12 changed files with 1104 additions and 118 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="dataSourceStorageLocal" created-in="IU-261.25134.95"> <component name="dataSourceStorageLocal" created-in="IU-253.32098.101">
<data-source name="articles" uuid="315cb5c9-2b0f-435b-b602-59823b160908"> <data-source name="articles" uuid="315cb5c9-2b0f-435b-b602-59823b160908">
<database-info product="SQLite" version="3.51.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.51.1.0" dbms="SQLITE" exact-version="3.51.1" exact-driver-version="3.51"> <database-info product="SQLite" version="3.51.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.51.1.0" dbms="SQLITE" exact-version="3.51.1" exact-driver-version="3.51">
<identifier-quote-string>&quot;</identifier-quote-string> <identifier-quote-string>&quot;</identifier-quote-string>
+3
View File
@@ -36,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
+310 -26
View File
@@ -5,6 +5,34 @@ $repliesByParent = [];
$articleObj = null; $articleObj = null;
include_once 'php/controller/showArticle-controller.php'; include_once 'php/controller/showArticle-controller.php';
/*
* Ermittelt, ob ohne JavaScript auf einen Kommentar
* geantwortet werden soll.
*/
$replyTo = filter_input(
INPUT_GET,
"reply_to",
FILTER_VALIDATE_INT
);
$replyAuthor = null;
if ($replyTo !== false && $replyTo !== null) {
foreach ($mainComments as $mainComment) {
if ($mainComment->getId() === $replyTo) {
$replyAuthor = $mainComment->getAuthor();
break;
}
}
}
/*
* Eine Antwort darf nur auf einen existierenden
* Hauptkommentar geschrieben werden.
*/
if ($replyAuthor === null) {
$replyTo = null;
}
?> ?>
<!-- <!--
Seite: Anzeige für Beiträge Seite: Anzeige für Beiträge
@@ -17,8 +45,6 @@ include_once 'php/controller/showArticle-controller.php';
<!-- Metadaten & Titel --> <!-- Metadaten & Titel -->
<div class="article-view-top-section"> <div class="article-view-top-section">
<div class="article-view-top-section">
<div class="category-and-likes-row"> <div class="category-and-likes-row">
<?php if (isset($category) && !empty($category)): ?> <?php if (isset($category) && !empty($category)): ?>
<span class="article-view-category"><?php echo htmlspecialchars($category); ?></span> <span class="article-view-category"><?php echo htmlspecialchars($category); ?></span>
@@ -106,39 +132,268 @@ include_once 'php/controller/showArticle-controller.php';
</div> </div>
<?php endif; ?> <?php endif; ?>
<section class="article-comments-section"> <section class="article-comments-section" id="comments">
<h2>Kommentare</h2> <h2>Kommentare</h2>
<?php if (isset($_SESSION["comment_message"])): ?>
<div class="alert-message <?php
echo ($_SESSION["comment_message_type"] ?? "") === "success"
? "is-success"
: "is-error";
?>">
<?php echo htmlspecialchars($_SESSION["comment_message"]); ?>
</div>
<?php
unset($_SESSION["comment_message"]);
unset($_SESSION["comment_message_type"]);
?>
<?php endif; ?>
<div id="comments-list"> <div id="comments-list">
<?php if (!empty($mainComments)): ?> <?php if (!empty($mainComments)): ?>
<?php foreach ($mainComments as $comment): ?> <?php foreach ($mainComments as $comment): ?>
<div class="comment-item" data-comment-id="<?php echo htmlspecialchars($comment->getId()); ?>"> <div class="comment-item"
<p> data-comment-id="<?php echo htmlspecialchars(
<strong><?php echo htmlspecialchars($comment->getAuthor()); ?></strong> (string) $comment->getId()
<span><?php echo htmlspecialchars($comment->getCreated()); ?></span> ); ?>">
</p>
<p><?php echo nl2br(htmlspecialchars($comment->getContent())); ?></p> <?php
$isDeleted = $comment->getContent()
=== "Dieser Kommentar wurde gelöscht.";
?>
<?php if ($isDeleted): ?>
<p class="deleted-comment">
Dieser Kommentar wurde gelöscht.
</p>
<?php else: ?>
<p>
<strong>
<?php echo htmlspecialchars($comment->getAuthor()); ?>
</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">
<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">
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $comment->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $comment->getArticleId()
); ?>">
<button type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');">
Kommentar löschen
</button>
</form>
<?php endif; ?>
<?php if (isset($_SESSION["user_email"])): ?>
<a href="index.php?pfad=<?php
echo urlencode($_GET["pfad"] ?? "showArticle");
?>&id=<?php
echo urlencode((string) $comment->getArticleId());
?>&reply_to=<?php
echo urlencode((string) $comment->getId());
?>#comment-form"
class="reply-button"
data-comment-id="<?php echo htmlspecialchars(
(string) $comment->getId()
); ?>"
data-author="<?php echo htmlspecialchars(
$comment->getAuthor()
); ?>">
Antworten
</a>
<?php endif; ?>
<?php if (isset($_SESSION["user_email"])): ?>
<button type="button"
class="reply-button"
data-comment-id="<?php echo htmlspecialchars($comment->getId()); ?>"
data-author="<?php echo htmlspecialchars($comment->getAuthor()); ?>">
Antworten
</button>
<?php endif; ?> <?php endif; ?>
<div class="comment-replies"> <div class="comment-replies">
<?php if (isset($repliesByParent[$comment->getId()])): ?> <?php if (isset($repliesByParent[$comment->getId()])): ?>
<?php foreach ($repliesByParent[$comment->getId()] as $reply): ?> <?php foreach ($repliesByParent[$comment->getId()] as $reply): ?>
<div class="comment-item comment-reply"> <div class="comment-item comment-reply">
<p>
<strong><?php echo htmlspecialchars($reply->getAuthor()); ?></strong>
<span><?php echo htmlspecialchars($reply->getCreated()); ?></span>
</p>
<p><?php echo nl2br(htmlspecialchars($reply->getContent())); ?></p> <?php
$isReplyDeleted = $reply->getContent()
=== "Dieser Kommentar wurde gelöscht.";
?>
<?php if ($isReplyDeleted): ?>
<p class="deleted-comment">
Dieser Kommentar wurde gelöscht.
</p>
<?php else: ?>
<p>
<strong>
<?php echo htmlspecialchars($reply->getAuthor()); ?>
</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">
<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">
<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 +408,46 @@ 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">
<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"
@@ -181,4 +465,4 @@ include_once 'php/controller/showArticle-controller.php';
</div> </div>
<?php endif; ?> <?php endif; ?>
</section> </section>
</main> </main>
+192 -43
View File
@@ -11,20 +11,31 @@ 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) { if (!form || !commentsList || !commentContent || !parentCommentInput) {
return; return;
} }
/** /**
* Aktiviert einen einzelnen Antworten-Button. * Aktiviert einen einzelnen Antworten-Link.
* *
* @param {HTMLButtonElement} button Antworten-Button * @param {HTMLAnchorElement} replyLink Antworten-Link
*/ */
function registerReplyButton(button) { function registerReplyButton(replyLink) {
button.addEventListener("click", function () { replyLink.addEventListener("click", function (event) {
parentCommentInput.value = button.dataset.commentId; /*
replyInfo.textContent = "Antwort auf " + button.dataset.author; * Mit JavaScript wird die Seite nicht neu geladen.
replyInfo.style.display = "block"; * Ohne JavaScript funktioniert der normale Link.
*/
event.preventDefault();
parentCommentInput.value = replyLink.dataset.commentId;
if (replyInfo) {
replyInfo.textContent =
"Antwort auf " + replyLink.dataset.author;
replyInfo.style.display = "block";
}
commentContent.focus(); commentContent.focus();
}); });
} }
@@ -45,18 +56,29 @@ document.addEventListener("DOMContentLoaded", function () {
const formData = new FormData(form); const formData = new FormData(form);
const parentCommentId = parentCommentInput.value; const parentCommentId = parentCommentInput.value;
fetch("php/ajax/add-comment.php", { fetch(form.action, {
method: "POST", method: "POST",
body: formData body: formData,
headers: {
"X-Requested-With": "XMLHttpRequest"
}
}) })
.then(response => response.json()) .then(function (response) {
.then(data => { if (!response.ok) {
throw new Error("Fehlerhafte Serverantwort.");
}
return response.json();
})
.then(function (data) {
if (!data.success) { if (!data.success) {
alert(data.message); alert(data.message);
return; return;
} }
const emptyMessage = commentsList.querySelector(".no-comments-message"); const emptyMessage = commentsList.querySelector(
".no-comments-message"
);
if (emptyMessage) { if (emptyMessage) {
emptyMessage.remove(); emptyMessage.remove();
@@ -64,49 +86,172 @@ document.addEventListener("DOMContentLoaded", function () {
const commentElement = document.createElement("div"); const commentElement = document.createElement("div");
commentElement.classList.add("comment-item"); commentElement.classList.add("comment-item");
commentElement.dataset.commentId = data.commentId;
if (parentCommentId) { if (parentCommentId !== "") {
commentElement.classList.add("comment-reply"); commentElement.classList.add("comment-reply");
commentElement.innerHTML = ` commentElement.innerHTML = `
<p> <p>
<strong>${escapeHtml(data.author)}</strong> <strong>${escapeHtml(data.author)}</strong>
<span>${escapeHtml(data.created)}</span> <span>${escapeHtml(data.created)}</span>
</p> </p>
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
`;
const parentReplies = document.querySelector( <p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
<details class="edit-comment-details">
<summary class="edit-comment-button">
Antwort bearbeiten
</summary>
<form
method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form"
>
<input
type="hidden"
name="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="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<button
type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');"
>
Kommentar löschen
</button>
</form>
`;
const parentReplies = commentsList.querySelector(
`.comment-item[data-comment-id="${parentCommentId}"] .comment-replies` `.comment-item[data-comment-id="${parentCommentId}"] .comment-replies`
); );
if (parentReplies) { if (parentReplies) {
parentReplies.appendChild(commentElement); parentReplies.appendChild(commentElement);
} else {
commentsList.prepend(commentElement);
} }
} else { } else {
commentElement.dataset.commentId = data.commentId;
commentElement.innerHTML = ` commentElement.innerHTML = `
<p> <p>
<strong>${escapeHtml(data.author)}</strong> <strong>${escapeHtml(data.author)}</strong>
<span>${escapeHtml(data.created)}</span> <span>${escapeHtml(data.created)}</span>
</p> </p>
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
<button type="button" <p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
class="reply-button"
data-comment-id="${escapeHtml(data.commentId)}"
data-author="${escapeHtml(data.author)}">
Antworten
</button>
<div class="comment-replies"></div> <details class="edit-comment-details">
`; <summary class="edit-comment-button">
Kommentar bearbeiten
</summary>
<form
method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form"
>
<input
type="hidden"
name="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="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<button
type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');"
>
Kommentar löschen
</button>
</form>
<a
href="#comment-form"
class="reply-button"
data-comment-id="${escapeHtml(data.commentId)}"
data-author="${escapeHtml(data.author)}"
>
Antworten
</a>
<div class="comment-replies"></div>
`;
commentsList.prepend(commentElement); commentsList.prepend(commentElement);
const newReplyButton = commentElement.querySelector(".reply-button"); const newReplyButton =
commentElement.querySelector(".reply-button");
if (newReplyButton) { if (newReplyButton) {
registerReplyButton(newReplyButton); registerReplyButton(newReplyButton);
@@ -115,10 +260,14 @@ document.addEventListener("DOMContentLoaded", function () {
commentContent.value = ""; commentContent.value = "";
parentCommentInput.value = ""; parentCommentInput.value = "";
replyInfo.textContent = "";
replyInfo.style.display = "none"; if (replyInfo) {
replyInfo.textContent = "";
replyInfo.style.display = "none";
}
}) })
.catch(() => { .catch(function (error) {
console.error(error);
alert("Kommentar konnte nicht gesendet werden."); alert("Kommentar konnte nicht gesendet werden.");
}); });
}); });
@@ -126,12 +275,12 @@ document.addEventListener("DOMContentLoaded", function () {
/** /**
* 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;
} }
}); });
+156 -30
View File
@@ -3,32 +3,153 @@ 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";
if (!isset($_SESSION["user_email"])) { /**
echo json_encode([ * Prüft, ob die Anfrage durch JavaScript per AJAX gesendet wurde.
"success" => false, */
"message" => "Du musst angemeldet sein, um zu kommentieren." $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");
}
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
);
}
/*
* 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(); );
}
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
);
} }
try { try {
@@ -41,18 +162,23 @@ try {
$parentCommentId $parentCommentId
); );
echo json_encode([ sendCommentResponse(
"success" => true, true,
"commentId" => $commentId, "Der Kommentar wurde erfolgreich gespeichert.",
"author" => $_SESSION["user_email"], $articleId,
"content" => $content, [
"created" => date("Y-m-d H:i:s"), "commentId" => $commentId,
"parentCommentId" => $parentCommentId "author" => $_SESSION["user_email"],
]); "content" => $content,
"created" => date("Y-m-d H:i:s"),
"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
);
} }
+105
View File
@@ -0,0 +1,105 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . "/../model/CommentManager.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();
}
/*
* 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();
+2 -1
View File
@@ -49,8 +49,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{
@@ -0,0 +1,92 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once "php/model/CommentManager.php";
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: index.php");
exit();
}
if (!isset($_SESSION["user_email"])) {
header("Location: index.php?pfad=login");
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;
} }
+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");
}
}
} }
+11 -11
View File
@@ -46,17 +46,17 @@ class DatabaseInitializer {
$db->exec(" $db->exec("
CREATE TABLE IF NOT EXISTS comments ( CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id INTEGER NOT NULL, article_id INTEGER NOT NULL,
parent_comment_id INTEGER NULL, parent_comment_id INTEGER NULL,
author TEXT NOT NULL, author TEXT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE, FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
FOREIGN KEY (author) REFERENCES users(email) ON DELETE CASCADE, FOREIGN KEY (author) REFERENCES users(email) ON DELETE SET NULL,
FOREIGN KEY (parent_comment_id) REFERENCES comments(id) ON DELETE CASCADE FOREIGN KEY (parent_comment_id) REFERENCES comments(id) ON DELETE CASCADE
); );
"); ");
$initializer = new self(); $initializer = new self();
$availableEmails = $initializer->seedDummyUsers($db); $availableEmails = $initializer->seedDummyUsers($db);