-
-
Save DavePodosyan/b4e6f0a261ce5c7ed3b30b0734d56291 to your computer and use it in GitHub Desktop.
<?php | |
/** | |
* | |
* A simple integration of Cloudflare Turnstile with Elementor Forms, following Elementor’s pattern for reCAPTCHA. | |
* | |
* Instructions: | |
* 1. Add this file to your WordPress theme directory. | |
* 2. Include the file in your theme's `functions.php` file using: | |
* | |
* // For child themes: | |
* require_once get_stylesheet_directory() . '/elementor-form-turnstile-handler.php'; | |
* | |
* // For parent themes: | |
* require_once get_template_directory() . '/elementor-form-turnstile-handler.php'; | |
* | |
* 3. Go to WordPress Dashboard > Elementor > Settings > Integrations > Cloudflare Turnstile | |
* 4. Enter your Turnstile Site Key and Secret Key. | |
* 5. Edit your Elementor form: | |
* - Add a new **Cloudflare Turnstile** field to your form (similar to adding a reCAPTCHA field). | |
* - Save the form. | |
* | |
* | |
* @author @DavePodosyan | |
* @version 1.1.1 | |
* | |
* * Changelog: | |
* 1.1.0 - Switched to explicit rendering mode for better popup/modal compatibility | |
* 1.0.0 - Initial release with basic Turnstile integration mimicking Elementor's reCAPTCHA logic | |
* | |
*/ | |
use Elementor\Settings; | |
use ElementorPro\Core\Utils; | |
use ElementorPro\Plugin; | |
if (! defined('ABSPATH')) { | |
exit; // Exit if accessed directly | |
} | |
/** | |
* Integration with Cloudflare Turnstile | |
*/ | |
class Turnstile_Handler | |
{ | |
const OPTION_NAME_SITE_KEY = 'elementor_pro_cf_turnstile_site_key'; | |
const OPTION_NAME_SECRET_KEY = 'elementor_pro_cf_turnstile_secret_key'; | |
protected static function get_turnstile_name() | |
{ | |
return 'cf_turnstile'; | |
} | |
public static function get_site_key() | |
{ | |
return get_option(self::OPTION_NAME_SITE_KEY); | |
} | |
public static function get_secret_key() | |
{ | |
return get_option(self::OPTION_NAME_SECRET_KEY); | |
} | |
public static function get_turnstile_type() | |
{ | |
return 'managed'; | |
} | |
public static function is_enabled() | |
{ | |
return static::get_site_key() && static::get_secret_key(); | |
} | |
public static function get_setup_message() | |
{ | |
return esc_html__('To use Cloudflare Turnstile, you need to add the API Key and complete the setup process in Dashboard > Elementor > Settings > Integrations > Claudflare Turnstile.', 'elementor-pro'); | |
} | |
public function register_admin_fields(Settings $settings) | |
{ | |
$settings->add_section(Settings::TAB_INTEGRATIONS, static::get_turnstile_name(), [ | |
'label' => esc_html__('Cloudflare Turnstile', 'elementor-pro'), | |
'callback' => function () { | |
echo sprintf( | |
/* translators: 1: Link opening tag, 2: Link closing tag. */ | |
esc_html__('%1$sCloudflare Turnstile%2$s is Cloudflare\'s CAPTCHA alternative solution where your users don\'t ever have to solve another puzzle to get to your website, no more stop lights and fire hydrants.', 'elementor-pro'), | |
'<a href="https://www.cloudflare.com/application-services/products/turnstile/" target="_blank">', | |
'</a>' | |
); | |
}, | |
'fields' => [ | |
'pro_cf_turnstile_site_key' => [ | |
'label' => esc_html__('Site Key', 'elementor-pro'), | |
'field_args' => [ | |
'type' => 'text', | |
], | |
], | |
'pro_cf_turnstile_secret_key' => [ | |
'label' => esc_html__('Secret Key', 'elementor-pro'), | |
'field_args' => [ | |
'type' => 'text', | |
], | |
], | |
], | |
]); | |
} | |
public function localize_settings($settings) | |
{ | |
$settings = array_replace_recursive($settings, [ | |
'forms' => [ | |
static::get_turnstile_name() => [ | |
'enabled' => static::is_enabled(), | |
'type' => static::get_turnstile_type(), | |
'site_key' => static::get_site_key(), | |
'setup_message' => static::get_setup_message(), | |
], | |
], | |
]); | |
return $settings; | |
} | |
protected static function get_script_name() | |
{ | |
return 'elementor-' . static::get_turnstile_name() . '-api'; | |
} | |
protected static function get_inline_script_name() | |
{ | |
return 'elementor-' . static::get_turnstile_name() . '-inline-handler'; | |
} | |
public function register_scripts() | |
{ | |
wp_register_script( | |
static::get_script_name(), | |
'https://challenges.cloudflare.com/turnstile/v0/api.js', | |
[], | |
null, | |
true | |
); | |
wp_register_script(static::get_inline_script_name(), '', ['elementor-frontend'], null, true); | |
} | |
public function enqueue_scripts() | |
{ | |
if (Plugin::elementor()->preview->is_preview_mode()) { | |
return; | |
} | |
wp_enqueue_script(static::get_script_name()); | |
wp_enqueue_script(static::get_inline_script_name()); | |
wp_add_inline_script( | |
static::get_inline_script_name(), | |
<<<JS | |
if (typeof window.ElementorTurnstileHandler === 'undefined') { | |
class TurnstileHandler extends elementorModules.frontend.handlers.Base { | |
getDefaultSettings() { | |
return { | |
selectors: { | |
turnstile: '.elementor-cf-turnstile:last', | |
submit: 'button[type="submit"]' | |
} | |
}; | |
} | |
getDefaultElements() { | |
const selectors = this.getDefaultSettings().selectors; | |
const \$turnstile = this.\$element.find(selectors.turnstile); | |
const \$form = \$turnstile.parents('form'); | |
const \$submit = \$form.find(selectors.submit); | |
return { \$turnstile, \$form, \$submit }; | |
} | |
bindEvents() { | |
this.waitForTurnstile(); | |
} | |
waitForTurnstile() { | |
if (window.turnstile && typeof window.turnstile.render === 'function') { | |
this.renderTurnstile(); | |
} else { | |
setTimeout(() => this.waitForTurnstile(), 350); | |
} | |
} | |
renderTurnstile() { | |
const el = this.elements.\$turnstile[0]; | |
if (!el || el.dataset.turnstileRendered === 'true') return; | |
if (!jQuery(el).is(':visible')) { | |
setTimeout(() => this.renderTurnstile(), 200); | |
return; | |
} | |
el.dataset.turnstileRendered = 'true'; | |
const sitekey = this.elements.\$turnstile.data('sitekey'); | |
const widgetId = window.turnstile.render(el, { | |
sitekey: sitekey, | |
callback: (token) => { | |
let \$input = this.elements.\$form.find('[name="cf-turnstile-response"]'); | |
if (!\$input.length) { | |
\$input = jQuery('<input>', { | |
type: 'hidden', | |
name: 'cf-turnstile-response' | |
}).appendTo(this.elements.\$form); | |
} | |
\$input.val(token); | |
} | |
}); | |
this.elements.\$form.on('reset error', () => { | |
window.turnstile.reset(widgetId); | |
}); | |
} | |
} | |
window.ElementorTurnstileHandler = TurnstileHandler; | |
} | |
jQuery( window ).on( 'elementor/frontend/init', () => { | |
elementorFrontend.elementsHandler.attachHandler( 'form', window.ElementorTurnstileHandler ); | |
}); | |
JS | |
); | |
} | |
/** | |
* @param Form_Record $record | |
* @param Ajax_Handler $ajax_handler | |
*/ | |
public function validation($record, $ajax_handler) | |
{ | |
$fields = $record->get_field([ | |
'type' => static::get_turnstile_name(), | |
]); | |
if (empty($fields)) { | |
return; | |
} | |
$field = current($fields); | |
// PHPCS - response protected by recaptcha secret | |
$recaptcha_response = Utils::_unstable_get_super_global_value($_POST, 'cf-turnstile-response'); // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
if (empty($recaptcha_response)) { | |
$ajax_handler->add_error($field['id'], esc_html__('The Captcha field cannot be blank. Please enter a value.', 'elementor-pro')); | |
return; | |
} | |
$recaptcha_errors = [ | |
'missing-input-secret' => esc_html__('The secret parameter is missing.', 'elementor-pro'), | |
'invalid-input-secret' => esc_html__('The secret parameter is invalid or malformed.', 'elementor-pro'), | |
'missing-input-response' => esc_html__('The response parameter is missing.', 'elementor-pro'), | |
'invalid-input-response' => esc_html__('The response parameter is invalid or malformed.', 'elementor-pro'), | |
]; | |
$recaptcha_secret = static::get_secret_key(); | |
$client_ip = Utils::get_client_ip(); | |
$request = [ | |
'body' => [ | |
'secret' => $recaptcha_secret, | |
'response' => $recaptcha_response, | |
'remoteip' => $client_ip, | |
], | |
]; | |
$response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', $request); | |
$response_code = wp_remote_retrieve_response_code($response); | |
if (200 !== (int) $response_code) { | |
/* translators: %d: Response code. */ | |
$ajax_handler->add_error($field['id'], sprintf(esc_html__('Can not connect to the Cloudflare Turnstile server (%d).', 'elementor-pro'), $response_code)); | |
return; | |
} | |
$body = wp_remote_retrieve_body($response); | |
$result = json_decode($body, true); | |
if (! $this->validate_result($result, $field)) { | |
$message = esc_html__('Invalid form, Cloudflare Turnstile validation failed.', 'elementor-pro'); | |
if (isset($result['error-codes'])) { | |
$result_errors = array_flip($result['error-codes']); | |
foreach ($recaptcha_errors as $error_key => $error_desc) { | |
if (isset($result_errors[$error_key])) { | |
$message = $recaptcha_errors[$error_key]; | |
break; | |
} | |
} | |
} | |
$this->add_error($ajax_handler, $field, $message); | |
} | |
// If success - remove the field form list (don't send it in emails and etc ) | |
$record->remove_field($field['id']); | |
} | |
/** | |
* @param Ajax_Handler $ajax_handler | |
* @param $field | |
* @param $message | |
*/ | |
protected function add_error($ajax_handler, $field, $message) | |
{ | |
$ajax_handler->add_error($field['id'], $message); | |
} | |
protected function validate_result($result, $field) | |
{ | |
if (! $result['success']) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* @param $item | |
* @param $item_index | |
* @param $widget Widget_Base | |
*/ | |
public function render_field($item, $item_index, $widget) | |
{ | |
$recaptcha_html = '<div class="elementor-field" id="form-field-' . $item['custom_id'] . '">'; | |
$recaptcha_name = static::get_turnstile_name(); | |
if (static::is_enabled()) { | |
$this->enqueue_scripts(); | |
$this->add_render_attributes($item, $item_index, $widget); | |
$recaptcha_html .= '<div ' . $widget->get_render_attribute_string($recaptcha_name . $item_index) . ' style="min-height:65px"></div><style>.elementor-cf-turnstile > div {display:flex;}</style>'; | |
} elseif (current_user_can('manage_options')) { | |
$recaptcha_html .= '<div class="elementor-alert elementor-alert-info">'; | |
$recaptcha_html .= static::get_setup_message(); | |
$recaptcha_html .= '</div>'; | |
} | |
$recaptcha_html .= '</div>'; | |
// PHPCS - It's all escaped | |
echo $recaptcha_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
} | |
/** | |
* @param $item | |
* @param $item_index | |
* @param $widget Widget_Base | |
*/ | |
protected function add_render_attributes($item, $item_index, $widget) | |
{ | |
$recaptcha_name = static::get_turnstile_name(); | |
$widget->add_render_attribute([ | |
$recaptcha_name . $item_index => [ | |
'class' => 'elementor-cf-turnstile', | |
'data-sitekey' => static::get_site_key(), | |
'data-type' => static::get_turnstile_type(), | |
], | |
]); | |
$this->add_version_specific_render_attributes($item, $item_index, $widget); | |
} | |
/** | |
* @param $item | |
* @param $item_index | |
* @param $widget Widget_Base | |
*/ | |
protected function add_version_specific_render_attributes($item, $item_index, $widget) | |
{ | |
$recaptcha_name = static::get_turnstile_name(); | |
$widget->add_render_attribute($recaptcha_name . $item_index, [ | |
'data-theme' => 'light', | |
'data-size' => 'flexible', | |
]); | |
} | |
public function add_field_type($field_types) | |
{ | |
$field_types['cf_turnstile'] = esc_html__('Cloudflare Turnstile', 'elementor-pro'); | |
return $field_types; | |
} | |
public function filter_field_item($item) | |
{ | |
if (static::get_turnstile_name() === $item['field_type']) { | |
$item['field_label'] = false; | |
} | |
return $item; | |
} | |
public function __construct() | |
{ | |
$this->register_scripts(); | |
add_filter('elementor_pro/forms/field_types', [$this, 'add_field_type']); | |
add_action('elementor_pro/forms/render_field/' . static::get_turnstile_name(), [$this, 'render_field'], 10, 3); | |
add_filter('elementor_pro/forms/render/item', [$this, 'filter_field_item']); | |
add_filter('elementor_pro/editor/localize_settings', [$this, 'localize_settings']); | |
if (static::is_enabled()) { | |
add_action('elementor_pro/forms/validation', [$this, 'validation'], 10, 2); | |
add_action('elementor/preview/enqueue_scripts', [$this, 'enqueue_scripts']); | |
} | |
if (is_admin()) { | |
add_action('elementor/admin/after_create_settings/' . Settings::PAGE_ID, [$this, 'register_admin_fields']); | |
} | |
} | |
} | |
add_action('elementor/init', function () { | |
new Turnstile_Handler(); | |
}); |
@DavePodosyan I did not test with your version, I tested with mine. The first thing I did was switch to explicit rendering and the error still occurred (our main form is hidden in a popup).
If you run explicit rendering on a page that has a form in a modal/popup, the HTML element is in the DOM, but it is not visible. Implement one, load the page, wait (don't use the popup), and you will receive error 300030.
This is a limitation imposed by turnstile for some reason, but apparently a well known one. So while you want your implementation to mirror Elementor's recaptcha, that seems like a goal not worth seeking. What seems worth seeking is equivalent functionality.
To sprinkle further pain: pop the form, close the form, pop the form again - more errors. The code I have provided solves all of these cases. It requires explicit rendering, tracking initialization state of the element, and in the case of pop/close/pop executing a turnstile.remove
then turnstile.render
.
TL;DR I have tested this extensively over the past few days.
I'll repeat the requirements I uncovered and added to the linked discussion:
So, a universal implementation:
- needs to use turnstile
explicit
mode - needs to reconnect
MutationObserver
s to track multiple potential popup forms (as well as pop/close/pop scenario). This should reconnect via therender(callback)
in case user interaction occurs - needs to track init/rendered state on the html element (pop/close/pop scenario)
- needs to track init/rendered id's on page state (memory) so that before re-render, a
turnstile.remove()
is executed
Can the PHP file be integrated into a mu-plugins ? In this case, no need to have : require_once get_stylesheet_directory() . '/elementor-form-turnstile-handler.php';
I simply add them into code snippets plugin.
@rosskevin
Just to be sure we’re on the same page:
Have you actually tested the updated v1.1.0 version (where I switched to explicit rendering, and only initialize when the element is visible)? And it did not work for you?
If you’re seeing a failure with the latest version, I’d be interested to know the steps so I can reproduce it, otherwise I believe the current structure handles the pop-up cases well without unnecessary complexity.