first commit

This commit is contained in:
2025-03-07 10:04:42 +00:00
commit f3a71e8d12
23 changed files with 732 additions and 0 deletions

17
app/Core/container.php Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}
}

View 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'];
}
}

View 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
View 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
View 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
View 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
View 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;
}
}