Compare commits

..

30 Commits

Author SHA1 Message Date
caroline.slt 6c1a893f80 Passwort vergessen Funktion 2026-06-26 00:05:45 +02:00
caroline.slt 788e803e84 Passwort vergessen Funktion 2026-06-25 23:45:17 +02:00
caroline.slt 4d0f1de01c Passwort vergessen Funktion 2026-06-25 23:40:33 +02:00
caroline.slt e71ee8300e Qualitätsanpassungen 2026-06-25 23:23:55 +02:00
caroline.slt f39885da10 CSS Anpassungen 2026-06-25 23:05:49 +02:00
caroline.slt b7ca5bcfe2 test rückgängig machen 2026-06-25 22:57:35 +02:00
caroline.slt 57b0782577 test 2026-06-25 22:53:45 +02:00
caroline.slt 4f524db221 Implementierung 2026-06-25 22:46:14 +02:00
NOrtmann1 d5a3f4298f Update README.md 2026-06-17 22:39:43 +02:00
NOrtmann1 b4a5c9c020 Update README.md 2026-06-17 22:38:23 +02:00
NOrtmann1 6d6f1f31b0 Update updateArticle-controller.php 2026-06-17 22:28:01 +02:00
NOrtmann1 cafffba921 Update updateArticle-controller.php 2026-06-17 22:21:00 +02:00
NOrtmann1 8fa758d825 Update dataSources.local.xml 2026-06-17 22:20:55 +02:00
niklas.ortmann 01fc8537cf Merge pull request 'Bewertungssystem' (#36) from Bewertungssystem into dev
Reviewed-on: #36
2026-06-17 21:29:24 +02:00
niklas.ortmann 4a6a3cf708 search-results -> Likes-Sortierung 2026-06-17 13:23:39 +02:00
niklas.ortmann cb44c3f5a8 Update search-results-controller.php 2026-06-17 13:11:03 +02:00
niklas.ortmann eb0a34e959 Create like-controller.php 2026-06-17 13:00:12 +02:00
niklas.ortmann 8d683cc1a0 showArticle -> like-Button 2026-06-17 12:53:33 +02:00
niklas.ortmann ee41dd4cfe Update LocalArticleManager.php 2026-06-17 12:35:17 +02:00
niklas.ortmann 3d50e6e3b3 ArticleManager um LIKES erweitert...
Einführung einer dynamischen Like-Funktion für Beiträge
2026-06-17 12:26:18 +02:00
niklas.ortmann 4f8d11881d Merge pull request 'home zeigt nun dynamisch die Kategorien an' (#35) from Startseite into dev
Reviewed-on: #35
2026-06-17 12:08:10 +02:00
niklas.ortmann 5c924d3277 Update profile.php 2026-06-17 12:06:49 +02:00
niklas.ortmann 7f703a8386 Update profile.php 2026-06-17 12:03:25 +02:00
niklas.ortmann f063ea4741 Update profile.php 2026-06-17 12:02:41 +02:00
niklas.ortmann ca643dd298 Update profile.php 2026-06-17 12:02:20 +02:00
niklas.ortmann e19225e49e Update profile.php 2026-06-17 12:01:38 +02:00
niklas.ortmann 9ffbca679e Update profile.php 2026-06-17 12:00:59 +02:00
niklas.ortmann d90a10e462 Update profile.php 2026-06-17 12:00:36 +02:00
niklas.ortmann b6f25d041b Anmerkungen von Maximilian zu Blatt 04
Ist der PHP-Code gut verständlich und übersichtlich strukturiert? Beinahe vollständig. ArticleManager::getInstance() erzeugt Dummy-Daten direkt beim Abruf der Instanz; etwas unübersichtlich, aber im Kontext der Dummy-Daten nicht zu gewichten (heißt es wird nicht negativ angerechnet).

Wie ist die Code-Qualität? Teilweise. articleAuthorValidator() ist mit TODO versehen und gibt immer true zurück. In home.php wird $dummy3->getId() aufgerufen, bevor geprüft wird, ob $dummy3 überhaupt existiert.

Wird redundanter Code vermieden? Teilweise. Die Kategorie-Listen stehen mehrfach separat in createArticle.php, updateArticle.php, navbar.php und article-validator.php. Fehlerausgaben für Artikel-Erstellung und Artikel-Bearbeitung sind ebenfalls fast identisch doppelt vorhanden.

Findet eine adäquate Trennung von PHP-Code und HTML-Code statt? Beinahe vollständig. In den Views werden teilweise noch Controller direkt eingebunden, z. B. profile.php bindet profile-controller.php ein und updateArticle.php nutzt showArticle-controller.php, um Daten zu laden (kann man theoretisch so machen, an dieser Stelle ist die Trennung dann aber nicht mehr wirklich sauber auf einem fundamentalen Level).

Ist eine Trennung in Model, View und Controller ersichtlich und auch korrekt durchgeführt? Teilweise. content-Dateien übernehmen teilweise Routing-/Controller-Aufgaben. Views binden Controller selbst ein.

Wurde das DAO-Pattern korrekt umgesetzt? Ok hier und da mit extends ein wenig fragwürdig aber kein Fehler.

Werden Sessions korrekt eingesetzt? Ok. session_start() wird mehrfach aufgerufen, z. B. in index.php, navbar.php, home-controller.php und search-results.php. Nach Login oder Registrierung wird keine neue Session-ID erzeugt (dies ist kein muss).

Findet in jedem Skript falls notwendig zunächst eine Authentifizierung angemeldeter Nutzer statt? Teilweise. createArticle-controller.php prüft bei direktem Aufruf nicht, ob ein Nutzer angemeldet ist. updateArticle-controller.php prüft ebenfalls nicht zu Beginn sauber die Authentifizierung. Die Authentifizierung liegt teilweise nur in der Formularseite, nicht im verarbeitenden Controller.

Ist bei „Eintrag bearbeiten/löschen“ sichergestellt, dass nur der Eigentümer den Eintrag verändern kann? Beinahe vollständig. Die eigentliche Aktualisierung prüft den Autor. Das Bearbeitungsformular updateArticle.php kann aber von jedem eingeloggten Nutzer mit fremder Artikel-ID geöffnet werden.

Werden in jedem Skript alle GET- bzw. POST-Parameter überprüft und ggf. auf Validität überprüft? Nein. pfad in index.php wird nicht per Whitelist validiert. id in showArticle-controller.php und updateArticle-controller.php wird nicht als gültige numerische ID geprüft. sort in search-results-controller.php wird nicht gegen erlaubte Werte validiert.

Werden bei Eingabefehlern durch den Nutzer die von ihm eingegebenen Daten bei der erneuten Formularanzeige übernommen? Teilweise. In createArticle.php wird old_category zwar gespeichert, aber nicht wieder im Dropdown ausgewählt (in updateArticle.php als TODO vermerkt). Beim Login wird die eingegebene E-Mail-Adresse nach einem Fehler nicht erneut angezeigt.

Wird eine adäquate Überprüfung von potentiellen Fehlern und Fehlerbehandlung durchgeführt? Teilweise. LocalArticleManager::saveArticle() prüft nicht, ob file_put_contents() erfolgreich war. getAllArticles() ignoriert fehlerhaftes JSON und gibt dann einfach ein leeres Array zurück. home-controller.php gibt im Fehlerfall direkt die Exception-Message aus.

Erfolgen bei Aktionen des Nutzers sowohl positive als auch negative Meldungen auf der nachfolgend angezeigten Seite? Teilweise. Nach erfolgreichem Login oder Registrierung gibt es keine sichtbare Erfolgsmeldung. Nach erfolgreicher Profiländerung gibt es ebenfalls keine positive Meldung. Beim Löschen des Accounts erfolgt nur eine Weiterleitung ohne Feedback.

Werden alle Eingabedaten beim Einbau in die HTML-Seite mit htmlspecialchars bzw. htmlentities HTML-kodiert? Beinahe vollständig. In home.php werden die Titel der Dummy-/Artikel-Links ohne htmlspecialchars() ausgegeben.

Werden alle Eingabedaten beim Einbau in eine URL mit urlencode URL-kodiert? Ok. Artikel-IDs werden mehrfach direkt in URLs eingesetzt, z. B. in profile.php, search-results.php und showCategory.php (Anmerkung: Rechne ich hier nicht negativ an). Auch die Formular-Action in updateArticle.php baut die ID ohne urlencode() in die URL ein (rechne ich ebenfalls nicht negativ an).
2026-06-17 11:58:49 +02:00
niklas.ortmann d1b3641754 Update dataSources.local.xml 2026-06-17 11:11:19 +02:00
33 changed files with 815 additions and 102 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>
+12 -7
View File
@@ -16,16 +16,21 @@
## Bekannte Fehler und Mängel ## Bekannte Fehler und Mängel
- Bitte auf die gesetzten TODO's achten. Wenn Inhalte fehlen, sind sie i.d.R. als TODO kommentiert. - Bitte auf die gesetzten TODO's achten. Wenn Inhalte fehlen, sind sie i.d.R. als TODO kommentiert.
- Die Suchseite und Kategorieseite packen momentan alle passenden Beiträge untereinander. Später sollen zunächst 10 - Die Kategorieseite listet momentan alle passenden Beiträge untereinander. Später sollen mit einem Paginator die neusten
Ergebnisse auf einer Seite angezeigt werden. Beiträge nacheinander aufgelistet werden (ähnlich wie bei der Suche, wenn nach Fach gefiltert wird).
- Wenn ein Bild aus einem Beitrag entfernt wird, dann wird noch nicht die Datei im Pfad /uploads gelöscht. - Wenn ein Bild aus einem Beitrag entfernt wird, dann wird noch nicht die Datei im Pfad /uploads gelöscht.
- id in showArticle-controller.php und updateArticle-controller.php wird nicht als gültige numerische ID geprüft.
- Bilder im Beitragseditor sollen zukünftig eine Bildunterschrift bekommen und größenverstellbar sein.
- Die Elemente eines Contents im Beitrag werden momentan stumpf untereinander aufgelistet. Soll später
sich responisve auch nebeneinander orientieren usw.
## Besonderheiten des Projektes ## Besonderheiten des Projektes
- Es wurde ein einfacher Beitrags-Editor erstellt. Mit diesem können Beiträge erstellt oder bearbeitet werden. - Es wurde AJAX verwendet, um asynchrone Erstellung von Kommentaren zu implementieren. Es ermöglicht dem Nutzer, einen
Es handelt es sich um eine einfache Version. Später sollen z.B. Bilder und die Positionierung der Elemente folgen. Kommentar abzusenden, ohne dass die gesamte Webseite neu geladen werden muss.
- Es sind drei Dummy-Beiträge r den Nutzer max.mustermann hinterlegt. - Mit JavaScript werden auch clientseitig die Kommentare visuell hinzugefügt und die Kommentarbäume aufgebaut.
- Die Such-Seite umfasst eine Such- und Sortierfunktion. Jedoch fehlt noch eine - JavaScript wird verwendet, um im erweitertem Beitragseditor clientseitig einzelne Content-Boxen erstellen und löschen
Filterfunktion (z.B. nur Mathe anzeigen). zu können.
- JavaScript wird ebenfalls verwendet, um in die Suchergebnisse clientseitig zu sortieren.
## Sonstiges ## Sonstiges
- Das Datenschema befindet sich unter /planung/Datenschema.pdf - Das Datenschema befindet sich unter /planung/Datenschema.pdf
+65
View File
@@ -0,0 +1,65 @@
<?php
require_once "php/model/UserManager.php";
$token = basename($_GET["token"] ?? "");
$file = "data/pending-password/" . $token . ".json";
$title = "Passwort zurücksetzen";
$message = "";
$link = "";
$isSuccess = false;
if (!file_exists($file)) {
$message = "Der Bestätigungslink ist ungültig oder bereits abgelaufen.";
} else {
$data = json_decode(file_get_contents($file), true);
if ($data === null || empty($data["email"]) || empty($data["password"])) {
$message = "Die Daten zur Passwortänderung konnten nicht gelesen werden.";
} else {
try {
$dao = UserManager::getInstance();
$user = $dao->findUser($data["email"]);
if ($user !== null) {
$dao->updateUser(
$user["email"],
$user["email"],
$user["vorname"],
$user["nachname"],
$data["password"]
);
unlink($file);
$title = "Passwort geändert";
$message = "Ihr Passwort wurde erfolgreich geändert. Sie können sich jetzt anmelden.";
$link = '<a class="button confirm-button" href="index.php?pfad=login">Zum Login</a>';
$isSuccess = true;
} else {
unlink($file);
$message = "Der Benutzer konnte nicht gefunden werden.";
}
} catch (Exception $e) {
$message = "Das Passwort konnte nicht geändert werden.";
}
}
}
?>
<main class="login-page">
<div class="login-container">
<h1><?php echo htmlspecialchars($title); ?></h1>
<p class="alert-message <?php echo $isSuccess ? 'is-success' : 'is-error'; ?> confirm-message">
<?php echo htmlspecialchars($message); ?>
</p>
<?php echo $link; ?>
</div>
</main>
+68
View File
@@ -0,0 +1,68 @@
<?php
require_once "php/model/UserManager.php";
$token = basename($_GET["token"] ?? "");
$file = "data/pending/" . $token . ".json";
$title = "Registrierung";
$message = "";
$link = "";
$isSuccess = false;
if (!file_exists($file)) {
$message = "Der Registrierungslink ist ungültig oder bereits abgelaufen.";
} else {
$data = json_decode(file_get_contents($file), true);
if ($data === null) {
$message = "Die Registrierungsdaten konnten nicht gelesen werden.";
} elseif (
empty($data["email"]) ||
empty($data["vorname"]) ||
empty($data["nachname"]) ||
empty($data["password"])
) {
$message = "Die Registrierungsdaten sind unvollständig.";
} else {
try {
$dao = UserManager::getInstance();
if ($dao->findUser($data["email"]) === null) {
$dao->addUser(
$data["email"],
$data["vorname"],
$data["nachname"],
$data["password"]
);
}
unlink($file);
$title = "Registrierung erfolgreich";
$message = "Ihre Registrierung wurde erfolgreich abgeschlossen. Sie können sich jetzt anmelden.";
$link = '<a class="button confirm-button" href="index.php?pfad=login">Zum Login</a>';
$isSuccess = true;
} catch (Exception $e) {
$message = "Die Registrierung konnte nicht abgeschlossen werden.";
}
}
}
?>
<main class="login-page">
<div class="login-container">
<h1><?php echo htmlspecialchars($title); ?></h1>
<p class="alert-message <?php echo $isSuccess ? 'is-success' : 'is-error'; ?> confirm-message">
<?php echo htmlspecialchars($message); ?>
</p>
<?php echo $link; ?>
</div>
</main>
+8 -2
View File
@@ -11,8 +11,8 @@ $error = $error ?? null;
<h1>Bitte anmelden</h1> <h1>Bitte anmelden</h1>
<?php if ($error): ?> <?php if (!empty($error)): ?>
<p style="color:red;"> <p class="alert-message is-error">
<?php echo htmlspecialchars($error); ?> <?php echo htmlspecialchars($error); ?>
</p> </p>
<?php endif; ?> <?php endif; ?>
@@ -41,6 +41,12 @@ $error = $error ?? null;
anmelden anmelden
</button> </button>
<div class="register-link">
<a href="index.php?pfad=password-forgotten">
Passwort vergessen?
</a>
</div>
<div class="register-link"> <div class="register-link">
<a href="index.php?pfad=register"> <a href="index.php?pfad=register">
Noch keinen Account? Jetzt hier registrieren! Noch keinen Account? Jetzt hier registrieren!
+54
View File
@@ -0,0 +1,54 @@
<?php
$error = $error ?? null;
$success = $success ?? null;
?>
<main class="login-page">
<div class="login-container">
<h1>Passwort vergessen</h1>
<?php if (!empty($error)): ?>
<p class="alert-message is-error">
<?php echo htmlspecialchars($error); ?>
</p>
<?php endif; ?>
<?php if (!empty($success)): ?>
<p class="alert-message is-success">
<?php echo $success; ?>
</p>
<?php endif; ?>
<form method="post" action="index.php?pfad=password-forgotten">
<p class="input-label">E-Mail-Adresse:</p>
<input type="email"
name="email"
class="login-input"
placeholder="E-Mail-Adresse"
required>
<p class="input-label">Neues Passwort:</p>
<input type="password"
name="password"
class="login-input"
placeholder="Neues Passwort"
required>
<button type="submit"
name="passwordForgottenSubmit"
class="button">
Passwort zurücksetzen
</button>
<div class="register-link">
<a href="index.php?pfad=login">
Zurück zum Login
</a>
</div>
</form>
</div>
</main>
+5 -10
View File
@@ -7,8 +7,8 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
<main class="form-page"> <main class="form-page">
<div class="flexbox"> <div class="flexbox">
<div class="container"> <div class="container">
<?php include_once "includes/alertMessages.php" ?>
<?php if (!empty($error)): ?> <?php if (!empty($error)): ?>
<p class="alert-message is-error"> <p class="alert-message is-error">
@@ -93,8 +93,6 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
<div class="container"> <div class="container">
<?php include_once "includes/alertMessages.php" ?>
<h2 class="section-title">Meine Beiträge</h2> <h2 class="section-title">Meine Beiträge</h2>
<div class="articles-list"> <div class="articles-list">
@@ -182,10 +180,9 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
<?php endif; ?> <?php endif; ?>
</div> </div>
</div>
<br> <div class="container">
<!-- Eigener Bereich für die Kommentare des Nutzers -->
<div class="comments-section"> <div class="comments-section">
<h2 class="section-title">Meine Kommentare</h2> <h2 class="section-title">Meine Kommentare</h2>
@@ -228,10 +225,8 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
</div> </div>
</div> </div>
<?php unset($_SESSION["message"]); ?>
</div>
</div> </div>
<?php unset($_SESSION["message"]); ?>
</div>
</main> </main>
+8 -1
View File
@@ -1,5 +1,6 @@
<?php <?php
$error = $error ?? null; $error = $error ?? null;
$success = $success ?? null;
?> ?>
<!-- <!--
@@ -12,11 +13,17 @@ $error = $error ?? null;
<h1>Jetzt Registrieren!</h1> <h1>Jetzt Registrieren!</h1>
<?php if (!empty($error)): ?> <?php if (!empty($error)): ?>
<p class="alert-message is-error" style="color:red;"> <p class="alert-message is-error">
<?php echo htmlspecialchars($error); ?> <?php echo htmlspecialchars($error); ?>
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php if (!empty($success)): ?>
<p class="alert-message is-success">
<?php echo $success; ?>
</p>
<?php endif; ?>
<form method="post" action="index.php?pfad=register"> <form method="post" action="index.php?pfad=register">
<p class="input-label">Email:</p> <p class="input-label">Email:</p>
+10 -3
View File
@@ -65,8 +65,8 @@ $resultCount = count($results);
</label> </label>
<!-- Noch disabled, da likes noch nicht implementiert--> <!-- Noch disabled, da likes noch nicht implementiert-->
<label class="s-res-filter-option"> <label class="s-res-filter-option">
<input type="radio" name="sort" value="likes" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?> disabled> <input type="radio" name="sort" value="likes" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?> onchange="this.form.submit()">
<span style="color: #94a3b8;">Beliebtheit (Likes)</span> <span>Beliebtheit (Likes)</span>
</label> </label>
<label class="s-res-filter-option"> <label class="s-res-filter-option">
<input type="radio" name="sort" value="newest" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?> onchange="this.form.submit()"> <input type="radio" name="sort" value="newest" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?> onchange="this.form.submit()">
@@ -103,7 +103,14 @@ $resultCount = count($results);
<?php echo htmlspecialchars($item['title']); ?> <?php echo htmlspecialchars($item['title']); ?>
</a> </a>
</h2> </h2>
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p> <div class="s-res-meta-row">
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p>
<span class="s-res-likes">
❤️ <?php echo isset($item['likes']) && is_array($item['likes']) ? count($item['likes']) : 0; ?>
</span>
</div>
</div> </div>
<div class="s-res-arrow">&rarr;</div> <div class="s-res-arrow">&rarr;</div>
</div> </div>
+24
View File
@@ -0,0 +1,24 @@
<?php
/*
* Zeigt den Inhalt einer simulierten E-Mail an.
* Die Datei wird über einen zufällig erzeugten Token geladen.
*/
$token = basename($_GET["token"] ?? "");
$file = __DIR__ . "/../data/mails/" . $token . ".html";
if (!file_exists($file)) {
echo "<p>Datei nicht gefunden.</p>";
exit();
}
?>
<main class="login-page">
<div class="login-container">
<h1>Simulierte E-Mail</h1>
<?php include $file; ?>
</div>
</main>
+24 -23
View File
@@ -1,28 +1,10 @@
<?php <?php
include_once 'php/controller/showArticle-controller.php';
require_once 'php/model/CommentManager.php';
$comments = []; $comments = [];
$mainComments = []; $mainComments = [];
$repliesByParent = []; $repliesByParent = [];
$articleObj = null;
if (isset($_GET["id"])) { include_once 'php/controller/showArticle-controller.php';
try {
$commentManager = CommentManager::getInstance();
$comments = $commentManager->getCommentsByArticle($_GET["id"]);
foreach ($comments as $comment) {
if ($comment->isReply()) {
$parentId = $comment->getParentCommentId();
$repliesByParent[$parentId][] = $comment;
} else {
$mainComments[] = $comment;
}
}
} catch (Exception $e) {
$_SESSION["message"] = "internal_error";
}
}
?> ?>
<!-- <!--
Seite: Anzeige für Beiträge Seite: Anzeige für Beiträge
@@ -35,9 +17,28 @@ if (isset($_GET["id"])) {
<!-- Metadaten & Titel --> <!-- Metadaten & Titel -->
<div class="article-view-top-section"> <div class="article-view-top-section">
<?php if (isset($category) && !empty($category)): ?> <div class="article-view-top-section">
<span class="article-view-category"><?php echo htmlspecialchars($category); ?></span>
<?php endif; ?> <div class="category-and-likes-row">
<?php if (isset($category) && !empty($category)): ?>
<span class="article-view-category"><?php echo htmlspecialchars($category); ?></span>
<?php endif; ?>
<!-- Like-Anzeige und dynamischer Like-Button -->
<?php if (isset($articleObj) && $articleObj !== null): ?>
<div class="article-view-likes">
<span>❤️ <span class="like-count"><?php echo $articleObj->getLikeCount(); ?></span></span>
<?php if (isset($_SESSION["user_email"])): ?>
<a href="php/controller/like-controller.php?id=<?php echo $articleObj->getId(); ?>" class="like-toggle-btn">
<?php echo $articleObj->hasLiked($_SESSION["user_email"]) ? '👎 Gefällt mir nicht mehr' : '👍 Gefällt mir'; ?>
</a>
<?php else: ?>
<span class="login-hint">(Anmelden zum Liken)</span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<h1 class="article-view-title"> <h1 class="article-view-title">
<?php if (isset($title)) { echo htmlspecialchars($title); } ?> <?php if (isset($title)) { echo htmlspecialchars($title); } ?>
+15 -15
View File
@@ -130,13 +130,6 @@ h1 {
color: #1f2937; color: #1f2937;
} }
.input-label {
margin-bottom: 5px;
font-weight: bold;
width: 100%;
color: #1f2937;
}
.login-input { .login-input {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
@@ -210,14 +203,6 @@ h1 {
.form-container { .form-container {
width: 90%; width: 90%;
max-width: 600px; max-width: 600px;
padding: 30px;
background-color: white;
border: 1px solid #dbe3ec;
border-radius: 10px;
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
}
.form-container {
flex: 1 1 450px; flex: 1 1 450px;
padding: 30px; padding: 30px;
background-color: white; background-color: white;
@@ -225,4 +210,19 @@ h1 {
border-radius: 10px; border-radius: 10px;
box-shadow: 0 6px 20px rgba(0,0,0,0.1); box-shadow: 0 6px 20px rgba(0,0,0,0.1);
box-sizing: border-box; box-sizing: border-box;
}
/* Darstellung der Registrierungsbestätigung */
.confirm-message {
text-align: center;
margin: 20px 0;
}
/* Anpassung des Login-Buttons auf der Bestätigungsseite */
.confirm-button {
display: block;
width: 100%;
text-align: center;
text-decoration: none;
box-sizing: border-box;
} }
+11
View File
@@ -247,6 +247,17 @@ CSS für die Suchergebnis-Seite
cursor: not-allowed; cursor: not-allowed;
} }
.s-res-meta-row {
display: flex;
gap: 15px;
align-items: center;
}
.s-res-likes {
font-size: 0.9em;
color: #475569;
}
/* Responsive Anpassungen unter 760px (für z.B. Smartphones) */ /* Responsive Anpassungen unter 760px (für z.B. Smartphones) */
@media (max-width: 768px) { @media (max-width: 768px) {
.s-res-layout-grid { .s-res-layout-grid {
+44
View File
@@ -239,4 +239,48 @@
.comment-login-hint p { .comment-login-hint p {
margin-bottom: 1rem; margin-bottom: 1rem;
}
/*
Like-Button etc.
*/
.category-and-likes-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.article-view-likes {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.95em;
}
.article-view-likes .like-count {
font-weight: bold;
}
.article-view-likes .login-hint {
font-size: 0.8em;
color: #777;
}
/* Interaktiver Like/Unlike-Button */
.like-toggle-btn {
text-decoration: none;
padding: 4px 10px;
border: 1px solid #bbb;
border-radius: 4px;
background-color: #f5f5f5;
color: #333;
font-weight: 500;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.like-toggle-btn:hover {
background-color: #eaeaea;
border-color: #999;
} }
+1
View File
@@ -0,0 +1 @@
<?php
+1
View File
@@ -0,0 +1 @@
<?php
+1
View File
@@ -0,0 +1 @@
<?php
+10
View File
@@ -63,6 +63,16 @@
Dein Beitrag wurde erfolgreich veröffentlicht! Dein Beitrag wurde erfolgreich veröffentlicht!
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "article_updated"): ?>
<p class="alert-message is-success">
Der Beitrag wurde erfolgreich bearbeitet und gespeichert.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "profile_updated"): ?>
<p class="alert-message is-success">
Das Profil wurde erfolgreich bearbeitet.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "image_upload_error"): ?> <?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "image_upload_error"): ?>
<p class="alert-message is-error"> <p class="alert-message is-error">
Das Bild konnte nicht hochgeladen werden. Bitte versuche es erneut oder verwende ein anderes Bildformat. Das Bild konnte nicht hochgeladen werden. Bitte versuche es erneut oder verwende ein anderes Bildformat.
+3
View File
@@ -17,6 +17,9 @@ if ($pfad === "login") {
if ($pfad === "register") { if ($pfad === "register") {
include_once "php/controller/register-controller.php"; include_once "php/controller/register-controller.php";
} }
if ($pfad === "password-forgotten") {
include_once "php/controller/password-forgotten-controller.php";
}
if ($pfad === "logout") { if ($pfad === "logout") {
include_once "php/controller/logout-controller.php"; include_once "php/controller/logout-controller.php";
@@ -6,6 +6,10 @@ 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';
if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login");
exit();
}
if ($_SERVER["REQUEST_METHOD"] === "POST") { if ($_SERVER["REQUEST_METHOD"] === "POST") {
$_SESSION["old_title"] = $_POST["title"] ?? ''; $_SESSION["old_title"] = $_POST["title"] ?? '';
$_SESSION["old_content"] = $_POST["content"] ?? ''; $_SESSION["old_content"] = $_POST["content"] ?? '';
@@ -6,6 +6,11 @@ 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";
if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login");
exit();
}
/* /*
Deregistrierung Deregistrierung
Funktion: Entfernt User aus der Datenbank und beendet die Session Funktion: Entfernt User aus der Datenbank und beendet die Session
@@ -5,6 +5,11 @@ if (session_status() === PHP_SESSION_NONE) {
require_once __DIR__ . "/../model/ArticleManager.php"; require_once __DIR__ . "/../model/ArticleManager.php";
if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login");
exit();
}
if ($_SERVER["REQUEST_METHOD"] === "POST") { if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (isset($_SESSION["user_email"])) { if (isset($_SESSION["user_email"])) {
+42
View File
@@ -0,0 +1,42 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../model/Article.php';
require_once __DIR__ . '/../model/ArticleManager.php';
// 2. Prüfen, ob eine gültige Artikel-ID übergeben wurde
if (isset($_GET["id"]) && !empty($_GET["id"])) {
$articleId = intval($_GET["id"]);
$userEmail = $_SESSION["user_email"];
if (!isset($_SESSION["user_email"]) || empty($_SESSION["user_email"])) {
$_SESSION["message"] = "unauthorized_access";
header("Location: ../../index.php?pfad=showArticle&id=" . $articleId);
exit();
}
try {
$articleManager = ArticleManager::getInstance();
$articleManager->toggleLike($articleId, $userEmail);
header("Location: ../../index.php?pfad=showArticle&id=" . $articleId);
exit();
} catch (NotFoundException $e) {
$_SESSION["message"] = "missing_id";
header("Location: ../../index.php");
exit();
} catch (Exception $e) {
$_SESSION["message"] = "internal_error";
header("Location: ../../index.php");
exit();
}
} else {
$_SESSION["message"] = "missing_id";
header("Location: ../../index.php");
exit();
}
?>
@@ -0,0 +1,76 @@
<?php
require_once "php/model/UserManager.php";
require_once "php/validator/user-validator.php";
$error = null;
$success = null;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$email = trim($_POST["email"] ?? "");
$plainPassword = $_POST["password"] ?? "";
if (!userEmailValidator($email)) {
$error = "Bitte gib eine gültige E-Mail-Adresse ein.";
} elseif (!userPasswordValidator($plainPassword)) {
$error = "Das Passwort muss 5 bis 12 Zeichen lang sein.";
} else {
try {
$dao = UserManager::getInstance();
$token = bin2hex(random_bytes(16));
$existingUser = $dao->findUser($email);
if (!is_dir("data/mails") && !mkdir("data/mails", 0777, true)) {
throw new RuntimeException("Ordner data/mails konnte nicht erstellt werden.");
}
if (!is_dir("data/pending-password") && !mkdir("data/pending-password", 0777, true)) {
throw new RuntimeException("Ordner data/pending-password konnte nicht erstellt werden.");
}
if (!is_writable("data/mails") || !is_writable("data/pending-password")) {
throw new RuntimeException("Ordner sind nicht beschreibbar.");
}
if ($existingUser !== null) {
$pendingData = [
"email" => $email,
"password" => $plainPassword
];
file_put_contents(
"data/pending-password/" . $token . ".json",
json_encode($pendingData, JSON_PRETTY_PRINT)
);
}
if ($existingUser !== null) {
$mailContent = "
<h2>Passwort zurücksetzen</h2>
<p>Falls Sie diese Anfrage nicht gestellt haben, können Sie diese Nachricht ignorieren.</p>
<p>
<a href='index.php?pfad=confirm-password&token=$token'>
Passwortänderung bestätigen
</a>
</p>
";
} else {
$mailContent = "
<h2>Passwort zurücksetzen</h2>
<p>Falls Sie diese Anfrage nicht gestellt haben, können Sie diese Nachricht ignorieren.</p>
<p>Für diese E-Mail-Adresse wurde kein Konto gefunden.</p>
";
}
file_put_contents("data/mails/" . $token . ".html", $mailContent);
$success = 'Weitere Infos finden Sie in der Datei
<a href="index.php?pfad=show-mail&token=' . htmlspecialchars($token) . '" target="_blank">xy</a>.';
} catch (Exception $e) {
$error = "Die Passwortänderung konnte nicht verarbeitet werden.";
}
}
}
+1
View File
@@ -63,6 +63,7 @@ try {
$_SESSION["user"] = $vorname . " " . $nachname; $_SESSION["user"] = $vorname . " " . $nachname;
$_SESSION["user_email"] = $newEmail; $_SESSION["user_email"] = $newEmail;
$_SESSION["message"] = "profile_updated";
header("Location: index.php?pfad=profile"); header("Location: index.php?pfad=profile");
exit(); exit();
} else { } else {
+61 -9
View File
@@ -4,6 +4,7 @@ require_once "php/model/UserManager.php";
require_once "php/validator/user-validator.php"; require_once "php/validator/user-validator.php";
$error = null; $error = null;
$success = null;
if ($_SERVER["REQUEST_METHOD"] === "POST") { if ($_SERVER["REQUEST_METHOD"] === "POST") {
@@ -24,20 +25,71 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
try { try {
$dao = UserManager::getInstance(); $dao = UserManager::getInstance();
$password = password_hash($plainPassword, PASSWORD_DEFAULT); // Token für die simulierte E-Mail und die spätere Bestätigung erzeugen.
$token = bin2hex(random_bytes(16));
$existingUser = $dao->findUser($email);
$dao->addUser($email, $vorname, $nachname, $password); if (!is_dir("data/mails") && !mkdir("data/mails", 0777, true)) {
throw new RuntimeException("Ordner data/mails konnte nicht erstellt werden.");
}
$_SESSION["user"] = $vorname . " " . $nachname; if (!is_dir("data/pending") && !mkdir("data/pending", 0777, true)) {
$_SESSION["user_email"] = $email; throw new RuntimeException("Ordner data/pending konnte nicht erstellt werden.");
}
header("Location: index.php"); if (!is_writable("data/mails")) {
exit(); throw new RuntimeException("Ordner data/mails ist nicht beschreibbar.");
}
if (!is_writable("data/pending")) {
throw new RuntimeException("Ordner data/pending ist nicht beschreibbar.");
}
if ($existingUser === null) {
$password = password_hash($plainPassword, PASSWORD_DEFAULT);
$pendingData = [
"email" => $email,
"vorname" => $vorname,
"nachname" => $nachname,
"password" => $password
];
file_put_contents(
"data/pending/" . $token . ".json",
json_encode($pendingData, JSON_PRETTY_PRINT)
);
$mailContent = "
<h2>Registrierung bestätigen</h2>
<p>Bitte ignorieren Sie diese Nachricht, wenn Sie sich nicht registrieren wollten.</p>
<p>
<a href='index.php?pfad=confirm-register&token=$token'>
Registrierung bestätigen
</a>
</p>
";
} else {
$mailContent = "
<h2>Registrierung</h2>
<p>Bitte ignorieren Sie diese Nachricht, wenn Sie sich nicht registrieren wollten.</p>
<p>Sie sind bereits registriert.</p>
<p>
<a href='index.php?pfad=password-forgotten'>
Passwort vergessen
</a>
</p>
";
}
file_put_contents("data/mails/" . $token . ".html", $mailContent);
// Neutrale Meldung, damit nicht sichtbar wird, ob die E-Mail bereits registriert ist.
$success = 'Weitere Infos finden Sie in der Datei
<a href="index.php?pfad=show-mail&token=' . htmlspecialchars($token) . '" target="_blank">xy</a>.';
} catch (InvalidArgumentException $e) {
$error = $e->getMessage();
} catch (Exception $e) { } catch (Exception $e) {
$error = "Die Registrierung konnte nicht gespeichert werden."; $error = "Die Registrierung konnte nicht verarbeitet werden.";
} }
} }
} }
+15 -10
View File
@@ -25,18 +25,22 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
if ($sortStyle === 'alphabet') { if ($sortStyle === 'alphabet') {
// Titel aufsteigend alphabetiisch sortiert // Titel aufsteigend alphabetiisch sortiert
usort($results, function ($a, $b) {
return strcasecmp($a->getTitle(), $b->getTitle());
});
} elseif ($sortStyle === 'likes') {
usort($results, function($a, $b) { usort($results, function($a, $b) {
return strcasecmp($a->title, $b->title); return $b->getLikeCount() <=> $a->getLikeCount();
}); });
} elseif ($sortStyle === 'newest') { } elseif ($sortStyle === 'newest') {
// Datum neu zu alt sortiert // Datum neu zu alt sortiert
usort($results, function($a, $b) { usort($results, function($a, $b) {
return strcmp($b->creationDate, $a->creationDate); return strcmp($b->getCreationDate(), $a->getCreationDate());
}); });
} elseif ($sortStyle === 'oldest') { } elseif ($sortStyle === 'oldest') {
// Datum alt zu neu sortiert // Datum alt zu neu sortiert
usort($results, function($a, $b) { usort($results, function($a, $b) {
return strcmp($a->creationDate, $b->creationDate); return strcmp($a->getCreationDate(), $b->getCreationDate());
}); });
} }
@@ -44,13 +48,14 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
$safeArrayResults = []; $safeArrayResults = [];
foreach ($results as $obj) { foreach ($results as $obj) {
$safeArrayResults[] = [ $safeArrayResults[] = [
"id" => $obj->id, "id" => $obj->getId(),
"title" => $obj->title, "title" => $obj->getTitle(),
"content" => $obj->content, "content" => $obj->getContent(),
"author" => $obj->author, "author" => $obj->getAuthor(),
"category" => $obj->category, "category" => $obj->getCategory(),
"tags" => $obj->tags, "tags" => $obj->getTags(),
"creationDate" => $obj->creationDate "creationDate" => $obj->getCreationDate(),
"likes" => $obj->getLikes(),
]; ];
} }
+15
View File
@@ -5,6 +5,7 @@ if (session_status() === PHP_SESSION_NONE) {
require_once 'php/model/Article.php'; require_once 'php/model/Article.php';
require_once 'php/model/ArticleManager.php'; require_once 'php/model/ArticleManager.php';
require_once 'php/model/CommentManager.php';
if (isset($_GET["id"]) && !empty($_GET["id"])){ if (isset($_GET["id"]) && !empty($_GET["id"])){
try { try {
@@ -17,11 +18,25 @@ if (isset($_GET["id"]) && !empty($_GET["id"])){
$category = $article->getCategory(); $category = $article->getCategory();
$author = $article->getAuthor(); $author = $article->getAuthor();
$tags = $article->getTags(); $tags = $article->getTags();
$articleObj = $article; // Objekt für die Like-Abfagen sichern
}else{ }else{
//header("location: index.php?pfad=404"); //header("location: index.php?pfad=404");
include_once "content/404.php"; include_once "content/404.php";
exit(); exit();
} }
$commentManager = CommentManager::getInstance();
$comments = $commentManager->getCommentsByArticle($_GET["id"]);
foreach ($comments as $comment) {
if ($comment->isReply()) {
$parentId = $comment->getParentCommentId();
$repliesByParent[$parentId][] = $comment;
} else {
$mainComments[] = $comment;
}
}
} catch (Exception $e){ } catch (Exception $e){
$_SESSION["message"] = "internal_error"; $_SESSION["message"] = "internal_error";
exit(); exit();
+20 -1
View File
@@ -8,10 +8,15 @@ 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';
if (!isset($_SESSION["user_email"])) {
header("Location: index.php?pfad=login");
exit();
}
if ($_SERVER["REQUEST_METHOD"] === "POST") { if ($_SERVER["REQUEST_METHOD"] === "POST") {
$_SESSION["old_title"] = $_POST["title"] ?? ''; $_SESSION["old_title"] = $_POST["title"] ?? '';
$_SESSION["old_content"] = $_POST["content"] ?? ''; $_SESSION["old_content"] = $_POST["content"] ?? '';
$_SESSION["old_category"] = $_POST["category"] ?? ''; // TODO: die Kategorie im Dropdown setzen, wenn der Editor erneut geöffnet wird. $_SESSION["old_category"] = $_POST["category"] ?? '';
$_SESSION["old_tags"] = $_POST["tags"] ?? ''; $_SESSION["old_tags"] = $_POST["tags"] ?? '';
if (isset($_GET["id"]) && !empty($_GET["id"])) { if (isset($_GET["id"]) && !empty($_GET["id"])) {
@@ -22,6 +27,20 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
exit(); exit();
} }
try {
$articleManager = ArticleManager::getInstance();
$article = $articleManager->getArticle($id);
if ($article->getAuthor() != $_SESSION["user_email"]) {
$_SESSION["message"] = "unauthorized_access";
header("location: ../../index.php");
exit();
}
} catch (Exception $e) {
$_SESSION["message"] = $e->getMessage();
header("location: ../../index.php?pfad=updateArticle&id=$id");
exit();
}
if (!isset($_POST["title"]) ||!isset($_POST["content"]) || !isset($_POST["category"])){ if (!isset($_POST["title"]) ||!isset($_POST["content"]) || !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");
+39 -10
View File
@@ -7,13 +7,14 @@
*/ */
class Article class Article
{ {
public $id; private $id;
public $title; private $title;
public $content; private $content;
public $author; private $author;
public $creationDate; private $creationDate;
public $category; private $category;
public $tags; private $tags;
private $likes;
/** /**
* Konstruktor * Konstruktor
@@ -26,7 +27,7 @@ class Article
* @param $tags string optionale Schlagworte für eine bessere Suche * @param $tags string optionale Schlagworte für eine bessere Suche
* @param $creationDate string Datum der Beitragserstellung * @param $creationDate string Datum der Beitragserstellung
*/ */
public function __construct(int $id, string $title, string $content, string $author, string $category, string $tags, string $creationDate) public function __construct(int $id, string $title, string $content, string $author, string $category, string $tags, string $creationDate, array $likes = [])
{ {
$this->id = $id; $this->id = $id;
$this->title = $title; $this->title = $title;
@@ -35,6 +36,7 @@ class Article
$this->creationDate = $creationDate; $this->creationDate = $creationDate;
$this->category = $category; $this->category = $category;
$this->tags = $tags; $this->tags = $tags;
$this->likes = $likes;
} }
/** /**
@@ -67,7 +69,7 @@ class Article
/** /**
* Gibt den Content eines Beitrags zurück. * Gibt den Content eines Beitrags zurück.
* TODO: Content muss noch definiert werden. *
* @return string * @return string
*/ */
public function getContent(): string public function getContent(): string
@@ -77,7 +79,7 @@ class Article
/** /**
* Setzt den Content eines Beitrags. * Setzt den Content eines Beitrags.
* TODO: Content muss noch definiert werden. *
* @param $content * @param $content
* @return void * @return void
*/ */
@@ -141,7 +143,34 @@ class Article
$this->tags = $tags; $this->tags = $tags;
} }
/**
* Gibt alle User-IDs zurück, die diesen Beitrag geliked haben.
* @return array
*/
public function getLikes(): array
{
return $this->likes;
}
/**
* Gibt die Gesamtzahl der Likes zurück.
* @return int
*/
public function getLikeCount(): int
{
return count($this->likes);
}
/**
* Prüft, ob ein bestimmter Nutzer den Beitrag bereits geliked hat.
*
* @param string $userId
* @return bool
*/
public function hasLiked(string $userId): bool
{
return in_array($userId, $this->likes);
}
} }
?> ?>
+12
View File
@@ -92,6 +92,18 @@ interface ArticleManagerDAO
*/ */
public function getArticlesByCategory($category); public function getArticlesByCategory($category);
/**
* Registriert oder entfernt ein Like eines Nutzers für einen Beitrag.
* Wenn schon geliked -> Unlike
* Wenn noch nicht geliked -> Like
*
* @param int $articleId Die ID des Beitrags
* @param string $userId Die ID des Nutzers
* @return bool True wenn geliked, False wenn unliked
* @throws InternalServerErrorException
* @throws NotFoundException
*/
public function toggleLike(int $articleId, string $userId): bool;
} }
?> ?>
+91 -4
View File
@@ -23,6 +23,7 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$db = $this->getConnection(); $db = $this->getConnection();
// Tabelle für Beiträge
$db->exec(" $db->exec("
CREATE TABLE articles ( CREATE TABLE articles (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -33,6 +34,15 @@ class DatabaseArticleManager implements ArticleManagerDAO {
tags TEXT, tags TEXT,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);"); );");
// Tabelle für Likes
$db->exec("
CREATE TABLE likes (
article_id INTEGER,
user_id TEXT,
PRIMARY KEY (article_id, user_id),
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE
);");
unset($db); unset($db);
} catch (PDOException $e) { } catch (PDOException $e) {
throw new InternalServerErrorException($e->getMessage()); throw new InternalServerErrorException($e->getMessage());
@@ -189,6 +199,8 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$row = $command->fetch(PDO::FETCH_ASSOC); $row = $command->fetch(PDO::FETCH_ASSOC);
if ($row) { if ($row) {
$likes = $this->getLikesForArticle(intval($row['id']));
return new Article( return new Article(
intval($row['id']), intval($row['id']),
$row['title'], $row['title'],
@@ -196,7 +208,8 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$row['author'], $row['author'],
$row['category'], $row['category'],
$row['tags'], $row['tags'],
$row['created'] $row['created'],
$likes
); );
} }
@@ -254,6 +267,8 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$filteredArticles = []; $filteredArticles = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$likes = $this->getLikesForArticle(intval($row['id']));
$filteredArticles[] = new Article( $filteredArticles[] = new Article(
intval($row['id']), intval($row['id']),
$row['title'], $row['title'],
@@ -261,7 +276,8 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$row['author'], $row['author'],
$row['category'], $row['category'],
$row['tags'], $row['tags'],
$row['created'] $row['created'],
$likes
); );
} }
@@ -287,6 +303,8 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$filteredArticles = []; $filteredArticles = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$likes = $this->getLikesForArticle(intval($row['id']));
$filteredArticles[] = new Article( $filteredArticles[] = new Article(
intval($row['id']), intval($row['id']),
$row['title'], $row['title'],
@@ -294,7 +312,8 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$row['author'], $row['author'],
$row['category'], $row['category'],
$row['tags'], $row['tags'],
$row['created'] $row['created'],
$likes
); );
} }
@@ -341,6 +360,8 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$filteredArticles = []; $filteredArticles = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$likes = $this->getLikesForArticle(intval($row['id']));
$filteredArticles[] = new Article( $filteredArticles[] = new Article(
intval($row['id']), intval($row['id']),
$row['title'] ?? '', $row['title'] ?? '',
@@ -348,7 +369,8 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$row['author'] ?? '', $row['author'] ?? '',
$row['category'] ?? '', $row['category'] ?? '',
$row['tags'] ?? '', $row['tags'] ?? '',
$row['created'] ?? '' // Nutzt 'created' aus deiner DB-Struktur $row['created'] ?? '',
$likes
); );
} }
@@ -359,4 +381,69 @@ class DatabaseArticleManager implements ArticleManagerDAO {
} }
} }
/**
* Holt alle User-IDs, die einen bestimmten Beitrag geliked haben.
*
* @return String[] UserIDs
* @throws InternalServerErrorException
*/
private function getLikesForArticle(int $articleId): array
{
try {
$db = $this->getConnection();
$sql = "SELECT user_id FROM likes WHERE article_id = :article_id;";
$command = $db->prepare($sql);
$command->execute([':article_id' => $articleId]);
return $command->fetchAll(PDO::FETCH_COLUMN) ?: [];
} catch (PDOException $e) {
return [];
}
}
public function toggleLike(int $articleId, string $userId): bool
{
// prüfen, ob der Artikel überhaupt existiert
$article = $this->getArticle($articleId);
if (!$article) {
throw new NotFoundException("missing_id");
}
try {
$db = $this->getConnection();
// prüfen, ob das Like bereits existiert
$checkSql = "SELECT COUNT(*) FROM likes WHERE article_id = :article_id AND user_id = :user_id;";
$checkCommand = $db->prepare($checkSql);
$checkCommand->execute([
':article_id' => $articleId,
':user_id' => $userId
]);
$hasLiked = (int)$checkCommand->fetchColumn() > 0;
if ($hasLiked) {
// wenn bereits geliked -> Unlike
$deleteSql = "DELETE FROM likes WHERE article_id = :article_id AND user_id = :user_id;";
$deleteCommand = $db->prepare($deleteSql);
$deleteCommand->execute([
':article_id' => $articleId,
':user_id' => $userId
]);
return false; // gibt false zurück, da der Beitrag jetzt nicht mehr geliked ist
} else {
// wenn noch nicht geliked -> Like
$insertSql = "INSERT INTO likes (article_id, user_id) VALUES (:article_id, :user_id);";
$insertCommand = $db->prepare($insertSql);
$insertCommand->execute([
':article_id' => $articleId,
':user_id' => $userId
]);
return true; // gibt true zurück, da der Beitrag jetzt geliked ist
}
} catch (PDOException $e) {
throw new InternalServerErrorException("internal_error");
}
}
} }
+64 -6
View File
@@ -62,7 +62,8 @@ class LocalArticleManager implements ArticleManagerDAO {
"author" => $author, "author" => $author,
"category" => $category, "category" => $category,
"tags" => $tags, "tags" => $tags,
"creationDate" => date("Y-m-d H:i:s") "creationDate" => date("Y-m-d H:i:s"),
"likes" => []
]; ];
$this->saveArticle($articles); $this->saveArticle($articles);
@@ -92,7 +93,8 @@ class LocalArticleManager implements ArticleManagerDAO {
"author" => $author, "author" => $author,
"category" => $article->getCategory(), "category" => $article->getCategory(),
"tags" => $article->getTags(), "tags" => $article->getTags(),
"creationDate" => $article->getCreationDate() "creationDate" => $article->getCreationDate(),
"likes" => $storedArticle['likes'] ?? []
]; ];
$updated = true; $updated = true;
break; break;
@@ -142,7 +144,17 @@ class LocalArticleManager implements ArticleManagerDAO {
foreach ($articles as $article) { foreach ($articles as $article) {
if (isset($article['id']) && $article['id'] == $id) { if (isset($article['id']) && $article['id'] == $id) {
return new Article(intval($article['id']), $article['title'], $article['content'], $article['author'], $article['category'], $article['tags'], $article['creationDate']); $likes = isset($article['likes']) && is_array($article['likes']) ? $article['likes'] : [];
return new Article(
intval($article['id']),
$article['title'],
$article['content'],
$article['author'],
$article['category'],
$article['tags'],
$article['creationDate'],
$likes
);
} }
} }
@@ -168,6 +180,7 @@ class LocalArticleManager implements ArticleManagerDAO {
foreach ($articles as $article) { foreach ($articles as $article) {
if (isset($article['author']) && $article['author'] == $author) { if (isset($article['author']) && $article['author'] == $author) {
$likes = isset($article['likes']) && is_array($article['likes']) ? $article['likes'] : [];
$filteredArticles[] = new Article( $filteredArticles[] = new Article(
intval($article['id']), intval($article['id']),
$article['title'], $article['title'],
@@ -175,7 +188,8 @@ class LocalArticleManager implements ArticleManagerDAO {
$article['author'], $article['author'],
$article['category'], $article['category'],
$article['tags'], $article['tags'],
$article['creationDate'] $article['creationDate'],
$likes
); );
} }
} }
@@ -201,6 +215,7 @@ class LocalArticleManager implements ArticleManagerDAO {
if (($cleanKeyword !== '' && strpos($title, $cleanKeyword) !== false) || if (($cleanKeyword !== '' && strpos($title, $cleanKeyword) !== false) ||
($cleanKeyword !== '' && strpos($content, $cleanKeyword) !== false)) { ($cleanKeyword !== '' && strpos($content, $cleanKeyword) !== false)) {
$likes = isset($article['likes']) && is_array($article['likes']) ? $article['likes'] : [];
$filteredArticles[] = new Article( $filteredArticles[] = new Article(
intval($article['id'] ?? 0), intval($article['id'] ?? 0),
$article['title'] ?? '', $article['title'] ?? '',
@@ -208,7 +223,8 @@ class LocalArticleManager implements ArticleManagerDAO {
$article['author'] ?? '', $article['author'] ?? '',
$article['category'] ?? '', $article['category'] ?? '',
$article['tags'] ?? '', $article['tags'] ?? '',
$article['creationDate'] ?? '' $article['creationDate'] ?? '',
$likes
); );
} }
} }
@@ -223,6 +239,7 @@ class LocalArticleManager implements ArticleManagerDAO {
foreach ($articles as $article) { foreach ($articles as $article) {
if (isset($article['category']) && $article['category'] == $category) { if (isset($article['category']) && $article['category'] == $category) {
$likes = isset($article['likes']) && is_array($article['likes']) ? $article['likes'] : [];
$filteredArticles[] = new Article( $filteredArticles[] = new Article(
intval($article['id']), intval($article['id']),
$article['title'], $article['title'],
@@ -230,12 +247,53 @@ class LocalArticleManager implements ArticleManagerDAO {
$article['author'], $article['author'],
$article['category'], $article['category'],
$article['tags'], $article['tags'],
$article['creationDate'] $article['creationDate'],
$likes
); );
} }
} }
return $filteredArticles; return $filteredArticles;
} }
public function toggleLike(int $articleId, string $userId): bool
{
$articles = $this->getAllArticles();
$articleFound = false;
$isLikedNow = false;
foreach ($articles as $index => $article) {
if (isset($article['id']) && $article['id'] == $articleId) {
$articleFound = true;
// Likes-Array initialisieren, falls nicht vorhanden
if (!isset($articles[$index]['likes']) || !is_array($articles[$index]['likes'])) {
$articles[$index]['likes'] = [];
}
$likeIndex = array_search($userId, $articles[$index]['likes']);
if ($likeIndex !== false) {
// Bereits geliked -> Unlike
unset($articles[$index]['likes'][$likeIndex]);
// Array-Keys neu indizieren, damit JSON sauber bleibt
$articles[$index]['likes'] = array_values($articles[$index]['likes']);
$isLikedNow = false;
} else {
// Noch nicht geliked -> Like (User-ID hinzufügen)
$articles[$index]['likes'][] = $userId;
$isLikedNow = true;
}
break;
}
}
if (!$articleFound) {
throw new NotFoundException("missing_id");
}
$this->saveArticle($articles);
return $isLikedNow;
}
} }
?> ?>