field_renderer = $field_renderer; $this->context_factory = $context_factory; $this->result_mapper = $result_mapper; $this->spam_engine = $spam_engine; $this->settings_repository = $settings_repository; $this->event_logger = $event_logger; $this->availability_callback = $availability_callback; } /** * Get stable integration source key. */ public function get_key(): string { return self::KEY; } /** * Get integration label. */ public function get_label(): string { if ( function_exists( 'esc_html__' ) ) { return esc_html__( 'WordPress login', '2o2code-spamless-forms' ); } return 'WordPress login'; } /** * Get stable integration slug. */ public function get_slug(): string { return $this->get_key(); } /** * Whether native login protection can register. */ public function is_available(): bool { if ( is_callable( $this->availability_callback ) ) { return (bool) call_user_func( $this->availability_callback ); } return function_exists( 'add_action' ) && function_exists( 'add_filter' ); } /** * Register native login hooks. */ public function register_hooks(): void { if ( ! $this->is_available() || ! $this->is_enabled() || ! function_exists( 'add_action' ) || ! function_exists( 'add_filter' ) ) { return; } add_action( 'login_form', array( $this, 'render_protection_fields' ), 20, 0 ); add_filter( 'authenticate', array( $this, 'validate_authentication' ), 100, 3 ); } /** * Render protection fields inside the native login form. */ public function render_protection_fields(): void { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ProtectionFieldRenderer returns escaped hidden fields. echo $this->field_renderer->render_fields( LoginContextFactory::DEFAULT_FORM_ID ); } /** * Validate native login attempts through the shared engine. * * @param mixed $user Existing authentication result. * @param string $username Submitted username. * @param string $password Submitted password. Intentionally ignored. * * @return mixed|\WP_Error */ public function validate_authentication( $user, string $username, string $password ) { unset( $password ); if ( ! $this->is_enabled() || $this->is_non_form_request() ) { return $user; } $posted_data = $this->get_post_data(); $settings = SettingsSnapshot::from_array( $this->settings_repository->get_settings() ); $context = $this->context_factory->build_from_login_data( $username, $posted_data, $this->get_rate_limit_identifier() ); $decision = $this->spam_engine->evaluate( $context, $settings ); if ( $this->event_logger instanceof EventLogger ) { $this->event_logger->log_decision( self::KEY, $context, $decision ); } return $this->result_mapper->map_login_result( $decision, $user ); } /** * Whether the integration is enabled in settings. */ private function is_enabled(): bool { $settings = $this->settings_repository->get_settings(); return ! empty( $settings[ self::SETTINGS_ENABLED_KEY ] ); } /** * Whether this request should be ignored by native login protection. */ private function is_non_form_request(): bool { return ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ); } /** * Get unslashed posted data. * * @return array */ private function get_post_data(): array { // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Public login submissions are normalized and sanitized by the context factory before evaluation. $posted_data = wp_unslash( $_POST ); // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return $posted_data; } /** * Get request identifier for immediate hashing by the rate-limit check. */ private function get_rate_limit_identifier(): string { // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The value is sanitized here and hashed before storage by the rate-limit check. if ( ! isset( $_SERVER['REMOTE_ADDR'] ) ) { return ''; } $remote_addr = (string) wp_unslash( $_SERVER['REMOTE_ADDR'] ); // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return sanitize_text_field( $remote_addr ); } }