- Bug: dashboard toonde 0 content (AdminPluginAPI::getContentDir() gaf relatief pad terug zonder normalisatie) - Dynamische pad-resolutie: PluginAPIInterface uitgebreid met getProjectRoot/getContentDir/getPluginsDir/getVersionInfo; CMSAPI en AdminPluginAPI implementeren deze universeel - public/index.php media-serving gebruikt $config['content_dir'] i.p.v. hardcoded /content - Navigation en Logs plugins halen paden via de API i.p.v. hardcoded dirname(__DIR__) - WordPress-stijl docblocks toegevoegd voor alle classes, methods, properties en functies (~450 docblocks, @since 2.6.5) - Security: hardcoded plaintext-wachtwoord 'admin' verwijderd uit AdminAuth.php; bij eerste installatie wordt een cryptografisch veilig wachtwoord gegenereerd (random_bytes, 16 tekens) en eenmalig op het inlogscherm getoond - Security: git-geschiedenis schoongemaakt (admin.json, admin.json.example, admin-console/config/admin.json verwijderd uit alle commits; filter-branch over alle branches + tags, gc --prune --aggressive) - README.md, README.en.md, AGENTS.md bijgewerkt - Test-scripts bijgewerkt naar clean-URL structuur + actuele ARIA-waarden - Versie verhoogd naar 2.6.5 - Tests: pentest 29/29, WCAG 25/25, functioneel 16/16, enhanced 25/25
314 lines
10 KiB
PHP
314 lines
10 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Front-end entry point van CodePress CMS.
|
|
*
|
|
* Bootstrap de CMS-core, laadt de configuratie, stuurt security headers uit,
|
|
* serveert media-/asset-/robots-requests, voert BotGuard- en rate-limit-
|
|
* controles uit, registreert analytics- en logvermeldingen en render
|
|
* uiteindelijk de pagina via CodePressCMS::render().
|
|
*
|
|
* @since 2.6.5
|
|
* @package CodePress
|
|
*/
|
|
require_once __DIR__ . '/../cms/core/index.php';
|
|
|
|
$config = include __DIR__ . '/../cms/core/config.php';
|
|
|
|
/**
|
|
* Log-systeem initialiseren op basis van de logging-configuratie.
|
|
*
|
|
* @since 2.6.5
|
|
*/
|
|
// Initialize dynamic logging
|
|
LogManager::init($config['logging'] ?? []);
|
|
|
|
/**
|
|
* Security headers instellen voor alle front-end responses.
|
|
*
|
|
* @since 2.6.5
|
|
*/
|
|
// Security headers
|
|
header('X-Content-Type-Options: nosniff');
|
|
header('X-Frame-Options: SAMEORIGIN');
|
|
header('X-XSS-Protection: 1; mode=block');
|
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
header('Content-Security-Policy: default-src \'self\'; script-src \'self\' \'unsafe-inline\'; style-src \'self\' \'unsafe-inline\'; img-src \'self\' data:; font-src \'self\';');
|
|
header_remove('X-Powered-By');
|
|
|
|
/**
|
|
* Media- en asset-serving via /-media/ en /-assets/ routes.
|
|
*
|
|
* Bepaalt het verzoekpad en de toegestane MIME-types, resolveert de
|
|
* content-map dynamisch op basis van config['content_dir'] en serveert
|
|
* bestanden buiten public/ met een pad-controle om traversal te blokkeren.
|
|
*
|
|
* @since 2.6.5
|
|
*/
|
|
// Serve media files from any content/ subdirectory via /-media/
|
|
// Serve files from content/-assets/ via /-assets/ (backward compatible)
|
|
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
|
$parsedUrl = parse_url($requestUri);
|
|
$path = $parsedUrl['path'] ?? '';
|
|
|
|
$allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'pdf', 'zip', 'mp4', 'webm', 'ogg', 'mp3', 'wav'];
|
|
$mimeTypes = [
|
|
'jpg' => '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 '<h1>404 - Not Found</h1>';
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* /-assets/ route: serveert bestanden uit content/-assets/ of de content-
|
|
* root, met backward-compatibiliteit en path-traversal-controle.
|
|
*
|
|
* @since 2.6.5
|
|
*/
|
|
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 '<h1>404 - Not Found</h1>';
|
|
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.5
|
|
*/
|
|
// Block direct access to content files
|
|
if (strpos($path, '/content/') === 0) {
|
|
http_response_code(403);
|
|
echo '<h1>403 - Forbidden</h1><p>Access denied.</p>';
|
|
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.5
|
|
*/
|
|
// 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.5
|
|
*/
|
|
// 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.5
|
|
*/
|
|
// 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.5
|
|
*/
|
|
// 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.5
|
|
*/
|
|
// 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.5
|
|
*/
|
|
// 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 '<!DOCTYPE html><html lang="nl"><head><meta charset="UTF-8"><title>429 Te Veel Verzoeken</title><meta name="robots" content="noindex,nofollow"></head><body><h1>429 Te Veel Verzoeken</h1><p>Probeer het over een minuut opnieuw.</p></body></html>';
|
|
} else {
|
|
http_response_code(403);
|
|
echo '<!DOCTYPE html><html lang="nl"><head><meta charset="UTF-8"><title>403 Toegang Geweigerd</title><meta name="robots" content="noindex,nofollow"></head><body><h1>403 Toegang Geweigerd</h1><p>Toegang geweigerd.</p></body></html>';
|
|
}
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Pagina renderen via de CodePressCMS-instantie.
|
|
*
|
|
* @since 2.6.5
|
|
*/
|
|
$cms->render(); |