186 lines
6.7 KiB
PHP
186 lines
6.7 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Core\Http;
|
|
|
|
use Core\Services\Auth;
|
|
use Core\Services\Csrf;
|
|
use Core\Services\Database;
|
|
use Core\Services\Permissions;
|
|
use Core\Services\Shortcodes;
|
|
use Core\Views\View;
|
|
use PDO;
|
|
use Throwable;
|
|
|
|
class Router
|
|
{
|
|
private array $routes = [];
|
|
|
|
public function get(string $path, callable $handler): void
|
|
{
|
|
$this->routes['GET'][$path] = $handler;
|
|
}
|
|
|
|
public function post(string $path, callable $handler): void
|
|
{
|
|
$this->routes['POST'][$path] = $handler;
|
|
}
|
|
|
|
public function registerModules(string $modulesPath): void
|
|
{
|
|
foreach (glob($modulesPath . '/*/module.php') as $moduleFile) {
|
|
$module = require $moduleFile;
|
|
if (is_callable($module)) {
|
|
$module($this);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function dispatch(string $uri, string $method): Response
|
|
{
|
|
$path = parse_url($uri, PHP_URL_PATH) ?: '/';
|
|
if ($path !== '/' && str_ends_with($path, '/')) {
|
|
$path = rtrim($path, '/');
|
|
}
|
|
$method = strtoupper($method);
|
|
|
|
|
|
if ($method === 'POST' && $this->requiresCsrf($path) && !Csrf::verifyRequest()) {
|
|
return new Response('Invalid request token. Please refresh and try again.', 419);
|
|
}
|
|
|
|
if ($method === 'GET') {
|
|
$redirect = $this->matchRedirect($path);
|
|
if ($redirect !== null) {
|
|
return $redirect;
|
|
}
|
|
}
|
|
|
|
if (str_starts_with($path, '/admin')) {
|
|
$permission = Permissions::routePermission($path);
|
|
if ($permission !== null && Auth::check() && !Auth::can($permission)) {
|
|
return new Response('', 302, ['Location' => '/admin?denied=1']);
|
|
}
|
|
}
|
|
|
|
if (isset($this->routes[$method][$path])) {
|
|
$handler = $this->routes[$method][$path];
|
|
return $handler();
|
|
}
|
|
|
|
if ($method === 'GET') {
|
|
if (str_starts_with($path, '/news/')) {
|
|
$postSlug = trim(substr($path, strlen('/news/')));
|
|
if ($postSlug !== '' && strpos($postSlug, '/') === false) {
|
|
$db = Database::get();
|
|
if ($db instanceof PDO) {
|
|
try {
|
|
$stmt = $db->prepare("
|
|
SELECT title, content_html, published_at, featured_image_url, author_name, category, tags
|
|
FROM ac_posts
|
|
WHERE slug = :slug AND is_published = 1
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([':slug' => $postSlug]);
|
|
$post = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if ($post) {
|
|
$view = new View(__DIR__ . '/../../modules/blog/views');
|
|
return new Response($view->render('site/show.php', [
|
|
'title' => (string)$post['title'],
|
|
'content_html' => (string)$post['content_html'],
|
|
'published_at' => (string)($post['published_at'] ?? ''),
|
|
'featured_image_url' => (string)($post['featured_image_url'] ?? ''),
|
|
'author_name' => (string)($post['author_name'] ?? ''),
|
|
'category' => (string)($post['category'] ?? ''),
|
|
'tags' => (string)($post['tags'] ?? ''),
|
|
]));
|
|
}
|
|
} catch (Throwable $e) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$slug = ltrim($path, '/');
|
|
if ($slug !== '' && strpos($slug, '/') === false) {
|
|
$db = Database::get();
|
|
if ($db instanceof PDO) {
|
|
try {
|
|
$stmt = $db->prepare("SELECT title, content_html FROM ac_pages WHERE slug = :slug AND is_published = 1 LIMIT 1");
|
|
$stmt->execute([':slug' => $slug]);
|
|
$page = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if ($page) {
|
|
$view = new View(__DIR__ . '/../../modules/pages/views');
|
|
return new Response($view->render('site/show.php', [
|
|
'title' => (string)$page['title'],
|
|
'content_html' => Shortcodes::render((string)$page['content_html'], [
|
|
'page_slug' => (string)$slug,
|
|
]),
|
|
]));
|
|
}
|
|
} catch (Throwable $e) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$view = new View();
|
|
return new Response($view->render('site/404.php', [
|
|
'title' => 'Not Found',
|
|
'message' => 'Page not found.',
|
|
]), 404);
|
|
}
|
|
|
|
|
|
|
|
private function requiresCsrf(string $path): bool
|
|
{
|
|
// All browser-initiated POST routes require CSRF protection.
|
|
return true;
|
|
}
|
|
|
|
private function matchRedirect(string $path): ?Response
|
|
{
|
|
$db = Database::get();
|
|
if (!($db instanceof PDO)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$db->exec("
|
|
CREATE TABLE IF NOT EXISTS ac_redirects (
|
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
source_path VARCHAR(255) NOT NULL UNIQUE,
|
|
target_url VARCHAR(1000) NOT NULL,
|
|
status_code SMALLINT NOT NULL DEFAULT 301,
|
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
");
|
|
$stmt = $db->prepare("
|
|
SELECT target_url, status_code
|
|
FROM ac_redirects
|
|
WHERE source_path = :path AND is_active = 1
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([':path' => $path]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!$row) {
|
|
return null;
|
|
}
|
|
$code = (int)($row['status_code'] ?? 301);
|
|
if (!in_array($code, [301, 302, 307, 308], true)) {
|
|
$code = 301;
|
|
}
|
|
$target = trim((string)($row['target_url'] ?? ''));
|
|
if ($target === '' || $target === $path) {
|
|
return null;
|
|
}
|
|
return new Response('', $code, ['Location' => $target]);
|
|
} catch (Throwable $e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|