From 6c2e71dd53579240abd806149c3fcca19499a67c Mon Sep 17 00:00:00 2001 From: Flummi Date: Fri, 20 Jun 2025 07:55:37 +0000 Subject: [PATCH] abschluss --- app/Core/Container.php | 39 ++++ app/Core/{router.php => Router.php} | 36 +++- app/Core/container.php | 19 -- app/Database/{database.php => Database.php} | 12 ++ app/Entity/Post.php | 72 +++++++ app/Entity/User.php | 47 +++++ app/Entity/post.php | 32 ---- app/Entity/user.php | 26 --- app/Http/Request.php | 106 +++++++++++ app/Http/Response.php | 95 ++++++++++ app/Http/request.php | 49 ----- app/Http/response.php | 47 ----- ...{authMiddleware.php => AuthMiddleware.php} | 31 ++- app/Middleware/MiddlewareInterface.php | 21 +++ app/Middleware/middlewareInterface.php | 9 - app/Model/PostModel.php | 117 ++++++++++++ app/Model/UserModel.php | 46 +++++ app/Model/postModel.php | 49 ----- app/Model/userModel.php | 31 --- app/Template/Twig.php | 156 ++++++++++++++++ app/Template/twig.php | 88 --------- app/Utils/{authHelper.php => AuthHelper.php} | 37 +++- documents/abschlussbericht.md | 42 +++++ documents/lastenheft.md | 116 ++++++++++++ documents/pflichtenheft.md | 131 +++++++++++++ documents/schema.sql | 39 ++++ gendoc.sh | 3 + public/css/blog.css | 5 +- public/index.php | 24 ++- routes/web.php | 176 ++++++++++++++++-- views/blogmain.twig | 2 +- views/blogpost.twig | 25 ++- views/layout.twig | 8 +- views/post_new.twig | 16 ++ 34 files changed, 1370 insertions(+), 382 deletions(-) create mode 100644 app/Core/Container.php rename app/Core/{router.php => Router.php} (56%) delete mode 100644 app/Core/container.php rename app/Database/{database.php => Database.php} (69%) create mode 100644 app/Entity/Post.php create mode 100644 app/Entity/User.php delete mode 100644 app/Entity/post.php delete mode 100644 app/Entity/user.php create mode 100644 app/Http/Request.php create mode 100644 app/Http/Response.php delete mode 100644 app/Http/request.php delete mode 100644 app/Http/response.php rename app/Middleware/{authMiddleware.php => AuthMiddleware.php} (51%) create mode 100644 app/Middleware/MiddlewareInterface.php delete mode 100644 app/Middleware/middlewareInterface.php create mode 100644 app/Model/PostModel.php create mode 100644 app/Model/UserModel.php delete mode 100644 app/Model/postModel.php delete mode 100644 app/Model/userModel.php create mode 100644 app/Template/Twig.php delete mode 100644 app/Template/twig.php rename app/Utils/{authHelper.php => AuthHelper.php} (51%) create mode 100644 documents/abschlussbericht.md create mode 100644 documents/lastenheft.md create mode 100644 documents/pflichtenheft.md create mode 100644 documents/schema.sql create mode 100755 gendoc.sh create mode 100644 views/post_new.twig diff --git a/app/Core/Container.php b/app/Core/Container.php new file mode 100644 index 0000000..f3b08e3 --- /dev/null +++ b/app/Core/Container.php @@ -0,0 +1,39 @@ +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); + } +} diff --git a/app/Core/router.php b/app/Core/Router.php similarity index 56% rename from app/Core/router.php rename to app/Core/Router.php index 6c5ecff..1e82708 100644 --- a/app/Core/router.php +++ b/app/Core/Router.php @@ -1,18 +1,40 @@ [^/]+)', $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; diff --git a/app/Core/container.php b/app/Core/container.php deleted file mode 100644 index 479bec5..0000000 --- a/app/Core/container.php +++ /dev/null @@ -1,19 +0,0 @@ -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); - } -} diff --git a/app/Database/database.php b/app/Database/Database.php similarity index 69% rename from app/Database/database.php rename to app/Database/Database.php index 7b22ae6..7f2ead6 100644 --- a/app/Database/database.php +++ b/app/Database/Database.php @@ -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"); diff --git a/app/Entity/Post.php b/app/Entity/Post.php new file mode 100644 index 0000000..9bf5ef9 --- /dev/null +++ b/app/Entity/Post.php @@ -0,0 +1,72 @@ +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); + } +} diff --git a/app/Entity/User.php b/app/Entity/User.php new file mode 100644 index 0000000..e8dcb15 --- /dev/null +++ b/app/Entity/User.php @@ -0,0 +1,47 @@ +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; + } +} diff --git a/app/Entity/post.php b/app/Entity/post.php deleted file mode 100644 index f480597..0000000 --- a/app/Entity/post.php +++ /dev/null @@ -1,32 +0,0 @@ -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; - } -} diff --git a/app/Entity/user.php b/app/Entity/user.php deleted file mode 100644 index a30ba10..0000000 --- a/app/Entity/user.php +++ /dev/null @@ -1,26 +0,0 @@ -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; - } -} diff --git a/app/Http/Request.php b/app/Http/Request.php new file mode 100644 index 0000000..297133f --- /dev/null +++ b/app/Http/Request.php @@ -0,0 +1,106 @@ +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) ?? []; + } +} diff --git a/app/Http/Response.php b/app/Http/Response.php new file mode 100644 index 0000000..21292e6 --- /dev/null +++ b/app/Http/Response.php @@ -0,0 +1,95 @@ +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; + } +} diff --git a/app/Http/request.php b/app/Http/request.php deleted file mode 100644 index 1754417..0000000 --- a/app/Http/request.php +++ /dev/null @@ -1,49 +0,0 @@ -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) ?? []; - } -} diff --git a/app/Http/response.php b/app/Http/response.php deleted file mode 100644 index d91cd2c..0000000 --- a/app/Http/response.php +++ /dev/null @@ -1,47 +0,0 @@ -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; - } -} diff --git a/app/Middleware/authMiddleware.php b/app/Middleware/AuthMiddleware.php similarity index 51% rename from app/Middleware/authMiddleware.php rename to app/Middleware/AuthMiddleware.php index 6c2093d..aabbd44 100644 --- a/app/Middleware/authMiddleware.php +++ b/app/Middleware/AuthMiddleware.php @@ -1,11 +1,23 @@ 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)); diff --git a/app/Middleware/MiddlewareInterface.php b/app/Middleware/MiddlewareInterface.php new file mode 100644 index 0000000..43ca796 --- /dev/null +++ b/app/Middleware/MiddlewareInterface.php @@ -0,0 +1,21 @@ +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(<<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(<<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(<<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(<<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(<<bindValue(':id', $id, \PDO::PARAM_INT); + return $stmt->execute(); + } +} diff --git a/app/Model/UserModel.php b/app/Model/UserModel.php new file mode 100644 index 0000000..472328b --- /dev/null +++ b/app/Model/UserModel.php @@ -0,0 +1,46 @@ +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(<<bindParam(':username', $username); + $stmt->execute(); + + $row = $stmt->fetchObject(); + if(!$row) + return null; + + return new User($row->id, $row->username, $row->password); + } +} diff --git a/app/Model/postModel.php b/app/Model/postModel.php deleted file mode 100644 index c186cd1..0000000 --- a/app/Model/postModel.php +++ /dev/null @@ -1,49 +0,0 @@ -db = Database::getConnection(); - } - - public function getPosts(): array { - $posts = []; - $query = $this->db->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(<<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); - } -} diff --git a/app/Model/userModel.php b/app/Model/userModel.php deleted file mode 100644 index 52e5842..0000000 --- a/app/Model/userModel.php +++ /dev/null @@ -1,31 +0,0 @@ -db = Database::getConnection(); - } - - public function getUserByUsername(string $username):?User { - $query = $this->db->prepare(<<bindParam(':username', $username); - $query->execute(); - - $row = $query->fetchObject(); - if(!$row) - return null; - - return new User($row->id, $row->username, $row->password); - } -} diff --git a/app/Template/Twig.php b/app/Template/Twig.php new file mode 100644 index 0000000..b659faf --- /dev/null +++ b/app/Template/Twig.php @@ -0,0 +1,156 @@ +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', '', $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', '', $code); + return preg_replace('~\{\{\s*(.+?)\s*\}\}~is', '', $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); + } +} diff --git a/app/Template/twig.php b/app/Template/twig.php deleted file mode 100644 index 5c1a1a2..0000000 --- a/app/Template/twig.php +++ /dev/null @@ -1,88 +0,0 @@ -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', '', $code); - } - - private function compileEchos($code): string { - $code = preg_replace('~\{\{\s*(.+?)\s*\}\}~is', '', $code); - return preg_replace('~\{\{\{\s*(.+?)\s*\}\}\}~is', '', $code); - } - - private function compileEscapedEchos($code):string { - return preg_replace('~\{{{\s*(.+?)\s*}}}~is', '', $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); - } -} diff --git a/app/Utils/authHelper.php b/app/Utils/AuthHelper.php similarity index 51% rename from app/Utils/authHelper.php rename to app/Utils/AuthHelper.php index 351159f..30da638 100644 --- a/app/Utils/authHelper.php +++ b/app/Utils/AuthHelper.php @@ -1,16 +1,36 @@ ` + - Dateien ins Webverzeichnis kopieren. + +3. **Datenbank einrichten:** + - Neue Datenbank und Benutzer anlegen. + - Das mitgelieferte SQL-Schema importieren: + `mysql -u -p < 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. + +--- diff --git a/documents/pflichtenheft.md b/documents/pflichtenheft.md new file mode 100644 index 0000000..d91f4a6 --- /dev/null +++ b/documents/pflichtenheft.md @@ -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 diff --git a/documents/schema.sql b/documents/schema.sql new file mode 100644 index 0000000..7814c34 --- /dev/null +++ b/documents/schema.sql @@ -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… it’s 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 diff --git a/gendoc.sh b/gendoc.sh new file mode 100755 index 0000000..2864635 --- /dev/null +++ b/gendoc.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./vendor/bin/phpdoc run -d ./app -d ./routes -d ./views -t ./public/docs diff --git a/public/css/blog.css b/public/css/blog.css index 7daef40..a45d6ee 100644 --- a/public/css/blog.css +++ b/public/css/blog.css @@ -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 { diff --git a/public/index.php b/public/index.php index d416e2d..d4da2b8 100644 --- a/public/index.php +++ b/public/index.php @@ -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 ); diff --git a/routes/web.php b/routes/web.php index 6575aae..d22355b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,11 +1,25 @@ 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('/'); diff --git a/views/blogmain.twig b/views/blogmain.twig index 9cf00c9..d797bdc 100644 --- a/views/blogmain.twig +++ b/views/blogmain.twig @@ -9,7 +9,7 @@
{{ $post->getTitle() }}

{{ $post->getContent(50) }}

-

Datum: 2025-03-04

+

Datum: {{ $post->getDateTime() }}

Autor: {{ $post->getAuthor() }}

{% endforeach; %} diff --git a/views/blogpost.twig b/views/blogpost.twig index 1a2a316..2557a0e 100644 --- a/views/blogpost.twig +++ b/views/blogpost.twig @@ -6,7 +6,30 @@

{{ $post->getTitle() }}

-

{{ $post->getContent() }}

+ {% if ($isLoggedIn): %} + +

{{ $post->getContent() }}

+ +
+ + +
+ + {% else: %} +

{{ $post->getContent() }}

+ {% endif; %}
back
diff --git a/views/layout.twig b/views/layout.twig index 0b65dcd..736848f 100644 --- a/views/layout.twig +++ b/views/layout.twig @@ -8,7 +8,13 @@
- Home - Login + Home - + {% if (@$isLoggedIn): %} + neuer Post - + Logout + {% else: %} + Login + {% endif; %}
{% yield content %}
diff --git a/views/post_new.twig b/views/post_new.twig new file mode 100644 index 0000000..8ad626b --- /dev/null +++ b/views/post_new.twig @@ -0,0 +1,16 @@ +{% extends layout %} + +{% block title %}Neuen Post anlegen{% endblock %} + +{% block content %} +

Neuen Blogpost anlegen

+
+
+

+
+

+ + +
+Zurück +{% endblock %}