diff --git a/.env.example b/.env.example deleted file mode 100644 index 243c3fd..0000000 --- a/.env.example +++ /dev/null @@ -1,30 +0,0 @@ -APP_NAME=ZEngine -APP_ENV=local -APP_DEBUG=true -APP_URL=http://localhost -APP_TIMEZONE=UTC - -ADMIN_TOKEN=your-secret-admin-token-here -APP_LOCALE=en - -DB_CONNECTION=mysql -DB_HOST=localhost -DB_PORT=3306 -DB_DATABASE=zengine -DB_USERNAME=root -DB_PASSWORD= - -SESSION_NAME=ZENGINE_SESSION -SESSION_LIFETIME=7200 -SESSION_PATH=/ -SESSION_DOMAIN= -SESSION_SECURE=false -SESSION_HTTPONLY=true -SESSION_SAMESITE=Lax - -COOKIE_EXPIRE=3600 -COOKIE_PATH=/ -COOKIE_DOMAIN= -COOKIE_SECURE=false -COOKIE_HTTPONLY=true -COOKIE_SAMESITE=Lax diff --git a/.htaccess b/.htaccess index 604e720..7caee24 100644 --- a/.htaccess +++ b/.htaccess @@ -1,8 +1,25 @@ -RewriteEngine On +ErrorDocument 403 /error-handler.php?code=403 +ErrorDocument 404 /error-handler.php?code=404 +ErrorDocument 500 /error-handler.php?code=500 -RewriteCond %{HTTP:Authorization} . -RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + Options -Indexes + RewriteEngine On -RewriteCond %{REQUEST_FILENAME} !-d -RewriteCond %{REQUEST_FILENAME} !-f -RewriteRule ^ index.php [L] \ No newline at end of file + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + RewriteRule ^(core|app|storage|config|database|backups|vendor|bootstrap|cron|tasks)(/.*)?$ - [F,L] + + RewriteRule ^(cron\.php)$ - [F,L] + + RewriteCond %{REQUEST_URI} \.(env|git|sql|md|lock|json|xml|ini|log|zip|htaccess)$ [NC] + RewriteRule .* - [F,L] + + RewriteRule ^error-handler\.php$ - [L] + + RewriteCond %{REQUEST_URI} !^/index\.php$ [NC] + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [QSA,L] + diff --git a/README.md b/README.md index c642620..a03e65a 100644 --- a/README.md +++ b/README.md @@ -181,12 +181,28 @@ Use it anywhere: $result = app('myservice')->doSomething(); ``` +## Cron-Jobs (Tasks) +Add your cron to `App\Tasks\MyCron` then add your task to `/cron.php` +```php +try { + $doSomething = new DoSomething(); + $doSomething->handle(); + +} catch (\Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; +} +``` +Note : cron.php only accessible on cli with CRON_KEY + + ## Error Handling Set `APP_DEBUG=true` in `.env` for detailed error pages with code snippets. Set `APP_DEBUG=false` for production to show clean error pages. +Set `MAINTENANCE_MODE=1` for Maintenance mode (ip whitelist & maintenance msg included) + All errors are logged to `storage/logs/error.log`. ## Contributing diff --git a/app/.htaccess b/app/.htaccess new file mode 100644 index 0000000..14249c5 --- /dev/null +++ b/app/.htaccess @@ -0,0 +1 @@ +Deny from all \ No newline at end of file diff --git a/app/Controllers/HomeController.php b/app/Controllers/HomeController.php deleted file mode 100644 index cc08336..0000000 --- a/app/Controllers/HomeController.php +++ /dev/null @@ -1,36 +0,0 @@ - 'Home Controller', - 'method' => $request->method(), - 'path' => $request->path(), - ]); - } - - public function show(Request $request, string $id): Response - { - return json([ - 'message' => 'Show method', - 'id' => $id, - ]); - } - - public function store(Request $request): Response - { - $data = $request->all(); - - return json([ - 'message' => 'Data stored', - 'data' => $data, - ], 201); - } -} diff --git a/app/Controllers/WelcomeController.php b/app/Controllers/WelcomeController.php new file mode 100644 index 0000000..591ea2d --- /dev/null +++ b/app/Controllers/WelcomeController.php @@ -0,0 +1,29 @@ +welcomeModel = new WelcomeModel(); + } + + public function showWelcome(): Response + { + $something = $this->welcomeModel->getSomething(); + + return view('welcome', [ + 'something' => $something, + 'version' => '1.0.2', + 'services_count' => '9' + ]); + } + +} diff --git a/app/Middleware/AdminMiddleware.php b/app/Middleware/AdminMiddleware.php deleted file mode 100644 index e2a1329..0000000 --- a/app/Middleware/AdminMiddleware.php +++ /dev/null @@ -1,24 +0,0 @@ -header('X-Admin-Token') ?? $request->input('admin_token'); - - if (!$token || $token !== env('ADMIN_TOKEN', 'secret-admin-token')) { - return Response::json([ - 'error' => 'Unauthorized', - 'message' => 'Admin token required' - ], 401); - } - - return $next($request); - } -} diff --git a/app/Middleware/GuestMiddleware.php b/app/Middleware/GuestMiddleware.php new file mode 100644 index 0000000..a2089b1 --- /dev/null +++ b/app/Middleware/GuestMiddleware.php @@ -0,0 +1,22 @@ +isJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Wohoooooo' + ], 200); + } + + return $next($request); + } +} + diff --git a/app/Models/WelcomeModel.php b/app/Models/WelcomeModel.php new file mode 100644 index 0000000..80d4f21 --- /dev/null +++ b/app/Models/WelcomeModel.php @@ -0,0 +1,26 @@ + 'John Doe', + 'another' => 'Jane Doe', + ]; + + return [ + 'success' => true, + 'veryImportant' => false, + $something + ]; + } + +} diff --git a/app/Tasks/DoSomething.php b/app/Tasks/DoSomething.php new file mode 100644 index 0000000..0903aa0 --- /dev/null +++ b/app/Tasks/DoSomething.php @@ -0,0 +1,13 @@ + + + + + + Access Denied - ZEngine Security + + + +
+
+
ZEngine Security
+
+ +
+
+
🚫
+

Access Denied

+
HTTP 403 Forbidden
+
+ +
+
► What Happened?
+
+ + + + You attempted to access a protected resource that is not available to the public. + This area contains sensitive framework files and configurations that are restricted for security reasons. + +
+ +
+
► Requested Path:
+
+
+ + +
► Why Was I Blocked?
+
    +
  • The requested path contains sensitive framework files
  • +
  • Direct access to core system files is prohibited
  • +
  • This is a security measure to protect the application
  • +
  • Only authorized routes can be accessed through the router
  • +
+ +
► Need Access?
+
    +
  • Contact your administrator if you believe this is an error
  • +
  • Check your account permissions and role
  • +
  • Some categories and features are restricted by role
  • +
+ +
+ + +
+ + +
+ + + + diff --git a/app/Views/errors/maintenance.php b/app/Views/errors/maintenance.php new file mode 100644 index 0000000..b395e34 --- /dev/null +++ b/app/Views/errors/maintenance.php @@ -0,0 +1,151 @@ + + + + + + Maintenance Mode + + + +
+
+
ZENGINE
+
+ +
+
+
Maintenance Mode
+
+ +

+ +
+
+
+
+
+ + +
+ + + + diff --git a/app/Views/welcome.php b/app/Views/welcome.php index ff7b8fd..380809d 100644 --- a/app/Views/welcome.php +++ b/app/Views/welcome.php @@ -203,6 +203,8 @@

Version

+

you can say message here /say/msg


+
► Services Loaded
diff --git a/app/routes.php b/app/routes.php index bfe674b..579f575 100644 --- a/app/routes.php +++ b/app/routes.php @@ -2,14 +2,31 @@ use ZEngine\Core\Http\Request; use ZEngine\Core\Http\Response; +use ZEngine\App\Controllers\WelcomeController; +use ZEngine\App\Middleware\GuestMiddleware; $router = router(); -$router->get('/', function (Request $request) { - $version = app()->version(); - return view('welcome', [ - 'version' => $version, - 'services_count' => app()->getContainer()->count() +if (env('MAINTENANCE_MODE', 0)) { + $clientIp = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; + $whitelistedIps = env('WHITELISTED_IPS', []); + + if (!in_array($clientIp, $whitelistedIps)) { + http_response_code(503); + include __DIR__ . '/Views/errors/maintenance.php'; + exit; + } +} + +$router->group(['middleware' => [GuestMiddleware::class]], function ($router) { + $router->get('/', [WelcomeController::class, 'showWelcome']); + $router->get('/welcome', [WelcomeController::class, 'showWelcome']); +}); + +$router->get('/say/{msg}', function ($msg) { + return json([ + 'success' => true, + 'message' => $msg ]); }); diff --git a/composer.json b/composer.json index 484c880..4c8e9b1 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "spexdw/z-engine", - "version": "1.0.1", + "version": "1.0.2", "description": "ZEngine - A modern, lightweight PHP framework with powerful service architecture", "type": "project", "keywords": ["framework", "php", "zengine", "service-oriented", "routing"], @@ -12,7 +12,8 @@ } ], "require": { - "php": "^8.1" + "php": "^8.1", + "phpmailer/phpmailer": "^7.0" }, "require-dev": { "phpunit/phpunit": "^10.0", diff --git a/config/.htaccess b/config/.htaccess new file mode 100644 index 0000000..14249c5 --- /dev/null +++ b/config/.htaccess @@ -0,0 +1 @@ +Deny from all \ No newline at end of file diff --git a/config/env.php b/config/env.php new file mode 100644 index 0000000..ac7d12f --- /dev/null +++ b/config/env.php @@ -0,0 +1,50 @@ + 'ZEngine', + 'APP_ENV' => 'local', + 'APP_KEY' => 'app_key', + 'APP_DEBUG' => true, + 'APP_URL' => 'http://localhost:1999', + 'APP_TIMEZONE' => 'UTC', + 'APP_LOCALE' => 'en', + 'CRON_KEY' => 'zengine_cron_key', + + 'DB_CONNECTION' => 'mysql', + 'DB_HOST' => 'localhost', + 'DB_PORT' => 3306, + 'DB_DATABASE' => 'zengine', + 'DB_USERNAME' => 'root', + 'DB_PASSWORD' => '', + + 'SESSION_NAME' => 'ZENGINE_SESSION', + 'SESSION_LIFETIME' => 7200, + 'SESSION_PATH' => '/', + 'SESSION_DOMAIN' => '', + 'SESSION_SECURE' => true, + 'SESSION_HTTPONLY' => true, + 'SESSION_SAMESITE' => 'Lax', + + 'COOKIE_EXPIRE' => 3600, + 'COOKIE_PATH' => '/', + 'COOKIE_DOMAIN' => '', + 'COOKIE_SECURE' => true, + 'COOKIE_HTTPONLY' => true, + 'COOKIE_SAMESITE' => 'Lax', + + 'SMTP_HOST' => '', + 'SMTP_PORT' => 587, + 'SMTP_USERNAME' => '', + 'SMTP_PASSWORD' => '', + 'SMTP_ENCRYPTION' => 'tls', + 'SMTP_FROM_ADDRESS' => '', + 'SMTP_FROM_NAME' => '', + 'SMTP_TIMEOUT' => 30, + 'SMTP_AUTH' => true, + 'SMTP_DEBUG' => 0, + + + 'MAINTENANCE_MODE' => 0, + 'MAINTENANCE_MESSAGE' => 'We are currently performing maintenance. Please check back soon.', + 'WHITELISTED_IPS' => ['127.0.0.1', '::1'], +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..eb13549 --- /dev/null +++ b/config/mail.php @@ -0,0 +1,16 @@ + env('APP_NAME'), + 'APP_URL' => env('APP_URL'), + 'SMTP_HOST' => env('SMTP_HOST'), + 'SMTP_PORT' => env('SMTP_PORT'), + 'SMTP_USERNAME' => env('SMTP_USERNAME'), + 'SMTP_PASSWORD' => env('SMTP_PASSWORD'), + 'SMTP_ENCRYPTION' => env('SMTP_ENCRYPTION'), + 'SMTP_FROM_ADDRESS' => env('SMTP_FROM_ADDRESS'), + 'SMTP_FROM_NAME' => env('SMTP_FROM_NAME'), + 'SMTP_TIMEOUT' => env('SMTP_TIMEOUT'), + 'SMTP_AUTH' => env('SMTP_AUTH'), + 'SMTP_DEBUG' => env('SMTP_DEBUG') +]; diff --git a/core/.htaccess b/core/.htaccess new file mode 100644 index 0000000..14249c5 --- /dev/null +++ b/core/.htaccess @@ -0,0 +1 @@ +Deny from all \ No newline at end of file diff --git a/core/Http/Request.php b/core/Http/Request.php index 9368982..dce2c83 100644 --- a/core/Http/Request.php +++ b/core/Http/Request.php @@ -10,6 +10,7 @@ class Request private array $files; private array $cookies; private array $headers; + private array $params = []; private ?string $content = null; public function __construct( @@ -35,15 +36,26 @@ public static function capture(): self private function extractHeaders(): array { $headers = []; + foreach ($this->server as $key => $value) { if (str_starts_with($key, 'HTTP_')) { $headerName = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5))))); $headers[$headerName] = $value; } } + + if (isset($this->server['CONTENT_TYPE'])) { + $headers['Content-Type'] = $this->server['CONTENT_TYPE']; + } + + if (isset($this->server['CONTENT_LENGTH'])) { + $headers['Content-Length'] = $this->server['CONTENT_LENGTH']; + } + return $headers; } + public function method(): string { return strtoupper($this->server['REQUEST_METHOD'] ?? 'GET'); @@ -71,16 +83,26 @@ public function post(string $key, mixed $default = null): mixed public function input(string $key, mixed $default = null): mixed { + if ($this->isJson()) { + return $this->json($key, $default); + } return $this->request[$key] ?? $this->query[$key] ?? $default; } public function all(): array { + if ($this->isJson()) { + return $this->json() ?? []; + } return array_merge($this->query, $this->request); } public function has(string $key): bool { + if ($this->isJson()) { + $data = $this->json(); + return isset($data[$key]); + } return isset($this->request[$key]) || isset($this->query[$key]); } @@ -152,4 +174,26 @@ public function isAjax(): bool { return $this->header('X-Requested-With') === 'XMLHttpRequest'; } + + public function setParams(array $params): void + { + $this->params = $params; + } + + public function param(string $key, mixed $default = null): mixed + { + return $this->params[$key] ?? $default; + } + + private array $apiData = []; + + public function setApiKey(array $key): void + { + $this->apiData['key'] = $key; + } + + public function getApiKey(): ?array + { + return $this->apiData['key'] ?? null; + } } diff --git a/core/Providers.php b/core/Providers.php index 5082a84..27efdfb 100644 --- a/core/Providers.php +++ b/core/Providers.php @@ -10,6 +10,7 @@ use ZEngine\Core\Services\ValidationService; use ZEngine\Core\Services\LoggerService; use ZEngine\Core\Services\HashService; +use ZEngine\Core\Services\MailService; class Providers { @@ -23,6 +24,7 @@ public static function register(Container $container): void self::registerValidation($container); self::registerLogger($container); self::registerHash($container); + self::registerMail($container); } private static function registerDatabase(Container $container): void @@ -37,7 +39,7 @@ private static function registerDatabase(Container $container): void private static function registerSession(Container $container): void { $container->singleton('session', function () { - return new SessionService(); + return SessionService::getInstance(); }); $container->alias('session', SessionService::class); } @@ -90,4 +92,13 @@ private static function registerHash(Container $container): void }); $container->alias('hash', HashService::class); } + + private static function registerMail(Container $container): void + { + $container->singleton('mail', function () { + return new MailService(config('mail')); + }); + $container->alias('mail', MailService::class); + } + } diff --git a/core/Router/Router.php b/core/Router/Router.php index fa51cd9..807230e 100644 --- a/core/Router/Router.php +++ b/core/Router/Router.php @@ -12,6 +12,7 @@ class Router private array $routes = []; private array $middleware = []; private Container $container; + private array $currentMiddleware = []; public function __construct(Container $container) { @@ -55,7 +56,8 @@ public function any(string $uri, Closure|array|string $action): Route private function addRoute(string $method, string $uri, Closure|array|string $action): Route { - $route = new Route($method, $uri, $action); + $fullUri = $this->currentPrefix . $uri; + $route = new Route($method, $fullUri, $action); $this->routes[$method][] = $route; return $route; } @@ -66,21 +68,33 @@ public function group(array $attributes, Closure $callback): void $middleware = $attributes['middleware'] ?? []; $previousPrefix = $this->getCurrentPrefix(); - $this->setPrefix($prefix); + $previousMiddleware = $this->currentMiddleware ?? []; - $callback($this); + $this->setPrefix($previousPrefix . $prefix); + $this->currentMiddleware = array_merge($previousMiddleware, (array) $middleware); - $this->setPrefix($previousPrefix); + $beforeCounts = []; + foreach ($this->routes as $method => $routes) { + $beforeCounts[$method] = count($routes); + } + + $callback($this); - if (!empty($middleware)) { - foreach ($this->getLastGroupRoutes() as $route) { - $route->middleware($middleware); + foreach ($this->routes as $method => $routes) { + $startIndex = $beforeCounts[$method] ?? 0; + for ($i = $startIndex; $i < count($routes); $i++) { + if (!empty($this->currentMiddleware)) { + $this->routes[$method][$i]->middleware($this->currentMiddleware); + } } } + + $this->setPrefix($previousPrefix); + $this->currentMiddleware = $previousMiddleware; } private string $currentPrefix = ''; - private int $lastRouteCount = 0; + private array $lastRouteCounts = []; private function getCurrentPrefix(): string { @@ -89,7 +103,10 @@ private function getCurrentPrefix(): string private function setPrefix(string $prefix): void { - $this->lastRouteCount = count($this->routes); + $this->lastRouteCounts = []; + foreach ($this->routes as $method => $methodRoutes) { + $this->lastRouteCounts[$method] = count($methodRoutes); + } $this->currentPrefix = $prefix; } @@ -97,7 +114,9 @@ private function getLastGroupRoutes(): array { $routes = []; foreach ($this->routes as $method => $methodRoutes) { - $routes = array_merge($routes, array_slice($methodRoutes, $this->lastRouteCount)); + $startIndex = $this->lastRouteCounts[$method] ?? 0; + $newRoutes = array_slice($methodRoutes, $startIndex); + $routes = array_merge($routes, $newRoutes); } return $routes; } @@ -123,6 +142,8 @@ public function dispatch(Request $request): Response private function runRoute(Route $route, array $params, Request $request): Response { + $request->setParams($params); + $middlewares = $route->getMiddleware(); $pipeline = array_reduce( diff --git a/core/services/Cache-Service.php b/core/Services/CacheService.php similarity index 70% rename from core/services/Cache-Service.php rename to core/Services/CacheService.php index b61dcf0..22b07c7 100644 --- a/core/services/Cache-Service.php +++ b/core/Services/CacheService.php @@ -19,6 +19,8 @@ public function __construct(array $config = []) public function get(string $key, mixed $default = null): mixed { + $this->autoCleanup(); + $file = $this->getFilePath($key); if (!file_exists($file)) { @@ -114,4 +116,47 @@ private function getFilePath(string $key): string { return $this->cacheDir . '/' . md5($key) . '.cache'; } + + public function cleanup(): int + { + $maxAge = 3 * 3600; // 3 hours + $cutoffTime = time() - $maxAge; + $deleted = 0; + + $files = glob($this->cacheDir . '/*.cache'); + + foreach ($files as $file) { + if (!is_file($file)) { + continue; + } + + if (filemtime($file) < $cutoffTime) { + if (unlink($file)) { + $deleted++; + } + continue; + } + + try { + $data = @unserialize(file_get_contents($file)); + if ($data && isset($data['expires_at']) && $data['expires_at'] < time()) { + if (unlink($file)) { + $deleted++; + } + } + } catch (\Exception $e) { + @unlink($file); + $deleted++; + } + } + + return $deleted; + } + + private function autoCleanup(): void + { + if (rand(1, 100) <= 2) { + $this->cleanup(); + } + } } diff --git a/core/services/Cookie-Service.php b/core/Services/CookieService.php similarity index 100% rename from core/services/Cookie-Service.php rename to core/Services/CookieService.php diff --git a/core/services/Database-Service.php b/core/Services/DatabaseService.php similarity index 85% rename from core/services/Database-Service.php rename to core/Services/DatabaseService.php index a0629e3..af1bcb1 100644 --- a/core/services/Database-Service.php +++ b/core/Services/DatabaseService.php @@ -236,4 +236,43 @@ public function count(): int $result = $this->db->query($sql, $this->bindings); return (int) ($result[0]['count'] ?? 0); } + + public function update(array $data): int + { + $set = implode(', ', array_map(fn($key) => "{$key} = ?", array_keys($data))); + + $sql = "UPDATE {$this->table} SET {$set}"; + + $bindings = array_values($data); + + if (!empty($this->wheres)) { + $sql .= " WHERE " . implode(' AND ', $this->wheres); + $bindings = array_merge($bindings, $this->bindings); + } + + return $this->db->execute($sql, $bindings); + } + + public function delete(): int + { + $sql = "DELETE FROM {$this->table}"; + + if (!empty($this->wheres)) { + $sql .= " WHERE " . implode(' AND ', $this->wheres); + } + + return $this->db->execute($sql, $this->bindings); + } + + public function insert(array $data): int + { + $columns = implode(', ', array_keys($data)); + $placeholders = implode(', ', array_fill(0, count($data), '?')); + $sql = "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})"; + + $this->db->execute($sql, array_values($data)); + + return (int) $this->db->connect()->lastInsertId(); + } + } diff --git a/core/services/FormBuilder-Service.php b/core/Services/FormBuilderService.php similarity index 100% rename from core/services/FormBuilder-Service.php rename to core/Services/FormBuilderService.php diff --git a/core/services/Hash-Service.php b/core/Services/HashService.php similarity index 100% rename from core/services/Hash-Service.php rename to core/Services/HashService.php diff --git a/core/services/Logger-Service.php b/core/Services/LoggerService.php similarity index 100% rename from core/services/Logger-Service.php rename to core/Services/LoggerService.php diff --git a/core/Services/MailService.php b/core/Services/MailService.php new file mode 100644 index 0000000..f349282 --- /dev/null +++ b/core/Services/MailService.php @@ -0,0 +1,58 @@ +config = $config; + $this->mailer = new PHPMailer(true); + $this->setupMailer(); + } + + private function setupMailer(): void + { + $this->mailer->isSMTP(); + $this->mailer->Host = $this->config['SMTP_HOST'] ?? 'localhost'; + $this->mailer->SMTPAuth = $this->config['SMTP_AUTH'] ?? true; + $this->mailer->Username = $this->config['SMTP_USERNAME'] ?? ''; + $this->mailer->Password = $this->config['SMTP_PASSWORD'] ?? ''; + $this->mailer->SMTPSecure = $this->config['SMTP_ENCRYPTION'] ?? 'tls'; + $this->mailer->Port = $this->config['SMTP_PORT'] ?? 587; + $this->mailer->Timeout = $this->config['SMTP_TIMEOUT'] ?? 30; + $this->mailer->SMTPDebug = $this->config['SMTP_DEBUG'] ?? 0; + $this->mailer->CharSet = 'UTF-8'; + + $this->mailer->setFrom( + $this->config['SMTP_FROM_ADDRESS'] ?? 'noreply@zengine.app', + $this->config['SMTP_FROM_NAME'] ?? 'ZEngine' + ); + } + + public function send(string $to, string $subject, string $body, array $data = []): bool + { + try { + $this->mailer->clearAddresses(); + $this->mailer->clearAttachments(); + + $this->mailer->addAddress($to); + $this->mailer->Subject = $subject; + $this->mailer->isHTML(true); + $this->mailer->Body = $body; + $this->mailer->AltBody = strip_tags($body); + + return $this->mailer->send(); + } catch (Exception $e) { + error_log('Mail error: ' . $e->getMessage()); + return false; + } + } + +} \ No newline at end of file diff --git a/core/Services/SessionService.php b/core/Services/SessionService.php new file mode 100644 index 0000000..54887ec --- /dev/null +++ b/core/Services/SessionService.php @@ -0,0 +1,287 @@ +sessionModel = new Session(); + $this->fingerprint = $this->generateFingerprint(); + $this->load(); + $this->cleanupExpiredSessions(); + } + + public static function getInstance(): SessionService + { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + private function load(): void + { + if ($this->loaded) { + return; + } + + $this->sessionId = $_COOKIE[$this->cookieName] ?? null; + + if ($this->sessionId && $this->validate()) { + $session = $this->sessionModel->find($this->sessionId); + + if ($session) { + $this->data = $session['payload'] ?? []; + $this->sessionModel->updateActivity($this->sessionId); + $this->loaded = true; + return; + } + } + + $this->createNewSession(); + } + + private function createNewSession(): void + { + $this->sessionId = $this->generateSessionId(); + $this->data = []; + + $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + + $this->sessionModel->create( + $this->sessionId, + null, + $ip, + $userAgent, + $this->data + ); + + $this->setCookie(); + $this->loaded = true; + } + + private function validate(): bool + { + if (!$this->sessionId) { + return false; + } + + $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + + if (!$this->sessionModel->validateFingerprint($this->sessionId, $ip, $userAgent)) { + $this->invalidateSession(); + return false; + } + + return true; + } + + private function invalidateSession(): void + { + if ($this->sessionId) { + $this->sessionModel->delete($this->sessionId); + } + + $this->data = []; + $this->sessionId = null; + $this->loaded = false; + $this->deleteCookie(); + } + + public function set(string $key, $value): void + { + if (!$this->loaded) { + return; + } + + $this->data[$key] = $value; + $this->save(); + } + + public function get(string $key, $default = null) + { + if (!$this->loaded) { + return $default; + } + + return $this->data[$key] ?? $default; + } + + public function has(string $key): bool + { + if (!$this->loaded) { + return false; + } + + return isset($this->data[$key]); + } + + public function remove(string $key): void + { + if (!$this->loaded) { + return; + } + + unset($this->data[$key]); + $this->save(); + } + + public function regenerate(?int $userId = null): void + { + $oldSessionId = $this->sessionId; + $oldData = $this->data; + + if ($oldSessionId) { + $this->sessionModel->delete($oldSessionId); + } + + $this->sessionId = $this->generateSessionId(); + $this->data = $oldData; + + $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + + $this->sessionModel->create( + $this->sessionId, + $userId, + $ip, + $userAgent, + $this->data + ); + + $this->setCookie(); + $this->loaded = true; + } + + public function destroy(): void + { + if ($this->sessionId) { + $this->sessionModel->delete($this->sessionId); + } + + $this->data = []; + $this->sessionId = null; + $this->loaded = false; + + $this->deleteCookie(); + } + + public function associateUser(int $userId): void + { + $this->regenerate($userId); + $this->set('user_id', $userId); + } + + public function token(): string + { + if (!$this->has('_token')) { + $this->set('_token', bin2hex(random_bytes(32))); + } + + return $this->get('_token', ''); + } + + public function flash(string $key, $value): void + { + $this->set('_flash_' . $key, $value); + } + + public function getFlash(string $key, $default = null) + { + $value = $this->get('_flash_' . $key, $default); + $this->remove('_flash_' . $key); + return $value; + } + + public function hasFlash(string $key): bool + { + return $this->has('_flash_' . $key); + } + + private function save(): void + { + if ($this->sessionId) { + $userId = isset($this->data['user_id']) ? (int)$this->data['user_id'] : null; + $this->sessionModel->update($this->sessionId, $this->data, null, $userId); + } + } + + private function generateSessionId(): string + { + return bin2hex(random_bytes(32)); + } + + private function generateFingerprint(): string + { + $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + + return hash('sha256', $ip . '|' . $userAgent . '|' . env('APP_KEY', 'default-key')); + } + + private function getFingerprintComponents(): array + { + $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + + return [ + hash('sha256', $ip), + hash('sha256', $userAgent) + ]; + } + + private function setCookie(): void + { + $secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on'; + + setcookie( + $this->cookieName, + $this->sessionId, + [ + 'expires' => time() + $this->lifetime, + 'path' => '/', + 'domain' => '', + 'secure' => $secure, + 'httponly' => true, + 'samesite' => 'Strict' + ] + ); + } + + private function deleteCookie(): void + { + setcookie( + $this->cookieName, + '', + [ + 'expires' => time() - 3600, + 'path' => '/', + 'domain' => '', + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Strict' + ] + ); + } + + private function cleanupExpiredSessions(): void + { + if (rand(1, 100) <= 2) { + $this->sessionModel->cleanup($this->lifetime); + } + } +} diff --git a/core/services/Validation-Service.php b/core/Services/ValidationService.php similarity index 100% rename from core/services/Validation-Service.php rename to core/Services/ValidationService.php diff --git a/core/helpers.php b/core/helpers.php index d556e93..92312f3 100644 --- a/core/helpers.php +++ b/core/helpers.php @@ -3,36 +3,14 @@ if (!function_exists('env')) { function env(string $key, mixed $default = null): mixed { - static $loaded = false; - - if (!$loaded) { - $envFile = dirname(__DIR__) . '/.env'; - if (file_exists($envFile)) { - $lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - if (strpos(trim($line), '#') === 0) { - continue; - } - [$name, $value] = explode('=', $line, 2); - $value = trim($value); - - if ($value === 'true' || $value === '(true)') { - $value = true; - } elseif ($value === 'false' || $value === '(false)') { - $value = false; - } elseif ($value === 'null' || $value === '(null)') { - $value = null; - } elseif ($value === 'empty' || $value === '(empty)') { - $value = ''; - } - - $_ENV[trim($name)] = $value; - } - } - $loaded = true; + static $config = null; + + if ($config === null) { + $envFile = dirname(__DIR__) . '/config/env.php'; + $config = file_exists($envFile) ? require $envFile : []; } - return $_ENV[$key] ?? $default; + return $config[$key] ?? $default; } } @@ -196,3 +174,10 @@ function hash_service(): \ZEngine\Core\Services\HashService return app('hash'); } } + +if (!function_exists('mail')) { + function mail(): \ZEngine\Core\Services\MailService + { + return app('mail'); + } +} diff --git a/core/services/Session-Service.php b/core/services/Session-Service.php deleted file mode 100644 index c12479d..0000000 --- a/core/services/Session-Service.php +++ /dev/null @@ -1,182 +0,0 @@ -config = array_merge([ - 'name' => 'ZENGINE_SESSION', - 'lifetime' => 7200, - 'path' => '/', - 'domain' => '', - 'secure' => false, - 'httponly' => true, - 'samesite' => 'Lax', - ], $config); - } - - public function start(): bool - { - if ($this->started) { - return true; - } - - if (session_status() === PHP_SESSION_ACTIVE) { - $this->started = true; - return true; - } - - session_name($this->config['name']); - - session_set_cookie_params([ - 'lifetime' => $this->config['lifetime'], - 'path' => $this->config['path'], - 'domain' => $this->config['domain'], - 'secure' => $this->config['secure'], - 'httponly' => $this->config['httponly'], - 'samesite' => $this->config['samesite'], - ]); - - $this->started = session_start(); - - return $this->started; - } - - public function set(string $key, mixed $value): void - { - $this->start(); - $_SESSION[$key] = $value; - } - - public function get(string $key, mixed $default = null): mixed - { - $this->start(); - return $_SESSION[$key] ?? $default; - } - - public function has(string $key): bool - { - $this->start(); - return isset($_SESSION[$key]); - } - - public function delete(string $key): void - { - $this->start(); - unset($_SESSION[$key]); - } - - public function all(): array - { - $this->start(); - return $_SESSION; - } - - public function clear(): void - { - $this->start(); - $_SESSION = []; - } - - public function destroy(): bool - { - $this->start(); - $_SESSION = []; - - if (isset($_COOKIE[session_name()])) { - $params = session_get_cookie_params(); - setcookie( - session_name(), - '', - time() - 42000, - $params['path'], - $params['domain'], - $params['secure'], - $params['httponly'] - ); - } - - return session_destroy(); - } - - public function regenerate(bool $deleteOld = true): bool - { - $this->start(); - return session_regenerate_id($deleteOld); - } - - public function flash(string $key, mixed $value): void - { - $this->set($key, $value); - $this->set('_flash_keys', array_merge($this->get('_flash_keys', []), [$key])); - } - - public function getFlash(string $key, mixed $default = null): mixed - { - $value = $this->get($key, $default); - $this->delete($key); - - $flashKeys = $this->get('_flash_keys', []); - if (($index = array_search($key, $flashKeys)) !== false) { - unset($flashKeys[$index]); - $this->set('_flash_keys', $flashKeys); - } - - return $value; - } - - public function keep(array $keys): void - { - $flashKeys = $this->get('_flash_keys', []); - $this->set('_flash_keys', array_diff($flashKeys, $keys)); - } - - public function reflash(): void - { - $flashKeys = $this->get('_flash_keys', []); - $this->keep($flashKeys); - } - - public function getId(): string - { - $this->start(); - return session_id(); - } - - public function setId(string $id): void - { - session_id($id); - } - - public function pull(string $key, mixed $default = null): mixed - { - $value = $this->get($key, $default); - $this->delete($key); - return $value; - } - - public function push(string $key, mixed $value): void - { - $array = $this->get($key, []); - $array[] = $value; - $this->set($key, $array); - } - - public function increment(string $key, int $amount = 1): int - { - $value = (int) $this->get($key, 0); - $value += $amount; - $this->set($key, $value); - return $value; - } - - public function decrement(string $key, int $amount = 1): int - { - return $this->increment($key, -$amount); - } -} diff --git a/cron.php b/cron.php new file mode 100644 index 0000000..52cf18b --- /dev/null +++ b/cron.php @@ -0,0 +1,34 @@ +handle(); + +} catch (\Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; +} + +echo "\n=== Cron Jobs Completed ===\n"; diff --git a/error-handler.php b/error-handler.php new file mode 100644 index 0000000..8ae22c7 --- /dev/null +++ b/error-handler.php @@ -0,0 +1,13 @@ +