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 comments', '2o2code-spamless-forms' ); } return 'WordPress comments'; } /** * Get stable integration slug. */ public function get_slug(): string { return $this->get_key(); } /** * Whether native comments 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 comment hooks. */ public function register_hooks(): void { if ( ! $this->is_available() || ! $this->is_enabled() || ! function_exists( 'add_action' ) || ! function_exists( 'add_filter' ) ) { return; } add_action( 'comment_form_after_fields', array( $this, 'render_protection_fields' ), 20, 0 ); add_action( 'comment_form_logged_in_after', array( $this, 'render_protection_fields' ), 20, 0 ); add_filter( 'pre_comment_approved', array( $this, 'validate_approval' ), 5, 2 ); } /** * Render protection fields inside native comment forms. */ public function render_protection_fields(): void { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ProtectionFieldRenderer returns escaped hidden fields. echo $this->field_renderer->render_fields( CommentsContextFactory::DEFAULT_FORM_ID ); } /** * Validate a native comment approval through the shared engine. * * @param int|string|\WP_Error $approved Native approval value. * @param array $comment_data Native comment data. * * @return int|string|\WP_Error */ public function validate_approval( $approved, array $comment_data ) { if ( ! $this->is_enabled() ) { return $approved; } $settings = SettingsSnapshot::from_array( $this->settings_repository->get_settings() ); $context = $this->context_factory->build_from_comment_data( $comment_data, $this->get_post_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_comment_result( $decision, $approved ); } /** * 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 ] ); } /** * Get unslashed posted data. * * @return array */ private function get_post_data(): array { // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Public comment 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 ); } }