abschluss

This commit is contained in:
2025-06-20 07:55:37 +00:00
parent 497e6a0bdf
commit 6c2e71dd53
34 changed files with 1370 additions and 382 deletions

39
app/Core/Container.php Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace Blog\Core;
use Exception;
/**
* Ein einfacher Dependency Injection Container.
*/
class Container {
/**
* @var array Liste der registrierten Instanzen.
*/
private array $instances = [];
/**
* Registriert eine Instanz oder Factory-Funktion im Container.
*
* @param string $key Der eindeutige Schlüssel für die Instanz.
* @param callable $factory Eine Factory-Funktion, die eine Instanz erzeugt.
* @return void
*/
public function set(string $key, callable $factory): void {
$this->instances[$key] = $factory;
}
/**
* Ruft eine registrierte Instanz ab und erstellt sie falls nötig.
*
* @param string $key Der Schlüssel der angeforderten Instanz.
* @return mixed Die abgerufene Instanz.
* @throws Exception Wenn keine Instanz mit dem gegebenen Schlüssel existiert.
*/
public function get(string $key): mixed {
if(!isset($this->instances[$key])) {
throw new Exception("No instance found for {$key}");
}
return $this->instances[$key]($this);
}
}

View File

@ -1,18 +1,40 @@
<?php
namespace Blog\Core;
use Blog\Middleware\middlewareInterface;
use Blog\Http\request;
use Blog\Http\response;
use Blog\Middleware\MiddlewareInterface;
use Blog\Http\Request;
use Blog\Http\Response;
/**
* Router-Klasse für das Routing von HTTP-Anfragen.
*/
class Router {
/**
* @var array Liste der registrierten Routen.
*/
private array $routes = [];
/**
* Fügt eine neue Route hinzu.
*
* @param string $method HTTP-Methode (z.B. GET, POST).
* @param string $path Pfad der Route, kann Platzhalter enthalten.
* @param callable $handler Handler-Funktion für die Route.
* @param array $middlewares Liste der Middlewares für die Route.
* @return void
*/
public function addRoute(string $method, string $path, callable $handler, array $middlewares = []): void {
$path = preg_replace('/{(\w+)}/', '(?P<$1>[^/]+)', $path);
$this->routes[] = compact("method", "path", "handler", "middlewares");
}
/**
* Verarbeitet eine eingehende HTTP-Anfrage und sucht nach einer passenden Route.
*
* @param Request $req Die eingehende HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @return void
*/
public function dispatch(Request $req, Response $res): void {
$method = $req->getMethod();
$uri = $req->getPath();
@ -36,6 +58,14 @@ class Router {
->send();
}
/**
* Führt die definierten Middlewares für eine Anfrage aus.
*
* @param array $middlewares Liste der Middlewares.
* @param Request $req Die eingehende HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @return bool Gibt true zurück, wenn alle Middlewares erfolgreich ausgeführt wurden, andernfalls false.
*/
private function handleMiddlewares(array $middlewares, Request $req, Response $res): bool {
foreach($middlewares as $middleware) {
$middlewareInstance = is_string($middleware) ? new $middleware() : $middleware;

View File

@ -1,19 +0,0 @@
<?php
namespace Blog\Core;
use Exception;
class Container {
private array $instances = [];
public function set(string $key, callable $factory): void {
$this->instances[$key] = $factory;
}
public function get(string $key): mixed {
if(!isset($this->instances[$key])) {
throw new Exception("No instance found for {$key}");
}
return $this->instances[$key]($this);
}
}

View File

@ -4,9 +4,21 @@ namespace Blog\Database;
use PDO;
use PDOException;
/**
* Verwaltet die Verbindung zur MySQL-Datenbank.
*/
class Database {
/**
* @var PDO|null Statische Instanz der Datenbankverbindung.
*/
private static ?PDO $pdo = null;
/**
* Stellt eine Verbindung zur Datenbank her oder gibt die bestehende zurück.
*
* @return PDO Die aktive PDO-Verbindung.
* @throws PDOException Falls die Verbindung fehlschlägt.
*/
public static function getConnection(): PDO {
if(self::$pdo === null) {
$config = parse_ini_file(__DIR__ . "/../../.env");

72
app/Entity/Post.php Normal file
View File

@ -0,0 +1,72 @@
<?php
namespace Blog\Entity;
/**
* Repräsentiert einen Blogpost mit ID, Titel, Inhalt, Autor und Zeitstempel.
*/
class Post {
/**
* Erstellt eine neue Post-Instanz.
*
* @param int $id Die eindeutige ID des Blogposts.
* @param string $title Der Titel des Blogposts.
* @param string $content Der Inhalt des Blogposts.
* @param string $author Der Name des Autors.
* @param int $stamp Der Zeitstempel der Veröffentlichung.
*/
public function __construct(
private int $id,
private string $title,
private string $content,
private string $author,
private int $stamp
) {}
/**
* Gibt die ID des Blogposts zurück.
*
* @return int Die eindeutige ID.
*/
public function getId(): int {
return $this->id;
}
/**
* Gibt den Titel des Blogposts zurück.
*
* @return string Der Titel des Blogposts.
*/
public function getTitle(): string {
return $this->title;
}
/**
* Gibt den Inhalt des Blogposts zurück. Optional kann eine maximale Länge angegeben werden.
*
* @param int|null $maxlength Die maximale Zeichenanzahl, falls angegeben.
* @return string Der gekürzte oder vollständige Inhalt des Blogposts.
*/
public function getContent($maxlength = null): string {
return $maxlength
? mb_strimwidth($this->content, 0, $maxlength, "...")
: $this->content;
}
/**
* Gibt den Namen des Autors zurück.
*
* @return string Der Name des Autors.
*/
public function getAuthor(): string {
return $this->author;
}
/**
* Gibt das Veröffentlichungsdatum und die Uhrzeit formatiert zurück.
*
* @return string Das Datum und die Uhrzeit im Format `d.m.Y H:i:s`.
*/
public function getDateTime(): string {
return date('d.m.Y H:i:s', $this->stamp);
}
}

47
app/Entity/User.php Normal file
View File

@ -0,0 +1,47 @@
<?php
namespace Blog\Entity;
/**
* Repräsentiert einen Benutzer mit ID, Benutzernamen und Passwort.
*/
class User {
/**
* Erstellt eine neue User-Instanz.
*
* @param int $id Die eindeutige ID des Benutzers.
* @param string $username Der Benutzername.
* @param string $password Das Passwort des Benutzers.
*/
public function __construct(
private int $id,
private string $username,
private string $password
) {}
/**
* Gibt die ID des Benutzers zurück.
*
* @return int Die eindeutige ID des Benutzers.
*/
public function getId(): int {
return $this->id;
}
/**
* Gibt den Benutzernamen zurück.
*
* @return string Der Benutzername.
*/
public function getUsername(): string {
return $this->username;
}
/**
* Gibt das Passwort des Benutzers zurück.
*
* @return string Das Passwort.
*/
public function getPassword(): string {
return $this->password;
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace Blog\Entity;
class Post {
public function __construct(
private int $id,
private string $title,
private string $content,
private string $author,
private int $stamp
) {}
public function getId() {
return $this->id;
}
public function getTitle() {
return $this->title;
}
public function getContent($maxlength = null) {
return $maxlength ? mb_strimwidth($this->content, 0, $maxlength, "...") : $this->content;
}
public function getAuthor() {
return $this->author;
}
public function getDateTime() {
return $this->stamp;
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace Blog\Entity;
class User {
private $id;
private $username;
private $password;
public function __construct($id, $username, $password) {
$this->id = $id;
$this->username = $username;
$this->password = $password;
}
public function getId() {
return $this->id;
}
public function getUsername() {
return $this->username;
}
public function getPassword() {
return $this->password;
}
}

106
app/Http/Request.php Normal file
View File

@ -0,0 +1,106 @@
<?php
namespace Blog\Http;
/**
* Stellt eine HTTP-Request-Objektklasse bereit.
*
* Kapselt HTTP-Methoden, Pfad, POST-Daten, Header und Rohdatenzugriff.
*/
class Request {
private string $method;
private string $path;
private array $postData;
/**
* Konstruktor für das Request-Objekt.
*
* @param string $method HTTP-Methode (z.B. GET, POST)
* @param string $uri URI der Anfrage
* @param array $postData Optional: POST-Daten (Standard: $_POST)
*/
public function __construct(string $method, string $uri, array $postData = []) {
$this->method = strtoupper($method);
$this->path = parse_url($uri, PHP_URL_PATH) ?? '/';
$this->postData = $postData ?: $_POST;
}
/**
* Gibt die HTTP-Methode zurück.
*
* @return string
*/
public function getMethod(): string {
return $this->method;
}
/**
* Gibt den Pfad der Anfrage zurück.
*
* @return string
*/
public function getPath(): string {
return $this->path;
}
/**
* Gibt einen POST-Wert anhand des Schlüssels zurück.
*
* @param string $key Schlüssel im POST-Array
* @param mixed $default Optionaler Standardwert
* @return mixed
*/
public function getPost(string $key, $default = null): mixed {
return $this->postData[$key] ?? $default;
}
/**
* Gibt alle POST-Daten als Array zurück.
*
* @return array
*/
public function allPost(): array {
return $this->postData;
}
/**
* Gibt einen Query-Parameter anhand des Schlüssels zurück.
*
* @param string $key Schlüssel im Query-String
* @param mixed $default Optionaler Standardwert
* @return mixed
*/
public function getQuery(string $key, $default = null): mixed {
$query = [];
parse_str(parse_url($this->path, PHP_URL_QUERY) ?? '', $query);
return $query[$key] ?? $default;
}
/**
* Gibt einen HTTP-Header zurück.
*
* @param string $key Header-Name (z.B. 'Content-Type')
* @return string|null
*/
public function getHeader(string $key): ?string {
$headerKey = 'HTTP_' . strtoupper(str_replace('-', '_', $key));
return $_SERVER[$headerKey] ?? null;
}
/**
* Gibt den rohen Anfrage-Body zurück.
*
* @return string
*/
public function getRawInput(): string {
return file_get_contents('php://input');
}
/**
* Gibt den Anfrage-Body als assoziatives Array zurück (JSON).
*
* @return array
*/
public function getJson(): array {
return json_decode($this->getRawInput(), true) ?? [];
}
}

95
app/Http/Response.php Normal file
View File

@ -0,0 +1,95 @@
<?php
namespace Blog\Http;
/**
* Stellt eine HTTP-Response-Objektklasse bereit.
*
* Kapselt Statuscode, Header, Body und Methoden zum Senden von Antworten.
*/
class Response {
private int $status = 200;
private array $headers = [];
private string $body = "";
/**
* Setzt den HTTP-Statuscode.
*
* @param int $status HTTP-Statuscode
* @return self
*/
public function setStatus(int $status): self {
$this->status = $status;
return $this;
}
/**
* Fügt einen HTTP-Header hinzu.
*
* @param string $key Header-Name
* @param string $val Header-Wert
* @return self
*/
public function addHeader(string $key, string $val): self {
$this->headers[$key] = $val;
return $this;
}
/**
* Gibt das Response-Objekt zurück (für Method Chaining).
*
* @return self
*/
public function getBody(): self {
return $this;
}
/**
* Fügt dem Body Inhalt hinzu.
*
* @param string $content Inhalt, der angehängt wird
* @return self
*/
public function write(string $content): self {
$this->body .= $content;
return $this;
}
/**
* Sendet die HTTP-Antwort an den Client.
*
* @return void
*/
public function send(): void {
http_response_code($this->status);
foreach($this->headers as $key => $val)
header("{$key}: {$val}");
echo $this->body;
}
/**
* Sendet eine JSON-Antwort.
*
* @param array $data Zu sendende Daten
* @param int $status Optionaler HTTP-Statuscode (Standard: 200)
* @return self
*/
public function json(array $data, int $status = 200): self {
$this->setStatus($status);
header("Content-Type: application/json");
$this->body = json_encode($data);
return $this;
}
/**
* Führt eine HTTP-Weiterleitung durch.
*
* @param string $url Ziel-URL
* @param int $status Optionaler Statuscode (Standard: 302)
* @return void
*/
public function redirect(string $url, int $status = 302): void {
http_response_code($status);
header("Location: {$url}");
exit;
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace Blog\Http;
class Request {
private string $method;
private string $path;
private array $postData;
public function __construct(string $method, string $uri, array $postData = []) {
$this->method = strtoupper($method);
$this->path = parse_url($uri, PHP_URL_PATH) ?? '/';
$this->postData = $postData ?: $_POST;
}
public function getMethod(): string {
return $this->method;
}
public function getPath(): string {
return $this->path;
}
public function getPost(string $key, $default = null): mixed {
return $this->postData[$key] ?? $default;
}
public function allPost(): array {
return $this->postData;
}
public function getQuery(string $key, $default = null): mixed {
$query = [];
parse_str(parse_url($this->path, PHP_URL_QUERY) ?? '', $query);
return $query[$key] ?? $default;
}
public function getHeader(string $key): ?string {
$headerKey = 'HTTP_' . strtoupper(str_replace('-', '_', $key));
return $_SERVER[$headerKey] ?? null;
}
public function getRawInput(): string {
return file_get_contents('php://input');
}
public function getJson(): array {
return json_decode($this->getRawInput(), true) ?? [];
}
}

View File

@ -1,47 +0,0 @@
<?php
namespace Blog\Http;
class Response {
private int $status = 200;
private array $headers = [];
private string $body = "";
public function setStatus(int $status): self {
$this->status = $status;
return $this;
}
public function addHeader(string $key, string $val): self {
$this->headers[$key] = $val;
return $this;
}
public function getBody(): self {
return $this;
}
public function write(string $content): self {
$this->body .= $content;
return $this;
}
public function send(): void {
http_response_code($this->status);
foreach($this->headers as $key => $val)
header("{$key}: {$val}");
echo $this->body;
}
public function json(array $data, int $status = 200): self {
$this->setStatus($status);
header("Content-Type: application/json");
$this->body = json_encode($data);
return $this;
}
public function redirect(string $url, int $status = 302): void {
http_response_code($status);
header("Location: {$url}");
exit;
}
}

View File

@ -1,11 +1,23 @@
<?php
namespace Blog\Middleware;
use Blog\Middleware\middlewareInterface;
use Blog\Http\request;
use Blog\Http\response;
use Blog\Middleware\MiddlewareInterface;
use Blog\Http\Request;
use Blog\Http\Response;
/**
* Middleware zur Authentifizierung und CSRF-Prüfung.
*
* Prüft, ob ein Benutzer eingeloggt ist und ob ein gültiges CSRF-Token vorliegt.
*/
class AuthMiddleware implements MiddlewareInterface {
/**
* Führt die Authentifizierungs- und CSRF-Prüfung durch.
*
* @param Request $request Das aktuelle Request-Objekt
* @param Response $response Das aktuelle Response-Objekt
* @return bool true, wenn die Anfrage fortgesetzt werden darf, sonst false
*/
public function handle(Request $request, Response $response): bool {
if(!isset($_SESSION['user'])) {
$response
@ -28,11 +40,22 @@ class AuthMiddleware implements MiddlewareInterface {
return true;
}
private function validateCSRFToken(Request $request): bool {
/**
* Prüft, ob das CSRF-Token gültig ist.
*
* @param Request $request Das aktuelle Request-Objekt
* @return bool true, wenn das Token gültig ist, sonst false
*/
public static function validateCSRFToken(Request $request): bool {
$token = $request->getPost('_csrf_token') ?? '';
return hash_equals($_SESSION['_csrf_token'] ?? '', $token);
}
/**
* Generiert und gibt ein CSRF-Token zurück.
*
* @return string Das generierte oder vorhandene CSRF-Token
*/
public static function generateCSRFToken(): string {
if(!isset($_SESSION['_csrf_token'])) {
$_SESSION['_csrf_token'] = bin2hex(random_bytes(32));

View File

@ -0,0 +1,21 @@
<?php
namespace Blog\Middleware;
use Blog\Http\Request;
use Blog\Http\Response;
/**
* Interface für Middleware-Komponenten.
*
* Definiert eine Methode zur Bearbeitung von HTTP-Anfragen und -Antworten.
*/
interface MiddlewareInterface {
/**
* Bearbeitet die eingehende Anfrage und Antwort.
*
* @param Request $request Das aktuelle Request-Objekt
* @param Response $response Das aktuelle Response-Objekt
* @return bool true, wenn die Anfrage fortgesetzt werden darf, sonst false
*/
public function handle(Request $request, Response $response): bool;
}

View File

@ -1,9 +0,0 @@
<?php
namespace Blog\Middleware;
use Blog\Http\request;
use Blog\Http\response;
interface MiddlewareInterface {
public function handle(Request $request, Response $response): bool;
}

117
app/Model/PostModel.php Normal file
View File

@ -0,0 +1,117 @@
<?php
namespace Blog\Model;
use Blog\Entity\Post;
use Blog\Database\Database;
use Exception;
class PostModel {
private $db;
public function __construct() {
$this->db = Database::getConnection();
}
/**
* Gibt alle Blogposts als Array von Post-Objekten zurück.
*
* @return Post[] Array mit allen Blogposts.
* @throws Exception Wenn keine Einträge vorhanden sind.
*/
public function getPosts(): array {
$posts = [];
$query = $this->db->query(<<<SQL
SELECT p.id, u.username, p.title, p.content, p.stamp
FROM posts p
JOIN users u ON u.id = p.author_id
ORDER BY p.id DESC
SQL);
if(!$query->rowCount())
throw new Exception("no entries available");
while($row = $query->fetchObject()) {
$posts[] = new Post($row->id, $row->title, $row->content, $row->username, $row->stamp);
}
return $posts;
}
/**
* Gibt einen einzelnen Blogpost anhand der ID zurück.
*
* @param int $id Die ID des Blogposts.
* @return Post|null Das Post-Objekt oder null, falls nicht gefunden.
* @throws Exception Wenn kein Eintrag gefunden wurde.
*/
public function getPost($id): ?Post {
$stmt = $this->db->prepare(<<<SQL
SELECT p.id, u.username, p.title, p.content, p.stamp
FROM posts p
JOIN users u ON u.id = p.author_id
WHERE p.id = :id
LIMIT 1
SQL);
$stmt->bindParam(":id", $id, $this->db::PARAM_INT);
$stmt->execute();
if(!$stmt->rowCount())
throw new Exception("no entry found");
$row = $stmt->fetchObject();
return new Post($row->id, $row->title, $row->content, $row->username, $row->stamp);
}
/**
* Aktualisiert den Inhalt eines Blogposts.
*
* @param int $id Die ID des Blogposts.
* @param string $content Der neue Inhalt.
* @return bool Erfolg der Aktualisierung.
*/
public function updatePostContent($id, $content) {
$stmt = $this->db->prepare(<<<SQL
UPDATE posts
SET content = :content
WHERE id = :id
SQL);
$stmt->bindValue(':content', $content, \PDO::PARAM_STR);
$stmt->bindValue(':id', $id, \PDO::PARAM_INT);
return $stmt->execute();
}
/**
* Legt einen neuen Blogpost an.
*
* @param string $title Der Titel des Blogposts.
* @param string $content Der Inhalt des Blogposts.
* @param int $authorId Die ID des Autors.
* @return bool Erfolg des Einfügens.
*/
public function createPost($title, $content, $authorId) {
$stmt = $this->db->prepare(<<<SQL
INSERT INTO posts (title, content, author_id, stamp)
VALUES
(:title, :content, :author_id, :stamp)
SQL);
$stmt->bindValue(':title', $title, \PDO::PARAM_STR);
$stmt->bindValue(':content', $content, \PDO::PARAM_STR);
$stmt->bindValue(':author_id', $authorId, \PDO::PARAM_INT);
$stmt->bindValue(':stamp', time(), \PDO::PARAM_INT);
return $stmt->execute();
}
/**
* Löscht einen Blogpost anhand der ID.
*
* @param int $id Die ID des Blogposts.
* @return bool Erfolg des Löschens.
*/
public function deletePost($id) {
$stmt = $this->db->prepare(<<<SQL
DELETE FROM posts
WHERE id = :id
SQL);
$stmt->bindValue(':id', $id, \PDO::PARAM_INT);
return $stmt->execute();
}
}

46
app/Model/UserModel.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace Blog\Model;
use Blog\Entity\User;
use Blog\Database\Database;
use PDO;
/**
* Modellklasse für Benutzer-bezogene Datenbankoperationen.
*/
class UserModel {
/**
* @var PDO Die Datenbankverbindung.
*/
private $db;
/**
* Konstruktor. Stellt die Datenbankverbindung her.
*/
public function __construct() {
$this->db = Database::getConnection();
}
/**
* Holt einen Benutzer anhand des Benutzernamens.
*
* @param string $username Der Benutzername.
* @return User|null Das User-Objekt oder null, falls nicht gefunden.
*/
public function getUserByUsername(string $username):?User {
$stmt = $this->db->prepare(<<<SQL
SELECT id, username, password
FROM users
WHERE username = :username
LIMIT 1
SQL);
$stmt->bindParam(':username', $username);
$stmt->execute();
$row = $stmt->fetchObject();
if(!$row)
return null;
return new User($row->id, $row->username, $row->password);
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace Blog\Model;
use Blog\Entity\post;
use Blog\Database\database;
use Exception;
class PostModel {
private $db;
public function __construct() {
$this->db = Database::getConnection();
}
public function getPosts(): array {
$posts = [];
$query = $this->db->query(<<<SQL
SELECT p.id, u.username, p.title, p.content, p.stamp
FROM posts p
JOIN users u ON u.id = p.author_id
SQL);
if(!$query->rowCount())
throw new Exception("no entries available");
while($row = $query->fetchObject()) {
$posts[] = new Post($row->id, $row->title, $row->content, $row->username, $row->stamp);
}
return $posts;
}
public function getPost($id): ?Post {
$query = $this->db->prepare(<<<SQL
SELECT p.id, u.username, p.title, p.content, p.stamp
FROM posts p
JOIN users u ON u.id = p.author_id
WHERE p.id = :id
LIMIT 1
SQL);
$query->bindParam(":id", $id, $this->db::PARAM_INT);
$query->execute();
if(!$query->rowCount())
throw new Exception("no entry found");
$row = $query->fetchObject();
return new Post($row->id, $row->title, $row->content, $row->username, $row->stamp);
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace Blog\Model;
use Blog\Entity\user;
use Blog\Database\database;
use PDO;
class UserModel {
private $db;
public function __construct() {
$this->db = Database::getConnection();
}
public function getUserByUsername(string $username):?User {
$query = $this->db->prepare(<<<SQL
SELECT id, username, password
FROM users
WHERE username = :username
LIMIT 1
SQL);
$query->bindParam(':username', $username);
$query->execute();
$row = $query->fetchObject();
if(!$row)
return null;
return new User($row->id, $row->username, $row->password);
}
}

156
app/Template/Twig.php Normal file
View File

@ -0,0 +1,156 @@
<?php
namespace Blog\Template;
use Exception;
/**
* Einfache Template-Engine zur Verarbeitung von Twig-ähnlichen Templates.
*/
class Twig {
/**
* @var array Enthält die erkannten Template-Blöcke.
*/
private array $blocks = [];
/**
* @var string Pfad zum Template-Verzeichnis.
*/
private string $viewsPath;
/**
* @var array Globale Variablen, die in allen Templates verfügbar sind.
*/
private array $globals = [];
/**
* Konstruktor.
*
* @param string $viewsPath Pfad zum Template-Verzeichnis.
*/
public function __construct($viewsPath) {
$this->viewsPath = rtrim($viewsPath, '/') . '/';
}
/**
* Setzt globale Variablen für alle Templates.
*
* @param array $globals Assoziatives Array mit globalen Variablen.
*/
public function setGlobals(array $globals) {
$this->globals = $globals;
}
/**
* Rendert ein Template mit den übergebenen Daten.
*
* @param string $file Name der Template-Datei (ohne Pfad).
* @param array $data Assoziatives Array mit Variablen für das Template.
* @return string Gerenderter HTML-Code.
* @throws Exception Wenn das Template nicht gefunden oder ein Fehler auftritt.
*/
public function render($file, $data = []): string {
$data = array_merge($this->globals, $data);
try {
$code = $this->includeFiles($file);
$code = $this->compileCode($code);
extract($data, EXTR_SKIP);
ob_start();
eval('?>' . $code);
return ob_get_clean();
} catch(Exception $e) {
throw new Exception("Error rendering template: " . $e->getMessage());
}
}
/**
* Wandelt den Template-Code in ausführbaren PHP-Code um.
*
* @param string $code Der Template-Code.
* @return string Kompilierter PHP-Code.
*/
private function compileCode($code): string {
$code = $this->compileBlock($code);
$code = $this->compileYield($code);
$code = $this->compileEchos($code);
$code = $this->compilePHP($code);
return $code;
}
/**
* Fügt eingebundene oder erweiterte Templates ein.
*
* @param string $file Name der Template-Datei.
* @return string Template-Code mit eingebundenen Dateien.
* @throws Exception Wenn die Datei nicht gefunden wird.
*/
private function includeFiles($file): string {
$filePath = $this->viewsPath . preg_replace("/\.twig$/", "", $file) . ".twig";
if(!file_exists($filePath)) {
throw new Exception("View file not found: {$filePath}");
}
$code = file_get_contents($filePath);
preg_match_all('/{% ?(extends|include) ?\'?(.*?)\'? ?%}/i', $code, $matches, PREG_SET_ORDER);
foreach($matches as $match) {
$includedCode = $this->includeFiles($match[2]);
$code = str_replace($match[0], $includedCode, $code);
}
return preg_replace('/{% ?(extends|include) ?\'?(.*?)\'? ?%}/i', '', $code);
}
/**
* Wandelt {% ... %} in PHP-Code um.
*
* @param string $code Der Template-Code.
* @return string PHP-Code.
*/
private function compilePHP($code): string {
return preg_replace('~\{%\s*(.+?)\s*%}~is', '<?php $1 ?>', $code);
}
/**
* Wandelt {{ ... }} in PHP-Echo-Ausgaben um.
*
* @param string $code Der Template-Code.
* @return string PHP-Code mit Echo-Ausgaben.
*/
private function compileEchos($code): string {
$code = preg_replace('~\{\{\{\s*(.+?)\s*\}\}\}~is', '<?=htmlspecialchars($1, ENT_QUOTES, "UTF-8")?>', $code);
return preg_replace('~\{\{\s*(.+?)\s*\}\}~is', '<?=$1?>', $code);
}
/**
* Sammelt und entfernt Block-Definitionen aus dem Template.
*
* @param string $code Der Template-Code.
* @return string Template-Code ohne Block-Definitionen.
*/
private function compileBlock($code): string {
preg_match_all('/{% ?block ?(.*?) ?%}(.*?){% ?endblock ?%}/is', $code, $matches, PREG_SET_ORDER);
foreach($matches as $match) {
if(!isset($this->blocks[$match[1]])) {
$this->blocks[$match[1]] = '';
}
$this->blocks[$match[1]] = str_replace('@parent', $this->blocks[$match[1]], $match[2]);
$code = str_replace($match[0], '', $code);
}
return $code;
}
/**
* Ersetzt yield-Platzhalter durch Block-Inhalte.
*
* @param string $code Der Template-Code.
* @return string Template-Code mit ersetzten Yields.
*/
private function compileYield($code): string {
foreach($this->blocks as $block => $value) {
$code = preg_replace('/{% ?yield ?' . $block . ' ?%}/', $value, $code);
}
return preg_replace('/{% ?yield ?(.*?) ?%}/i', '', $code);
}
}

View File

@ -1,88 +0,0 @@
<?php
namespace Blog\Template;
use Exception;
class Twig {
private array $blocks = [];
private string $viewsPath;
public function __construct($viewsPath) {
$this->viewsPath = rtrim($viewsPath, '/') . '/';
}
public function render($file, $data = []): string {
try {
$code = $this->includeFiles($file);
$code = $this->compileCode($code);
extract($data, EXTR_SKIP);
ob_start();
eval('?>' . $code);
return ob_get_clean();
} catch(Exception $e) {
throw new Expection("Error rendering template: " . $e->getMessage());
}
}
private function compileCode($code): string {
$code = $this->compileBlock($code);
$code = $this->compileYield($code);
$code = $this->compileEchos($code);
$code = $this->compilePHP($code);
return $code;
}
private function includeFiles($file): string {
$filePath = $this->viewsPath . preg_replace("/\.twig$/", "", $file) . ".twig";
if(!file_exists($filePath)) {
throw new Exception("View file not found: {$filePath}");
}
$code = file_get_contents($filePath);
preg_match_all('/{% ?(extends|include) ?\'?(.*?)\'? ?%}/i', $code, $matches, PREG_SET_ORDER);
foreach($matches as $match) {
$includedCode = $this->includeFiles($match[2]);
$code = str_replace($match[0], $includedCode, $code);
}
return preg_replace('/{% ?(extends|include) ?\'?(.*?)\'? ?%}/i', '', $code);
}
private function compilePHP($code): string {
return preg_replace('~\{%\s*(.+?)\s*%}~is', '<?php $1 ?>', $code);
}
private function compileEchos($code): string {
$code = preg_replace('~\{\{\s*(.+?)\s*\}\}~is', '<?=$1?>', $code);
return preg_replace('~\{\{\{\s*(.+?)\s*\}\}\}~is', '<?=htmlspecialchars($1, ENT_QUOTES, "UTF-8")?>', $code);
}
private function compileEscapedEchos($code):string {
return preg_replace('~\{{{\s*(.+?)\s*}}}~is', '<?=htmlentities($1, ENT_QUOTES, "UTF-8")?>', $code);
}
/*
*
*
*/
private function compileBlock($code): string {
preg_match_all('/{% ?block ?(.*?) ?%}(.*?){% ?endblock ?%}/is', $code, $matches, PREG_SET_ORDER);
foreach($matches as $match) {
if(!isset($this->blocks[$match[1]])) {
$this->blocks[$match[1]] = '';
}
$this->blocks[$match[1]] = str_replace('@parent', $this->blocks[$match[1]], $match[2]);
$code = str_replace($match[0], '', $code);
}
return $code;
}
private function compileYield($code): string {
foreach($this->blocks as $block => $value) {
$code = preg_replace('/{% ?yield ?' . $block . ' ?%}/', $value, $code);
}
return preg_replace('/{% ?yield ?(.*?) ?%}/i', '', $code);
}
}

View File

@ -1,16 +1,36 @@
<?php
namespace Blog\Utils;
use Blog\Model\userModel;
use Blog\Model\UserModel;
use Exception;
/**
* Hilfsklasse für Authentifizierung und Session-Management.
*/
class AuthHelper {
/**
* @var UserModel|null
*/
private static ?UserModel $userModel = null;
/**
* Initialisiert den AuthHelper mit einem UserModel.
*
* @param UserModel $userModel
* @return void
*/
public static function initialize(UserModel $userModel): void {
self::$userModel = $userModel;
}
/**
* Versucht, einen Benutzer mit Benutzername und Passwort einzuloggen.
*
* @param string $username
* @param string $password
* @return bool True bei Erfolg, sonst false.
* @throws Exception Wenn das UserModel nicht initialisiert wurde.
*/
public static function login(string $username, string $password): bool {
if(!self::$userModel)
throw new Exception("AuthHelper was not initialized. Please pass UserModel.");
@ -28,15 +48,30 @@ class AuthHelper {
return true;
}
/**
* Loggt den aktuellen Benutzer aus und zerstört die Session.
*
* @return void
*/
public static function logout(): void {
session_unset();
session_destroy();
}
/**
* Prüft, ob ein Benutzer eingeloggt ist.
*
* @return bool True, wenn ein Benutzer eingeloggt ist, sonst false.
*/
public static function isLoggedIn(): bool {
return isset($_SESSION['user']);
}
/**
* Gibt die aktuellen Benutzerdaten aus der Session zurück.
*
* @return array|null Benutzerdaten oder null, falls nicht eingeloggt.
*/
public static function getCurrentUser(): ?array {
return $_SESSION['user'] ?? null;
}

View File

@ -0,0 +1,42 @@
# Projektabschlussbericht Webframework in PHP
## 1. Projektübersicht
Das Ziel des Projekts war die Entwicklung eines kleinen Webframeworks in PHP, auf dessen Basis ein einfacher Blog mit Benutzeranmeldung realisiert wurde. Das Projekt wurde gemäß den im Pflichtenheft definierten Anforderungen umgesetzt.
## 2. Projektverlauf
Das Projekt wurde in vier Meilensteine unterteilt:
1. **Entwurf des Frameworks**
2. **Implementierung der Kernfunktionen**
3. **Entwicklung des Blog-Systems**
4. **Testen und Dokumentation**
Alle Meilensteine wurden im vorgesehenen Zeitrahmen abgeschlossen. Auftretende Probleme konnten zeitnah gelöst werden.
## 3. Zielerreichung
- **Funktionale Anforderungen:**
Alle im Pflichtenheft definierten funktionalen Anforderungen wurden vollständig umgesetzt. Das Framework unterstützt Routing, MVC, Datenbankanbindung (PDO) und ein Template-System. Der Blog bietet Benutzeranmeldung, Beitragsverwaltung und ein responsives Design.
- **Nicht-funktionale Anforderungen:**
Sicherheitsmaßnahmen wie Passwort-Hashing und CSRF-Schutz wurden implementiert. Die Performance entspricht den Erwartungen, und die Dokumentation ist vollständig.
## 4. Projektergebnisse
- Fertiges Webframework in PHP
- Funktionsfähiges Blog-System
- Ausführliche Dokumentation und Installationsanleitung
- Quellcode im Repository verfügbar
## 5. Abweichungen und Erfahrungen
Im Projektverlauf gab es keine wesentlichen Abweichungen vom ursprünglichen Plan. Kleinere Anpassungen wurden zeitnah umgesetzt. Die gewählte Architektur hat sich als flexibel und wartbar erwiesen.
## 6. Fazit und Ausblick
Das Projekt wurde erfolgreich abgeschlossen. Das Framework kann als Basis für weitere Webanwendungen genutzt und bei Bedarf erweitert werden. Für die Zukunft sind optionale Erweiterungen wie eine Kommentarfunktion, Benutzerregistrierung oder Medienverwaltung denkbar.
---
**Datum:** 20.06.2025

116
documents/lastenheft.md Normal file
View File

@ -0,0 +1,116 @@
# Lastenheft Blogsystem
## 1. Zielbestimmung
Das zu entwickelnde System ist ein webbasiertes Blogsystem, das es autorisierten Benutzern ermöglicht, Blogbeiträge zu erstellen, zu bearbeiten und zu löschen. Besucher können die Beiträge lesen. Das System soll einfach bedienbar, sicher und erweiterbar sein.
## 2. Produkteinsatz
- **Zielgruppe:** Autoren (Lehrkräfte, Schüler, Redaktion), Leser (Öffentlichkeit)
- **Betriebssystem:** Linux-Server
- **Zugriff:** Webbrowser (Desktop und mobil)
- **Sprache:** Deutsch
## 3. Produktübersicht
Das Blogsystem bietet folgende Kernfunktionen:
- Anzeige einer Übersicht aller Blogposts
- Detailansicht einzelner Blogposts
- Benutzer-Login/-Logout
- Erstellung, Bearbeitung und Löschung von Blogposts (nur für eingeloggte Nutzer)
- Schutz vor CSRF-Angriffen
## 4. Produktfunktionen
- **Blogübersicht:**
Besucher sehen eine Liste aller veröffentlichten Beiträge mit Titel, Vorschau, Datum und Autor.
- **Detailansicht:**
Besucher können einzelne Beiträge vollständig lesen.
- **Benutzerverwaltung:**
Nutzer können sich einloggen und ausloggen.
- **Beitragserstellung:**
Eingeloggte Nutzer können neue Beiträge verfassen.
- **Beitragsbearbeitung:**
Eingeloggte Nutzer können bestehende Beiträge bearbeiten.
- **Beitragslöschung:**
Eingeloggte Nutzer können Beiträge löschen.
- **Sicherheit:**
CSRF-Schutz bei Formularen, Passwort-Hashing, Session-Management.
## 5. Nicht-funktionale Anforderungen
- **Benutzerfreundlichkeit:**
Intuitive Bedienung, responsive Design.
- **Performance:**
Schnelle Ladezeiten, auch bei vielen Beiträgen.
- **Sicherheit:**
Schutz vor gängigen Webangriffen (CSRF, XSS, SQL-Injection).
- **Wartbarkeit:**
Klare Code-Struktur, Dokumentation, Erweiterbarkeit.
## 6. Schnittstellen
- **Datenbank:**
MySQL/MariaDB für Speicherung von Nutzern und Beiträgen.
- **Webserver:**
Apache/Nginx mit PHP-Unterstützung.
## 7. Lieferumfang
- Quellcode des Blogsystems
- Installationsanleitung
- Datenbankschema
- Kurzanleitung für Nutzer
## 8. Abgrenzung
- Keine Kommentarfunktion
- Keine Medienverwaltung (Bilder, Videos)
- Keine Benutzerregistrierung über die Oberfläche
## 9. Installationsanleitung (Kurzfassung)
1. **Voraussetzungen:**
- Linux-Server mit Apache oder Nginx
- PHP (mindestens Version 7.4)
- MySQL oder MariaDB
- Git (optional)
2. **Quellcode bereitstellen:**
- Repository klonen oder Quellcode entpacken:
`git clone <REPOSITORY-URL>`
- Dateien ins Webverzeichnis kopieren.
3. **Datenbank einrichten:**
- Neue Datenbank und Benutzer anlegen.
- Das mitgelieferte SQL-Schema importieren:
`mysql -u <user> -p <datenbankname> < schema.sql`
4. **Konfiguration:**
- Zugangsdaten zur Datenbank in der Konfigurationsdatei eintragen (`.env`).
5. **Berechtigungen setzen:**
- Schreibrechte für Upload- oder Cache-Verzeichnisse vergeben (falls benötigt).
6. **Webserver neu laden:**
- Apache/Nginx neu starten:
`sudo systemctl reload apache2` oder `sudo systemctl reload nginx`
## 10. Kurzanleitung für Nutzer
- **Anmelden:**
Über die Login-Seite mit Benutzername und Passwort anmelden.
- **Blogbeiträge lesen:**
Die Startseite zeigt eine Übersicht aller veröffentlichten Beiträge. Ein Klick auf den Titel öffnet die Detailansicht.
- **Neuen Beitrag erstellen:**
Nach dem Login auf „Neuer Beitrag“ klicken, Titel und Inhalt eingeben und speichern.
- **Beitrag bearbeiten/löschen:**
Eigene Beiträge können nach dem Login bearbeitet oder gelöscht werden.
- **Abmelden:**
Über den „Logout“-Button oben rechts abmelden.
---

131
documents/pflichtenheft.md Normal file
View File

@ -0,0 +1,131 @@
# Pflichtenheft Webframework in PHP
## 1. Einleitung
### 1.1 Projektbeschreibung
Ziel des Projekts ist die Entwicklung eines kleinen Webframeworks in PHP. Auf diesem Framework soll ein einfacher Blog mit Benutzeranmeldung implementiert werden.
### 1.2 Zielgruppe
Die Zielgruppe sind Entwickler, die ein leichtgewichtiges Framework für kleine Webanwendungen suchen, sowie Endbenutzer, die den Blog nutzen möchten.
## 2. Anforderungen
### 2.1 Funktionale Anforderungen
#### 2.1.1 Webframework
- **Routing:** Implementierung eines einfachen Routingsystems, das URL-Anfragen an die entsprechenden Controller weiterleitet.
- **MVC-Architektur:** Das Framework soll nach dem Model-View-Controller (MVC) Muster aufgebaut sein.
- **Datenbankanbindung:** Bereitstellung einer einfachen Datenbankanbindung (mit PDO).
- **Template-System:** Ein einfaches Template-System zur Trennung von Logik und Präsentation.
#### 2.1.2 Blog
- **Benutzeranmeldung:** Implementierung eines Login-Systems für registrierte Benutzer.
- **Beitragsverwaltung:** Benutzer sollen Blogbeiträge erstellen, bearbeiten und löschen können.
- **Responsive Design:** Der Blog soll auf verschiedenen Geräten (Desktop, Tablet, Smartphone) gut aussehen.
### 2.2 Nicht-funktionale Anforderungen
- **Sicherheit:** Implementierung von Sicherheitsmaßnahmen (z.B. Passwort-Hashing, CSRF-Schutz).
- **Performance:** Das Framework und der Blog sollen schnell und ressourcenschonend sein.
- **Dokumentation:** Ausführliche Dokumentation des Frameworks und des Blogs.
## 3. Technische Anforderungen
### 3.1 Programmiersprache
- PHP (Version 8.0 oder höher)
### 3.2 Datenbank
- MySQL
### 3.3 Webserver
- Apache oder Nginx
### 3.4 Entwicklungsumgebung
- IDE (Visual Studio Code)
- Composer für die Paketverwaltung
## 4. Zeitplan
### 4.1 Meilensteine
- **Meilenstein 1:** Entwurf des Frameworks (2 Wochen)
- **Meilenstein 2:** Implementierung der Kernfunktionen des Frameworks (4 Wochen)
- **Meilenstein 3:** Entwicklung des Blog-Systems (4 Wochen)
- **Meilenstein 4:** Testen und Dokumentation (2 Wochen)
## 5. Abnahmebedingungen
- Das Projekt gilt als erfolgreich abgeschlossen, wenn alle funktionalen und nicht-funktionalen Anforderungen erfüllt sind und die Software in einer Testumgebung fehlerfrei läuft.
## 6. Projekt-Struktur-Plan
1. **Projektmanagement**
- Planung und Organisation
- Meilenstein- und Zeitplan
- Dokumentation
2. **Framework-Entwicklung**
- Routing-System
- MVC-Architektur (Model, View, Controller)
- Datenbankanbindung (PDO)
- Template-System
3. **Blog-Entwicklung**
- Benutzeranmeldung (Login/Logout)
- Beitragsverwaltung (Erstellen, Bearbeiten, Löschen)
- Responsive Design
4. **Sicherheit**
- Passwort-Hashing
- CSRF-Schutz
- Validierung und Fehlerbehandlung
5. **Testen**
- Funktionstests für Blog-Funktionen
- Manuelle Tests (Usability, Responsivität)
6. **Deployment**
- Einrichtung der Serverumgebung
- Installation und Konfiguration
- Dokumentation für Installation und Nutzung
7. **Abschluss**
- Abnahme durch Auftraggeber
- Abschlussdokumentation
- Projektübergabe
## 7. Anhang
- Glossar der verwendeten Begriffe
- Referenzen zu verwendeten Technologien und Frameworks
## Glossar der verwendeten Begriffe
- **Routing:** Mechanismus zur Zuordnung von URLs zu bestimmten Funktionen oder Controllern in einer Webanwendung.
- **MVC (Model-View-Controller):** Architekturmuster zur Trennung von Datenmodell, Benutzeroberfläche und Steuerungslogik.
- **PDO (PHP Data Objects):** Erweiterung für PHP zur objektorientierten Datenbankanbindung.
- **Template-System:** System zur Trennung von Anwendungslogik und Darstellung (HTML).
- **CSRF (Cross-Site Request Forgery):** Angriffsmethode, bei der unerwünschte Aktionen im Namen eines angemeldeten Nutzers ausgeführt werden.
- **Passwort-Hashing:** Verfahren zur sicheren Speicherung von Passwörtern durch Umwandlung in einen nicht rückrechenbaren Wert.
- **Composer:** Paketverwaltungstool für PHP.
- **Responsive Design:** Gestaltung von Webseiten, die sich an verschiedene Bildschirmgrößen und Geräte anpasst.
## Referenzen zu verwendeten Technologien und Frameworks
- [PHP](https://www.php.net/) Programmiersprache für die Serverlogik
- [MySQL](https://www.mysql.com/) Relationale Datenbank
- [PDO (PHP Data Objects)](https://www.php.net/manual/de/book.pdo.php) Datenbankzugriff in PHP
- [Composer](https://getcomposer.org/) Paketverwaltung für PHP
- [Apache HTTP Server](https://httpd.apache.org/) Webserver
- [Nginx](https://nginx.org/) Webserver
- [Visual Studio Code](https://code.visualstudio.com/) Entwicklungsumgebung
- [Bootstrap](https://getbootstrap.com/) Framework für responsives Design

39
documents/schema.sql Normal file
View File

@ -0,0 +1,39 @@
-- Adminer 4.17.1 MySQL 11.7.2-MariaDB dump
SET NAMES utf8;
SET time_zone = '+00:00';
SET foreign_key_checks = 0;
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
SET NAMES utf8mb4;
CREATE DATABASE `blog` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */;
USE `blog`;
DROP TABLE IF EXISTS `posts`;
CREATE TABLE `posts` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`author_id` int(20) NOT NULL,
`title` varchar(255) NOT NULL,
`content` text NOT NULL,
`stamp` int(10) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `posts` (`id`, `author_id`, `title`, `content`, `stamp`) VALUES
(1, 1, 'Testeintrag', 'Bacon ipsum dolor amet cupim ball tip alcatra, meatloaf pork belly porchetta tongue pancetta. Cupim beef picanha salami meatball burgdoggen porchetta chuck shankle ground round doner ham hock tenderloin pastrami drumstick. Tongue pork belly tenderloin, cow shankle pork chop venison beef ribs flank bresaola salami kielbasa picanha brisket. Meatball meatloaf ham hock, rump filet mignon alcatra boudin jowl. Pancetta turducken biltong boudin, brisket landjaeger ham shank pork chop turkey andouille meatloaf sausage. Frankfurter tenderloin salami kevin chuck meatloaf kielbasa pork loin spare ribs picanha chicken pancetta short loin ground round swine. Strip steak alcatra doner boudin, pork bresaola kielbasa ham hock meatloaf venison leberkas tongue bacon ground round.\r\n\r\nTri-tip pig brisket chicken shank flank pork chop prosciutto pork. Buffalo short ribs t-bone shankle ham hock. Landjaeger brisket kielbasa fatback, t-bone salami venison boudin jowl. Brisket strip steak meatball, frankfurter shankle rump picanha cow doner.\r\n\r\nShort loin cow pig, filet mignon swine turkey burgdoggen jerky pork. Strip steak bacon jowl spare ribs tongue turkey. Pig t-bone pork belly, hamburger drumstick sausage ham biltong meatball shank tenderloin. Corned beef porchetta andouille, capicola cow pancetta burgdoggen hamburger meatloaf leberkas. Tongue shank drumstick ribeye spare ribs swine pork sausage kevin beef ribs chuck. Tongue salami jerky tenderloin.\r\n\r\nPork chop pastrami chuck buffalo chislic ham hock. Cow ground round meatball fatback shank pork pastrami brisket landjaeger strip steak. Picanha chicken jerky, pancetta cupim biltong tenderloin kevin turkey swine sausage. T-bone ground round filet mignon leberkas tongue. Beef biltong capicola short ribs beef ribs tri-tip pork doner chicken meatloaf. Bacon kevin jerky sirloin, salami venison shoulder picanha prosciutto boudin. Ham hock capicola t-bone drumstick leberkas kevin alcatra doner hamburger cupim pork chop, salami ham.\r\n\r\nChislic cow drumstick, ribeye pancetta fatback sirloin flank alcatra andouille chicken tenderloin cupim ground round buffalo. Biltong sausage ribeye chicken cow chuck short ribs turkey. Shoulder sausage shankle tongue t-bone. Buffalo biltong short ribs beef ribs doner shankle. Strip steak beef ribs turducken pastrami bresaola jowl ball tip tenderloin ham hock boudin ham hamburger pork loin turkey. Short ribs flank beef prosciutto, meatball meatloaf buffalo short loin beef ribs pork loin.\r\nDoes your lorem ipsum text long for something a little meatier? Give our generator a try… its tasty!', 1744353753),
(2, 1, 'Test', 'test test', 1750398912);
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`login` varchar(255) NOT NULL,
`password` varchar(167) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `users` (`id`, `username`, `login`, `password`) VALUES
(1, 'Stefan', 'stefan', '$argon2id$v=19$m=65536,t=4,p=1$WC5NMjdFM2J3RllJYmlyQg$Rr44G1o045aqhQHcrgysyLoK5l0XWrL3PH3gT7jcEYc');
-- 2025-06-20 07:36:23

3
gendoc.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
./vendor/bin/phpdoc run -d ./app -d ./routes -d ./views -t ./public/docs

View File

@ -19,9 +19,10 @@ body {
border-bottom: 1px solid #333;
}
.post a {
color: #bb86fc;
a {
color: #6d6fd6;
text-decoration: none;
font-weight: bold;
}
.post a:hover {

View File

@ -14,20 +14,28 @@ if(empty($_SESSION['token']))
require_once __DIR__ . "/../vendor/autoload.php";
use Blog\Core\router;
use Blog\Core\container;
use Blog\Template\twig;
use Blog\Utils\authHelper;
use Blog\Core\Router;
use Blog\Core\Container;
use Blog\Template\Twig;
use Blog\Utils\AuthHelper;
AuthHelper::initialize(new Blog\Model\userModel());
AuthHelper::initialize(new Blog\Model\UserModel());
$router = new Router();
$container = new Container();
$container->set('twig', fn() => new Twig(__DIR__ . "/../views"));
$container->set('postModel', fn() => new Blog\Model\PostModel());
$container->set('userModel', fn() => new Blog\Model\UserModel());
$twig = new Twig(__DIR__ . "/../views");
$twig->setGlobals([
'isLoggedIn' => Blog\Utils\AuthHelper::isLoggedIn(),
'csrf' => Blog\Middleware\AuthMiddleware::generateCSRFToken()
]);
$twig = $container->set('twig', fn() => $twig);
require_once __DIR__ . "/../routes/web.php";
$router->dispatch(
new Blog\Http\request($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']),
new Blog\Http\response
new Blog\Http\Request($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']),
new Blog\Http\Response
);

View File

@ -1,11 +1,25 @@
<?php
use Blog\Core\router;
use Blog\Core\container;
use Blog\Utils\authHelper;
use Blog\Http\request;
use Blog\Http\response;
use Blog\Middleware\authMiddleware;
use Blog\Core\Router;
use Blog\Core\Container;
use Blog\Utils\AuthHelper;
use Blog\Http\Request;
use Blog\Http\Response;
use Blog\Middleware\AuthMiddleware;
/**
* Registriert alle HTTP-Routen für den Blog.
*
* @var Router $router Die Router-Instanz für die Anwendung.
*/
/**
* Startseite mit einer Liste der Blogposts.
*
* @route GET /
* @param Request $req Die HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @return Response Die Antwort mit gerendertem HTML-Inhalt.
*/
$router->addRoute('GET', '/', function(Request $req, Response $res) use($container) {
$twig = $container->get('twig');
$postModel = $container->get('postModel');
@ -19,6 +33,64 @@ $router->addRoute('GET', '/', function(Request $req, Response $res) use($contain
return $res;
});
/**
* Zeigt das Formular zum Erstellen eines neuen Blogposts (nur für eingeloggte Nutzer).
*
* @route GET /post/new
* @param Request $req Die HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @return Response Die Antwort mit gerendertem HTML-Inhalt.
*/
$router->addRoute('GET', '/post/new', function(Request $req, Response $res) use($container) {
if(!AuthHelper::isLoggedIn()) {
return $res->setStatus(403)->getBody()->write("403 - Nicht erlaubt.");
}
$twig = $container->get('twig');
$res->getBody()->write(
$twig->render("post_new")
);
return $res;
});
/**
* Verarbeitet das Formular zum Erstellen eines neuen Blogposts.
*
* @route POST /post/new
* @param Request $req Die HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @return Response Die Antwort mit Umleitung oder Fehlerstatus.
*/
$router->addRoute('POST', '/post/new', function(Request $req, Response $res) use($container) {
if(!AuthHelper::isLoggedIn()) {
return $res->setStatus(403)->getBody()->write("403 - Nicht erlaubt.");
}
if(!authMiddleware::validateCSRFToken($req)) {
return $res->setStatus(419)->getBody()->write("419 - Ungültiger CSRF-Token.");
}
$title = $req->getPost('title');
$content = $req->getPost('content');
$authorId = $_SESSION['user']['id'] ?? null;
if(!$title || !$content || !$authorId) {
return $res->setStatus(400)->getBody()->write("400 - Fehlende Felder.");
}
$postModel = $container->get('postModel');
$postModel->createPost($title, $content, $authorId);
return $res->redirect('/');
});
/**
* Zeigt einen einzelnen Blogpost basierend auf der ID.
*
* @route GET /post/{id}
* @param Request $req Die HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @param string $id Die ID des Blogposts.
* @return Response Die Antwort mit gerendertem Blogpost.
*/
$router->addRoute('GET', '/post/{id}', function(Request $req, Response $res, $id) use($container) {
$twig = $container->get('twig');
$postModel = $container->get('postModel');
@ -34,27 +106,101 @@ $router->addRoute('GET', '/post/{id}', function(Request $req, Response $res, $id
return $res;
});
/**
* Speichert Änderungen an einem Blogpost (nur für eingeloggte Nutzer).
*
* @route POST /post/{id}/edit
* @param Request $req Die HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @param string $id Die ID des Blogposts.
* @return Response Die Antwort mit Umleitung oder Fehlerstatus.
*/
$router->addRoute('POST', '/post/{id}/edit', function(Request $req, Response $res, $id) use($container) {
if(!AuthHelper::isLoggedIn()) {
return $res->setStatus(403)->getBody()->write("403 - Nicht erlaubt.");
}
if (!authMiddleware::validateCSRFToken($req)) {
return $res->setStatus(419)->getBody()->write("419 - Ungültiger CSRF-Token.");
}
$content = $req->getPost('content');
$postModel = $container->get('postModel');
$post = $postModel->getPost($id);
if (!$post) {
return $res->setStatus(404)->getBody()->write("404 - Post nicht gefunden.");
}
$postModel->updatePostContent($id, $content);
return $res->redirect('/post/' . $id);
});
/**
* Löscht einen Blogpost basierend auf der ID (nur für eingeloggte Nutzer).
*
* @route POST /post/delete/{id}
* @param Request $req Die HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @param string $id Die ID des Blogposts.
* @return Response Die Antwort mit Umleitung oder Fehlerstatus.
*/
$router->addRoute('POST', '/post/delete/{id}', function(Request $req, Response $res, $id) use($container) {
if(!AuthHelper::isLoggedIn()) {
return $res->setStatus(403)->getBody()->write("403 - Nicht erlaubt.");
}
if (!authMiddleware::validateCSRFToken($req)) {
return $res->setStatus(419)->getBody()->write("419 - Ungültiger CSRF-Token.");
}
$postModel = $container->get('postModel');
$post = $postModel->getPost($id);
if (!$post) {
return $res->setStatus(404)->getBody()->write("404 - Post nicht gefunden.");
}
$postModel->deletePost($id);
return $res->redirect('/');
});
/**
* Zeigt die Login-Seite.
*
* @route GET /login
* @param Request $req Die HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @return Response Die Antwort mit gerendertem Loginformular.
*/
$router->addRoute('GET', '/login', function(Request $req, Response $res) use($container) {
$twig = $container->get('twig');
$res->getBody()->write(
$twig->render("login", [
"csrf" => authMiddleware::generateCSRFToken()
])
$twig->render("login")
);
return $res;
});
/**
* Verarbeitet die Login-Anfrage und authentifiziert den Nutzer.
*
* @route POST /login
* @param Request $req Die HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @return Response Die Antwort mit Umleitung oder Fehlerstatus.
*/
$router->addRoute('POST', '/login', function(Request $req, Response $res) use($container) {
$twig = $container->get('twig');
$userModel = $container->get('userModel');
$username = $req->getPost('username');
$password = $req->getPost('password');
$csrfToken = $req->getPost('_csrf_token');
if(!authMiddleware::validateCSRFToken($csrfToken)) {
if(!authMiddleware::validateCSRFToken($req)) {
return $res
->setStatus(419)
->getBody()
@ -73,6 +219,14 @@ $router->addRoute('POST', '/login', function(Request $req, Response $res) use($c
return $res->redirect('/');
});
/**
* Führt den Logout des Nutzers aus.
*
* @route GET /logout
* @param Request $req Die HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @return Response Die Antwort mit Umleitung zur Startseite.
*/
$router->addRoute('GET', '/logout', function(Request $req, Response $res) use($container) {
AuthHelper::logout();
return $res->redirect('/');

View File

@ -9,7 +9,7 @@
<div class="post">
<a href="/post/{{ $post->getId() }}">{{ $post->getTitle() }}</a>
<p>{{ $post->getContent(50) }}</p>
<p>Datum: 2025-03-04</p>
<p>Datum: {{ $post->getDateTime() }}</p>
<p>Autor: {{ $post->getAuthor() }}</p>
</div>
{% endforeach; %}

View File

@ -6,7 +6,30 @@
<div id="overview">
<h1>{{ $post->getTitle() }}</h1>
<section class="post-view">
<p>{{ $post->getContent() }}</p>
{% if ($isLoggedIn): %}
<form id="editForm" method="post" action="/post/{{ $post->getId() }}/edit" style="display:none;">
<textarea name="content" rows="10" cols="80">{{ $post->getContent() }}</textarea>
<br>
<button type="submit">Speichern</button>
<button type="button" onclick="toggleEdit(false)">Abbrechen</button>
<input type="hidden" name="_csrf_token" value="{{ $csrf }}">
</form>
<p id="postContent">{{ $post->getContent() }}</p>
<button id="editBtn" onclick="toggleEdit(true)">Bearbeiten</button>
<form action="/post/delete/{{ $post->getId() }}" method="post" onsubmit="return confirm('Beitrag wirklich löschen?');">
<input type="hidden" name="_csrf_token" value="{{ $csrf }}">
<button type="submit">Löschen</button>
</form>
<script>
function toggleEdit(edit) {
document.getElementById('editForm').style.display = edit ? 'block' : 'none';
document.getElementById('postContent').style.display = edit ? 'none' : 'block';
document.getElementById('editBtn').style.display = edit ? 'none' : 'inline';
}
</script>
{% else: %}
<p>{{ $post->getContent() }}</p>
{% endif; %}
</section>
<a href="/">back</a>
</div>

View File

@ -8,7 +8,13 @@
</head>
<body>
<div class="container" id="blog">
<a href="/">Home</a> - <a href="/login">Login</a>
<a href="/">Home</a> -
{% if (@$isLoggedIn): %}
<a href="/post/new">neuer Post</a> -
<a href="/logout">Logout</a>
{% else: %}
<a href="/login">Login</a>
{% endif; %}
<hr>
{% yield content %}
</div>

16
views/post_new.twig Normal file
View File

@ -0,0 +1,16 @@
{% extends layout %}
{% block title %}Neuen Post anlegen{% endblock %}
{% block content %}
<h1>Neuen Blogpost anlegen</h1>
<form method="post" action="/post/new">
<label for="title">Titel:</label><br>
<input type="text" id="title" name="title" required><br><br>
<label for="content">Inhalt:</label><br>
<textarea id="content" name="content" rows="10" cols="80" required></textarea><br><br>
<input type="hidden" name="_csrf_token" value="{{ $csrf }}">
<button type="submit">Anlegen</button>
</form>
<a href="/">Zurück</a>
{% endblock %}