JezK
Edit File: Manager.php
<?php namespace WeDevs\Dokan\Captcha; use WeDevs\Dokan\Admin\Settings; use WeDevs\Dokan\Captcha\Providers\GoogleRecaptchaV3Provider; use WeDevs\Dokan\Contracts\Hookable; /** * Captcha service manager. * * Central registry and facade for captcha providers. Handles provider selection, * asset registration, field rendering and server-side validation. Resolved via * Dokan DI container. * * @since 4.3.0 */ class Manager implements Hookable { /** @var ProviderInterface[] */ protected array $providers = []; /** * Register WordPress hooks used by the captcha system. * * @return void */ public function register_hooks(): void { add_action( 'init', [ $this, 'register_providers_from_filter' ] ); // Hook to render extra form fields if needed add_action( 'dokan_contact_form', [ $this, 'maybe_render_contact_form_field' ], 20 ); // Render & validate captcha on the Dokan (`register_form`) and WooCommerce My Account (`woocommerce_register_form`) registration forms. add_action( 'register_form', [ $this, 'maybe_render_registration_field' ], 20 ); add_action( 'woocommerce_register_form', [ $this, 'maybe_render_registration_field' ], 20 ); add_action( 'woocommerce_register_post', [ $this, 'validate_registration_captcha' ], 20, 3 ); // Allow providers to inject their settings fields into admin settings add_filter( 'dokan_settings_fields', [ $this, 'filter_settings_fields' ], 10, 2 ); } /** * Resolve providers via filter and register them. * * @return void */ public function register_providers_from_filter(): void { $default = []; $providers = apply_filters( 'dokan_captcha_providers', $default ); if ( ! is_array( $providers ) ) { $providers = (array) $providers; } foreach ( $providers as $provider ) { if ( is_string( $provider ) && class_exists( $provider ) ) { $provider = new $provider(); } if ( $provider instanceof ProviderInterface ) { $this->register_provider( $provider ); } } } /** * Register a captcha provider instance. * * @param ProviderInterface $provider Provider instance implementing the contract. * * @return void */ public function register_provider( ProviderInterface $provider ): void { $this->providers[ $provider->get_slug() ] = $provider; } /** Get active provider slug selected from settings */ public function get_active_provider_slug(): string { $slug = function_exists( 'dokan_get_option' ) ? dokan_get_option( 'captcha_provider', 'dokan_appearance', 'google_recaptcha_v3' ) : 'google_recaptcha_v3'; if ( empty( $slug ) || ! isset( $this->providers[ $slug ] ) ) { // Backward compatibility: default to google if provider not chosen $slug = 'google_recaptcha_v3'; } return $slug; } /** Is captcha globally enabled? Falls back to provider-specific flag if global flag missing. */ public function is_enabled(): bool { // Global flag $global = function_exists( 'dokan_get_option' ) ? dokan_get_option( 'captcha_enable_status', 'dokan_appearance', '' ) : ''; if ( '' !== $global ) { return $this->to_bool( $global ); } // Fallback: Google legacy flag $google_enabled = function_exists( 'dokan_get_option' ) ? dokan_get_option( 'recaptcha_enable_status', 'dokan_appearance', 'on' ) : 'on'; return $this->to_bool( $google_enabled ); } /** Return active provider instance, or null if not ready */ public function get_active_provider(): ?ProviderInterface { $slug = $this->get_active_provider_slug(); $provider = $this->providers[ $slug ] ?? null; if ( ! $provider ) { return null; } if ( ! $this->is_enabled() ) { return null; } if ( ! $provider->is_ready() ) { return null; } return $provider; } /** Enqueue/register assets for the active provider */ public function register_assets(): void { $provider = $this->get_active_provider(); if ( $provider ) { $provider->register_assets(); } } /** Validate token for a context. */ public function validate( string $context, string $token ): bool { $provider = $this->get_active_provider(); if ( ! $provider ) { return false; } return $provider->validate( $context, $token ); } /** Render hidden/widget field for forms if needed */ public function render_field_html( string $context, array $args = [] ): string { $provider = $this->get_active_provider(); if ( ! $provider ) { return ''; } return $provider->render_field_html( $context, $args ); } /** Echoes provider fields into contact form, keeping backward compatibility */ public function maybe_render_contact_form_field( $seller_id ): void { // phpcs:ignore WordPress.NamingConventions.ValidVariableName echo $this->render_field_html( 'dokan_contact_seller_recaptcha', [ 'seller_id' => $seller_id ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Render the captcha field on the registration form. * * Hooked to both `register_form` (Dokan vendor registration & onboarding templates) and * `woocommerce_register_form` (WooCommerce My Account registration). A static guard makes * sure the field is output only once per request, avoiding a duplicate token field/widget. * * @since 5.0.6 * * @return void */ public function maybe_render_registration_field(): void { static $rendered = false; if ( $rendered ) { return; } // Skip wp-login.php: it also fires `register_form` but has no token script and is validated by WordPress core, not here. if ( isset( $GLOBALS['pagenow'] ) && 'wp-login.php' === $GLOBALS['pagenow'] ) { return; } if ( ! $this->get_active_provider() ) { return; } // Make sure the active provider's front-end assets are loaded on the registration page. $this->register_assets(); $rendered = true; echo $this->render_field_html( 'dokan_registration_recaptcha' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Validate the captcha token submitted with a registration request. * * Hooked to `woocommerce_register_post`, which fires for every WooCommerce registration * after the registration nonce has already been verified upstream. Adds an error to the * registration error bag when verification fails, which aborts the registration. * * @since 5.0.6 * * @param string $username Submitted username. * @param string $email Submitted email. * @param \WP_Error $validation_errors Registration error bag. * * @return void */ public function validate_registration_captcha( $username, $email, $validation_errors ): void { // Enforce only on a real registration-form submit; programmatic wc_create_new_customer() callers post no `register` field nor captcha. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Presence check only; WooCommerce verifies the registration nonce before this hook fires. if ( ! isset( $_POST['register'] ) ) { return; } // Captcha is rendered on the registration form only, not during checkout account creation. if ( function_exists( 'is_checkout' ) && is_checkout() ) { return; } // REST / WP-CLI registrations do not render the captcha field; skip to avoid a lockout. if ( defined( 'REST_REQUEST' ) || defined( 'WP_CLI' ) ) { return; } // No active/ready provider means no captcha was rendered; nothing to validate. if ( ! $this->get_active_provider() ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce verifies the registration nonce before this hook fires. $token = isset( $_POST['dokan_recaptcha_token'] ) ? sanitize_text_field( wp_unslash( $_POST['dokan_recaptcha_token'] ) ) : ''; if ( ! $this->validate( 'dokan_registration_recaptcha', $token ) ) { $validation_errors->add( 'dokan_captcha_error', __( 'Captcha verification failed. Please try again.', 'dokan-lite' ) ); } } /** * Filter admin settings fields to append provider-specific settings under dokan_appearance. * * @param array $settings_fields Fields to be rendered in admin settings. * @param Settings $settings_instance Settings instance. * * @return array */ public function filter_settings_fields( array $settings_fields, $settings_instance ): array { if ( ! isset( $settings_fields['dokan_appearance'] ) || ! is_array( $settings_fields['dokan_appearance'] ) ) { return $settings_fields; } $settings_fields['dokan_appearance']['captcha_enable_status'] = [ 'name' => 'captcha_enable_status', 'label' => __( 'Enable Captcha Service', 'dokan-lite' ), 'desc' => __( 'Enable or disable captcha across forms that support it.', 'dokan-lite' ), 'type' => 'switcher', 'default' => 'on', ]; // Build provider options as slug => label $options = []; foreach ( $this->providers as $p ) { $options[ $p->get_slug() ] = $p->get_label(); } $settings_fields['dokan_appearance']['captcha_provider'] = [ 'name' => 'captcha_provider', 'label' => __( 'Captcha Provider', 'dokan-lite' ), 'type' => 'select', 'desc' => __( 'Select the captcha provider to use for this store.', 'dokan-lite' ), 'options' => $options, 'default' => dokan_get_container()->get( GoogleRecaptchaV3Provider::class )->get_slug(), 'show_if' => [ 'captcha_enable_status' => [ 'equal' => 'on', ], ], ]; foreach ( $this->providers as $provider ) { $fields = $provider->get_admin_settings_fields(); foreach ( $fields as $key => $config ) { if ( ! isset( $settings_fields['dokan_appearance'][ $key ] ) ) { // Show when global captcha enabled AND selected provider matches $config['show_if'] = [ 'captcha_enable_status' => [ 'equal' => 'on', ], 'captcha_provider' => [ 'equal' => $provider->get_slug(), ], ]; $settings_fields['dokan_appearance'][ $key ] = $config; } } } return $settings_fields; } /** Utility */ protected function to_bool( $value ): bool { $truthy = [ 'yes', 1, '1', 'true', 'on' ]; if ( is_bool( $value ) ) { return $value; } $value = strtolower( (string) $value ); return in_array( $value, $truthy, true ); } }