view = new View(__DIR__ . '/views'); } public function show(): Response { $slug = trim((string)($_GET['slug'] ?? '')); if ($slug === '') { return $this->notFound(); } $db = Database::get(); if (!$db instanceof PDO) { return $this->notFound(); } $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) { return $this->notFound(); } $rendered = Shortcodes::render((string)$page['content_html'], [ 'page_slug' => $slug, 'page_title' => (string)$page['title'], ]); // WYSIWYG editors often wrap shortcode blocks in

, which breaks grid placement. $rendered = preg_replace( '~

\s*(<(?:section|div|form|a)[^>]*class="[^"]*ac-shortcode[^"]*"[^>]*>.*?)\s*

~is', '$1', $rendered ) ?? $rendered; $rendered = preg_replace('~(<(?:section|div|form|a)[^>]*class="[^"]*ac-shortcode[^"]*"[^>]*>)\s*~i', '$1', $rendered) ?? $rendered; $rendered = preg_replace('~\s*()~i', '$1', $rendered) ?? $rendered; return new Response($this->view->render('site/show.php', [ 'title' => (string)$page['title'], 'content_html' => $rendered, ])); } public function adminIndex(): Response { if ($guard = $this->guard(['admin', 'manager', 'editor'])) { return $guard; } $db = Database::get(); $pages = []; if ($db instanceof PDO) { try { $stmt = $db->query("SELECT id, title, slug, is_published, is_home, is_blog_index, updated_at FROM ac_pages ORDER BY updated_at DESC"); $pages = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : []; } catch (Throwable $e) { $pages = []; } } return new Response($this->view->render('admin/index.php', [ 'title' => 'Pages', 'pages' => $pages, ])); } public function adminEdit(): Response { if ($guard = $this->guard(['admin', 'manager', 'editor'])) { return $guard; } $id = isset($_GET['id']) ? (int)$_GET['id'] : 0; $page = [ 'id' => 0, 'title' => '', 'slug' => '', 'content_html' => '', 'is_published' => 0, 'is_home' => 0, 'is_blog_index' => 0, ]; $db = Database::get(); if ($id > 0 && $db instanceof PDO) { $stmt = $db->prepare("SELECT id, title, slug, content_html, is_published, is_home, is_blog_index FROM ac_pages WHERE id = :id LIMIT 1"); $stmt->execute([':id' => $id]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row) { $page = $row; } } return new Response($this->view->render('admin/edit.php', [ 'title' => $id > 0 ? 'Edit Page' : 'New Page', 'page' => $page, 'error' => '', ])); } public function adminSave(): Response { if ($guard = $this->guard(['admin', 'manager', 'editor'])) { return $guard; } $db = Database::get(); if (!$db instanceof PDO) { return new Response('', 302, ['Location' => '/admin/pages']); } $id = (int)($_POST['id'] ?? 0); $title = trim((string)($_POST['title'] ?? '')); $slug = trim((string)($_POST['slug'] ?? '')); $content = (string)($_POST['content_html'] ?? ''); $isPublished = isset($_POST['is_published']) ? 1 : 0; $isHome = isset($_POST['is_home']) ? 1 : 0; $isBlogIndex = isset($_POST['is_blog_index']) ? 1 : 0; if ($title === '') { return $this->renderEditError($id, $title, $slug, $content, $isPublished, $isHome, $isBlogIndex, 'Title is required.'); } if ($slug === '') { $slug = $this->slugify($title); } else { $slug = $this->slugify($slug); } try { if ($id > 0) { $chk = $db->prepare("SELECT id FROM ac_pages WHERE slug = :slug AND id != :id LIMIT 1"); $chk->execute([':slug' => $slug, ':id' => $id]); } else { $chk = $db->prepare("SELECT id FROM ac_pages WHERE slug = :slug LIMIT 1"); $chk->execute([':slug' => $slug]); } if ($chk->fetch()) { return $this->renderEditError($id, $title, $slug, $content, $isPublished, $isHome, $isBlogIndex, 'Slug already exists.'); } if ($isHome === 1) { $db->exec("UPDATE ac_pages SET is_home = 0"); } if ($isBlogIndex === 1) { $db->exec("UPDATE ac_pages SET is_blog_index = 0"); } if ($id > 0) { $stmt = $db->prepare(" UPDATE ac_pages SET title = :title, slug = :slug, content_html = :content, is_published = :published, is_home = :is_home, is_blog_index = :is_blog_index WHERE id = :id "); $stmt->execute([ ':title' => $title, ':slug' => $slug, ':content' => $content, ':published' => $isPublished, ':is_home' => $isHome, ':is_blog_index' => $isBlogIndex, ':id' => $id, ]); } else { $stmt = $db->prepare(" INSERT INTO ac_pages (title, slug, content_html, is_published, is_home, is_blog_index) VALUES (:title, :slug, :content, :published, :is_home, :is_blog_index) "); $stmt->execute([ ':title' => $title, ':slug' => $slug, ':content' => $content, ':published' => $isPublished, ':is_home' => $isHome, ':is_blog_index' => $isBlogIndex, ]); } } catch (Throwable $e) { return $this->renderEditError($id, $title, $slug, $content, $isPublished, $isHome, $isBlogIndex, 'Unable to save page.'); } return new Response('', 302, ['Location' => '/admin/pages']); } public function adminDelete(): Response { if ($guard = $this->guard(['admin', 'manager'])) { return $guard; } $db = Database::get(); if (!$db instanceof PDO) { return new Response('', 302, ['Location' => '/admin/pages']); } $id = (int)($_POST['id'] ?? 0); if ($id > 0) { $stmt = $db->prepare("DELETE FROM ac_pages WHERE id = :id"); $stmt->execute([':id' => $id]); } return new Response('', 302, ['Location' => '/admin/pages']); } private function renderEditError(int $id, string $title, string $slug, string $content, int $isPublished, int $isHome, int $isBlogIndex, string $error): Response { $page = [ 'id' => $id, 'title' => $title, 'slug' => $slug, 'content_html' => $content, 'is_published' => $isPublished, 'is_home' => $isHome, 'is_blog_index' => $isBlogIndex, ]; return new Response($this->view->render('admin/edit.php', [ 'title' => $id > 0 ? 'Edit Page' : 'New Page', 'page' => $page, 'error' => $error, ])); } private function notFound(): Response { $view = new View(); return new Response($view->render('site/404.php', [ 'title' => 'Not Found', 'message' => 'Page not found.', ]), 404); } private function slugify(string $value): string { $value = strtolower(trim($value)); $value = preg_replace('~[^a-z0-9]+~', '-', $value) ?? $value; $value = trim($value, '-'); return $value !== '' ? $value : 'page'; } private function guard(array $roles): ?Response { if (!Auth::check()) { return new Response('', 302, ['Location' => '/admin/login']); } if (!Auth::hasRole($roles)) { return new Response('', 302, ['Location' => '/admin']); } return null; } }