Handle captcha during subscription [MAILPOET-2015]

This commit is contained in:
wxa
2019-07-04 20:19:00 +03:00
committed by M. Shull
parent 08af443c1f
commit b174a55d07
11 changed files with 194 additions and 37 deletions

View File

@ -60,11 +60,22 @@ jQuery(function ($) { // eslint-disable-line func-names
data: formData.data, data: formData.data,
}) })
.fail(function handleFailedPost(response) { .fail(function handleFailedPost(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) {
$('.mailpoet_captcha_update').click();
}
form.find('.mailpoet_validate_error').html( form.find('.mailpoet_validate_error').html(
response.errors.map(function buildErrorMessage(error) { response.errors.map(function buildErrorMessage(error) {
return error.message; return error.message;
}).join('<br />') }).join('<br />')
).show(); ).show();
}
}) })
.done(function handleRecaptcha(response) { .done(function handleRecaptcha(response) {
if (window.grecaptcha && formData.recaptcha) { if (window.grecaptcha && formData.recaptcha) {
@ -83,6 +94,10 @@ jQuery(function ($) { // eslint-disable-line func-names
} else { } else {
// display success message // display success message
form.find('.mailpoet_validate_success').show(); form.find('.mailpoet_validate_success').show();
// hide elements marked with a class
form.find('.mailpoet_form_hide_on_success').each(function hideOnSuccess() {
$(this).hide();
});
} }
// reset form // reset form
@ -109,5 +124,14 @@ jQuery(function ($) { // eslint-disable-line func-names
return false; return false;
}); });
}); });
$('.mailpoet_captcha_update').click(function updateCaptcha(e) {
var captcha = $('img.mailpoet_captcha');
var captchaSrc = captcha.attr('src');
var hashPos = captchaSrc.indexOf('#');
var newSrc = hashPos > 0 ? captchaSrc.substring(0, hashPos) : captchaSrc;
captcha.attr('src', newSrc + '#' + new Date().getTime());
e.preventDefault();
});
}); });
}); });

View File

@ -3,6 +3,7 @@ namespace MailPoet\API\JSON;
use MailPoet\Config\AccessControl; use MailPoet\Config\AccessControl;
use MailPoet\Settings\SettingsController; use MailPoet\Settings\SettingsController;
use MailPoet\Subscription\Captcha;
use MailPoetVendor\Psr\Container\ContainerInterface; use MailPoetVendor\Psr\Container\ContainerInterface;
use MailPoet\Util\Helpers; use MailPoet\Util\Helpers;
use MailPoet\Util\Security; use MailPoet\Util\Security;
@ -79,7 +80,7 @@ class API {
$this->setRequestData($_POST); $this->setRequestData($_POST);
$ignoreToken = ( $ignoreToken = (
$this->settings->get('re_captcha.enabled') && $this->settings->get('captcha.type') != Captcha::TYPE_DISABLED &&
$this->_request_endpoint === 'subscribers' && $this->_request_endpoint === 'subscribers' &&
$this->_request_method === 'subscribe' $this->_request_method === 'subscribe'
); );

View File

@ -18,7 +18,9 @@ use MailPoet\Settings\SettingsController;
use MailPoet\Subscribers\RequiredCustomFieldValidator; use MailPoet\Subscribers\RequiredCustomFieldValidator;
use MailPoet\Subscribers\Source; use MailPoet\Subscribers\Source;
use MailPoet\Subscribers\SubscriberActions; use MailPoet\Subscribers\SubscriberActions;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Throttling as SubscriptionThrottling; use MailPoet\Subscription\Throttling as SubscriptionThrottling;
use MailPoet\Subscription\Url as SubscriptionUrl;
use MailPoet\WP\Functions as WPFunctions; use MailPoet\WP\Functions as WPFunctions;
if (!defined('ABSPATH')) exit; if (!defined('ABSPATH')) exit;
@ -46,6 +48,9 @@ class Subscribers extends APIEndpoint {
/** @var Listing\Handler */ /** @var Listing\Handler */
private $listing_handler; private $listing_handler;
/** @var Captcha */
private $subscription_captcha;
/** @var WPFunctions */ /** @var WPFunctions */
private $wp; private $wp;
@ -58,6 +63,7 @@ class Subscribers extends APIEndpoint {
SubscriberActions $subscriber_actions, SubscriberActions $subscriber_actions,
RequiredCustomFieldValidator $required_custom_field_validator, RequiredCustomFieldValidator $required_custom_field_validator,
Listing\Handler $listing_handler, Listing\Handler $listing_handler,
Captcha $subscription_captcha,
WPFunctions $wp, WPFunctions $wp,
SettingsController $settings SettingsController $settings
) { ) {
@ -66,6 +72,7 @@ class Subscribers extends APIEndpoint {
$this->subscriber_actions = $subscriber_actions; $this->subscriber_actions = $subscriber_actions;
$this->required_custom_field_validator = $required_custom_field_validator; $this->required_custom_field_validator = $required_custom_field_validator;
$this->listing_handler = $listing_handler; $this->listing_handler = $listing_handler;
$this->subscription_captcha = $subscription_captcha;
$this->wp = $wp; $this->wp = $wp;
$this->settings = $settings; $this->settings = $settings;
} }
@ -140,8 +147,6 @@ class Subscribers extends APIEndpoint {
$form = Form::findOne($form_id); $form = Form::findOne($form_id);
unset($data['form_id']); unset($data['form_id']);
$recaptcha = $this->settings->get('re_captcha');
if (!$form instanceof Form) { if (!$form instanceof Form) {
return $this->badRequest([ return $this->badRequest([
APIError::BAD_REQUEST => WPFunctions::get()->__('Please specify a valid form ID.', 'mailpoet'), APIError::BAD_REQUEST => WPFunctions::get()->__('Please specify a valid form ID.', 'mailpoet'),
@ -153,31 +158,17 @@ class Subscribers extends APIEndpoint {
]); ]);
} }
if (!empty($recaptcha['enabled']) && empty($data['recaptcha'])) { $captcha = $this->settings->get('captcha');
return $this->badRequest([
APIError::BAD_REQUEST => WPFunctions::get()->__('Please check the CAPTCHA.', 'mailpoet'),
]);
}
if (!empty($recaptcha['enabled'])) { if (!empty($captcha['type']) && $captcha['type'] === Captcha::TYPE_BUILTIN) {
$res = empty($data['recaptcha']) ? $data['recaptcha-no-js'] : $data['recaptcha']; if (empty($data['captcha'])) {
$res = WPFunctions::get()->wpRemotePost('https://www.google.com/recaptcha/api/siteverify', [ // Save form data to session
'body' => [ $_SESSION[Captcha::SESSION_FORM_KEY] = array_merge($data, ['form_id' => $form_id]);
'secret' => $recaptcha['secret_token'], } elseif (!empty($_SESSION[Captcha::SESSION_FORM_KEY])) {
'response' => $res, // Restore form data from session
], $data = array_merge($_SESSION[Captcha::SESSION_FORM_KEY], ['captcha' => $data['captcha']]);
]);
if (is_wp_error($res)) {
return $this->badRequest([
APIError::BAD_REQUEST => WPFunctions::get()->__('Error while validating the CAPTCHA.', 'mailpoet'),
]);
}
$res = json_decode(wp_remote_retrieve_body($res));
if (empty($res->success)) {
return $this->badRequest([
APIError::BAD_REQUEST => WPFunctions::get()->__('Error while validating the CAPTCHA.', 'mailpoet'),
]);
} }
// Otherwise use the post data
} }
$data = $this->deobfuscateFormPayload($data); $data = $this->deobfuscateFormPayload($data);
@ -201,6 +192,64 @@ class Subscribers extends APIEndpoint {
]); ]);
} }
$is_builtin_captcha_required = false;
if (!empty($captcha['type']) && $captcha['type'] === Captcha::TYPE_BUILTIN) {
$is_builtin_captcha_required = $this->subscription_captcha->isRequired(isset($data['email']) ? $data['email'] : '');
if ($is_builtin_captcha_required && empty($data['captcha'])) {
$meta = [];
$meta['redirect_url'] = SubscriptionUrl::getCaptchaUrl();
return $this->badRequest([
APIError::BAD_REQUEST => WPFunctions::get()->__('Please check the CAPTCHA.', 'mailpoet'),
], $meta);
}
}
if (!empty($captcha['type']) && $captcha['type'] === Captcha::TYPE_RECAPTCHA && empty($data['recaptcha'])) {
return $this->badRequest([
APIError::BAD_REQUEST => WPFunctions::get()->__('Please check the CAPTCHA.', 'mailpoet'),
]);
}
if (!empty($captcha['type'])) {
if ($captcha['type'] === Captcha::TYPE_RECAPTCHA) {
$res = empty($data['recaptcha']) ? $data['recaptcha-no-js'] : $data['recaptcha'];
$res = WPFunctions::get()->wpRemotePost('https://www.google.com/recaptcha/api/siteverify', [
'body' => [
'secret' => $captcha['recaptcha_secret_token'],
'response' => $res,
],
]);
if (is_wp_error($res)) {
return $this->badRequest([
APIError::BAD_REQUEST => WPFunctions::get()->__('Error while validating the CAPTCHA.', 'mailpoet'),
]);
}
$res = json_decode(wp_remote_retrieve_body($res));
if (empty($res->success)) {
return $this->badRequest([
APIError::BAD_REQUEST => WPFunctions::get()->__('Error while validating the CAPTCHA.', 'mailpoet'),
]);
}
} elseif ($captcha['type'] === Captcha::TYPE_BUILTIN && $is_builtin_captcha_required) {
if (empty($_SESSION[Captcha::SESSION_KEY])) {
return $this->badRequest([
APIError::BAD_REQUEST => WPFunctions::get()->__('Please regenerate the CAPTCHA.', 'mailpoet'),
]);
} elseif (!hash_equals(strtolower($data['captcha']), $_SESSION[Captcha::SESSION_KEY])) {
$_SESSION[Captcha::SESSION_KEY] = null;
$meta = [];
$meta['refresh_captcha'] = true;
return $this->badRequest([
APIError::BAD_REQUEST => WPFunctions::get()->__('The characters entered do not match with the previous captcha.', 'mailpoet'),
], $meta);
} else {
// Captcha has been verified, invalidate the session vars
$_SESSION[Captcha::SESSION_KEY] = null;
$_SESSION[Captcha::SESSION_FORM_KEY] = null;
}
}
}
// only accept fields defined in the form // only accept fields defined in the form
$form_fields = $form->getFieldList(); $form_fields = $form->getFieldList();
$data = array_intersect_key($data, array_flip($form_fields)); $data = array_intersect_key($data, array_flip($form_fields));

View File

@ -179,6 +179,7 @@ class Initializer {
function initialize() { function initialize() {
try { try {
$this->setupSession();
$this->maybeDbUpdate(); $this->maybeDbUpdate();
$this->setupInstaller(); $this->setupInstaller();
$this->setupUpdater(); $this->setupUpdater();
@ -207,6 +208,11 @@ class Initializer {
define(self::INITIALIZED, true); define(self::INITIALIZED, true);
} }
function setupSession() {
$session = new Session;
$session->init();
}
function maybeDbUpdate() { function maybeDbUpdate() {
try { try {
$current_db_version = $this->settings->get('db_version'); $current_db_version = $this->settings->get('db_version');

View File

@ -177,6 +177,9 @@ class Populator {
'confirmation' => $mailpoet_page_id, 'confirmation' => $mailpoet_page_id,
'captcha' => $mailpoet_page_id, 'captcha' => $mailpoet_page_id,
]); ]);
} elseif (empty($subscription['captcha'])) {
// For existing installations
$this->settings->set('subscription.pages', array_merge($subscription, ['captcha' => $mailpoet_page_id]));
} }
} }

13
lib/Config/Session.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace MailPoet\Config;
if (!defined('ABSPATH')) exit;
class Session {
function init() {
if (!session_id()) {
return session_start();
}
}
}

View File

@ -145,6 +145,7 @@ class ContainerConfigurator implements IContainerConfigurator {
// User Flags // User Flags
$container->autowire(\MailPoet\Settings\UserFlagsController::class); $container->autowire(\MailPoet\Settings\UserFlagsController::class);
// Subscription // Subscription
$container->autowire(\MailPoet\Subscription\Captcha::class)->setPublic(true);
$container->autowire(\MailPoet\Subscription\Comment::class)->setPublic(true); $container->autowire(\MailPoet\Subscription\Comment::class)->setPublic(true);
$container->autowire(\MailPoet\Subscription\Form::class)->setPublic(true); $container->autowire(\MailPoet\Subscription\Form::class)->setPublic(true);
$container->autowire(\MailPoet\Subscription\Manage::class)->setPublic(true); $container->autowire(\MailPoet\Subscription\Manage::class)->setPublic(true);

View File

@ -2,6 +2,10 @@
namespace MailPoet\Subscription; namespace MailPoet\Subscription;
use MailPoet\Models\Subscriber;
use MailPoet\Models\SubscriberIP;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Gregwar\Captcha\CaptchaBuilder; use MailPoetVendor\Gregwar\Captcha\CaptchaBuilder;
class Captcha { class Captcha {
@ -12,11 +16,57 @@ class Captcha {
const SESSION_KEY = 'mailpoet_captcha'; const SESSION_KEY = 'mailpoet_captcha';
const SESSION_FORM_KEY = 'mailpoet_captcha_form'; const SESSION_FORM_KEY = 'mailpoet_captcha_form';
/** @var WPFunctions */
private $wp;
function __construct(WPFunctions $wp = null) {
if ($wp === null) {
$wp = new WPFunctions;
}
$this->wp = $wp;
}
function isSupported() { function isSupported() {
return extension_loaded('gd') && function_exists('imagettftext'); return extension_loaded('gd') && function_exists('imagettftext');
} }
function renderImage($width = null, $height = null) { function isRequired($subscriber_email = null) {
if ($this->wp->isUserLoggedIn()) {
return false;
}
// Check limits per recipient
$subscription_captcha_recipient_limit = $this->wp->applyFilters('mailpoet_subscription_captcha_recipient_limit', 1);
if ($subscriber_email) {
$subscriber = Subscriber::where('email', $subscriber_email)->findOne();
if ($subscriber instanceof Subscriber
&& $subscriber->count_confirmations >= $subscription_captcha_recipient_limit
) {
return true;
}
}
// Check limits per IP address
$subscription_captcha_window = $this->wp->applyFilters('mailpoet_subscription_captcha_window', MONTH_IN_SECONDS);
$subscriber_ip = Helpers::getIP();
if (!empty($subscriber_ip)) {
$subscription_count = SubscriberIP::where('ip', $subscriber_ip)
->whereRaw(
'(`created_at` >= NOW() - INTERVAL ? SECOND)',
[(int)$subscription_captcha_window]
)->count();
if ($subscription_count > 0) {
return true;
}
}
return false;
}
function renderImage($width = null, $height = null, $return = false) {
if (!$this->isSupported()) { if (!$this->isSupported()) {
return false; return false;
} }
@ -36,6 +86,10 @@ class Captcha {
$_SESSION[self::SESSION_KEY] = $builder->getPhrase(); $_SESSION[self::SESSION_KEY] = $builder->getPhrase();
if ($return) {
return $builder->get();
}
header('Content-Type: image/jpeg'); header('Content-Type: image/jpeg');
$builder->output(); $builder->output();
exit; exit;

View File

@ -25,7 +25,9 @@ class Form {
$form_id = (!empty($request_data['data']['form_id'])) ? (int)$request_data['data']['form_id'] : false; $form_id = (!empty($request_data['data']['form_id'])) ? (int)$request_data['data']['form_id'] : false;
$response = $this->api->processRoute(); $response = $this->api->processRoute();
if ($response->status !== APIResponse::STATUS_OK) { if ($response->status !== APIResponse::STATUS_OK) {
return $this->url_helper->redirectBack( return (isset($response->meta['redirect_url'])) ?
$this->url_helper->redirectTo($response->meta['redirect_url']) :
$this->url_helper->redirectBack(
[ [
'mailpoet_error' => ($form_id) ? $form_id : true, 'mailpoet_error' => ($form_id) ? $form_id : true,
'mailpoet_success' => null, 'mailpoet_success' => null,

View File

@ -2,6 +2,7 @@
namespace MailPoet\Subscription; namespace MailPoet\Subscription;
use MailPoet\Models\Form as FormModel;
use MailPoet\Models\Subscriber; use MailPoet\Models\Subscriber;
use MailPoet\Models\SubscriberSegment; use MailPoet\Models\SubscriberSegment;
use MailPoet\Models\CustomField; use MailPoet\Models\CustomField;
@ -290,6 +291,8 @@ class Pages {
); );
$form_id = isset($_SESSION[Captcha::SESSION_FORM_KEY]['form_id']) ? (int)$_SESSION[Captcha::SESSION_FORM_KEY]['form_id'] : 0; $form_id = isset($_SESSION[Captcha::SESSION_FORM_KEY]['form_id']) ? (int)$_SESSION[Captcha::SESSION_FORM_KEY]['form_id'] : 0;
$form_model = FormModel::findOne($form_id);
$form_model = $form_model->asArray();
$form_html = '<form method="POST" ' . $form_html = '<form method="POST" ' .
'action="' . admin_url('admin-post.php?action=mailpoet_subscription_form') . '" ' . 'action="' . admin_url('admin-post.php?action=mailpoet_subscription_form') . '" ' .
@ -315,7 +318,7 @@ class Pages {
$form_html .= FormRenderer::renderBlocks($form, $honeypot = false); $form_html .= FormRenderer::renderBlocks($form, $honeypot = false);
$form_html .= '</div>'; $form_html .= '</div>';
$form_html .= '<div class="mailpoet_message">'; $form_html .= '<div class="mailpoet_message">';
$form_html .= '<p class="mailpoet_validate_success" style="display:none;">Check your inbox or spam folder to confirm your subscription.</p>'; $form_html .= '<p class="mailpoet_validate_success" style="display:none;">' . $form_model['settings']['success_message'] . '</p>';
$form_html .= '<p class="mailpoet_validate_error" style="display:none;"></p>'; $form_html .= '<p class="mailpoet_validate_error" style="display:none;"></p>';
$form_html .= '</div>'; $form_html .= '</div>';
$form_html .= '</form>'; $form_html .= '</form>';

View File

@ -14,7 +14,6 @@ class Throttling {
$subscription_limit_base = $wp->applyFilters('mailpoet_subscription_limit_base', MINUTE_IN_SECONDS); $subscription_limit_base = $wp->applyFilters('mailpoet_subscription_limit_base', MINUTE_IN_SECONDS);
$subscriber_ip = Helpers::getIP(); $subscriber_ip = Helpers::getIP();
$wp = new WPFunctions;
if ($subscription_limit_enabled && !$wp->isUserLoggedIn()) { if ($subscription_limit_enabled && !$wp->isUserLoggedIn()) {
if (!empty($subscriber_ip)) { if (!empty($subscriber_ip)) {
@ -43,12 +42,14 @@ class Throttling {
$ip->ip = $subscriber_ip; $ip->ip = $subscriber_ip;
$ip->save(); $ip->save();
self::purge($subscription_limit_window); self::purge();
return false; return false;
} }
static function purge($interval) { static function purge() {
$wp = new WPFunctions;
$interval = $wp->applyFilters('mailpoet_subscription_purge_window', MONTH_IN_SECONDS);
return SubscriberIP::whereRaw( return SubscriberIP::whereRaw(
'(`created_at` < NOW() - INTERVAL ? SECOND)', '(`created_at` < NOW() - INTERVAL ? SECOND)',
[$interval] [$interval]