diff --git a/content/createArticle.php b/content/createArticle.php index c682836..1dd03ea 100644 --- a/content/createArticle.php +++ b/content/createArticle.php @@ -27,6 +27,8 @@ $blocks = array_values($blocks); // sequentielle Indizes 0..n-1 sicherstellen -->
+ +
diff --git a/content/profile.php b/content/profile.php index 815e5ad..3109587 100644 --- a/content/profile.php +++ b/content/profile.php @@ -18,6 +18,8 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error); + + + + + + + + + +

@@ -233,6 +238,8 @@ if ($replyAuthor === null) { action="index.php?pfad=updateComment" class="edit-comment-form"> + + + + + + + + + + " id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container"> + +
diff --git a/css/navbar.css b/css/navbar.css index c68e94f..9d9b208 100644 --- a/css/navbar.css +++ b/css/navbar.css @@ -285,6 +285,32 @@ CSS für die navbar 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 { display: block; list-style: none; diff --git a/includes/alertMessages.php b/includes/alertMessages.php index 645f532..2c42d14 100644 --- a/includes/alertMessages.php +++ b/includes/alertMessages.php @@ -89,6 +89,12 @@ Es ist ein Datenbankfehler aufgetreten. Bitte versuche es erneut.

+ +

+ Deine Sitzung ist abgelaufen oder die Anfrage konnte nicht überprüft werden. + Bitte lade die Seite neu und versuche es erneut. +

+ +?> \ No newline at end of file diff --git a/includes/csrf.php b/includes/csrf.php new file mode 100644 index 0000000..db0575e --- /dev/null +++ b/includes/csrf.php @@ -0,0 +1,88 @@ +'; +} + +/** + * 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(); + } +} \ No newline at end of file diff --git a/includes/navbar.php b/includes/navbar.php index c62204e..b16bf03 100644 --- a/includes/navbar.php +++ b/includes/navbar.php @@ -179,4 +179,4 @@ Globales Menü, wird via PHP später in alle Seiten eingebunden - \ No newline at end of file + diff --git a/index.php b/index.php index 42d054c..b70d61c 100644 --- a/index.php +++ b/index.php @@ -2,6 +2,7 @@ if (session_status() === PHP_SESSION_NONE) { session_start(); } +include_once "includes/csrf.php"; include_once "php/controller/index-controller.php"; ?> diff --git a/js/comments.js b/js/comments.js index b9b83b0..9ad43a8 100644 --- a/js/comments.js +++ b/js/comments.js @@ -109,6 +109,12 @@ document.addEventListener("DOMContentLoaded", function () { action="index.php?pfad=updateComment" class="edit-comment-form" > + + + + + + + + getArticle($articleId); +if ($existingArticle === null) { + sendCommentResponse( + false, + "Der zugehörige Beitrag wurde nicht gefunden.", + null + ); +} + if ($content === "") { sendCommentResponse( false, @@ -152,6 +178,30 @@ if ( ); } +/* + * 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 { $commentManager = CommentManager::getInstance(); diff --git a/php/controller/createArticle-controller.php b/php/controller/createArticle-controller.php index de3ed9d..3817cb1 100644 --- a/php/controller/createArticle-controller.php +++ b/php/controller/createArticle-controller.php @@ -6,6 +6,7 @@ require_once '../model/LocalArticleManager.php'; require_once '../model/ArticleManager.php'; require_once '../validator/article-validator.php'; require_once '../../includes/article-block-helper.php'; +require_once '../../includes/csrf.php'; if (!isset($_SESSION["user"])) { header("Location: index.php?pfad=login"); @@ -14,6 +15,13 @@ if (!isset($_SESSION["user"])) { 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); diff --git a/php/controller/deleteAccount-controller.php b/php/controller/deleteAccount-controller.php index 351364d..d65b51c 100644 --- a/php/controller/deleteAccount-controller.php +++ b/php/controller/deleteAccount-controller.php @@ -5,12 +5,24 @@ if (session_status() === PHP_SESSION_NONE) { require_once __DIR__ . "/../model/UserManager.php"; require_once __DIR__ . "/../model/ArticleManager.php"; +require_once __DIR__ . "/../../includes/csrf.php"; if (!isset($_SESSION["user"])) { header("Location: index.php?pfad=login"); 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 Funktion: Entfernt User aus der Datenbank und beendet die Session diff --git a/php/controller/deleteArticle-controller.php b/php/controller/deleteArticle-controller.php index ffc664d..150b92e 100644 --- a/php/controller/deleteArticle-controller.php +++ b/php/controller/deleteArticle-controller.php @@ -4,6 +4,7 @@ if (session_status() === PHP_SESSION_NONE) { } require_once __DIR__ . "/../model/ArticleManager.php"; +require_once __DIR__ . "/../../includes/csrf.php"; if (!isset($_SESSION["user"])) { header("Location: index.php?pfad=login"); @@ -12,6 +13,13 @@ if (!isset($_SESSION["user"])) { 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"])) { $user = $_SESSION["user_email"]; } else { @@ -22,9 +30,10 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") { exit(); } - if (isset($_POST["id"]) && !empty($_POST["id"])) { - $id = $_POST["id"]; - } else { + // Die Beitrags-ID muss eine gültige numerische ID sein. + $id = filter_input(INPUT_POST, "id", FILTER_VALIDATE_INT); + + if ($id === false || $id === null) { $_SESSION["message"] = "missing_id"; header("location: ../../index.php?pfad=profile"); exit(); @@ -44,4 +53,4 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") { $_SESSION["message"] = "article_deleted"; header("location: ../../index.php?pfad=profile"); exit(); -} +} \ No newline at end of file diff --git a/php/controller/deleteComment-controller.php b/php/controller/deleteComment-controller.php index 4eeaeca..a006757 100644 --- a/php/controller/deleteComment-controller.php +++ b/php/controller/deleteComment-controller.php @@ -5,6 +5,7 @@ if (session_status() === PHP_SESSION_NONE) { } require_once __DIR__ . "/../model/CommentManager.php"; +require_once __DIR__ . "/../../includes/csrf.php"; /* * Kommentare dürfen nur über ein POST-Formular gelöscht werden. @@ -44,6 +45,23 @@ if (!isset($_SESSION["user_email"])) { 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. */ @@ -102,4 +120,4 @@ header( . "#comments" ); -exit(); +exit(); \ No newline at end of file diff --git a/php/controller/like-controller.php b/php/controller/like-controller.php index d0d9a95..56bdc33 100644 --- a/php/controller/like-controller.php +++ b/php/controller/like-controller.php @@ -5,6 +5,12 @@ if (session_status() === PHP_SESSION_NONE) { require_once __DIR__ . '/../model/Article.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 if (isset($_GET["id"]) && !empty($_GET["id"])) { @@ -17,6 +23,13 @@ if (isset($_GET["id"]) && !empty($_GET["id"])) { 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 { $articleManager = ArticleManager::getInstance(); $articleManager->toggleLike($articleId, $userEmail); diff --git a/php/controller/logout-controller.php b/php/controller/logout-controller.php index 49386ab..a336fab 100644 --- a/php/controller/logout-controller.php +++ b/php/controller/logout-controller.php @@ -1,4 +1,8 @@ getArticle($id); if($article != null){ @@ -38,7 +42,7 @@ if (isset($_GET["id"]) && !empty($_GET["id"])){ } $commentManager = CommentManager::getInstance(); - $comments = $commentManager->getCommentsByArticle($_GET["id"]); + $comments = $commentManager->getCommentsByArticle($id); // NEU: validierte ID statt rohem $_GET["id"] foreach ($comments as $comment) { if ($comment->isReply()) { diff --git a/php/controller/updateArticle-controller.php b/php/controller/updateArticle-controller.php index cb58617..bc24544 100644 --- a/php/controller/updateArticle-controller.php +++ b/php/controller/updateArticle-controller.php @@ -8,6 +8,7 @@ require_once '../model/ArticleManager.php'; require_once '../model/Article.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"])) { header("Location: index.php?pfad=login"); @@ -16,9 +17,17 @@ if (!isset($_SESSION["user"])) { if ($_SERVER["REQUEST_METHOD"] === "POST") { - if (isset($_GET["id"]) && !empty($_GET["id"])) { - $id = $_GET["id"]; - } else { + // CSRF-Token prüfen, bevor irgendeine Änderung vorgenommen wird + if (!csrf_verify()) { + $_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"; header("location: ../../index.php?pfad=updateArticle"); exit(); @@ -27,6 +36,14 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") { try { $articleManager = ArticleManager::getInstance(); $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"]) { $_SESSION["message"] = "unauthorized_access"; header("location: ../../index.php"); diff --git a/php/controller/updateComment-controller.php b/php/controller/updateComment-controller.php index e215911..2182f22 100644 --- a/php/controller/updateComment-controller.php +++ b/php/controller/updateComment-controller.php @@ -5,6 +5,7 @@ if (session_status() === PHP_SESSION_NONE) { } require_once "php/model/CommentManager.php"; +require_once "includes/csrf.php"; if ($_SERVER["REQUEST_METHOD"] !== "POST") { header("Location: index.php"); @@ -16,6 +17,14 @@ if (!isset($_SESSION["user_email"])) { 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", @@ -89,4 +98,4 @@ header( . urlencode((string) $articleId) . "#comments" ); -exit(); +exit(); \ No newline at end of file diff --git a/php/model/DatabaseArticleManager.php b/php/model/DatabaseArticleManager.php index c9485b5..8e3f57e 100644 --- a/php/model/DatabaseArticleManager.php +++ b/php/model/DatabaseArticleManager.php @@ -63,12 +63,9 @@ class DatabaseArticleManager implements ArticleManagerDAO { VALUES (:title, :content, :author, :category, :tags);"; $command = $db->prepare($sql); - if (!$command) { - throw new InternalServerErrorException("internal_error"); - } // Verknüpft die übergebenen Parameter exakt mit den SQL-Platzhaltern - $success = $command->execute([ + $command->execute([ ":title" => $title, ":content" => $content, ":author" => $author, @@ -76,14 +73,10 @@ class DatabaseArticleManager implements ArticleManagerDAO { ":tags" => $tags ]); - if (!$success) { - throw new InternalServerErrorException("internal_error"); - } - return intval($db->lastInsertId()); } catch (PDOException $e) { - throw new InternalServerErrorException($e->getMessage()); + throw new InternalServerErrorException("internal_error"); } } @@ -106,11 +99,8 @@ class DatabaseArticleManager implements ArticleManagerDAO { WHERE id = :id;"; $command = $db->prepare($sql); - if (!$command) { - throw new InternalServerErrorException("internal_error"); - } - $success = $command->execute([ + $command->execute([ ":id" => $id, ":title" => $article->getTitle(), ":content" => $article->getContent(), @@ -120,7 +110,7 @@ class DatabaseArticleManager implements ArticleManagerDAO { ]); // 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 if (!$this->getArticle($id)) { throw new NotFoundException("missing_id"); @@ -148,13 +138,7 @@ class DatabaseArticleManager implements ArticleManagerDAO { $sql = "DELETE FROM articles WHERE id = :id;"; $command = $db->prepare($sql); - if (!$command) { - throw new InternalServerErrorException("internal_error"); - } - - if (!$command->execute([":id" => $id])) { - throw new InternalServerErrorException("internal_error"); - } + $command->execute([":id" => $id]); } catch (PDOException $exc) { throw new InternalServerErrorException("internal_error"); } @@ -167,10 +151,6 @@ class DatabaseArticleManager implements ArticleManagerDAO { $sql = "SELECT * FROM articles WHERE id = :id;"; $command = $db->prepare($sql); - if (!$command) { - throw new InternalServerErrorException("internal_error"); - } - $command->execute([":id" => $id]); $row = $command->fetch(PDO::FETCH_ASSOC); @@ -202,10 +182,6 @@ class DatabaseArticleManager implements ArticleManagerDAO { $sql = "SELECT * FROM articles;"; $command = $db->query($sql); - if (!$command) { - throw new InternalServerErrorException("internal_error"); - } - $rows = $command->fetchAll(PDO::FETCH_ASSOC); $articles = []; @@ -234,10 +210,6 @@ class DatabaseArticleManager implements ArticleManagerDAO { $sql = "SELECT * FROM articles WHERE author = :author;"; $command = $db->prepare($sql); - if (!$command) { - throw new InternalServerErrorException("internal_error"); - } - $command->execute([":author" => $author]); $rows = $command->fetchAll(PDO::FETCH_ASSOC); $filteredArticles = []; @@ -270,10 +242,6 @@ class DatabaseArticleManager implements ArticleManagerDAO { $sql = "SELECT * FROM articles WHERE category = :category;"; $command = $db->prepare($sql); - if (!$command) { - throw new InternalServerErrorException("internal_error"); - } - $command->execute([":category" => $category]); $rows = $command->fetchAll(PDO::FETCH_ASSOC); $filteredArticles = []; @@ -310,7 +278,7 @@ class DatabaseArticleManager implements ArticleManagerDAO { try { $db = $this->getConnection(); - + $sql = "SELECT id, title, content, author, category, tags, created FROM articles WHERE title LIKE :keyword @@ -318,20 +286,14 @@ class DatabaseArticleManager implements ArticleManagerDAO { OR tags LIKE :keyword;"; $command = $db->prepare($sql); - if (!$command) { - throw new InternalServerErrorException("internal_error"); - } - // Wildcards für die SQL-Suche hinzufügen + // Wildcards für die Suche hinzufügen $searchParam = '%' . $cleankeyword . '%'; - $success = $command->execute([ + + $command->execute([ ":keyword" => $searchParam ]); - if (!$success) { - throw new InternalServerErrorException("internal_error"); - } - $rows = $command->fetchAll(PDO::FETCH_ASSOC); $filteredArticles = []; @@ -358,7 +320,6 @@ class DatabaseArticleManager implements ArticleManagerDAO { } } - /** * Holt alle User-IDs, die einen bestimmten Beitrag geliked haben. * diff --git a/php/validator/article-validator.php b/php/validator/article-validator.php index e07b810..7dcccca 100644 --- a/php/validator/article-validator.php +++ b/php/validator/article-validator.php @@ -1,5 +1,20 @@ ["min_range" => 1]]; + + return filter_var($id, FILTER_VALIDATE_INT, $options); +} + /** * Prüft, ob der Titel die folgenden Bedingungen erfüllt: * Buchstaben von a-z; A-Z