<?php
|
namespace JVBase\integrations;
|
|
use JVBase\meta\MetaManager;
|
use WP_Error;
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class Cloudflare extends Integrations
|
{
|
private string $site_key;
|
private string $secret_key;
|
private bool $auto_protect_forms = true;
|
private string $theme = 'light';
|
private string $size = 'normal';
|
|
public function __construct()
|
{
|
$this->service_name = 'cloudflare';
|
$this->title = 'Cloudflare Turnstile';
|
$this->icon = 'cloud';
|
$this->apiBase = 'https://challenges.cloudflare.com';
|
$this->apiEndpoints = ['turnstile/v0/siteverify'];
|
|
$this->fields = [
|
'site_key' => [
|
'label' => 'Site Key',
|
'type' => 'text',
|
'placeholder' => 'Enter Public key for Turnstile widget display.',
|
'hint' => 'Public key for Turnstile widget',
|
'required' => true
|
],
|
'secret_key' => [
|
'label' => 'Secret Key',
|
'type' => 'text',
|
'subtype' => 'password',
|
'placeholder' => 'Enter Cloudflare Turnstile Secret Key',
|
'required' => true,
|
'hint' => 'Secret key for server-side verification'
|
]
|
];
|
|
$this->instructions = [
|
'Go to <a href="https://dash.cloudflare.com/?to=/:account/turnstile" target="_blank">Cloudflare Dashboard</a>',
|
'Create a new Turnstile widget',
|
'Add your domain to the widget settings',
|
'Copy the Site Key and Secret Key'
|
];
|
|
$this->advanced = [];
|
parent::__construct();
|
}
|
|
protected function initialize(): void
|
{
|
$this->site_key = $this->credentials['site_key'] ?? '';
|
$this->secret_key = $this->credentials['secret_key'] ?? '';
|
$this->auto_protect_forms = $this->credentials['auto_protect_forms'] ?? true;
|
$this->theme = $this->credentials['theme'] ?? 'light';
|
$this->size = $this->credentials['size'] ?? 'normal';
|
|
if ($this->isSetUp()) {
|
$this->initializeTurnstileProtection();
|
}
|
}
|
|
/**
|
* Check if scripts should be enqueued
|
*/
|
public function shouldEnqueueScripts(): bool
|
{
|
return $this->isSetUp() && !is_admin();
|
}
|
|
/**
|
* Test connection by attempting a verification with an invalid token
|
*/
|
public function performConnectionTest(): bool
|
{
|
if (empty($this->site_key) || empty($this->secret_key)) {
|
return false;
|
}
|
|
// Test with a dummy token to verify API connectivity
|
$response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
'body' => [
|
'secret' => $this->secret_key,
|
'response' => 'test-token-connection-check'
|
],
|
'timeout' => 10
|
]);
|
|
// We expect this to fail with success:false, but not with a connection error
|
if (is_wp_error($response)) {
|
$this->logError('Connection test failed: ', [
|
'message' => $response->get_error_message()
|
]);
|
return false;
|
}
|
|
$result = json_decode(wp_remote_retrieve_body($response), true);
|
|
error_log('Cloudflare Result: '.print_r($result, true));
|
|
// If we get a response with success field (even if false), connection works
|
return array_key_exists('success', $result) && $result['success'];
|
}
|
|
/**
|
* Initialize Turnstile protection on various forms
|
*/
|
private function initializeTurnstileProtection(): void
|
{
|
// Skip on local development if configured
|
if (defined('JVB_LOCAL') && strpos(get_home_url(), JVB_LOCAL) !== false) {
|
return;
|
}
|
|
if (!$this->auto_protect_forms) {
|
return;
|
}
|
|
// WordPress login/registration forms
|
add_action('login_enqueue_scripts', [$this, 'enqueueTurnstileScripts']);
|
add_action('login_form', [$this, 'renderTurnstile']);
|
add_action('register_form', [$this, 'renderTurnstile']);
|
add_action('lostpassword_form', [$this, 'renderTurnstile']);
|
|
// Verification hooks
|
add_filter('authenticate', [$this, 'verifyLoginTurnstile'], 99, 3);
|
add_filter('registration_errors', [$this, 'verifyRegisterTurnstile'], 10, 3);
|
add_action('lostpassword_post', [$this, 'verifyLostpasswordTurnstile']);
|
|
// Custom form support
|
add_action('jvb_form_before_submit', [$this, 'renderTurnstile']);
|
add_filter('jvb_form_validate', [$this, 'validateFormTurnstile'], 10, 2);
|
}
|
|
protected function validateCredentials(array $credentials): bool
|
{
|
if (empty($credentials['site_key'])) {
|
$this->logError('Missing Site Key', [
|
'method' => 'validateCredentials'
|
]);
|
return false;
|
}
|
|
if (empty($credentials['secret_key'])) {
|
$this->logError('Missing Secret Key', [
|
'method' => 'validateCredentials'
|
]);
|
return false;
|
}
|
|
// Validate format
|
if (!preg_match('/^[0-9a-zA-Z._-]+$/', $credentials['site_key'])) {
|
|
$this->logError('Invalid Site Key', [
|
'method' => 'validateCredentials'
|
]);
|
return false;
|
}
|
|
return true;
|
}
|
|
/**
|
* Enqueue Turnstile script
|
*/
|
public function enqueueTurnstileScripts(): void
|
{
|
wp_enqueue_script(
|
'cloudflare-turnstile',
|
'https://challenges.cloudflare.com/turnstile/v0/api.js',
|
[],
|
null,
|
true
|
);
|
wp_script_add_data('cloudflare-turnstile', 'async', true);
|
wp_script_add_data('cloudflare-turnstile', 'defer', true);
|
}
|
|
/**
|
* Render Turnstile widget
|
* Can be called directly: jvbConnect('cloudflare')->renderTurnstile()
|
*/
|
public function renderTurnstile(array|string $options = []): void
|
{
|
$this->ensureInitialized();
|
if (!$this->isSetUp()) {
|
return;
|
}
|
|
if (!is_array($options)) {
|
$options = [];
|
}
|
|
$defaults = [
|
'theme' => $this->theme,
|
'size' => $this->size,
|
'wrapper_class' => 'cf-turnstile-wrapper',
|
'wrapper_style' => 'margin: 1em 0;'
|
];
|
|
$options = wp_parse_args($options, $defaults);
|
|
echo '<div class="' . esc_attr($options['wrapper_class']) . '" style="' . esc_attr($options['wrapper_style']) . '">';
|
echo '<div class="cf-turnstile" ';
|
echo 'data-sitekey="' . esc_attr($this->site_key) . '" ';
|
echo 'data-theme="' . esc_attr($options['theme']) . '" ';
|
echo 'data-size="' . esc_attr($options['size']) . '">';
|
echo '</div>';
|
echo '</div>';
|
}
|
|
/**
|
* Verify Turnstile response
|
* Can be called directly: JVB()->connect('cloudflare')->verifyTurnstile($token)
|
*/
|
public function verifyTurnstile(?string $token = null, string $remote_ip = ''): bool
|
{
|
if (!$this->isSetUp()) {
|
return false;
|
}
|
|
// If no token provided, try to get from POST
|
if (!$token && isset($_POST['cf-turnstile-response'])) {
|
$token = sanitize_text_field($_POST['cf-turnstile-response']);
|
}
|
|
if (empty($token)) {
|
return false;
|
}
|
|
$data = [
|
'secret' => $this->secret_key,
|
'response' => $token
|
];
|
|
if (empty($remote_ip) && isset($_SERVER['REMOTE_ADDR'])) {
|
$remote_ip = $_SERVER['REMOTE_ADDR'];
|
}
|
|
if (!empty($remote_ip)) {
|
$data['remoteip'] = $remote_ip;
|
}
|
|
$response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
'body' => $data,
|
'timeout' => 10
|
]);
|
|
if (is_wp_error($response)) {
|
$this->logError('Turnstile verification failed: ' . $response->get_error_message());
|
return false;
|
}
|
|
$result = json_decode(wp_remote_retrieve_body($response), true);
|
|
if (!($result['success'] ?? false)) {
|
$error_codes = implode(', ', $result['error-codes'] ?? ['Unknown error']);
|
$this->logError('Turnstile verification failed: ' . $error_codes);
|
}
|
|
return $result['success'] ?? false;
|
}
|
|
/**
|
* Verify login form Turnstile
|
*/
|
public function verifyLoginTurnstile($user, string $username, string $password)
|
{
|
// Skip verification if already logged in or no credentials
|
if (is_user_logged_in() || empty($username) || empty($password)) {
|
return $user;
|
}
|
|
// Skip on AJAX requests for compatibility
|
if (wp_doing_ajax()) {
|
return $user;
|
}
|
|
// If already have an error, return it
|
if (is_wp_error($user)) {
|
return $user;
|
}
|
|
// Verify Turnstile
|
if (!$this->verifyTurnstile()) {
|
return new \WP_Error(
|
'turnstile_verification_failed',
|
'<strong>ERROR</strong>: Please complete the security check.'
|
);
|
}
|
|
return $user;
|
}
|
|
/**
|
* Verify registration form Turnstile
|
*/
|
public function verifyRegisterTurnstile($errors, string $sanitized_user_login, string $user_email)
|
{
|
if (!$this->verifyTurnstile()) {
|
$errors->add(
|
'turnstile_verification_failed',
|
'<strong>ERROR</strong>: Please complete the security check.'
|
);
|
}
|
return $errors;
|
}
|
|
/**
|
* Verify lost password form Turnstile
|
*/
|
public function verifyLostpasswordTurnstile($errors): void
|
{
|
if (!$this->verifyTurnstile()) {
|
if (!is_wp_error($errors)) {
|
$errors = new \WP_Error();
|
}
|
$errors->add(
|
'turnstile_verification_failed',
|
'<strong>ERROR</strong>: Please complete the security check.'
|
);
|
}
|
}
|
|
/**
|
* Validate Turnstile for custom JVB forms
|
*/
|
public function validateFormTurnstile(array $errors, array $form_data): array
|
{
|
if (!$this->verifyTurnstile()) {
|
$errors[] = 'Please complete the security verification.';
|
}
|
return $errors;
|
}
|
|
/**
|
* Get site key for frontend use
|
*/
|
public function getSiteKey(): string
|
{
|
return $this->site_key;
|
}
|
|
/**
|
* Get service description
|
*/
|
public function getServiceDescription(): string
|
{
|
return "Add Turnstile captcha protection to your forms and login pages to prevent spam and bot submissions.";
|
}
|
|
/**
|
* These methods are not used by Cloudflare but must be implemented
|
*/
|
protected function getRequestHeaders(): array
|
{
|
// Cloudflare Turnstile uses POST parameters, not headers
|
return [];
|
}
|
|
protected function getApiUrl(string $endpoint, ?string $baseKey = null): string
|
{
|
// Only used for direct API calls, not through base class methods
|
return 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
}
|
}
|