Add code to display the invisible ReCaptcha in the frontend

This commit adds the required code to display the invisible ReCaptcha in
the frontend when a form is rendered and this type of captcha is
selected in the admin.

[MAILPOET-4145]
This commit is contained in:
Rodrigo Primo
2022-04-18 14:51:47 -03:00
committed by Veljko V
parent 84e22b82bd
commit c1bd52b964
6 changed files with 152 additions and 83 deletions

View File

@@ -77,6 +77,76 @@ jQuery(($) => {
});
}
function submitSubscribeForm(form, formData, parsley) {
form.addClass('mailpoet_form_sending');
// ajax request
MailPoet.Ajax.post({
url: window.MailPoetForm.ajax_url,
token: formData.token,
api_version: formData.api_version,
endpoint: 'subscribers',
action: 'subscribe',
data: formData.data,
})
.fail((response) => {
if (
response.meta !== undefined &&
response.meta.redirect_url !== undefined
) {
// go to page
window.top.location.href = response.meta.redirect_url;
} else {
if (response.meta && response.meta.refresh_captcha) {
updateCaptcha();
}
form
.find('.mailpoet_validate_error')
.html(response.errors.map((error) => error.message).join('<br />'))
.show();
}
})
.done((response) => {
if (window.grecaptcha && formData.recaptcha) {
window.grecaptcha.reset(formData.recaptcha);
}
return response;
})
.done((response) => {
// successfully subscribed
if (
response.meta !== undefined &&
response.meta.redirect_url !== undefined
) {
setFormCookieAfterSubscription(form);
// go to page
window.location.href = response.meta.redirect_url;
} else {
displaySuccessMessage(form);
}
// reset form
form.trigger('reset');
// reset validation
parsley.reset();
// reset captcha
if (window.grecaptcha && formData.recaptcha) {
window.grecaptcha.reset(formData.recaptcha);
}
// resize iframe
if (
window.frameElement !== null &&
MailPoet !== undefined &&
MailPoet.Iframe
) {
MailPoet.Iframe.autoSize(window.frameElement);
}
})
.always(() => {
form.removeClass('mailpoet_form_sending');
});
}
function renderCaptcha(element, iteration) {
if (!window.recaptcha || !window.grecaptcha.ready) {
if (iteration < 20) {
@@ -85,12 +155,31 @@ jQuery(($) => {
return;
}
const recaptcha = $(element);
const form = $(recaptcha).parent('form');
const sitekey = recaptcha.attr('data-sitekey');
let size = recaptcha.attr('data-size');
// Users should not be able to change the size if it is equal to 'invisible' as this would
// change the type of the ReCaptcha.
if (size !== 'invisible') {
size = Hooks.applyFilters('mailpoet_re_captcha_size', 'compact');
}
const container = recaptcha.find('> .mailpoet_recaptcha_container').get(0);
const field = recaptcha.find('> .mailpoet_recaptcha_field');
if (sitekey) {
const size = Hooks.applyFilters('mailpoet_re_captcha_size', 'compact');
const widgetId = window.grecaptcha.render(container, { sitekey, size });
const params = { sitekey, size };
if (size === 'invisible') {
params.callback = function invisibleReCaptchaCallback(token) {
const formData = form.mailpoetSerializeObject() || {};
formData.data.recaptcha = token;
submitSubscribeForm(form, formData, form.parsley());
};
}
const widgetId = window.grecaptcha.render(container, params);
field.val(widgetId);
}
}
@@ -299,88 +388,36 @@ jQuery(($) => {
}, 2500);
return false;
}
const formData = form.mailpoetSerializeObject() || {};
// check if we're on the same domain
if (isSameDomain(window.MailPoetForm.ajax_url) === false) {
// non ajax post request
return true;
}
const formData = form.mailpoetSerializeObject() || {};
const size = form.children('.mailpoet_recaptcha').attr('data-size');
if (window.grecaptcha && formData.recaptcha) {
// The API for the invisible and checkbox ReCaptchas is slightly different. For the
// former, we need to call execute() and then the ReCaptcha API calls the callback set
// inside renderCaptcha() with a token if the captcha was solved successfully. The
// callback then calls submitSubscribeForm() with the token. For the latter, we get the
// token here after calling getResponse() and then we can call submitSubscribeForm()
// directly.
if (size === 'invisible') {
window.grecaptcha.execute();
} else {
formData.data.recaptcha = window.grecaptcha.getResponse(
formData.recaptcha,
);
}
form.addClass('mailpoet_form_sending');
// ajax request
MailPoet.Ajax.post({
url: window.MailPoetForm.ajax_url,
token: formData.token,
api_version: formData.api_version,
endpoint: 'subscribers',
action: 'subscribe',
data: formData.data,
})
.fail((response) => {
if (
response.meta !== undefined &&
response.meta.redirect_url !== undefined
) {
// go to page
window.top.location.href = response.meta.redirect_url;
} else {
if (response.meta && response.meta.refresh_captcha) {
updateCaptcha();
}
form
.find('.mailpoet_validate_error')
.html(
response.errors.map((error) => error.message).join('<br />'),
)
.show();
}
})
.done((response) => {
if (window.grecaptcha && formData.recaptcha) {
window.grecaptcha.reset(formData.recaptcha);
}
return response;
})
.done((response) => {
// successfully subscribed
if (
response.meta !== undefined &&
response.meta.redirect_url !== undefined
) {
setFormCookieAfterSubscription(form);
// go to page
window.location.href = response.meta.redirect_url;
} else {
displaySuccessMessage(form);
}
// reset form
form.trigger('reset');
// reset validation
parsley.reset();
// reset captcha
if (window.grecaptcha && formData.recaptcha) {
window.grecaptcha.reset(formData.recaptcha);
if (size !== 'invisible') {
submitSubscribeForm(form, formData, parsley);
}
// resize iframe
if (
window.frameElement !== null &&
MailPoet !== undefined &&
MailPoet.Iframe
) {
MailPoet.Iframe.autoSize(window.frameElement);
}
})
.always(() => {
form.removeClass('mailpoet_form_sending');
});
return false;
});
});

View File

@@ -33,7 +33,7 @@ class AssetsController {
public function printScripts() {
ob_start();
$captcha = $this->settings->get('captcha');
if (!empty($captcha['type']) && $captcha['type'] === Captcha::TYPE_RECAPTCHA) {
if (!empty($captcha['type']) && Captcha::isReCaptcha($captcha['type'])) {
echo '<script src="' . self::RECAPTCHA_API_URL . '" async defer></script>';
}
@@ -62,7 +62,7 @@ class AssetsController {
public function setupFrontEndDependencies() {
$captcha = $this->settings->get('captcha');
if (!empty($captcha['type']) && $captcha['type'] === Captcha::TYPE_RECAPTCHA) {
if (!empty($captcha['type']) && Captcha::isRecaptcha($captcha['type'])) {
$this->wp->wpEnqueueScript(
'mailpoet_recaptcha',
self::RECAPTCHA_API_URL

View File

@@ -70,7 +70,7 @@ class Renderer {
if (
$captchaEnabled
&& $block['type'] === FormEntity::SUBMIT_BLOCK_TYPE
&& $this->settings->get('captcha.type') === Captcha::TYPE_RECAPTCHA
&& Captcha::isRecaptcha($this->settings->get('captcha.type'))
) {
$html .= $this->renderReCaptcha();
}
@@ -89,8 +89,15 @@ class Renderer {
}
private function renderReCaptcha(): string {
if ($this->settings->get('captcha.type') === Captcha::TYPE_RECAPTCHA) {
$siteKey = $this->settings->get('captcha.recaptcha_site_token');
return '<div class="mailpoet_recaptcha" data-sitekey="' . $siteKey . '">
$size = '';
} else {
$siteKey = $this->settings->get('captcha.recaptcha_invisible_site_token');
$size = 'invisible';
}
$html = '<div class="mailpoet_recaptcha" data-sitekey="' . $siteKey . '" ' . ($size === 'invisible' ? 'data-size="invisible"' : '') . '>
<div class="mailpoet_recaptcha_container"></div>
<noscript>
<div>
@@ -108,5 +115,7 @@ class Renderer {
</noscript>
<input class="mailpoet_recaptcha_field" type="hidden" name="recaptcha">
</div>';
return $html;
}
}

View File

@@ -216,15 +216,21 @@ class SubscriberSubscribeController {
}
}
if ($captchaSettings['type'] === Captcha::TYPE_RECAPTCHA && empty($data['recaptcha'])) {
if (Captcha::isReCaptcha($captchaSettings['type']) && empty($data['recaptcha'])) {
return ['error' => __('Please check the CAPTCHA.', 'mailpoet')];
}
if ($captchaSettings['type'] === Captcha::TYPE_RECAPTCHA) {
if (Captcha::isReCaptcha($captchaSettings['type'])) {
if ($captchaSettings['type'] === Captcha::TYPE_RECAPTCHA_INVISIBLE) {
$secretToken = $captchaSettings['recaptcha_invisible_secret_token'];
} else {
$secretToken = $captchaSettings['recaptcha_secret_token'];
}
$response = empty($data['recaptcha']) ? $data['recaptcha-no-js'] : $data['recaptcha'];
$response = $this->wp->wpRemotePost('https://www.google.com/recaptcha/api/siteverify', [
'body' => [
'secret' => $captchaSettings['recaptcha_secret_token'],
'secret' => $secretToken,
'response' => $response,
],
]);

View File

@@ -11,6 +11,7 @@ use MailPoetVendor\Gregwar\Captcha\CaptchaBuilder;
class Captcha {
const TYPE_BUILTIN = 'built-in';
const TYPE_RECAPTCHA = 'recaptcha';
const TYPE_RECAPTCHA_INVISIBLE = 'recaptcha-invisible';
const TYPE_DISABLED = null;
/** @var WPFunctions */
@@ -22,6 +23,10 @@ class Captcha {
/** @var SubscriberIPsRepository */
private $subscriberIPsRepository;
public static function isReCaptcha(?string $captchaType) {
return in_array($captchaType, [self::TYPE_RECAPTCHA, self::TYPE_RECAPTCHA_INVISIBLE]);
}
public function __construct(
SubscriberIPsRepository $subscriberIPsRepository,
WPFunctions $wp = null,

View File

@@ -697,6 +697,18 @@ class SubscribersTest extends \MailPoetTest {
$this->settings->set('captcha', []);
}
public function testItCannotSubscribeWithoutInvisibleReCaptchaWhenEnabled() {
$this->settings->set('captcha', ['type' => Captcha::TYPE_RECAPTCHA_INVISIBLE]);
$response = $this->endpoint->subscribe([
$this->obfuscatedEmail => 'toto@mailpoet.com',
'form_id' => $this->form->getId(),
$this->obfuscatedSegments => [$this->segment1->getId(), $this->segment2->getId()],
]);
expect($response->status)->equals(APIResponse::STATUS_BAD_REQUEST);
expect($response->errors[0]['message'])->equals('Please check the CAPTCHA.');
$this->settings->set('captcha', []);
}
public function testItCannotSubscribeWithoutBuiltInCaptchaWhenEnabled() {
$this->settings->set('captcha', ['type' => Captcha::TYPE_BUILTIN]);
$email = 'toto@mailpoet.com';