$context Small id-only context map. */ public static function flag(string $reason, array $context = []): void { $mode = self::enforcing() ? 'blocked' : 'would-block'; if (!isset($context['ip'])) { $context['ip'] = self::clientIpHint(); } $context['mode'] = $mode; if (class_exists(Logger::class)) { Logger::warning(self::LOG_TAG . ' ' . $mode . ': ' . $reason, $context); } elseif (defined('WP_DEBUG') && WP_DEBUG) { // Logger should always exist, but never let logging itself fatal a request. error_log(self::LOG_TAG . ' ' . $mode . ': ' . $reason . ' ' . wp_json_encode($context)); } } /** * Convenience: a check failed. Always logs; returns whether the caller should * actually block (true only in enforce mode). Callers keep their own reject * response so HTTP status / message stay route-appropriate. * * if (!$ownsBooking && Guard::denied('payment_complete_ownership', $ctx)) { * return new WP_REST_Response([...], 403); * } * * @param array $context */ public static function denied(string $reason, array $context = []): bool { self::flag($reason, $context); return self::enforcing(); } /** * Coarse client IP for log correlation. Uses REMOTE_ADDR only (not forwarded * headers) so it can't be trivially spoofed into the logs. */ private static function clientIpHint(): string { $ip = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : ''; return $ip !== '' ? sanitize_text_field($ip) : 'unknown'; } }