Created
October 10, 2022 08:46
-
-
Save kagg-design/0e6d59da1557a662717f22f27cab3ed1 to your computer and use it in GitHub Desktop.
Turnstile integration with Quform
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Turnstile class file. | |
* | |
* @package wtei-core | |
*/ | |
namespace WTEICore; | |
use JsonException; | |
use Quform_Element_Page; | |
use Quform_Form; | |
/** | |
* Class Turnstile | |
*/ | |
class Turnstile { | |
/** | |
* Site key. | |
* | |
* @var string | |
*/ | |
private string $site_key; | |
/** | |
* Secret key. | |
* | |
* @var string | |
*/ | |
private string $secret_key; | |
/** | |
* Form shown. | |
* | |
* @var bool | |
*/ | |
private bool $form_shown = false; | |
/** | |
* Init class. | |
*/ | |
public function init(): void { | |
add_action( 'plugins_loaded', [ $this, 'hooks' ] ); | |
} | |
/** | |
* Add hooks. | |
* | |
* @return void | |
*/ | |
public function hooks(): void { | |
if ( ! class_exists( Quform::class, false ) ) { | |
return; | |
} | |
$this->site_key = defined( 'TURNSTILE_SITE_KEY' ) ? TURNSTILE_SITE_KEY : ''; | |
$this->secret_key = defined( 'TURNSTILE_SECRET_KEY' ) ? TURNSTILE_SECRET_KEY : ''; | |
add_action( 'do_shortcode_tag', [ $this, 'add_turnstile' ], 10, 4 ); | |
add_action( 'wp_print_footer_scripts', [ $this, 'print_footer_scripts' ], 0 ); | |
add_filter( 'quform_pre_validate', [ $this, 'validate' ], 10, 2 ); | |
} | |
/** | |
* Filters the output created by a shortcode callback and adds turnstile. | |
* | |
* @param string $output Shortcode output. | |
* @param string $tag Shortcode name. | |
* @param array|string $attr Shortcode attributes array or empty string. | |
* @param array $m Regular expression match array. | |
* | |
* @return string | |
* @noinspection PhpMissingParamTypeInspection | |
*/ | |
public function add_turnstile( string $output, string $tag, $attr, $m ): string { | |
if ( 'quform' !== $tag ) { | |
return $output; | |
} | |
$this->form_shown = true; | |
$max_id = '9999_9999'; | |
if ( preg_match_all( '/quform-element-(\d+?)_(\d+)\D/', $output, $m ) ) { | |
$element_ids = array_map( 'intval', array_unique( $m[2] ) ); | |
$max_id = $m[1][0] . '_' . ( max( $element_ids ) + 1 ); | |
} | |
ob_start(); | |
?> | |
<div class="quform-element quform-element-turnstile quform-element-<?php echo esc_attr( $max_id ); ?> quform-cf quform-element-required quform-turnstile-no-size"> | |
<div class="quform-spacer"> | |
<div class="quform-inner quform-inner-turnstile quform-inner-<?php echo esc_attr( $max_id ); ?>"> | |
<div class="quform-input quform-input-turnstile quform-input-<?php echo esc_attr( $max_id ); ?> quform-cf"> | |
<div | |
class="cf-turnstile quform-turnstile" | |
data-sitekey="<?php echo esc_attr( $this->site_key ); ?>"> | |
</div> | |
<noscript><?php esc_html_e( 'Please enable JavaScript to submit this form.', 'wtei' ); ?></noscript> | |
</div> | |
</div> | |
</div> | |
</div> | |
<?php | |
$turnstile = ob_get_clean(); | |
return preg_replace( '/(<div class="quform-element quform-element-submit)/', $turnstile . '$1', $output ); | |
} | |
/** | |
* Print footer scripts. | |
* | |
* @return void | |
*/ | |
public function print_footer_scripts(): void { | |
if ( ! $this->form_shown ) { | |
return; | |
} | |
// phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript | |
?> | |
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> | |
<?php | |
// phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedScript | |
} | |
/** | |
* Validate Turnstile. | |
* | |
* @param array $result Result. | |
* @param Quform_Form $form Form. | |
* | |
* @return array | |
* @throws JsonException JsonException. | |
*/ | |
public function validate( array $result, Quform_Form $form ): array { | |
$page = $form->getCurrentPage(); | |
$page_id = $page ? $page->getId() : 0; | |
$turnstile_name = $this->get_max_element_id( $page ); | |
$turnstile_error = [ | |
'type' => 'error', | |
'error' => | |
[ | |
'enabled' => false, | |
'title' => '', | |
'content' => '', | |
], | |
'errors' => [ $turnstile_name => '' ], | |
'page' => $page_id, | |
]; | |
// Nonce is verified by QuForm. | |
// phpcs:disable WordPress.Security.NonceVerification.Missing | |
$response = isset( $_POST['cf-turnstile-response'] ) ? | |
sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) ) : | |
''; | |
// phpcs:enable WordPress.Security.NonceVerification.Missing | |
$validation = wp_remote_post( | |
'https://challenges.cloudflare.com/turnstile/v0/siteverify', | |
[ | |
'body' => [ | |
'secret' => $this->secret_key, | |
'response' => $response, | |
], | |
] | |
); | |
if ( is_wp_error( $validation ) ) { | |
$turnstile_error['errors'] = [ $turnstile_name => $validation->get_error_message() ]; | |
return $turnstile_error; | |
} | |
$body = json_decode( $validation['body'], true, 512, JSON_THROW_ON_ERROR ); | |
if ( ! $body['success'] ) { | |
$turnstile_error['errors'] = [ $turnstile_name => $this->get_turnstile_error_message( $body['error-codes'] ) ]; | |
return $turnstile_error; | |
} | |
return $result; | |
} | |
/** | |
* Get max form element id. | |
* | |
* @param Quform_Element_Page|null $page Current page. | |
* | |
* @return string | |
*/ | |
private function get_max_element_id( ?Quform_Element_Page $page ): string { | |
$max_id = '9999_9999'; | |
if ( null === $page ) { | |
return $max_id; | |
} | |
$ids = array_map( | |
static function ( $element ) { | |
return $element->getId(); | |
}, | |
$page->getElements() | |
); | |
return $page->getForm()->getId() . '_' . ( max( $ids ) + 1 ); | |
} | |
/** | |
* Get turnstile error message. | |
* | |
* @param array $error_codes Error codes. | |
* | |
* @return string | |
*/ | |
private function get_turnstile_error_message( array $error_codes ): string { | |
$errors = [ | |
'missing-input-secret' => __( 'The secret parameter was not passed.', 'wtei' ), | |
'invalid-input-secret' => __( 'The secret parameter was invalid or did not exist.', 'wtei' ), | |
'missing-input-response' => __( 'The response parameter was not passed.', 'wtei' ), | |
'invalid-input-response' => __( 'The response parameter is invalid or has expired.', 'wtei' ), | |
'bad-request' => __( 'The request was rejected because it was malformed.', 'wtei' ), | |
'timeout-or-duplicate' => __( 'The response parameter has already been validated before.', 'wtei' ), | |
'internal-error' => __( 'An internal error happened while validating the response. The request can be retried.', 'wtei' ), | |
]; | |
$message_arr = []; | |
foreach ( $error_codes as $error_code ) { | |
if ( array_key_exists( $error_code, $errors ) ) { | |
$message_arr[] = $errors[ $error_code ]; | |
} | |
} | |
$header = _n( 'Turnstile error:', 'Turnstile errors:', count( $message_arr ), 'wtei' ); | |
return $header . ' ' . implode( '; ', $message_arr ); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment