diff --git a/assets/js/src/public.js b/assets/js/src/public.js
index d5ea37e133..7353e480c3 100644
--- a/assets/js/src/public.js
+++ b/assets/js/src/public.js
@@ -60,11 +60,22 @@ jQuery(function ($) { // eslint-disable-line func-names
data: formData.data,
})
.fail(function handleFailedPost(response) {
- form.find('.mailpoet_validate_error').html(
- response.errors.map(function buildErrorMessage(error) {
- return error.message;
- }).join('
')
- ).show();
+ 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(
+ response.errors.map(function buildErrorMessage(error) {
+ return error.message;
+ }).join('
')
+ ).show();
+ }
})
.done(function handleRecaptcha(response) {
if (window.grecaptcha && formData.recaptcha) {
@@ -83,6 +94,10 @@ jQuery(function ($) { // eslint-disable-line func-names
} else {
// display success message
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
@@ -109,5 +124,14 @@ jQuery(function ($) { // eslint-disable-line func-names
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();
+ });
});
});
diff --git a/lib/API/JSON/API.php b/lib/API/JSON/API.php
index e1717a0026..5f9ec15bc6 100644
--- a/lib/API/JSON/API.php
+++ b/lib/API/JSON/API.php
@@ -3,6 +3,7 @@ namespace MailPoet\API\JSON;
use MailPoet\Config\AccessControl;
use MailPoet\Settings\SettingsController;
+use MailPoet\Subscription\Captcha;
use MailPoetVendor\Psr\Container\ContainerInterface;
use MailPoet\Util\Helpers;
use MailPoet\Util\Security;
@@ -79,7 +80,7 @@ class API {
$this->setRequestData($_POST);
$ignoreToken = (
- $this->settings->get('re_captcha.enabled') &&
+ $this->settings->get('captcha.type') != Captcha::TYPE_DISABLED &&
$this->_request_endpoint === 'subscribers' &&
$this->_request_method === 'subscribe'
);
diff --git a/lib/API/JSON/v1/Subscribers.php b/lib/API/JSON/v1/Subscribers.php
index 21839eccf7..c7bec4eac2 100644
--- a/lib/API/JSON/v1/Subscribers.php
+++ b/lib/API/JSON/v1/Subscribers.php
@@ -18,7 +18,9 @@ use MailPoet\Settings\SettingsController;
use MailPoet\Subscribers\RequiredCustomFieldValidator;
use MailPoet\Subscribers\Source;
use MailPoet\Subscribers\SubscriberActions;
+use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Throttling as SubscriptionThrottling;
+use MailPoet\Subscription\Url as SubscriptionUrl;
use MailPoet\WP\Functions as WPFunctions;
if (!defined('ABSPATH')) exit;
@@ -46,6 +48,9 @@ class Subscribers extends APIEndpoint {
/** @var Listing\Handler */
private $listing_handler;
+ /** @var Captcha */
+ private $subscription_captcha;
+
/** @var WPFunctions */
private $wp;
@@ -58,6 +63,7 @@ class Subscribers extends APIEndpoint {
SubscriberActions $subscriber_actions,
RequiredCustomFieldValidator $required_custom_field_validator,
Listing\Handler $listing_handler,
+ Captcha $subscription_captcha,
WPFunctions $wp,
SettingsController $settings
) {
@@ -66,6 +72,7 @@ class Subscribers extends APIEndpoint {
$this->subscriber_actions = $subscriber_actions;
$this->required_custom_field_validator = $required_custom_field_validator;
$this->listing_handler = $listing_handler;
+ $this->subscription_captcha = $subscription_captcha;
$this->wp = $wp;
$this->settings = $settings;
}
@@ -140,8 +147,6 @@ class Subscribers extends APIEndpoint {
$form = Form::findOne($form_id);
unset($data['form_id']);
- $recaptcha = $this->settings->get('re_captcha');
-
if (!$form instanceof Form) {
return $this->badRequest([
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'])) {
- return $this->badRequest([
- APIError::BAD_REQUEST => WPFunctions::get()->__('Please check the CAPTCHA.', 'mailpoet'),
- ]);
- }
+ $captcha = $this->settings->get('captcha');
- if (!empty($recaptcha['enabled'])) {
- $res = empty($data['recaptcha']) ? $data['recaptcha-no-js'] : $data['recaptcha'];
- $res = WPFunctions::get()->wpRemotePost('https://www.google.com/recaptcha/api/siteverify', [
- 'body' => [
- 'secret' => $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'),
- ]);
+ if (!empty($captcha['type']) && $captcha['type'] === Captcha::TYPE_BUILTIN) {
+ if (empty($data['captcha'])) {
+ // Save form data to session
+ $_SESSION[Captcha::SESSION_FORM_KEY] = array_merge($data, ['form_id' => $form_id]);
+ } elseif (!empty($_SESSION[Captcha::SESSION_FORM_KEY])) {
+ // Restore form data from session
+ $data = array_merge($_SESSION[Captcha::SESSION_FORM_KEY], ['captcha' => $data['captcha']]);
}
+ // Otherwise use the post 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
$form_fields = $form->getFieldList();
$data = array_intersect_key($data, array_flip($form_fields));
diff --git a/lib/Config/Initializer.php b/lib/Config/Initializer.php
index ee6b4aae30..408f8658dd 100644
--- a/lib/Config/Initializer.php
+++ b/lib/Config/Initializer.php
@@ -179,6 +179,7 @@ class Initializer {
function initialize() {
try {
+ $this->setupSession();
$this->maybeDbUpdate();
$this->setupInstaller();
$this->setupUpdater();
@@ -207,6 +208,11 @@ class Initializer {
define(self::INITIALIZED, true);
}
+ function setupSession() {
+ $session = new Session;
+ $session->init();
+ }
+
function maybeDbUpdate() {
try {
$current_db_version = $this->settings->get('db_version');
diff --git a/lib/Config/Populator.php b/lib/Config/Populator.php
index 7e54c2f737..8391a41a34 100644
--- a/lib/Config/Populator.php
+++ b/lib/Config/Populator.php
@@ -177,6 +177,9 @@ class Populator {
'confirmation' => $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]));
}
}
diff --git a/lib/Config/Session.php b/lib/Config/Session.php
new file mode 100644
index 0000000000..1effb360be
--- /dev/null
+++ b/lib/Config/Session.php
@@ -0,0 +1,13 @@
+autowire(\MailPoet\Settings\UserFlagsController::class);
// Subscription
+ $container->autowire(\MailPoet\Subscription\Captcha::class)->setPublic(true);
$container->autowire(\MailPoet\Subscription\Comment::class)->setPublic(true);
$container->autowire(\MailPoet\Subscription\Form::class)->setPublic(true);
$container->autowire(\MailPoet\Subscription\Manage::class)->setPublic(true);
diff --git a/lib/Subscription/Captcha.php b/lib/Subscription/Captcha.php
index 2e1d2ca3d4..c0fb1ec0ef 100644
--- a/lib/Subscription/Captcha.php
+++ b/lib/Subscription/Captcha.php
@@ -2,6 +2,10 @@
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;
class Captcha {
@@ -12,11 +16,57 @@ class Captcha {
const SESSION_KEY = 'mailpoet_captcha';
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() {
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()) {
return false;
}
@@ -36,6 +86,10 @@ class Captcha {
$_SESSION[self::SESSION_KEY] = $builder->getPhrase();
+ if ($return) {
+ return $builder->get();
+ }
+
header('Content-Type: image/jpeg');
$builder->output();
exit;
diff --git a/lib/Subscription/Form.php b/lib/Subscription/Form.php
index 2d4309d0f8..45b0801d81 100644
--- a/lib/Subscription/Form.php
+++ b/lib/Subscription/Form.php
@@ -25,7 +25,9 @@ class Form {
$form_id = (!empty($request_data['data']['form_id'])) ? (int)$request_data['data']['form_id'] : false;
$response = $this->api->processRoute();
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_success' => null,
diff --git a/lib/Subscription/Pages.php b/lib/Subscription/Pages.php
index f62d6ee4a1..15322c3b01 100644
--- a/lib/Subscription/Pages.php
+++ b/lib/Subscription/Pages.php
@@ -2,6 +2,7 @@
namespace MailPoet\Subscription;
+use MailPoet\Models\Form as FormModel;
use MailPoet\Models\Subscriber;
use MailPoet\Models\SubscriberSegment;
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_model = FormModel::findOne($form_id);
+ $form_model = $form_model->asArray();
$form_html = '