Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/images/cloudflare.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
209 changes: 209 additions & 0 deletions docs/captcha.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# Dokan Captcha System: Migration Guide & Roadmap

This single document explains how to migrate from the legacy Google reCAPTCHA v3 implementation to the new provider-based Captcha system in Dokan 4.0.8 and outlines future improvements.

- Target audience: Theme/plugin developers and site maintainers
- Applies to: Dokan Lite ≥ DOKAN_SINCE


## TL;DR

- Resolve the Captcha Manager from the container: `dokan_get_container()->get(\WeDevs\Dokan\Captcha\Manager::class)`
- Do not use removed helper functions.
- Keep the hidden input named `dokan_recaptcha_token` in forms.
- The provider injects any extra form markup via the `dokan_contact_form` action.
- Let the Manager handle scripts via `->register_assets()`.
- Choose a provider and set credentials in Admin → Dokan → Settings → Appearance.


## What Changed

Previously, reCAPTCHA v3 support was procedural and Google-only. The new system:

- Introduces a provider-based architecture
- `WeDevs\Dokan\Captcha\ProviderInterface`
- `WeDevs\Dokan\Captcha\AbstractProvider` (optional base)
- `WeDevs\Dokan\Captcha\Manager` (service facade)
- Built-in providers:
- `WeDevs\Dokan\Captcha\Providers\GoogleRecaptchaV3Provider`
- `WeDevs\Dokan\Captcha\Providers\CloudflareTurnstileProvider`
- Is resolved from Dokan’s DI container (no singletons)
- Registers providers via the `dokan_captcha_providers` filter so third parties can add providers
- Delegates assets, markup, and validation to the active provider
- Adds general Captcha settings plus provider-specific settings in the Appearance section


## Removed/Deprecated Helpers

The following legacy functions were removed and should not be used anymore:

- `dokan_get_recaptcha_site_and_secret_keys()`
- `dokan_handle_recaptcha_validation()`
- `dokan_captcha()`

Replace these with the Manager service (see examples below).


## Admin Settings Overview

Navigate to: WP Admin → Dokan → Settings → Appearance

- Enable Captcha Service: `captcha_enable_status` (on/off)
- Captcha Provider: `captcha_provider`
- `google_recaptcha_v3`
- `cloudflare_turnstile`
- Google reCAPTCHA v3 fields (legacy-compatible)
- `recaptcha_enable_status`
- `recaptcha_site_key`
- `recaptcha_secret_key`
- Cloudflare Turnstile fields
- `turnstile_enable_status`
- `turnstile_site_key`
- `turnstile_secret_key`

Providers can contribute fields via `Manager::filter_settings_fields()` and `ProviderInterface::get_admin_settings_fields()`.


## Frontend Integration

- The Store Contact Form (templates/widgets/store-contact-form.php) contains:
- Hidden input: `<input type="hidden" name="dokan_recaptcha_token" class="dokan_recaptcha_token">`
- Action hook: `do_action( 'dokan_contact_form', $seller_id );`
- The Manager echoes provider markup into the form via the `dokan_contact_form` hook.
- `Assets::enqueue_front_scripts()` asks the Manager to register/enqueue provider assets for store/product pages.
- For Google reCAPTCHA v3, token generation is handled by `window.dokan_execute_recaptcha()` (already included in Dokan helper JS). For Cloudflare Turnstile, the visible widget writes the token into the same hidden field.


## Server-side Validation

The AJAX handler validates the token via the Manager:

```php
$captcha_manager = dokan_get_container()->get( \WeDevs\Dokan\Captcha\Manager::class );
$is_valid = $captcha_manager && $captcha_manager->get_active_provider()
? $captcha_manager->validate( 'dokan_contact_seller_recaptcha', $recaptcha_token )
: false;
```


## Minimal Migration Steps

1) Remove legacy helper function calls
- Stop calling `dokan_get_recaptcha_site_and_secret_keys()`, `dokan_handle_recaptcha_validation()`, `dokan_captcha()`.

2) Resolve the Manager from the container
```php
$manager = dokan_get_container()->get( \WeDevs\Dokan\Captcha\Manager::class );
```

3) Register/enqueue assets through the Manager
```php
$manager->register_assets();
```

4) Render fields/markup via hooks or helper method
- Prefer letting the Manager print provider HTML via `dokan_contact_form`.
- If you need manual rendering:
```php
echo $manager->render_field_html( 'your_context_key' );
```

5) Validate tokens server-side through the Manager
```php
$is_valid = $manager->validate( 'your_context_key', $token );
```

6) Ensure your forms have the hidden token input
- Keep `<input type="hidden" name="dokan_recaptcha_token" class="dokan_recaptcha_token">` or adapt your own input name and pass the value to `$manager->validate()`.

7) Configure the provider in Admin settings
- Choose a provider and supply credentials. Ensure `captcha_enable_status` is on.


## Example: Custom Form Integration

Server-side controller:
```php
$manager = dokan_get_container()->get( \WeDevs\Dokan\Captcha\Manager::class );
if ( $manager && $manager->get_active_provider() ) {
$token = isset( $_POST['dokan_recaptcha_token'] ) ? wp_unslash( $_POST['dokan_recaptcha_token'] ) : '';
if ( ! $manager->validate( 'my_custom_form', $token ) ) {
wp_send_json_error( 'Captcha validation failed' );
}
}
```

View (form):
```php
<?php do_action( 'dokan_contact_form', 0 ); // reuses provider injection ?>
<input type="hidden" name="dokan_recaptcha_token" class="dokan_recaptcha_token">
```

Assets (when rendering the page):
```php
$manager->register_assets();
```


## Extending: Add a New Provider

1) Implement `ProviderInterface` (or extend `AbstractProvider`)
- `get_slug()`: unique slug
- `get_label()`: human-readable name
- `is_ready()`: handled by `AbstractProvider` via `compute_readiness()`
- `register_assets()`: enqueue/register scripts
- `render_field_html()`: output visible widget markup if needed
- `validate( $context, $token )`: return true/false after remote verification
- `get_admin_settings_fields()`: settings array merged into Appearance

2) Register the provider via filter
```php
add_filter( 'dokan_captcha_providers', function( $providers ) {
$providers[] = \Vendor\Package\MyProvider::class; // or new instance
return $providers;
});
```

3) Optional: implement `WeDevs\Dokan\Contracts\Hookable` and register hooks if needed.


## Backward Compatibility Notes

- Default provider falls back to Google reCAPTCHA v3 if none is selected.
- Existing hidden field name `dokan_recaptcha_token` is retained.
- The JS helper `window.dokan_execute_recaptcha()` still exists and is used for the v3 flow.


## Troubleshooting

- Provider not working
- Verify `captcha_enable_status` is on and credentials are present.
- Check that your active provider is selected.
- Ensure the form contains the hidden token field and that `Manager->register_assets()` has been called on that page.

- Token missing/empty
- For reCAPTCHA v3, confirm `dokan-google-recaptcha` is enqueued and `grecaptcha.execute` runs before submit.
- For Turnstile, confirm the widget renders and the callback fills the hidden field.

- Validation fails repeatedly
- For reCAPTCHA v3, ensure the action/context key used in `validate()` matches the action used in `grecaptcha.execute()`.
- Ensure the server has outbound connectivity to Google/Cloudflare APIs.


## References

- Providers
- `includes/Captcha/ProviderInterface.php`
- `includes/Captcha/AbstractProvider.php`
- `includes/Captcha/Providers/GoogleRecaptchaV3Provider.php`
- `includes/Captcha/Providers/CloudflareTurnstileProvider.php`
- Manager and integration points
- `includes/Captcha/Manager.php`
- `includes/Assets.php` (calls `Manager->register_assets()`)
- `includes/Ajax.php` (validates tokens via Manager)
- Container/Service Providers
- `includes/DependencyManagement/Providers/CaptchaServiceProvider.php`
- `includes/DependencyManagement/Providers/ServiceProvider.php`


---
36 changes: 0 additions & 36 deletions includes/Admin/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -858,42 +858,6 @@ public function get_settings_fields() {
],
],
],
'recaptcha_validation_label' => [
'name' => 'recaptcha_validation_label',
'type' => 'social',
'desc' => sprintf(
/* translators: 1) Opening anchor tag, 2) Closing anchor tag, 3) Opening anchor tag, 4) Closing anchor tag */
__( '%1$sreCAPTCHA_v3%2$s credentials required to enable invisible captcha for contact forms. %3$sGet Help%4$s', 'dokan-lite' ),
'<a href="https://developers.google.com/recaptcha/docs/v3" target="_blank" rel="noopener noreferrer">',
'</a>',
'<a href="https://wedevs.com/docs/dokan/settings/dokan-recaptacha-v3-integration" target="_blank" rel="noopener noreferrer">',
'</a>'
),
'label' => __( 'Google reCAPTCHA Validation', 'dokan-lite' ),
'icon_url' => DOKAN_PLUGIN_ASSEST . '/images/google.svg',
'social_desc' => __( 'You can successfully connect to your Google reCaptcha account from here.', 'dokan-lite' ),
'enable_status' => [
'name' => 'recaptcha_enable_status',
'default' => 'on',
],
'recaptcha_site_key' => [
'name' => 'recaptcha_site_key',
'type' => 'text',
'label' => __( 'Site Key', 'dokan-lite' ),
'tooltip' => __( 'Insert Google reCAPTCHA v3 site key.', 'dokan-lite' ),
'social_field' => true,
'is_lite' => true,
],
'recaptcha_secret_key' => [
'name' => 'recaptcha_secret_key',
'label' => __( 'Secret Key', 'dokan-lite' ),
'type' => 'text',
'tooltip' => __( 'Insert Google reCAPTCHA v3 secret key.', 'dokan-lite' ),
'social_field' => true,
'is_lite' => true,
],
'is_lite' => true,
],
'contact_seller' => [
'name' => 'contact_seller',
'label' => __( 'Show Contact Form on Store Page', 'dokan-lite' ),
Expand Down
10 changes: 5 additions & 5 deletions includes/Ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,12 +344,12 @@
wp_send_json_error( $message );
}

// Validate recaptcha if site key and secret key exist
if ( dokan_get_recaptcha_site_and_secret_keys( true ) ) {
$recaptcha_keys = dokan_get_recaptcha_site_and_secret_keys();
$recaptcha_validate = dokan_handle_recaptcha_validation( 'dokan_contact_seller_recaptcha', $recaptcha_token, $recaptcha_keys['secret_key'] );
// Validate captcha using the selected provider if available
$captcha_manager = dokan_get_container()->get( \WeDevs\Dokan\Captcha\Manager::class );
if ( $captcha_manager && $captcha_manager->get_active_provider() ) {
$is_valid = $captcha_manager->validate( 'dokan_contact_seller_recaptcha', $recaptcha_token );

if ( empty( $recaptcha_validate ) ) {
if ( empty( $is_valid ) ) {
$message = sprintf( $error_template, __( 'reCAPTCHA verification failed!', 'dokan-lite' ) );
wp_send_json_error( $message );
}
Expand Down Expand Up @@ -949,7 +949,7 @@
*
* @return int attachment ID
*/
final public function insert_attachment( $object, $cropped ) {

Check warning on line 952 in includes/Ajax.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

It is recommended not to use reserved keyword "object" as function parameter name. Found: $object
$attachment_id = wp_insert_attachment( $object, $cropped );
$metadata = wp_generate_attachment_metadata( $attachment_id, $cropped );
$metadata = apply_filters( 'wp_header_image_attachment_metadata', $metadata );
Expand Down
18 changes: 2 additions & 16 deletions includes/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
public function get_localized_price() {
return [
'precision' => wc_get_price_decimals(),
'symbol' => html_entity_decode( get_woocommerce_currency_symbol() ),

Check failure on line 166 in includes/Assets.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

The default value of the $flags parameter for html_entity_decode() was changed from ENT_COMPAT to ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 in PHP 8.1. For cross-version compatibility, the $flags parameter should be explicitly set.
'decimal' => esc_attr( wc_get_price_decimal_separator() ),
'thousand' => esc_attr( wc_get_price_thousand_separator() ),
'position' => esc_attr( get_option( 'woocommerce_currency_pos' ) ),
Expand Down Expand Up @@ -440,12 +440,6 @@
'src' => $asset_url . '/vendors/date-range-picker/daterangepicker.min.js',
'deps' => [ 'jquery', 'moment', 'dokan-util-helper' ],
],
'dokan-google-recaptcha' => [
'src' => 'https://www.google.com/recaptcha/api.js?render=' . dokan_get_option( 'recaptcha_site_key', 'dokan_appearance' ),
'deps' => [ 'dokan-util-helper' ],
'in_footer' => false,
],

// customize scripts
'customize-base' => [
'src' => site_url( 'wp-includes/js/customize-base.js' ),
Expand Down Expand Up @@ -856,17 +850,9 @@
wp_enqueue_script( 'dokan-vendor-address' );
}

// Scripts for contact form widget google recaptcha
// Scripts for contact form captcha (provider managed)
if ( dokan_is_store_page() || is_product() ) {
// Checks if recaptcha site key and secret key exist
if ( dokan_get_recaptcha_site_and_secret_keys( true ) ) {
$recaptcha_keys = dokan_get_recaptcha_site_and_secret_keys();

wp_enqueue_script( 'dokan-google-recaptcha' );

// Localized script for recaptcha
wp_localize_script( 'dokan-google-recaptcha', 'dokan_google_recaptcha', [ 'recaptcha_sitekey' => $recaptcha_keys['site_key'] ] );
}
dokan_get_container()->get( \WeDevs\Dokan\Captcha\Manager::class )->register_assets();
}

// localized form validate script
Expand Down
93 changes: 93 additions & 0 deletions includes/Captcha/AbstractProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
namespace WeDevs\Dokan\Captcha;

use WeDevs\Dokan\Contracts\Hookable;

/**
* Base captcha provider.
*
* Provides shared helpers such as option access and readiness caching for
* concrete captcha providers.
*
* @since DOKAN_SINCE
*/
abstract class AbstractProvider implements ProviderInterface, Hookable {
/** Cached readiness */
protected ?bool $ready = null;

/** Convenience: get option from dokan appearance */
protected function get_option( string $key, $default = '' ) {

Check warning on line 19 in includes/Captcha/AbstractProvider.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

It is recommended not to use reserved keyword "default" as function parameter name. Found: $default
return function_exists( 'dokan_get_option' ) ? dokan_get_option( $key, 'dokan_appearance', $default ) : $default;
}

/**
* Whether this provider is ready to be used.
*
* Implements lazy cached readiness and delegates the actual check to
* compute_readiness().
*
* @return bool True if ready, false otherwise.
*/
public function is_ready(): bool {
if ( null !== $this->ready ) {
return $this->ready;
}
$this->ready = $this->compute_readiness();
return $this->ready;
}


/**
* Compute provider readiness.
*
* Concrete providers must implement their own rules to decide whether they
* are ready to operate (e.g., enabled plus required credentials provided).
*
* @return bool
*/
abstract protected function compute_readiness(): bool;

/**
* Register hooks for the provider.
*
* This method is responsible for registering the necessary hooks and actions
* required for the provider's functionality. Default implementation is empty.
*
* @return void
*/
public function register_hooks(): void {
add_filter( 'dokan_captcha_providers', [ $this, 'enlist' ] );
}

/**
* Add the current provider to the list of providers.
*
* This method appends the current provider instance to the provided list of providers.
*
* @param array $providers The existing list of providers.
*
* @return array The updated list of providers with the current provider added.
*/
public function enlist( array $providers ): array {
$providers[] = $this;

return $providers;
}

/**
* Convert a truthy-ish value to boolean.
*
* @param mixed $value Value to evaluate.
*
* @return bool
*/
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 );
}
}
Loading
Loading