Skip to content

Commit

Permalink
Add CloudFlare Turnstile Captcha
Browse files Browse the repository at this point in the history
  • Loading branch information
sonvnn committed Dec 13, 2024
1 parent d3ddcb2 commit 5d33b27
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 78 deletions.
23 changes: 14 additions & 9 deletions framework/elements/formbuilder/ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

use Joomla\CMS\Factory;
use Joomla\CMS\Mail\MailerFactoryInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Language\Text;
use Joomla\Utilities\IpHelper;
use Astroid\Helper;
use Astroid\Framework;
$mainframe = Factory::getApplication();
Expand Down Expand Up @@ -45,7 +45,7 @@
$mail = Factory::getContainer()->get(MailerFactoryInterface::class)->createMailer();
$message = $params->get('email_body', '');
$email_headers = $params->get('email_headers', '');
$gcaptcha = $mainframe->input->post->get('g-recaptcha-response');

$pluginParams = Helper::getPluginParams('captcha', 'astroidcaptcha');

foreach ($asformbuilder as $field => $value) {
Expand All @@ -57,20 +57,27 @@
if (intval($params->get('enable_captcha', 0))) {
$captcha_type = $pluginParams->get('captcha_type', 'default');
$invalidCaptchaMessage = Text::_('ASTROID_AJAX_ERROR_INVALID_CAPTCHA');
if ($captcha_type == 'recaptcha' || $captcha_type == 'recaptcha_invisible') {
if ($captcha_type == 'recaptcha') {
$gcaptcha = $mainframe->input->post->get('g-recaptcha-response');
if (empty($gcaptcha) || !Helper\Captcha::verifyGoogleCaptcha($gcaptcha)) {
throw new \Exception($invalidCaptchaMessage);
}
} elseif ($captcha_type == 'turnstile') {
$token = $mainframe->input->post->get('cf-turnstile-response');
if (empty($token) || !Helper\Captcha::verifyCloudFlareTurnstile($token)) {
throw new \Exception($invalidCaptchaMessage);
}
} elseif (!Helper\Captcha::getCaptcha('as-formbuilder-captcha')) {
throw new \Exception($invalidCaptchaMessage);
}
}
//get sender UP
$senderip = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
$senderip = IpHelper::getIp();
// Subject Structure
$site_name = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '';
$mail_subject = $params->get('email_subject', '') . ' - ' . $site_name;
$mail_subject = preg_replace_callback('/\{\{(\S+?)\}\}/siU', function ($matches) use (&$asformbuilder, &$site_name) {
$config = Factory::getApplication()->getConfig();
$site_name = $config->get( 'sitename', '' );
$mail_subject = $params->get('email_subject', '');
$mail_subject = preg_replace_callback('/\{\{(\S+?)\}\}/siU', function ($matches) use (&$asformbuilder, &$site_name) {
if (isset($asformbuilder[$matches[1]])) {
return $asformbuilder[$matches[1]];
} elseif ($matches[1] == 'site-name') {
Expand All @@ -82,8 +89,6 @@
$mail_body = $message;
$mail_body .= '<p><strong>' . Text::_('ASTROID_FORMBUILDER_SENDER_IP'). '</strong>: ' . $senderip .'</p>';

$config = Factory::getConfig();

$sender = array( $config->get( 'mailfrom' ), $config->get( 'fromname' ) );
$recipient = $config->get( 'mailfrom' );

Expand Down
18 changes: 12 additions & 6 deletions framework/elements/formbuilder/formbuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
Expand Down Expand Up @@ -61,16 +60,21 @@
if ($params->get('enable_captcha', 0) == 1) {
$captcha_type = $pluginParams->get('captcha_type', 'default');
$captcha_attr = ' data-captcha="'.$captcha_type.'"';
if ($captcha_type == 'recaptcha' || $captcha_type == 'recaptcha_invisible') {

if ($captcha_type == 'recaptcha') {
$size = $pluginParams->get('g_size', 'normal');
$captcha_attr .= ' data-sitekey="'.$pluginParams->get('g_site_key', '').'"';
if ($captcha_type == 'recaptcha_invisible') {
$captcha_attr .= ' data-size="'.$size.'"';
if ($size == 'invisible') {
$document->loadGoogleReCaptcha([], 'explicit');
$captcha_attr .= ' data-badge="'.$pluginParams->get('badge', 'bottomright').'"';
} else {
$document->loadGoogleReCaptcha();
}
$captcha_attr .= ' data-tabindex="'.$pluginParams->get('tabindex', '0').'"';
} else if ($captcha_type == 'turnstile') {
$captcha_attr .= ' data-sitekey="'.$pluginParams->get('t_site_key', '').'"';
$captcha_attr .= ' data-size="'.$pluginParams->get('t_size', 'normal').'"';
$document->loadCloudFlareTurnstile();
}
}

Expand Down Expand Up @@ -186,8 +190,10 @@
if ($params->get('enable_captcha', 0) == 1) {
$captcha_type = $pluginParams->get('captcha_type', 'default');
echo '<div class="mt-2">';
if ($captcha_type == 'recaptcha' || $captcha_type == 'recaptcha_invisible') {
if ($captcha_type == 'recaptcha') {
echo '<div class="google-recaptcha"></div>';
} else if ($captcha_type == 'turnstile') {
echo '<div class="cloudflare-turnstile"></div>';
} else {
echo Helper\Captcha::loadCaptcha('as-formbuilder-captcha');
}
Expand All @@ -197,4 +203,4 @@
echo '<div class="as-formbuilder-status mt-4"></div>';
echo '</form>';

Factory::getApplication()->getDocument()->getWebAssetManager()->registerAndUseScript('astroid.formbuilder', "astroid/formbuilder.min.js", ['relative' => true, 'version' => 'auto'], [], ['jquery']);
Factory::getApplication()->getDocument()->getWebAssetManager()->registerAndUseScript('astroid.formbuilder', "astroid/formbuilder.min.js", ['relative' => true, 'version' => 'auto']);
27 changes: 26 additions & 1 deletion framework/library/astroid/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -1042,7 +1042,9 @@ public function loadGoogleReCaptcha($onload = [], $render = ''): void
}
if (!empty($onload['url'])) {
$wa->registerAndUseScript('google.recaptcha.onload', $onload['url'], [], ['defer' => true]);
$query[] = 'onload=' . $onload['function'];
if (!empty($onload['function'])) {
$query[] = 'onload=' . $onload['function'];
}
$depends[] = 'google.recaptcha.onload';
}
if (!empty($render)) {
Expand All @@ -1053,6 +1055,29 @@ public function loadGoogleReCaptcha($onload = [], $render = ''): void
.implode('&',$query), [], ['defer' => true], $depends);
}

public function loadCloudFlareTurnstile($onload = [], $render = ''): void
{
$app = Factory::getApplication();
$wa = $app->getDocument()->getWebAssetManager();
$query = array();
$depends = [];
if (empty($onload)) {
$onload = ['url' => '', 'function' => ''];
}
if (!empty($onload['url'])) {
$wa->registerAndUseScript('cloudflare.turnstile.onload', $onload['url'], [], ['defer' => true]);
if (!empty($onload['function'])) {
$query[] = 'onload=' . $onload['function'];
}
$depends[] = 'cloudflare.turnstile.onload';
}
if (!empty($render)) {
$query[] = 'render=' . $render;
}
$query = !empty($query) ? '?' . implode('&',$query) : '';
$wa->registerAndUseScript('cloudflare.turnstile', 'https://challenges.cloudflare.com/turnstile/v0/api.js'.$query, [], ['defer' => true], $depends);
}

public function moveFile(&$array, $a, $b): void
{
$out = array_splice($array, $a, 1);
Expand Down
37 changes: 37 additions & 0 deletions framework/library/astroid/Helper/Captcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,41 @@ public static function verifyGoogleCaptcha($gRecaptchaResponse, $secretKey = '',
}
return true;
}

public static function verifyCloudFlareTurnstile($turnstileResponse, $secretKey = '') {
$app = Factory::getApplication();
if (empty($secretKey)) {
$pluginParams = Helper::getPluginParams('captcha', 'astroidcaptcha');
$secretKey = $pluginParams->get('t_secret_key', '');
}
if (empty($secretKey)) {
throw new \RuntimeException($app->getLanguage()->_('ASTROID_GOOGLE_TURNSTILE_ERROR_NO_PRIVATE_KEY'));
}
$remoteip = IpHelper::getIp();
// Check for IP
if (empty($remoteip)) {
throw new \RuntimeException($app->getLanguage()->_('ASTROID_GOOGLE_TURNSTILE_ERROR_NO_IP'));
}
if (empty($turnstileResponse)) {
throw new \RuntimeException($app->getLanguage()->_('ASTROID_GOOGLE_RECAPTCHA_ERROR_EMPTY_SOLUTION'));
}
$response = file_get_contents("https://challenges.cloudflare.com/turnstile/v0/siteverify", false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-type: application/x-www-form-urlencoded',
'content' => http_build_query([
'secret' => $secretKey,
'response' => $turnstileResponse,
'remoteip' => $remoteip
]),
],
]));

$result = json_decode($response, true);
if ($result['success']) {
return true;
} else {
return false;
}
}
}
85 changes: 52 additions & 33 deletions js/formbuilder.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
class FormBuilder {
constructor(el) {
this.el = el;
if (this.el.dataset.captcha === 'recaptcha' || this.el.dataset.captcha === 'recaptcha_invisible') {
if (this.el.dataset.captcha === 'recaptcha') {
this.initReCaptcha();
} else if (this.el.dataset.captcha === 'turnstile') {
this.initTurnstile();
}
this.el.querySelector('.as-form-builer-submit').addEventListener('click', this.onSubmit.bind(this));
}
Expand All @@ -15,66 +17,83 @@ class FormBuilder {
let config = {
'sitekey': this.el.dataset.sitekey,
'tabindex': this.el.dataset.tabindex,
'size': this.el.dataset.size,
'theme': color
}
if (this.el.dataset.captcha === 'recaptcha_invisible') {
if (this.el.dataset.size === 'invisible') {
config['badge'] = this.el.dataset.badge;
config['size'] = 'invisible';
config['callback'] = this.onCallAjax.bind(this);
}
grecaptcha.ready(() => {
grecaptcha.render(this.el.querySelector('.google-recaptcha'), config);
});
}

initTurnstile() {
let color = 'light';
if (typeof ASTROID_COLOR_MODE !== 'undefined') {
color = ASTROID_COLOR_MODE;
}
let config = {
'sitekey': this.el.dataset.sitekey,
'size': this.el.dataset.size,
'theme': color
}
turnstile.render(this.el.querySelector('.cloudflare-turnstile'), config);
}

onSubmit() {
if (!this.el.checkValidity()) {
this.el.classList.add('was-validated');
return;
}
if (this.el.dataset.captcha === 'recaptcha_invisible') {
if (this.el.dataset.captcha === 'recaptcha' && this.el.dataset.size === 'invisible') {
grecaptcha.execute();
} else {
this.onCallAjax();
}
}

onCallAjax(token) {
var request = {},
$this = jQuery(this.el),
data = $this.serializeArray();
let form = this.el;
let formData = new FormData(form);
let id = Date.now() * 1000 + Math.random() * 1000;
id = id.toString(16).replace(/\./g, "").padEnd(14, "0")+Math.trunc(Math.random() * 100000000);
for (let i = 0; i < data.length; i++) {
request[data[i]['name']] = data[i]['value'];
}
request[$this.find('.token').attr('name')] = 1;
jQuery.ajax({
type : 'POST',
url : $this.attr('action')+'&t='+id,
data : request,
beforeSend: function(){
$this.find('.as-formbuilder-status').empty();
$this.find('.as-form-builer-submit').attr('disabled', 'disabled');
},
success: function (response) {
id = id.toString(16).replace(/\./g, "").padEnd(14, "0") + Math.trunc(Math.random() * 100000000);

var xhr = new XMLHttpRequest();
xhr.open('POST', form.action + '&t=' + id, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
var response = JSON.parse(xhr.responseText);
var statusElement = form.querySelector('.as-formbuilder-status');
statusElement.innerHTML = '';
form.querySelector('.as-form-builer-submit').disabled = false;
form.classList.remove('was-validated');

if (response.status === 'success') {
$this.find('.as-formbuilder-status').append('<div class="alert alert-success" role="alert">'+response.message+'</div>');
$this.trigger("reset");
$this.find('.google-recaptcha').each(function(){
grecaptcha.reset(this);
})
statusElement.innerHTML = '<div class="alert alert-success" role="alert">' + response.message + '</div>';
form.reset();
form.querySelectorAll('.google-recaptcha').forEach(function(el) {
grecaptcha.reset(el);
});
form.querySelectorAll('.cloudflare-turnstile').forEach(function(el) {
turnstile.reset(el);
});
} else {
$this.find('.as-formbuilder-status').append('<div class="alert alert-danger" role="alert">'+response.message+'</div>');
statusElement.innerHTML = '<div class="alert alert-danger" role="alert">' + response.message + '</div>';
}
$this.find('.as-form-builer-submit').removeAttr('disabled');
$this.removeClass('was-validated');
}
});
};

xhr.send(new URLSearchParams(formData).toString());
form.querySelector('.as-formbuilder-status').innerHTML = '';
form.querySelector('.as-form-builer-submit').disabled = true;
}
}
jQuery(function($) {
$('.as-form-builder').each(function() {
new FormBuilder(this);
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.as-form-builder').forEach(function(el) {
new FormBuilder(el);
});
});
2 changes: 1 addition & 1 deletion js/formbuilder.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 5d33b27

Please sign in to comment.