Compare commits

...

51 Commits

Author SHA1 Message Date
niklas.ortmann a382d9254e Update README.md 2026-07-01 19:33:56 +02:00
niklas.ortmann 2b7f44b817 Merge remote-tracking branch 'origin/datenschutzkonformeRegistrierung' into dev 2026-07-01 19:30:44 +02:00
niklas.ortmann b5e4f44252 Merge branch 'dev' of https://git.gigaworld.ddns.net/niklas.ortmann/webprogrammierung into dev 2026-07-01 19:19:52 +02:00
niklas.ortmann 8561b02a08 Update README.md 2026-07-01 19:19:43 +02:00
niklas.ortmann 01f4ff2d28 Update dataSources.local.xml 2026-07-01 19:19:41 +02:00
niklas.ortmann b7e4387373 Merge pull request 'Database - und LocalArticleManager angepasst, sodass nun auch nach tags gesucht wird.' (#47) from SearchByTags into dev
Reviewed-on: #47
Reviewed-by: niklas.ortmann <ortmann.niklas@yahoo.de>
2026-07-01 19:15:06 +02:00
niklas.ortmann e9a92eaec7 merge 2026-07-01 19:11:32 +02:00
rirat-0 7a4f2d738f Debugging 2026-07-01 00:00:46 +02:00
rirat-0 a0a469a9df Update der Suchfunktionen um nach tags zu suchen 2026-06-30 23:58:02 +02:00
niklas.ortmann 54209929fe Merge pull request 'Implementierung von Nutzungsbedingungen und Barrierefreiheitserklärung' (#46) from Nutzungsbedingungen&barrierefreiheit into dev
Reviewed-on: #46
Reviewed-by: niklas.ortmann <ortmann.niklas@yahoo.de>
2026-06-30 22:41:56 +02:00
rirat-0 5321534d71 anpassung der filterJS fuer ergebnislimits 2026-06-30 21:13:02 +02:00
caroline.slt 7cf542c270 Richtig Einbinden 2026-06-30 21:09:49 +02:00
caroline.slt c45d23a6b3 Korrektur 2026-06-30 21:07:53 +02:00
caroline.slt ab094429b7 Erste Implementierung 2026-06-30 21:04:24 +02:00
caroline.slt cc5f46c00e Reviewänderungen 2026-06-30 20:25:46 +02:00
rirat-0 1b2ab1cccb rechtschreibung 2026-06-30 20:25:46 +02:00
rirat-0 adbf488e01 datenschutz und impressum wurden ersten, die readme entsprechend angepasst 2026-06-30 20:25:46 +02:00
niklas.ortmann 15ea5ddbfb Merge pull request 'datenschutz und impressum wurden erstellt, die readme entsprechend angepasst' (#43) from ImpressumUndDatenschutz into dev
Reviewed-on: #43
Reviewed-by: niklas.ortmann <ortmann.niklas@yahoo.de>
2026-06-30 18:13:51 +02:00
rirat-0 cbb662d159 testing 2026-06-30 16:16:14 +02:00
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
rirat-0 17034c928b anpassung der search results fuer die filterung nach likes 2026-06-17 22:03:51 +02:00
rirat-0 395041fd44 anpassung, damit die like sortierung client seitig ist 2026-06-17 21:59:12 +02:00
rirat-0 c117c7e641 Merge branch 'dev' into SuchergebnisseJS 2026-06-17 21:48:58 +02:00
rirat-0 12ad9759cf aufraeumen 2 2026-06-17 20:42:31 +02:00
rirat-0 057639be7c anpassung der error message 2026-06-17 20:38:19 +02:00
rirat-0 87e7be1c10 Implementation des validators und korrektes einbinden 2026-06-17 20:30:45 +02:00
rirat-0 1db77346d0 Revert "Implementierung des validators"
This reverts commit 1e6ac0fcd9.
2026-06-17 20:14:59 +02:00
rirat-0 1e6ac0fcd9 Implementierung des validators 2026-06-17 20:14:07 +02:00
rirat-0 b3c74c2172 Debugging 8 2026-06-17 19:54:31 +02:00
rirat-0 0805958033 Anpassung der filter.js
es gab ein problem bei der sortierung, da nur 10 ergebnisse angezeigt werden. mit dieser anpassung koennen ergebnisse 'nachruecken'
2026-06-17 19:47:33 +02:00
rirat-0 5c0e978c93 Debugging 7 2026-06-17 19:38:00 +02:00
rirat-0 520298bae4 Debugging 6 2026-06-17 19:35:02 +02:00
rirat-0 969adc4836 Debugging 5 2026-06-17 19:33:09 +02:00
rirat-0 d9eca06197 Debugging 4 2026-06-17 19:30:11 +02:00
rirat-0 48f2e90e49 Debugging 3 2026-06-17 19:21:58 +02:00
rirat-0 358247a2a1 Filter funktion implementiert 2026-06-17 19:19:31 +02:00
rirat-0 de4d2fe5c0 sortieren nach likes nun moeglich 2026-06-17 17:58:40 +02:00
rirat-0 e1102eb7db Merge branch 'dev' into SuchergebnisseJS 2026-06-17 17:48:08 +02:00
rirat-0 a8df9590fd updater sorter.js fuer client seite 2026-06-17 15:38:59 +02:00
rirat-0 3e453e22ec verschieben der script aufrufe in die index.php 2026-06-17 15:31:05 +02:00
rirat-0 beeab0ec90 aufraeumen 2026-06-17 14:50:05 +02:00
rirat-0 9353a7eaaa Debugging 2 2026-06-17 14:48:33 +02:00
rirat-0 cac8f3046d Debugging 1 2026-06-17 14:45:01 +02:00
rirat-0 66eeac372c Anpassung der results seite und erstellung der script-datei 2026-06-17 14:41:05 +02:00
25 changed files with 857 additions and 74 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="IU-261.24374.151">
<component name="dataSourceStorageLocal" created-in="IU-253.32098.101">
<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">
<identifier-quote-string>&quot;</identifier-quote-string>
+5
View File
@@ -13,6 +13,10 @@
- Per Klick auf das Logo gelangt man auf die Home-Seite.
## Ausgelassene Teilaufgaben
- Das Anerkennen der Datenschutzerklärung und das Einhalten der
Nutzungsbedingungen (bspw. was Rechte an eingestellten Texten und
hochgeladenen Fotos angeht) muss ein Nutzer bei der Registrierung via einer
Checkbox und dem Opt-In-Verfahren fehlt noch.
## Bekannte Fehler und Mängel
- Bitte auf die gesetzten TODO's achten. Wenn Inhalte fehlen, sind sie i.d.R. als TODO kommentiert.
@@ -23,6 +27,7 @@
- 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.
- Die Datenbank ist noch in die verschiedenen Manager unterteilt. Zukünftig werden alle Daten in einer Datenbank gespeichert.
## Besonderheiten des Projektes
- Es wurde AJAX verwendet, um asynchrone Erstellung von Kommentaren zu implementieren. Es ermöglicht dem Nutzer, einen
+66
View File
@@ -0,0 +1,66 @@
<!--
Seite: Barrierefreiheitserklärung
Inhalt: Erklärung zur Barrierefreiheit der Plattform
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="../images/logos/logo_icon.ico">
<link rel="stylesheet" href="../css/main.css">
<link rel="stylesheet" href="../css/navbar.css">
<link rel="stylesheet" href="../css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<h1>Barrierefreiheitserklärung</h1>
<p>
Wir bemühen uns, die Plattform EduForge möglichst barrierefrei zugänglich zu gestalten.
Die Website wurde im Rahmen eines Hochschulprojekts erstellt.
</p>
<h2>Stand der Vereinbarkeit</h2>
<p>
Diese Website ist teilweise barrierefrei. Einzelne Funktionen oder Inhalte können
noch nicht vollständig barrierefrei nutzbar sein.
</p>
<h2>Nicht barrierefreie Inhalte</h2>
<p>
Einzelne Bilder verfügen möglicherweise noch nicht über vollständige Alternativtexte.
Außerdem kann die Bedienung mit Tastatur oder Screenreader in einzelnen Bereichen
eingeschränkt sein.
</p>
<h2>Feedback und Kontakt</h2>
<p>
Sollten Ihnen Barrieren auf unserer Website auffallen, kontaktieren Sie uns bitte
über die im Impressum angegebenen Kontaktdaten.
</p>
<h2>Erstellungsdatum</h2>
<p>
Diese Erklärung wurde im Rahmen eines Hochschulprojekts erstellt.
</p>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
<?php
/** @var string $title */
/** @var string $message */
/** @var string $link */
/** @var bool $isSuccess */
$title = $title ?? "Passwort zurücksetzen";
$message = $message ?? "Der Bestätigungslink konnte nicht verarbeitet werden.";
$link = $link ?? "";
$isSuccess = $isSuccess ?? false;
?>
<!--
Ansicht: Passwortbestätigung
Funktion: Zeigt das Ergebnis der Passwortänderung an.
-->
<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>
+29
View File
@@ -0,0 +1,29 @@
<?php
/** @var string $title */
/** @var string $message */
/** @var string $link */
/** @var bool $isSuccess */
$title = $title ?? "Registrierung";
$message = $message ?? "Der Registrierungslink konnte nicht verarbeitet werden.";
$link = $link ?? "";
$isSuccess = $isSuccess ?? false;
?>
<!--
Ansicht: Registrierungsbestätigung
Funktion: Zeigt das Ergebnis der Registrierungsbestätigung an.
-->
<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>
+10 -2
View File
@@ -11,8 +11,10 @@ $error = $error ?? null;
<h1>Bitte anmelden</h1>
<?php if ($error): ?>
<p style="color:red;">
<?php include_once "includes/alertMessages.php"; ?>
<?php if (!empty($error)): ?>
<p class="alert-message is-error">
<?php echo htmlspecialchars($error); ?>
</p>
<?php endif; ?>
@@ -41,6 +43,12 @@ $error = $error ?? null;
anmelden
</button>
<div class="register-link">
<a href="index.php?pfad=password-forgotten">
Passwort vergessen?
</a>
</div>
<div class="register-link">
<a href="index.php?pfad=register">
Noch keinen Account? Jetzt hier registrieren!
+45 -6
View File
@@ -23,19 +23,58 @@
include_once 'includes/navbar.php';
?>
<main>
<main>
<h1>Nutzungsbedingungen</h1>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
sea takimata sanctus est Lorem ipsum dolor sit amet.
Diese Nutzungsbedingungen regeln die Nutzung der Plattform EduForge.
Mit der Registrierung und Nutzung der Website erkennen Nutzerinnen und Nutzer
die nachfolgenden Bedingungen an.
</p>
<h2>1. Nutzung der Plattform</h2>
<p>
EduForge dient dem Austausch von Lerninhalten, Beiträgen und Kommentaren.
Die Nutzung der Plattform darf ausschließlich im Rahmen der geltenden Gesetze erfolgen.
</p>
<h2>2. Verhalten der Nutzerinnen und Nutzer</h2>
<p>
Nutzerinnen und Nutzer verpflichten sich zu einem respektvollen Umgang miteinander.
Das Veröffentlichen von beleidigenden, diskriminierenden, rechtswidrigen oder
anderweitig unangemessenen Inhalten ist untersagt.
</p>
<h2>3. Veröffentlichung von Inhalten</h2>
<p>
Nutzerinnen und Nutzer sind für die von ihnen veröffentlichten Inhalte selbst verantwortlich.
Dies gilt insbesondere für Beiträge, Kommentare, Bilder und sonstige hochgeladene Dateien.
</p>
<h2>4. Urheberrechte</h2>
<p>
Es dürfen ausschließlich Inhalte veröffentlicht werden, an denen die Nutzerinnen und Nutzer
die erforderlichen Rechte besitzen. Durch das Einstellen von Inhalten versichern die
Nutzerinnen und Nutzer, keine Rechte Dritter zu verletzen.
</p>
<h2>5. Entfernung von Inhalten</h2>
<p>
Inhalte, die gegen diese Nutzungsbedingungen oder geltendes Recht verstoßen,
können vom Betreiber der Plattform entfernt werden.
</p>
<h2>6. Änderungen der Nutzungsbedingungen</h2>
<p>
Der Betreiber behält sich vor, diese Nutzungsbedingungen anzupassen, sofern dies
für den Betrieb oder die Weiterentwicklung der Plattform erforderlich ist.
</p>
</main>
</main>
<?php
+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>
+8 -1
View File
@@ -1,5 +1,6 @@
<?php
$error = $error ?? null;
$success = $success ?? null;
?>
<!--
@@ -12,11 +13,17 @@ $error = $error ?? null;
<h1>Jetzt Registrieren!</h1>
<?php if (!empty($error)): ?>
<p class="alert-message is-error" style="color:red;">
<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=register">
<p class="input-label">Email:</p>
+56 -30
View File
@@ -27,7 +27,8 @@ if ($currentPage < 1) {
$offset = ($currentPage - 1) * $limit;
// Nur die Ergebnisse für die aktuelle Seite ausschneiden
$results = array_slice($all_results, $offset, $limit);
//$results = array_slice($all_results, $offset, $limit);
$results = $all_results;
$resultCount = count($results);
?>
@@ -45,9 +46,9 @@ $resultCount = count($results);
<aside class="s-res-sidebar">
<!-- Sortierfuntion Box und Such Box-->
<form id="search-form-id" action="php/controller/search-results-controller.php" method="GET" class="s-res-sidebar-form">
<!-- Dieses Feld hält die aktuelle Seitenzahl für den Submit bereit -->
<input type="hidden" name="page" id="s-res-page-input" value="<?php echo $currentPage; ?>">
<form action="php/controller/search-results-controller.php" method="GET" id="search-form-id" class="s-res-sidebar-form">
<input type="hidden" id="s-res-page-input" name="page" value="<?php echo $_GET['page'] ?? 1; ?>">
<div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Suche anpassen</h3>
@@ -60,25 +61,57 @@ $resultCount = count($results);
<?php $currentSort = $_SESSION['search_sort'] ?? 'alphabet'; ?>
<div class="s-res-filter-group">
<label class="s-res-filter-option">
<input type="radio" name="sort" value="alphabet" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?> onchange="this.form.submit()">
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
<span>Alphabetisch</span>
</label>
<!-- Noch disabled, da likes noch nicht implementiert-->
<label class="s-res-filter-option">
<input type="radio" name="sort" value="likes" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?> onchange="this.form.submit()">
<input type="radio" name="sort" value="likes" class="sort-radio" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?>>
<span>Beliebtheit (Likes)</span>
</label>
<label class="s-res-filter-option">
<input type="radio" name="sort" value="newest" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?> onchange="this.form.submit()">
<input type="radio" name="sort" value="newest" class="sort-radio" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?>>
<span>Neueste Beiträge</span>
</label>
<label class="s-res-filter-option">
<input type="radio" name="sort" value="oldest" <?php echo $currentSort === 'oldest' ? 'checked' : ''; ?> onchange="this.form.submit()">
<input type="radio" name="sort" value="oldest" class="sort-radio" <?php echo $currentSort === 'oldest' ? 'checked' : ''; ?>>
<span>Älteste Beiträge</span>
</label>
</div>
</div>
<div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Kategorie filtern</h3>
<select id="category-filter" class="s-res-limit-select" style="width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #cbd5e1;">
<option value="all">Alle Kategorien</option>
<option value="Deutsch">Deutsch</option>
<option value="Englisch">Englisch</option>
<option value="Franzoesisch">Französisch</option>
<option value="Latein">Latein</option>
<option value="Literatur">Literatur</option>
<option value="Mathematik">Mathematik</option>
<option value="Biologie">Biologie</option>
<option value="Informatik">Informatik</option>
<option value="Chemie">Chemie</option>
<option value="Physik">Physik</option>
<option value="Astronomie">Astronomie</option>
<option value="Geschichte">Geschichte</option>
<option value="Erdkunde">Erdkunde</option>
<option value="Sozialkunde">Sozialkunde</option>
<option value="Wirtschaftskunde">Wirtschaftskunde</option>
<option value="Religion">Religion</option>
<option value="Ethikunterricht">Ethikunterricht</option>
<option value="Philosophie">Philosophie</option>
<option value="Psychologie">Psychologie</option>
<option value="Kunst">Kunst</option>
<option value="Musik">Musik</option>
<option value="Theater">Theater</option>
<option value="Technik">Technik</option>
<option value="Werken">Werken</option>
<option value="Hauswirtschaft">Hauswirtschaft</option>
<option value="Sport">Sport</option>
</select>
</div>
</form>
</aside>
@@ -95,8 +128,11 @@ $resultCount = count($results);
<?php
if (!empty($results)): ?>
<?php foreach ($results as $item): ?>
<div class="s-res-item">
<?php foreach ($results as $item):
// Anzahl der Likes ermitteln (falls es ein Array ist, zählen; falls Zahl, direkt nutzen)
$likesCount = isset($item['likes']) && is_array($item['likes']) ? count($item['likes']) : ($item['likes'] ?? 0);
?>
<div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
<div class="s-res-content">
<h2 class="s-res-item-title">
<a href="index.php?pfad=showArticle&id=<?php echo $item['id']; ?>" class="s-res-link">
@@ -107,10 +143,9 @@ $resultCount = count($results);
<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; ?>
❤️ <?php echo $likesCount; ?>
</span>
</div>
</div>
<div class="s-res-arrow">&rarr;</div>
</div>
@@ -120,8 +155,8 @@ $resultCount = count($results);
elseif (isset($_SESSION["search_query"]) && $_SESSION["search_query"] !== "" && $resultCount === 0): ?>
<p>Keine Beiträge zu diesem Suchbegriff gefunden.</p>
<?php
elseif (isset($_SESSION["message"]) && $_SESSION["message"] == "missing_parameters"): ?>
<p>Bitte überprüfe deine Sucheingabe und versuche es erneut!</p>
elseif (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_search_query"): ?>
<p>Unzulässige Suchanfrage</p>
<?php endif; ?>
<?php
@@ -134,7 +169,7 @@ $resultCount = count($results);
<!-- Auswahl der Ergebnisse pro Seite -->
<div class="s-res-limit-selector">
<label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label>
<select id="s-res-per-page" name="limit" class="s-res-limit-select" form="search-form-id" onchange="this.form.submit()">
<select id="s-res-per-page" name="limit" class="s-res-limit-select">
<option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option>
<option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option>
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option>
@@ -143,20 +178,11 @@ $resultCount = count($results);
</div>
<div class="s-res-page-navigation">
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage - 1; ?>" <?php echo $currentPage <= 1 ? 'disabled' : ''; ?>>
«
</button>
<!-- Dynamische Seitenzahlen -->
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<button type="button"
class="s-res-page-btn <?php echo $i === $currentPage ? 's-res-page-btn-active' : ''; ?>"
data-page="<?php echo $i; ?>">
<?php echo $i; ?>
</button>
<?php endfor; ?>
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage + 1; ?>" <?php echo $currentPage >= $totalPages ? 'disabled' : ''; ?>>
»
</button>
<button type="button" class="s-res-page-btn" id="prev-page-btn" data-page="0">«</button>
<span id="dynamic-page-numbers"></span>
<button type="button" class="s-res-page-btn" id="next-page-btn" data-page="2">»</button>
</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>
+15 -15
View File
@@ -130,13 +130,6 @@ h1 {
color: #1f2937;
}
.input-label {
margin-bottom: 5px;
font-weight: bold;
width: 100%;
color: #1f2937;
}
.login-input {
width: 100%;
padding: 12px;
@@ -210,14 +203,6 @@ h1 {
.form-container {
width: 90%;
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;
padding: 30px;
background-color: white;
@@ -226,3 +211,18 @@ h1 {
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
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
@@ -78,6 +78,17 @@
Das Bild konnte nicht hochgeladen werden. Bitte versuche es erneut oder verwende ein anderes Bildformat.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "registration_confirmed"): ?>
<p class="alert-message is-success">
Ihre Registrierung wurde erfolgreich abgeschlossen. Sie können sich jetzt anmelden.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "password_changed"): ?>
<p class="alert-message is-success">
Ihr Passwort wurde erfolgreich geändert. Sie können sich jetzt anmelden.
</p>
<?php endif; ?>
<?php
unset($_SESSION["message"]);
?>
+1
View File
@@ -2,4 +2,5 @@
<a href="index.php?pfad=impressum">Impressum</a>
<a href="index.php?pfad=datenschutz">Datenschutz</a>
<a href="index.php?pfad=nutzungsbedingungen">Nutzungsbedingungen</a>
<a href="index.php?pfad=accessibility">Barrierefreiheit</a>
</footer>
+13
View File
@@ -17,6 +17,17 @@ if ($pfad === "login") {
if ($pfad === "register") {
include_once "php/controller/register-controller.php";
}
if ($pfad === "password-forgotten") {
include_once "php/controller/password-forgotten-controller.php";
}
if ($pfad === "confirm-register") {
include_once "php/controller/confirm-register-controller.php";
}
if ($pfad === "confirm-password") {
include_once "php/controller/confirm-password-controller.php";
}
if ($pfad === "logout") {
include_once "php/controller/logout-controller.php";
@@ -53,8 +64,10 @@ if ($pfad === "deleteAccount") {
<link rel="stylesheet" href="css/message.css">
<script src="js/paginator.js" async></script>
<script src="js/sorter.js" async></script>
<script src="js/comments.js" defer></script>
<script src="js/editor.js" async></script>
<script src="js/filter.js" async></script>
<title>EduForge</title>
</head>
+116
View File
@@ -0,0 +1,116 @@
let currentClientPage = 1;
let itemsPerPage = 10;
function initFilter() {
const filterSelect = document.getElementById('category-filter');
const listContainer = document.querySelector('.s-res-list');
const limitSelect = document.getElementById('s-res-per-page');
if (!filterSelect || !listContainer) return;
if (limitSelect) {
itemsPerPage = parseInt(limitSelect.value, 10) || 10;
}
updateVisibility();
if (limitSelect) {
limitSelect.addEventListener('change', function() {
//neues limit einlesen
itemsPerPage = parseInt(this.value, 10);
currentClientPage = 1;
updateVisibility();
});
}
filterSelect.addEventListener('change', function() {
currentClientPage = 1;
updateVisibility();
});
const navigationContainer = document.querySelector('.s-res-page-navigation');
if (navigationContainer) {
navigationContainer.addEventListener('click', function(e) {
const button = e.target.closest('.s-res-page-btn');
if (!button || button.disabled) return;
e.preventDefault();
const targetPage = button.getAttribute('data-page');
if (targetPage) {
currentClientPage = parseInt(targetPage, 10);
updateVisibility();
}
});
}
}
function updateVisibility() {
const filterSelect = document.getElementById('category-filter');
const listContainer = document.querySelector('.s-res-list');
const selectedCategory = filterSelect.value.toLowerCase().trim();
const cards = listContainer.querySelectorAll('.s-res-item');
let visibleCards = [];
cards.forEach(card => {
const cardCategory = (card.getAttribute('data-category') || '').toLowerCase().trim();
if (selectedCategory === 'all' || cardCategory.includes(selectedCategory) || selectedCategory.includes(cardCategory)) {
visibleCards.push(card);
} else {
card.style.display = 'none';
}
});
const totalVisible = visibleCards.length;
const totalPages = Math.max(1, Math.ceil(totalVisible / itemsPerPage));
if (currentClientPage < 1) currentClientPage = 1;
if (currentClientPage > totalPages) currentClientPage = totalPages;
const startOffset = (currentClientPage - 1) * itemsPerPage;
const endOffset = startOffset + itemsPerPage;
visibleCards.forEach((card, index) => {
if (index >= startOffset && index < endOffset) {
card.style.display = 'flex';
} else {
card.style.display = 'none';
}
});
updatePaginatorUI(currentClientPage, totalPages);
}
function updatePaginatorUI(currentPage, totalPages) {
const prevBtn = document.getElementById('prev-page-btn');
const nextBtn = document.getElementById('next-page-btn');
const numbersContainer = document.getElementById('dynamic-page-numbers');
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.setAttribute('data-page', currentPage - 1);
prevBtn.disabled = (currentPage <= 1);
nextBtn.setAttribute('data-page', currentPage + 1);
nextBtn.disabled = (currentPage >= totalPages);
let buttonsHTML = '';
for (let i = 1; i <= totalPages; i++) {
const activeClass = (i === currentPage) ? 's-res-page-btn-active' : '';
buttonsHTML += `<button type="button" class="s-res-page-btn ${activeClass}" data-page="${i}">${i}</button> `;
}
numbersContainer.innerHTML = buttonsHTML;
}
// Hilfsfunktion für Math.ceil in JS
function ceil(val) { return Math.ceil(val); }
// ist das DOM bereits vollständig aufgebaut?
if (document.readyState === 'loading') {
// Falls noch geladen wird, auf das Event warten
document.addEventListener('DOMContentLoaded', initFilter);
} else {
// Falls das HTML bereits komplett da ist, sofort ausführen
initFilter();
}
+50
View File
@@ -0,0 +1,50 @@
function initSorter() {
const listContainer = document.querySelector('.s-res-list');
const sortRadios = document.querySelectorAll('.sort-radio');
// wenn keine liste vorhanden, abbrechen
if (!listContainer || sortRadios.length === 0) return;
sortRadios.forEach(radio => {
radio.addEventListener('change', function() {
const cards = Array.from(listContainer.querySelectorAll('.s-res-item'));
const sortValue = this.value;
cards.sort((a, b) => {
if (sortValue === 'likes') {
const likesA = parseInt(a.getAttribute('data-likes') || '0', 10);
const likesB = parseInt(b.getAttribute('data-likes') || '0', 10);
return likesB - likesA;
}
else if (sortValue === 'alphabet') {
// alphabetische sortierung
const titleA = a.querySelector('.s-res-link').textContent.trim().toLowerCase();
const titleB = b.querySelector('.s-res-link').textContent.trim().toLowerCase();
return titleA.localeCompare(titleB);
}
else if (sortValue === 'newest' || sortValue === 'oldest') {
// hoehere ID wird als neuer gesehen
const urlA = a.querySelector('.s-res-link').getAttribute('href');
const urlB = b.querySelector('.s-res-link').getAttribute('href');
const idA = parseInt(urlA.match(/id=(\d+)/)[1], 10);
const idB = parseInt(urlB.match(/id=(\d+)/)[1], 10);
return sortValue === 'newest' ? idB - idA : idA - idB;
}
return 0;
});
listContainer.innerHTML = '';
cards.forEach(card => listContainer.appendChild(card));
});
});
}
// ist das DOM bereits vollständig aufgebaut?
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSorter);
} else {
initSorter();
}
@@ -0,0 +1,65 @@
<?php
require_once "php/model/UserManager.php";
/*
Verarbeitung der Passwortbestätigung
Funktion: Liest die zwischengespeicherten Passwortdaten aus,
aktualisiert das Passwort des Benutzers und gibt das Ergebnis
für die View zurück.
*/
$title = "Passwort zurücksetzen";
$message = "";
$link = "";
$isSuccess = false;
$token = basename($_GET["token"] ?? "");
$file = "data/pending-password/" . $token . ".json";
if (empty($token) || !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"]
);
$_SESSION["message"] = "password_changed";
header("Location: index.php?pfad=login");
exit();
} else {
$message = "Der Benutzer konnte nicht gefunden werden.";
}
unlink($file);
} catch (Exception $e) {
$message = "Das Passwort konnte nicht geändert werden.";
}
}
}
@@ -0,0 +1,64 @@
<?php
require_once "php/model/UserManager.php";
/*
Verarbeitung der Registrierungsbestätigung
Funktion: Liest die zwischengespeicherten Registrierungsdaten aus,
legt den Benutzer an und gibt das Ergebnis für die View zurück.
*/
$title = "Registrierung";
$message = "";
$link = "";
$isSuccess = false;
$token = basename($_GET["token"] ?? "");
$file = "data/pending/" . $token . ".json";
if (empty($token) || !file_exists($file)) {
$message = "Der Registrierungslink ist ungültig oder bereits abgelaufen.";
} else {
$data = json_decode(file_get_contents($file), true);
if (
$data === null ||
empty($data["email"]) ||
empty($data["vorname"]) ||
empty($data["nachname"]) ||
empty($data["password"])
) {
$message = "Die Registrierungsdaten konnten nicht gelesen werden.";
} else {
try {
$dao = UserManager::getInstance();
if ($dao->findUser($data["email"]) === null) {
$dao->addUser(
$data["email"],
$data["vorname"],
$data["nachname"],
$data["password"]
);
}
unlink($file);
$_SESSION["message"] = "registration_confirmed";
header("Location: index.php?pfad=login");
exit();
} catch (Exception $e) {
$message = "Die Registrierung konnte nicht abgeschlossen werden.";
}
}
}
@@ -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.";
}
}
}
+60 -8
View File
@@ -4,6 +4,7 @@ require_once "php/model/UserManager.php";
require_once "php/validator/user-validator.php";
$error = null;
$success = null;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
@@ -24,20 +25,71 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
try {
$dao = UserManager::getInstance();
// Token für die simulierte E-Mail und die spätere Bestätigung erzeugen.
$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") && !mkdir("data/pending", 0777, true)) {
throw new RuntimeException("Ordner data/pending konnte nicht erstellt werden.");
}
if (!is_writable("data/mails")) {
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);
$dao->addUser($email, $vorname, $nachname, $password);
$pendingData = [
"email" => $email,
"vorname" => $vorname,
"nachname" => $nachname,
"password" => $password
];
$_SESSION["user"] = $vorname . " " . $nachname;
$_SESSION["user_email"] = $email;
file_put_contents(
"data/pending/" . $token . ".json",
json_encode($pendingData, JSON_PRETTY_PRINT)
);
header("Location: index.php");
exit();
$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) {
$error = "Die Registrierung konnte nicht gespeichert werden.";
$error = "Die Registrierung konnte nicht verarbeitet werden.";
}
}
}
+6 -2
View File
@@ -5,14 +5,15 @@ if (session_status() === PHP_SESSION_NONE) {
require_once '../model/LocalArticleManager.php';
require_once '../model/ArticleManager.php';
require_once '../model/Article.php';
require_once '../validator/search-validator.php';
if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
$search = trim($_GET["q"]);
if (empty($search)) {
if (!searchQueryValidator($search)) {
$_SESSION["search_results"] = [];
$_SESSION["search_query"] = "";
$_SESSION["message"] = "missing_parameters";
$_SESSION["message"] = "invalid_search_query";
} else {
try {
@@ -70,6 +71,9 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
$sort = $_GET['sort'] ?? 'alphabet';
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
if (!searchLimitValidator($limit)) {
$limit = 10;
}
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&limit=" . $limit . "&page=" . $page);
exit();
+2 -1
View File
@@ -338,7 +338,8 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$sql = "SELECT id, title, content, author, category, tags, created
FROM articles
WHERE title LIKE :keyword
OR content LIKE :keyword;";
OR content LIKE :keyword
OR tags LIKE :keyword";
$command = $db->prepare($sql);
if (!$command) {
+3 -1
View File
@@ -211,9 +211,11 @@ class LocalArticleManager implements ArticleManagerDAO {
foreach ($articles as $article) {
$title = isset($article['title']) ? strtolower((string)$article['title']) : '';
$content = isset($article['content']) ? strtolower((string)$article['content']) : '';
$tags = isset($article['tags']) ? strtolower((string)$article['tags']) : '';
if (($cleanKeyword !== '' && strpos($title, $cleanKeyword) !== false) ||
($cleanKeyword !== '' && strpos($content, $cleanKeyword) !== false)) {
($cleanKeyword !== '' && strpos($content, $cleanKeyword) !== false) ||
($cleanKeyword !== '' && strpos($tags, $cleanKeyword) !== false)) {
$likes = isset($article['likes']) && is_array($article['likes']) ? $article['likes'] : [];
$filteredArticles[] = new Article(
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* Prüft, ob eine Suchanfrage gültig ist.
*
* Erlaubt werden Buchstaben, Zahlen, Umlaute, typische Satzzeichen und Leerzeichen.
* Die Länge muss zwischen 1 und 50 Zeichen liegen.
*
* @param string $query Zu prüfender Suchbegriff
*
* @return bool true wenn die Suche gültig ist, sonst false
*/
function searchQueryValidator($query)
{
$query = trim($query);
// Mindestens 1 Zeichen, maximal 50 Zeichen
$length = mb_strlen($query);
if ($length < 1 || $length > 50) {
return false;
}
// Erlaubt Buchstaben (inkl. Umlaut/ß), Zahlen, Leerzeichen sowie ?, !, ., -, _
$searchPattern = '/^[a-zA-Z0-9äöüÄÖÜß\s?!.,\-_]+$/u';
return preg_match($searchPattern, $query) === 1;
}
/**
* Prüft, ob das gewählte Treffer-Limit erlaubt ist.
*
* @param int|string $limit Das zu prüfende Limit
*
* @return bool true wenn das Limit 10, 20, 50 oder 100 ist, sonst false
*/
function searchLimitValidator($limit)
{
$allowedLimits = [10, 20, 50, 100];
return in_array((int)$limit, $allowedLimits, true);
}