Initial habit tracker project
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
class AuthController
|
||||
{
|
||||
public function showLogin(): void
|
||||
{
|
||||
View::render('auth/login');
|
||||
}
|
||||
|
||||
public function login(): void
|
||||
{
|
||||
verify_csrf_token();
|
||||
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || $password === '') {
|
||||
flash('error', 'Bitte gib eine gueltige E-Mail und dein Passwort ein.');
|
||||
redirect('login');
|
||||
}
|
||||
|
||||
$user = User::findByEmail($email);
|
||||
|
||||
if (!$user || !password_verify($password, $user['password_hash'])) {
|
||||
flash('error', 'E-Mail oder Passwort ist nicht korrekt.');
|
||||
redirect('login');
|
||||
}
|
||||
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_id'] = (int) $user['id'];
|
||||
$_SESSION['user_name'] = $user['name'];
|
||||
redirect('dashboard');
|
||||
}
|
||||
|
||||
public function showRegister(): void
|
||||
{
|
||||
View::render('auth/register');
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
verify_csrf_token();
|
||||
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if ($name === '' || !filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($password) < 8) {
|
||||
flash('error', 'Bitte Name, gueltige E-Mail und ein Passwort mit mindestens 8 Zeichen eingeben.');
|
||||
redirect('register');
|
||||
}
|
||||
|
||||
if (User::findByEmail($email)) {
|
||||
flash('error', 'Diese E-Mail ist bereits registriert.');
|
||||
redirect('register');
|
||||
}
|
||||
|
||||
User::create($name, $email, $password);
|
||||
flash('success', 'Registrierung erfolgreich. Du kannst dich jetzt anmelden.');
|
||||
redirect('login');
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
{
|
||||
verify_csrf_token();
|
||||
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
|
||||
}
|
||||
session_destroy();
|
||||
redirect('login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
class HabitController
|
||||
{
|
||||
public function dashboard(): void
|
||||
{
|
||||
require_login();
|
||||
|
||||
$filters = [
|
||||
'category_id' => $_GET['category_id'] ?? '',
|
||||
'status' => $_GET['status'] ?? '',
|
||||
];
|
||||
|
||||
View::render('habits/dashboard', [
|
||||
'habits' => Habit::allForUser(current_user_id(), $filters),
|
||||
'categories' => Category::all(),
|
||||
'filters' => $filters,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
require_login();
|
||||
|
||||
View::render('habits/form', [
|
||||
'habit' => null,
|
||||
'categories' => Category::all(),
|
||||
'errors' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(): void
|
||||
{
|
||||
require_login();
|
||||
verify_csrf_token();
|
||||
|
||||
$data = $this->validatedData();
|
||||
|
||||
if (!empty($data['errors'])) {
|
||||
View::render('habits/form', [
|
||||
'habit' => $_POST,
|
||||
'categories' => Category::all(),
|
||||
'errors' => $data['errors'],
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Habit::create(current_user_id(), $data);
|
||||
flash('success', 'Habit wurde erstellt.');
|
||||
redirect('dashboard');
|
||||
}
|
||||
|
||||
public function edit(): void
|
||||
{
|
||||
require_login();
|
||||
|
||||
$habit = Habit::findForUser((int) ($_GET['id'] ?? 0), current_user_id());
|
||||
|
||||
if (!$habit) {
|
||||
flash('error', 'Habit wurde nicht gefunden.');
|
||||
redirect('dashboard');
|
||||
}
|
||||
|
||||
View::render('habits/form', [
|
||||
'habit' => $habit,
|
||||
'categories' => Category::all(),
|
||||
'errors' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(): void
|
||||
{
|
||||
require_login();
|
||||
verify_csrf_token();
|
||||
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
$data = $this->validatedData();
|
||||
|
||||
if (!empty($data['errors'])) {
|
||||
View::render('habits/form', [
|
||||
'habit' => array_merge($_POST, ['id' => $id]),
|
||||
'categories' => Category::all(),
|
||||
'errors' => $data['errors'],
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Habit::update($id, current_user_id(), $data);
|
||||
flash('success', 'Habit wurde gespeichert.');
|
||||
redirect('dashboard');
|
||||
}
|
||||
|
||||
public function delete(): void
|
||||
{
|
||||
require_login();
|
||||
verify_csrf_token();
|
||||
|
||||
Habit::delete((int) ($_POST['id'] ?? 0), current_user_id());
|
||||
flash('success', 'Habit wurde geloescht.');
|
||||
redirect('dashboard');
|
||||
}
|
||||
|
||||
public function toggle(): void
|
||||
{
|
||||
require_login();
|
||||
verify_csrf_token();
|
||||
|
||||
Habit::toggleToday((int) ($_POST['id'] ?? 0), current_user_id());
|
||||
redirect('dashboard');
|
||||
}
|
||||
|
||||
private function validatedData(): array
|
||||
{
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$categoryId = $_POST['category_id'] ?? '';
|
||||
$targetPerWeek = (int) ($_POST['target_per_week'] ?? 0);
|
||||
$errors = [];
|
||||
|
||||
if ($title === '') {
|
||||
$errors['title'] = 'Bitte gib einen Namen fuer den Habit ein.';
|
||||
}
|
||||
|
||||
if ($targetPerWeek < 1 || $targetPerWeek > 7) {
|
||||
$errors['target_per_week'] = 'Bitte waehle einen Wert zwischen 1 und 7.';
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'category_id' => $categoryId,
|
||||
'target_per_week' => $targetPerWeek,
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
|
||||
class Database
|
||||
{
|
||||
private static ?PDO $connection = null;
|
||||
|
||||
public static function connection(): PDO
|
||||
{
|
||||
if (self::$connection !== null) {
|
||||
return self::$connection;
|
||||
}
|
||||
|
||||
$db = config('database');
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
$db['host'],
|
||||
$db['dbname'],
|
||||
$db['charset']
|
||||
);
|
||||
|
||||
try {
|
||||
self::$connection = new PDO($dsn, $db['user'], $db['password'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
} catch (PDOException $exception) {
|
||||
throw new PDOException('Datenbankverbindung fehlgeschlagen.');
|
||||
}
|
||||
|
||||
return self::$connection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
class View
|
||||
{
|
||||
public static function render(string $view, array $data = []): void
|
||||
{
|
||||
extract($data, EXTR_SKIP);
|
||||
|
||||
$viewPath = __DIR__ . '/../Views/' . $view . '.php';
|
||||
|
||||
require __DIR__ . '/../Views/layout/header.php';
|
||||
require $viewPath;
|
||||
require __DIR__ . '/../Views/layout/footer.php';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
class Category
|
||||
{
|
||||
public static function all(): array
|
||||
{
|
||||
return Database::connection()
|
||||
->query('SELECT id, name, color FROM categories ORDER BY name')
|
||||
->fetchAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
class Habit
|
||||
{
|
||||
public static function allForUser(int $userId, array $filters = []): array
|
||||
{
|
||||
$where = ['h.user_id = :user_id'];
|
||||
$params = ['user_id' => $userId];
|
||||
|
||||
if (!empty($filters['category_id'])) {
|
||||
$where[] = 'h.category_id = :category_id';
|
||||
$params['category_id'] = (int) $filters['category_id'];
|
||||
}
|
||||
|
||||
if (($filters['status'] ?? '') === 'done') {
|
||||
$where[] = 'hl.id IS NOT NULL';
|
||||
} elseif (($filters['status'] ?? '') === 'open') {
|
||||
$where[] = 'hl.id IS NULL';
|
||||
}
|
||||
|
||||
$sql = 'SELECT h.*, c.name AS category_name, c.color AS category_color,
|
||||
CASE WHEN hl.id IS NULL THEN 0 ELSE 1 END AS done_today
|
||||
FROM habits h
|
||||
LEFT JOIN categories c ON c.id = h.category_id
|
||||
LEFT JOIN habit_logs hl
|
||||
ON hl.habit_id = h.id
|
||||
AND hl.completed_on = CURRENT_DATE
|
||||
WHERE ' . implode(' AND ', $where) . '
|
||||
ORDER BY h.created_at DESC';
|
||||
|
||||
$statement = Database::connection()->prepare($sql);
|
||||
$statement->execute($params);
|
||||
|
||||
return $statement->fetchAll();
|
||||
}
|
||||
|
||||
public static function findForUser(int $id, int $userId): ?array
|
||||
{
|
||||
$statement = Database::connection()->prepare(
|
||||
'SELECT * FROM habits WHERE id = :id AND user_id = :user_id LIMIT 1'
|
||||
);
|
||||
$statement->execute(['id' => $id, 'user_id' => $userId]);
|
||||
|
||||
$habit = $statement->fetch();
|
||||
return $habit ?: null;
|
||||
}
|
||||
|
||||
public static function create(int $userId, array $data): bool
|
||||
{
|
||||
$sql = 'INSERT INTO habits (user_id, category_id, title, description, target_per_week)
|
||||
VALUES (:user_id, :category_id, :title, :description, :target_per_week)';
|
||||
|
||||
return Database::connection()->prepare($sql)->execute([
|
||||
'user_id' => $userId,
|
||||
'category_id' => $data['category_id'] ?: null,
|
||||
'title' => $data['title'],
|
||||
'description' => $data['description'],
|
||||
'target_per_week' => (int) $data['target_per_week'],
|
||||
]);
|
||||
}
|
||||
|
||||
public static function update(int $id, int $userId, array $data): bool
|
||||
{
|
||||
$sql = 'UPDATE habits
|
||||
SET category_id = :category_id,
|
||||
title = :title,
|
||||
description = :description,
|
||||
target_per_week = :target_per_week
|
||||
WHERE id = :id AND user_id = :user_id';
|
||||
|
||||
return Database::connection()->prepare($sql)->execute([
|
||||
'id' => $id,
|
||||
'user_id' => $userId,
|
||||
'category_id' => $data['category_id'] ?: null,
|
||||
'title' => $data['title'],
|
||||
'description' => $data['description'],
|
||||
'target_per_week' => (int) $data['target_per_week'],
|
||||
]);
|
||||
}
|
||||
|
||||
public static function delete(int $id, int $userId): bool
|
||||
{
|
||||
$statement = Database::connection()->prepare('DELETE FROM habits WHERE id = :id AND user_id = :user_id');
|
||||
|
||||
return $statement->execute(['id' => $id, 'user_id' => $userId]);
|
||||
}
|
||||
|
||||
public static function toggleToday(int $id, int $userId): bool
|
||||
{
|
||||
$habit = self::findForUser($id, $userId);
|
||||
|
||||
if (!$habit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$db = Database::connection();
|
||||
$check = $db->prepare(
|
||||
'SELECT id FROM habit_logs WHERE habit_id = :habit_id AND completed_on = CURRENT_DATE LIMIT 1'
|
||||
);
|
||||
$check->execute(['habit_id' => $id]);
|
||||
$log = $check->fetch();
|
||||
|
||||
if ($log) {
|
||||
return $db->prepare('DELETE FROM habit_logs WHERE id = :id')->execute(['id' => $log['id']]);
|
||||
}
|
||||
|
||||
$insert = $db->prepare('INSERT INTO habit_logs (habit_id, completed_on) VALUES (:habit_id, CURRENT_DATE)');
|
||||
return $insert->execute(['habit_id' => $id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
class User
|
||||
{
|
||||
public static function create(string $name, string $email, string $password): bool
|
||||
{
|
||||
$sql = 'INSERT INTO users (name, email, password_hash) VALUES (:name, :email, :password_hash)';
|
||||
$statement = Database::connection()->prepare($sql);
|
||||
|
||||
return $statement->execute([
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function findByEmail(string $email): ?array
|
||||
{
|
||||
$statement = Database::connection()->prepare('SELECT * FROM users WHERE email = :email LIMIT 1');
|
||||
$statement->execute(['email' => $email]);
|
||||
|
||||
$user = $statement->fetch();
|
||||
return $user ?: null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<section class="auth-panel">
|
||||
<h1>Anmelden</h1>
|
||||
<form method="post" action="index.php?route=login" class="form">
|
||||
<?= csrf_field() ?>
|
||||
<label for="email">E-Mail</label>
|
||||
<input id="email" name="email" type="email" autocomplete="email" required>
|
||||
|
||||
<label for="password">Passwort</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required>
|
||||
|
||||
<button type="submit">Einloggen</button>
|
||||
</form>
|
||||
<p>Noch kein Konto? <a href="index.php?route=register">Jetzt registrieren</a></p>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<section class="auth-panel">
|
||||
<h1>Registrieren</h1>
|
||||
<form method="post" action="index.php?route=register" class="form">
|
||||
<?= csrf_field() ?>
|
||||
<label for="name">Name</label>
|
||||
<input id="name" name="name" type="text" autocomplete="name" required>
|
||||
|
||||
<label for="email">E-Mail</label>
|
||||
<input id="email" name="email" type="email" autocomplete="email" required>
|
||||
|
||||
<label for="password">Passwort</label>
|
||||
<input id="password" name="password" type="password" minlength="8" autocomplete="new-password" required>
|
||||
<small>Mindestens 8 Zeichen.</small>
|
||||
|
||||
<button type="submit">Konto erstellen</button>
|
||||
</form>
|
||||
<p>Schon registriert? <a href="index.php?route=login">Zum Login</a></p>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<section class="dashboard-head">
|
||||
<div>
|
||||
<p class="eyebrow">Heute</p>
|
||||
<h1>Deine Habits</h1>
|
||||
</div>
|
||||
<a class="button" href="index.php?route=habits/create">Habit erstellen</a>
|
||||
</section>
|
||||
|
||||
<form method="get" action="index.php" class="filters">
|
||||
<input type="hidden" name="route" value="dashboard">
|
||||
|
||||
<label for="category_id">Kategorie</label>
|
||||
<select id="category_id" name="category_id">
|
||||
<option value="">Alle Kategorien</option>
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<option value="<?= (int) $category['id'] ?>" <?= (string) $filters['category_id'] === (string) $category['id'] ? 'selected' : '' ?>>
|
||||
<?= e($category['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<label for="status">Status</label>
|
||||
<select id="status" name="status">
|
||||
<option value="">Alle</option>
|
||||
<option value="open" <?= $filters['status'] === 'open' ? 'selected' : '' ?>>Offen</option>
|
||||
<option value="done" <?= $filters['status'] === 'done' ? 'selected' : '' ?>>Erledigt</option>
|
||||
</select>
|
||||
|
||||
<button type="submit">Filtern</button>
|
||||
</form>
|
||||
|
||||
<?php if (empty($habits)): ?>
|
||||
<p class="empty">Noch keine Habits gefunden. Erstelle deinen ersten Habit.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="habit-list">
|
||||
<?php foreach ($habits as $habit): ?>
|
||||
<article class="habit-card <?= $habit['done_today'] ? 'done' : '' ?>">
|
||||
<div class="habit-main">
|
||||
<span class="category-dot" style="background-color: <?= e($habit['category_color'] ?? '#9ca3af') ?>"></span>
|
||||
<div>
|
||||
<h2><?= e($habit['title']) ?></h2>
|
||||
<p><?= e($habit['description']) ?></p>
|
||||
<small>
|
||||
<?= e($habit['category_name'] ?? 'Ohne Kategorie') ?> |
|
||||
Ziel: <?= (int) $habit['target_per_week'] ?>x pro Woche
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="habit-actions">
|
||||
<form method="post" action="index.php?route=habits/toggle">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $habit['id'] ?>">
|
||||
<button type="submit"><?= $habit['done_today'] ? 'Heute erledigt' : 'Abhaken' ?></button>
|
||||
</form>
|
||||
<a href="index.php?route=habits/edit&id=<?= (int) $habit['id'] ?>">Bearbeiten</a>
|
||||
<form method="post" action="index.php?route=habits/delete" onsubmit="return confirm('Diesen Habit wirklich loeschen?');">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $habit['id'] ?>">
|
||||
<button class="danger" type="submit">Loeschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php $isEdit = !empty($habit['id']); ?>
|
||||
|
||||
<section class="form-panel">
|
||||
<h1><?= $isEdit ? 'Habit bearbeiten' : 'Habit erstellen' ?></h1>
|
||||
|
||||
<form method="post" action="index.php?route=<?= $isEdit ? 'habits/update' : 'habits/store' ?>" class="form">
|
||||
<?= csrf_field() ?>
|
||||
<?php if ($isEdit): ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $habit['id'] ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<label for="title">Name des Habits</label>
|
||||
<input
|
||||
id="title"
|
||||
name="title"
|
||||
type="text"
|
||||
class="<?= !empty($errors['title']) ? 'input-error' : '' ?>"
|
||||
value="<?= e($habit['title'] ?? '') ?>"
|
||||
required
|
||||
aria-describedby="title-error"
|
||||
>
|
||||
<?php if (!empty($errors['title'])): ?>
|
||||
<small id="title-error" class="field-error"><?= e($errors['title']) ?></small>
|
||||
<?php endif; ?>
|
||||
|
||||
<label for="description">Beschreibung <span>optional</span></label>
|
||||
<textarea id="description" name="description" rows="4"><?= e($habit['description'] ?? '') ?></textarea>
|
||||
|
||||
<label for="category_id">Kategorie</label>
|
||||
<select id="category_id" name="category_id">
|
||||
<option value="">Keine Kategorie</option>
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<option value="<?= (int) $category['id'] ?>" <?= (string) ($habit['category_id'] ?? '') === (string) $category['id'] ? 'selected' : '' ?>>
|
||||
<?= e($category['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<label for="target_per_week">Ziel pro Woche</label>
|
||||
<input
|
||||
id="target_per_week"
|
||||
name="target_per_week"
|
||||
type="number"
|
||||
class="<?= !empty($errors['target_per_week']) ? 'input-error' : '' ?>"
|
||||
min="1"
|
||||
max="7"
|
||||
value="<?= e((string) ($habit['target_per_week'] ?? '3')) ?>"
|
||||
required
|
||||
aria-describedby="target-error"
|
||||
>
|
||||
<?php if (!empty($errors['target_per_week'])): ?>
|
||||
<small id="target-error" class="field-error"><?= e($errors['target_per_week']) ?></small>
|
||||
<?php endif; ?>
|
||||
|
||||
<button type="submit"><?= $isEdit ? 'Speichern' : 'Erstellen' ?></button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= e(config('app_name')) ?></title>
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="index.php?route=dashboard">Habit Tracker</a>
|
||||
<nav class="nav">
|
||||
<?php if (current_user_id()): ?>
|
||||
<a href="index.php?route=dashboard">Dashboard</a>
|
||||
<a href="index.php?route=habits/create">Neuer Habit</a>
|
||||
<form method="post" action="index.php?route=logout" class="logout-form">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="link-button">Logout</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<a href="index.php?route=login">Login</a>
|
||||
<a href="index.php?route=register">Registrieren</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="page">
|
||||
<?php if ($message = flash('success')): ?>
|
||||
<p class="notice success"><?= e($message) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($message = flash('error')): ?>
|
||||
<p class="notice error"><?= e($message) ?></p>
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
function config(string $key, $default = null)
|
||||
{
|
||||
static $config = null;
|
||||
|
||||
if ($config === null) {
|
||||
$config = require __DIR__ . '/../config/config.php';
|
||||
}
|
||||
|
||||
$parts = explode('.', $key);
|
||||
$value = $config;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (!is_array($value) || !array_key_exists($part, $value)) {
|
||||
return $default;
|
||||
}
|
||||
$value = $value[$part];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function e(?string $value): string
|
||||
{
|
||||
return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function redirect(string $route): never
|
||||
{
|
||||
header('Location: index.php?route=' . urlencode($route));
|
||||
exit;
|
||||
}
|
||||
|
||||
function current_user_id(): ?int
|
||||
{
|
||||
return $_SESSION['user_id'] ?? null;
|
||||
}
|
||||
|
||||
function require_login(): void
|
||||
{
|
||||
if (!current_user_id()) {
|
||||
redirect('login');
|
||||
}
|
||||
}
|
||||
|
||||
function flash(string $key, ?string $message = null): ?string
|
||||
{
|
||||
if ($message !== null) {
|
||||
$_SESSION['flash'][$key] = $message;
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $_SESSION['flash'][$key] ?? null;
|
||||
unset($_SESSION['flash'][$key]);
|
||||
return $value;
|
||||
}
|
||||
|
||||
function csrf_token(): string
|
||||
{
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
function csrf_field(): string
|
||||
{
|
||||
return '<input type="hidden" name="csrf_token" value="' . e(csrf_token()) . '">';
|
||||
}
|
||||
|
||||
function verify_csrf_token(): void
|
||||
{
|
||||
$token = $_POST['csrf_token'] ?? '';
|
||||
|
||||
if (!is_string($token) || !hash_equals($_SESSION['csrf_token'] ?? '', $token)) {
|
||||
http_response_code(419);
|
||||
echo 'Sicherheitspruefung fehlgeschlagen. Bitte lade die Seite neu.';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user