Map van rol-sleutel naar lijst met toegestane routes. */ public const ROLE_PERMISSIONS = [ 'admin' => ['*'], 'content-manager' => ['dashboard', 'content', 'content-list', 'content-files', 'content-edit', 'content-new', 'content-delete', 'content-file-upload', 'content-file-delete', 'content-file-move', 'content-dir-create', 'content-dir-create-in', 'content-dir-rename', 'content-dir-rename-in', 'content-dir-delete', 'content-dir-delete-in', 'content-move', 'content-backup', 'content-restore', 'content-git-init', 'content-git-commit', 'content-git-restore', 'guide', 'logout'], 'bi-manager' => ['dashboard', 'statistics', 'logs', 'guide', 'logout'], 'site-admin' => ['dashboard', 'theme', 'theme-new', 'theme-edit', 'theme-file-upload', 'theme-file-delete', 'theme-file-move', 'theme-activate', 'theme-delete', 'theme-scss', 'plugins', 'plugins-new', 'plugins-edit', 'plugins-config', 'plugins-toggle', 'plugins-delete', 'statistics', 'logs', 'update', 'guide', 'logout'], ]; /** * Leesbare rol-labels voor weergave in de admin-interface. * * @since 2.6.4 * @var array Map van rol-sleutel naar label. */ public const ROLE_LABELS = [ 'admin' => 'Admin', 'content-manager' => 'Content Beheerder', 'bi-manager' => 'BI Beheerder', 'site-admin' => 'Site Admin', ]; /** * Constructor: initialiseer authenticatie met de applicatie-configuratie. * * Laadt de admin-configuratie, bepaalt het pad naar het lockout-bestand en * start de sessie met de juiste security-instellingen. * * @since 2.6.4 * * @param array $appConfig Applicatie-configuratie; vereist keys 'log_file' en 'admin_config'. */ public function __construct(array $appConfig) { $this->config = $appConfig; $this->adminConfig = $this->loadAdminConfig(); $this->lockFile = dirname($appConfig['log_file']) . '/login_attempts.json'; $this->startSession(); } /** * Laadt de admin-configuratie uit admin.json. * * Als het bestand nog niet bestaat wordt het aangemaakt vanuit * admin.json.example, of indien die ook ontbreekt met een standaard admin- * gebruiker. Geeft een lege structuur terug als de JSON ongeldig is. * * @since 2.6.4 * * @return array Admin-configuratie met keys 'users' en 'security'. */ private function loadAdminConfig(): array { $path = $this->config['admin_config']; $examplePath = dirname($path) . '/admin.json.example'; if (!file_exists($path)) { if (file_exists($examplePath)) { @copy($examplePath, $path); } else { // Genereer een cryptografisch veilig wachtwoord bij eerste // installatie. Het wachtwoord wordt nooit in de broncode // opgeslagen; de hash wordt weggeschreven naar admin.json // en het plaintext wachtwoord éénmalig getoond op het // inlogscherm via getGeneratedPassword(). $plainPassword = $this->generateRandomPassword(); $this->generatedPassword = $plainPassword; $defaultAdminConfig = [ 'users' => [ [ 'username' => 'admin', 'password_hash' => password_hash($plainPassword, PASSWORD_BCRYPT), 'role' => 'admin', 'created' => date('Y-m-d'), ] ], 'security' => [ 'session_timeout' => 1800, 'max_login_attempts' => 5, 'lockout_duration' => 900, ] ]; $dir = dirname($path); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } @file_put_contents($path, json_encode($defaultAdminConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } } $data = file_exists($path) ? json_decode(file_get_contents($path), true) : null; return is_array($data) ? $data : ['users' => [], 'security' => []]; } /** * Genereer een cryptografisch veilig willekeurig wachtwoord. * * Gebruik random_bytes() (CSPRNG) met een mix van alfanumerieke tekens * en een vaste lengte, zodat het wachtwoord voldoende entropie heeft * en veilig te typen is. * * @since 2.6.4 * * @return string Willekeurig wachtwoord van 16 alfanumerieke tekens. */ private function generateRandomPassword(int $length = 16): string { $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $max = strlen($alphabet) - 1; $password = ''; for ($i = 0; $i < $length; $i++) { $password .= $alphabet[random_int(0, $max)]; } return $password; } /** * Geef het bij eerste installatie gegenereerde wachtwoord terug. * * De login-pagina gebruikt dit om het wachtwoord eenmalig te tonen * zodra admin.json is aangemaakt. Na de eerste succesvolle login * wordt het veld geleegd. Geeft een lege string terug wanneer er * geen nieuw wachtwoord is gegenereerd in deze request. * * @since 2.6.4 * * @return string Plaintext wachtwoord of lege string. */ public function getGeneratedPassword(): string { return $this->generatedPassword; } /** * Slaat de huidige admin-configuratie op naar admin.json. * * Schrijft de interne admin-configuratie weg als pretty-printed JSON met * behoud van unicode-tekens. * * @since 2.6.4 * * @return void */ public function saveAdminConfig(): void { file_put_contents( $this->config['admin_config'], json_encode($this->adminConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) ); } /** * Start de sessie met veilige cookie-parameters en handhaaft de session timeout. * * Stelt cookie-flags in (HttpOnly, SameSite=Strict, Secure bij HTTPS), start * de sessie en beëindigt deze bij inactiviteit langer dan de geconfigureerde * timeout. Vernieuwt de activiteit-timestamp bij geauthenticeerde gebruikers. * * @since 2.6.4 * * @return void */ private function startSession(): void { if (session_status() === PHP_SESSION_NONE) { $timeout = $this->adminConfig['security']['session_timeout'] ?? 1800; $isHttps = !empty($_SERVER['HTTPS']) || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') || (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on'); session_set_cookie_params([ 'lifetime' => $timeout, 'path' => '/', 'secure' => $isHttps, 'httponly' => true, 'samesite' => 'Strict' ]); session_start(); } // Check session timeout if (isset($_SESSION['admin_last_activity'])) { $timeout = $this->adminConfig['security']['session_timeout'] ?? 1800; if (time() - $_SESSION['admin_last_activity'] > $timeout) { $this->logout(); return; } } if ($this->isAuthenticated()) { $_SESSION['admin_last_activity'] = time(); } } /** * Verifieert inloggegevens en start een admin-sessie bij succes. * * Controleert eerst of het account niet is vergrendeld door brute-force * protection. Bij een fout wachtwoord wordt een mislukte poging geregistreerd * en gelogd. Bij succes worden mislukte pogingen gewist en de sessie * gevuld met gebruiker, rol en CSRF-token. * * @since 2.6.4 * * @param string $username Gebruikersnaam. * @param string $password Wachtwoord (plaintext). * @return array Resultaat met keys 'success' (bool) en 'message' (string). */ public function login(string $username, string $password): array { // Check brute-force lockout $lockout = $this->checkLockout($username); if ($lockout['locked']) { return [ 'success' => false, 'message' => 'Account tijdelijk vergrendeld. Probeer over ' . $lockout['remaining'] . ' seconden opnieuw.' ]; } // Find user $user = $this->findUser($username); if (!$user || !password_verify($password, $user['password_hash'])) { $this->recordFailedAttempt($username); $this->log('warning', "Mislukte inlogpoging: {$username}"); return ['success' => false, 'message' => 'Onjuiste gebruikersnaam of wachtwoord.']; } // Success - clear failed attempts $this->clearFailedAttempts($username); // Set session $_SESSION['admin_user'] = $username; $_SESSION['admin_role'] = $user['role'] ?? 'admin'; $_SESSION['admin_last_activity'] = time(); $_SESSION['admin_csrf_token'] = bin2hex(random_bytes(32)); $this->log('info', "Ingelogd: {$username}"); return ['success' => true, 'message' => 'Ingelogd.']; } /** * Logt de huidige gebruiker uit en vernietigt de sessie. * * Maakt de sessie-array leeg, verwijdert het sessie-cookie en vernietigt de * sessie. De uitlogactie wordt gelogd. * * @since 2.6.4 * * @return void */ public function logout(): void { $user = $_SESSION['admin_user'] ?? 'unknown'; $_SESSION = []; if (ini_get('session.use_cookies')) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly'] ); } session_destroy(); $this->log('info', "Uitgelogd: {$user}"); } /** * Controleert of de huidige bezoeker is ingelogd. * * @since 2.6.4 * * @return bool True indien een admin-gebruiker in de sessie staat. */ public function isAuthenticated(): bool { return isset($_SESSION['admin_user']); } /** * Geeft de gegevens van de ingelogde gebruiker, verrijkt met profielvelden. * * Levert username en rol uit de sessie en vult deze aan met email, * author_name en author_email uit admin.json indien aanwezig. * * @since 2.6.4 * * @return array|null Gebruikersdata als array, of null indien niet ingelogd. */ public function getCurrentUser(): ?array { if (!$this->isAuthenticated()) { return null; } $username = $_SESSION['admin_user']; $userData = [ 'username' => $username, 'role' => $_SESSION['admin_role'] ?? 'admin' ]; // Enrich with profile fields from admin.json $userEntry = $this->findUser($username); if ($userEntry) { $userData['email'] = $userEntry['email'] ?? ''; $userData['author_name'] = $userEntry['author_name'] ?? ''; $userData['author_email'] = $userEntry['author_email'] ?? ''; } return $userData; } /** * Geeft de huidige rol van de ingelogde gebruiker. * * Retourneert de override-rol indien ingesteld (voor test-doeleinden), anders * de werkelijke rol. * * @since 2.6.4 * * @return string Rol-sleutel. */ public function getCurrentRole(): string { if (isset($_SESSION['admin_role_override'])) { return $_SESSION['admin_role_override']; } return $_SESSION['admin_role'] ?? 'admin'; } /** * Geeft de werkelijke rol van de ingelogde gebruiker, eventuele override negerend. * * @since 2.6.4 * * @return string Rol-sleutel. */ public function getRealRole(): string { return $_SESSION['admin_role'] ?? 'admin'; } /** * Controleert of de ingelogde gebruiker een actieve rol-override heeft. * * @since 2.6.4 * * @return bool True indien een override-rol is ingesteld. */ public function hasRoleOverride(): bool { return isset($_SESSION['admin_role_override']); } /** * Wisselt tijdelijk naar een andere rol voor test-doeleinden. * * Alleen werkelijke admins kunnen wisselen; admins kunnen niet wisselen naar * 'admin' (zinloos) en enkel naar lagere rollen. Slaat de override op in de * sessie. De actie wordt gelogd. * * @since 2.6.4 * * @param string $role Doel-rol-sleutel. * @return array Resultaat met keys 'success' (bool) en 'message' (string). */ public function switchRole(string $role): array { if (!$this->isAuthenticated()) { return ['success' => false, 'message' => 'Niet ingelogd.']; } // Only real admins can switch roles $realRole = $this->getRealRole(); if ($realRole !== 'admin') { return ['success' => false, 'message' => 'Alleen admins kunnen van rol wisselen.']; } if (!isset(self::ROLE_PERMISSIONS[$role])) { return ['success' => false, 'message' => 'Ongeldige rol.']; } // Admins cannot switch to 'admin' (no point) — only to lower roles for testing if ($role === 'admin') { return ['success' => false, 'message' => 'Je bent al admin.']; } $_SESSION['admin_role_override'] = $role; $this->log('info', "Rol-switch: {$_SESSION['admin_user']} -> {$role} (test)"); return ['success' => true, 'message' => 'Rol gewijzigd naar ' . self::getRoleLabel($role) . '.']; } /** * Reset de rol-override terug naar de werkelijke admin-rol. * * Verwijdert de override uit de sessie indien aanwezig en logt de actie. * * @since 2.6.4 * * @return array Resultaat met keys 'success' (bool) en 'message' (string). */ public function resetRole(): array { if (isset($_SESSION['admin_role_override'])) { unset($_SESSION['admin_role_override']); $this->log('info', "Rol-switch gereset: {$_SESSION['admin_user']}"); return ['success' => true, 'message' => 'Rol teruggezet naar admin.']; } return ['success' => false, 'message' => 'Geen rol-override actief.']; } /** * Controleert of de huidige gebruiker een bepaalde route mag openen. * * Gebruikt de permissies van de huidige rol; de wildcard '*' geeft toegang * tot alles. Onbekende rollen krijgen enkel 'dashboard' en 'logout'. * * @since 2.6.4 * * @param string $route Route-sleutel die gecontroleerd moet worden. * @return bool True indien de gebruiker de route mag benaderen. */ public function hasPermission(string $route): bool { $role = $this->getCurrentRole(); $permissions = self::ROLE_PERMISSIONS[$role] ?? ['dashboard', 'logout']; if (in_array('*', $permissions, true)) { return true; } return in_array($route, $permissions, true); } /** * Geeft alle beschikbare rollen met hun leesbare labels. * * @since 2.6.4 * * @return array Map van rol-sleutel naar label. */ public static function getRoles(): array { return self::ROLE_LABELS; } /** * Geeft het leesbare label voor een rol. * * @since 2.6.4 * * @param string $role Rol-sleutel. * @return string Leesbaar label, of de rol-sleutel zelf indien onbekend. */ public static function getRoleLabel(string $role): string { return self::ROLE_LABELS[$role] ?? $role; } /** * Geeft het huidige CSRF-token, of genereert een nieuw token indien afwezig. * * Slaat een cryptografisch willekeurig token op in de sessie en retourneert * dit voor gebruik in admin-formulieren. * * @since 2.6.4 * * @return string 64-tekens hexadecimaal CSRF-token. */ public function getCsrfToken(): string { if (!isset($_SESSION['admin_csrf_token'])) { $_SESSION['admin_csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['admin_csrf_token']; } /** * Verifieert een meegegeven CSRF-token tegen het token in de sessie. * * Gebruikt hash_equals om timing-attacks te voorkomen. * * @since 2.6.4 * * @param string $token Token uit het formulier of verzoek. * @return bool True indien het token overeenkomt met het sessie-token. */ public function verifyCsrf(string $token): bool { return isset($_SESSION['admin_csrf_token']) && hash_equals($_SESSION['admin_csrf_token'], $token); } /** * Regeneert het CSRF-token in de sessie. * * Maakt een nieuw cryptografisch willekeurig token aan, nuttig na * privilege-wijzigingen of na het verwerken van een formulier. * * @since 2.6.4 * * @return void */ public function regenerateCsrfToken(): void { $_SESSION['admin_csrf_token'] = bin2hex(random_bytes(32)); } // --- User Management --- /** * Geeft alle gebruikers met hun profiel- en rolinformatie. * * Levert een array geïndexeerd op gebruikersnaam, aangevuld met het * leesbare rol-label en eventuele profielvelden. * * @since 2.6.4 * * @return array Map van gebruikersnaam naar gebruikersdata. */ public function getUsers(): array { $users = []; foreach ($this->adminConfig['users'] ?? [] as $u) { $role = $u['role'] ?? 'admin'; $users[$u['username']] = [ 'username' => $u['username'], 'role' => $role, 'role_label' => self::getRoleLabel($role), 'created' => $u['created'] ?? '', 'email' => $u['email'] ?? '', 'author_name' => $u['author_name'] ?? '', 'author_email' => $u['author_email'] ?? '', ]; } return $users; } /** * Voegt een nieuwe gebruiker toe aan de admin-configuratie. * * Controleert op dubbele gebruikersnamen, minimum wachtwoordlengte (8) en * geldigheid van de rol. Het wachtwoord wordt opgeslagen als bcrypt-hash. * Wijzigingen worden direct weggeschreven en gelogd. * * @since 2.6.4 * * @param string $username Gebruikersnaam. * @param string $password Wachtwoord (plaintext, minimaal 8 tekens). * @param string $role Rol-sleutel. Default 'admin'. * @param string $email E-mailadres van de gebruiker. Default ''. * @param string $authorName Naam voor auteur-metadata. Default ''. * @param string $authorEmail E-mail voor auteur-metadata. Default ''. * @return array Resultaat met keys 'success' (bool) en 'message' (string). */ public function addUser(string $username, string $password, string $role = 'admin', string $email = '', string $authorName = '', string $authorEmail = ''): array { if ($this->findUser($username)) { return ['success' => false, 'message' => 'Gebruiker bestaat al.']; } if (strlen($password) < 8) { return ['success' => false, 'message' => 'Wachtwoord moet minimaal 8 tekens zijn.']; } if (!isset(self::ROLE_PERMISSIONS[$role])) { return ['success' => false, 'message' => 'Ongeldige rol.']; } $this->adminConfig['users'][] = [ 'username' => $username, 'password_hash' => password_hash($password, PASSWORD_DEFAULT), 'role' => $role, 'email' => $email, 'author_name' => $authorName, 'author_email' => $authorEmail, 'created' => date('Y-m-d') ]; $this->saveAdminConfig(); $this->log('info', "Gebruiker aangemaakt: {$username} (rol: {$role})"); return ['success' => true, 'message' => 'Gebruiker aangemaakt.']; } /** * Werkt de profielvelden (email, author_name, author_email) van een gebruiker bij. * * Zoekt de gebruiker op naam en overschrijft de profielvelden, slaat de * configuratie op en logt de wijziging. * * @since 2.6.4 * * @param string $username Gebruikersnaam. * @param string $email E-mailadres. Default ''. * @param string $authorName Naam voor auteur-metadata. Default ''. * @param string $authorEmail E-mail voor auteur-metadata. Default ''. * @return array Resultaat met keys 'success' (bool) en 'message' (string). */ public function updateUserProfile(string $username, string $email = '', string $authorName = '', string $authorEmail = ''): array { foreach ($this->adminConfig['users'] as &$userEntry) { if ($userEntry['username'] === $username) { $userEntry['email'] = $email; $userEntry['author_name'] = $authorName; $userEntry['author_email'] = $authorEmail; $this->saveAdminConfig(); $this->log('info', "Profiel bijgewerkt: {$username}"); return ['success' => true, 'message' => 'Profiel opgeslagen.']; } } return ['success' => false, 'message' => 'Gebruiker niet gevonden.']; } /** * Wijzigt de rol van een bestaande gebruiker. * * Een gebruiker kan nooit zijn eigen rol wijzigen (security guard). De rol * moet een bekende sleutel uit ROLE_PERMISSIONS zijn. Wijzigingen worden * opgeslagen en gelogd. * * @since 2.6.4 * * @param string $username Gebruikersnaam. * @param string $role Nieuwe rol-sleutel. * @return array Resultaat met keys 'success' (bool) en 'message' (string). */ public function changeRole(string $username, string $role): array { if ($username === ($_SESSION['admin_user'] ?? '')) { return ['success' => false, 'message' => 'Je kunt je eigen rol niet wijzigen.']; } if (!isset(self::ROLE_PERMISSIONS[$role])) { return ['success' => false, 'message' => 'Ongeldige rol.']; } foreach ($this->adminConfig['users'] as &$user) { if ($user['username'] === $username) { $user['role'] = $role; $this->saveAdminConfig(); $this->log('info', "Rol gewijzigd: {$username} -> {$role}"); return ['success' => true, 'message' => 'Rol gewijzigd.']; } } return ['success' => false, 'message' => 'Gebruiker niet gevonden.']; } /** * Verwijdert een gebruiker uit de admin-configuratie. * * Een gebruiker kan zichzelf niet verwijderen. De gebruikerslijst wordt * gefilterd, opgeslagen en de actie gelogd. * * @since 2.6.4 * * @param string $username Gebruikersnaam. * @return array Resultaat met keys 'success' (bool) en 'message' (string). */ public function deleteUser(string $username): array { if ($username === ($_SESSION['admin_user'] ?? '')) { return ['success' => false, 'message' => 'Je kunt jezelf niet verwijderen.']; } $this->adminConfig['users'] = array_values(array_filter( $this->adminConfig['users'], fn($u) => $u['username'] !== $username )); $this->saveAdminConfig(); $this->log('info', "Gebruiker verwijderd: {$username}"); return ['success' => true, 'message' => 'Gebruiker verwijderd.']; } /** * Zet een nieuw wachtwoord voor een gebruiker. * * Controleert de minimum wachtwoordlengte (8), hasht het wachtwoord met de * standaard PHP-hash en slaat de wijziging op. * * @since 2.6.4 * * @param string $username Gebruikersnaam. * @param string $newPassword Nieuw wachtwoord (plaintext, minimaal 8 tekens). * @return array Resultaat met keys 'success' (bool) en 'message' (string). */ public function changePassword(string $username, string $newPassword): array { if (strlen($newPassword) < 8) { return ['success' => false, 'message' => 'Wachtwoord moet minimaal 8 tekens zijn.']; } foreach ($this->adminConfig['users'] as &$user) { if ($user['username'] === $username) { $user['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT); $this->saveAdminConfig(); $this->log('info', "Wachtwoord gewijzigd: {$username}"); return ['success' => true, 'message' => 'Wachtwoord gewijzigd.']; } } return ['success' => false, 'message' => 'Gebruiker niet gevonden.']; } /** * Wijzigt het eigen wachtwoord na verificatie van het huidige wachtwoord. * * Verifieert eerst het huidige wachtwoord tegen de opgeslagen hash, controleert * de lengte van het nieuwe wachtwoord (minimaal 8) en slaat de nieuwe hash op. * * @since 2.6.4 * * @param string $username Gebruikersnaam. * @param string $currentPassword Huidig wachtwoord (plaintext). * @param string $newPassword Nieuw wachtwoord (plaintext, minimaal 8 tekens). * @return array Resultaat met keys 'success' (bool) en 'message' (string). */ public function changeOwnPassword(string $username, string $currentPassword, string $newPassword): array { $user = $this->findUser($username); if (!$user) { return ['success' => false, 'message' => 'Gebruiker niet gevonden.']; } if (!password_verify($currentPassword, $user['password_hash'])) { return ['success' => false, 'message' => 'Huidig wachtwoord is onjuist.']; } if (strlen($newPassword) < 8) { return ['success' => false, 'message' => 'Nieuw wachtwoord moet minimaal 8 tekens zijn.']; } foreach ($this->adminConfig['users'] as &$u) { if ($u['username'] === $username) { $u['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT); $this->saveAdminConfig(); $this->log('info', "Eigen wachtwoord gewijzigd: {$username}"); return ['success' => true, 'message' => 'Wachtwoord gewijzigd.']; } } return ['success' => false, 'message' => 'Fout bij wijzigen wachtwoord.']; } // --- Private helpers --- /** * Zoekt een gebruiker op gebruikersnaam in de admin-configuratie. * * @since 2.6.4 * * @param string $username Gebruikersnaam. * @return array|null Gebruikersdata als array, of null indien niet gevonden. */ private function findUser(string $username): ?array { foreach ($this->adminConfig['users'] ?? [] as $user) { if ($user['username'] === $username) { return $user; } } return null; } /** * Controleert of een gebruiker momenteel is vergrendeld door brute-force protection. * * Vergelijkt het aantal mislukte pogingen en de verstreken tijd sinds de * laatste poging met de geconfigureerde limieten. Verloopt de lockout dan * worden de pogingen gewist. * * @since 2.6.4 * * @param string $username Gebruikersnaam. * @return array Resultaat met keys 'locked' (bool) en optioneel 'remaining' (int, seconden). */ private function checkLockout(string $username): array { $attempts = $this->getFailedAttempts(); $maxAttempts = $this->adminConfig['security']['max_login_attempts'] ?? 5; $lockoutDuration = $this->adminConfig['security']['lockout_duration'] ?? 900; if (!isset($attempts[$username])) { return ['locked' => false]; } $record = $attempts[$username]; if ($record['count'] >= $maxAttempts) { $elapsed = time() - $record['last_attempt']; if ($elapsed < $lockoutDuration) { return ['locked' => true, 'remaining' => $lockoutDuration - $elapsed]; } // Lockout expired $this->clearFailedAttempts($username); } return ['locked' => false]; } /** * Registreert een mislukte inlogpoging voor een gebruiker. * * Verhoogt de teller en actualiseert de timestamp van de laatste poging, * waarna de gegevens worden weggeschreven naar het lockout-bestand. * * @since 2.6.4 * * @param string $username Gebruikersnaam. * @return void */ private function recordFailedAttempt(string $username): void { $attempts = $this->getFailedAttempts(); if (!isset($attempts[$username])) { $attempts[$username] = ['count' => 0, 'last_attempt' => 0]; } $attempts[$username]['count']++; $attempts[$username]['last_attempt'] = time(); file_put_contents($this->lockFile, json_encode($attempts)); } /** * Wist alle mislukte inlogpogingen voor een gebruiker. * * Verwijdert de gebruiker uit het lockout-bestand en slaat dit op. * * @since 2.6.4 * * @param string $username Gebruikersnaam. * @return void */ private function clearFailedAttempts(string $username): void { $attempts = $this->getFailedAttempts(); unset($attempts[$username]); file_put_contents($this->lockFile, json_encode($attempts)); } /** * Wist de lockout voor een gebruiker (publiek, gebruikt door CLI reset). * * Wrapper rond clearFailedAttempts die ook van buiten de class kan worden * aangeroepen, bijvoorbeeld door een CLI-tool voor het resetten van vergrendelingen. * * @since 2.6.4 * * @param string $username Gebruikersnaam. * @return void */ public function clearLockout(string $username): void { $this->clearFailedAttempts($username); } /** * Leest alle mislukte inlogpogingen uit het lockout-bestand. * * @since 2.6.4 * * @return array Map van gebruikersnaam naar poging-data. */ private function getFailedAttempts(): array { if (!file_exists($this->lockFile)) { return []; } $data = json_decode(file_get_contents($this->lockFile), true); return is_array($data) ? $data : []; } /** * Schrijft een bericht naar het logbestand met timestamp, level en IP-adres. * * Maakt de logdirectory aan indien nodig. Het IP-adres wordt via RequestLogger * bepaald indien beschikbaar, anders via REMOTE_ADDR met een fallback. * * @since 2.6.4 * * @param string $level Log-niveau (bijv. 'info', 'warning'). * @param string $message Bericht dat gelogd moet worden. * @return void */ private function log(string $level, string $message): void { $logFile = $this->config['log_file']; $dir = dirname($logFile); if (!is_dir($dir)) { mkdir($dir, 0755, true); } $timestamp = date('Y-m-d H:i:s'); if (!class_exists('RequestLogger')) { $loggerClass = __DIR__ . '/../../cms/core/class/RequestLogger.php'; if (file_exists($loggerClass)) { require_once $loggerClass; } } $ip = class_exists('RequestLogger') ? RequestLogger::getClientIp() : ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'); file_put_contents($logFile, "[{$timestamp}] [{$level}] [{$ip}] {$message}\n", FILE_APPEND); } }