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 Cloudflare Dashboard', '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); // If we get a response with success field (even if false), connection works return array_key_exists('success', $result); } /** * 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 '
'; } /** * 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', 'ERROR: 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', 'ERROR: 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', 'ERROR: 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'; } }