Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6336442f2e | ||
|
|
9ff26f0f59 |
No files matched your search
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"workbench.colorTheme": "Dark 2026",
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.background": "#162df7ec",
|
||||
"activityBar.foreground": "#000000",
|
||||
"breadcrumb.foreground": "#FFFFFF",
|
||||
},
|
||||
"files.exclude": {
|
||||
"*.git*": true,
|
||||
"WEBROOT/config_backups": true,
|
||||
"WEBROOT/static": true,
|
||||
"WEBROOT/templates": true,
|
||||
},
|
||||
"editor.renderWhitespace": "all",
|
||||
"editor.smoothScrolling": true,
|
||||
"editor.fontSize": 14,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.minimap.enabled": true,
|
||||
"editor.minimap.scale": 2,
|
||||
"editor.minimap.side": "right",
|
||||
"editor.minimap.maxColumn": 100,
|
||||
"editor.minimap.renderCharacters": true,
|
||||
"editor.minimap.showSlider": "always",
|
||||
"files.autoSave": "off",
|
||||
"files.autoSaveDelay": 3000,
|
||||
"window.title": "MVC",
|
||||
"workbench.editor.wrapTabs": true,
|
||||
},
|
||||
"extensions": {
|
||||
"recommendations": [
|
||||
"oderwat.indent-rainbow",
|
||||
"pranaygp.vscode-css-peek",
|
||||
"wayou.vscode-todo-highlight",
|
||||
"mrmlnc.vscode-mustache",
|
||||
"fabiospampinato.vscode-highlight",
|
||||
"junstyle.php-cs-fixer",
|
||||
"mtea.gettext-editor",
|
||||
"neilbrayfield.php-docblocker",
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"mohamed-ali.html-css-class-completion",
|
||||
"rifi2k.format-html-in-php",
|
||||
"mrorz.language-gettext",
|
||||
"voldemortensen.rainbow-tags",
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
RewriteEngine On
|
||||
|
||||
RewriteRule ^(Commands|config_backups|controller|modules)($|/.*) - [F,L,NC]
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
|
||||
|
||||
# -----------------------------------
|
||||
# .php entfernen
|
||||
# -----------------------------------
|
||||
|
||||
# browser requestet datei.php -> redirect zu /datei
|
||||
RewriteCond %{THE_REQUEST} \s/+(.+?)\.php[\s?] [NC]
|
||||
RewriteRule ^ /%1 [R=301,L]
|
||||
|
||||
# /datei -> datei.php
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{DOCUMENT_ROOT}/$1.php -f
|
||||
RewriteRule ^(.+)$ $1.php [L]
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require_once __DIR__ . '/../../src/MVC/autoloader.php';
|
||||
|
||||
use MVC\Controller\ApiController;
|
||||
|
||||
$api = (new ApiController());
|
||||
|
||||
try {
|
||||
echo ($api->get());
|
||||
} catch (\Exception $e) {
|
||||
echo ($e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once __DIR__ . '/src/MVC/autoloader.php';
|
||||
use MVC\Classes\Logger;
|
||||
set_exception_handler([Logger::class, 'logException']);
|
||||
set_error_handler([Logger::class, 'logError']);
|
||||
|
||||
// 1. Determine the service
|
||||
$service = $_GET['service'] ?? null;
|
||||
|
||||
// Fallback to path-based routing if no service parameter is given
|
||||
if ($service === null) {
|
||||
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
$service = trim($uri, '/');
|
||||
if (empty($service)) {
|
||||
$service = 'home';
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$router = new \MVC\Core\Router();
|
||||
|
||||
// Load routes from config
|
||||
require_once __DIR__ . '/src/config/routes.php';
|
||||
|
||||
// 2. Dispatch the service
|
||||
$routeInfo = $router->getRoute($service);
|
||||
$output = $router->dispatch($service);
|
||||
|
||||
// 3. Conditional Layout Rendering
|
||||
if ($routeInfo && !empty($routeInfo['isApi'])) {
|
||||
// API Route: Just echo the output and exit
|
||||
echo $output;
|
||||
exit;
|
||||
}
|
||||
|
||||
$layout = new \MVC\Layout\Layout('main');
|
||||
$layout->setBlock('title', 'MVC Starter');
|
||||
$layout->setBlock('header_title', '🚀 MVC Dashboard');
|
||||
|
||||
$headerActions = '<button class="btn-toggle" onclick="toggleTheme()">🌓 Theme umschalten</button>';
|
||||
if (isset($_SESSION['user_logged_in'])) {
|
||||
$headerActions = '<span style="margin-right: 15px; font-size: 0.9rem;">👤 ' . htmlspecialchars($_SESSION['username']) . '</span>' . '<a href="?service=logout" class="btn-toggle" style="text-decoration: none; background: rgba(255,255,255,0.2); margin-right: 10px;">🚪 Logout</a> ' . $headerActions;
|
||||
} else {
|
||||
$headerActions = '<a href="?service=login" class="btn-toggle" style="text-decoration: none; background: var(--accent); color: white; margin-right: 10px;">🔑 Login</a> ' . $headerActions;
|
||||
}
|
||||
$layout->setBlock('header_actions', $headerActions);
|
||||
|
||||
// Sidebar Block
|
||||
ob_start();
|
||||
// Load navigation from config
|
||||
$navItems = require __DIR__ . '/src/config/navigation.php';
|
||||
?> <nav>
|
||||
<?php foreach ($navItems as $navKey => $item):
|
||||
$isLoggedIn = isset($_SESSION['user_logged_in']);
|
||||
$isAdmin = isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
|
||||
|
||||
// Check visibility based on Router middleware
|
||||
$requiresAuth = $router->hasMiddleware($navKey, 'auth');
|
||||
$requiresAdmin = $router->hasMiddleware($navKey, 'admin');
|
||||
|
||||
if ($requiresAuth && !$isLoggedIn) continue;
|
||||
if ($requiresAdmin && !$isAdmin) continue;
|
||||
?>
|
||||
<a href="?service=<?= $navKey ?>"
|
||||
class="<?= $service === $navKey ? 'active' : '' ?>"><?= $item['label'] ?></a>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if (!isset($_SESSION['user_logged_in'])): ?>
|
||||
<div
|
||||
style="margin-top: 20px; padding: 10px; font-size: 0.8rem; opacity: 0.5; border-top: 1px solid var(--border);">
|
||||
🔒 Melden Sie sich an, um alle Funktionen zu sehen.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php
|
||||
$layout->setBlock('sidebar', ob_get_clean());
|
||||
|
||||
$layout->setBlock('content', $output);
|
||||
echo $layout->render();
|
||||
} catch (\Throwable $e) {
|
||||
Logger::logException($e);
|
||||
http_response_code(500);
|
||||
echo "<h1>Error</h1><p>" . htmlspecialchars($e->getMessage()) . "</p>";
|
||||
if (ini_get('display_errors')) echo "<pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre>";
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[2026-06-24 07:12:40] [SECURITY] Blocked IP attempt: 127.0.0.1
|
||||
@@ -0,0 +1,4 @@
|
||||
[2026-06-24 07:12:40] [CRITICAL] Uncaught Exception: 'Exception' with message 'Service 'favicon.ico' not found.' in /mnt/c/Users/Bruno/Desktop/WSL/DOCKER/MVC/WebTime/WEBROOT/src/MVC/Core/Router.php:33
|
||||
Stack trace:
|
||||
#0 /mnt/c/Users/Bruno/Desktop/WSL/DOCKER/MVC/WebTime/WEBROOT/index.php(28): MVC\Core\Router->dispatch()
|
||||
#1 {main}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Classes;
|
||||
|
||||
class Api {
|
||||
public static function jsonResponse($data, int $statusCode = 200) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code($statusCode);
|
||||
echo json_encode(
|
||||
[
|
||||
'status' => $statusCode === 200 ? 'success' : 'error',
|
||||
'data' => $data,
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
|
||||
);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Classes;
|
||||
|
||||
use MVC\Model\Users;
|
||||
|
||||
class Auth {
|
||||
|
||||
/**
|
||||
* Startet die Session falls noch nicht geschehen
|
||||
*/
|
||||
private static function init() {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Versucht einen Benutzer einzuloggen
|
||||
*/
|
||||
public static function login(string $username, string $password): bool {
|
||||
self::init();
|
||||
$user = Users::getByUsername($username);
|
||||
|
||||
if ($user && password_verify($password, $user['PASSWORD'])) {
|
||||
$_SESSION['user_logged_in'] = true;
|
||||
$_SESSION['user_id'] = $user['ID'];
|
||||
$_SESSION['username'] = $user['USERNAME'];
|
||||
$_SESSION['role'] = $user['ROLE'];
|
||||
$_SESSION['is_admin'] = ($user['ROLE'] === 'admin');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loggt den aktuellen Benutzer aus
|
||||
*/
|
||||
public static function logout() {
|
||||
self::init();
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein Benutzer eingeloggt ist
|
||||
*/
|
||||
public static function check(): bool {
|
||||
self::init();
|
||||
return isset($_SESSION['user_logged_in']) && $_SESSION['user_logged_in'] === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den aktuellen Benutzer-Datensatz zurück
|
||||
*/
|
||||
public static function user() {
|
||||
self::init();
|
||||
if (!self::check()) return null;
|
||||
|
||||
return [
|
||||
'id' => $_SESSION['user_id'],
|
||||
'username' => $_SESSION['username'],
|
||||
'role' => $_SESSION['role'],
|
||||
'is_admin' => $_SESSION['is_admin']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob der Benutzer eine bestimmte Rolle hat
|
||||
*/
|
||||
public static function hasRole(string $role): bool {
|
||||
self::init();
|
||||
return self::check() && $_SESSION['role'] === $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob der Benutzer Admin ist
|
||||
*/
|
||||
public static function isAdmin(): bool {
|
||||
return self::hasRole('admin');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Classes;
|
||||
|
||||
class Client {
|
||||
|
||||
public static function ip() {
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ipList = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
return trim($ipList[0]);
|
||||
} else if (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
return $_SERVER['REMOTE_ADDR'];
|
||||
} else {
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
|
||||
public static function port() {
|
||||
return $_SERVER['REMOTE_PORT'] ?? 'UNKNOWN';
|
||||
}
|
||||
|
||||
public static function country() {
|
||||
$response = file_get_contents('http://ip-api.com/json/{' . self::ip() . '}?fields=country,countryCode');
|
||||
$data = json_decode($response);
|
||||
return $data->country ?? 'UNKNOWN';
|
||||
}
|
||||
|
||||
public static function userAgent() {
|
||||
return $_SERVER['HTTP_USER_AGENT'] ?? 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Classes;
|
||||
|
||||
class Cookie {
|
||||
public function set(string $CookieName, int $CookieValue, int $CookieTime) {
|
||||
setcookie($CookieName, $CookieValue, time()+($CookieTime*60));
|
||||
}
|
||||
|
||||
public function read(string $CookieName) {
|
||||
if (isset($_COOKIE[$CookieName])) {
|
||||
return $_COOKIE[$CookieName];
|
||||
}
|
||||
return 'Cookie not set';
|
||||
}
|
||||
|
||||
public function Delete(string $CookieName) {
|
||||
setcookie($CookieName, "", time()-(3600*24));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Classes;
|
||||
|
||||
class Form {
|
||||
|
||||
/**
|
||||
* Startet ein HTML Formular
|
||||
*/
|
||||
public static function open(string $service, string $method = 'POST', array $options = []): string {
|
||||
$enctype = isset($options['enctype']) ? ' enctype="' . $options['enctype'] . '"' : '';
|
||||
$class = isset($options['class']) ? ' class="' . $options['class'] . '"' : '';
|
||||
$id = isset($options['id']) ? ' id="' . $options['id'] . '"' : '';
|
||||
|
||||
return "<form action=\"?service=$service\" method=\"$method\"$enctype$class$id>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Schließt ein HTML Formular
|
||||
*/
|
||||
public static function close(): string {
|
||||
return "</form>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ein Input-Feld mit Label und Styling
|
||||
*/
|
||||
public static function input(string $name, string $label = '', string $type = 'text', $value = '', array $options = []): string {
|
||||
$placeholder = $options['placeholder'] ?? '';
|
||||
$required = isset($options['required']) && $options['required'] ? 'required' : '';
|
||||
$class = $options['class'] ?? 'form-control';
|
||||
|
||||
$html = "<div class=\"form-group\">";
|
||||
if ($label) {
|
||||
$html .= "<label for=\"$name\">$label</label>";
|
||||
}
|
||||
$html .= "<input type=\"$type\" name=\"$name\" id=\"$name\" value=\"" . htmlspecialchars($value) . "\" class=\"$class\" placeholder=\"$placeholder\" $required>";
|
||||
$html .= "</div>";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Textarea
|
||||
*/
|
||||
public static function textarea(string $name, string $label = '', string $value = '', array $options = []): string {
|
||||
$placeholder = $options['placeholder'] ?? '';
|
||||
$rows = $options['rows'] ?? 3;
|
||||
$class = $options['class'] ?? 'form-control';
|
||||
|
||||
$html = "<div class=\"form-group\">";
|
||||
if ($label) {
|
||||
$html .= "<label for=\"$name\">$label</label>";
|
||||
}
|
||||
$html .= "<textarea name=\"$name\" id=\"$name\" class=\"$class\" rows=\"$rows\" placeholder=\"$placeholder\">" . htmlspecialchars($value) . "</textarea>";
|
||||
$html .= "</div>";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Submit-Button
|
||||
*/
|
||||
public static function submit(string $label = 'Speichern', string $class = 'btn btn-primary'): string {
|
||||
return "<button type=\"submit\" class=\"$class\">$label</button>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Einfache Validierung von Formulardaten
|
||||
*/
|
||||
public static function validate(array $data, array $rules): array {
|
||||
$errors = [];
|
||||
foreach ($rules as $field => $fieldRules) {
|
||||
$value = $data[$field] ?? null;
|
||||
$rulesArray = explode('|', $fieldRules);
|
||||
|
||||
foreach ($rulesArray as $rule) {
|
||||
if ($rule === 'required' && empty($value)) {
|
||||
$errors[$field] = "Das Feld $field ist erforderlich.";
|
||||
}
|
||||
if ($rule === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[$field] = "Bitte geben Sie eine gültige E-Mail Adresse an.";
|
||||
}
|
||||
if (strpos($rule, 'min:') === 0) {
|
||||
$min = (int)substr($rule, 4);
|
||||
if (strlen($value) < $min) {
|
||||
$errors[$field] = "Das Feld $field muss mindestens $min Zeichen lang sein.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Classes;
|
||||
|
||||
class Logger {
|
||||
private static function getLogDir() {
|
||||
return dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'logs';
|
||||
}
|
||||
|
||||
private static $config = null;
|
||||
|
||||
private static function getConfig() {
|
||||
if (self::$config === null) {
|
||||
$configFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'env.ini';
|
||||
self::$config = file_exists($configFile) ? parse_ini_file($configFile) : [];
|
||||
}
|
||||
return self::$config;
|
||||
}
|
||||
|
||||
public static function log($message, $level = 'INFO', $file = 'system.log') {
|
||||
$config = self::getConfig();
|
||||
if ($file === 'access.log' && empty($config['LOG_ACCESS'])) return;
|
||||
if ($file === 'blocked.log' && empty($config['LOG_SECURITY'])) return;
|
||||
|
||||
$logDir = self::getLogDir();
|
||||
if (!is_dir($logDir) && !mkdir($logDir, 0777, true)) return;
|
||||
|
||||
$logFile = $logDir . DIRECTORY_SEPARATOR . $file;
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
file_put_contents($logFile, "[$timestamp] [$level] $message\n", FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
public static function logException(\Throwable $e) {
|
||||
$message = sprintf("Uncaught Exception: '%s' with message '%s' in %s:%d\nStack trace:\n%s", get_class($e), $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTraceAsString());
|
||||
self::log($message, 'CRITICAL', 'error.log');
|
||||
}
|
||||
|
||||
public static function logError($errno, $errstr, $errfile, $errline) {
|
||||
$levels = [E_ERROR => 'ERROR', E_WARNING => 'WARNING', E_PARSE => 'PARSE_ERROR', E_NOTICE => 'NOTICE', E_USER_ERROR => 'USER_ERROR', E_USER_WARNING => 'USER_WARNING', E_USER_NOTICE => 'USER_NOTICE', E_DEPRECATED => 'DEPRECATED', E_USER_DEPRECATED => 'USER_DEPRECATED'];
|
||||
self::log("$errstr in $errfile on line $errline", $levels[$errno] ?? 'UNKNOWN', 'error.log');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Classes;
|
||||
|
||||
use MVC\Classes\Client;
|
||||
use MVC\Classes\Request;
|
||||
use MVC\Classes\Logger;
|
||||
use MVC\Model\Mailing;
|
||||
|
||||
class Mailer {
|
||||
private const DEFAULT_MAIL = 'bruno.eckhardt07@gmail.com';
|
||||
|
||||
private static function getConfig() {
|
||||
return parse_ini_file(__DIR__ . '/../../config/env.ini');
|
||||
}
|
||||
|
||||
private static function getDefaultRecipient(array $config): string {
|
||||
return $config['MAIL_TO'] ?? self::DEFAULT_MAIL;
|
||||
}
|
||||
|
||||
private static function getSender(array $config): string {
|
||||
return $config['MAIL_FROM'] ?? self::DEFAULT_MAIL;
|
||||
}
|
||||
|
||||
private static function resolveRecipient(?string $to, array $config): string {
|
||||
$to = trim((string) $to);
|
||||
return $to !== '' ? $to : self::getDefaultRecipient($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a standard email and optionally logs it.
|
||||
*/
|
||||
public static function send($subject, $message, $url = null, bool $log = true, ?string $to = null) {
|
||||
$config = self::getConfig();
|
||||
$from = self::getSender($config);
|
||||
$recipient = self::resolveRecipient($to, $config);
|
||||
|
||||
$headers = "From: " . $from . "\r\n";
|
||||
$headers .= "Reply-To: " . $from . "\r\n";
|
||||
$headers .= "X-Mailer: PHP/" . phpversion();
|
||||
|
||||
// mail() unterdrückt Fehlermeldungen standardmäßig nicht,
|
||||
// wir prüfen den Rückgabewert.
|
||||
$success = @mail($recipient, $subject, $message, $headers);
|
||||
|
||||
if (!$success) {
|
||||
Logger::log("Mail dispatch failed to '$recipient' for subject: '$subject'. Check your system's sendmail/SMTP configuration.", "ERROR", "error.log");
|
||||
}
|
||||
|
||||
if ($log) {
|
||||
self::log($subject, $message, $url, false);
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an error email and optionally logs it as an error.
|
||||
*/
|
||||
public static function error($title, $error, bool $log = true) {
|
||||
$subject = "ERROR: $title";
|
||||
$config = self::getConfig();
|
||||
$from = self::getSender($config);
|
||||
$to = self::getDefaultRecipient($config);
|
||||
|
||||
$headers = "From: " . $from . "\r\n";
|
||||
$headers .= "Reply-To: " . $from . "\r\n";
|
||||
$headers .= "X-Mailer: PHP/" . phpversion();
|
||||
|
||||
$success = @mail($to, $subject, $error, $headers);
|
||||
|
||||
if (!$success) {
|
||||
Logger::log("Error-Mail dispatch failed: '$title'.", "ERROR", "error.log");
|
||||
}
|
||||
|
||||
if ($log) {
|
||||
self::log($subject, $error, null, true);
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet eine E-Mail basierend auf einem View-Template
|
||||
*/
|
||||
public static function sendTemplate($subject, $template, $data = [], bool $log = true, ?string $to = null) {
|
||||
$view = new \MVC\Core\View(dirname(__DIR__) . '/View');
|
||||
foreach ($data as $key => $value) {
|
||||
$view->assign($key, $value);
|
||||
}
|
||||
$htmlContent = $view->render($template);
|
||||
|
||||
$config = self::getConfig();
|
||||
$from = self::getSender($config);
|
||||
$recipient = self::resolveRecipient($to, $config);
|
||||
|
||||
$headers = "MIME-Version: 1.0" . "\r\n";
|
||||
$headers .= "Content-type:text/html;charset=UTF-8" . "\r\n";
|
||||
$headers .= "From: " . $from . "\r\n";
|
||||
$headers .= "Reply-To: " . $from . "\r\n";
|
||||
|
||||
$success = @mail($recipient, $subject, $htmlContent, $headers);
|
||||
|
||||
if ($log) {
|
||||
self::log($subject, "HTML Template: $template", null, false);
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the email to the database or falls back to file logging.
|
||||
*/
|
||||
private static function log($subject, $message, $url, bool $isError) {
|
||||
$url = $url ?? Request::uri();
|
||||
$requestTime = Request::time();
|
||||
$date = ($requestTime !== 'UNKNOWN') ? date('Y-m-d H:i:s', $requestTime) : date('Y-m-d H:i:s');
|
||||
$ip = Client::ip();
|
||||
|
||||
try {
|
||||
if (class_exists('MVC\Model\Mailing')) {
|
||||
Mailing::log($subject, $message, $url, $ip, $date);
|
||||
return;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// DB-Log fehlgeschlagen
|
||||
}
|
||||
|
||||
self::logToFile($subject, $message, $url, $ip, $date, $isError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt Log-Einträge in eine Datei (Nutzt nun die Logger-Klasse für Konsistenz).
|
||||
*/
|
||||
private static function logToFile($subject, $message, $url, $ip, $date, bool $isError) {
|
||||
$fileName = $isError ? 'mail_errors.log' : 'mails.log';
|
||||
$logEntry = "IP: $ip | URL: $url\nSubject: $subject\nMessage: $message\n" . str_repeat("-", 30);
|
||||
|
||||
Logger::log($logEntry, $isError ? 'MAIL_ERROR' : 'MAIL_INFO', $fileName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Classes;
|
||||
|
||||
class Request {
|
||||
public static function method() {
|
||||
return $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN';
|
||||
}
|
||||
|
||||
public static function uri() {
|
||||
return $_SERVER['REQUEST_URI'] ?? 'UNKNOWN';
|
||||
}
|
||||
|
||||
public static function headers() {
|
||||
return getallheaders();
|
||||
}
|
||||
|
||||
public static function body() {
|
||||
return file_get_contents('php://input');
|
||||
}
|
||||
|
||||
public static function queryParams() {
|
||||
return $_GET;
|
||||
}
|
||||
|
||||
public static function time() {
|
||||
return $_SERVER['REQUEST_TIME'] ?? 'UNKNOWN';
|
||||
}
|
||||
|
||||
public static function referrer() {
|
||||
return $_SERVER['HTTP_REFERER'] ?? 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Classes;
|
||||
|
||||
class Server {
|
||||
public static function documentRoot() {
|
||||
return $_SERVER['DOCUMENT_ROOT'] ?? 'UNKNOWN';
|
||||
}
|
||||
|
||||
public static function currentDir() {
|
||||
return __DIR__;
|
||||
}
|
||||
|
||||
public static function currentScript() {
|
||||
return $_SERVER['SCRIPT_NAME'] ?? 'UNKNOWN';
|
||||
}
|
||||
|
||||
public static function name() {
|
||||
return $_SERVER['SERVER_NAME'] ?? 'UNKNOWN';
|
||||
}
|
||||
|
||||
public static function port() {
|
||||
return $_SERVER['SERVER_PORT'] ?? 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Classes;
|
||||
|
||||
class Storage {
|
||||
|
||||
private static function getBaseDir() {
|
||||
return dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'storage';
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt eine Datei hoch
|
||||
*/
|
||||
public static function upload(string $fileKey, string $subDir = 'uploads'): array {
|
||||
if (!isset($_FILES[$fileKey]) || $_FILES[$fileKey]['error'] !== UPLOAD_ERR_OK) {
|
||||
return ['success' => false, 'error' => 'Keine Datei oder Upload-Fehler.'];
|
||||
}
|
||||
|
||||
$baseDir = self::getBaseDir();
|
||||
$targetDir = $baseDir . DIRECTORY_SEPARATOR . $subDir;
|
||||
|
||||
if (!is_dir($targetDir)) {
|
||||
mkdir($targetDir, 0755, true);
|
||||
}
|
||||
|
||||
$fileName = basename($_FILES[$fileKey]['name']);
|
||||
$targetPath = $targetDir . DIRECTORY_SEPARATOR . $fileName;
|
||||
|
||||
// Sicherheitsprüfung: existiert bereits?
|
||||
if (file_exists($targetPath)) {
|
||||
$fileName = time() . '_' . $fileName;
|
||||
$targetPath = $targetDir . DIRECTORY_SEPARATOR . $fileName;
|
||||
}
|
||||
|
||||
if (move_uploaded_file($_FILES[$fileKey]['tmp_name'], $targetPath)) {
|
||||
return [
|
||||
'success' => true,
|
||||
'name' => $fileName,
|
||||
'path' => $subDir . '/' . $fileName,
|
||||
'size' => $_FILES[$fileKey]['size']
|
||||
];
|
||||
}
|
||||
|
||||
return ['success' => false, 'error' => 'Fehler beim Verschieben der Datei.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Listet Dateien in einem Verzeichnis auf
|
||||
*/
|
||||
public static function listFiles(string $subDir = 'uploads'): array {
|
||||
$dir = self::getBaseDir() . DIRECTORY_SEPARATOR . $subDir;
|
||||
if (!is_dir($dir)) return [];
|
||||
|
||||
$files = [];
|
||||
foreach (scandir($dir) as $file) {
|
||||
if ($file === '.' || $file === '..') continue;
|
||||
$path = $dir . DIRECTORY_SEPARATOR . $file;
|
||||
$files[] = [
|
||||
'name' => $file,
|
||||
'size' => round(filesize($path) / 1024, 2),
|
||||
'date' => date('Y-m-d H:i:s', filemtime($path))
|
||||
];
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht eine Datei
|
||||
*/
|
||||
public static function delete(string $path): bool {
|
||||
$fullPath = self::getBaseDir() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path);
|
||||
if (file_exists($fullPath) && is_file($fullPath)) {
|
||||
return unlink($fullPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzwingt den Download einer Datei
|
||||
*/
|
||||
public static function download(string $path) {
|
||||
$fullPath = self::getBaseDir() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path);
|
||||
if (file_exists($fullPath) && is_file($fullPath)) {
|
||||
header('Content-Description: File Transfer');
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename="' . basename($fullPath) . '"');
|
||||
header('Expires: 0');
|
||||
header('Cache-Control: must-revalidate');
|
||||
header('Pragma: public');
|
||||
header('Content-Length: ' . filesize($fullPath));
|
||||
readfile($fullPath);
|
||||
exit;
|
||||
}
|
||||
die("Datei nicht gefunden.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace MVC\Controller;
|
||||
use MVC\Core\Controller;
|
||||
use MVC\Classes\Mailer;
|
||||
use MVC\Model\Users;
|
||||
class AdminController extends Controller {
|
||||
public function mail_service() {
|
||||
$msg = null;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$to = $_POST['to'] ?? '';
|
||||
if ($to !== '' && !filter_var($to, FILTER_VALIDATE_EMAIL)) $msg = ["type" => "error", "text" => "Bitte eine gültige Empfänger-Adresse angeben."];
|
||||
elseif (Mailer::send($_POST['subject'] ?? '', $_POST['body'] ?? '', null, true, $to)) $msg = ["type" => "success", "text" => "Mail gesendet!"];
|
||||
else $msg = ["type" => "error", "text" => "Fehler!"];
|
||||
}
|
||||
return $this->render('admin/mail', ['message' => $msg]);
|
||||
}
|
||||
public function user_service() {
|
||||
$msg = null;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$user = $_POST['username'] ?? '';
|
||||
$pass = $_POST['password'] ?? '';
|
||||
if ($user && $pass) {
|
||||
if (Users::insert(['USERNAME' => $user, 'PASSWORD' => password_hash($pass, PASSWORD_DEFAULT), 'EMAIL' => $_POST['email'] ?? '', 'ROLE' => $_POST['role'] ?? 'user'])) $msg = ["type" => "success", "text" => "User $user erstellt!"];
|
||||
else $msg = ["type" => "error", "text" => "Fehler (DB)!"];
|
||||
} else $msg = ["type" => "error", "text" => "Name/Passwort fehlen!"];
|
||||
}
|
||||
return $this->render('admin/users', ['message' => $msg]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Controller;
|
||||
|
||||
use MVC\Core\Controller;
|
||||
use MVC\Classes\Api;
|
||||
|
||||
class ApiController extends Controller {
|
||||
public static function get(){
|
||||
return Api::jsonResponse([
|
||||
'data' => 'DummyData',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Controller;
|
||||
|
||||
use MVC\Core\Controller;
|
||||
use MVC\Model\Users;
|
||||
|
||||
class AuthController extends Controller {
|
||||
public function login() {
|
||||
if (isset($_SESSION['user_logged_in'])) {
|
||||
header('Location: ?service=home');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
$user = Users::getByUsername($username);
|
||||
|
||||
if ($user && password_verify($password, $user['PASSWORD'])) {
|
||||
$_SESSION['user_logged_in'] = true;
|
||||
$_SESSION['username'] = $user['USERNAME'];
|
||||
$_SESSION['role'] = $user['ROLE'];
|
||||
$_SESSION['is_admin'] = ($user['ROLE'] === 'admin');
|
||||
|
||||
header('Location: ?service=home');
|
||||
exit;
|
||||
} else {
|
||||
return $this->render('login', ['error' => 'Ungültige Anmeldedaten']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('login');
|
||||
}
|
||||
|
||||
public function logout() {
|
||||
session_destroy();
|
||||
header('Location: ?service=login');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Controller;
|
||||
|
||||
use MVC\Core\Controller;
|
||||
|
||||
class DocsController extends Controller {
|
||||
public function index() {
|
||||
return $this->render('docs', [
|
||||
'title' => '📚 System-Dokumentation'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace MVC\Controller;
|
||||
use MVC\Core\Controller;
|
||||
use MVC\Classes\Client;
|
||||
use MVC\Classes\Cookie;
|
||||
use MVC\Classes\Request;
|
||||
use MVC\Classes\Server;
|
||||
class HomeController extends Controller {
|
||||
|
||||
public function index() {
|
||||
return $this->render('home');
|
||||
}
|
||||
|
||||
public function toggle_theme() {
|
||||
$_SESSION['theme'] = ($_SESSION['theme'] ?? 'light') === 'dark' ? 'light' : 'dark';
|
||||
|
||||
return $this->json(
|
||||
[
|
||||
'success' => true
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Controller;
|
||||
|
||||
use MVC\Core\Controller;
|
||||
use MVC\Classes\Storage;
|
||||
use MVC\Classes\Form;
|
||||
|
||||
class TemplateController extends Controller {
|
||||
|
||||
public function upload() {
|
||||
$message = null;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$upload = Storage::upload('file_upload');
|
||||
if ($upload['success']) {
|
||||
$message = [
|
||||
'type' => 'success',
|
||||
'text' => 'Datei "' . $upload['name'] . '" erfolgreich hochgeladen.'
|
||||
];
|
||||
} else {
|
||||
$message = [
|
||||
'type' => 'error',
|
||||
'text' => $upload['error']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('templates/upload', [
|
||||
'message' => $message,
|
||||
'current_service' => 'template_upload'
|
||||
]);
|
||||
}
|
||||
|
||||
public function download() {
|
||||
$files = Storage::listFiles();
|
||||
|
||||
return $this->render('templates/download', [
|
||||
'files' => $files
|
||||
]);
|
||||
}
|
||||
|
||||
public function data_form() {
|
||||
$errors = [];
|
||||
$message = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$errors = Form::validate($_POST, [
|
||||
'firstname' => 'required',
|
||||
'lastname' => 'required',
|
||||
'email' => 'required|email'
|
||||
]);
|
||||
|
||||
if (empty($errors)) {
|
||||
$message = ['type' => 'success', 'text' => 'Daten erfolgreich validiert und gespeichert!'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('templates/data_form', [
|
||||
'current_service' => 'template_form',
|
||||
'errors' => $errors,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
|
||||
public function data_table() {
|
||||
return $this->render('templates/data_table');
|
||||
}
|
||||
|
||||
public function download_file() {
|
||||
$fileName = $_GET['file'] ?? null;
|
||||
if ($fileName) {
|
||||
// Wir hängen 'uploads/' davor, da Storage::upload standardmäßig dorthin speichert
|
||||
Storage::download('uploads/' . $fileName);
|
||||
}
|
||||
header('Location: ?service=template_download');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
namespace MVC\Controller;
|
||||
use MVC\Core\Controller;
|
||||
use MVC\Classes\Client;
|
||||
use MVC\Classes\Cookie;
|
||||
use MVC\Classes\Request;
|
||||
use MVC\Classes\Server;
|
||||
class TestController extends Controller {
|
||||
|
||||
public function service1() {
|
||||
return $this->render(
|
||||
'test1',
|
||||
[
|
||||
'msg' => 'Service 1 ist bereit!'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function service2() {
|
||||
return $this->render(
|
||||
'test2',
|
||||
[
|
||||
'data' =>
|
||||
[
|
||||
'Layout' => 'Header + Sidebar',
|
||||
'Theme' => 'Auto-Toggle'
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Controller;
|
||||
|
||||
use MVC\Core\Controller;
|
||||
use MVC\Model\Zeiterfassung;
|
||||
use MVC\Model\Users;
|
||||
|
||||
class WebtimeController extends Controller {
|
||||
|
||||
public function webtime() {
|
||||
|
||||
// User aus Session + DB laden
|
||||
$username = $_SESSION['username'] ?? null;
|
||||
|
||||
if (!$username) {
|
||||
header('Location: ?service=login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = Users::getByUsername($username);
|
||||
$userId = (int)($user['ID'] ?? 0);
|
||||
|
||||
if (!$userId) {
|
||||
header('Location: ?service=login');
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Einstempeln ---
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'start') {
|
||||
if (!Zeiterfassung::isRunning($userId)) {
|
||||
Zeiterfassung::insert([
|
||||
'DATUM' => date('d.m.Y'),
|
||||
'BEGIN' => date('H:i'),
|
||||
'USER_ID' => $userId,
|
||||
]);
|
||||
}
|
||||
header('Location: ?service=webtime');
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Ausstempeln + Aufgaben speichern ---
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'stop') {
|
||||
$open = Zeiterfassung::getOpenEntry($userId);
|
||||
if ($open) {
|
||||
Zeiterfassung::update(
|
||||
[
|
||||
'ENDE' => date('H:i'),
|
||||
'A_1' => $_POST['a_1'] ?? null,
|
||||
'A_2' => $_POST['a_2'] ?? null,
|
||||
'A_3' => $_POST['a_3'] ?? null,
|
||||
'A_4' => $_POST['a_4'] ?? null,
|
||||
],
|
||||
"ID = ?",
|
||||
$open['ID']
|
||||
);
|
||||
}
|
||||
header('Location: ?service=webtime');
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Eintrag bearbeiten ---
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'edit') {
|
||||
$id = (int)($_POST['entry_id'] ?? 0);
|
||||
if ($id) {
|
||||
Zeiterfassung::update(
|
||||
[
|
||||
'DATUM' => $_POST['datum'],
|
||||
'BEGIN' => $_POST['begin'],
|
||||
'ENDE' => $_POST['ende'] !== '' ? $_POST['ende'] : null,
|
||||
'A_1' => $_POST['a_1'] ?? null,
|
||||
'A_2' => $_POST['a_2'] ?? null,
|
||||
'A_3' => $_POST['a_3'] ?? null,
|
||||
'A_4' => $_POST['a_4'] ?? null,
|
||||
],
|
||||
"ID = ? AND USER_ID = ?",
|
||||
$id, $userId
|
||||
);
|
||||
}
|
||||
header('Location: ?service=webtime');
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Eintrag löschen ---
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') {
|
||||
$id = (int)($_POST['entry_id'] ?? 0);
|
||||
if ($id) {
|
||||
Zeiterfassung::delete("ID = ? AND USER_ID = ?", $id, $userId);
|
||||
}
|
||||
header('Location: ?service=webtime');
|
||||
exit;
|
||||
}
|
||||
|
||||
$isRunning = Zeiterfassung::isRunning($userId);
|
||||
$openEntry = Zeiterfassung::getOpenEntry($userId);
|
||||
$eintraege = Zeiterfassung::getByUser($userId);
|
||||
|
||||
return $this->render('webtime', [
|
||||
'isRunning' => $isRunning,
|
||||
'openEntry' => $openEntry,
|
||||
'eintraege' => $eintraege,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Core;
|
||||
|
||||
class Controller {
|
||||
protected $view;
|
||||
|
||||
public function __construct() {
|
||||
$this->view = new View(__DIR__ . '/../View');
|
||||
}
|
||||
|
||||
protected function render($template, $data = []) {
|
||||
foreach ($data as $key => $value) {
|
||||
$this->view->assign($key, $value);
|
||||
}
|
||||
return $this->view->render($template);
|
||||
}
|
||||
|
||||
protected function json($data, $status = 200) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code($status);
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Core;
|
||||
|
||||
class Middleware {
|
||||
/**
|
||||
* Mapping von Middleware-Namen auf Klassen.
|
||||
*/
|
||||
protected static $map = [
|
||||
'auth' => \MVC\Middleware\AuthMiddleware::class,
|
||||
'admin' => \MVC\Middleware\AdminMiddleware::class,
|
||||
'block' => \MVC\Middleware\BlockMiddleware::class,
|
||||
'global' => \MVC\Middleware\EveryRequestMiddleware::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Führt eine Middleware aus.
|
||||
*/
|
||||
public static function run($name) {
|
||||
if (!isset(self::$map[$name])) {
|
||||
return; // Oder Exception werfen
|
||||
}
|
||||
|
||||
$class = self::$map[$name];
|
||||
$middleware = new $class();
|
||||
|
||||
if (!$middleware instanceof MiddlewareInterface) {
|
||||
throw new \Exception("Middleware '$class' muss MiddlewareInterface implementieren.");
|
||||
}
|
||||
|
||||
$middleware->handle();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Core;
|
||||
|
||||
interface MiddlewareInterface {
|
||||
/**
|
||||
* Hauptmethode der Middleware.
|
||||
*/
|
||||
public function handle();
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Core;
|
||||
|
||||
require_once __DIR__ . '/../../database/databaseConnect.php';
|
||||
require_once __DIR__ . '/../../utils/databaseMethods.php';
|
||||
|
||||
abstract class Model {
|
||||
protected static $table;
|
||||
protected static $conn;
|
||||
|
||||
protected static function getConnection() {
|
||||
if (!self::$conn) {
|
||||
self::$conn = \db_connect();
|
||||
}
|
||||
return self::$conn;
|
||||
}
|
||||
|
||||
public static function all() {
|
||||
return \DatabaseShow(static::$table, "*");
|
||||
}
|
||||
|
||||
public static function select($columns = "*") {
|
||||
if (is_array($columns)) {
|
||||
$columns = implode(", ", $columns);
|
||||
}
|
||||
return \DatabaseShow(static::$table, $columns);
|
||||
}
|
||||
|
||||
public static function find($id, $column = 'ID') {
|
||||
$result = \DatabaseShow(static::$table, "*", "$column = ?", $id);
|
||||
return $result[0] ?? null;
|
||||
}
|
||||
|
||||
public static function where($where, ...$params) {
|
||||
return \DatabaseShow(static::$table, "*", $where, ...$params);
|
||||
}
|
||||
|
||||
public static function insert($data) {
|
||||
$columns = implode(", ", array_keys($data));
|
||||
$values = array_values($data);
|
||||
return \DatabaseInsert(static::$table, $columns, ...$values);
|
||||
}
|
||||
|
||||
public static function update($data, $where, ...$whereParams) {
|
||||
$sets = [];
|
||||
$values = [];
|
||||
foreach ($data as $column => $value) {
|
||||
$sets[] = "$column = ?";
|
||||
$values[] = $value;
|
||||
}
|
||||
$setString = implode(", ", $sets);
|
||||
return \DatabaseUpdate(static::$table, $setString, $where, ...array_merge($values, $whereParams));
|
||||
}
|
||||
|
||||
public static function delete($where, ...$params) {
|
||||
return \DatabaseDelete(static::$table, $where, ...$params);
|
||||
}
|
||||
|
||||
public static function query($sql, ...$params) {
|
||||
$conn = self::getConnection();
|
||||
if (empty($params)) {
|
||||
return $conn->query($sql);
|
||||
}
|
||||
$stmt = $conn->prepare($sql);
|
||||
$types = str_repeat('s', count($params));
|
||||
$stmt->bind_param($types, ...$params);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Core;
|
||||
|
||||
class Router {
|
||||
protected $routes = [];
|
||||
protected $globalMiddlewares = [];
|
||||
|
||||
public function addGlobal($middleware) {
|
||||
$this->globalMiddlewares[] = $middleware;
|
||||
}
|
||||
|
||||
public function add($service, $controller, $method, $middlewares = [], $isApi = null) {
|
||||
// Auto-detect API route if $isApi is not explicitly set
|
||||
if ($isApi === null) {
|
||||
$isApi = strpos($service, 'api/') === 0;
|
||||
}
|
||||
|
||||
$this->routes[$service] = [
|
||||
'controller' => $controller,
|
||||
'method' => $method,
|
||||
'middlewares' => $middlewares,
|
||||
'isApi' => $isApi
|
||||
];
|
||||
}
|
||||
|
||||
public function getRoute($service) {
|
||||
return $this->routes[$service] ?? null;
|
||||
}
|
||||
|
||||
public function dispatch($service) {
|
||||
if (!isset($this->routes[$service])) {
|
||||
throw new \Exception("Service '$service' not found.");
|
||||
}
|
||||
|
||||
// 1. Run Global Middlewares
|
||||
foreach ($this->globalMiddlewares as $middleware) {
|
||||
$this->runMiddleware($middleware);
|
||||
}
|
||||
|
||||
$route = $this->routes[$service];
|
||||
|
||||
// 2. Run Route-Specific Middlewares
|
||||
foreach ($route['middlewares'] as $middleware) {
|
||||
$this->runMiddleware($middleware);
|
||||
}
|
||||
|
||||
$controllerClass = $route['controller'];
|
||||
$method = $route['method'];
|
||||
|
||||
if (!class_exists($controllerClass)) {
|
||||
throw new \Exception("Controller class '$controllerClass' not found.");
|
||||
}
|
||||
|
||||
$controller = new $controllerClass();
|
||||
|
||||
if (!method_exists($controller, $method)) {
|
||||
throw new \Exception("Method '$method' not found in controller '$controllerClass'.");
|
||||
}
|
||||
|
||||
return $controller->$method();
|
||||
}
|
||||
|
||||
public function getRouteMiddleware($service) {
|
||||
return $this->routes[$service]['middlewares'] ?? [];
|
||||
}
|
||||
|
||||
public function hasMiddleware($service, $middleware) {
|
||||
return in_array($middleware, $this->getRouteMiddleware($service));
|
||||
}
|
||||
|
||||
protected function runMiddleware($middleware) {
|
||||
Middleware::run($middleware);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Core;
|
||||
|
||||
class View {
|
||||
protected $data = [];
|
||||
protected $viewPath;
|
||||
|
||||
public function __construct($viewPath) {
|
||||
$this->viewPath = $viewPath;
|
||||
}
|
||||
|
||||
public function assign($key, $value) {
|
||||
$this->data[$key] = $value;
|
||||
}
|
||||
|
||||
public function render($template) {
|
||||
extract($this->data);
|
||||
$file = $this->viewPath . '/' . $template . '.php';
|
||||
if (file_exists($file)) {
|
||||
ob_start();
|
||||
include $file;
|
||||
return ob_get_clean();
|
||||
}
|
||||
return "View $template not found at $file";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>403 Forbidden</title>
|
||||
<style>
|
||||
body { background: white; color: black; font-family: monospace; padding: 50px; }
|
||||
h1 { border-bottom: 1px solid black; padding-bottom: 10px; }
|
||||
pre { background: #eee; padding: 15px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>403 Forbidden</h1>
|
||||
<p>Access to this resource is denied.</p>
|
||||
<hr>
|
||||
<pre>
|
||||
Status: Blocked
|
||||
Source: <?php echo htmlspecialchars($ip); ?>
|
||||
Time: <?php echo date('r'); ?>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Systemmeldung</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #1a1a2e; color: #e94560; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
|
||||
.card { background: #16213e; padding: 3rem; border-radius: 20px; text-align: center; box-shadow: 0 10px 30px rgba(0,0,0,0.5); border: 1px solid #0f3460; }
|
||||
.icon { font-size: 4rem; margin-bottom: 1rem; }
|
||||
h1 { margin: 0; font-weight: 300; letter-spacing: 2px; }
|
||||
p { color: #95a5a6; margin-top: 1.5rem; }
|
||||
.footer { margin-top: 2rem; font-size: 0.8rem; opacity: 0.5; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon">🛡️</div>
|
||||
<h1>ZUGRIFF GESPERRT</h1>
|
||||
<p>Ihre Verbindung wurde aus Sicherheitsgründen unterbrochen.</p>
|
||||
<div class="footer">ID: <?php echo htmlspecialchars($ip); ?> | <?php echo date('Y-m-d H:i:s'); ?></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Zugriff verweigert</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; background: #f4f4f4; color: #333; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
|
||||
.container { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); text-align: center; max-width: 400px; border-top: 5px solid #d9534f; }
|
||||
h1 { color: #d9534f; margin-top: 0; }
|
||||
p { line-height: 1.6; }
|
||||
.ip { font-weight: bold; color: #777; font-size: 0.9rem; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>403 - Zugriff verweigert</h1>
|
||||
<p>Ihre IP-Adresse wurde aufgrund von Sicherheitsrichtlinien vorübergehend blockiert.</p>
|
||||
<p>Bitte wenden Sie sich an den Administrator, falls Sie glauben, dass dies ein Fehler ist.</p>
|
||||
<div class="ip">IP: <?php echo htmlspecialchars($ip); ?></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace MVC\Layout;
|
||||
class Layout {
|
||||
|
||||
protected $blocks = [];
|
||||
|
||||
protected $template;
|
||||
|
||||
public function __construct($template = 'main') {
|
||||
$this->template = $template;
|
||||
}
|
||||
|
||||
public function setBlock($name, $content) {
|
||||
$this->blocks[$name] = $content;
|
||||
}
|
||||
|
||||
public function getBlock($name) {
|
||||
return $this->blocks[$name] ?? '';
|
||||
}
|
||||
|
||||
public function render() {
|
||||
$layoutFile = __DIR__ . '/Templates/' . $this->template . '.php';
|
||||
|
||||
if (file_exists($layoutFile)) {
|
||||
ob_start();
|
||||
include $layoutFile;
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
return "Layout template nicht gefunden.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" class="<?= ($_SESSION['theme'] ?? 'light') === 'dark' ? 'dark-mode' : '' ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><?= $this->getBlock('title') ?></title>
|
||||
<link rel="icon" type="image/svg+xml"
|
||||
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23007bff' viewBox='0 0 16 16'><path d='M2.114 8.063V7.9c1.005-.102 1.497-.615 1.497-1.6V4.503c0-1.094.39-1.538 1.354-1.538h.273V2h-.376C3.25 2 2.49 2.759 2.49 4.352v1.524c0 1.094-.376 1.456-1.49 1.456v1.299c1.114 0 1.49.362 1.49 1.456v1.524c0 1.593.759 2.352 2.372 2.352h.376v-.964h-.273c-.964 0-1.354-.444-1.354-1.538V9.663c0-.984-.492-1.497-1.497-1.6M13.886 7.9v.163c-1.005.103-1.497.616-1.497 1.6v1.798c0 1.094-.39 1.538-1.354 1.538h-.273v.964h.376c1.613 0 2.372-.759 2.372-2.352v-1.524c0-1.094.376-1.456 1.49-1.456V7.332c-1.114 0-1.49-.362-1.49-1.456V4.352C13.51 2.759 12.75 2 11.138 2h-.376v.964h.273c.964 0 1.354.444 1.354 1.538V6.3c0 .984.492 1.497 1.497 1.6'/></svg>">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--text: #333;
|
||||
--sidebar: #f4f4f4;
|
||||
--header: #007bff;
|
||||
--accent: #007bff;
|
||||
--border: #ddd;
|
||||
}
|
||||
|
||||
.dark-mode {
|
||||
--bg: #1a1a1a;
|
||||
--text: #f4f4f4;
|
||||
--sidebar: #252525;
|
||||
--header: #111;
|
||||
--accent: #3498db;
|
||||
--border: #333;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
transition: 0.3s;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
height: 60px;
|
||||
background: var(--header);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: var(--sidebar);
|
||||
padding: 20px;
|
||||
border-right: 1px solid var(--border);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 40px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn-toggle {
|
||||
padding: 8px 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: block;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
a:link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="main-header">
|
||||
<div class="logo"><?= $this->getBlock('header_title') ?></div>
|
||||
<div class="header-actions">
|
||||
<?= $this->getBlock('header_actions') ?>
|
||||
</div>
|
||||
</header>
|
||||
<div class="main-container">
|
||||
<div class="sidebar">
|
||||
<?= $this->getBlock('sidebar') ?>
|
||||
</div>
|
||||
<div class="content">
|
||||
<?= $this->getBlock('content') ?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function toggleTheme() {
|
||||
fetch('?service=toggle_theme').then(() => location.reload());
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Middleware;
|
||||
|
||||
use MVC\Core\MiddlewareInterface;
|
||||
|
||||
class AdminMiddleware implements MiddlewareInterface {
|
||||
public function handle() {
|
||||
if (!isset($_SESSION['is_admin']) || $_SESSION['is_admin'] !== true) {
|
||||
http_response_code(403);
|
||||
die("Zugriff verweigert: Admin-Rechte erforderlich.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Middleware;
|
||||
|
||||
use MVC\Core\MiddlewareInterface;
|
||||
|
||||
class AuthMiddleware implements MiddlewareInterface {
|
||||
public function handle() {
|
||||
if (empty($_SESSION['user_logged_in'])) {
|
||||
header('Location: ?service=login');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Middleware;
|
||||
|
||||
use MVC\Core\MiddlewareInterface;
|
||||
use MVC\Classes\Logger;
|
||||
use MVC\Classes\Client;
|
||||
|
||||
/**
|
||||
* Beispiel-Middleware: Blockiert bestimmte Anfragen oder IPs
|
||||
*/
|
||||
class BlockMiddleware implements MiddlewareInterface {
|
||||
public function handle() {
|
||||
$blockedIps = ['127.0.0.2'];
|
||||
$ip = Client::ip();
|
||||
if (in_array($ip, $blockedIps)) {
|
||||
Logger::log("Blocked IP attempt: $ip", 'SECURITY', 'blocked.log');
|
||||
|
||||
// Lade Konfiguration für das Template
|
||||
$configFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'env.ini';
|
||||
$config = file_exists($configFile) ? parse_ini_file($configFile) : [];
|
||||
$template = $config['BLOCK_PAGE_TEMPLATE'] ?? 'standard';
|
||||
|
||||
$templatePath = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Layout' . DIRECTORY_SEPARATOR . 'Block' . DIRECTORY_SEPARATOR . $template . '.php';
|
||||
|
||||
http_response_code(403);
|
||||
if (file_exists($templatePath)) {
|
||||
include $templatePath;
|
||||
} else {
|
||||
die("Ihre IP-Adresse wurde blockiert.");
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Middleware;
|
||||
|
||||
use MVC\Core\MiddlewareInterface;
|
||||
use MVC\Classes\Logger;
|
||||
use MVC\Classes\Client;
|
||||
|
||||
/**
|
||||
* Beispiel-Middleware: Wird bei jedem Request ausgeführt (Global)
|
||||
*/
|
||||
class EveryRequestMiddleware implements MiddlewareInterface {
|
||||
public function handle() {
|
||||
$ip = Client::ip();
|
||||
|
||||
// Bestimme Service (Logik konsistent mit index.php)
|
||||
$service = $_GET['service'] ?? null;
|
||||
if ($service === null) {
|
||||
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH);
|
||||
$service = trim($uri, '/');
|
||||
if (empty($service)) {
|
||||
$service = 'home';
|
||||
}
|
||||
}
|
||||
|
||||
Logger::log("IP: $ip - Service: $service", 'ACCESS', 'access.log');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Model;
|
||||
|
||||
use MVC\Core\Model;
|
||||
|
||||
class Mailing extends Model
|
||||
{
|
||||
protected static $table = "Mailing";
|
||||
|
||||
public static function log(
|
||||
string $subject,
|
||||
string $message,
|
||||
string $url,
|
||||
string $ip,
|
||||
string $date
|
||||
): bool {
|
||||
return self::insert([
|
||||
'DATUM' => $date,
|
||||
'SUBJECT' => $subject,
|
||||
'MESSAGE' => $message,
|
||||
'URL' => $url,
|
||||
'CLIENT_IP' => $ip,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Model;
|
||||
|
||||
use MVC\Core\Model;
|
||||
|
||||
class Users extends Model
|
||||
{
|
||||
protected static $table = "Users";
|
||||
|
||||
public static function getByUsername(string $username): ?array
|
||||
{
|
||||
$result = self::where("USERNAME = ?", $username);
|
||||
return $result[0] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace MVC\Model;
|
||||
|
||||
use MVC\Core\Model;
|
||||
|
||||
class Zeiterfassung extends Model {
|
||||
protected static $table = "Zeiterfassung";
|
||||
|
||||
public static function getByUser(int $userId): array {
|
||||
return self::where("USER_ID = ? ORDER BY ID DESC", $userId);
|
||||
}
|
||||
|
||||
public static function isRunning(int $userId): bool {
|
||||
$result = self::where(
|
||||
"USER_ID = ? AND ENDE IS NULL LIMIT 1",
|
||||
$userId
|
||||
);
|
||||
return !empty($result);
|
||||
}
|
||||
|
||||
public static function getOpenEntry(int $userId): ?array {
|
||||
$result = self::where(
|
||||
"USER_ID = ? AND ENDE IS NULL ORDER BY ID DESC LIMIT 1",
|
||||
$userId
|
||||
);
|
||||
return $result[0] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<h2>📧 E-Mail Service</h2>
|
||||
|
||||
<?php if (isset($message)): ?> <div
|
||||
style="padding: 15px; margin-bottom: 20px; border-radius: 5px; background: <?= $message['type'] === 'success' ? 'rgba(46, 204, 113, 0.2)' : 'rgba(231, 76, 60, 0.2)' ?>; color: var(--text);">
|
||||
<?= $message['text'] ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="background: var(--sidebar); padding: 30px; border-radius: 8px; border: 1px solid var(--border);">
|
||||
<form method="POST">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px;">Empfänger</label>
|
||||
<input type="email" name="to"
|
||||
value="<?= htmlspecialchars($_POST['to'] ?? 'bruno.eckhardt07@gmail.com') ?>" required
|
||||
style="width: 100%; padding: 10px; border-radius: 4px; border: 1px solid var(--border); background: var(--bg); color: var(--text);">
|
||||
</div>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px;">Betreff</label>
|
||||
<input type="text" name="subject" value="<?= htmlspecialchars($_POST['subject'] ?? '') ?>" required
|
||||
style="width: 100%; padding: 10px; border-radius: 4px; border: 1px solid var(--border); background: var(--bg); color: var(--text);">
|
||||
</div>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px;">Nachricht</label>
|
||||
<textarea name="body" required
|
||||
style="width: 100%; height: 150px; padding: 10px; border-radius: 4px; border: 1px solid var(--border); background: var(--bg); color: var(--text);"><?= htmlspecialchars($_POST['body'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<button type="submit"
|
||||
style="background: var(--accent); color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer;">E-Mail
|
||||
senden</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<h2>👤 Benutzerverwaltung</h2>
|
||||
<?php if (isset($message)): ?>
|
||||
<div
|
||||
style="padding:15px;margin-bottom:20px;border-radius:5px;background:<?= $message['type']==='success'?'rgba(46,204,113,0.2)':'rgba(231,76,60,0.2)'?>;">
|
||||
<?= $message['text']?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST" style="background:var(--sidebar);padding:20px;border-radius:8px;">
|
||||
<input type="text" name="username" placeholder="Benutzername" required
|
||||
style="width:100%;margin-bottom:10px;padding:10px;background:var(--bg);color:var(--text);border:1px solid var(--border);">
|
||||
<input type="password" name="password" placeholder="Passwort" required
|
||||
style="width:100%;margin-bottom:10px;padding:10px;background:var(--bg);color:var(--text);border:1px solid var(--border);">
|
||||
<input type="email" name="email" placeholder="E-Mail"
|
||||
style="width:100%;margin-bottom:10px;padding:10px;background:var(--bg);color:var(--text);border:1px solid var(--border);">
|
||||
<select name="role"
|
||||
style="width:100%;margin-bottom:10px;padding:10px;background:var(--bg);color:var(--text);border:1px solid var(--border);">
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<button type="submit"
|
||||
style="background:var(--accent);color:white;padding:10px 20px;border:none;border-radius:4px;cursor:pointer;">Erstellen</button>
|
||||
</form>
|
||||
@@ -0,0 +1,485 @@
|
||||
<div class="docs-container">
|
||||
<h1>🏛️ MVC Framework - Die Entwickler-Bibel</h1>
|
||||
|
||||
<div class="toc">
|
||||
<h3>📖 Inhaltsverzeichnis</h3>
|
||||
<div class="toc-grid">
|
||||
<ul>
|
||||
<li><strong>1. Kern-Architektur</strong>
|
||||
<ul>
|
||||
<li><a href="#request-lifecycle">Request Lifecycle</a></li>
|
||||
<li><a href="#directory-structure">Verzeichnisstruktur</a></li>
|
||||
<li><a href="#autoloader">Autoloader & Namespaces</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>2. Routing & Middleware</strong>
|
||||
<ul>
|
||||
<li><a href="#routing-basics">Routing Grundlagen</a></li>
|
||||
<li><a href="#middleware">Middleware (Auth/Admin)</a></li>
|
||||
<li><a href="#navigation">Navigations-System</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>3. Datenbank & Models</strong>
|
||||
<ul>
|
||||
<li><a href="#db-methods">Database Methods (Low-Level)</a></li>
|
||||
<li><a href="#model-core">Core\Model (Abstraktion)</a></li>
|
||||
<li><a href="#install-db">Installations-Script</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><strong>4. UI & Layout</strong>
|
||||
<ul>
|
||||
<li><a href="#layout-engine">Layout Engine (Blöcke)</a></li>
|
||||
<li><a href="#view-system">View System</a></li>
|
||||
<li><a href="#theme-toggle">DarkMode & Themes</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>5. Framework Klassen</strong>
|
||||
<ul>
|
||||
<li><a href="#class-auth">Auth (Sicherheit)</a></li>
|
||||
<li><a href="#class-form">Form (Generatoren)</a></li>
|
||||
<li><a href="#class-storage">Storage (Files)</a></li>
|
||||
<li><a href="#class-mailer">Mailer (E-Mail)</a></li>
|
||||
<li><a href="#class-logger">Logger (Debugging)</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>6. Tools & API</strong>
|
||||
<ul>
|
||||
<li><a href="#cli-tools">CLI Werkzeuge (make.php)</a></li>
|
||||
<li><a href="#api-dev">API Entwicklung</a></li>
|
||||
<li><a href="#best-practices">Best Practices</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. Kern-Architektur -->
|
||||
<section id="request-lifecycle">
|
||||
<h2>1.1 Request Lifecycle</h2>
|
||||
<p>Der Weg einer Anfrage durch das Framework ist linear und vorhersehbar:</p>
|
||||
<ol>
|
||||
<li><strong>Entry:</strong> Alle Requests treffen auf <code>WEBROOT/index.php</code>.</li>
|
||||
<li><strong>Bootstrap:</strong> Session wird gestartet, der <code>autoloader</code> lädt Klassen
|
||||
nach Bedarf.</li>
|
||||
<li><strong>Routing:</strong> Der <code>Router</code> lädt die <code>routes.php</code> und prüft
|
||||
den <code>GET['service']</code> Parameter.</li>
|
||||
<li><strong>Security:</strong> Middlewares (z.B. <code>auth</code>) werden ausgeführt.</li>
|
||||
<li><strong>Logic:</strong> Der Controller führt die Zielmethode aus.</li>
|
||||
<li><strong>Rendering:</strong> Der Controller ruft <code>$this->render()</code> auf.</li>
|
||||
<li><strong>Output:</strong> Das fertige Layout wird an den Browser gesendet.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section id="directory-structure">
|
||||
<h2>1.2 Detaillierte Verzeichnisstruktur</h2>
|
||||
<pre>
|
||||
├── Framework.code-workpace
|
||||
├── README.md
|
||||
├── init.php
|
||||
└── /WEBROOT
|
||||
├── /api
|
||||
| └── /v1
|
||||
├── /src
|
||||
| ├── /config
|
||||
| ├── /database
|
||||
| ├── /MVC
|
||||
| ├── /Core
|
||||
| ├── /Classes
|
||||
| ├── /Controller
|
||||
| ├── /Model
|
||||
| ├── /View
|
||||
| └── /Layout
|
||||
| └── /Templates
|
||||
├── /commands
|
||||
├── /static
|
||||
├── /storage
|
||||
└── /logs
|
||||
</pre>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="autoloader">
|
||||
<h2>1.3 Autoloader & Namespaces</h2>
|
||||
<p>Das Framework nutzt einen PSR-4 kompatiblen Autoloader in <code>src/MVC/autoloader.php</code>.
|
||||
</p>
|
||||
<pre><code>// Automatische Auflösung:
|
||||
// MVC\Classes\Auth -> src/MVC/Classes/Auth.php
|
||||
// MVC\Core\Router -> src/MVC/Core/Router.php</code></pre>
|
||||
</section>
|
||||
|
||||
<!-- 2. Routing -->
|
||||
<section id="routing-basics">
|
||||
<h2>2.1 Routing Grundlagen</h2>
|
||||
<p>Routen werden zentral in <code>src/config/routes.php</code> definiert.</p>
|
||||
<pre><code>$router->add('home', \MVC\Controller\HomeController::class, 'index');</code></pre>
|
||||
</section>
|
||||
|
||||
<section id="middleware">
|
||||
<h2>2.2 Middleware System</h2>
|
||||
<p>Middlewares sind Klassen, die vor dem eigentlichen Controller ausgeführt werden. Sie dienen der
|
||||
Validierung von Requests, Authentifizierung oder Logging.</p>
|
||||
|
||||
<h3>Globale Middlewares</h3>
|
||||
<p>Diese werden bei <strong>jedem</strong> Request ausgeführt (z.B. IP-Blocker oder globales
|
||||
Logging):</p>
|
||||
<pre><code>$router->addGlobal('block');
|
||||
$router->addGlobal('global');</code></pre>
|
||||
|
||||
<h3>Routen-spezifische Middlewares</h3>
|
||||
<p>Diese werden nur für bestimmte Services definiert:</p>
|
||||
<pre><code>// Erfordert Login ('auth') und Admin-Status ('admin')
|
||||
$router->add('admin', Controller::class, 'index', ['auth', 'admin']);</code></pre>
|
||||
|
||||
<h3>Verfügbare Middlewares</h3>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Klasse</th>
|
||||
<th>Funktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>auth</code></td>
|
||||
<td><code>AuthMiddleware</code></td>
|
||||
<td>Prüft, ob der User eingeloggt ist (<code>$_SESSION['user_logged_in']</code>).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>admin</code></td>
|
||||
<td><code>AdminMiddleware</code></td>
|
||||
<td>Prüft auf Administrator-Rechte (<code>$_SESSION['is_admin']</code>).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>block</code></td>
|
||||
<td><code>BlockMiddleware</code></td>
|
||||
<td>Einfacher IP-Blocker für gesperrte Adressen.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>global</code></td>
|
||||
<td><code>EveryRequestMiddleware</code></td>
|
||||
<td>Platzhalter für Logik, die immer laufen soll.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Eigene Middleware erstellen</h3>
|
||||
<ol>
|
||||
<li>Erstelle eine Klasse in <code>src/MVC/Middleware/</code>.</li>
|
||||
<li>Implementiere das <code>MiddlewareInterface</code>.</li>
|
||||
<li>Registriere den Namen im Mapping der <code>src/MVC/Core/Middleware.php</code>.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section id="navigation">
|
||||
<h2>2.3 Navigations-System</h2>
|
||||
<p>Die Sidebar wird dynamisch aus der <code>src/config/navigation.php</code> generiert. Der Router
|
||||
filtert Einträge basierend auf den Berechtigungen (Middleware) des Nutzers.</p>
|
||||
<pre><code>return [
|
||||
'home' => ['label' => '🏠 Home'],
|
||||
'admin_users' => ['label' => '👤 User Service'],
|
||||
];</code></pre>
|
||||
</section>
|
||||
|
||||
<!-- 3. Datenbank -->
|
||||
<section id="db-methods">
|
||||
<h2>3.1 Database Methods (Low-Level)</h2>
|
||||
<p>Sichere DB-Operationen via <code>src/utils/databaseMethods.php</code> unter Verwendung von
|
||||
Prepared Statements:</p>
|
||||
<ul>
|
||||
<li><code>DatabaseShow($table, $cols, $where, ...$values)</code></li>
|
||||
<li><code>DatabaseInsert($table, $cols, ...$values)</code></li>
|
||||
<li><code>DatabaseUpdate($table, $set, $where, ...$values)</code></li>
|
||||
<li><code>DatabaseDelete($table, $where, ...$values)</code></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="model-core">
|
||||
<h2>3.2 Core\Model (Abstraktion)</h2>
|
||||
<p>Ermöglicht objektorientierte DB-Zugriffe durch Vererbung.</p>
|
||||
<pre><code>class User extends \MVC\Core\Model {
|
||||
protected static $table = "Users";
|
||||
}
|
||||
$allUsers = User::all();</code></pre>
|
||||
</section>
|
||||
|
||||
<section id="install-db">
|
||||
<h2>3.3 Installations-Script</h2>
|
||||
<p>Das Script <code>install_db.php</code> automatisiert das gesamte DB-Setup:</p>
|
||||
<ul>
|
||||
<li><strong>Root-Access:</strong> Verbindet sich mit Root-Rechten (siehe <code>env.ini</code>).
|
||||
</li>
|
||||
<li><strong>DB-Erstellung:</strong> Erstellt die Datenbank und einen dedizierten Worker-User.
|
||||
</li>
|
||||
<li><strong>Tabellen:</strong> Legt <code>Users</code> (mit Admin-Account) und
|
||||
<code>Mailing</code> an.
|
||||
</li>
|
||||
<li><strong>Models:</strong> Generiert automatisch die zugehörigen Model-Dateien im Quellcode.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 4. UI -->
|
||||
<section id="layout-engine">
|
||||
<h2>4.1 Layout Engine (Blöcke)</h2>
|
||||
<p>Das Framework nutzt ein flexibles Block-System in <code>MVC\Layout\Layout</code>:</p>
|
||||
<pre><code>$layout = new Layout('main');
|
||||
$layout->setBlock('title', 'Mein Titel');
|
||||
$layout->setBlock('content', 'HTML-Inhalt');
|
||||
echo $layout->render();</code></pre>
|
||||
</section>
|
||||
|
||||
<section id="view-system">
|
||||
<h2>4.2 View System</h2>
|
||||
<p>Die <code>MVC\Core\View</code> Klasse extrahiert Daten-Arrays direkt in PHP-Templates:</p>
|
||||
<pre><code>// Im Controller:
|
||||
return $this->render('home', ['name' => 'Max']);
|
||||
|
||||
// In src/MVC/View/home.php:
|
||||
<h1>Hallo <?= $name ?></h1></code></pre>
|
||||
</section>
|
||||
|
||||
<section id="theme-toggle">
|
||||
<h2>4.3 DarkMode & Themes</h2>
|
||||
<p>Unterstützt nahtlose Umschaltung zwischen Light- und DarkMode:</p>
|
||||
<ul>
|
||||
<li><strong>Zustand:</strong> Gespeichert in <code>$_SESSION['theme']</code>.</li>
|
||||
<li><strong>Frontend:</strong> Die Klasse <code>.dark-mode</code> am <code><html></code>
|
||||
Tag schaltet CSS-Variablen um.</li>
|
||||
<li><strong>Toggle:</strong> Über den Service <code>toggle_theme</code> steuerbar.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 5. Klassen -->
|
||||
<section id="class-auth">
|
||||
<h2>5.1 Auth Klasse</h2>
|
||||
<p>Zentrales Identitätsmanagement:</p>
|
||||
<pre><code>Auth::login($user, $pass); // Login-Versuch
|
||||
Auth::check(); // Eingeloggt?
|
||||
Auth::isAdmin(); // Admin-Rechte?
|
||||
Auth::user(); // Gibt User-Array zurück</code></pre>
|
||||
</section>
|
||||
|
||||
<section id="class-form">
|
||||
<h2>5.2 Form Generator</h2>
|
||||
<p>Erstellt konsistente HTML-Formulare mit Bootstrap-ähnlichem Styling:</p>
|
||||
<pre><code>echo Form::open('service_name');
|
||||
echo Form::input('email', 'E-Mail', 'email');
|
||||
echo Form::textarea('msg', 'Nachricht');
|
||||
echo Form::submit('Senden');
|
||||
echo Form::close();</code></pre>
|
||||
</section>
|
||||
|
||||
<section id="class-storage">
|
||||
<h2>5.3 Storage & File Handling</h2>
|
||||
<p>Abstraktionslayer für das Dateisystem:</p>
|
||||
<ul>
|
||||
<li><strong>Upload:</strong> <code>Storage::upload('file_input')</code> speichert sicher in
|
||||
<code>/storage/uploads/</code>.
|
||||
</li>
|
||||
<li><strong>Download:</strong> <code>Storage::download('path/to/file')</code> erzwingt
|
||||
Datei-Download mit korrekten Headern.</li>
|
||||
<li><strong>Listing:</strong> <code>Storage::listFiles()</code> gibt Array aller verfügbaren
|
||||
Dateien zurück.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="class-mailer">
|
||||
<h2>5.4 Mailer Klasse</h2>
|
||||
<p>Versand von System-Mails:</p>
|
||||
<ul>
|
||||
<li><strong>Log:</strong> Jede Mail wird (wenn möglich) in der DB-Tabelle <code>Mailing</code>
|
||||
protokolliert.</li>
|
||||
<li><strong>Fallback:</strong> Bei DB-Fehlern erfolgt automatisches Logging in
|
||||
<code>logs/mails.log</code>.
|
||||
</li>
|
||||
<li><strong>Error-Mail:</strong> Spezielle <code>error()</code> Methode für kritische
|
||||
Systembenachrichtigungen.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="class-logger">
|
||||
<h2>5.5 Logger & Debugging</h2>
|
||||
<p>Zentrales Fehler-Management:</p>
|
||||
<ul>
|
||||
<li><strong>Auto-Catch:</strong> Registriert sich als globaler Exception- und Error-Handler.
|
||||
</li>
|
||||
<li><strong>Tracing:</strong> Schreibt vollständige Stacktraces in
|
||||
<code>WEBROOT/logs/error.log</code>.
|
||||
</li>
|
||||
<li><strong>Manuell:</strong> <code>Logger::log('Nachricht', 'LEVEL')</code> für eigenes
|
||||
Logging.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 6. Tools -->
|
||||
<section id="cli-tools">
|
||||
<h2>6.1 CLI Werkzeuge (make.php)</h2>
|
||||
<p>Das Schweizer Taschenmesser für die Entwicklung:</p>
|
||||
<ul>
|
||||
<li><code>-c, --controller</code>: Nur den Controller erstellen.</li>
|
||||
<li><code>-v, --view</code>: Nur die View erstellen.</li>
|
||||
<li><code>-r, --route</code>: Nur die Route registrieren.</li>
|
||||
<li><code>-n, --nav</code>: Nur den Navigationspunkt hinzufügen.</li>
|
||||
<li><code>-a, --auth</code>: Erfordert Login für diesen Service.</li>
|
||||
<li><code>-l, --label "Text"</code>: Anzeigename in der Sidebar.</li>
|
||||
</ul>
|
||||
<p>How to Use:</p>
|
||||
<ul>
|
||||
<li><code>php make.php module [Name]</code>: Erstellt Controller, View, Route und Nav-Eintrag in
|
||||
einem Rutsch.</li>
|
||||
<li><code>php make.php model [Tabelle] [Spalten]</code>: Generiert fertige Model-Klassen inkl.
|
||||
Spaltendokumentation.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="api-dev">
|
||||
<h2>6.2 API Entwicklung</h2>
|
||||
<p>Standardisierte JSON-Antworten über die <code>Api</code> Klasse:</p>
|
||||
<pre><code>// In src/MVC/Classes/Api.php
|
||||
Api::jsonResponse(['data' => '...'], 200);</code></pre>
|
||||
<p>API-Files befinden sich übersichtlich in <code>WEBROOT/api/v1/</code>.</p>
|
||||
</section>
|
||||
|
||||
<section id="best-practices">
|
||||
<h2>6.3 Best Practices</h2>
|
||||
<ul>
|
||||
<li><strong>Security:</strong> Keine Variablen direkt in SQL-Strings (Prepared Statements
|
||||
nutzen!).</li>
|
||||
<li><strong>Output:</strong> Variablen in Views immer mit <code>htmlspecialchars()</code>
|
||||
maskieren.</li>
|
||||
<li><strong>Architecture:</strong> Controller rufen Models auf, Views zeigen nur Daten an. Keine
|
||||
DB-Logik in der View!</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.docs-container {
|
||||
max-width: 1100px;
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.toc {
|
||||
background: var(--sidebar);
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 40px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.toc h3 {
|
||||
margin-top: 0;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
display: inline-block;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.toc-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.toc ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.toc ul ul {
|
||||
padding-left: 20px;
|
||||
list-style: disc;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.toc a {
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.toc a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 60px;
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--accent);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
h2::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 30px;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 25px;
|
||||
color: var(--text);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #333;
|
||||
font-size: 0.9rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
pre::after {
|
||||
content: 'CODE';
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 10px;
|
||||
font-size: 0.7rem;
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Consolas', monospace;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-bottom: 20px;
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toc-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
<h1>Dashboard</h1><p>Willkommen in deinem neuen MVC-System mit integriertem Header.</p>
|
||||
@@ -0,0 +1,31 @@
|
||||
<style>
|
||||
.login-container { max-width: 400px; margin: 50px auto; padding: 30px; border: 1px solid
|
||||
var(--border); border-radius: 8px; background: var(--sidebar); box-shadow: 0 4px 15px
|
||||
rgba(0,0,0,0.1); }
|
||||
.login-container h2 { text-align: center; margin-top: 0; color: var(--text); }
|
||||
.login-container input { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid
|
||||
var(--border); border-radius: 4px; box-sizing: border-box; background: var(--bg); color:
|
||||
var(--text); }
|
||||
.login-container button { width: 100%; padding: 12px; background: var(--header); color: white;
|
||||
border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; font-weight: bold; }
|
||||
.login-container button:hover { opacity: 0.9; }
|
||||
.error-msg { color: #e74c3c; background: rgba(231, 76, 60, 0.1); padding: 10px; border-radius: 4px;
|
||||
margin-bottom: 15px; text-align: center; font-size: 0.9rem; }
|
||||
</style>
|
||||
<div class="login-container">
|
||||
<h2>🔑 Login</h2>
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="error-msg"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<div>
|
||||
<label>Benutzername</label>
|
||||
<input type="text" name="username" required autofocus placeholder="Admin / User">
|
||||
</div>
|
||||
<div>
|
||||
<label>Passwort</label>
|
||||
<input type="password" name="password" required placeholder="********">
|
||||
</div>
|
||||
<button type="submit">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,147 @@
|
||||
<h2>📝 Daten-Formular</h2>
|
||||
|
||||
<?php use MVC\Classes\Form; ?>
|
||||
|
||||
<?php if (isset($message)): ?>
|
||||
<div style="padding:15px;margin-bottom:20px;border-radius:5px;background:rgba(46,204,113,0.2);">
|
||||
<?= $message['text']?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div
|
||||
style="padding:15px;margin-bottom:20px;border-radius:5px;background:rgba(231,76,60,0.2);color:#c0392b;">
|
||||
<ul>
|
||||
<?php foreach($errors as $error): ?>
|
||||
<li><?= $error ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<?= Form::open($current_service) ?>
|
||||
<div class="form-row">
|
||||
<div class="form-group col">
|
||||
<?= Form::input('firstname', 'Vorname', 'text', $_POST['firstname'] ?? '', ['placeholder' => 'Max', 'required' => true]) ?>
|
||||
</div>
|
||||
<div class="form-group col">
|
||||
<?= Form::input('lastname', 'Nachname', 'text', $_POST['lastname'] ?? '', ['placeholder' => 'Mustermann', 'required' => true]) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= Form::input('email', 'E-Mail Adresse', 'email', $_POST['email'] ?? '', ['placeholder' => 'max@example.com', 'required' => true]) ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="category">Kategorie</label>
|
||||
<select name="category" id="category" class="form-control">
|
||||
<option value="support">Support</option>
|
||||
<option value="sales">Vertrieb</option>
|
||||
<option value="other">Sonstiges</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Interessen</label>
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-label"><input type="checkbox" name="interests[]"
|
||||
value="coding"> Coding</label>
|
||||
<label class="checkbox-label"><input type="checkbox" name="interests[]"
|
||||
value="design"> Design</label>
|
||||
<label class="checkbox-label"><input type="checkbox" name="interests[]"
|
||||
value="marketing"> Marketing</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Newsletter</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-label"><input type="radio" name="newsletter" value="yes"
|
||||
checked> Ja, ich möchte den Newsletter</label>
|
||||
<label class="radio-label"><input type="radio" name="newsletter" value="no"> Nein,
|
||||
danke</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= Form::submit('Daten speichern') ?>
|
||||
<button type="reset" class="btn btn-secondary">Abbrechen</button>
|
||||
<?= Form::close() ?>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
background: var(--sidebar);
|
||||
padding: 25px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.checkbox-group,
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.checkbox-label,
|
||||
.radio-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<h2>📊 Datenübersicht</h2>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder="Suchen..." id="tableSearch" class="form-control">
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary">+ Neuer Eintrag</button>
|
||||
<button class="btn btn-secondary">Exportieren</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Datum</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php for($i=1; $i<=5; $i++): ?>
|
||||
<tr>
|
||||
<td>#<?= 1000 + $i ?></td>
|
||||
<td><strong>Beispiel Datensatz <?= $i ?></strong></td>
|
||||
<td><span class="badge badge-success">Aktiv</span></td>
|
||||
<td><?= date('d.m.Y') ?></td>
|
||||
<td>
|
||||
<button class="btn-icon">✏️</button>
|
||||
<button class="btn-icon">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endfor; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--sidebar);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--sidebar);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
text-align: left;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #2ecc71;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<h2>📥 Datei Downloads
|
||||
</h2>
|
||||
|
||||
<div class="card">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Dateiname</th>
|
||||
<th>Größe</th>
|
||||
<th>Datum</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($files)): ?>
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center; padding: 20px;">Keine Dateien
|
||||
verfügbar.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($files as $file): ?>
|
||||
<tr>
|
||||
<td><strong><?= htmlspecialchars($file['name']) ?></strong></td>
|
||||
<td><?= htmlspecialchars($file['size']) ?> KB</td>
|
||||
<td><?= htmlspecialchars($file['date']) ?></td>
|
||||
<td>
|
||||
<a href="?service=download_file&file=<?= urlencode($file['name']) ?>"
|
||||
class="btn-download">💾 Download</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
background: var(--sidebar);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
text-align: left;
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
color: var(--text);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background: rgba(0, 0, 0, 0.01);
|
||||
}
|
||||
|
||||
.btn-download {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-download:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<h2>📤 Datei Upload</h2>
|
||||
|
||||
<?php use MVC\Classes\Form; ?>
|
||||
|
||||
<?php if (isset($message)): ?>
|
||||
<div
|
||||
class="alert <?= $message['type'] === 'success' ? 'alert-success' : 'alert-error' ?>">
|
||||
<?= htmlspecialchars($message['text']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<?= Form::open($current_service, 'POST', ['enctype' => 'multipart/form-data']) ?>
|
||||
|
||||
<?= Form::input('file_upload', 'Datei auswählen', 'file', '', ['required' => true]) ?>
|
||||
|
||||
<?= Form::textarea('description', 'Beschreibung (optional)', '', ['placeholder' => 'Kurze Beschreibung der Datei...']) ?>
|
||||
|
||||
<?= Form::submit('Hochladen') ?>
|
||||
|
||||
<?= Form::close() ?>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
background: var(--sidebar);
|
||||
padding: 25px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--accent);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 5px solid;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: rgba(46, 204, 113, 0.1);
|
||||
border-color: #2ecc71;
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: rgba(231, 76, 60, 0.1);
|
||||
border-color: #e74c3c;
|
||||
color: #c0392b;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
<h1>Service 1</h1><p><?= $msg ?></p>
|
||||
@@ -0,0 +1 @@
|
||||
<h1>System Check</h1><ul><?php foreach($data as $k => $v): ?><li><b><?= $k ?>:</b> <?= $v ?></li><?php endforeach; ?></ul>
|
||||
@@ -0,0 +1,351 @@
|
||||
<h2>⏱️ WebTime – Zeiterfassung</h2>
|
||||
|
||||
<!-- STATUS-KARTE -->
|
||||
<div class="status-card <?= $isRunning ? 'running' : 'stopped' ?>">
|
||||
<?php if ($isRunning && $openEntry): ?>
|
||||
<div class="status-info">
|
||||
<span class="status-dot"></span>
|
||||
<strong>Läuft seit <?= htmlspecialchars($openEntry['BEGIN']) ?> Uhr</strong>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="stop">
|
||||
<div class="aufgaben-grid">
|
||||
<input type="text" name="a_1"
|
||||
value="<?= htmlspecialchars($openEntry['A_1'] ?? '') ?>"
|
||||
placeholder="Aufgabe 1" class="aufgabe-input">
|
||||
<input type="text" name="a_2"
|
||||
value="<?= htmlspecialchars($openEntry['A_2'] ?? '') ?>"
|
||||
placeholder="Aufgabe 2" class="aufgabe-input">
|
||||
<input type="text" name="a_3"
|
||||
value="<?= htmlspecialchars($openEntry['A_3'] ?? '') ?>"
|
||||
placeholder="Aufgabe 3" class="aufgabe-input">
|
||||
<input type="text" name="a_4"
|
||||
value="<?= htmlspecialchars($openEntry['A_4'] ?? '') ?>"
|
||||
placeholder="Aufgabe 4" class="aufgabe-input">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-stop">⏹ Ausstempeln</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<div class="status-info">
|
||||
<span class="status-dot"></span>
|
||||
<strong>Nicht eingestempelt</strong>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="start">
|
||||
<button type="submit" class="btn btn-start">▶ Einstempeln</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- TABELLE -->
|
||||
<div class="card" style="margin-top: 30px;">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Beginn</th>
|
||||
<th>Ende</th>
|
||||
<th>Dauer</th>
|
||||
<th>Aufgaben</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($eintraege as $e):
|
||||
$dauer = '–';
|
||||
if (!empty($e['BEGIN']) && !empty($e['ENDE'])) {
|
||||
$diff = strtotime($e['ENDE']) - strtotime($e['BEGIN']);
|
||||
if ($diff > 0) {
|
||||
$dauer = sprintf('%02d:%02d h', floor($diff / 3600), floor(($diff % 3600) / 60));
|
||||
}
|
||||
}
|
||||
$aufgaben = array_filter([
|
||||
$e['A_1'] ?? null,
|
||||
$e['A_2'] ?? null,
|
||||
$e['A_3'] ?? null,
|
||||
$e['A_4'] ?? null,
|
||||
]);
|
||||
?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($e['DATUM'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($e['BEGIN'] ?? '') ?></td>
|
||||
<td>
|
||||
<?php if (!empty($e['ENDE'])): ?>
|
||||
<?= htmlspecialchars($e['ENDE']) ?>
|
||||
<?php else: ?>
|
||||
<span class="badge-running">läuft</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $dauer ?></td>
|
||||
<td>
|
||||
<?php foreach ($aufgaben as $a): ?>
|
||||
<span class="tag"><?= htmlspecialchars($a) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn-icon"
|
||||
onclick='openEdit(<?= htmlspecialchars(json_encode($e), ENT_QUOTES) ?>)'>✏️</button>
|
||||
<form method="POST" style="display:inline"
|
||||
onsubmit="return confirm('Eintrag löschen?')">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="entry_id" value="<?= (int)$e['ID'] ?>">
|
||||
<button type="submit" class="btn-icon">🗑️</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($eintraege)): ?>
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:center; padding: 20px; opacity: 0.5;">
|
||||
Noch keine Einträge vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- EDIT MODAL -->
|
||||
<div id="editModal" class="modal" style="display:none">
|
||||
<div class="modal-box">
|
||||
<h3 style="margin-top:0">✏️ Eintrag bearbeiten</h3>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="edit">
|
||||
<input type="hidden" name="entry_id" id="edit_id">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Datum</label>
|
||||
<input type="text" name="datum" id="edit_datum"
|
||||
class="form-control" placeholder="01.01.2025">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Beginn</label>
|
||||
<input type="text" name="begin" id="edit_begin"
|
||||
class="form-control" placeholder="08:00">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Ende</label>
|
||||
<input type="text" name="ende" id="edit_ende"
|
||||
class="form-control" placeholder="17:00">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Aufgabe 1</label>
|
||||
<input type="text" name="a_1" id="edit_a_1" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Aufgabe 2</label>
|
||||
<input type="text" name="a_2" id="edit_a_2" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Aufgabe 3</label>
|
||||
<input type="text" name="a_3" id="edit_a_3" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Aufgabe 4</label>
|
||||
<input type="text" name="a_4" id="edit_a_4" class="form-control">
|
||||
</div>
|
||||
<div style="display:flex; gap:10px; margin-top:20px">
|
||||
<button type="submit" class="btn btn-start">💾 Speichern</button>
|
||||
<button type="button" class="btn btn-stop"
|
||||
onclick="document.getElementById('editModal').style.display='none'">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openEdit(e) {
|
||||
document.getElementById('edit_id').value = e.ID ?? '';
|
||||
document.getElementById('edit_datum').value = e.DATUM ?? '';
|
||||
document.getElementById('edit_begin').value = e.BEGIN ?? '';
|
||||
document.getElementById('edit_ende').value = e.ENDE ?? '';
|
||||
document.getElementById('edit_a_1').value = e.A_1 ?? '';
|
||||
document.getElementById('edit_a_2').value = e.A_2 ?? '';
|
||||
document.getElementById('edit_a_3').value = e.A_3 ?? '';
|
||||
document.getElementById('edit_a_4').value = e.A_4 ?? '';
|
||||
document.getElementById('editModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// Modal schließen bei Klick außerhalb
|
||||
document.getElementById('editModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) this.style.display = 'none';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.status-card {
|
||||
padding: 25px 30px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid var(--border);
|
||||
background: var(--sidebar);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.status-card.running { border-color: #2ecc71; }
|
||||
.status-card.stopped { border-color: var(--border); }
|
||||
|
||||
.status-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #95a5a6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.running .status-dot {
|
||||
background: #2ecc71;
|
||||
animation: pulse 1.2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.aufgaben-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.aufgabe-input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.btn-start { background: #2ecc71; color: white; }
|
||||
.btn-stop { background: #e74c3c; color: white; }
|
||||
.btn:hover { opacity: 0.85; }
|
||||
|
||||
.card {
|
||||
background: var(--sidebar);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
text-align: left;
|
||||
padding: 13px 15px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.data-table th {
|
||||
background: rgba(0,0,0,0.04);
|
||||
font-weight: bold;
|
||||
}
|
||||
.data-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
.data-table tr:hover td {
|
||||
background: rgba(0,0,0,0.02);
|
||||
}
|
||||
|
||||
.badge-running {
|
||||
color: #2ecc71;
|
||||
font-weight: bold;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background: rgba(0,123,255,0.1);
|
||||
color: var(--accent);
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.78rem;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-icon:hover {
|
||||
background: rgba(0,0,0,0.07);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
}
|
||||
.modal-box {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 30px;
|
||||
width: 520px;
|
||||
max-width: 95vw;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.form-group {
|
||||
flex: 1;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
box-sizing: border-box;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(0,123,255,0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
spl_autoload_register(function ($class) {
|
||||
$prefix = 'MVC\\';
|
||||
$base_dir = __DIR__ . '/';
|
||||
|
||||
$len = strlen($prefix);
|
||||
if (strncmp($prefix, $class, $len) !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relative_class = substr($class, $len);
|
||||
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
[database]
|
||||
APP_ENV = "dev"
|
||||
|
||||
DB_ROOT_USERNAME = "root"
|
||||
DB_ROOT_PASSWORD = "bBHofurUJWKzPE8Z"
|
||||
|
||||
DB_DEV_HOST = "blackpi.ipv64.net"
|
||||
DB_DEV_USERNAME = "worker"
|
||||
DB_DEV_PASSWORD = "bBHofurUJWKzPE8Z"
|
||||
DB_DEV_DATABASE = "backend"
|
||||
|
||||
DB_LIVE_HOST = ""
|
||||
DB_LIVE_USERNAME = ""
|
||||
DB_LIVE_PASSWORD = ""
|
||||
DB_LIVE_DATABASE = ""
|
||||
|
||||
[mail]
|
||||
MAIL_FROM = "bruno.eckhardt07@gmail.com"
|
||||
MAIL_TO = "bruno.eckhardt07@gmail.com"
|
||||
|
||||
[logging]
|
||||
LOG_ACCESS = false
|
||||
LOG_SECURITY = true
|
||||
BLOCK_PAGE_TEMPLATE = "standard"
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Define your sidebar navigation items here.
|
||||
* Format: 'service_name' => ['label' => 'Label']
|
||||
*/
|
||||
return [
|
||||
'home' => ['label' => '🏠 Home'],
|
||||
'test1' => ['label' => '🛠️ Test 1'],
|
||||
'test2' => ['label' => '📊 Test 2'],
|
||||
'admin_mail' => ['label' => '📧 Mail Service'],
|
||||
'admin_users' => ['label' => '👤 User Service'],
|
||||
'template_upload' => ['label' => '📤 Upload Template'],
|
||||
'template_download' => ['label' => '📥 Download Template'],
|
||||
'template_form' => ['label' => '📝 Form Template'],
|
||||
'template_table' => ['label' => '📊 Table Template'],
|
||||
'docs' => ['label' => '📚 Dokumentation'],
|
||||
'webtime' => ['label' => '⏱️ WebTime'],
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Define your routes here.
|
||||
* Format: $router->add(service_name, controller_class, method_name, options);
|
||||
*/
|
||||
|
||||
// Global Middlewares (executed on every request)
|
||||
$router->addGlobal('global'); // Beispiel: Logging
|
||||
$router->addGlobal('block'); // Beispiel: IP-Blocker
|
||||
|
||||
$router->add('home', \MVC\Controller\HomeController::class, 'index');
|
||||
$router->add('toggle_theme', \MVC\Controller\HomeController::class, 'toggle_theme');
|
||||
$router->add('docs', \MVC\Controller\DocsController::class, 'index');
|
||||
$router->add('test1', \MVC\Controller\TestController::class, 'service1', ['auth']);
|
||||
$router->add('test2', \MVC\Controller\TestController::class, 'service2', ['auth']);
|
||||
$router->add('login', \MVC\Controller\AuthController::class, 'login');
|
||||
$router->add('logout', \MVC\Controller\AuthController::class, 'logout');
|
||||
$router->add('admin_mail', \MVC\Controller\AdminController::class, 'mail_service', ['auth', 'admin']);
|
||||
$router->add('admin_users', \MVC\Controller\AdminController::class, 'user_service', ['auth', 'admin']);
|
||||
$router->add('template_upload', \MVC\Controller\TemplateController::class, 'upload', ['auth']);
|
||||
$router->add('template_download', \MVC\Controller\TemplateController::class, 'download', ['auth']);
|
||||
$router->add('template_form', \MVC\Controller\TemplateController::class, 'data_form', ['auth']);
|
||||
$router->add('template_table', \MVC\Controller\TemplateController::class, 'data_table', ['auth']);
|
||||
$router->add('download_file', \MVC\Controller\TemplateController::class, 'download_file', ['auth']);
|
||||
$router->add('webtime',\MVC\Controller\WebtimeController::class, 'webtime', ['auth']);
|
||||
|
||||
// API Routes (Path-based)
|
||||
$router->add('api/v1/test', \MVC\Controller\ApiController::class, 'get');
|
||||
$router->add('api/v1/become', \MVC\Controller\ApiController::class, 'become');
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
function db_connect() {
|
||||
$config = parse_ini_file(__DIR__ . '/../config/env.ini');
|
||||
$env = strtoupper($config['APP_ENV']);
|
||||
try {
|
||||
$conn = new mysqli(
|
||||
$config["DB_{$env}_HOST"],
|
||||
$config["DB_{$env}_USERNAME"],
|
||||
$config["DB_{$env}_PASSWORD"],
|
||||
$config["DB_{$env}_DATABASE"]
|
||||
);
|
||||
|
||||
$conn->set_charset("utf8mb4");
|
||||
|
||||
return $conn;
|
||||
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function db_connect_root() {
|
||||
$config = parse_ini_file(__DIR__ . '/../config/env.ini');
|
||||
$env = strtoupper($config['APP_ENV']);
|
||||
try {
|
||||
$conn = new mysqli(
|
||||
$config["DB_{$env}_HOST"],
|
||||
$config["DB_ROOT_USERNAME"],
|
||||
$config["DB_ROOT_PASSWORD"]
|
||||
);
|
||||
|
||||
$conn->set_charset("utf8mb4");
|
||||
|
||||
return $conn;
|
||||
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
function DatabaseCreateTable($table, $columnsDefinition) {
|
||||
include_once(__DIR__ . '/../database/databaseConnect.php');
|
||||
$conn = db_connect();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `$table` ($columnsDefinition)";
|
||||
$stmt = $conn->prepare($sql);
|
||||
|
||||
if (!$stmt) {
|
||||
die("Fehler beim Prepare: " . $conn->error);
|
||||
}
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
die("Fehler beim Execute: " . $stmt->error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function DatabaseShowTables() {
|
||||
include_once(__DIR__ . '/../database/databaseConnect.php');
|
||||
$conn = db_connect();
|
||||
|
||||
$tablesResult = $conn->query("SHOW TABLES");
|
||||
if (!$tablesResult) {
|
||||
die("Fehler beim Laden der Tabellen: " . $conn->error);
|
||||
}
|
||||
|
||||
$tables = [];
|
||||
|
||||
while ($tableRow = $tablesResult->fetch_array()) {
|
||||
$tables[] = $tableRow[0];
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
function DatabaseShowTableStructure($table) {
|
||||
include_once(__DIR__ . '/../database/databaseConnect.php');
|
||||
$conn = db_connect();
|
||||
|
||||
$stmt = $conn->prepare("SHOW COLUMNS FROM `$table`");
|
||||
if (!$stmt) {
|
||||
die("Fehler beim Prepare: " . $conn->error);
|
||||
}
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
die("Fehler beim Execute: " . $stmt->error);
|
||||
}
|
||||
|
||||
$result = $stmt->get_result();
|
||||
|
||||
$columns = [];
|
||||
|
||||
while ($column = $result->fetch_assoc()) {
|
||||
$columns[] = [
|
||||
"Field" => $column["Field"],
|
||||
"Type" => $column["Type"],
|
||||
"Null" => $column["Null"],
|
||||
"Key" => $column["Key"],
|
||||
"Default" => $column["Default"],
|
||||
"Extra" => $column["Extra"]
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
function DatabaseShow($table, $columns = "*", $where = "", ...$values) {
|
||||
include_once(__DIR__ . '/../database/databaseConnect.php');
|
||||
$conn = db_connect();
|
||||
|
||||
$sql = "SELECT $columns FROM `$table`";
|
||||
|
||||
if (!empty($where)) {
|
||||
$sql .= " WHERE $where";
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
|
||||
if (!$stmt) {
|
||||
die("Fehler beim Prepare: " . $conn->error);
|
||||
}
|
||||
|
||||
if (!$stmt->execute($values)) {
|
||||
die("Fehler beim Execute: " . $stmt->error);
|
||||
}
|
||||
|
||||
$result = $stmt->get_result();
|
||||
return $result->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
|
||||
|
||||
function DatabaseInsert($table, $columns, ...$values) {
|
||||
include_once(__DIR__ . '/../database/databaseConnect.php');
|
||||
$conn = db_connect();
|
||||
|
||||
$placeholders = rtrim(str_repeat("?,", count($values)), ",");
|
||||
$sql = "INSERT INTO `$table` ($columns) VALUES ($placeholders)";
|
||||
$stmt = $conn->prepare($sql);
|
||||
|
||||
if (!$stmt) {
|
||||
die("Fehler beim Prepare: " . $conn->error);
|
||||
}
|
||||
|
||||
if (!$stmt->execute($values)) {
|
||||
die("Fehler beim Execute: " . $stmt->error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function DatabaseDelete($table, $where, ...$values) {
|
||||
include_once(__DIR__ . '/../database/databaseConnect.php');
|
||||
$conn = db_connect();
|
||||
|
||||
$sql = "DELETE FROM `$table` WHERE $where";
|
||||
$stmt = $conn->prepare($sql);
|
||||
|
||||
if (!$stmt) {
|
||||
die("Fehler beim Prepare: " . $conn->error);
|
||||
}
|
||||
|
||||
if (!$stmt->execute($values)) {
|
||||
die("Fehler beim Execute: " . $stmt->error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function DatabaseUpdate($table, $set, $where, ...$values) {
|
||||
include_once(__DIR__ . '/../database/databaseConnect.php');
|
||||
$conn = db_connect();
|
||||
|
||||
$sql = "UPDATE `$table` SET $set WHERE $where";
|
||||
$stmt = $conn->prepare($sql);
|
||||
|
||||
if (!$stmt) {
|
||||
die("Fehler beim Prepare: " . $conn->error);
|
||||
}
|
||||
|
||||
if (!$stmt->execute($values)) {
|
||||
die("Fehler beim Execute: " . $stmt->error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getFKValue($conn, $table, $key, $value, $id, &$cache) {
|
||||
if ($id === null) return null;
|
||||
|
||||
$cacheKey = $table . "_" . $id;
|
||||
|
||||
if (isset($cache[$cacheKey])) {
|
||||
return $cache[$cacheKey];
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("SELECT `$value` FROM `$table` WHERE `$key` = ? LIMIT 1");
|
||||
if (!$stmt) {
|
||||
die("FK Prepare Fehler: " . $conn->error);
|
||||
}
|
||||
|
||||
$stmt->bind_param("i", $id);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->get_result()->fetch_assoc();
|
||||
$valueResult = $result[$value] ?? null;
|
||||
|
||||
$cache[$cacheKey] = $valueResult;
|
||||
|
||||
return $valueResult;
|
||||
}
|
||||
|
||||
function DatabaseShowWithFK($table, $fkMap = [], $columns = "*", $where = "", ...$values) {
|
||||
include_once(__DIR__ . '/../database/databaseConnect.php');
|
||||
$conn = db_connect();
|
||||
|
||||
$cache = [];
|
||||
|
||||
$select = is_array($columns) ? implode(", ", $columns) : $columns;
|
||||
|
||||
$sql = "SELECT $select FROM `$table`";
|
||||
if (!empty($where)) {
|
||||
$sql .= " WHERE $where";
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
if (!$stmt) die("Fehler beim Prepare: " . $conn->error);
|
||||
|
||||
if (!empty($values)) {
|
||||
$stmt->execute($values);
|
||||
} else {
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
$result = $stmt->get_result();
|
||||
if (!$result) die("Fehler beim get_result: " . $conn->error);
|
||||
|
||||
$rows = $result->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
foreach ($fkMap as $fkColumn => $fkInfo) {
|
||||
|
||||
if (!isset($row[$fkColumn]) || $row[$fkColumn] === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row[$fkColumn] = getFKValue(
|
||||
$conn,
|
||||
$fkInfo['table'],
|
||||
$fkInfo['key'],
|
||||
$fkInfo['value'],
|
||||
$row[$fkColumn],
|
||||
$cache
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
"rows" => $rows
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
if (php_sapi_name() !== 'cli') die("CLI only.\n");
|
||||
|
||||
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||
|
||||
require_once __DIR__ . '/WEBROOT/src/database/databaseConnect.php';
|
||||
require_once __DIR__ . '/WEBROOT/src/utils/databaseMethods.php';
|
||||
require_once __DIR__ . '/WEBROOT/src/MVC/Core/Model.php';
|
||||
|
||||
// ─── Config ──────────────────────────────────────────────────────────────────
|
||||
$configPath = __DIR__ . '/WEBROOT/src/config/env.ini';
|
||||
if (!file_exists($configPath)) die("❌ env.ini nicht gefunden: $configPath\n");
|
||||
|
||||
$config = parse_ini_file($configPath);
|
||||
if ($config === false) die("❌ env.ini konnte nicht geparst werden.\n");
|
||||
|
||||
$env = strtoupper($config['APP_ENV'] ?? '');
|
||||
$dbName = $config["DB_{$env}_DATABASE"] ?? '';
|
||||
$workerUser = $config["DB_{$env}_USERNAME"] ?? '';
|
||||
$workerPass = $config["DB_{$env}_PASSWORD"] ?? '';
|
||||
|
||||
if (!$env || !$dbName || !$workerUser || !$workerPass) {
|
||||
die("❌ Unvollständige Konfiguration in env.ini.\n");
|
||||
}
|
||||
|
||||
echo "🚀 Starte Datenbank-Setup als Root...\n";
|
||||
echo " Umgebung : $env\n";
|
||||
echo " Datenbank: $dbName\n";
|
||||
echo " DB-User : $workerUser\n\n";
|
||||
|
||||
// ─── Root-Verbindung ─────────────────────────────────────────────────────────
|
||||
$rootConn = db_connect_root();
|
||||
if (!$rootConn) die("❌ Root-Verbindung fehlgeschlagen.\n");
|
||||
|
||||
try {
|
||||
// 1. Datenbank
|
||||
echo "📁 Erstelle Datenbank '$dbName'...\n";
|
||||
$rootConn->query("CREATE DATABASE IF NOT EXISTS `$dbName` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
echo " ✅ Datenbank bereit.\n\n";
|
||||
|
||||
// 2. Worker-User (DROP + CREATE = sauber, kein GRANT-Fehler)
|
||||
echo "👤 Lege Worker-User '$workerUser' an...\n";
|
||||
foreach (['localhost', '%'] as $host) {
|
||||
$rootConn->query("DROP USER IF EXISTS '$workerUser'@'$host'");
|
||||
$rootConn->query("CREATE USER '$workerUser'@'$host' IDENTIFIED BY '$workerPass'");
|
||||
echo " ✅ '$workerUser'@'$host' erstellt.\n";
|
||||
}
|
||||
|
||||
// 3. Berechtigungen
|
||||
echo "\n🔐 Vergebe Berechtigungen...\n";
|
||||
foreach (['localhost', '%'] as $host) {
|
||||
$rootConn->query("GRANT ALL PRIVILEGES ON `$dbName`.* TO '$workerUser'@'$host'");
|
||||
echo " ✅ GRANT für '$workerUser'@'$host'.\n";
|
||||
}
|
||||
$rootConn->query("FLUSH PRIVILEGES");
|
||||
echo " ✅ FLUSH PRIVILEGES.\n\n";
|
||||
|
||||
} catch (\mysqli_sql_exception $e) {
|
||||
die("❌ Root-Setup fehlgeschlagen: " . $e->getMessage() . "\n");
|
||||
} finally {
|
||||
$rootConn->close();
|
||||
}
|
||||
|
||||
// ─── Tabellen + Models ───────────────────────────────────────────────────────
|
||||
echo "🚀 Tabellen-Installation...\n\n";
|
||||
|
||||
$tables = [
|
||||
'Mailing' => [
|
||||
'columns' => implode(', ', [
|
||||
'`ID` INT AUTO_INCREMENT PRIMARY KEY',
|
||||
'`DATUM` DATETIME NOT NULL',
|
||||
'`SUBJECT` VARCHAR(255)',
|
||||
'`MESSAGE` TEXT',
|
||||
'`URL` VARCHAR(255)',
|
||||
'`CLIENT_IP` VARCHAR(45)',
|
||||
]),
|
||||
'model' => <<<'PHP'
|
||||
<?php
|
||||
|
||||
namespace MVC\Model;
|
||||
|
||||
use MVC\Core\Model;
|
||||
|
||||
class Mailing extends Model
|
||||
{
|
||||
protected static $table = "Mailing";
|
||||
|
||||
public static function log(
|
||||
string $subject,
|
||||
string $message,
|
||||
string $url,
|
||||
string $ip,
|
||||
string $date
|
||||
): bool {
|
||||
return self::insert([
|
||||
'DATUM' => $date,
|
||||
'SUBJECT' => $subject,
|
||||
'MESSAGE' => $message,
|
||||
'URL' => $url,
|
||||
'CLIENT_IP' => $ip,
|
||||
]);
|
||||
}
|
||||
}
|
||||
PHP
|
||||
],
|
||||
|
||||
'Users' => [
|
||||
'columns' => implode(', ', [
|
||||
'`ID` INT AUTO_INCREMENT PRIMARY KEY',
|
||||
'`USERNAME` VARCHAR(50) UNIQUE NOT NULL',
|
||||
'`PASSWORD` VARCHAR(255) NOT NULL',
|
||||
'`EMAIL` VARCHAR(100)',
|
||||
'`ROLE` VARCHAR(20) DEFAULT \'user\'',
|
||||
'`CREATED_AT` DATETIME DEFAULT CURRENT_TIMESTAMP',
|
||||
]),
|
||||
'model' => <<<'PHP'
|
||||
<?php
|
||||
|
||||
namespace MVC\Model;
|
||||
|
||||
use MVC\Core\Model;
|
||||
|
||||
class Users extends Model
|
||||
{
|
||||
protected static $table = "Users";
|
||||
|
||||
public static function getByUsername(string $username): ?array
|
||||
{
|
||||
$result = self::where("USERNAME = ?", $username);
|
||||
return $result[0] ?? null;
|
||||
}
|
||||
}
|
||||
PHP
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($tables as $name => $info) {
|
||||
echo "--- Tabelle: $name ---\n";
|
||||
try {
|
||||
$created = DatabaseCreateTable($name, $info['columns']);
|
||||
|
||||
if ($created === false || $created === null) {
|
||||
echo "❌ Tabelle '$name': DatabaseCreateTable schlug fehl.\n\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = __DIR__ . "/WEBROOT/src/MVC/Model/{$name}.php";
|
||||
|
||||
if (!file_exists($path)) {
|
||||
$dir = dirname($path);
|
||||
if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
|
||||
echo "❌ Verzeichnis konnte nicht erstellt werden: $dir\n\n";
|
||||
continue;
|
||||
}
|
||||
if (file_put_contents($path, $info['model']) === false) {
|
||||
echo "❌ Model-Datei konnte nicht geschrieben werden: $path\n\n";
|
||||
continue;
|
||||
}
|
||||
echo "✅ Tabelle + Model '$name' erstellt.\n\n";
|
||||
} else {
|
||||
echo "ℹ️ Model '$name' existiert bereits.\n\n";
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
echo "❌ Fehler bei '$name': " . $e->getMessage() . "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Test-User ───────────────────────────────────────────────────────────────
|
||||
echo "👤 Erstelle Test-User 'admin'...\n";
|
||||
|
||||
$usersModelPath = __DIR__ . '/WEBROOT/src/MVC/Model/Users.php';
|
||||
|
||||
if (!file_exists($usersModelPath)) {
|
||||
echo "❌ Users.php nicht gefunden – überspringe User-Anlage.\n";
|
||||
} else {
|
||||
require_once $usersModelPath;
|
||||
|
||||
try {
|
||||
if (!\MVC\Model\Users::getByUsername('admin')) {
|
||||
\MVC\Model\Users::insert([
|
||||
'USERNAME' => 'admin',
|
||||
'PASSWORD' => password_hash('admin123', PASSWORD_BCRYPT),
|
||||
'EMAIL' => 'admin@example.com',
|
||||
'ROLE' => 'admin',
|
||||
]);
|
||||
echo "✅ Test-User 'admin' erstellt.\n";
|
||||
echo "⚠️ Passwort nach erstem Login bitte ändern!\n";
|
||||
} else {
|
||||
echo "ℹ️ Test-User 'admin' existiert bereits.\n";
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
echo "❌ Fehler beim Test-User: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n✨ Installation abgeschlossen.\n";
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
/**
|
||||
* MVC Unified CLI Tool
|
||||
* Nutzung: php make.php <command> [args]
|
||||
*/
|
||||
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
die("Dieses Script muss über die CLI ausgeführt werden.\n");
|
||||
}
|
||||
|
||||
$command = $argv[1] ?? null;
|
||||
|
||||
if (!$command || in_array($command, ['help', '-h', '--help'])) {
|
||||
showHelp();
|
||||
exit;
|
||||
}
|
||||
|
||||
switch ($command) {
|
||||
case 'model':
|
||||
createModel(array_slice($argv, 2));
|
||||
break;
|
||||
case 'module':
|
||||
createModule(array_slice($argv, 2));
|
||||
break;
|
||||
default:
|
||||
echo "❌ Unbekannter Befehl: $command\n";
|
||||
showHelp();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function showHelp() {
|
||||
echo "MVC CLI Tool\n";
|
||||
echo "-----------\n";
|
||||
echo "Nutzung: php make.php <befehl> [optionen]\n\n";
|
||||
echo "Befehle:\n";
|
||||
echo " model Erstellt ein neues Model basierend auf einer Datenbanktabelle\n";
|
||||
echo " module Erstellt ein neues Modul (Controller, View, Route, Navigation)\n\n";
|
||||
echo "Details zu model:\n";
|
||||
echo " php make.php model <TabellenName> <Spalte1,Spalte2,...>\n\n";
|
||||
echo "Details zu module:\n";
|
||||
echo " php make.php module <Name> [optionen]\n";
|
||||
echo " Optionen:\n";
|
||||
echo " -c, --controller Nur Controller erstellen\n";
|
||||
echo " -v, --view Nur View erstellen\n";
|
||||
echo " -r, --route Nur Route hinzufügen\n";
|
||||
echo " -n, --nav Nur Navigationspunkt hinzufügen\n";
|
||||
echo " -a, --auth Route/Nav erfordert Login\n";
|
||||
echo " -l, --label Label für Navigationspunkt\n";
|
||||
}
|
||||
|
||||
function createModel($args) {
|
||||
$tableName = $args[0] ?? null;
|
||||
$columnsInput = $args[1] ?? null;
|
||||
|
||||
// Interaktive Abfrage, falls Argumente fehlen
|
||||
if (!$tableName) {
|
||||
echo "Tabellenname eingeben (z.B. Benutzer): ";
|
||||
$handle = fopen("php://stdin", "r");
|
||||
$tableName = trim(fgets($handle));
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
if (!$columnsInput) {
|
||||
echo "Spalten kommagetrennt eingeben (z.B. NAME,EMAIL,ROLLE): ";
|
||||
$handle = fopen("php://stdin", "r");
|
||||
$columnsInput = trim(fgets($handle));
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
if (!$tableName) die("❌ Fehler: Kein Tabellenname angegeben.\n");
|
||||
|
||||
// Namen formatieren
|
||||
$className = str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($tableName))));
|
||||
$modelFile = __DIR__ . "/WEBROOT/src/MVC/Model/{$className}.php";
|
||||
|
||||
// Spalten aufbereiten
|
||||
$columns = array_map('trim', explode(',', $columnsInput));
|
||||
$columnList = implode("', '", $columns);
|
||||
|
||||
// Model-Inhalt generieren
|
||||
$content = <<<PHPMODEL
|
||||
<?php
|
||||
|
||||
namespace MVC\Model;
|
||||
|
||||
use MVC\Core\Model;
|
||||
|
||||
/**
|
||||
* Model für die Tabelle '{$tableName}'
|
||||
* Verfügbare Spalten: '{$columnList}'
|
||||
*/
|
||||
class {$className} extends Model {
|
||||
protected static \$table = "{$tableName}";
|
||||
|
||||
// Hier kannst du spezifische Methoden für dieses Model hinzufügen
|
||||
|
||||
/**
|
||||
* Beispiel: Suche nach einer spezifischen Spalte
|
||||
*/
|
||||
public static function getByFirstColumn(\$value) {
|
||||
return self::where("{$columns[0]} = ?", \$value);
|
||||
}
|
||||
}
|
||||
PHPMODEL;
|
||||
|
||||
// Datei schreiben
|
||||
if (!is_dir(dirname($modelFile))) {
|
||||
mkdir(dirname($modelFile), 0755, true);
|
||||
}
|
||||
|
||||
if (!file_exists($modelFile)) {
|
||||
file_put_contents($modelFile, $content);
|
||||
echo "✅ Model erstellt: WEBROOT/src/MVC/Model/{$className}.php\n";
|
||||
echo "--- INFO ---\n";
|
||||
echo "Tabelle: {$tableName}\n";
|
||||
echo "Klasse: MVC\\Model\\{$className}\n";
|
||||
echo "Spalten: " . implode(', ', $columns) . "\n";
|
||||
} else {
|
||||
echo "⚠️ Model {$className}.php existiert bereits.\n";
|
||||
}
|
||||
}
|
||||
|
||||
function createModule($args) {
|
||||
// Da wir args als Slice übergeben, müssen wir sie manuell parsen
|
||||
$name = null;
|
||||
$manualFlags = [];
|
||||
$label = null;
|
||||
|
||||
for ($i = 0; $i < count($args); $i++) {
|
||||
$arg = $args[$i];
|
||||
if ($arg === '-l' || $arg === '--label') {
|
||||
$label = $args[++$i] ?? null;
|
||||
} else if (strpos($arg, '-') === 0) {
|
||||
$flag = ltrim($arg, '-');
|
||||
$manualFlags[] = $flag;
|
||||
} else if ($name === null) {
|
||||
$name = $arg;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$name) {
|
||||
echo "❌ Fehler: Kein Modulname angegeben.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$hasFlag = function($short, $long) use ($manualFlags) {
|
||||
return in_array($short, $manualFlags) || in_array($long, $manualFlags);
|
||||
};
|
||||
|
||||
$doController = $hasFlag('c', 'controller');
|
||||
$doView = $hasFlag('v', 'view');
|
||||
$doRoute = $hasFlag('r', 'route');
|
||||
$doNav = $hasFlag('n', 'nav');
|
||||
$isAuth = $hasFlag('a', 'auth');
|
||||
|
||||
// Wenn gar keine spezifischen Flags gesetzt wurden, mache alles
|
||||
if (!($doController || $doView || $doRoute || $doNav)) {
|
||||
$doController = $doView = $doRoute = $doNav = true;
|
||||
}
|
||||
|
||||
// Namen formatieren
|
||||
$key = strtolower(str_replace(' ', '_', $name));
|
||||
$className = str_replace(' ', '', ucwords(str_replace('_', ' ', $key)));
|
||||
$label = $label ?: ucwords(str_replace('_', ' ', $key));
|
||||
|
||||
$controllerFile = __DIR__ . "/WEBROOT/src/MVC/Controller/{$className}Controller.php";
|
||||
$viewFile = __DIR__ . "/WEBROOT/src/MVC/View/{$key}.php";
|
||||
$routesFile = __DIR__ . "/WEBROOT/src/config/routes.php";
|
||||
$navFile = __DIR__ . "/WEBROOT/src/config/navigation.php";
|
||||
|
||||
// 1. Controller erstellen
|
||||
if ($doController) {
|
||||
$controllerContent = <<<PHPCTRL
|
||||
<?php
|
||||
|
||||
namespace MVC\Controller;
|
||||
|
||||
use MVC\Core\Controller;
|
||||
use MVC\Classes\Client;
|
||||
use MVC\Classes\Cookie;
|
||||
use MVC\Classes\Request;
|
||||
use MVC\Classes\Server;
|
||||
// use MVC\Model\YourModel; // Importiere dein Model
|
||||
|
||||
class {$className}Controller extends Controller {
|
||||
public function {$key}() {
|
||||
// Nutze Models für den Datenbankzugriff
|
||||
// \$data = YourModel::all();
|
||||
|
||||
return \$this->render('{$key}', [
|
||||
'title' => '{$className} Modul',
|
||||
'message' => 'Willkommen in deinem neuen Modul {$className}!'
|
||||
]);
|
||||
}
|
||||
}
|
||||
PHPCTRL;
|
||||
|
||||
if (!file_exists($controllerFile)) {
|
||||
file_put_contents($controllerFile, $controllerContent);
|
||||
echo "✅ Controller erstellt: WEBROOT/src/MVC/Controller/{$className}Controller.php\n";
|
||||
} else {
|
||||
echo "⚠️ Controller existiert bereits.\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 2. View erstellen
|
||||
if ($doView) {
|
||||
$viewContent = <<<HTML
|
||||
<h2><?= htmlspecialchars(\$title) ?></h2>
|
||||
<div class="card">
|
||||
<p><?= htmlspecialchars(\$message) ?></p>
|
||||
<p>Hier kannst du nun dein Projekt aufbauen.</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--card-border);
|
||||
}
|
||||
</style>
|
||||
HTML;
|
||||
|
||||
if (!file_exists($viewFile)) {
|
||||
file_put_contents($viewFile, $viewContent);
|
||||
echo "✅ View erstellt: WEBROOT/src/MVC/View/{$key}.php\n";
|
||||
} else {
|
||||
echo "⚠️ View existiert bereits.\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Route hinzufügen
|
||||
if ($doRoute) {
|
||||
if (file_exists($routesFile)) {
|
||||
$routesContent = file_get_contents($routesFile);
|
||||
$routeLine = "\$router->add('{$key}',
|
||||
\\MVC\\Controller\\{$className}Controller::class, '{$key}'" . ($isAuth ? ",
|
||||
['auth']" : "") . ");\n";
|
||||
|
||||
if (strpos($routesContent, "'{$key}'") === false) {
|
||||
file_put_contents($routesFile, $routesContent . $routeLine);
|
||||
echo "✅ Route hinzugefügt: {$routesFile}\n";
|
||||
} else {
|
||||
echo "⚠️ Route für '{$key}' existiert bereits in routes.php\n";
|
||||
}
|
||||
} else {
|
||||
echo "❌ Fehler: routes.php nicht gefunden!\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Navigation hinzufügen
|
||||
if ($doNav) {
|
||||
if (file_exists($navFile)) {
|
||||
$navContent = file_get_contents($navFile);
|
||||
$navItem = " '{$key}' => ['label' => '{$label}'],\n";
|
||||
|
||||
if (strpos($navContent, "'{$key}'") === false) {
|
||||
// Wir suchen nach dem Ende des Arrays ];
|
||||
$newNavContent = preg_replace('/(\];\s*)$/', $navItem . '$1',
|
||||
trim($navContent));
|
||||
file_put_contents($navFile, $newNavContent . "\n");
|
||||
echo "✅ Navigation hinzugefügt: {$navFile}\n";
|
||||
} else {
|
||||
echo "⚠️ Navigation für '{$key}' existiert bereits in navigation.php\n";
|
||||
}
|
||||
} else {
|
||||
echo "❌ Fehler: navigation.php nicht gefunden!\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\nFertig!\n";
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
$port = 8000;
|
||||
$host = 'localhost';
|
||||
$root = __DIR__ . '/WEBROOT';
|
||||
echo "\n🚀 Starte MVC Dev-Server auf http://$host:$port\n";
|
||||
echo "Drücke STRG+C zum Beenden.\n\n";
|
||||
passthru("php -S $host:$port -t \"$root\" \"$root/index.php\"");
|
||||
Reference in new issue
Block a user