Skip to content

Commit

Permalink
Add Google reCaptcha library
Browse files Browse the repository at this point in the history
  • Loading branch information
sonvnn committed Dec 12, 2024
1 parent 711578b commit 14e0342
Show file tree
Hide file tree
Showing 13 changed files with 411 additions and 73 deletions.
4 changes: 2 additions & 2 deletions framework/elements/formbuilder/ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,17 @@
$message = $params->get('email_body', '');
$email_headers = $params->get('email_headers', '');
$gcaptcha = $mainframe->input->post->get('g-recaptcha-response');
$pluginParams = Helper::getPluginParams();
$pluginParams = Helper::getPluginParams('captcha', 'astroidcaptcha');

foreach ($asformbuilder as $field => $value) {
$message = str_replace('{{'.$field.'}}', $value, $message);
$email_headers = str_replace('{{'.$field.'}}', $value, $email_headers);
}
$replyToMail = $replyToName = '';

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 (empty($gcaptcha) || !Helper\Captcha::verifyGoogleCaptcha($gcaptcha)) {
throw new \Exception($invalidCaptchaMessage);
Expand Down
12 changes: 9 additions & 3 deletions framework/elements/formbuilder/formbuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,21 @@

$document = Framework::getDocument();
$show_label = $params->get('show_label', 1);
$pluginParams = Helper::getPluginParams();
$pluginParams = Helper::getPluginParams('captcha', 'astroidcaptcha');
$captcha_attr = '';
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') {
$document->loadGoogleReCaptcha();

$captcha_attr .= ' data-sitekey="'.$pluginParams->get('g_site_key', '').'"';
if ($captcha_type == 'recaptcha_invisible') {
$document->loadGoogleReCaptcha([], 'explicit');
$captcha_attr .= ' data-badge="'.$pluginParams->get('badge', 'bottomright').'"';
} else {
$document->loadGoogleReCaptcha();
}
$captcha_attr .= ' data-tabindex="'.$pluginParams->get('tabindex', '0').'"';
}
}

Expand Down Expand Up @@ -184,7 +191,6 @@
} else {
echo Helper\Captcha::loadCaptcha('as-formbuilder-captcha');
}
echo '<input type="hidden" name="captcha_type" value="'.$captcha_type.'">';
echo '</div>';
}
echo '<button type="button" class="as-form-builer-submit btn ' . $button_class . $button_margin . '">'.$btn_title.'</button>';
Expand Down
22 changes: 19 additions & 3 deletions framework/library/astroid/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -1031,10 +1031,26 @@ public function loadLenis(): void
}
}

public function loadGoogleReCaptcha(): void
public function loadGoogleReCaptcha($onload = [], $render = ''): void
{
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseScript('google.recaptcha', '//www.google.com/recaptcha/api.js', ['relative' => true, 'version' => 'auto']);
$app = Factory::getApplication();
$wa = $app->getDocument()->getWebAssetManager();
$query = array();
$depends = [];
if (empty($onload)) {
$onload = ['url' => '', 'function' => ''];
}
if (!empty($onload['url'])) {
$wa->registerAndUseScript('google.recaptcha.onload', $onload['url'], [], ['defer' => true]);
$query[] = 'onload=' . $onload['function'];
$depends[] = 'google.recaptcha.onload';
}
if (!empty($render)) {
$query[] = 'render=' . $render;
}
$query[] = 'hl=' . $app->getLanguage()->getTag();
$wa->registerAndUseScript('google.recaptcha', '//www.google.com/recaptcha/api.js?'
.implode('&',$query), [], ['defer' => true], $depends);
}

public function moveFile(&$array, $a, $b): void
Expand Down
4 changes: 2 additions & 2 deletions framework/library/astroid/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ public static function loadLanguage($extension, $client = 'site')
$lang->load($extension, ($client == 'site' ? JPATH_SITE : JPATH_ADMINISTRATOR));
}

public static function getPluginParams()
public static function getPluginParams($group = 'system', $plugin = 'astroid')
{
$plugin = PluginHelper::getPlugin('system', 'astroid');
$plugin = PluginHelper::getPlugin($group, $plugin);
return new Registry($plugin->params);
}

Expand Down
50 changes: 25 additions & 25 deletions framework/library/astroid/Helper/Captcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

use Astroid\Helper;
use Joomla\CMS\Factory;
use Joomla\CMS\Captcha\Google\HttpBridgePostRequestMethod;
use ReCaptcha\ReCaptcha;
use ReCaptcha\RequestMethod;
use Joomla\Utilities\IpHelper;

defined('_JEXEC') or die;

Expand All @@ -24,7 +28,7 @@ public static function loadCaptcha($context = '') {
$value2 = rand(1,100);
$app->setUserState( $context.'.value1', $value1 );
$app->setUserState( $context.'.value2', $value2 );
return '<div class="'.$context.'">'.($value1 . ' + ' . $value2 .' = ?').'</div><div class="'.$context.'-result"><input type="text" name="'.$context.'" class="form-control" placeholder="'.($value1 . ' + ' . $value2 .' = ?').'"></div>';
return '<div class="'.$context.'">'.($value1 . ' + ' . $value2 .' = ?').'</div><div class="'.$context.'-result"><input type="text" name="'.$context.'" class="form-control required" placeholder="'.($value1 . ' + ' . $value2 .' = ?').'" required></div>';
}

public static function getCaptcha($context = '') {
Expand All @@ -35,35 +39,31 @@ public static function getCaptcha($context = '') {
return ( $value1 + $value2 == $value_result );
}

public static function verifyGoogleCaptcha($gRecaptchaResponse, $secretKey = '') {
if (empty($gRecaptchaResponse)) {
return false;
}
public static function verifyGoogleCaptcha($gRecaptchaResponse, $secretKey = '', RequestMethod $requestMethod = new HttpBridgePostRequestMethod()) {
$app = Factory::getApplication();
if (empty($secretKey)) {
$pluginParams = Helper::getPluginParams();
$pluginParams = Helper::getPluginParams('captcha', 'astroidcaptcha');
$secretKey = $pluginParams->get('g_secret_key', '');
}
if (empty($secretKey)) {
throw new \RuntimeException($app->getLanguage()->_('ASTROID_GOOGLE_RECAPTCHA_ERROR_NO_PRIVATE_KEY'));
}
$remoteip = IpHelper::getIp();
// Check for IP
if (empty($remoteip)) {
throw new \RuntimeException($app->getLanguage()->_('ASTROID_GOOGLE_RECAPTCHA_ERROR_NO_IP'));
}
if (empty($gRecaptchaResponse)) {
throw new \RuntimeException($app->getLanguage()->_('ASTROID_GOOGLE_RECAPTCHA_ERROR_EMPTY_SOLUTION'));
}
$reCaptcha = new ReCaptcha($secretKey, $requestMethod);
$response = $reCaptcha->verify($gRecaptchaResponse, $remoteip);
if (!$response->isSuccess()) {
foreach ($response->getErrorCodes() as $error) {
throw new \RuntimeException($error);
}
return false;
}
$url = 'https://www.google.com/recaptcha/api/siteverify';
$data = [
'secret' => $secretKey,
'response' => $gRecaptchaResponse
];

$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data),
],
];

$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$response = json_decode($result, true);

return isset($response['success']) && $response['success'] === true;
return true;
}
}
115 changes: 79 additions & 36 deletions js/formbuilder.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,80 @@
jQuery(function($){
if ($('.as-form-builder').length) {
$(document).on('submit', '.as-form-builder' , function (e) {
e.preventDefault();
var request = {},
$this = $(this),
data = $this.serializeArray();
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;
$.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) {
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('.g-recaptcha').each(function(){
grecaptcha.reset(this);
})
} else {
$this.find('.as-formbuilder-status').append('<div class="alert alert-danger" role="alert">'+response.message+'</div>');
}
$this.find('.as-form-builer-submit').removeAttr('disabled');
}
});
});
}
class FormBuilder {
constructor(el) {
this.el = el;
if (this.el.dataset.captcha === 'recaptcha' || this.el.dataset.captcha === 'recaptcha_invisible') {
this.initReCaptcha();
}
this.el.querySelector('.as-form-builer-submit').addEventListener('click', this.onSubmit.bind(this));
}

initReCaptcha() {
let color = 'light';
if (typeof ASTROID_COLOR_MODE !== 'undefined') {
color = ASTROID_COLOR_MODE;
}
let config = {
'sitekey': this.el.dataset.sitekey,
'tabindex': this.el.dataset.tabindex,
'theme': color
}
if (this.el.dataset.captcha === 'recaptcha_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);
});
}

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

onCallAjax(token) {
var request = {},
$this = jQuery(this.el),
data = $this.serializeArray();
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) {
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);
})
} else {
$this.find('.as-formbuilder-status').append('<div class="alert alert-danger" role="alert">'+response.message+'</div>');
}
$this.find('.as-form-builer-submit').removeAttr('disabled');
$this.removeClass('was-validated');
}
});
}
}
jQuery(function($) {
$('.as-form-builder').each(function() {
new FormBuilder(this);
});
});
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.

25 changes: 25 additions & 0 deletions js/recaptcha_invisible.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
((window, document) => {

window.AstroidinitReCaptchaInvisible = () => {
const optionKeys = ['sitekey', 'badge', 'size', 'tabindex', 'callback', 'expired-callback', 'error-callback'];
document.querySelectorAll('.g-recaptcha').forEach(element => {
let options = {};
if (element.dataset) {
options = element.dataset;
} else {
optionKeys.forEach(key => {
const optionKeyFq = `data-${optionKeys[key]}`;
if (element.hasAttribute(optionKeyFq)) {
options[optionKeys[key]] = element.getAttribute(optionKeyFq);
}
});
}

// Set the widget id of the recaptcha item
element.setAttribute('data-recaptcha-widget-id', window.grecaptcha.render(element, options));

// Execute the invisible reCAPTCHA
window.grecaptcha.execute(element.getAttribute('data-recaptcha-widget-id'));
});
};
})(window, document);
1 change: 1 addition & 0 deletions js/recaptcha_invisible.min.js

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

21 changes: 20 additions & 1 deletion language/en-GB/en-GB.astroid.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1188,12 +1188,31 @@ TPL_ASTROID_MINIFYJS_EXCLUDES_LABEL="Exclude JS Files"
TPL_ASTROID_MINIFYJS_EXCLUDES_DESC="Add comma separated filesnames to exclude files from JS minification. You can add filename directly or use available filename patterns: <br/> <code>*.min.js</code><br/> <code>jquery.*</code><br/> <code>*bootstrap*</code>"

; ReCaptcha
COM_PLUGINS_ASTROID_CAPTCHA_FIELDSET_LABEL="Captcha"
ASTROID_RECAPTCHA_NOTE="Google ReCaptcha"
ASTROID_GOOGLE_SITE_KEY="Site Key"
ASTROID_GOOGLE_SITE_KEY_DESC="Use this site key in the HTML code your site serves to users."
ASTROID_GOOGLE_SECRET_KEY="Secret Key"
ASTROID_GOOGLE_SECRET_KEY_DESC="Use this secret key for communication between your site and reCAPTCHA."
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_BADGE_BOTTOMLEFT="Bottom left"
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_BADGE_BOTTOMRIGHT="Bottom right"
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_BADGE_DESC="Positioning of the reCAPTCHA badge."
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_BADGE_INLINE="Inline"
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_BADGE_LABEL="Badge"
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_CALLBACK_DESC="(Optional) JavaScript callback, executed after successful reCAPTCHA response."
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_CALLBACK_LABEL="Callback"
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_ERROR_CALLBACK_DESC="(Optional) JavaScript callback, executed when the reCAPTCHA encounters an error."
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_ERROR_CALLBACK_LABEL="Error Callback"
ASTROID_GOOGLE_RECAPTCHA_ERROR_EMPTY_SOLUTION="Empty solution not allowed."
ASTROID_GOOGLE_RECAPTCHA_ERROR_NO_IP="For security reasons, you must pass the remote IP address to reCAPTCHA."
ASTROID_GOOGLE_RECAPTCHA_ERROR_NO_PRIVATE_KEY="reCAPTCHA plugin needs a secret key to be set in its parameters. Please contact a site administrator."
ASTROID_GOOGLE_RECAPTCHA_ERROR_NO_PUBLIC_KEY="reCAPTCHA plugin needs a site key to be set in its parameters. Please contact a site administrator."
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_EXPIRED_CALLBACK_DESC="(Optional) JavaScript callback, executed when the reCAPTCHA expired."
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_EXPIRED_CALLBACK_LABEL="Expired Callback"
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_PRIVACY_CAPABILITY_IP_ADDRESS="The Invisible reCAPTCHA plugin integrates with Google's reCAPTCHA system as a spam protection service. As part of this service, the IP address of the user answering the captcha challenge is transmitted to Google."
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_PRIVATE_KEY_DESC="Used in the communication between your server and the reCAPTCHA server. Be sure to keep it a secret."
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_PUBLIC_KEY_DESC="Used in the JavaScript code that is served to your users."
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_TABINDEX_DESC="The tabindex of the challenge."
ASTROID_GOOGLE_RECAPTCHA_INVISIBLE_TABINDEX_LABEL="Tabindex"

; Widgets
ASTROID_ELEMENT_CATEGORY_SYSTEM="System"
Expand Down
Loading

0 comments on commit 14e0342

Please sign in to comment.