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 = '
'; $form_html .= ''; $form_html .= ''; $form_html .= '
'; diff --git a/lib/Subscription/Throttling.php b/lib/Subscription/Throttling.php index b0021e9ae2..1104283ec5 100644 --- a/lib/Subscription/Throttling.php +++ b/lib/Subscription/Throttling.php @@ -14,7 +14,6 @@ class Throttling { $subscription_limit_base = $wp->applyFilters('mailpoet_subscription_limit_base', MINUTE_IN_SECONDS); $subscriber_ip = Helpers::getIP(); - $wp = new WPFunctions; if ($subscription_limit_enabled && !$wp->isUserLoggedIn()) { if (!empty($subscriber_ip)) { @@ -43,12 +42,14 @@ class Throttling { $ip->ip = $subscriber_ip; $ip->save(); - self::purge($subscription_limit_window); + self::purge(); 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( '(`created_at` < NOW() - INTERVAL ? SECOND)', [$interval]