*/ public function list(): array { $raw = Config::get(self::OPTION_KEY, []); if (!is_array($raw)) { return []; } $rules = []; foreach ($raw as $entry) { if (is_array($entry) && is_string($entry['rule'] ?? null) && IpRule::parse($entry['rule'])['rule'] !== null) { $rules[] = ['rule' => $entry['rule'], 'added_at' => (string) ($entry['added_at'] ?? ''), 'note' => (string) ($entry['note'] ?? '')]; } } return $rules; } /** * @param list $protectedIps * * @return array{ok: bool, http: int, code?: string, message?: string, rule?: string} */ public function block(string $raw, array $protectedIps, ?string $callerIp, string $note = ''): array { $parsed = IpRule::parse($raw); $rule = $parsed['rule']; if ($rule === null) { return ['ok' => false, 'http' => 400, 'code' => 'VALIDATION_ERROR', 'message' => (string) $parsed['error']]; } if ($rule->containsAny($protectedIps) || ($callerIp !== null && $rule->contains($callerIp))) { return ['ok' => false, 'http' => 422, 'code' => 'PROTECTED_ADDRESS', 'message' => 'Cette règle couvrirait l\'adresse de la plateforme : elle couperait la supervision du site.']; } $rules = $this->list(); $canonical = $rule->canonical(); foreach ($rules as $existing) { if ($existing['rule'] === $canonical) { return ['ok' => true, 'http' => 200, 'rule' => $canonical]; } } if (count($rules) >= self::MAX_RULES) { return ['ok' => false, 'http' => 409, 'code' => 'CONFLICT', 'message' => sprintf('Liste pleine (%d règles) : retirez-en avant d\'en ajouter.', self::MAX_RULES)]; } $rules[] = ['rule' => $canonical, 'added_at' => gmdate('c'), 'note' => mb_substr(trim(strip_tags($note)), 0, 120)]; Config::set(self::OPTION_KEY, $rules); Logger::info('security', 'Adresse bloquée à la connexion : ' . $canonical); return ['ok' => true, 'http' => 200, 'rule' => $canonical]; } /** @return array{ok: bool, http: int, code?: string, message?: string, rule?: string} */ public function unblock(string $raw): array { $parsed = IpRule::parse($raw); if ($parsed['rule'] === null) { return ['ok' => false, 'http' => 400, 'code' => 'VALIDATION_ERROR', 'message' => (string) $parsed['error']]; } $canonical = $parsed['rule']->canonical(); $rules = array_values(array_filter($this->list(), static fn (array $e): bool => $e['rule'] !== $canonical)); Config::set(self::OPTION_KEY, $rules); Logger::info('security', 'Adresse débloquée à la connexion : ' . $canonical); return ['ok' => true, 'http' => 200, 'rule' => $canonical]; } public function isBlocked(string $ip): bool { foreach ($this->list() as $entry) { $rule = IpRule::parse($entry['rule'])['rule']; if ($rule !== null && $rule->contains($ip)) { return true; } } return false; } public static function register(): void { add_filter('authenticate', static function (mixed $user): mixed { $ip = sanitize_text_field(wp_unslash((string) ($_SERVER['REMOTE_ADDR'] ?? ''))); if ($ip !== '' && (new self())->isBlocked($ip)) { return new \WP_Error('sentinelo_ip_blocked', __('Login refused from this address.', 'sentinelo')); } return $user; }, 99, 1); add_action('login_init', static function (): void { $ip = sanitize_text_field(wp_unslash((string) ($_SERVER['REMOTE_ADDR'] ?? ''))); if ($ip !== '' && (new self())->isBlocked($ip)) { wp_die(esc_html__('Login refused from this address.', 'sentinelo'), '', ['response' => 403]); } }, 1); } }