| | |
| | | <?php |
| | | |
| | | namespace JVBase\integrations\services; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\integrations\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class Cloudflare extends Integrations |
| | | { |
| | | private string $site_key; |
| | | private string $secret_key; |
| | | use Admin, Instance; |
| | | private string $theme = 'light'; |
| | | private string $size = 'normal'; |
| | | |
| | | protected static array $instances = []; |
| | | public static function getInstance(?int $userID = null):self |
| | | { |
| | | $key = is_null($userID) ? 'base' : $userID; |
| | | if (!array_key_exists($key, self::$instances)) { |
| | | self::$instances[$key] = new self($userID); |
| | | } |
| | | return self::$instances[$key]; |
| | | } |
| | | |
| | | protected function __construct() |
| | | { |
| | | $this->service_name = 'cloudflare'; |
| | | $this->title = 'Cloudflare Turnstile'; |
| | | $this->title = 'Cloudflare'; |
| | | $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->defineFields(); |
| | | $this->defineInstructions(); |
| | | parent::__construct(); |
| | | } |
| | | |
| | | protected function defineFields():void |
| | | { |
| | | $site_key = $this->addField('site_key', 'Site Key', 'text', 'manage_options'); |
| | | $site_key->setRequired(); |
| | | $site_key->setHint('Public key for Turnstile Widget'); |
| | | $secret_key = $this->addField('secret_key', 'Secret Key', 'text', 'manage_options'); |
| | | $secret_key->setRequired(); |
| | | $secret_key->setSubType('password'); |
| | | $secret_key->setHint('Secret key for server-side verification of Turnstile Widget'); |
| | | } |
| | | |
| | | protected function defineInstructions():void |
| | | { |
| | | $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->theme = $this->credentials['theme'] ?? 'light'; |
| | | $this->size = $this->credentials['size'] ?? 'normal'; |
| | | |
| | | // TODO: Implement initialize() method. |
| | | } |
| | | |
| | | /** |
| | | * Check if scripts should be enqueued |
| | | */ |
| | | public function shouldEnqueueScripts(): bool |
| | | public function getServiceDescription():string |
| | | { |
| | | return $this->isSetUp() && !is_admin(); |
| | | return "Add Turnstile captcha protection to your forms and login pages to prevent spam and bot submissions."; |
| | | } |
| | | |
| | | /** |
| | | * Test connection by attempting a verification with an invalid token |
| | | */ |
| | | public function performConnectionTest(): bool |
| | | { |
| | | if (empty($this->site_key) || empty($this->secret_key)) { |
| | | $credentials = $this->loadCredentials(); |
| | | if (!array_key_exists('site_key', $credentials) || !array_key_exists('secret_key', $credentials)) { |
| | | 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, |
| | | 'secret' => $credentials['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); |
| | | } |
| | | |
| | | 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 |
| | | */ |
| | |
| | | /** |
| | | * Render Turnstile widget |
| | | * Can be called directly: jvbConnect('cloudflare')->renderTurnstile() |
| | | * Or Cloudflare::getInstance()->renderTurnstile() |
| | | */ |
| | | public function renderTurnstile(array|string $options = []): void |
| | | { |
| | | $this->ensureInitialized(); |
| | | if (!$this->isSetUp()) { |
| | | return; |
| | | } |
| | |
| | | 'wrapper_style' => 'margin: 1em 0;' |
| | | ]; |
| | | |
| | | $cred = $this->loadCredentials(); |
| | | $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>'; |
| | | echo sprintf( |
| | | '<div class="%s" style="%s"> |
| | | <div class="cf-turnstile" data-sitekey="%s" data-theme="%s" data-size="%s"></div> |
| | | </div>', |
| | | esc_attr($options['wrapper_class']), |
| | | esc_attr($options['wrapper_style']), |
| | | $cred['site_key'], |
| | | $options['theme'], |
| | | $options['size'] |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Verify Turnstile response |
| | | * Can be called directly: JVB()->connect('cloudflare')->verifyTurnstile($token) |
| | | * Verify the Turnstile response |
| | | * Can be called directly via JVB()->connect('cloudflare')->verifyTurnstile($token) |
| | | * or Cloudflare::getInstance()->verifyTurnstile($token) |
| | | * @param string|null $token |
| | | * @param string $remote_ip |
| | | * @return bool |
| | | */ |
| | | public function verifyTurnstile(?string $token = null, string $remote_ip = ''): bool |
| | | public function verifyTurnstile(?string $token = null, string $remote_ip = ''):bool |
| | | { |
| | | $this->ensureInitialized(); |
| | | if (!$this->isSetUp()) { |
| | | return false; |
| | | return true; |
| | | } |
| | | |
| | | // If no token provided, try to get from POST |
| | | // If no token provided, try to get it from $_POST |
| | | if (!$token && isset($_POST['cf-turnstile-response'])) { |
| | | $token = sanitize_text_field($_POST['cf-turnstile-response']); |
| | | } |
| | |
| | | return false; |
| | | } |
| | | |
| | | $cred = $this->loadCredentials(); |
| | | $data = [ |
| | | 'secret' => $this->secret_key, |
| | | 'response' => $token |
| | | 'secret' => $cred['secret_key'], |
| | | 'response' => $token, |
| | | ]; |
| | | |
| | | if (empty($remote_ip) && isset($_SERVER['REMOTE_ADDR'])) { |
| | | $remote_ip = $_SERVER['REMOTE_ADDR']; |
| | | $remote_ip = sanitize_text_field($_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()); |
| | | $this->logError('verifyTurnstile', 'Turnstile verification failed: ' . $response->get_error_message()); |
| | | return false; |
| | | } |
| | | |
| | | $result = json_decode(wp_remote_retrieve_body($response), true); |
| | | |
| | | if (!($result['success'] ?? false)) { |
| | | if (!$result['success']??false) { |
| | | $error_codes = implode(', ', $result['error-codes'] ?? ['Unknown error']); |
| | | $this->logError('Turnstile verification failed: ' . $error_codes); |
| | | $this->logError('verifyTurnstile', 'Turnstile verification failed: ' . $error_codes); |
| | | } |
| | | |
| | | return $result['success'] ?? false; |
| | | } |
| | | |
| | | /** |
| | | * 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 []; |
| | | } |
| | | } |