settings = $settings; } public function is_configured(): bool { return ! empty( $this->settings['cf_site_key'] ) && ! empty( $this->settings['cf_secret_key'] ); } public function get_html( array $args ): string { $style = sprintf( 'margin:%dpx %dpx %dpx %dpx', $args['margin']['top'], $args['margin']['right'], $args['margin']['bottom'], $args['margin']['left'] ); return sprintf( '
', esc_attr( $args['instance'] ), esc_attr( $style ), esc_attr( $args['action'] ) ); } public function get_opt_in_text(): string { return sprintf( wp_kses( /* Translators: 1: CloudFlare's privacy policy URL, 2: CloudFlare's terms of use URL */ __( 'For security, use of CloudFlare\'s Turnstile service is required which is subject to the CloudFlare Privacy Policy and Terms of Use.', 'it-l10n-ithemes-security-pro' ), [ 'a' => [ 'href' => [] ] ] ), 'https://www.cloudflare.com/privacypolicy/', 'https://www.cloudflare.com/website-terms/' ); } public function register_assets(): void { wp_register_script( \ITSEC_Recaptcha::HANDLE, plugins_url( 'js/turnstile.js', __DIR__ ), [ 'jquery' ], 4 ); wp_localize_script( \ITSEC_Recaptcha::HANDLE, 'itsecRecaptcha', [ 'config' => [ 'sitekey' => $this->settings['cf_site_key'], 'theme' => $this->settings['cf_theme'], 'size' => $this->settings['cf_size'], ], ] ); } public function get_sdk_url( bool $load_immediately ): string { $script = 'https://challenges.cloudflare.com/turnstile/v0/api.js'; $query_args = [ 'render' => 'explicit', ]; if ( $load_immediately ) { $query_args['onload'] = $this->get_js_load_function(); } return add_query_arg( $query_args, $script ); } public function get_js_load_function(): string { return 'itsecCloudFlareTurnstileLoad'; } public function validate( array $args ): Result { if ( ! empty( $args['token'] ) ) { $input_response = $args['token']; } else if ( ! empty( $_POST['cf-turnstile-response'] ) ) { $input_response = $_POST['cf-turnstile-response']; } else { return Result::error( new \WP_Error( 'itsec.recaptcha.form-not-submitted', esc_html__( 'You must submit the CAPTCHA to proceed. Please try again.', 'it-l10n-ithemes-security-pro' ), compact( 'args' ) ) ); } $response = wp_remote_post( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', [ 'body' => [ 'secret' => $this->settings['cf_secret_key'], 'response' => $input_response, 'remoteip' => \ITSEC_Lib::get_ip(), ] ] ); if ( is_wp_error( $response ) ) { $http_error = new \WP_Error( 'itsec.recaptcha.http-error', __( 'Cannot connect to CloudFlare Turnstile API.', 'it-l10n-ithemes-security-pro' ) ); $http_error->merge_from( $response ); return Result::error( $http_error ); } $body = wp_remote_retrieve_body( $response ); $status = json_decode( $body, true ); if ( ! $this->is_valid_response_format( $status ) ) { return Result::error( new \WP_Error( 'itsec.recaptcha.invalid-response', __( 'The CloudFlare Turnstile API returned an invalid response format.', 'it-l10n-ithemes-security-pro' ), [ 'response' => $body ] ) ); } $validation_error = $this->validate_response( $status, $args ); if ( ! $validation_error ) { return Result::success( true ); } if ( ! $status['error-codes'] ) { return Result::error( $validation_error ); } if ( [ 'invalid-input-secret' ] === $status['error-codes'] ) { return Result::error( new \WP_Error( 'itsec.recaptcha.invalid-api-keys', __( 'The CloudFlare API keys are invalid.', 'it-l10n-ithemes-security-pro' ), [ 'report_to_admin' => true ] ) ); } foreach ( $status['error-codes'] as $error_code ) { $validation_error->add( sprintf( 'itsec.recaptcha.cloudflare.%s', $error_code ), sprintf( __( 'The CloudFlare Turnstile server reported the following error: %s.', 'it-l10n-ithemes-security-pro' ), $error_code ), [ 'report_to_admin' => ! in_array( $error_code, self::IGNORED_ERRORS, true ) ] ); } return Result::error( $validation_error ); } /** * Is the CAPTCHA response from CloudFlare valid. * * @param mixed $response * * @return bool */ private function is_valid_response_format( $response ): bool { if ( ! is_array( $response ) ) { return false; } if ( ! isset( $response['success'] ) ) { return false; } if ( $response['success'] && ! isset( $response['action'] ) ) { return false; } return true; } /** * Validate the response. * * @param array $response The response from CloudFlare. * @param array $args The args passed by the user. * * @return \WP_Error|null */ private function validate_response( array $response, array $args ): ?\WP_Error { \ITSEC_Log::add_debug( 'recaptcha', 'validate-response', compact( 'response', 'args' ) ); $error = new \WP_Error( 'itsec.recaptcha.incorrect', esc_html__( 'The CAPTCHA you submitted does not appear to be valid. Please try again.', 'it-l10n-ithemes-security-pro' ) ); if ( ! $response['success'] ) { $error->add_data( [ 'validate_error' => 'invalid-token', 'args' => $args ] ); return $error; } if ( ! $this->validate_action( $response, $args ) ) { $error->add_data( [ 'validate_error' => 'action-mismatch', 'args' => $args ] ); return $error; } return null; } /** * Validates that the action matches. * * @param array $status Response from CloudFlare. * @param array $args Validation args. * * @return bool */ private function validate_action( array $status, array $args ): bool { return empty( $args['action'] ) || $status['action'] === $args['action']; } }