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 slug. */ public function get_slug(): string { return $this->get_key(); } /** * 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__( 'Contact Form 7', '2o2code-spamless-forms' ); } return 'Contact Form 7'; } /** * Whether Contact Form 7 appears available. */ public function is_available(): bool { if ( is_callable( $this->availability_callback ) ) { return (bool) call_user_func( $this->availability_callback ); } return defined( 'WPCF7_VERSION' ) || class_exists( 'WPCF7_ContactForm' ) || function_exists( 'wpcf7' ); } /** * Register Contact Form 7 hooks. */ public function register_hooks(): void { if ( ! $this->is_available() || ! $this->is_enabled() || ! function_exists( 'add_filter' ) ) { return; } add_filter( 'wpcf7_form_elements', array( $this, 'render_protection_fields' ), 20, 1 ); add_filter( 'wpcf7_validate', array( $this, 'validate_submission' ), 20, 2 ); } /** * Append protection fields to rendered forms. * * @param string $form_html Existing form HTML. */ public function render_protection_fields( string $form_html ): string { return $form_html . $this->field_renderer->render_fields( ContactForm7ContextFactory::DEFAULT_FORM_ID ); } /** * Validate a Contact Form 7 submission through the shared engine. * * @param mixed $validation_result Native validation result. * @param mixed $tags Native tags. * * @return mixed */ public function validate_submission( $validation_result, $tags = null ) { if ( ! $this->is_enabled() ) { return $validation_result; } $settings = SettingsSnapshot::from_array( $this->settings_repository->get_settings() ); $context = $this->context_factory->build_from_submission( $this->get_current_submission(), $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_validation_result( $decision, $validation_result, $this->get_first_valid_tag( $tags ) ); } /** * 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 current Contact Form 7 submission if available. * * @return object|null */ private function get_current_submission() { $submission_class = 'WPCF7_Submission'; if ( ! class_exists( $submission_class ) || ! is_callable( array( $submission_class, 'get_instance' ) ) ) { return null; } $submission = call_user_func( array( $submission_class, 'get_instance' ) ); return is_object( $submission ) ? $submission : null; } /** * Get unslashed posted data. * * @return array */ private function get_post_data(): array { // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Public CF7 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 ); } /** * Get the first named native validation tag. * * @param mixed $tags Native tags. * * @return mixed */ private function get_first_valid_tag( $tags ) { if ( ! is_array( $tags ) ) { return null; } foreach ( $tags as $tag ) { $name = ''; if ( is_object( $tag ) && isset( $tag->name ) ) { $name = trim( (string) $tag->name ); } elseif ( is_array( $tag ) && isset( $tag['name'] ) ) { $name = trim( (string) $tag['name'] ); } elseif ( is_string( $tag ) ) { $name = trim( $tag ); } if ( '' === $name ) { continue; } if ( function_exists( 'wpcf7_is_name' ) && ! wpcf7_is_name( $name ) ) { continue; } return $tag; } return null; } }