first commit
This commit is contained in:
commit
f3a71e8d12
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
vendor
|
||||||
|
.env
|
||||||
|
public/adminer
|
17
app/Core/container.php
Normal file
17
app/Core/container.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
namespace Blog\Core;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
48
app/Core/router.php
Normal file
48
app/Core/router.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
namespace Blog\Core;
|
||||||
|
|
||||||
|
use Blog\Middleware\middlewareInterface;
|
||||||
|
use Blog\Http\request;
|
||||||
|
use Blog\Http\response;
|
||||||
|
|
||||||
|
class Router {
|
||||||
|
private array $routes = [];
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dispatch(Request $req, Response $res): void {
|
||||||
|
$method = $req->getMethod();
|
||||||
|
$uri = $req->getPath();
|
||||||
|
|
||||||
|
foreach($this->routes as $route) {
|
||||||
|
if($route['method'] === $method && preg_match("~^" . $route['path'] . "$~", $uri, $matches)) {
|
||||||
|
array_shift($matches);
|
||||||
|
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
|
||||||
|
if(!$this->handleMiddlewares($route['middlewares'], $req, $res))
|
||||||
|
return;
|
||||||
|
call_user_func_array($route['handler'], array_merge([ $req, $res ], $params));
|
||||||
|
$res->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$res
|
||||||
|
->setStatus(404)
|
||||||
|
->getBody()
|
||||||
|
->write("404 - Not Found")
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleMiddlewares(array $middlewares, Request $req, Response $res): bool {
|
||||||
|
foreach($middlewares as $middleware) {
|
||||||
|
$middlewareInstance = is_string($middleware) ? new $middleware() : $middleware;
|
||||||
|
if($middlewareInstance instanceof MiddlewareInterface)
|
||||||
|
if(!$middlewareInstance->handle($req, $res))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
29
app/Database/database.php
Normal file
29
app/Database/database.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
namespace Blog\Database;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
private static ?PDO $pdo = null;
|
||||||
|
|
||||||
|
public static function getConnection(): PDO {
|
||||||
|
if(self::$pdo === null) {
|
||||||
|
$config = parse_ini_file(__DIR__ . "/../../.env");
|
||||||
|
try {
|
||||||
|
self::$pdo = new PDO(
|
||||||
|
"mysql:host={$config['DB_HOST']};dbname={$config['DB_NAME']};charset=utf8mb4",
|
||||||
|
$config['DB_USER'],
|
||||||
|
$config['DB_PASS'], [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ,
|
||||||
|
PDO::ATTR_PERSISTENT => true
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch(PDOException $e) {
|
||||||
|
die("Datenbankverbindung fehlgeschlagen: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self::$pdo;
|
||||||
|
}
|
||||||
|
}
|
32
app/Entity/post.php
Normal file
32
app/Entity/post.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
26
app/Entity/user.php
Normal file
26
app/Entity/user.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
49
app/Http/request.php
Normal file
49
app/Http/request.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?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) ?? [];
|
||||||
|
}
|
||||||
|
}
|
47
app/Http/response.php
Normal file
47
app/Http/response.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
42
app/Middleware/authMiddleware.php
Normal file
42
app/Middleware/authMiddleware.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
namespace Blog\Middleware;
|
||||||
|
|
||||||
|
use Blog\Middleware\middlewareInterface;
|
||||||
|
use Blog\Http\request;
|
||||||
|
use Blog\Http\response;
|
||||||
|
|
||||||
|
class AuthMiddleware implements MiddlewareInterface {
|
||||||
|
public function handle(Request $request, Response $response): bool {
|
||||||
|
if(!isset($_SESSION['user'])) {
|
||||||
|
$response
|
||||||
|
->setStatus(403)
|
||||||
|
->getBody()
|
||||||
|
->write("403 - Forbidden")
|
||||||
|
->send();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->getMethod() !== 'GET' && !$this->validateCSRFToken($request)) {
|
||||||
|
$response
|
||||||
|
->setStatus(419)
|
||||||
|
->getBody()
|
||||||
|
->write("419 - Session expired or invalid CSRF token.")
|
||||||
|
->send();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateCSRFToken(Request $request): bool {
|
||||||
|
$token = $request->getPost('_csrf_token') ?? '';
|
||||||
|
return hash_equals($_SESSION['_csrf_token'] ?? '', $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateCSRFToken(): string {
|
||||||
|
if(!isset($_SESSION['_csrf_token'])) {
|
||||||
|
$_SESSION['_csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
return $_SESSION['_csrf_token'];
|
||||||
|
}
|
||||||
|
}
|
9
app/Middleware/middlewareInterface.php
Normal file
9
app/Middleware/middlewareInterface.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
namespace Blog\Middleware;
|
||||||
|
|
||||||
|
use Blog\Http\request;
|
||||||
|
use Blog\Http\response;
|
||||||
|
|
||||||
|
interface MiddlewareInterface {
|
||||||
|
public function handle(Request $request, Response $response): bool;
|
||||||
|
}
|
49
app/Model/postModel.php
Normal file
49
app/Model/postModel.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
31
app/Model/userModel.php
Normal file
31
app/Model/userModel.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
85
app/Template/twig.php
Normal file
85
app/Template/twig.php
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
namespace Blog\Template;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class Twig {
|
||||||
|
private $blocks = [];
|
||||||
|
private $viewsPath;
|
||||||
|
private $debugMode;
|
||||||
|
|
||||||
|
public function __construct($viewsPath, $debugMode = false) {
|
||||||
|
$this->viewsPath = rtrim($viewsPath, '/') . '/';
|
||||||
|
$this->debugMode = $debugMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render($file, $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) {
|
||||||
|
if($this->debugMode)
|
||||||
|
echo "Error rendering template: " . $e->getMessage();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function compileCode($code) {
|
||||||
|
$code = $this->compileBlock($code);
|
||||||
|
$code = $this->compileYield($code);
|
||||||
|
$code = $this->compileEchos($code);
|
||||||
|
$code = $this->compilePHP($code);
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function includeFiles($file) {
|
||||||
|
$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) {
|
||||||
|
return preg_replace('~\{%\s*(.+?)\s*%}~is', '<?php $1 ?>', $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function compileEchos($code) {
|
||||||
|
$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) {
|
||||||
|
return preg_replace('~\{{{\s*(.+?)\s*}}}~is', '<?=htmlentities($1, ENT_QUOTES, "UTF-8")?>', $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function compileBlock($code) {
|
||||||
|
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) {
|
||||||
|
foreach($this->blocks as $block => $value)
|
||||||
|
$code = preg_replace('/{% ?yield ?' . $block . ' ?%}/', $value, $code);
|
||||||
|
return preg_replace('/{% ?yield ?(.*?) ?%}/i', '', $code);
|
||||||
|
}
|
||||||
|
}
|
43
app/Utils/authHelper.php
Normal file
43
app/Utils/authHelper.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
namespace Blog\Utils;
|
||||||
|
|
||||||
|
use Blog\Model\userModel;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class AuthHelper {
|
||||||
|
private static ?UserModel $userModel = null;
|
||||||
|
|
||||||
|
public static function initialize(UserModel $userModel): void {
|
||||||
|
self::$userModel = $userModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function login(string $username, string $password): bool {
|
||||||
|
if(!self::$userModel)
|
||||||
|
throw new Exception("AuthHelper was not initialized. Please pass UserModel.");
|
||||||
|
|
||||||
|
$user = self::$userModel->getUserByUsername($username);
|
||||||
|
if(!$user || !password_verify($password, $user->getPassword())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['user'] = [
|
||||||
|
'id' => $user->getId(),
|
||||||
|
'username' => $user->getUsername()
|
||||||
|
];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function logout(): void {
|
||||||
|
session_unset();
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isLoggedIn(): bool {
|
||||||
|
return isset($_SESSION['user']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCurrentUser(): ?array {
|
||||||
|
return $_SESSION['user'] ?? null;
|
||||||
|
}
|
||||||
|
}
|
7
composer.json
Normal file
7
composer.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Blog\\": "app/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
public/css/blog.css
Normal file
47
public/css/blog.css
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
body {
|
||||||
|
background-color: #121212;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post a {
|
||||||
|
color: #bb86fc;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back {
|
||||||
|
display: block;
|
||||||
|
margin: 20px 0;
|
||||||
|
color: #bb86fc;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
34
public/index.php
Normal file
34
public/index.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set("display_errors", 1);
|
||||||
|
|
||||||
|
session_start([
|
||||||
|
'cookie_lifetime' => 86400,
|
||||||
|
'cookie_secure' => true,
|
||||||
|
'cookie_httponly' => true,
|
||||||
|
'cookie_samesite' => 'Strict'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if(empty($_SESSION['token']))
|
||||||
|
$_SESSION['token'] = bin2hex(random_bytes(32));
|
||||||
|
|
||||||
|
require_once __DIR__ . "/../vendor/autoload.php";
|
||||||
|
|
||||||
|
use Blog\Core\router;
|
||||||
|
use Blog\Core\container;
|
||||||
|
use Blog\Template\twig;
|
||||||
|
use Blog\Utils\authHelper;
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
require_once __DIR__ . "/../routes/web.php";
|
||||||
|
|
||||||
|
$router->dispatch(
|
||||||
|
new Blog\Http\request($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']),
|
||||||
|
new Blog\Http\response
|
||||||
|
);
|
78
routes/web.php
Normal file
78
routes/web.php
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
use Blog\Core\router;
|
||||||
|
use Blog\Core\container;
|
||||||
|
use Blog\Utils\authHelper;
|
||||||
|
use Blog\Http\request;
|
||||||
|
use Blog\Http\response;
|
||||||
|
|
||||||
|
$router->addRoute('GET', '/', function(Request $req, Response $res) use($container) {
|
||||||
|
$twig = $container->get('twig');
|
||||||
|
$postModel = $container->get('postModel');
|
||||||
|
|
||||||
|
$posts = $postModel->getPosts();
|
||||||
|
$res->getBody()->write(
|
||||||
|
$twig->render("blogmain", [
|
||||||
|
"posts" => $posts
|
||||||
|
])
|
||||||
|
);
|
||||||
|
return $res;
|
||||||
|
});
|
||||||
|
|
||||||
|
$router->addRoute('GET', '/post/{id}', function(Request $req, Response $res, $id) use($container) {
|
||||||
|
$twig = $container->get('twig');
|
||||||
|
$postModel = $container->get('postModel');
|
||||||
|
|
||||||
|
$post = $postModel->getPost($id);
|
||||||
|
|
||||||
|
$res->getBody()->write(
|
||||||
|
$twig->render("blogpost", [
|
||||||
|
"post" => $post
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
});
|
||||||
|
|
||||||
|
$router->addRoute('GET', '/login', function(Request $req, Response $res) use($container) {
|
||||||
|
$twig = $container->get('twig');
|
||||||
|
|
||||||
|
$res->getBody()->write(
|
||||||
|
$twig->render("login", [
|
||||||
|
"csrf" => Blog\Middleware\authMiddleware::generateCSRFToken()
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
});
|
||||||
|
|
||||||
|
$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(!Blog\Middleware\authMiddleware::validateCSRFToken($csrfToken)) {
|
||||||
|
return $res
|
||||||
|
->setStatus(419)
|
||||||
|
->getBody()
|
||||||
|
->write("419 - Session abgelaufen oder ungültiger CSRF-Token.")
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!AuthHelper::login($username, $password)) {
|
||||||
|
$res
|
||||||
|
->setStatus(401)
|
||||||
|
->getBody()
|
||||||
|
->write("401 - Unauthorized: Invalid login information.");
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res->redirect('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
$router->addRoute('GET', '/logout', function(Request $req, Response $res) use($container) {
|
||||||
|
AuthHelper::logout();
|
||||||
|
return $res->redirect('/');
|
||||||
|
});
|
17
views/blogmain.twig
Normal file
17
views/blogmain.twig
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends layout %}
|
||||||
|
|
||||||
|
{% block title %}Blog{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="overview">
|
||||||
|
<h1>Blog Overview</h1>
|
||||||
|
{% foreach($posts as $post): %}
|
||||||
|
<div class="post">
|
||||||
|
<a href="/post/{{ $post->getId() }}">{{ $post->getTitle() }}</a>
|
||||||
|
<p>{{ $post->getContent(50) }}</p>
|
||||||
|
<p>Datum: 2025-03-04</p>
|
||||||
|
<p>Autor: Beispielautor</p>
|
||||||
|
</div>
|
||||||
|
{% endforeach; %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
13
views/blogpost.twig
Normal file
13
views/blogpost.twig
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends layout %}
|
||||||
|
|
||||||
|
{% block title %}{{ $post->getTitle() }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="overview">
|
||||||
|
<h1>{{ $post->getTitle() }}</h1>
|
||||||
|
<section class="post-view">
|
||||||
|
<p>{{ $post->getContent() }}</p>
|
||||||
|
</section>
|
||||||
|
<a href="/">back</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
14
views/layout.twig
Normal file
14
views/layout.twig
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{% yield title %}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<base href="https://blog.flumm.io">
|
||||||
|
<link rel="stylesheet" href="./css/blog.css?v={{ filemtime(__dir__ . '/../../public/css/blog.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container" id="blog">
|
||||||
|
{% yield content %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
views/login.twig
Normal file
12
views/login.twig
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends layout %}
|
||||||
|
|
||||||
|
{% block title %}Login{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
<input type="text" name="username" placeholder="Benutzername" required>
|
||||||
|
<input type="password" name="password" placeholder="Passwort" required>
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ $csrf }}">
|
||||||
|
<button type="submit">Einloggen</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user