markup_injector = $markup_injector; $this->context_factory = $context_factory; $this->result_mapper = $result_mapper; $this->spam_engine = $spam_engine; $this->settings_repository = $settings_repository; $this->elementor_availability_callback = $elementor_availability_callback; $this->provider_availability_callback = $provider_availability_callback; $this->widget_matcher_callback = $widget_matcher_callback; $this->event_logger = $event_logger; $this->plugin_activation_checker = $plugin_activation_checker instanceof PluginActivationChecker ? $plugin_activation_checker : new PluginActivationChecker(); } /** * 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__( 'Elementor Form Lite', '2o2code-spamless-forms' ); } return 'Elementor Form Lite'; } /** * Get stable integration slug. */ public function get_slug(): string { return self::KEY; } /** * Whether Elementor and the complete Hello Plus Form Lite contract exist. */ public function is_available(): bool { return $this->is_elementor_available() && $this->is_provider_available(); } /** * Register render and native provider validation hooks. */ public function register_hooks(): void { if ( ! $this->is_available() || ! $this->is_enabled() || ! function_exists( 'add_filter' ) || ! function_exists( 'add_action' ) ) { return; } add_filter( 'elementor/widget/render_content', array( $this, 'filter_render_content' ), 20, 2 ); add_action( 'hello_plus/forms/validation', array( $this, 'validate_submission' ), 20, 2 ); } /** * Add fields only to a supported ehp-form widget instance. * * @param mixed $content Rendered widget content. * @param mixed $widget Elementor widget. */ public function filter_render_content( $content, $widget ): string { $content = is_string( $content ) ? $content : ''; if ( ! $this->is_enabled() || ! $this->is_form_lite_widget( $widget ) || ! method_exists( $widget, 'get_id' ) ) { return $content; } $widget_id = $widget->get_id(); return is_scalar( $widget_id ) ? $this->markup_injector->inject( $content, (string) $widget_id ) : $content; } /** * Evaluate a native Hello Plus validation request. * * @param mixed $record Hello Plus form record. * @param mixed $ajax_handler Hello Plus AJAX handler. */ public function validate_submission( $record, $ajax_handler ): void { if ( ! $this->is_enabled() ) { return; } $posted_data = $this->get_post_data(); if ( ! $this->is_supported_request( $record, $ajax_handler, $posted_data ) ) { return; } $settings = SettingsSnapshot::from_array( $this->settings_repository->get_settings() ); $context = $this->context_factory->build_from_record( $record, $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 ); } $this->result_mapper->map_validation_result( $decision, $ajax_handler ); } /** * Whether Elementor Core is loaded. */ private function is_elementor_available(): bool { if ( is_callable( $this->elementor_availability_callback ) ) { return (bool) call_user_func( $this->elementor_availability_callback ); } return defined( 'ELEMENTOR_VERSION' ) && class_exists( '\Elementor\Plugin' ) && $this->plugin_activation_checker->is_class_owner_active( '\Elementor\Plugin' ); } /** * Whether the complete provider contract is registered. */ private function is_provider_available(): bool { if ( is_callable( $this->provider_availability_callback ) ) { return (bool) call_user_func( $this->provider_availability_callback ); } if ( ! defined( 'HELLOPLUS_VERSION' ) || ! class_exists( '\HelloPlus\Modules\Forms\Module' ) || ! class_exists( '\HelloPlus\Modules\Forms\Widgets\Ehp_Form' ) || ! class_exists( '\HelloPlus\Modules\Forms\Classes\Form_Record' ) || ! class_exists( '\HelloPlus\Modules\Forms\Components\Ajax_Handler' ) || ! $this->has_provider_ajax_hooks() ) { return false; } if ( ! $this->plugin_activation_checker->is_class_owner_active( '\HelloPlus\Modules\Forms\Module' ) ) { return false; } $elementor_class = '\\Elementor\\Plugin'; if ( ! is_callable( array( $elementor_class, 'instance' ) ) ) { return false; } try { $elementor = call_user_func( array( $elementor_class, 'instance' ) ); } catch ( Throwable $throwable ) { unset( $throwable ); return false; } if ( ! is_object( $elementor ) || ! isset( $elementor->widgets_manager ) || ! is_object( $elementor->widgets_manager ) || ! method_exists( $elementor->widgets_manager, 'get_widget_types' ) ) { return false; } $widget = $elementor->widgets_manager->get_widget_types( self::WIDGET_NAME ); return is_object( $widget ) && is_a( $widget, '\HelloPlus\Modules\Forms\Widgets\Ehp_Form' ); } /** * Whether both native AJAX routes are registered by Hello Plus. */ private function has_provider_ajax_hooks(): bool { if ( ! function_exists( 'has_action' ) ) { return false; } return false !== has_action( 'wp_ajax_' . self::AJAX_ACTION ) && false !== has_action( 'wp_ajax_nopriv_' . self::AJAX_ACTION ); } /** * Whether the render callback belongs to the exact provider widget. * * @param mixed $widget Elementor widget. */ private function is_form_lite_widget( $widget ): bool { if ( is_callable( $this->widget_matcher_callback ) ) { return (bool) call_user_func( $this->widget_matcher_callback, $widget ); } return is_object( $widget ) && method_exists( $widget, 'get_name' ) && self::WIDGET_NAME === call_user_func( array( $widget, 'get_name' ) ) && is_a( $widget, '\HelloPlus\Modules\Forms\Widgets\Ehp_Form' ); } /** * Validate provider object types and the bounded native request shape. * * @param mixed $record Hello Plus record. * @param mixed $ajax_handler Hello Plus AJAX handler. * @param array $posted_data Unslashed root request data. */ private function is_supported_request( $record, $ajax_handler, array $posted_data ): bool { $allow_test_doubles = is_callable( $this->provider_availability_callback ); if ( ! is_object( $record ) || ! method_exists( $record, 'get' ) || ( ! $allow_test_doubles && ! is_a( $record, '\HelloPlus\Modules\Forms\Classes\Form_Record' ) ) || ! is_object( $ajax_handler ) || ! method_exists( $ajax_handler, 'add_error' ) || ( ! $allow_test_doubles && ! is_a( $ajax_handler, '\HelloPlus\Modules\Forms\Components\Ajax_Handler' ) ) ) { return false; } $action = isset( $posted_data['action'] ) && is_scalar( $posted_data['action'] ) ? strtolower( preg_replace( '/[^a-z0-9_]/i', '', (string) $posted_data['action'] ) ?? '' ) : ''; $post_id = isset( $posted_data['post_id'] ) && is_scalar( $posted_data['post_id'] ) ? (int) $posted_data['post_id'] : 0; $form_id = isset( $posted_data['form_id'] ) && is_scalar( $posted_data['form_id'] ) ? (string) $posted_data['form_id'] : ''; $form_fields = isset( $posted_data['form_fields'] ) ? $posted_data['form_fields'] : null; $fields = $record->get( 'fields' ); return self::AJAX_ACTION === $action && $post_id > 0 && ElementorFormLiteContextFactory::is_valid_widget_id( $form_id ) && is_array( $form_fields ) && is_array( $fields ); } /** * Whether this integration is enabled. */ private function is_enabled(): bool { $settings = $this->settings_repository->get_settings(); return ! empty( $settings[ self::SETTINGS_ENABLED_KEY ] ); } /** * Get unslashed posted data after the provider has verified its nonce. * * @return array */ private function get_post_data(): array { // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This callback runs inside Hello Plus after its native nonce validation; the bounded shape is verified before evaluation. $posted_data = wp_unslash( $_POST ); // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return $posted_data; } /** * Get the request identifier for immediate hashing only. */ private function get_rate_limit_identifier(): string { if ( ! isset( $_SERVER['REMOTE_ADDR'] ) || ! is_scalar( $_SERVER['REMOTE_ADDR'] ) ) { return ''; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized here and immediately hashed by the shared rate-limit check. return sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) ); } }