'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp', 'svg' => 'image/svg+xml', 'pdf' => 'application/pdf', 'zip' => 'application/zip', 'mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/wav', 'css' => 'text/css', 'js' => 'application/javascript', ]; // Resolve the content directory once (absolute, from config) so media // serving follows a customised content_dir instead of a hardcoded path. $contentBase = realpath($config['content_dir']); $contentBase = $contentBase !== false ? $contentBase : rtrim($config['content_dir'], '/'); if (strpos($path, '/-media/') === 0) { $relative = ltrim(substr($path, 7), '/'); $filePath = $contentBase . '/' . $relative; if ($contentBase !== '' && strpos($filePath, $contentBase) === 0) { $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); if (in_array($ext, $allowedExt) && file_exists($filePath) && !is_dir($filePath)) { if (isset($mimeTypes[$ext])) { header('Content-Type: ' . $mimeTypes[$ext]); } readfile($filePath); exit; } } http_response_code(404); echo '

404 - Not Found

'; exit; } /** * /-assets/ route: serveert bestanden uit content/-assets/ of de content- * root, met backward-compatibiliteit en path-traversal-controle. * * @since 2.6.4 */ if (strpos($path, '/-assets/') === 0) { $relative = ltrim(substr($path, 8), '/'); $servePath = null; $candidates = [ $contentBase . '/-assets/' . $relative, $contentBase . '/' . $relative, ]; foreach ($candidates as $candidate) { if ($contentBase === '' || strpos($candidate, $contentBase) !== 0) continue; if (file_exists($candidate) && !is_dir($candidate)) { $ext = strtolower(pathinfo($candidate, PATHINFO_EXTENSION)); if (in_array($ext, $allowedExt)) { $servePath = $candidate; break; } } } if ($servePath !== null) { $ext = strtolower(pathinfo($servePath, PATHINFO_EXTENSION)); if (isset($mimeTypes[$ext])) { header('Content-Type: ' . $mimeTypes[$ext]); } readfile($servePath); exit; } http_response_code(404); echo '

404 - Not Found

'; exit; } // Serve dynamic robots.txt if ($path === '/robots.txt') { header('Content-Type: text/plain; charset=utf-8'); echo BotGuard::generateRobotsTxt($config['security'] ?? []); exit; } /** * Directe toegang tot /content/ blokkeren. * * Voorkomt dat ruwe content-bestanden via het web opgevraagd worden. * * @since 2.6.4 */ // Block direct access to content files if (strpos($path, '/content/') === 0) { http_response_code(403); echo '

403 - Forbidden

Access denied.

'; exit; } /** * Security-checks en rate limiting uitvoeren. * * Bepaalt client-IP, BotGuard-controle, IP-blocklist en rate-limiter op * basis van de security-config. IP's op de allowed/uitgesloten lijsten * slaan de controles over. Het requestStatus wordt bijgehouden voor * latere logging en blocking. * * @since 2.6.4 */ // Load RequestLogger for IP & request logging require_once __DIR__ . '/../cms/core/class/RequestLogger.php'; // Execute security checks & rate limiting $clientIp = RequestLogger::getClientIp(); $secSettings = $config['security'] ?? [ 'block_ai_bots' => true, 'block_scrapers' => true, 'block_search_engines' => false, 'block_empty_user_agent' => true, 'rate_limit_enabled' => true, 'rate_limit_max' => 60, 'rate_limit_window' => 60, ]; $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; $analyticsSettings = $config['analytics'] ?? []; $excludedIps = $analyticsSettings['excluded_ips'] ?? []; $isAllowedIp = RequestLogger::ipMatchesList($clientIp, $secSettings['allowed_ips'] ?? []) || RequestLogger::ipMatchesList($clientIp, $excludedIps); $requestStatus = 'ok'; if (!$isAllowedIp) { // 1. IP Blocklist if (in_array($clientIp, $secSettings['blocked_ips'] ?? [], true)) { $requestStatus = 'blocked:ip'; } // 2. BotGuard User-Agent check if ($requestStatus === 'ok') { $botBlockReason = BotGuard::shouldBlock($userAgent, $secSettings); if ($botBlockReason !== null) { $requestStatus = $botBlockReason; } } // 3. Rate Limiting per IP if ($requestStatus === 'ok' && !empty($secSettings['rate_limit_enabled'])) { $cacheDir = dirname(__DIR__) . '/admin/storage/cache'; $rateLimiter = new RateLimiter( (int)($secSettings['rate_limit_max'] ?? 60), (int)($secSettings['rate_limit_window'] ?? 60), new FileCache($cacheDir) ); if (!$rateLimiter->isAllowed($clientIp)) { $requestStatus = 'blocked:ratelimit'; } } } /** * CMS-instantie aanmaken met de geladen configuratie. * * @since 2.6.4 */ // Instantiate CMS instance $cms = new CodePressCMS($config); /** * Analytics en GeoIP-lookup uitvoeren. * * Land, IP-anonimisatie en analytics-instellingen worden bepaald op basis * van de analytics-configuratie. * * @since 2.6.4 */ // Analytics & GeoIP settings $analyticsSettings = $config['analytics'] ?? []; $geoCountry = null; if (!empty($analyticsSettings['enabled'])) { $geoIp = new GeoIP($analyticsSettings); $geoCountry = $geoIp->lookupCountry($clientIp); } /** * IP-anonimisatie toepassen voor opslag indien ingeschakeld. * * @since 2.6.4 */ // Apply IP anonymization for storage if enabled $storedIp = !empty($analyticsSettings['anonymize_ip']) ? RequestLogger::anonymizeIp($clientIp) : $clientIp; /** * Pagina-view loggen (niet voor media-/assets-requests). * * Start sessie indien nodig, stelt huidige pagina/referrer vast, schrijft * een request-logregel, vuurt een LogManager-requestevent en registreert * geaggregeerde analytics (mits ingeschakeld en IP niet uitgesloten). * * @since 2.6.4 */ // Log page view (not for media/assets) if (!str_starts_with($path, '/-media/') && !str_starts_with($path, '/-assets/')) { if (session_status() === PHP_SESSION_NONE) { @session_start(); } $loggedInUser = $_SESSION['admin_user'] ?? 'Gast'; $currentPage = $_GET['page'] ?? $cms->getEffectiveDefaultPage(); $referrer = $_SERVER['HTTP_REFERER'] ?? ''; $requestLogFile = dirname(__DIR__) . '/admin/storage/logs/requests.log'; $logger = new RequestLogger($requestLogFile); $logger->log( $currentPage, $storedIp, $userAgent, $referrer, $loggedInUser, $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '', $requestStatus, $geoCountry ); // Also record through the dynamic log manager (requests event) LogManager::log(LogManager::EVENT_REQUESTS, 'info', 'Request: ' . $currentPage, [ 'ip' => $storedIp, 'page' => $currentPage, 'status' => $requestStatus, 'country' => $geoCountry, 'user' => $loggedInUser, ]); // Record aggregated statistics if (!empty($analyticsSettings['enabled']) && !in_array($clientIp, $excludedIps, true)) { $analytics = new Analytics($analyticsSettings); $analytics->record($currentPage, $storedIp, $userAgent, $referrer, $geoCountry, $requestStatus); } } /** * Geblokkeerde verzoeken afhandelen. * * Geeft 429 bij rate-limit en 403 bij andere blokkades, met noindex-headers. * * @since 2.6.4 */ // Block request if status is not ok if ($requestStatus !== 'ok') { if ($requestStatus === 'blocked:ratelimit') { http_response_code(429); header('Retry-After: ' . (int)($secSettings['rate_limit_window'] ?? 60)); echo '429 Te Veel Verzoeken

429 Te Veel Verzoeken

Probeer het over een minuut opnieuw.

'; } else { http_response_code(403); echo '403 Toegang Geweigerd

403 Toegang Geweigerd

Toegang geweigerd.

'; } exit; } /** * Pagina renderen via de CodePressCMS-instantie. * * @since 2.6.4 */ $cms->render();