date('c'), 'first_seen_url' => $sanitizeJourneyUrl($requestUri), 'first_referrer' => trim((string)($_SERVER['HTTP_REFERER'] ?? '')), 'user_agent' => trim((string)($_SERVER['HTTP_USER_AGENT'] ?? '')), 'accept_language' => trim((string)($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '')), ]; } $_SESSION['visitor_meta']['last_seen_at'] = date('c'); $_SESSION['visitor_meta']['last_seen_url'] = $sanitizeJourneyUrl($requestUri); if (in_array($requestMethod, ['GET', 'HEAD'], true)) { $normalizedPath = strtolower((string)$path); $noisePaths = [ 'favicon.ico', 'robots.txt', 'manifest.json', 'site.webmanifest', 'browserconfig.xml', 'apple-touch-icon.png', 'apple-touch-icon-precomposed.png', ]; $excluded = in_array($normalizedPath, $noisePaths, true) || ($path !== '' && preg_match('/^(admin|login|logout|media)(\/|$)/i', $path) === 1); $existingJourney = $_SESSION['visitor_journey'] ?? []; if (is_array($existingJourney) && !empty($existingJourney)) { $existingJourney = array_values(array_filter($existingJourney, static function($item) use ($noisePaths) { $u = trim((string)($item['u'] ?? '')); if ($u === '') { return false; } $urlPath = strtolower((string)(parse_url($u, PHP_URL_PATH) ?? '')); $urlPath = ltrim($urlPath, '/'); return $urlPath === '' || !in_array($urlPath, $noisePaths, true); })); $_SESSION['visitor_journey'] = $existingJourney; } if (!$excluded) { $journey = $_SESSION['visitor_journey'] ?? []; if (!is_array($journey)) { $journey = []; } $entry = ['t' => date('c'), 'u' => $sanitizeJourneyUrl($requestUri)]; $last = !empty($journey) ? $journey[count($journey) - 1] : null; if (empty($last) || ($last['u'] ?? '') !== $entry['u']) { $journey[] = $entry; if (count($journey) > 25) { $journey = array_slice($journey, -25); } $_SESSION['visitor_journey'] = $journey; } } } // Token access must be applied before visibility is calculated. if (!$security->isLoggedIn()) { $token = $_GET['token'] ?? $_GET['audience_token'] ?? $_GET['guest_token'] ?? $_GET['family_token'] ?? null; if ($token) { $security->setAudienceAccess($token); } } // Access visibility scope $visibleAccessLevels = getVisibleAccessLevels($security); $audienceSegmentFilter = getAudienceSegmentFilter($security); $renderNotFoundResponse = function() use ($db, $post, $template, $media, $security, $requestAccessToken, $visibleAccessLevels, $audienceSegmentFilter) { http_response_code(404); $page404 = null; $page404Id = (int)$db->getSetting('system_page_404_id', '0'); if ($page404Id > 0) { $assigned404Page = $post->getById($page404Id); $assigned404Path = trim((string)($assigned404Page['full_path'] ?? '')); if ($assigned404Page && ($assigned404Page['post_type'] ?? '') === 'page' && ($assigned404Page['status'] ?? '') === 'published' && $assigned404Path !== '') { $page404 = $post->getByPath($assigned404Path, $visibleAccessLevels, $audienceSegmentFilter); } } if ($page404 && canAccessTokenProtected($page404, $requestAccessToken, $security->isLoggedIn())) { $page404Template = resolveContentTemplateName($page404, false); echo $template->render($page404Template, [ 'entry' => $page404, 'page' => $page404, 'post' => $page404, 'section' => [ 'name' => $page404['title'], 'slug' => $page404['slug'], 'description' => '', 'content' => $page404['content'], 'full_path' => $page404['full_path'] ?? '', ], 'children' => [], 'posts' => [], 'has_children' => false, 'media' => $media, 'post_media' => !empty($page404['id']) ? $media->getByPost((int)$page404['id']) : [], 'page_title' => $page404['title'], 'meta_source' => $page404, 'csrf_token' => $security->generateCSRFToken() ]); return; } echo $template->render('404', ['page_title' => 'Page Not Found']); }; $renderPageResponse = function($pageData, $options = []) use ($post, $template, $security, $requestAccessToken, $visibleAccessLevels, $audienceSegmentFilter, $renderNotFoundResponse) { if (!$pageData) { return false; } if (!canAccessTokenProtected($pageData, $requestAccessToken, $security->isLoggedIn())) { $renderNotFoundResponse(); return true; } $children = $post->getChildrenByParent($pageData['id'], $visibleAccessLevels, 100, 0, $audienceSegmentFilter); if (!empty($children)) { $templateName = resolveHierarchyTemplateName($pageData); echo $template->render($templateName, ['section' => ['name' => $pageData['title'], 'slug' => $pageData['slug'], 'description' => '', 'content' => $pageData['content'], 'full_path' => $pageData['full_path'] ?? ''], 'meta_source' => $pageData, 'posts' => $children, 'page_title' => $pageData['title']]); } else { $templateName = $options['template_name'] ?? resolveContentTemplateName($pageData, false); $templateVars = [ 'entry' => $pageData, 'page' => $pageData, 'post' => $pageData, 'children' => [], 'posts' => [], 'has_children' => false, 'page_title' => $pageData['title'], 'meta_source' => $pageData, 'csrf_token' => $security->generateCSRFToken(), ]; if (!empty($options['template_vars']) && is_array($options['template_vars'])) { $templateVars = array_merge($templateVars, $options['template_vars']); } echo $template->render($templateName, $templateVars); } return true; }; // Simple routing if (empty($path)) { $homePage = null; $homePageId = (int)$db->getSetting('system_page_home_id', '0'); if ($homePageId > 0) { $assignedHomePage = $post->getById($homePageId); $assignedHomePath = trim((string)($assignedHomePage['full_path'] ?? '')); if ($assignedHomePage && ($assignedHomePage['post_type'] ?? '') === 'page' && ($assignedHomePage['status'] ?? '') === 'published' && $assignedHomePath !== '') { $homePage = $post->getByPath($assignedHomePath, $visibleAccessLevels, $audienceSegmentFilter); } } if (!empty($homePage)) { echo $template->render('home', [ 'page_title' => 'Home', 'home_content' => $homePage, 'meta_source' => $homePage, 'csrf_token' => $security->generateCSRFToken(), ]); } else { $homeContentPost = $post->getBySlugAndType('index', 'page', $visibleAccessLevels, $audienceSegmentFilter); if (!empty($homeContentPost)) { echo $template->render('home', [ 'page_title' => 'Home', 'home_content' => $homeContentPost, 'meta_source' => $homeContentPost, 'csrf_token' => $security->generateCSRFToken(), ]); } else { $posts = $post->getAll($visibleAccessLevels, null, 20, 0, $audienceSegmentFilter); echo $template->render('home', [ 'posts' => $posts, 'page_title' => 'Home', ]); } } } elseif ($path === 'login') { // Login page if ($security->isLoggedIn()) { redirect('/admin'); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { $error = 'Invalid security token. Please refresh the page and try again.'; } else { $username = $_POST['username'] ?? ''; $password = $_POST['password'] ?? ''; $result = $security->login($username, $password); if ($result['success']) { redirect('/admin'); } else { $error = $result['error']; } } } echo $template->render('admin/login', [ 'page_title' => 'Login', 'layout_mode' => 'admin', 'error' => $error ?? null, 'csrf_token' => $security->generateCSRFToken() ]); } elseif ($path === 'logout' || $path === 'admin/logout') { // Logout $security->logout(); redirect('/'); } elseif ($path === 'audience-access' || $path === 'guest-access' || $path === 'family-access') { // Audience access (legacy routes are still supported) if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { $error = 'Invalid security token. Please refresh the page and try again.'; } else { $key = $_POST['key'] ?? ''; if ($security->setAudienceAccess($key)) { redirect('/'); } else { $error = 'Invalid access key'; } } } echo $template->render('audience-access', [ 'page_title' => 'Audience Access', 'error' => $error ?? null, 'csrf_token' => $security->generateCSRFToken() ]); } elseif (preg_match('#^uploads/cache/(\d+)/(\d+)w\.(webp|jpg)$#', $path, $matches)) { $mediaId = (int)$matches[1]; $width = (int)$matches[2]; $format = $matches[3]; $mediaRow = $media->getById($mediaId); if (!$mediaRow || (int)$mediaRow['access_level'] !== 0) { http_response_code(404); exit; } $variant = $media->renderVariant($mediaId, $width, $format, $visibleAccessLevels); if (!$variant) { http_response_code(404); exit; } header('Content-Type: ' . $variant['mime']); header('Cache-Control: public, max-age=31536000, immutable'); readfile($variant['path']); exit; } elseif (preg_match('#^secure-media/original/(\d+)$#', $path, $matches)) { $mediaId = (int)$matches[1]; $mediaRow = $media->getById($mediaId); if (!$mediaRow) { http_response_code(404); exit; } if ((int)$mediaRow['access_level'] === 0) { redirect($media->getOriginalUrl($mediaRow), 302); } $original = $media->renderOriginal($mediaId, $visibleAccessLevels); if (!$original) { http_response_code(404); exit; } header('Content-Type: ' . $original['mime']); $safeName = str_replace('"', '', (string)($original['name'] ?? 'file')); header('Content-Disposition: inline; filename="' . $safeName . '"'); header('Cache-Control: private, max-age=0, no-store'); readfile($original['path']); exit; } elseif (preg_match('#^media/([a-z0-9\-]{1,160})\.([a-z0-9]{2,5})$#i', $path, $matches)) { $slug = strtolower((string)$matches[1]); $requestedExt = strtolower((string)$matches[2]); $mediaRow = $media->getByPublicSlug($slug); if (!$mediaRow) { http_response_code(404); exit; } $qs = (string)($_SERVER['QUERY_STRING'] ?? ''); $widthRequest = 0; if (isset($_GET['w'])) { $widthRequest = (int)$_GET['w']; } elseif (isset($_GET['width'])) { $widthRequest = (int)$_GET['width']; } elseif (preg_match('/(\d+)w?/i', $qs, $m)) { $widthRequest = (int)$m[1]; } $fmt = (string)($_GET['fmt'] ?? $_GET['format'] ?? ''); $fmt = strtolower(trim($fmt)); if ($fmt === 'jpeg') { $fmt = 'jpg'; } if (!in_array($fmt, ['webp', 'jpg'], true)) { $fmt = ''; } $storedExt = strtolower((string)pathinfo((string)$mediaRow['file_path'], PATHINFO_EXTENSION)); if ($storedExt === 'jpeg') { $storedExt = 'jpg'; } if ($requestedExt === 'jpeg') { $requestedExt = 'jpg'; } if ($storedExt !== '' && $requestedExt !== $storedExt && $fmt === '') { $target = '/media/' . rawurlencode($slug) . '.' . $storedExt; if ($qs !== '') { $target .= '?' . $qs; } redirect($target, 301); } $isPublic = (int)($mediaRow['access_level'] ?? 0) === 0; if ($widthRequest > 0) { $allowed = $media->getAllowedWidths(); $allowed = array_map('intval', array_values($allowed ?: [])); sort($allowed); $width = 0; foreach ($allowed as $w) { if ($w >= $widthRequest) { $width = $w; break; } } if ($width <= 0) { $width = (int)end($allowed); } $format = $fmt !== '' ? $fmt : 'webp'; $variant = $media->renderVariant((int)$mediaRow['id'], $width, $format, $visibleAccessLevels); if (!$variant) { http_response_code(404); exit; } header('Content-Type: ' . $variant['mime']); header('Cache-Control: ' . ($isPublic ? 'public, max-age=86400' : 'private, max-age=0, no-store')); readfile($variant['path']); exit; } if (!$isPublic) { $original = $media->renderOriginal((int)$mediaRow['id'], $visibleAccessLevels); if (!$original) { http_response_code(404); exit; } header('Content-Type: ' . $original['mime']); $safeName = str_replace('"', '', (string)($original['name'] ?? 'file')); header('Content-Disposition: inline; filename="' . $safeName . '"'); header('Cache-Control: private, max-age=0, no-store'); readfile($original['path']); exit; } if (!is_file((string)($mediaRow['file_path'] ?? ''))) { http_response_code(404); exit; } header('Content-Type: ' . ((string)($mediaRow['mime_type'] ?? 'application/octet-stream'))); header('Cache-Control: public, max-age=86400'); readfile($mediaRow['file_path']); exit; } elseif (preg_match('#^secure-media/(\d+)/(\d+)w\.(webp|jpg)$#', $path, $matches)) { $mediaId = (int)$matches[1]; $width = (int)$matches[2]; $format = $matches[3]; $variant = $media->renderVariant($mediaId, $width, $format, $visibleAccessLevels); if (!$variant) { http_response_code(404); exit; } header('Content-Type: ' . $variant['mime']); header('Cache-Control: private, max-age=0, no-store'); readfile($variant['path']); exit; } elseif ($path === 'admin') { if (!$security->isLoggedIn()) { redirect('/login'); } $dashboardStats = [ 'total_content' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM posts')['count'] ?? 0), 'published_items' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM posts WHERE status = ?', ['published'])['count'] ?? 0), 'draft_items' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM posts WHERE status = ?', ['draft'])['count'] ?? 0), 'pages_projects' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM posts WHERE post_type IN ("page", "project")')['count'] ?? 0), 'blog_posts' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM posts WHERE post_type = ?', ['post'])['count'] ?? 0), 'audience_segments' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM guest_segments')['count'] ?? 0), 'taxonomy_terms' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM terms')['count'] ?? 0), ]; $recentContent = $db->query( 'SELECT id, title, slug, full_path, post_type, status, created_at, updated_at FROM posts ORDER BY COALESCE(updated_at, created_at) DESC, id DESC LIMIT 8' ) ?: []; echo $template->render('admin/dashboard', [ 'page_title' => 'Dashboard', 'layout_mode' => 'admin', 'dashboard_stats' => $dashboardStats, 'recent_content' => $recentContent, ]); } elseif ($path === 'settings' || $path === 'admin/settings') { // Settings page if (!$security->isLoggedIn()) { redirect('/login'); } if ($path === 'settings' && $_SERVER['REQUEST_METHOD'] === 'GET') { redirect('/admin/settings', 301); } // Get success/error messages from URL $success = null; $error = null; if (isset($_GET['success'])) { switch ($_GET['success']) { case 'guest_key_updated': case 'family_key_updated': case 'audience_key_updated': $success = 'Audience access key updated successfully!'; break; case 'password_updated': $success = 'Password changed successfully!'; break; case 'theme_updated': $success = 'Admin theme updated successfully!'; break; case 'frontend_theme_updated': $success = 'Frontend theme updated successfully!'; break; case 'system_pages_updated': $success = 'System pages updated successfully!'; break; case 'updates_applied': $success = 'Pending updates applied successfully!'; break; } } if (isset($_GET['error'])) { $error = $_GET['error']; if ($error === 'invalid_token') { $error = 'Invalid security token. Please try again.'; } } $systemPageOptions = array_values(array_filter($post->getAllPages() ?: [], function($item) { return ($item['post_type'] ?? '') === 'page'; })); $systemPageSettings = [ 'home' => (int)$db->getSetting('system_page_home_id', '0'), 'blog_index' => (int)$db->getSetting('system_page_blog_index_id', '0'), 'contact' => (int)$db->getSetting('system_page_contact_id', '0'), '404' => (int)$db->getSetting('system_page_404_id', '0'), ]; $migrationStatus = $migrationRunner->getStatus(); $pendingMigrations = $migrationRunner->getPendingMigrations(); $mediaVariantWidths = $db->getSetting('media_variant_widths') ?: '320,480,640,768,1024,1280,1600,1920,2560'; $frontendTheme = getCurrentFrontendTheme(); $frontendThemeOptions = getAvailableFrontendThemes(); echo $template->render('admin/settings', [ 'page_title' => 'Settings', 'layout_mode' => 'admin', 'csrf_token' => $security->generateCSRFToken(), 'success' => $success, 'error' => $error, 'system_page_options' => $systemPageOptions, 'system_page_settings' => $systemPageSettings, 'migration_status' => $migrationStatus, 'pending_migrations' => $pendingMigrations, 'media_variant_widths' => $mediaVariantWidths, 'frontend_theme' => $frontendTheme, 'frontend_theme_options' => $frontendThemeOptions, ]); } elseif ($path === 'settings/update-theme' || $path === 'admin/settings/update-theme') { if (!$security->isLoggedIn()) { redirect('/login'); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { redirect('/admin/settings?error=invalid_token'); } $adminThemeMode = trim((string)($_POST['admin_theme_mode'] ?? 'light')); $allowedModes = ['light', 'dark']; if (!in_array($adminThemeMode, $allowedModes, true)) { redirect('/admin/settings?error=' . urlencode('Invalid admin theme mode')); } if (!$db->setSetting('admin_theme_mode', $adminThemeMode)) { redirect('/admin/settings?error=' . urlencode('Failed to save admin theme')); } $template->set('admin_theme_mode', $adminThemeMode); redirect('/admin/settings?success=theme_updated'); } redirect('/admin/settings'); } elseif ($path === 'settings/update-frontend-theme' || $path === 'admin/settings/update-frontend-theme') { if (!$security->isLoggedIn()) { redirect('/login'); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { redirect('/admin/settings?error=invalid_token'); } $frontendTheme = trim((string)($_POST['frontend_theme'] ?? 'default')); $allowedThemes = getAvailableFrontendThemes(); if (!isset($allowedThemes[$frontendTheme])) { redirect('/admin/settings?error=' . urlencode('Invalid frontend theme selected')); } if (!$db->setSetting('frontend_theme', $frontendTheme)) { redirect('/admin/settings?error=' . urlencode('Failed to save frontend theme')); } redirect('/admin/settings?success=frontend_theme_updated'); } redirect('/admin/settings'); } elseif ($path === 'settings/update-media-settings' || $path === 'admin/settings/update-media-settings') { if (!$security->isLoggedIn()) { redirect('/login'); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { redirect('/admin/settings?error=invalid_token'); } $variantWidths = trim((string)($_POST['media_variant_widths'] ?? '')); if ($variantWidths === '') { redirect('/admin/settings?error=' . urlencode('Image variant widths cannot be empty.')); } // Validate: only digits, commas, and spaces $validated = preg_replace('/[^0-9,\s]/', '', $variantWidths); $widths = array_filter(array_map('intval', array_map('trim', explode(',', $validated)))); if (empty($widths)) { redirect('/admin/settings?error=' . urlencode('Invalid image variant widths format. Use comma-separated numbers.')); } // Sort and deduplicate $widths = array_values(array_unique($widths)); sort($widths); $widthsString = implode(',', $widths); if (!$db->setSetting('media_variant_widths', $widthsString)) { redirect('/admin/settings?error=' . urlencode('Failed to save media settings.')); } redirect('/admin/settings?success=media_settings_updated'); } redirect('/admin/settings'); } elseif ($path === 'settings/run-updates' || $path === 'admin/settings/run-updates') { if (!$security->isLoggedIn()) { redirect('/login'); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { redirect('/admin/settings?error=invalid_token'); } if ($migrationRunner->runPending()) { redirect('/admin/settings?success=updates_applied'); } redirect('/admin/settings?error=' . urlencode($migrationRunner->getLastError() ?: 'Failed to apply pending updates.')); } redirect('/admin/settings'); } elseif ($path === 'settings/update-system-pages' || $path === 'admin/settings/update-system-pages') { if (!$security->isLoggedIn()) { redirect('/login'); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { redirect('/admin/settings?error=invalid_token'); } foreach (['home' => 'system_page_home_id', 'blog_index' => 'system_page_blog_index_id', 'contact' => 'system_page_contact_id', '404' => 'system_page_404_id'] as $inputKey => $settingKey) { $pageId = (int)($_POST[$inputKey . '_page_id'] ?? 0); if ($pageId > 0) { $pageItem = $post->getById($pageId); if (!$pageItem || ($pageItem['post_type'] ?? '') !== 'page' || ($pageItem['status'] ?? '') !== 'published') { redirect('/admin/settings?error=' . urlencode('Invalid system page selection.')); } } if (!$db->setSetting($settingKey, (string)$pageId)) { redirect('/admin/settings?error=' . urlencode('Failed to save system pages.')); } } redirect('/admin/settings?success=system_pages_updated'); } redirect('/admin/settings'); } elseif ($path === 'settings/update-password' || $path === 'admin/settings/update-password') { // Update password if (!$security->isLoggedIn()) { redirect('/login'); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { redirect('/admin/settings?error=invalid_token'); } $currentPassword = $_POST['current_password'] ?? ''; $newPassword = $_POST['new_password'] ?? ''; $confirmPassword = $_POST['confirm_password'] ?? ''; // Check if passwords match if ($newPassword !== $confirmPassword) { redirect('/admin/settings?error=' . urlencode('Passwords do not match')); } $userId = $security->getCurrentUser()['id']; $result = $security->changePassword($userId, $currentPassword, $newPassword); if ($result['success']) { redirect('/admin/settings?success=password_updated'); } else { redirect('/admin/settings?error=' . urlencode($result['error'])); } } redirect('/admin/settings'); } elseif ($path === 'audiences' || $path === 'admin/audiences') { if (!$security->isLoggedIn()) { redirect('/login'); } if ($path === 'audiences' && $_SERVER['REQUEST_METHOD'] === 'GET') { redirect('/admin/audiences', 301); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { redirect('/admin/audiences?error=invalid_token'); } $action = $_POST['action'] ?? ''; if ($action === 'create-segment') { $segmentName = $_POST['segment_name'] ?? ''; $segmentDescription = $_POST['segment_description'] ?? ''; $includePublicContent = !empty($_POST['include_public_content']) ? 1 : 0; if ($post->createAudienceSegment($segmentName, $segmentDescription, $includePublicContent)) { redirect('/admin/audiences?success=segment_created'); } redirect('/admin/audiences?error=' . urlencode($post->getLastError() ?: 'segment_create_failed')); } if ($action === 'update-segment-visibility') { $segmentId = (int)($_POST['segment_id'] ?? 0); $includePublicContent = !empty($_POST['include_public_content']) ? 1 : 0; if ($post->updateAudienceSegmentVisibility($segmentId, $includePublicContent)) { redirect('/admin/audiences?success=segment_updated'); } redirect('/admin/audiences?error=' . urlencode($post->getLastError() ?: 'segment_update_failed')); } if ($action === 'delete-segment') { $segmentId = (int)($_POST['segment_id'] ?? 0); if ($post->deleteAudienceSegment($segmentId)) { redirect('/admin/audiences?success=segment_deleted'); } redirect('/admin/audiences?error=' . urlencode($post->getLastError() ?: 'segment_delete_failed')); } if ($action === 'regenerate-token') { $segmentId = (int)($_POST['segment_id'] ?? 0); $tokenData = $security->regenerateAudienceSegmentToken($segmentId, 6); if ($tokenData) { redirect('/admin/audiences?success=token_regenerated'); } redirect('/admin/audiences?error=' . urlencode('token_regenerate_failed')); } } $segments = $post->getAudienceSegments() ?: []; $tokens = $security->getAudienceTokens() ?: []; $tokensBySegmentId = []; foreach ($tokens as $tokenRow) { $segmentId = (int)($tokenRow['segment_id'] ?? 0); if ($segmentId > 0 && !isset($tokensBySegmentId[$segmentId]) && (int)($tokenRow['is_active'] ?? 0) === 1) { $tokensBySegmentId[$segmentId] = $tokenRow; } } foreach ($segments as &$segment) { $segmentId = (int)($segment['id'] ?? 0); if ($segmentId <= 0) { continue; } $tokenRow = $tokensBySegmentId[$segmentId] ?? $security->ensureAudienceSegmentToken($segmentId, 6); $segment['token'] = $tokenRow['token'] ?? ''; $segment['share_link'] = rtrim($config['site']['url'], '/') . '?token=' . rawurlencode($segment['token']); } unset($segment); echo $template->render('admin/audiences', [ 'page_title' => 'Audience Segments', 'layout_mode' => 'admin', 'segments' => $segments, 'success' => $_GET['success'] ?? null, 'error' => $_GET['error'] ?? null, 'csrf_token' => $security->generateCSRFToken() ]); } elseif ($path === 'taxonomies' || $path === 'admin/taxonomies') { if (!$security->isLoggedIn()) { redirect('/login'); } if ($path === 'taxonomies' && $_SERVER['REQUEST_METHOD'] === 'GET') { redirect('/admin/taxonomies', 301); } $activeTaxonomy = $_GET['taxonomy'] ?? 'category'; $taxonomyMap = []; foreach (($post->getTaxonomies() ?? []) as $taxonomyItem) { $taxonomyMap[$taxonomyItem['slug']] = $taxonomyItem; } if (!isset($taxonomyMap[$activeTaxonomy])) { $activeTaxonomy = array_key_first($taxonomyMap) ?: 'category'; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { redirect('/admin/taxonomies?taxonomy=' . urlencode($activeTaxonomy) . '&error=invalid_token'); } $action = $_POST['action'] ?? ''; if ($action === 'create-term') { $taxonomySlug = $_POST['taxonomy_slug'] ?? $activeTaxonomy; $termName = $_POST['term_name'] ?? ''; $termDescription = $_POST['term_description'] ?? ''; if ($post->createTerm($taxonomySlug, $termName, $termDescription)) { redirect('/admin/taxonomies?taxonomy=' . urlencode($taxonomySlug) . '&success=term_created'); } redirect('/admin/taxonomies?taxonomy=' . urlencode($taxonomySlug) . '&error=' . urlencode($post->getLastError() ?: 'term_create_failed')); } if ($action === 'delete-term') { $taxonomySlug = $_POST['taxonomy_slug'] ?? $activeTaxonomy; $termId = (int)($_POST['term_id'] ?? 0); if ($post->deleteTerm($termId)) { redirect('/admin/taxonomies?taxonomy=' . urlencode($taxonomySlug) . '&success=term_deleted'); } redirect('/admin/taxonomies?taxonomy=' . urlencode($taxonomySlug) . '&error=' . urlencode($post->getLastError() ?: 'term_delete_failed')); } } echo $template->render('admin/taxonomies', [ 'page_title' => 'Taxonomies', 'layout_mode' => 'admin', 'taxonomies' => array_values($taxonomyMap), 'active_taxonomy' => $activeTaxonomy, 'terms' => $post->getTermsWithUsage($activeTaxonomy), 'success' => $_GET['success'] ?? null, 'error' => $_GET['error'] ?? null, 'csrf_token' => $security->generateCSRFToken() ]); } elseif ($path === 'admin/media-inline-upload') { header('Content-Type: application/json; charset=utf-8'); register_shutdown_function(static function () { $err = error_get_last(); if ($err && in_array((int)($err['type'] ?? 0), [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR], true)) { if (!headers_sent()) { http_response_code(500); header('Content-Type: application/json; charset=utf-8'); } echo json_encode([ 'success' => false, 'error' => 'php_fatal', 'message' => (string)($err['message'] ?? ''), 'file' => (string)($err['file'] ?? ''), 'line' => (int)($err['line'] ?? 0), ]); } }); try { if (!$security->isLoggedIn()) { jsonResponse(['success' => false, 'error' => 'unauthorized'], 401); } if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) !== 'POST') { jsonResponse(['success' => false, 'error' => 'method_not_allowed'], 405); } if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { jsonResponse(['success' => false, 'error' => 'invalid_token'], 400); } $userId = (int)($security->getCurrentUser()['id'] ?? 0); $postId = (int)($_POST['post_id'] ?? 0); if ($postId <= 0) { jsonResponse(['success' => false, 'error' => 'invalid_post'], 400); } $postRow = $post->getById($postId); if (!$postRow || (int)($postRow['user_id'] ?? 0) !== $userId) { jsonResponse(['success' => false, 'error' => 'forbidden'], 403); } $file = $_FILES['file'] ?? null; if (!$file || !isset($file['tmp_name'])) { jsonResponse(['success' => false, 'error' => 'no_file'], 400); } $result = $media->upload($file, [ 'post_id' => $postId, 'created_by' => $userId, 'title' => (string)($_POST['title'] ?? ''), 'alt_text' => (string)($_POST['alt_text'] ?? ''), 'caption' => (string)($_POST['caption'] ?? ''), 'public_slug' => (string)($_POST['public_slug'] ?? ''), 'access_level' => 0, ]); if (empty($result['success'])) { jsonResponse(['success' => false, 'error' => $result['error'] ?? 'upload_failed'], 400); } $mediaId = (int)($result['id'] ?? 0); $mediaRow = $media->getById($mediaId); if (!$mediaRow) { jsonResponse(['success' => false, 'error' => 'media_not_found'], 400); } $ext = strtolower((string)pathinfo((string)($mediaRow['file_path'] ?? ''), PATHINFO_EXTENSION)); if ($ext === 'jpeg') { $ext = 'jpg'; } $publicSlug = trim((string)($mediaRow['public_slug'] ?? '')); $publicUrlBase = $publicSlug !== '' ? url('/media/' . $publicSlug . '.' . ($ext !== '' ? $ext : 'jpg')) : $media->getOriginalUrl($mediaRow); $alt = trim((string)($mediaRow['alt_text'] ?? '')); $title = trim((string)($mediaRow['title'] ?? '')); $allowed = $media->getAllowedWidths(); $allowed = array_map('intval', array_values($allowed ?: [])); sort($allowed); $originalWidth = (int)($mediaRow['width'] ?? 0); $srcsetWebp = []; $srcsetJpg = []; foreach ($allowed as $w) { if ($originalWidth > 0 && $w > $originalWidth + 50) { continue; } $srcsetWebp[] = $publicUrlBase . '?' . $w . 'w&fmt=webp ' . $w . 'w'; $srcsetJpg[] = $publicUrlBase . '?' . $w . 'w&fmt=jpg ' . $w . 'w'; } $defaultW = 0; foreach ($allowed as $w) { if ($w >= 640) { $defaultW = $w; break; } } if ($defaultW <= 0) { $defaultW = (int)end($allowed); } $srcJpg = $publicUrlBase . '?' . $defaultW . 'w&fmt=jpg'; $dimAttrs = ''; if (!empty($mediaRow['width']) && !empty($mediaRow['height'])) { $dimAttrs = ' width="' . (int)$mediaRow['width'] . '" height="' . (int)$mediaRow['height'] . '"'; } if ($title !== '') { $dimAttrs .= ' title="' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '"'; } $embedHtml = '' . '' . '' . htmlspecialchars($alt, ENT_QUOTES, 'UTF-8') . '' . ''; $simpleImgAttrs = ''; if (!empty($mediaRow['width']) && !empty($mediaRow['height'])) { $simpleImgAttrs .= ' width="' . (int)$mediaRow['width'] . '" height="' . (int)$mediaRow['height'] . '"'; } if ($title !== '') { $simpleImgAttrs .= ' title="' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '"'; } $simpleImg = '' . htmlspecialchars($alt, ENT_QUOTES, 'UTF-8') . ''; $inlinePostUpdate = []; if (!empty($_POST['set_featured'])) { $inlinePostUpdate['featured_media_id'] = $mediaId; } if (!empty($_POST['set_og_image'])) { $inlinePostUpdate['og_media_id'] = $mediaId; } if (!empty($inlinePostUpdate)) { $post->update($postId, $inlinePostUpdate, $userId); } jsonResponse([ 'success' => true, 'media_id' => $mediaId, 'media_title' => (string)($mediaRow['title'] ?? $mediaRow['original_name'] ?? ''), 'public_slug' => $publicSlug, 'public_url' => $publicUrlBase, 'embed_html' => $embedHtml, 'simple_img' => $simpleImg, ], 200); } catch (\Throwable $e) { error_log('[media-inline-upload] ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine()); if (!headers_sent()) { http_response_code(500); header('Content-Type: application/json; charset=utf-8'); } echo json_encode([ 'success' => false, 'error' => 'php_throw', 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), ]); } } elseif ($path === 'admin/media') { if (!$security->isLoggedIn()) { redirect('/login'); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $returnTo = (string)($_POST['return_to'] ?? ''); if ($returnTo === '' || strpos($returnTo, '/') !== 0 || strpos($returnTo, '/admin') !== 0 || preg_match('/[\r\n]/', $returnTo)) { $returnTo = '/admin/media'; } $appendParam = static function($url, $key, $value) { $sep = (strpos($url, '?') === false) ? '?' : '&'; return $url . $sep . rawurlencode((string)$key) . '=' . rawurlencode((string)$value); }; if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { redirect($appendParam($returnTo, 'error', 'invalid_token')); } $action = $_POST['action'] ?? ''; if ($action === 'attach') { $mediaId = (int)($_POST['media_id'] ?? 0); $postId = (int)($_POST['post_id'] ?? 0); if ($mediaId > 0 && $postId > 0 && $media->attachToPost($mediaId, $postId)) { redirect($appendParam($returnTo, 'success', 'attached')); } redirect($appendParam($returnTo, 'error', 'attach_failed')); } if ($action === 'detach') { $mediaId = (int)($_POST['media_id'] ?? 0); if ($mediaId > 0 && $media->detachFromPost($mediaId)) { redirect($appendParam($returnTo, 'success', 'detached')); } redirect($appendParam($returnTo, 'error', 'detach_failed')); } if ($action === 'update') { $mediaId = (int)($_POST['media_id'] ?? 0); if ($mediaId <= 0) { redirect($appendParam($returnTo, 'error', 'invalid_media')); } $currentMedia = $media->getById($mediaId); $oldPostId = (int)($currentMedia['post_id'] ?? 0); $data = []; foreach (['alt_text', 'title', 'caption', 'tags', 'galleries', 'public_slug'] as $field) { if (array_key_exists($field, $_POST)) { $data[$field] = $_POST[$field]; } } if (array_key_exists('access_level', $_POST)) { $data['access_level'] = (int)$_POST['access_level']; } $postId = (int)($_POST['post_id'] ?? 0); if ($postId > 0) { $data['post_id'] = $postId; } else { $data['post_id'] = null; } if ($media->update($mediaId, $data)) { if (array_key_exists('post_id', $data)) { $newPostId = (int)$data['post_id']; if ($newPostId !== $oldPostId) { $media->attachToPost($mediaId, $newPostId); } } redirect($appendParam($returnTo, 'success', 'updated')); } redirect($appendParam($returnTo, 'error', $media->getLastError() ?: 'update_failed')); } if ($action === 'upload') { $accessLevel = (int)($_POST['access_level'] ?? 0); if (!in_array($accessLevel, [0, 1, 2], true)) { $accessLevel = 0; } $caption = (string)($_POST['caption'] ?? ''); $tags = $_POST['tags'] ?? ''; $galleries = $_POST['galleries'] ?? ''; $userId = (int)($security->getCurrentUser()['id'] ?? 0); $postId = (int)($_POST['post_id'] ?? 0); $files = $_FILES['files'] ?? null; if (!$files || !isset($files['name'])) { redirect($appendParam($returnTo, 'error', 'no_files')); } $uploaded = 0; $firstError = null; $count = is_array($files['name']) ? count($files['name']) : 1; for ($i = 0; $i < $count; $i++) { $file = [ 'name' => is_array($files['name']) ? $files['name'][$i] : $files['name'], 'type' => is_array($files['type']) ? $files['type'][$i] : $files['type'], 'tmp_name' => is_array($files['tmp_name']) ? $files['tmp_name'][$i] : $files['tmp_name'], 'error' => is_array($files['error']) ? $files['error'][$i] : $files['error'], 'size' => is_array($files['size']) ? $files['size'][$i] : $files['size'], ]; if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) { $firstError = $firstError ?: 'Upload error.'; continue; } $options = [ 'access_level' => $accessLevel, 'caption' => $caption, 'created_by' => $userId, 'tags' => $tags, 'galleries' => $galleries, ]; if ($postId > 0) { $options['post_id'] = $postId; } $result = $media->upload($file, $options); if (!empty($result['success'])) { $uploaded++; } else { $firstError = $firstError ?: ($result['error'] ?? 'upload_failed'); } } if ($uploaded > 0) { $returnTo = $appendParam($returnTo, 'success', 'uploaded'); $returnTo = $appendParam($returnTo, 'count', (string)$uploaded); if ($firstError) { $returnTo = $appendParam($returnTo, 'warning', (string)$firstError); } redirect($returnTo); } redirect($appendParam($returnTo, 'error', $firstError ?: 'upload_failed')); } redirect($returnTo); } $attachPostId = (int)($_GET['attach_post_id'] ?? 0); $returnTo = (string)($_GET['return_to'] ?? ''); $returnTo = (string)(parse_url($returnTo, PHP_URL_PATH) ?? ''); if ($returnTo === '' || strpos($returnTo, '/') !== 0 || strpos($returnTo, '/admin') !== 0) { $returnTo = '/admin/media'; } $editMediaId = (int)($_GET['edit_id'] ?? 0); $editMedia = $editMediaId > 0 ? $media->getById($editMediaId) : null; $editMediaTags = ''; $editMediaGalleries = ''; if ($editMedia) { $editMediaTags = implode(', ', array_column($media->getMediaTerms($editMediaId, 'tag'), 'name')); $editMediaGalleries = implode(', ', array_column($media->getMediaTerms($editMediaId, 'gallery'), 'name')); } $filters = [ 'search' => trim((string)($_GET['q'] ?? '')), 'access_level' => isset($_GET['access_level']) ? (string)$_GET['access_level'] : '', 'unattached' => !empty($_GET['unattached']) ? 1 : 0, ]; $filterCombo = trim((string)($_GET['filter'] ?? '')); if ($filterCombo !== '' && strpos($filterCombo, ':') !== false) { list($taxSlug, $termSlug) = explode(':', $filterCombo, 2); if (in_array($taxSlug, ['tag', 'gallery'], true) && $termSlug !== '') { $filters['taxonomy_slug'] = $taxSlug; $filters['term_slug'] = $termSlug; } } $mediaItems = $media->getAllWithPostInfo($filters, 60, 0); $mediaTotal = $media->countAll($filters); $allPostsForAttach = $post->getAllPagesProjects() ?: []; $allPostsList = $post->getAllPostsList() ?: []; $allPostsForAttach = array_merge($allPostsForAttach, $allPostsList); echo $template->render('admin/media', [ 'page_title' => 'Media', 'layout_mode' => 'admin', 'success' => $_GET['success'] ?? null, 'warning' => $_GET['warning'] ?? null, 'error' => $_GET['error'] ?? null, 'filters' => $filters, 'filter_combo' => $filterCombo, 'attach_post_id' => $attachPostId, 'return_to' => $returnTo, 'edit_media' => $editMedia, 'edit_media_tags' => $editMediaTags, 'edit_media_galleries' => $editMediaGalleries, 'media_items' => $mediaItems, 'media_total' => $mediaTotal, 'media_tags' => $post->getTermsByTaxonomy('tag') ?: [], 'media_galleries' => $post->getTermsByTaxonomy('gallery') ?: [], 'media' => $media, 'all_posts_for_attach' => $allPostsForAttach, 'csrf_token' => $security->generateCSRFToken(), ]); } elseif ($path === 'structure' || $path === 'admin/content') { // Structure management: root pages/projects and child items if (!$security->isLoggedIn()) { redirect('/login'); } if ($path === 'structure' && $_SERVER['REQUEST_METHOD'] === 'GET') { redirect('/admin/content', 301); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $isAjax = strtolower((string)($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest'; $respondJson = function($ok, $message = '', $httpCode = 200) { http_response_code($httpCode); header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'success' => (bool)$ok, 'message' => (string)$message, ]); exit; }; if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { if ($isAjax) { $respondJson(false, 'Invalid security token.', 400); } redirect('/admin/content?error=invalid_token'); } $structurePayload = trim((string)($_POST['structure_payload'] ?? '')); if ($structurePayload !== '') { $decodedPayload = json_decode($structurePayload, true); if (!is_array($decodedPayload)) { if ($isAjax) { $respondJson(false, 'Invalid structure payload.', 400); } redirect('/admin/content?error=' . urlencode('Invalid structure payload.')); } if ($post->saveStructureTree($decodedPayload, $security->getCurrentUser()['id'])) { if ($isAjax) { $respondJson(true, 'Content tree updated.'); } redirect('/admin/content?success=structure_updated'); } if ($isAjax) { $respondJson(false, $post->getLastError() ?: 'structure_update_failed', 400); } redirect('/admin/content?error=' . urlencode($post->getLastError() ?: 'structure_update_failed')); } $sortOrders = $_POST['sort_order'] ?? []; if (is_array($sortOrders) && !empty($sortOrders)) { foreach ($sortOrders as $postId => $value) { $postId = (int)$postId; if ($postId <= 0) { continue; } $post->update($postId, ['sort_order' => (int)$value], $security->getCurrentUser()['id']); } } if ($isAjax) { $respondJson(false, 'Empty structure payload.', 400); } redirect('/admin/content?success=sort_updated'); } $items = $post->getStructureTree(); $tree = buildTree($items ?: []); $allPagesProjects = $post->getAllPagesProjects(); $allPosts = $post->getAllPostsList(); echo $template->render('admin/structure', [ 'page_title' => 'Content', 'layout_mode' => 'admin', 'success' => $_GET['success'] ?? null, 'error' => $_GET['error'] ?? null, 'all_pages_projects' => $allPagesProjects, 'all_posts' => $allPosts, 'csrf_token' => $security->generateCSRFToken(), 'tree' => $tree, ]); } elseif ( $path === 'new-post' || $path === 'admin/new' || preg_match('/^edit\/(.+)$/', $path, $matches) || preg_match('/^admin\/edit\/(.+)$/', $path, $matches) ) { // Create/edit post if (!$security->isLoggedIn()) { redirect('/login'); } if ($path === 'new-post' && $_SERVER['REQUEST_METHOD'] === 'GET') { $queryString = $_SERVER['QUERY_STRING'] ?? ''; redirect('/admin/new' . ($queryString !== '' ? '?' . $queryString : ''), 301); } if (preg_match('/^edit\/(.+)$/', $path, $legacyEditMatches) && $_SERVER['REQUEST_METHOD'] === 'GET') { redirect('/admin/edit/' . rawurlencode($legacyEditMatches[1]), 301); } $editSlug = isset($matches[1]) ? rawurldecode((string)$matches[1]) : null; $editPost = null; $isEditMode = false; $formError = null; if ($editSlug !== null && $editSlug !== '') { $slugCandidates = array_values(array_unique(array_filter([ $editSlug, trim($editSlug, '/'), trim($editSlug, '/') !== '' ? trim($editSlug, '/') . '/' : null, ], function($value) { return $value !== null && $value !== ''; }))); foreach ($slugCandidates as $slugCandidate) { $editPost = $post->getBySlug($slugCandidate, 2); if ($editPost) { break; } } if (!$editPost || $editPost['user_id'] != $security->getCurrentUser()['id']) { redirect('/'); } $isEditMode = true; } elseif (!empty($_GET['parent_id'])) { $parent = $post->getById((int)$_GET['parent_id']); if ($parent && in_array(($parent['post_type'] ?? ''), ['page', 'project'], true)) { $editPost = [ 'parent_id' => (int)$parent['id'], 'post_type' => $parent['post_type'], 'access_level' => $parent['access_level'] ?? 0, 'status' => 'published', ]; } } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $formPath = $isEditMode && !empty($editPost['slug']) ? '/admin/edit/' . rawurlencode((string)$editPost['slug']) : '/admin/new'; // Check if it's a detach action (unassign media from post) $action = $_POST['action'] ?? ''; if ($action === 'detach') { $mediaId = (int)($_POST['media_id'] ?? 0); $returnTo = $_POST['return_to'] ?? $formPath; if ($mediaId > 0 && $media->detachFromPost($mediaId)) { redirect($returnTo . '?success=detached'); } redirect($returnTo . '?error=detach_failed'); } if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { redirect($formPath . '?error=invalid_token'); } $data = [ 'title' => $_POST['title'] ?? '', 'slug' => $_POST['slug'] ?? '', 'parent_id' => $_POST['parent_id'] ?? null, 'template_key' => $_POST['template_key'] ?? null, 'menu_header' => !empty($_POST['menu_header']) ? 1 : 0, 'menu_footer' => !empty($_POST['menu_footer']) ? 1 : 0, 'menu_order' => (int)($_POST['menu_order'] ?? 0), 'meta_title' => $_POST['meta_title'] ?? null, 'meta_description' => $_POST['meta_description'] ?? null, 'excerpt' => $_POST['excerpt'] ?? null, 'meta_robots' => $_POST['meta_robots'] ?? 'index,follow', 'canonical_url' => $_POST['canonical_url'] ?? null, 'hreflang' => $_POST['hreflang'] ?? null, 'token_required' => !empty($_POST['token_required']) ? 1 : 0, 'access_token' => $_POST['access_token'] ?? null, 'sort_order' => (int)($_POST['sort_order'] ?? 0), 'content' => $_POST['content'] ?? '', 'access_level' => (int)($_POST['access_level'] ?? 0), 'post_type' => $_POST['post_type'] ?? 'post', 'status' => $_POST['status'] ?? 'published', 'categories' => [ 'names' => $_POST['categories'] ?? '', 'selected_ids' => $_POST['category_ids'] ?? [] ], 'tags' => [ 'names' => $_POST['tags'] ?? '', 'selected_ids' => $_POST['tag_ids'] ?? [] ], 'audience_segment_ids' => $_POST['audience_segment_ids'] ?? $_POST['guest_segment_ids'] ?? [], 'featured_media_id' => (int)($_POST['featured_media_id'] ?? 0) ?: null, 'og_media_id' => (int)($_POST['og_media_id'] ?? 0) ?: null, ]; $userId = $security->getCurrentUser()['id']; if ($isEditMode && !empty($editPost['id'])) { // Update if ($post->update($editPost['id'], $data, $userId)) { redirect($formPath . '?success=saved'); } $formError = $post->getLastError() ?: 'Failed to update item.'; $editPost = array_merge($editPost ?? [], $data, ['id' => $editPost['id']]); } else { // Create $postId = $post->create($data, $userId); if ($postId) { $newPost = $post->getById($postId); redirect(buildPostRoutePath($newPost)); } $formError = $post->getLastError() ?: 'Failed to create item.'; $editPost = array_merge($editPost ?? [], $data); } } $attachedMedia = []; if (!empty($editPost['id'])) { $attachedMedia = $media->getByPost((int)$editPost['id']); } $libraryMedia = $media->getAll([], 500, 0); echo $template->render('admin/post-edit', [ 'page_title' => $isEditMode ? 'Edit Post' : 'New Post', 'layout_mode' => 'admin', 'success' => $_GET['success'] ?? null, 'error' => $formError, 'post' => $editPost, 'template_options' => getSelectableFrontendTemplates((string)($editPost['template_key'] ?? '')), 'pages' => $post->getAvailableParents($editPost['id'] ?? null), 'category_terms' => $post->getTermsByTaxonomy('category'), 'tag_terms' => $post->getTermsByTaxonomy('tag'), 'audience_segments' => $post->getAudienceSegments(), 'media' => $media, 'attached_media' => $attachedMedia, 'library_media' => $libraryMedia, 'csrf_token' => $security->generateCSRFToken() ]); } elseif ($path === 'contact') { $contactFlash = $_SESSION['contact_form_flash'] ?? null; unset($_SESSION['contact_form_flash']); $contactFormState = [ 'status' => $contactFlash['status'] ?? null, 'message' => $contactFlash['message'] ?? null, 'values' => $contactFlash['values'] ?? ['name' => '', 'email' => '', 'company' => '', 'message' => '', 'source_topic' => '', 'source_url' => ''], ]; if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['contact_form'])) { $name = trim((string)($_POST['name'] ?? '')); $email = trim((string)($_POST['email'] ?? '')); $company = trim((string)($_POST['company'] ?? '')); $message = trim((string)($_POST['message'] ?? '')); $website = trim((string)($_POST['website'] ?? '')); $sourceTopic = trim((string)($_POST['source_topic'] ?? '')); $sourceUrl = trim((string)($_POST['source_url'] ?? '')); if ($sourceUrl === '') { $sourceUrl = trim((string)($_SERVER['HTTP_REFERER'] ?? '')); } $flashValues = ['name' => $name, 'email' => $email, 'company' => $company, 'message' => $message, 'source_topic' => $sourceTopic, 'source_url' => $sourceUrl]; if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { $_SESSION['contact_form_flash'] = ['status' => 'error', 'message' => 'Invalid security token. Please refresh the page and try again.', 'values' => $flashValues]; redirect('/contact'); } if ($website !== '') { $_SESSION['contact_form_flash'] = ['status' => 'success', 'message' => 'Thank you. Your message has been sent.', 'values' => ['name' => '', 'email' => '', 'company' => '', 'message' => '', 'source_topic' => '', 'source_url' => '']]; redirect('/contact'); } if ($name === '' || !$security->validateEmail($email) || $message === '') { $_SESSION['contact_form_flash'] = ['status' => 'error', 'message' => 'Please fill in name, valid email, and message.', 'values' => $flashValues]; redirect('/contact'); } $recipientEmail = trim((string)($config['install']['admin_email'] ?? '')); $siteTitle = trim((string)($config['site']['title'] ?? 'NestCMS')); if (!filter_var($recipientEmail, FILTER_VALIDATE_EMAIL)) { $_SESSION['contact_form_flash'] = ['status' => 'error', 'message' => 'Contact form recipient email is not configured.', 'values' => $flashValues]; redirect('/contact'); } $subjectTopicSafe = preg_replace("/[\r\n]+/", ' ', $sourceTopic); $subject = $subjectTopicSafe !== '' ? 'New inquiry: ' . $subjectTopicSafe . ' — ' . $siteTitle : 'New inquiry — ' . $siteTitle; $headers = implode("\r\n", [ 'From: ' . $siteTitle . ' <' . $recipientEmail . '>', 'Reply-To: ' . $email, 'Content-Type: text/plain; charset=UTF-8', ]); $body = "Name: {$name}\nEmail: {$email}\n"; if ($company !== '') { $body .= "Company: {$company}\n"; } if ($sourceTopic !== '') { $body .= "Topic: {$sourceTopic}\n"; } if ($sourceUrl !== '') { $body .= "Source: {$sourceUrl}\n"; } $body .= "\nMessage:\n{$message}\n"; $visitorMeta = $_SESSION['visitor_meta'] ?? []; $visitorJourney = $_SESSION['visitor_journey'] ?? []; $remoteAddr = trim((string)($_SERVER['REMOTE_ADDR'] ?? '')); $forwardedFor = trim((string)($_SERVER['HTTP_X_FORWARDED_FOR'] ?? '')); $userAgent = trim((string)($_SERVER['HTTP_USER_AGENT'] ?? '')); $acceptLanguage = trim((string)($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '')); $dnt = trim((string)($_SERVER['HTTP_DNT'] ?? '')); $cookies = array_keys($_COOKIE ?? []); $body .= "\n---\nVisitor Context\n"; if ($remoteAddr !== '') { $body .= "IP: {$remoteAddr}\n"; } if ($forwardedFor !== '') { $body .= "X-Forwarded-For: {$forwardedFor}\n"; } if ($userAgent !== '') { $body .= "User-Agent: {$userAgent}\n"; } if ($acceptLanguage !== '') { $body .= "Accept-Language: {$acceptLanguage}\n"; } if ($dnt !== '') { $body .= "DNT: {$dnt}\n"; } $body .= "Session: " . session_id() . "\n"; if (is_array($visitorMeta) && !empty($visitorMeta)) { foreach (['first_seen_at' => 'First Seen', 'first_seen_url' => 'First URL', 'first_referrer' => 'First Referrer', 'last_seen_at' => 'Last Seen', 'last_seen_url' => 'Last URL'] as $key => $label) { $value = trim((string)($visitorMeta[$key] ?? '')); if ($value !== '') { $body .= $label . ": " . $value . "\n"; } } } if (!empty($cookies)) { $body .= "Cookies: " . implode(', ', $cookies) . "\n"; } if (is_array($visitorJourney) && !empty($visitorJourney)) { $body .= "\nJourney:\n"; $count = 0; foreach ($visitorJourney as $item) { if ($count >= 25) { break; } $t = trim((string)($item['t'] ?? '')); $u = trim((string)($item['u'] ?? '')); if ($t !== '' && $u !== '') { $body .= "- {$t} {$u}\n"; $count++; } } } $sent = @mail($recipientEmail, $subject, $body, $headers); $_SESSION['contact_form_flash'] = $sent ? ['status' => 'success', 'message' => 'Thank you. Your message has been sent.', 'values' => ['name' => '', 'email' => '', 'company' => '', 'message' => '', 'source_topic' => '', 'source_url' => '']] : ['status' => 'error', 'message' => 'Unable to send the message right now. Please try again later.', 'values' => $flashValues]; redirect('/contact'); } $contactPage = null; $contactPageId = (int)$db->getSetting('system_page_contact_id', '0'); if ($contactPageId > 0) { $assignedContactPage = $post->getById($contactPageId); $assignedContactPath = trim((string)($assignedContactPage['full_path'] ?? '')); if ($assignedContactPage && ($assignedContactPage['post_type'] ?? '') === 'page' && ($assignedContactPage['status'] ?? '') === 'published' && $assignedContactPath !== '') { $contactPage = $post->getByPath($assignedContactPath, $visibleAccessLevels, $audienceSegmentFilter); } } if (!$contactPage) { $contactPage = $post->getByPath('contact', $visibleAccessLevels, $audienceSegmentFilter); } if (!$renderPageResponse($contactPage, ['template_name' => 'contact', 'template_vars' => ['form_status' => $contactFormState['status'], 'form_message' => $contactFormState['message'], 'form_values' => $contactFormState['values']]])) { $renderNotFoundResponse(); } } elseif ($path === 'blog') { $blogPage = null; $blogPageId = (int)$db->getSetting('system_page_blog_index_id', '0'); if ($blogPageId > 0) { $assignedBlogPage = $post->getById($blogPageId); $assignedBlogPath = trim((string)($assignedBlogPage['full_path'] ?? '')); if ($assignedBlogPage && ($assignedBlogPage['post_type'] ?? '') === 'page' && ($assignedBlogPage['status'] ?? '') === 'published' && $assignedBlogPath !== '') { $blogPage = $post->getByPath($assignedBlogPath, $visibleAccessLevels, $audienceSegmentFilter); } } if (!$blogPage) { $blogPage = $post->getByPath('blog', $visibleAccessLevels, $audienceSegmentFilter); } $blogPosts = $post->getAll($visibleAccessLevels, 'post', 20, 0, $audienceSegmentFilter); if ($blogPage && !canAccessTokenProtected($blogPage, $requestAccessToken, $security->isLoggedIn())) { $renderNotFoundResponse(); } else { echo $template->render('blog-index', [ 'blog_page' => $blogPage, 'posts' => $blogPosts, 'meta_source' => $blogPage ?: [ 'meta_title' => 'Blog', 'meta_description' => 'Latest posts', ], 'page_title' => $blogPage['title'] ?? 'Blog' ]); } } elseif (preg_match('#^blog/tag/([^/]+)$#', $path, $matches)) { $termSlug = $matches[1]; $term = $post->getTermByTaxonomyAndSlug('tag', $termSlug); if (!$term) { $renderNotFoundResponse(); } else { $archivePosts = $post->getPostsByTermSlug('tag', $termSlug, $visibleAccessLevels, 50, 0, $audienceSegmentFilter, 'post'); echo $template->render('blog-archive', [ 'archive' => [ 'name' => $term['name'], 'slug' => $term['slug'], 'description' => $term['description'] ?? '', 'taxonomy' => 'tag', 'full_path' => 'blog/tag/' . $term['slug'], ], 'meta_source' => [ 'meta_title' => $term['name'], 'meta_description' => $term['description'] ?? null, ], 'posts' => $archivePosts, 'page_title' => 'Tag: ' . $term['name'] ]); } } elseif (preg_match('#^blog/([^/]+)$#', $path, $matches)) { $slug = $matches[1]; $term = $post->getTermByTaxonomyAndSlug('category', $slug); if ($term) { $archivePosts = $post->getPostsByTermSlug('category', $slug, $visibleAccessLevels, 50, 0, $audienceSegmentFilter, 'post'); echo $template->render('blog-archive', [ 'archive' => [ 'name' => $term['name'], 'slug' => $term['slug'], 'description' => $term['description'] ?? '', 'taxonomy' => 'category', 'full_path' => 'blog/' . $term['slug'], ], 'meta_source' => [ 'meta_title' => $term['name'], 'meta_description' => $term['description'] ?? null, ], 'posts' => $archivePosts, 'page_title' => $term['name'] ]); } else { $postData = $post->getBySlugAndType($slug, 'post', $visibleAccessLevels, $audienceSegmentFilter); if (!$postData) { $renderNotFoundResponse(); } elseif (!canAccessTokenProtected($postData, $requestAccessToken, $security->isLoggedIn())) { $renderNotFoundResponse(); } else { echo $template->render('blog-post', [ 'post' => $postData, 'media' => $media, 'post_media' => !empty($postData['id']) ? $media->getByPost((int)$postData['id']) : [], 'page_title' => $postData['title'], 'csrf_token' => $security->generateCSRFToken() ]); } } } elseif (preg_match('#^category/([^/]+)$#', $path, $matches)) { redirect('/blog/' . rawurlencode($matches[1]), 301); } elseif (preg_match('#^tag/([^/]+)$#', $path, $matches)) { redirect('/blog/tag/' . rawurlencode($matches[1]), 301); } elseif (preg_match('/^post\/(.+)$/', $path, $matches)) { redirect('/blog/' . rawurlencode($matches[1]), 301); } elseif (preg_match('/^(?:delete|admin\/delete)\/(.+)$/', $path, $matches)) { // Delete post if (!$security->isLoggedIn()) { redirect('/login'); } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo $template->render('404', [ 'page_title' => 'Method Not Allowed' ]); exit; } if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) { redirect('/?error=invalid_token'); } $slug = $matches[1]; $postData = $post->getBySlug($slug, 2); if ($postData) { $userId = $security->getCurrentUser()['id']; $post->delete($postData['id'], $userId); } redirect('/'); } elseif (!empty($path)) { // Universal hierarchy route: /parent/child/grandchild $pageData = $post->getByPath($path, $visibleAccessLevels, $audienceSegmentFilter); if (!$pageData) { $renderNotFoundResponse(); } elseif (!canAccessTokenProtected($pageData, $requestAccessToken, $security->isLoggedIn())) { $renderNotFoundResponse(); } else { $children = $post->getChildrenByParent($pageData['id'], $visibleAccessLevels, 100, 0, $audienceSegmentFilter); $hasChildren = !empty($children); $templateName = resolveContentTemplateName($pageData, $hasChildren); echo $template->render($templateName, [ 'entry' => $pageData, 'page' => $pageData, 'post' => $pageData, 'section' => [ 'name' => $pageData['title'], 'slug' => $pageData['slug'], 'description' => '', 'content' => $pageData['content'], 'full_path' => $pageData['full_path'] ?? '', ], 'meta_source' => $pageData, 'posts' => $children, 'children' => $children, 'has_children' => $hasChildren, 'media' => $media, 'post_media' => !empty($pageData['id']) ? $media->getByPost((int)$pageData['id']) : [], 'page_title' => $pageData['title'], 'csrf_token' => $security->generateCSRFToken() ]); } } else { // 404 $renderNotFoundResponse(); }