Refactor the captcha system

The current Captcha class has a lot of responsibilities. It renders the captcha
image, can check if a certain captcha type is a Google captcha, if a captcha is
required for a certain email. The SubscriberSubscribeController is not only in
charge of "controlling" the subscription process but also validates, whether a
captcha is correct or not. This architecture made it difficult to extend the
functionality and introduce the audio captcha feature.

Therefore this commit refactors the captcha architecture and tries to seperate
the different concerns into several classes and objects. Validation is now done
by validators.

The CaptchaPhrase now is in charge of keeping the captcha phrase consistent
between the image and the new audio, so that you can renew the captcha and both
captchas are in sync.

[MAILPOET-4514]
This commit is contained in:
David Remer
2022-06-21 13:13:40 +03:00
committed by Aschepikov
parent 7fb8d64628
commit 4832771185
41 changed files with 1428 additions and 526 deletions

View File

@ -5,7 +5,7 @@ namespace MailPoet\API\JSON;
use MailPoet\Config\AccessControl;
use MailPoet\Exception;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\Tracy\ApiPanel\ApiPanel;
use MailPoet\Tracy\DIPanel\DIPanel;
use MailPoet\Util\Helpers;
@ -100,7 +100,7 @@ class API {
}
$ignoreToken = (
$this->settings->get('captcha.type') != Captcha::TYPE_DISABLED &&
$this->settings->get('captcha.type') != CaptchaConstants::TYPE_DISABLED &&
$this->requestEndpoint === 'subscribers' &&
$this->requestMethod === 'subscribe'
);

View File

@ -11,7 +11,7 @@ use MailPoet\Services\Bridge;
use MailPoet\Settings\Hosts;
use MailPoet\Settings\Pages;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaRenderer;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice as WPNotice;
@ -28,8 +28,8 @@ class Settings {
/** @var ServicesChecker */
private $servicesChecker;
/** @var Captcha */
private $captcha;
/** @var CaptchaRenderer */
private $captchaRenderer;
/** @var SegmentsSimpleListRepository */
private $segmentsListRepository;
@ -45,7 +45,7 @@ class Settings {
SettingsController $settings,
WPFunctions $wp,
ServicesChecker $servicesChecker,
Captcha $captcha,
CaptchaRenderer $captchaRenderer,
SegmentsSimpleListRepository $segmentsListRepository,
Bridge $bridge,
AuthorizedSenderDomainController $senderDomainController
@ -54,7 +54,7 @@ class Settings {
$this->settings = $settings;
$this->wp = $wp;
$this->servicesChecker = $servicesChecker;
$this->captcha = $captcha;
$this->captchaRenderer = $captchaRenderer;
$this->segmentsListRepository = $segmentsListRepository;
$this->bridge = $bridge;
$this->senderDomainController = $senderDomainController;
@ -83,8 +83,8 @@ class Settings {
'root' => ABSPATH,
'plugin' => dirname(dirname(dirname(__DIR__))),
],
'built_in_captcha_supported' => $this->captcha->isSupported(),
'current_site_title' => $this->wp->getBloginfo('name'),
'built_in_captcha_supported' => $this->captchaRenderer->isSupported(),
];
$data['authorized_emails'] = [];

View File

@ -28,7 +28,8 @@ use MailPoet\Settings\SettingsController;
use MailPoet\Settings\UserFlagsRepository;
use MailPoet\Subscribers\NewSubscriberNotificationMailer;
use MailPoet\Subscribers\Source;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\Subscription\Captcha\CaptchaRenderer;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
@ -42,8 +43,8 @@ class Populator {
private $settings;
/** @var WPFunctions */
private $wp;
/** @var Captcha */
private $captcha;
/** @var CaptchaRenderer */
private $captchaRenderer;
/** @var ReferralDetector */
private $referralDetector;
const TEMPLATES_NAMESPACE = '\MailPoet\Config\PopulatorData\Templates\\';
@ -59,7 +60,7 @@ class Populator {
public function __construct(
SettingsController $settings,
WPFunctions $wp,
Captcha $captcha,
CaptchaRenderer $captchaRenderer,
ReferralDetector $referralDetector,
EntityManager $entityManager,
WP $wpSegment,
@ -68,7 +69,7 @@ class Populator {
) {
$this->settings = $settings;
$this->wp = $wp;
$this->captcha = $captcha;
$this->captchaRenderer = $captchaRenderer;
$this->wpSegment = $wpSegment;
$this->referralDetector = $referralDetector;
$this->prefix = Env::$dbPrefix;
@ -259,11 +260,11 @@ class Populator {
$captcha = $this->settings->fetch('captcha');
$reCaptcha = $this->settings->fetch('re_captcha');
if (empty($captcha)) {
$captchaType = Captcha::TYPE_DISABLED;
$captchaType = CaptchaConstants::TYPE_DISABLED;
if (!empty($reCaptcha['enabled'])) {
$captchaType = Captcha::TYPE_RECAPTCHA;
} elseif ($this->captcha->isSupported()) {
$captchaType = Captcha::TYPE_BUILTIN;
$captchaType = CaptchaConstants::TYPE_RECAPTCHA;
} elseif ($this->captchaRenderer->isSupported()) {
$captchaType = CaptchaConstants::TYPE_BUILTIN;
}
$this->settings->set('captcha', [
'type' => $captchaType,

View File

@ -409,9 +409,13 @@ class ContainerConfigurator implements IContainerConfigurator {
$container->autowire(\MailPoet\Settings\UserFlagsController::class);
$container->autowire(\MailPoet\Settings\UserFlagsRepository::class)->setPublic(true);
// Subscription
$container->autowire(\MailPoet\Subscription\Captcha::class)->setPublic(true);
$container->autowire(\MailPoet\Subscription\Captcha\CaptchaConstants::class)->setPublic(true);
$container->autowire(\MailPoet\Subscription\CaptchaFormRenderer::class)->setPublic(true);
$container->autowire(\MailPoet\Subscription\CaptchaSession::class);
$container->autowire(\MailPoet\Subscription\Captcha\CaptchaSession::class);
$container->autowire(\MailPoet\Subscription\Captcha\CaptchaRenderer::class);
$container->autowire(\MailPoet\Subscription\Captcha\CaptchaPhrase::class);
$container->autowire(\MailPoet\Subscription\Captcha\Validator\BuiltInCaptchaValidator::class)->setPublic(true);
$container->autowire(\MailPoet\Subscription\Captcha\Validator\RecaptchaValidator::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);

View File

@ -5,7 +5,7 @@ namespace MailPoet\Form;
use MailPoet\Config\Env;
use MailPoet\Config\Renderer as BasicRenderer;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\WP\Functions as WPFunctions;
class AssetsController {
@ -37,7 +37,7 @@ class AssetsController {
public function printScripts() {
ob_start();
$captcha = $this->settings->get('captcha');
if (!empty($captcha['type']) && Captcha::isReCaptcha($captcha['type'])) {
if (!empty($captcha['type']) && CaptchaConstants::isReCaptcha($captcha['type'])) {
echo '<script src="' . esc_attr(self::RECAPTCHA_API_URL) . '" async defer></script>';
}
@ -66,7 +66,7 @@ class AssetsController {
public function setupFrontEndDependencies() {
$captcha = $this->settings->get('captcha');
if (!empty($captcha['type']) && Captcha::isRecaptcha($captcha['type'])) {
if (!empty($captcha['type']) && CaptchaConstants::isRecaptcha($captcha['type'])) {
$this->wp->wpEnqueueScript(
'mailpoet_recaptcha',
self::RECAPTCHA_API_URL

View File

@ -7,7 +7,7 @@ use MailPoet\Form\Templates\FormTemplate;
use MailPoet\Form\Util\CustomFonts;
use MailPoet\Form\Util\Styles;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaConstants;
class Renderer {
/** @var Styles */
@ -69,7 +69,7 @@ class Renderer {
if (
$captchaEnabled
&& $block['type'] === FormEntity::SUBMIT_BLOCK_TYPE
&& Captcha::isRecaptcha($this->settings->get('captcha.type'))
&& CaptchaConstants::isRecaptcha($this->settings->get('captcha.type'))
) {
$html .= $this->renderReCaptcha();
}
@ -88,7 +88,7 @@ class Renderer {
}
private function renderReCaptcha(): string {
if ($this->settings->get('captcha.type') === Captcha::TYPE_RECAPTCHA) {
if ($this->settings->get('captcha.type') === CaptchaConstants::TYPE_RECAPTCHA) {
$siteKey = $this->settings->get('captcha.recaptcha_site_token');
$size = '';
} else {

View File

@ -149,7 +149,7 @@ class MailerFactory {
}
private function getReturnPathAddress(array $sender): ?string {
$bounceAddress = $this->settings->get('bounce.address');
$bounceAddress = (string)$this->settings->get('bounce.address');
return $this->wp->isEmail($bounceAddress) ? $bounceAddress : $sender['from_email'];
}

View File

@ -39,8 +39,8 @@ class Subscription {
/** @var WPFunctions */
private $wp;
/** @var UserSubscription\Captcha */
private $captcha;
/** @var UserSubscription\Captcha\CaptchaRenderer */
private $captchaRenderer;
/*** @var Request */
private $request;
@ -48,12 +48,12 @@ class Subscription {
public function __construct(
UserSubscription\Pages $subscriptionPages,
WPFunctions $wp,
UserSubscription\Captcha $captcha,
UserSubscription\Captcha\CaptchaRenderer $captchaRenderer,
Request $request
) {
$this->subscriptionPages = $subscriptionPages;
$this->wp = $wp;
$this->captcha = $captcha;
$this->captchaRenderer = $captchaRenderer;
$this->request = $request;
}
@ -65,12 +65,12 @@ class Subscription {
$width = !empty($data['width']) ? (int)$data['width'] : null;
$height = !empty($data['height']) ? (int)$data['height'] : null;
$sessionId = !empty($data['captcha_session_id']) ? $data['captcha_session_id'] : null;
return $this->captcha->renderImage($width, $height, $sessionId);
return $this->captchaRenderer->renderImage($width, $height, $sessionId);
}
public function captchaAudio($data) {
$sessionId = !empty($data['captcha_session_id']) ? $data['captcha_session_id'] : null;
return $this->captcha->renderAudio($sessionId);
return $this->captchaRenderer->renderAudio($sessionId);
}
public function confirm($data) {

View File

@ -132,7 +132,8 @@ class ConfirmationEmailMailer {
'meta' => $this->mailerMetaInfo->getConfirmationMetaInfo($subscriber),
];
try {
$result = $this->mailerFactory->getDefaultMailer()->send($email, $subscriber, $extraParams);
$defaultMailer = $this->mailerFactory->getDefaultMailer();
$result = $defaultMailer->send($email, $subscriber, $extraParams);
} catch (\Exception $e) {
throw new \Exception(__('Something went wrong with your subscription. Please contact the website owner.', 'mailpoet'));
}

View File

@ -11,9 +11,11 @@ use MailPoet\NotFoundException;
use MailPoet\Segments\SubscribersFinder;
use MailPoet\Settings\SettingsController;
use MailPoet\Statistics\StatisticsFormsRepository;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\CaptchaSession;
use MailPoet\Subscription\SubscriptionUrlFactory;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\Subscription\Captcha\CaptchaSession;
use MailPoet\Subscription\Captcha\Validator\BuiltInCaptchaValidator;
use MailPoet\Subscription\Captcha\Validator\RecaptchaValidator;
use MailPoet\Subscription\Captcha\Validator\ValidationError;
use MailPoet\Subscription\Throttling as SubscriptionThrottling;
use MailPoet\Tags\TagRepository;
use MailPoet\UnexpectedValueException;
@ -23,15 +25,9 @@ class SubscriberSubscribeController {
/** @var FormsRepository */
private $formsRepository;
/** @var Captcha */
private $subscriptionCaptcha;
/** @var CaptchaSession */
private $captchaSession;
/** @var SubscriptionUrlFactory */
private $subscriptionUrlFactory;
/** @var FieldNameObfuscator */
private $fieldNameObfuscator;
@ -61,13 +57,16 @@ class SubscriberSubscribeController {
/** @var SubscriberTagRepository */
private $subscriberTagRepository;
/** @var BuiltInCaptchaValidator */
private $builtInCaptchaValidator;
/** @var RecaptchaValidator */
private $recaptchaValidator;
public function __construct(
Captcha $subscriptionCaptcha,
CaptchaSession $captchaSession,
SubscriberActions $subscriberActions,
SubscribersFinder $subscribersFinder,
SubscriptionUrlFactory $subscriptionUrlFactory,
SubscriptionThrottling $throttling,
FieldNameObfuscator $fieldNameObfuscator,
RequiredCustomFieldValidator $requiredCustomFieldValidator,
@ -76,12 +75,12 @@ class SubscriberSubscribeController {
StatisticsFormsRepository $statisticsFormsRepository,
TagRepository $tagRepository,
SubscriberTagRepository $subscriberTagRepository,
WPFunctions $wp
WPFunctions $wp,
BuiltInCaptchaValidator $builtInCaptchaValidator,
RecaptchaValidator $recaptchaValidator
) {
$this->formsRepository = $formsRepository;
$this->subscriptionCaptcha = $subscriptionCaptcha;
$this->captchaSession = $captchaSession;
$this->subscriptionUrlFactory = $subscriptionUrlFactory;
$this->requiredCustomFieldValidator = $requiredCustomFieldValidator;
$this->fieldNameObfuscator = $fieldNameObfuscator;
$this->settings = $settings;
@ -92,6 +91,8 @@ class SubscriberSubscribeController {
$this->statisticsFormsRepository = $statisticsFormsRepository;
$this->tagRepository = $tagRepository;
$this->subscriberTagRepository = $subscriberTagRepository;
$this->builtInCaptchaValidator = $builtInCaptchaValidator;
$this->recaptchaValidator = $recaptchaValidator;
}
public function subscribe(array $data): array {
@ -152,7 +153,7 @@ class SubscriberSubscribeController {
$subscriber = $this->subscriberActions->subscribe($data, $segmentIds);
if (!empty($captchaSettings['type']) && $captchaSettings['type'] === Captcha::TYPE_BUILTIN) {
if (!empty($captchaSettings['type']) && $captchaSettings['type'] === CaptchaConstants::TYPE_BUILTIN) {
// Captcha has been verified, invalidate the session vars
$this->captchaSession->reset();
}
@ -198,11 +199,14 @@ class SubscriberSubscribeController {
}
private function initCaptcha(?array $captchaSettings, FormEntity $form, array $data): array {
if (!$captchaSettings || !isset($captchaSettings['type'])) {
if (
!$captchaSettings
|| !isset($captchaSettings['type'])
|| $captchaSettings['type'] !== CaptchaConstants::TYPE_BUILTIN
) {
return $data;
}
if ($captchaSettings['type'] === Captcha::TYPE_BUILTIN) {
$captchaSessionId = isset($data['captcha_session_id']) ? $data['captcha_session_id'] : null;
$this->captchaSession->init($captchaSessionId);
if (!isset($data['captcha'])) {
@ -212,8 +216,6 @@ class SubscriberSubscribeController {
// Restore form data from session
$data = array_merge($this->captchaSession->getFormData(), ['captcha' => $data['captcha']]);
}
// Otherwise use the post data
}
return $data;
}
@ -221,56 +223,17 @@ class SubscriberSubscribeController {
if (empty($captchaSettings['type'])) {
return [];
}
$meta = [];
$isBuiltinCaptchaRequired = false;
if ($captchaSettings['type'] === Captcha::TYPE_BUILTIN) {
$isBuiltinCaptchaRequired = $this->subscriptionCaptcha->isRequired(isset($data['email']) ? $data['email'] : '');
if ($isBuiltinCaptchaRequired && empty($data['captcha'])) {
$meta['redirect_url'] = $this->subscriptionUrlFactory->getCaptchaUrl($this->captchaSession->getId());
$meta['error'] = __('Please fill in the CAPTCHA.', 'mailpoet');
return $meta;
try {
if ($captchaSettings['type'] === CaptchaConstants::TYPE_BUILTIN) {
$this->builtInCaptchaValidator->validate($data);
}
if (CaptchaConstants::isReCaptcha($captchaSettings['type'])) {
$this->recaptchaValidator->validate($data);
}
if (Captcha::isReCaptcha($captchaSettings['type']) && empty($data['recaptchaResponseToken'])) {
return ['error' => __('Please check the CAPTCHA.', 'mailpoet')];
} catch (ValidationError $error) {
return $error->getMeta();
}
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['recaptchaResponseToken']) ? $data['recaptcha-no-js'] : $data['recaptchaResponseToken'];
$response = $this->wp->wpRemotePost('https://www.google.com/recaptcha/api/siteverify', [
'body' => [
'secret' => $secretToken,
'response' => $response,
],
]);
if (is_wp_error($response)) {
return ['error' => __('Error while validating the CAPTCHA.', 'mailpoet')];
}
$response = json_decode(wp_remote_retrieve_body($response));
if (empty($response->success)) {
return ['error' => __('Error while validating the CAPTCHA.', 'mailpoet')];
}
} elseif ($captchaSettings['type'] === Captcha::TYPE_BUILTIN && $isBuiltinCaptchaRequired) {
$captchaHash = $this->captchaSession->getCaptchaHash();
if (empty($captchaHash)) {
$meta['error'] = __('Please regenerate the CAPTCHA.', 'mailpoet');
} elseif (!hash_equals(strtolower($data['captcha']), strtolower($captchaHash))) {
$this->captchaSession->setCaptchaHash(null);
$meta['refresh_captcha'] = true;
$meta['error'] = __('The characters entered do not match with the previous CAPTCHA.', 'mailpoet');
}
}
return $meta;
return [];
}
private function getSegmentIds(FormEntity $form, array $segmentIds): array {

View File

@ -1,181 +0,0 @@
<?php
namespace MailPoet\Subscription;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Config\Env;
use MailPoet\Subscribers\SubscriberIPsRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Gregwar\Captcha\CaptchaBuilder;
use MailPoetVendor\Gregwar\Captcha\PhraseBuilder;
class Captcha {
const TYPE_BUILTIN = 'built-in';
const TYPE_RECAPTCHA = 'recaptcha';
const TYPE_RECAPTCHA_INVISIBLE = 'recaptcha-invisible';
const TYPE_DISABLED = null;
/** @var WPFunctions */
private $wp;
/** @var CaptchaSession */
private $captchaSession;
/** @var SubscriberIPsRepository */
private $subscriberIPsRepository;
/** @var SubscribersRepository */
private $subscribersRepository;
public static function isReCaptcha(?string $captchaType) {
return in_array($captchaType, [self::TYPE_RECAPTCHA, self::TYPE_RECAPTCHA_INVISIBLE]);
}
public function __construct(
SubscriberIPsRepository $subscriberIPsRepository,
SubscribersRepository $subscribersRepository,
WPFunctions $wp = null,
CaptchaSession $captchaSession = null
) {
if ($wp === null) {
$wp = new WPFunctions;
}
if ($captchaSession === null) {
$captchaSession = new CaptchaSession($wp);
}
$this->wp = $wp;
$this->captchaSession = $captchaSession;
$this->subscriberIPsRepository = $subscriberIPsRepository;
$this->subscribersRepository = $subscribersRepository;
}
public function isSupported() {
return extension_loaded('gd') && function_exists('imagettftext');
}
public function isRequired($subscriberEmail = null) {
if ($this->isUserExemptFromCaptcha()) {
return false;
}
$subscriptionCaptchaRecipientLimit = $this->wp->applyFilters('mailpoet_subscription_captcha_recipient_limit', 0);
if ($subscriptionCaptchaRecipientLimit === 0) {
return true;
}
// Check limits per recipient if enabled
if ($subscriberEmail) {
$subscriber = $this->subscribersRepository->findOneBy(['email' => $subscriberEmail]);
if (
$subscriber instanceof SubscriberEntity
&& $subscriber->getConfirmationsCount() >= $subscriptionCaptchaRecipientLimit
) {
return true;
}
}
// Check limits per IP address
$subscriptionCaptchaWindow = $this->wp->applyFilters('mailpoet_subscription_captcha_window', MONTH_IN_SECONDS);
$subscriberIp = Helpers::getIP();
if (empty($subscriberIp)) {
return false;
}
$subscriptionCount = $this->subscriberIPsRepository->getCountByIPAndCreatedAtAfterTimeInSeconds(
$subscriberIp,
(int)$subscriptionCaptchaWindow
);
if ($subscriptionCount > 0) {
return true;
}
return false;
}
private function isUserExemptFromCaptcha() {
if (!$this->wp->isUserLoggedIn()) {
return false;
}
$user = $this->wp->wpGetCurrentUser();
$roles = $this->wp->applyFilters('mailpoet_subscription_captcha_exclude_roles', ['administrator', 'editor']);
return !empty(array_intersect($roles, (array)$user->roles));
}
public function renderAudio($sessionId, $return = false) {
$audioPath = Env::$assetsPath . '/audio/';
$this->captchaSession->init($sessionId);
$captcha = (string)$this->captchaSession->getCaptchaHash();
if (! $captcha) {
$builder = new PhraseBuilder();
$captcha = $builder->build();
$this->captchaSession->setCaptchaHash($captcha);
}
$audio = null;
foreach (str_split($captcha) as $character) {
$file = $audioPath . strtolower($character) . '.mp3';
if (!file_exists($file)) {
throw new \RuntimeException("File not found.");
}
$audio .= file_get_contents($file);
}
if ($return) {
return $audio;
}
header("Cache-Control: no-store, no-cache, must-revalidate");
header('Content-Type: audio/mpeg');
//phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped, WordPressDotOrg.sniffs.OutputEscaping.UnescapedOutputParameter
echo $audio;
exit;
}
public function renderImage($width = null, $height = null, $sessionId = null, $return = false) {
if (!$this->isSupported()) {
return false;
}
$fontNumbers = array_merge(range(0, 3), [5]); // skip font #4
$fontNumber = $fontNumbers[mt_rand(0, count($fontNumbers) - 1)];
$reflector = new \ReflectionClass(CaptchaBuilder::class);
$captchaDirectory = dirname((string)$reflector->getFileName());
$font = $captchaDirectory . '/Font/captcha' . $fontNumber . '.ttf';
$this->captchaSession->init($sessionId);
$phrase = $this->captchaSession->getCaptchaHash();
if (!$phrase) {
$phrase = null;
}
$builder = CaptchaBuilder::create($phrase)
->setBackgroundColor(255, 255, 255)
->setTextColor(1, 1, 1)
->setMaxBehindLines(0)
->build($width ?: 220, $height ?: 60, $font);
$this->captchaSession->setCaptchaHash($builder->getPhrase());
if ($return) {
return $builder->get();
}
header("Expires: Sat, 01 Jan 2019 01:00:00 GMT"); // time in the past
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header('X-Cache-Enabled: False');
header('X-LiteSpeed-Cache-Control: no-cache');
header('Content-Type: image/jpeg');
$builder->output();
exit;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace MailPoet\Subscription\Captcha;
class CaptchaConstants {
const TYPE_BUILTIN = 'built-in';
const TYPE_RECAPTCHA = 'recaptcha';
const TYPE_RECAPTCHA_INVISIBLE = 'recaptcha-invisible';
const TYPE_DISABLED = null;
public static function isReCaptcha(?string $captchaType) {
return in_array($captchaType, [self::TYPE_RECAPTCHA, self::TYPE_RECAPTCHA_INVISIBLE]);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace MailPoet\Subscription\Captcha;
use MailPoetVendor\Gregwar\Captcha\PhraseBuilder;
class CaptchaPhrase {
/** @var CaptchaSession */
private $session;
/** @var PhraseBuilder */
private $phraseBuilder;
public function __construct(
CaptchaSession $session,
PhraseBuilder $phraseBuilder = null
) {
$this->session = $session;
$this->phraseBuilder = $phraseBuilder ? $phraseBuilder : new PhraseBuilder();
}
public function getPhrase(): ?string {
$storage = $this->session->getCaptchaHash();
return (isset($storage['phrase']) && is_string($storage['phrase'])) ? $storage['phrase'] : null;
}
public function resetPhrase() {
$this->session->setCaptchaHash(null);
}
public function getPhraseForType(string $type, string $sessionId = null): string {
$this->session->init($sessionId);
$storage = $this->session->getCaptchaHash();
if (!$storage) {
$storage = [
'phrase' => $this->phraseBuilder->build(),
'total_loaded' => 1,
'loaded_by_types' => [],
];
}
if (!isset($storage['loaded_by_types'][$type])) {
$storage['loaded_by_types'][$type] = 0;
}
if ($this->needsToRegenerateCaptcha($storage, $type)) {
$storage['phrase'] = $this->phraseBuilder->build();
$storage['total_loaded']++;
}
$storage['loaded_by_types'][$type]++;
$this->session->setCaptchaHash($storage);
return $storage['phrase'];
}
private function needsToRegenerateCaptcha(array $storage, string $type): bool {
return $storage['loaded_by_types'][$type] === $storage['total_loaded'];
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace MailPoet\Subscription\Captcha;
use MailPoet\Config\Env;
use MailPoetVendor\Gregwar\Captcha\CaptchaBuilder;
class CaptchaRenderer {
private $phrase;
public function __construct(
CaptchaPhrase $phrase
) {
$this->phrase = $phrase;
}
public function isSupported() {
return extension_loaded('gd') && function_exists('imagettftext');
}
public function renderAudio($sessionId, $return = false) {
$audioPath = Env::$assetsPath . '/audio/';
$phrase = $this->phrase->getPhraseForType('audio', $sessionId);
$audio = null;
foreach (str_split($phrase) as $character) {
$file = $audioPath . strtolower($character) . '.mp3';
if (!file_exists($file)) {
throw new \RuntimeException("File not found.");
}
$audio .= file_get_contents($file);
}
if ($return) {
return $audio;
}
header("Cache-Control: no-store, no-cache, must-revalidate");
header('Content-Type: audio/mpeg');
//phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped, WordPressDotOrg.sniffs.OutputEscaping.UnescapedOutputParameter
echo $audio;
exit;
}
public function renderImage($width = null, $height = null, $sessionId = null, $return = false) {
if (!$this->isSupported()) {
return false;
}
$fontNumbers = array_merge(range(0, 3), [5]); // skip font #4
$fontNumber = $fontNumbers[mt_rand(0, count($fontNumbers) - 1)];
$reflector = new \ReflectionClass(CaptchaBuilder::class);
$captchaDirectory = dirname((string)$reflector->getFileName());
$font = $captchaDirectory . '/Font/captcha' . $fontNumber . '.ttf';
$phrase = $this->phrase->getPhraseForType('image', $sessionId);
$builder = CaptchaBuilder::create($phrase)
->setBackgroundColor(255, 255, 255)
->setTextColor(1, 1, 1)
->setMaxBehindLines(0)
->build($width ?: 220, $height ?: 60, $font);
if ($return) {
return $builder->get();
}
header("Expires: Sat, 01 Jan 2019 01:00:00 GMT"); // time in the past
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header('X-Cache-Enabled: False');
header('X-LiteSpeed-Cache-Control: no-cache');
header('Content-Type: image/jpeg');
$builder->output();
exit;
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace MailPoet\Subscription;
namespace MailPoet\Subscription\Captcha;
use MailPoet\Util\Security;
use MailPoet\WP\Functions as WPFunctions;
@ -15,8 +15,8 @@ class CaptchaSession {
/** @var WPFunctions */
private $wp;
/** @var string */
private $id;
/** @var ?string */
private $id = null;
public function __construct(
WPFunctions $wp
@ -28,10 +28,7 @@ class CaptchaSession {
$this->id = $id ?: Security::generateRandomString(self::ID_LENGTH);
}
public function getId() {
if ($this->id === null) {
throw new \Exception("MailPoet captcha session not initialized.");
}
public function getId(): ?string {
return $this->id;
}
@ -57,6 +54,6 @@ class CaptchaSession {
}
private function getKey($type) {
return implode('_', ['MAILPOET', $this->getId(), $type]);
return \implode('_', ['MAILPOET', $this->getId(), $type]);
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace MailPoet\Subscription\Captcha\Validator;
use MailPoet\Models\Subscriber;
use MailPoet\Subscribers\SubscriberIPsRepository;
use MailPoet\Subscription\Captcha\CaptchaPhrase;
use MailPoet\Subscription\Captcha\CaptchaSession;
use MailPoet\Subscription\SubscriptionUrlFactory;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
class BuiltInCaptchaValidator implements CaptchaValidator {
/** @var SubscriptionUrlFactory */
private $subscriptionUrlFactory;
/** @var CaptchaPhrase */
private $captchaPhrase;
/** @var CaptchaSession */
private $captchaSession;
/** @var WPFunctions */
private $wp;
/** @var SubscriberIPsRepository */
private $subscriberIPsRepository;
public function __construct(
SubscriptionUrlFactory $subscriptionUrlFactory,
CaptchaPhrase $captchaPhrase,
CaptchaSession $captchaSession,
WPFunctions $wp,
SubscriberIPsRepository $subscriberIPsRepository
) {
$this->subscriptionUrlFactory = $subscriptionUrlFactory;
$this->captchaPhrase = $captchaPhrase;
$this->captchaSession = $captchaSession;
$this->wp = $wp;
$this->subscriberIPsRepository = $subscriberIPsRepository;
}
public function validate(array $data): bool {
$isBuiltinCaptchaRequired = $this->isRequired(isset($data['email']) ? $data['email'] : '');
if (!$isBuiltinCaptchaRequired) {
return true;
}
if (empty($data['captcha'])) {
throw new ValidationError(
__('Please fill in the CAPTCHA.', 'mailpoet'),
[
'redirect_url' => $this->subscriptionUrlFactory->getCaptchaUrl($this->getSessionId()),
]
);
}
$captchaHash = $this->captchaPhrase->getPhrase();
if (empty($captchaHash)) {
throw new ValidationError(
__('Please regenerate the CAPTCHA.', 'mailpoet'),
[
'redirect_url' => $this->subscriptionUrlFactory->getCaptchaUrl($this->getSessionId()),
]
);
}
if (!hash_equals(strtolower($data['captcha']), strtolower($captchaHash))) {
$this->captchaPhrase->resetPhrase();
throw new ValidationError(
__('The characters entered do not match with the previous CAPTCHA.', 'mailpoet'),
[
'refresh_captcha' => true,
]
);
}
return true;
}
private function getSessionId() {
$id = $this->captchaSession->getId();
if ($id === null) {
$this->captchaSession->init();
$id = $this->captchaSession->getId();
}
return $id;
}
public function isRequired($subscriberEmail = null) {
if ($this->isUserExemptFromCaptcha()) {
return false;
}
$subscriptionCaptchaRecipientLimit = $this->wp->applyFilters('mailpoet_subscription_captcha_recipient_limit', 0);
if ($subscriptionCaptchaRecipientLimit === 0) {
return true;
}
// Check limits per recipient if enabled
if ($subscriberEmail) {
$subscriber = Subscriber::where('email', $subscriberEmail)->findOne();
if (
$subscriber instanceof Subscriber
&& $subscriber->countConfirmations >= $subscriptionCaptchaRecipientLimit
) {
return true;
}
}
// Check limits per IP address
/** @var int|string $subscriptionCaptchaWindow */
$subscriptionCaptchaWindow = $this->wp->applyFilters('mailpoet_subscription_captcha_window', MONTH_IN_SECONDS);
$subscriberIp = Helpers::getIP();
if (empty($subscriberIp)) {
return false;
}
$subscriptionCount = $this->subscriberIPsRepository->getCountByIPAndCreatedAtAfterTimeInSeconds(
$subscriberIp,
(int)$subscriptionCaptchaWindow
);
if ($subscriptionCount > 0) {
return true;
}
return false;
}
private function isUserExemptFromCaptcha() {
if (!$this->wp->isUserLoggedIn()) {
return false;
}
$user = $this->wp->wpGetCurrentUser();
$roles = $this->wp->applyFilters('mailpoet_subscription_captcha_exclude_roles', ['administrator', 'editor']);
return !empty(array_intersect((array)$roles, $user->roles));
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace MailPoet\Subscription\Captcha\Validator;
interface CaptchaValidator {
/**
* @param array $data
* @return bool
* @throws ValidationError
*/
public function validate(array $data): bool;
}

View File

@ -0,0 +1,51 @@
<?php
namespace MailPoet\Subscription\Captcha\Validator;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\WP\Functions as WPFunctions;
class RecaptchaValidator implements CaptchaValidator {
/** @var SettingsController */
private $settings;
/** @var WPFunctions */
private $wp;
public function __construct(
SettingsController $settings,
WPFunctions $wp
) {
$this->settings = $settings;
$this->wp = $wp;
}
public function validate(array $data): bool {
$captchaSettings = $this->settings->get('captcha');
if (empty($data['recaptchaResponseToken'])) {
throw new ValidationError(__('Please check the CAPTCHA.', 'mailpoet'));
}
$secretToken = $captchaSettings['type'] === CaptchaConstants::TYPE_RECAPTCHA_INVISIBLE ? $captchaSettings['recaptcha_invisible_secret_token'] : $captchaSettings['recaptcha_secret_token'];
$response = $this->wp->wpRemotePost('https://www.google.com/recaptcha/api/siteverify', [
'body' => [
'secret' => $secretToken,
'response' => $data['recaptchaResponseToken'],
],
]);
if ($this->wp->isWpError($response)) {
throw new ValidationError(__('Error while validating the CAPTCHA.', 'mailpoet'));
}
$response = json_decode($this->wp->wpRemoteRetrieveBody($response));
if (empty($response->success)) {
throw new ValidationError(__('Error while validating the CAPTCHA.', 'mailpoet'));
}
return true;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace MailPoet\Subscription\Captcha\Validator;
class ValidationError extends \RuntimeException {
private $meta = [];
public function __construct(
$message = "",
array $meta = [],
$code = 0,
\Throwable $previous = null
) {
$this->meta = $meta;
$this->meta['error'] = $message;
parent::__construct($message, $code, $previous);
}
public function getMeta(): array {
return $this->meta;
}
}

View File

@ -7,6 +7,7 @@ use MailPoet\Entities\FormEntity;
use MailPoet\Form\FormsRepository;
use MailPoet\Form\Renderer as FormRenderer;
use MailPoet\Form\Util\Styles;
use MailPoet\Subscription\Captcha\CaptchaSession;
use MailPoet\Util\Url as UrlHelper;
class CaptchaFormRenderer {
@ -106,7 +107,7 @@ class CaptchaFormRenderer {
'id="mailpoet_captcha_form" ' .
'novalidate>';
$formHtml .= '<input type="hidden" name="data[form_id]" value="' . $formId . '" />';
$formHtml .= '<input type="hidden" name="data[captcha_session_id]" value="' . htmlspecialchars($this->captchaSession->getId()) . '" />';
$formHtml .= '<input type="hidden" name="data[captcha_session_id]" value="' . htmlspecialchars((string)$this->captchaSession->getId()) . '" />';
$formHtml .= '<input type="hidden" name="api_version" value="v1" />';
$formHtml .= '<input type="hidden" name="endpoint" value="subscribers" />';
$formHtml .= '<input type="hidden" name="mailpoet_method" value="subscribe" />';

View File

@ -4,7 +4,7 @@ namespace MailPoet\Util\Notices;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Notice;
@ -37,7 +37,7 @@ class HeadersAlreadySentNotice {
if (!$shouldDisplay) {
return null;
}
$captchaEnabled = $this->settings->get('captcha.type') === Captcha::TYPE_BUILTIN;
$captchaEnabled = $this->settings->get('captcha.type') === CaptchaConstants::TYPE_BUILTIN;
$trackingEnabled = $this->trackingConfig->isEmailTrackingEnabled();
if ($this->areHeadersAlreadySent()) {
return $this->display($captchaEnabled, $trackingEnabled);

View File

@ -855,16 +855,6 @@ parameters:
count: 1
path: ../../lib/Subscribers/SubscribersRepository.php
-
message: "#^Cannot cast mixed to int\\.$#"
count: 1
path: ../../lib/Subscription/Captcha.php
-
message: "#^Parameter \\#1 \\$arr1 of function array_intersect expects array, mixed given\\.$#"
count: 1
path: ../../lib/Subscription/Captcha.php
-
message: "#^Parameter \\#1 \\$blocks of method MailPoet\\\\Form\\\\Renderer\\:\\:renderBlocks\\(\\) expects array, mixed given\\.$#"
count: 1

View File

@ -855,16 +855,6 @@ parameters:
count: 1
path: ../../lib/Subscribers/SubscribersRepository.php
-
message: "#^Cannot cast mixed to int\\.$#"
count: 1
path: ../../lib/Subscription/Captcha.php
-
message: "#^Parameter \\#1 \\$array of function array_intersect expects array, mixed given\\.$#"
count: 1
path: ../../lib/Subscription/Captcha.php
-
message: "#^Parameter \\#1 \\$blocks of method MailPoet\\\\Form\\\\Renderer\\:\\:renderBlocks\\(\\) expects array, mixed given\\.$#"
count: 1

View File

@ -854,16 +854,6 @@ parameters:
count: 1
path: ../../lib/Subscribers/SubscribersRepository.php
-
message: "#^Cannot cast mixed to int\\.$#"
count: 1
path: ../../lib/Subscription/Captcha.php
-
message: "#^Parameter \\#1 \\$array of function array_intersect expects array, mixed given\\.$#"
count: 1
path: ../../lib/Subscription/Captcha.php
-
message: "#^Parameter \\#1 \\$blocks of method MailPoet\\\\Form\\\\Renderer\\:\\:renderBlocks\\(\\) expects array, mixed given\\.$#"
count: 1

View File

@ -2,7 +2,7 @@
namespace MailPoet\Test\Acceptance;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\Test\DataFactories\Form;
use MailPoet\Test\DataFactories\Settings;
@ -31,7 +31,7 @@ class GutenbergFormBlockCest {
->withConfirmationEmailSubject()
->withConfirmationEmailBody()
->withConfirmationEmailEnabled()
->withCaptchaType(Captcha::TYPE_DISABLED);
->withCaptchaType(CaptchaConstants::TYPE_DISABLED);
}
public function subscriptionGutenbergBlock(\AcceptanceTester $i): void {

View File

@ -3,7 +3,7 @@
namespace MailPoet\Test\Acceptance;
use Codeception\Util\Locator;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\Test\DataFactories\Form;
use MailPoet\Test\DataFactories\Segment;
use MailPoet\Test\DataFactories\Settings;
@ -27,7 +27,7 @@ class SubscribeToMultipleListsCest {
->withConfirmationEmailEnabled()
->withConfirmationEmailBody()
->withConfirmationEmailSubject('Subscribe to multiple test subject')
->withCaptchaType(Captcha::TYPE_DISABLED);
->withCaptchaType(CaptchaConstants::TYPE_DISABLED);
$formFactory->withDefaultSuccessMessage();

View File

@ -3,7 +3,7 @@
namespace MailPoet\Test\Acceptance;
use Codeception\Util\Locator;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\Test\DataFactories\Form;
use MailPoet\Test\DataFactories\Settings;
@ -27,7 +27,7 @@ class SubscriptionFormCest {
->withConfirmationEmailSubject()
->withConfirmationEmailBody()
->withConfirmationEmailEnabled()
->withCaptchaType(Captcha::TYPE_DISABLED);
->withCaptchaType(CaptchaConstants::TYPE_DISABLED);
$formName = 'Subscription Acceptance Test Form';
$formFactory = new Form();

View File

@ -2,7 +2,7 @@
namespace MailPoet\Test\Acceptance;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\Test\DataFactories\Form;
use MailPoet\Test\DataFactories\Settings;
use MailPoet\Test\DataFactories\Subscriber;
@ -18,7 +18,7 @@ class BuiltInCaptchaSubscriptionCest {
public function _before(\AcceptanceTester $i) {
$this->subscriberEmail = 'test-form@example.com';
$this->settingsFactory = new Settings();
$this->settingsFactory->withCaptchaType(Captcha::TYPE_BUILTIN);
$this->settingsFactory->withCaptchaType(CaptchaConstants::TYPE_BUILTIN);
$this->settingsFactory
->withConfirmationEmailSubject()
->withConfirmationEmailBody()

View File

@ -13,7 +13,8 @@ use MailPoet\Migrator\Migrator;
use MailPoet\Referrals\ReferralDetector;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\SettingsRepository;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\Subscription\Captcha\CaptchaRenderer;
use MailPoet\WP\Functions as WPFunctions;
class SetupTest extends \MailPoetTest {
@ -30,8 +31,8 @@ class SetupTest extends \MailPoetTest {
$settings = SettingsController::getInstance();
$referralDetector = new ReferralDetector($wpStub, $settings);
$subscriptionCaptcha = $this->diContainer->get(Captcha::class);
$populator = $this->getServiceWithOverrides(Populator::class, ['wp' => $wpStub, 'referralDetector' => $referralDetector]);
$captchaRenderer = $this->diContainer->get(CaptchaRenderer::class);
$migrator = $this->diContainer->get(Migrator::class);
$cronActionScheduler = $this->diContainer->get(ActionScheduler::class);
$router = new Setup($wpStub, new Activator($this->connection, $settings, $populator, $wpStub, $migrator, $cronActionScheduler));
@ -43,7 +44,7 @@ class SetupTest extends \MailPoetTest {
expect($signupConfirmation)->true();
$captcha = $settings->fetch('captcha');
$captchaType = $subscriptionCaptcha->isSupported() ? Captcha::TYPE_BUILTIN : Captcha::TYPE_DISABLED;
$captchaType = $captchaRenderer->isSupported() ? CaptchaConstants::TYPE_BUILTIN : CaptchaConstants::TYPE_DISABLED;
expect($captcha['type'])->equals($captchaType);
expect($captcha['recaptcha_site_token'])->equals('');
expect($captcha['recaptcha_secret_token'])->equals('');

View File

@ -5,9 +5,9 @@ namespace MailPoet\Test\API\JSON\v1;
use Codeception\Util\Fixtures;
use MailPoet\API\JSON\Error;
use MailPoet\API\JSON\ErrorResponse;
use MailPoet\API\JSON\SuccessResponse;
use MailPoet\API\JSON\Response as APIResponse;
use MailPoet\API\JSON\ResponseBuilders\SubscribersResponseBuilder;
use MailPoet\API\JSON\SuccessResponse;
use MailPoet\API\JSON\v1\Subscribers;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\CustomFieldEntity;
@ -34,9 +34,9 @@ use MailPoet\Subscribers\SubscriberListingRepository;
use MailPoet\Subscribers\SubscriberSaveController;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Subscribers\SubscriberSubscribeController;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\CaptchaSession;
use MailPoet\Test\DataFactories\CustomField as CustomFieldFactory;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\Subscription\Captcha\CaptchaSession;
use MailPoet\Test\DataFactories\DynamicSegment;
use MailPoet\Test\DataFactories\Newsletter as NewsletterFactory;
use MailPoet\Test\DataFactories\Segment as SegmentFactory;
@ -718,7 +718,7 @@ class SubscribersTest extends \MailPoetTest {
}
public function testItCannotSubscribeWithoutReCaptchaWhenEnabled() {
$this->settings->set('captcha', ['type' => Captcha::TYPE_RECAPTCHA]);
$this->settings->set('captcha', ['type' => CaptchaConstants::TYPE_RECAPTCHA]);
$response = $this->endpoint->subscribe([
$this->obfuscatedEmail => 'toto@mailpoet.com',
'form_id' => $this->form->getId(),
@ -730,7 +730,7 @@ class SubscribersTest extends \MailPoetTest {
}
public function testItCannotSubscribeWithoutInvisibleReCaptchaWhenEnabled() {
$this->settings->set('captcha', ['type' => Captcha::TYPE_RECAPTCHA_INVISIBLE]);
$this->settings->set('captcha', ['type' => CaptchaConstants::TYPE_RECAPTCHA_INVISIBLE]);
$response = $this->endpoint->subscribe([
$this->obfuscatedEmail => 'toto@mailpoet.com',
'form_id' => $this->form->getId(),
@ -742,7 +742,7 @@ class SubscribersTest extends \MailPoetTest {
}
public function testItCannotSubscribeWithoutBuiltInCaptchaWhenEnabled() {
$this->settings->set('captcha', ['type' => Captcha::TYPE_BUILTIN]);
$this->settings->set('captcha', ['type' => CaptchaConstants::TYPE_BUILTIN]);
$email = 'toto@mailpoet.com';
(new SubscriberFactory())
->withEmail($email)
@ -759,13 +759,13 @@ class SubscribersTest extends \MailPoetTest {
}
public function testItCanSubscribeWithBuiltInCaptchaWhenEnabled() {
$this->settings->set('captcha', ['type' => Captcha::TYPE_BUILTIN]);
$this->settings->set('captcha', ['type' => CaptchaConstants::TYPE_BUILTIN]);
$email = 'toto@mailpoet.com';
(new SubscriberFactory())
->withEmail($email)
->withCountConfirmations(1)
->create();
$captchaValue = 'ihG5W';
$captchaValue = ['phrase' => 'ihG5W'];
$captchaSessionId = 'abcdfgh';
$this->captchaSession->init($captchaSessionId);
$this->captchaSession->setCaptchaHash($captchaValue);
@ -774,7 +774,7 @@ class SubscribersTest extends \MailPoetTest {
'form_id' => $this->form->getId(),
'captcha_session_id' => $captchaSessionId,
$this->obfuscatedSegments => [$this->segment1->getId(), $this->segment2->getId()],
'captcha' => $captchaValue,
'captcha' => $captchaValue['phrase'],
]);
expect($response->status)->equals(APIResponse::STATUS_OK);
$this->settings->set('captcha', []);

View File

@ -5,7 +5,8 @@ namespace MailPoet\Test\Router\Endpoints;
use Codeception\Stub;
use Codeception\Stub\Expected;
use MailPoet\Router\Endpoints\Subscription;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\Subscription\Captcha\CaptchaRenderer;
use MailPoet\Subscription\Pages;
use MailPoet\Util\Request;
use MailPoet\WP\Functions as WPFunctions;
@ -16,8 +17,8 @@ class SubscriptionTest extends \MailPoetTest {
/** @var WPFunctions */
private $wp;
/** @var Captcha */
private $captcha;
/** @var CaptchaRenderer */
private $captchaRenderer;
/*** @var Request */
private $request;
@ -25,8 +26,8 @@ class SubscriptionTest extends \MailPoetTest {
public function _before() {
$this->data = [];
$this->wp = WPFunctions::get();
$this->captcha = $this->diContainer->get(Captcha::class);
$this->request = $this->diContainer->get(Request::class);
$this->captchaRenderer = $this->diContainer->get(CaptchaRenderer::class);
}
public function testItDisplaysConfirmPage() {
@ -34,7 +35,7 @@ class SubscriptionTest extends \MailPoetTest {
'wp' => $this->wp,
'confirm' => Expected::exactly(1),
], $this);
$subscription = new Subscription($pages, $this->wp, $this->captcha, $this->request);
$subscription = new Subscription($pages, $this->wp, $this->captchaRenderer, $this->request);
$subscription->confirm($this->data);
}
@ -44,7 +45,7 @@ class SubscriptionTest extends \MailPoetTest {
'getManageLink' => Expected::exactly(1),
'getManageContent' => Expected::exactly(1),
], $this);
$subscription = new Subscription($pages, $this->wp, $this->captcha, $this->request);
$subscription = new Subscription($pages, $this->wp, $this->captchaRenderer, $this->request);
$subscription->manage($this->data);
do_shortcode('[mailpoet_manage]');
do_shortcode('[mailpoet_manage_subscription]');
@ -55,7 +56,7 @@ class SubscriptionTest extends \MailPoetTest {
'wp' => new WPFunctions,
'unsubscribe' => Expected::exactly(1),
], $this);
$subscription = new Subscription($pages, $this->wp, $this->captcha, $this->request);
$subscription = new Subscription($pages, $this->wp, $this->captchaRenderer, $this->request);
$subscription->unsubscribe($this->data);
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace MailPoet\Test\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaRenderer;
use MailPoet\Subscription\Captcha\CaptchaSession;
class CaptchaRendererTest extends \MailPoetTest
{
/** @var CaptchaRenderer */
private $testee;
/** @var CaptchaSession */
private $session;
public function _before()
{
$this->testee = $this->diContainer->get(CaptchaRenderer::class);
$this->session = $this->diContainer->get(CaptchaSession::class);
}
public function testItRendersImage() {
$result = $this->testee->renderImage(null, null, null, true);
expect(strpos($result, 'JPEG') !== false)->true();
$sessionId = $this->session->getId();
expect($sessionId)->notNull();
}
public function testItRendersAudio() {
$this->session->init();
$hashData = [
'phrase' => 'a',
'total_loaded' => 1,
'loaded_by_types' => [],
];
$this->session->setCaptchaHash($hashData);
$sessionId = $this->session->getId();
$result = $this->testee->renderAudio($sessionId, true);
$partOfAudio = '(-1166::BBKKQQVZZ^^bbggkkoosxx|';
expect(strpos($result, $partOfAudio) !== false)->true();
}
/**
* We need to ensure that a new captcha phrase is created when reloading
*/
public function testItChangesCaptchaAndRendersNewImageWhenReloading() {
$this->session->init();
$sessionId = $this->session->getId();
$firstImage = $this->testee->renderImage(null, null, $sessionId, true);
$firstCaptcha = $this->session->getCaptchaHash();
$secondImage = $this->testee->renderImage(null, null, $sessionId, true);
$secondCaptcha = $this->session->getCaptchaHash();
expect($secondImage)->notEquals($firstImage);
expect($firstCaptcha['phrase'])->notEquals($secondCaptcha['phrase']);
}
/**
* We need to ensure that a new captcha phrase is created when reloading
*/
public function testItChangesCaptchaAndRendersNewAudioWhenReloading() {
$this->session->init();
$sessionId = $this->session->getId();
$fistAudio = $this->testee->renderAudio($sessionId, true);
$firstCaptcha = $this->session->getCaptchaHash();
$secondAudio = $this->testee->renderAudio($sessionId, true);
$secondCaptcha = $this->session->getCaptchaHash();
expect($fistAudio)->notEquals($secondAudio);
expect($firstCaptcha['phrase'])->notEquals($secondCaptcha['phrase']);
}
/**
* We need to make sure that the audio presented to a listener plays the same captcha
* the image shows.
*/
public function testImageAndAudioStayInSync() {
$this->session->init();
$sessionId = $this->session->getId();
$this->testee->renderAudio($sessionId, true);
$audioCaptcha = $this->session->getCaptchaHash();
$this->testee->renderImage(null, null, $sessionId, true);
$imageCaptcha = $this->session->getCaptchaHash();
expect($audioCaptcha['phrase'])->equals($imageCaptcha['phrase']);
$this->testee->renderImage(null, null, $sessionId, true);
$secondImageCaptcha = $this->session->getCaptchaHash();
$this->testee->renderAudio($sessionId, true);
$secondAudioCaptcha = $this->session->getCaptchaHash();
expect($secondAudioCaptcha['phrase'])->equals($secondImageCaptcha['phrase']);
}
}

View File

@ -1,8 +1,8 @@
<?php
namespace MailPoet\Test\Subscription;
namespace MailPoet\Test\Subscription\Captcha;
use MailPoet\Subscription\CaptchaSession;
use MailPoet\Subscription\Captcha\CaptchaSession;
use MailPoet\WP\Functions as WPFunctions;
class CaptchaSessionTest extends \MailPoetTest {

View File

@ -0,0 +1,115 @@
<?php
namespace Mailpoet\Test\Subscription\Captcha\Validator;
use Codeception\Util\Fixtures;
use MailPoet\Entities\SubscriberIPEntity;
use MailPoet\Models\Subscriber;
use MailPoet\Subscription\Captcha\CaptchaSession;
use MailPoet\Subscription\Captcha\Validator\BuiltInCaptchaValidator;
use MailPoet\Subscription\Captcha\Validator\ValidationError;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
class BuiltInCaptchaValidatorTest extends \MailPoetTest
{
/** @var BuiltInCaptchaValidator */
private $testee;
/** @var CaptchaSession */
private $session;
public function _before() {
$this->testee = $this->diContainer->get(BuiltInCaptchaValidator::class);
$this->session = $this->diContainer->get(CaptchaSession::class);
}
public function testEmptyCaptchaThrowsError() {
try {
$this->testee->validate([]);
} catch (ValidationError $error) {
$meta = $error->getMeta();
$this->assertEquals('Please fill in the CAPTCHA.', $meta['error']);
$this->assertTrue(array_key_exists('redirect_url', $meta));
}
}
public function testWrongCaptchaThrowsError() {
$this->session->init();
$this->session->setCaptchaHash(['phrase' => 'abc']);
try {
$this->testee->validate(['captcha' => '123']);
} catch (ValidationError $error) {
$meta = $error->getMeta();
$this->assertEquals('The characters entered do not match with the previous CAPTCHA.', $meta['error']);
}
}
public function testThrowsErrorWhenCaptchaHasTimedOut() {
$this->session->init();
$this->session->setCaptchaHash(['phrase' => null]);
try {
$this->testee->validate(['captcha' => '123']);
} catch (ValidationError $error) {
$meta = $error->getMeta();
$this->assertEquals('Please regenerate the CAPTCHA.', $meta['error']);
$this->assertTrue(array_key_exists('redirect_url', $meta));
}
}
public function testReturnsTrueWhenCaptchaIsSolved() {
$this->session->init();
$this->session->setCaptchaHash(['phrase' => 'abc']);
$this->assertTrue($this->testee->validate(['captcha' => 'abc']));
}
public function testItRequiresCaptchaForFirstSubscription() {
$email = 'non-existent-subscriber@example.com';
$result = $this->testee->isRequired($email);
expect($result)->equals(true);
}
public function testItRequiresCaptchaForUnrepeatedIPAddress() {
$result = $this->testee->isRequired();
expect($result)->equals(true);
}
public function testItTakesFilterIntoAccountToDisableCaptcha() {
$wp = new WPFunctions;
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$filter = function() {
return 1;
};
$wp->addFilter('mailpoet_subscription_captcha_recipient_limit', $filter);
$email = 'non-existent-subscriber@example.com';
$result = $this->testee->isRequired($email);
expect($result)->equals(false);
$result = $this->testee->isRequired();
expect($result)->equals(false);
$subscriber = Subscriber::create();
$subscriber->hydrate(Fixtures::get('subscriber_template'));
$subscriber->countConfirmations = 1;
$subscriber->save();
$result = $this->testee->isRequired($subscriber->email);
expect($result)->equals(true);
$ip = new SubscriberIPEntity('127.0.0.1');
$ip->setCreatedAt(Carbon::now()->subMinutes(1));
$this->entityManager->persist($ip);
$this->entityManager->flush();
$email = 'non-existent-subscriber@example.com';
$result = $this->testee->isRequired($email);
expect($result)->equals(true);
unset($_SERVER['REMOTE_ADDR']);
$wp->removeFilter('mailpoet_subscription_captcha_recipient_limit', $filter);
}
}

View File

@ -1,92 +0,0 @@
<?php
namespace MailPoet\Test\Subscription;
use Codeception\Util\Fixtures;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberIPEntity;
use MailPoet\Models\Subscriber;
use MailPoet\Subscribers\SubscriberIPsRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\CaptchaSession;
use MailPoet\Util\Cookies;
use MailPoet\Test\DataFactories\Subscriber as SubscriberFactory;
use MailPoet\WP\Functions as WPFunctions;
use MailPoet\WP\Functions;
use MailPoetVendor\Carbon\Carbon;
class CaptchaTest extends \MailPoetTest {
const CAPTCHA_SESSION_ID = 'ABC';
/** @var Captcha */
private $captcha;
/** @var CaptchaSession */
private $captchaSession;
public function _before() {
$cookiesMock = $this->createMock(Cookies::class);
$cookiesMock->method('get')->willReturn('abcd');
$subscriberIPsRepository = $this->diContainer->get(SubscriberIPsRepository::class);
$subscribersRepository = $this->diContainer->get(SubscribersRepository::class);
$this->captchaSession = new CaptchaSession(new Functions());
$this->captchaSession->init(self::CAPTCHA_SESSION_ID);
$this->captcha = new Captcha($subscriberIPsRepository, $subscribersRepository, new WPFunctions, $this->captchaSession);
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$this->captchaSession->reset();
}
public function testItRequiresCaptchaForFirstSubscription() {
$email = 'non-existent-subscriber@example.com';
$result = $this->captcha->isRequired($email);
expect($result)->equals(true);
}
public function testItRequiresCaptchaForUnrepeatedIPAddress() {
$result = $this->captcha->isRequired();
expect($result)->equals(true);
}
public function testItTakesFilterIntoAccountToDisableCaptcha() {
$wp = new WPFunctions;
$filter = function() {
return 1;
};
$wp->addFilter('mailpoet_subscription_captcha_recipient_limit', $filter);
$email = 'non-existent-subscriber@example.com';
$result = $this->captcha->isRequired($email);
expect($result)->equals(false);
$result = $this->captcha->isRequired();
expect($result)->equals(false);
$subscriberFactory = new SubscriberFactory();
$subscriber = $subscriberFactory->create();
$subscriber->setConfirmationsCount(1);
$result = $this->captcha->isRequired($subscriber->getEmail());
expect($result)->equals(true);
$ip = new SubscriberIPEntity('127.0.0.1');
$ip->setCreatedAt(Carbon::now()->subMinutes(1));
$this->entityManager->persist($ip);
$this->entityManager->flush();
$email = 'non-existent-subscriber@example.com';
$result = $this->captcha->isRequired($email);
expect($result)->equals(true);
$wp->removeFilter('mailpoet_subscription_captcha_recipient_limit', $filter);
}
public function testItRendersImageAndStoresHashToSession() {
expect($this->captchaSession->getCaptchaHash())->false();
$image = $this->captcha->renderImage(null, null, self::CAPTCHA_SESSION_ID, true);
expect($image)->notEmpty();
expect($this->captchaSession->getCaptchaHash())->notEmpty();
}
public function _after() {
$this->truncateEntity(SubscriberIPEntity::class);
$this->truncateEntity(SubscriberEntity::class);
}
}

View File

@ -18,6 +18,7 @@ class ThrottlingTest extends \MailPoetTest {
parent::_before();
$this->throttling = $this->diContainer->get(Throttling::class);
$this->subscriberIPsRepository = $this->diContainer->get(SubscriberIPsRepository::class);
$this->subscriberIPsRepository->deleteCreatedAtBeforeTimeInSeconds(0);
}
public function testItProgressivelyThrottlesSubscriptions() {

View File

@ -8,7 +8,7 @@ use MailPoet\Form\Renderer;
use MailPoet\Form\Util\CustomFonts;
use MailPoet\Form\Util\Styles;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use PHPUnit\Framework\MockObject\MockObject;
require_once __DIR__ . '/HtmlParser.php';
@ -50,7 +50,7 @@ class RendererTest extends \MailPoetUnitTest {
$this->settingsMock
->method('get')
->with('captcha.type')
->willReturn(Captcha::TYPE_DISABLED);
->willReturn(CaptchaConstants::TYPE_DISABLED);
$html = $this->renderer->renderBlocks(Fixtures::get('simple_form_body'));
$blocks = $this->htmlParser->findByXpath($html, "//div[@class='block']");
expect($blocks->length)->equals(2);
@ -61,7 +61,7 @@ class RendererTest extends \MailPoetUnitTest {
$this->settingsMock
->method('get')
->with('captcha.type')
->willReturn(Captcha::TYPE_DISABLED);
->willReturn(CaptchaConstants::TYPE_DISABLED);
$html = $this->renderer->renderBlocks(Fixtures::get('simple_form_body'));
$hpLabel = $this->htmlParser->findByXpath($html, "//label[@class='mailpoet_hp_email_label']");
expect($hpLabel->length)->equals(1);
@ -75,7 +75,7 @@ class RendererTest extends \MailPoetUnitTest {
$this->settingsMock
->method('get')
->will($this->returnValueMap([
['captcha.type', null, Captcha::TYPE_RECAPTCHA],
['captcha.type', null, CaptchaConstants::TYPE_RECAPTCHA],
['captcha.recaptcha_site_token', null, $token],
]));
$html = $this->renderer->renderBlocks(Fixtures::get('simple_form_body'));
@ -96,7 +96,7 @@ class RendererTest extends \MailPoetUnitTest {
$this->settingsMock
->method('get')
->with('captcha.type')
->willReturn(Captcha::TYPE_DISABLED);
->willReturn(CaptchaConstants::TYPE_DISABLED);
$html = $this->renderer->renderBlocks(Fixtures::get('simple_form_body'), [], null, false);
$hpLabel = $this->htmlParser->findByXpath($html, "//label[@class='mailpoet_hp_email_label']");
expect($hpLabel->length)->equals(0);

View File

@ -12,8 +12,11 @@ use MailPoet\Form\Util\FieldNameObfuscator;
use MailPoet\Segments\SubscribersFinder;
use MailPoet\Settings\SettingsController;
use MailPoet\Statistics\StatisticsFormsRepository;
use MailPoet\Subscription\Captcha;
use MailPoet\Subscription\CaptchaSession;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\Subscription\Captcha\CaptchaSession;
use MailPoet\Subscription\Captcha\Validator\BuiltInCaptchaValidator;
use MailPoet\Subscription\Captcha\Validator\RecaptchaValidator;
use MailPoet\Subscription\Captcha\Validator\ValidationError;
use MailPoet\Subscription\SubscriptionUrlFactory;
use MailPoet\Subscription\Throttling;
use MailPoet\Subscription\Throttling as SubscriptionThrottling;
@ -23,7 +26,6 @@ use MailPoet\WP\Functions as WPFunctions;
class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
public function testErrorGetsThrownWhenEmailFieldIsNotObfuscated() {
$subscriptionCaptcha = Stub::makeEmpty(Captcha::class);
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$subscriberActions = Stub::makeEmpty(
SubscriberActions::class,
@ -33,7 +35,6 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$this
);
$subscribersFinder = Stub::makeEmpty(SubscribersFinder::class);
$subscriptionUrlFactory = Stub::makeEmpty(SubscriptionUrlFactory::class);
$throttling = Stub::makeEmpty(SubscriptionThrottling::class);
$fieldNameObfuscator = Stub::makeEmpty(FieldNameObfuscator::class);
$requiredCustomFieldValidator = Stub::makeEmpty(RequiredCustomFieldValidator::class);
@ -41,7 +42,8 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$form = Stub::makeEmpty(FormEntity::class);
$tagRepository = Stub::makeEmpty(TagRepository::class);
$subscriberTagRepository = Stub::makeEmpty(SubscriberTagRepository::class);
$builtInCaptchaValidator = Stub::makeEmpty(BuiltInCaptchaValidator::class);
$recaptchaValidator = Stub::makeEmpty(RecaptchaValidator::class);
$formsRepository = Stub::makeEmpty(
FormsRepository::class,
@ -67,11 +69,9 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$this
);
$testee = new SubscriberSubscribeController(
$subscriptionCaptcha,
$captchaSession,
$subscriberActions,
$subscribersFinder,
$subscriptionUrlFactory,
$throttling,
$fieldNameObfuscator,
$requiredCustomFieldValidator,
@ -80,7 +80,9 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$statisticsFormsRepository,
$tagRepository,
$subscriberTagRepository,
$wp
$wp,
$builtInCaptchaValidator,
$recaptchaValidator
);
$this->expectException(UnexpectedValueException::class);
@ -93,7 +95,6 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
}
public function testNoSubscriptionWhenThrottle() {
$subscriptionCaptcha = Stub::makeEmpty(Captcha::class);
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$subscriberActions = Stub::makeEmpty(
SubscriberActions::class,
@ -103,7 +104,6 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$this
);
$subscribersFinder = Stub::makeEmpty(SubscribersFinder::class);
$subscriptionUrlFactory = Stub::makeEmpty(SubscriptionUrlFactory::class);
$throttling = Stub::makeEmpty(
SubscriptionThrottling::class,
[
@ -152,13 +152,13 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
);
$tagRepository = Stub::makeEmpty(TagRepository::class);
$subscriberTagRepository = Stub::makeEmpty(SubscriberTagRepository::class);
$builtInCaptchaValidator = Stub::makeEmpty(BuiltInCaptchaValidator::class);
$recaptchaValidator = Stub::makeEmpty(RecaptchaValidator::class);
$testee = new SubscriberSubscribeController(
$subscriptionCaptcha,
$captchaSession,
$subscriberActions,
$subscribersFinder,
$subscriptionUrlFactory,
$throttling,
$fieldNameObfuscator,
$requiredCustomFieldValidator,
@ -167,7 +167,9 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$statisticsFormsRepository,
$tagRepository,
$subscriberTagRepository,
$wp
$wp,
$builtInCaptchaValidator,
$recaptchaValidator
);
$result = $testee->subscribe(array_merge(['form_id' => 1], $submitData));
@ -178,7 +180,6 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
}
public function testNoSubscriptionWhenActionHookBeforeSubscriptionThrowsError() {
$subscriptionCaptcha = Stub::makeEmpty(Captcha::class);
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$subscriberActions = Stub::makeEmpty(
SubscriberActions::class,
@ -188,7 +189,6 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$this
);
$subscribersFinder = Stub::makeEmpty(SubscribersFinder::class);
$subscriptionUrlFactory = Stub::makeEmpty(SubscriptionUrlFactory::class);
$throttling = Stub::makeEmpty(SubscriptionThrottling::class);
$fieldNameObfuscator = Stub::makeEmpty(FieldNameObfuscator::class,
[
@ -235,13 +235,13 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
);
$tagRepository = Stub::makeEmpty(TagRepository::class);
$subscriberTagRepository = Stub::makeEmpty(SubscriberTagRepository::class);
$builtInCaptchaValidator = Stub::makeEmpty(BuiltInCaptchaValidator::class);
$recaptchaValidator = Stub::makeEmpty(RecaptchaValidator::class);
$testee = new SubscriberSubscribeController(
$subscriptionCaptcha,
$captchaSession,
$subscriberActions,
$subscribersFinder,
$subscriptionUrlFactory,
$throttling,
$fieldNameObfuscator,
$requiredCustomFieldValidator,
@ -250,19 +250,18 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$statisticsFormsRepository,
$tagRepository,
$subscriberTagRepository,
$wp
$wp,
$builtInCaptchaValidator,
$recaptchaValidator
);
$this->expectException(UnexpectedValueException::class);
$testee->subscribe(array_merge(['form_id' => 1], $submitData));
}
public function testBuiltinCaptchaNotFilledOut() {
public function testBuiltInValidatorFails() {
$captchaSessionId = 'captcha_session_id';
$subscriptionCaptcha = Stub::makeEmpty(Captcha::class, [
'isRequired' => true,
]);
$captchaSession = Stub::makeEmpty(CaptchaSession::class,
[
'init' => function($receivedSessionId) use ($captchaSessionId) {
@ -278,12 +277,6 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
);
$subscribersFinder = Stub::makeEmpty(SubscribersFinder::class);
$expectedRedirectLink = 'redirect';
$subscriptionUrlFactory = Stub::makeEmpty(
SubscriptionUrlFactory::class,
[
'getCaptchaUrl' => $expectedRedirectLink,
]
);
$throttling = Stub::makeEmpty(
SubscriptionThrottling::class,
[
@ -298,7 +291,7 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
]);
$requiredCustomFieldValidator = Stub::makeEmpty(RequiredCustomFieldValidator::class);
$captchaSettings = [
'type' => Captcha::TYPE_BUILTIN,
'type' => CaptchaConstants::TYPE_BUILTIN,
];
$settings = Stub::makeEmpty(SettingsController::class,
[
@ -344,13 +337,27 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
);
$tagRepository = Stub::makeEmpty(TagRepository::class);
$subscriberTagRepository = Stub::makeEmpty(SubscriberTagRepository::class);
$builtInCaptchaValidator = Stub::make(
BuiltInCaptchaValidator::class,
[
'validate' => Expected::once(function() use ($expectedRedirectLink) {
throw new ValidationError('Please fill in the CAPTCHA.', ['redirect_url' => $expectedRedirectLink]);
}),
],
$this
);
$recaptchaValidator = Stub::make(
RecaptchaValidator::class,
[
'validate' => Expected::never()
],
$this
);
$testee = new SubscriberSubscribeController(
$subscriptionCaptcha,
$captchaSession,
$subscriberActions,
$subscribersFinder,
$subscriptionUrlFactory,
$throttling,
$fieldNameObfuscator,
$requiredCustomFieldValidator,
@ -359,26 +366,25 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$statisticsFormsRepository,
$tagRepository,
$subscriberTagRepository,
$wp
$wp,
$builtInCaptchaValidator,
$recaptchaValidator
);
$result = $testee->subscribe(array_merge(['form_id' => 1], $submitData));
expect($result)->equals([
'redirect_url' => $expectedRedirectLink,
'error' => 'Please fill in the CAPTCHA.',
'redirect_url' => $expectedRedirectLink,
]);
}
public function testBuiltinCaptchaNotValid() {
public function testRecaptchaValidatorFails() {
$captchaSessionId = 'captcha_session_id';
$subscriptionCaptcha = Stub::makeEmpty(Captcha::class, [
'isRequired' => true,
]);
$captchaSession = Stub::makeEmpty(CaptchaSession::class,
[
'getCaptchaHash' => 'a_string_that_does_not_match',
'getCaptchaHash' => ['phrase' => 'a_string_that_does_not_match'],
'init' => function($receivedSessionId) use ($captchaSessionId) {
expect($receivedSessionId)->equals($captchaSessionId);
},
@ -391,13 +397,6 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$this
);
$subscribersFinder = Stub::makeEmpty(SubscribersFinder::class);
$expectedRedirectLink = 'redirect';
$subscriptionUrlFactory = Stub::makeEmpty(
SubscriptionUrlFactory::class,
[
'getCaptchaUrl' => $expectedRedirectLink,
]
);
$throttling = Stub::makeEmpty(
SubscriptionThrottling::class,
[
@ -413,7 +412,7 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$requiredCustomFieldValidator = Stub::makeEmpty(RequiredCustomFieldValidator::class);
$captchaSettings = [
'type' => Captcha::TYPE_BUILTIN,
'type' => CaptchaConstants::TYPE_RECAPTCHA,
];
$settings = Stub::makeEmpty(SettingsController::class,
[
@ -463,13 +462,31 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
);
$tagRepository = Stub::makeEmpty(TagRepository::class);
$subscriberTagRepository = Stub::makeEmpty(SubscriberTagRepository::class);
$builtInCaptchaValidator = Stub::make(
BuiltInCaptchaValidator::class,
[
'validate' => Expected::never()
],
$this
);
$recaptchaValidator = Stub::make(
RecaptchaValidator::class,
[
'validate' => function() {
throw new ValidationError(
"The characters entered do not match with the previous CAPTCHA.",
[
'refresh_captcha' => true,
]
);
}
],
$this
);
$testee = new SubscriberSubscribeController(
$subscriptionCaptcha,
$captchaSession,
$subscriberActions,
$subscribersFinder,
$subscriptionUrlFactory,
$throttling,
$fieldNameObfuscator,
$requiredCustomFieldValidator,
@ -478,7 +495,9 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$statisticsFormsRepository,
$tagRepository,
$subscriberTagRepository,
$wp
$wp,
$builtInCaptchaValidator,
$recaptchaValidator
);
$result = $testee->subscribe(array_merge(['form_id' => 1], $submitData));
@ -521,11 +540,9 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
->willReturn([15]);
$testee = new SubscriberSubscribeController(
Stub::makeEmpty(Captcha::class),
Stub::makeEmpty(CaptchaSession::class),
Stub::makeEmpty(SubscriberActions::class),
$subscribersFinder,
Stub::makeEmpty(SubscriptionUrlFactory::class),
Stub::makeEmpty(Throttling::class),
Stub::makeEmpty(FieldNameObfuscator::class),
Stub::makeEmpty(RequiredCustomFieldValidator::class),
@ -534,7 +551,9 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
Stub::makeEmpty(StatisticsFormsRepository::class),
Stub::makeEmpty(TagRepository::class),
Stub::makeEmpty(SubscriberTagRepository::class),
Stub::makeEmpty(WPFunctions::class)
Stub::makeEmpty(WPFunctions::class),
Stub::makeEmpty(BuiltInCaptchaValidator::class),
Stub::makeEmpty(RecaptchaValidator::class)
);
$result = $testee->isSubscribedToAnyFormSegments($form, $subscriber);
@ -574,11 +593,9 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
->willReturn([]);
$testee = new SubscriberSubscribeController(
Stub::makeEmpty(Captcha::class),
Stub::makeEmpty(CaptchaSession::class),
Stub::makeEmpty(SubscriberActions::class),
$subscribersFinder,
Stub::makeEmpty(SubscriptionUrlFactory::class),
Stub::makeEmpty(SubscriptionThrottling::class),
Stub::makeEmpty(FieldNameObfuscator::class),
Stub::makeEmpty(RequiredCustomFieldValidator::class),
@ -587,7 +604,9 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
Stub::makeEmpty(StatisticsFormsRepository::class),
Stub::makeEmpty(TagRepository::class),
Stub::makeEmpty(SubscriberTagRepository::class),
Stub::makeEmpty(WPFunctions::class)
Stub::makeEmpty(WPFunctions::class),
Stub::makeEmpty(BuiltInCaptchaValidator::class),
Stub::makeEmpty(RecaptchaValidator::class)
);
$result = $testee->isSubscribedToAnyFormSegments($form, $subscriber);
@ -599,9 +618,6 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$captchaSessionId = 'captcha_session_id';
$captcha = 'captcha';
$subscriptionCaptcha = Stub::makeEmpty(Captcha::class, [
'isRequired' => true,
]);
$captchaSession = Stub::makeEmpty(CaptchaSession::class,
[
'getCaptchaHash' => $captcha,
@ -651,13 +667,6 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$this
);
$subscribersFinder = Stub::makeEmpty(SubscribersFinder::class);
$subscriptionUrlFactory = Stub::makeEmpty(SubscriptionUrlFactory::class,
[
'subscribe' => function($receivedSubscriber, $receivedForm) use ($subscriber, $form) {
expect($receivedSubscriber)->equals($subscriber);
expect($receivedForm)->equals($form);
},
]);
$throttling = Stub::makeEmpty(SubscriptionThrottling::class);
$fieldNameObfuscator = Stub::makeEmpty(FieldNameObfuscator::class,
[
@ -671,7 +680,7 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
'get' => function($value) {
if ($value === 'captcha') {
return [
'type' => Captcha::TYPE_BUILTIN,
'type' => CaptchaConstants::TYPE_BUILTIN,
];
}
},
@ -699,13 +708,22 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
);
$tagRepository = Stub::makeEmpty(TagRepository::class);
$subscriberTagRepository = Stub::makeEmpty(SubscriberTagRepository::class);
$builtInCaptchaValidator = Stub::make(
BuiltInCaptchaValidator::class,
[
'validate' => function($data) use ($captcha) {
expect($data['captcha'])->equals($captcha);
return true;
}
],
$this
);
$recaptchaValidator = Stub::make(RecaptchaValidator::class);
$testee = new SubscriberSubscribeController(
$subscriptionCaptcha,
$captchaSession,
$subscriberActions,
$subscribersFinder,
$subscriptionUrlFactory,
$throttling,
$fieldNameObfuscator,
$requiredCustomFieldValidator,
@ -714,7 +732,9 @@ class SubscriberSubscribeControllerUnitTest extends \MailPoetUnitTest {
$statisticsFormsRepository,
$tagRepository,
$subscriberTagRepository,
$wp
$wp,
$builtInCaptchaValidator,
$recaptchaValidator
);
$result = $testee->subscribe(array_merge(['form_id' => 1], $submitData));

View File

@ -0,0 +1,123 @@
<?php
namespace MailPoet\Subscription\Captcha;
use Codeception\Stub;
use MailPoetVendor\Gregwar\Captcha\PhraseBuilder;
class CaptchaPhraseTest extends \MailPoetUnitTest
{
public function testItGeneratesPhraseWhenNewSession() {
$expectedPhrase = 'abc';
$session = Stub::make(
CaptchaSession::class,
[
'init' => function($sessionId) {},
'getCaptchaHash' => false,
'setCaptchaHash' => Stub\Expected::once(function($data) use ($expectedPhrase) {
expect($data['phrase'])->equals($expectedPhrase);
})
],
$this
);
$phraseBuilder = Stub::make(
PhraseBuilder::class,
[
'build' => Stub\Expected::once(function() use ($expectedPhrase) { return $expectedPhrase; }),
],
$this
);
$testee = new CaptchaPhrase($session, $phraseBuilder);
$phrase = $testee->getPhraseForType('type-a', null);
expect($phrase)->equals($expectedPhrase);
}
public function testItRegeneratesPhraseWhenCalledTwice() {
$expectedFirstPhrase = 'abc';
$expectedSecondPhrase = 'def';
$session = Stub::make(
CaptchaSession::class,
[
'init' => function($sessionId) {},
'getCaptchaHash' => false,
'setCaptchaHash' => Stub\Expected::exactly(2, function($data) use ($expectedFirstPhrase, $expectedSecondPhrase) {
static $count;
if (! $count) {
$count = 1;
expect($data['phrase'])->equals($expectedFirstPhrase);
return;
}
expect($data['phrase'])->equals($expectedSecondPhrase);
})
],
$this
);
$phraseBuilder = Stub::make(
PhraseBuilder::class,
[
'build' => Stub\Expected::exactly(2, function() use ($expectedFirstPhrase, $expectedSecondPhrase) {
static $count;
if (! $count) {
$count = 1;
return $expectedFirstPhrase;
}
return $expectedSecondPhrase;
}),
],
$this
);
$testee = new CaptchaPhrase($session, $phraseBuilder);
$phrase = $testee->getPhraseForType('type-a', null);
expect($phrase)->equals($expectedFirstPhrase);
$phrase = $testee->getPhraseForType('type-a', null);
expect($phrase)->equals($expectedSecondPhrase);
}
public function testItKeepsDifferentTypesInSync() {
$phrase = 'abc';
$expectedFirstStorage = [
'phrase' => $phrase,
'total_loaded' => 1,
'loaded_by_types' => [
'type-a' => 1,
],
];
$session = Stub::make(
CaptchaSession::class,
[
'init' => function($sessionId) {},
'getCaptchaHash' => Stub\Expected::exactly(2, function() use ($expectedFirstStorage){
static $count;
if (! $count) {
$count = 1;
return false;
}
return $expectedFirstStorage;
}),
'setCaptchaHash' => Stub\Expected::exactly(2, function($storage) use ($expectedFirstStorage) {
static $count;
if ($count) {
return;
}
$count = 1;
expect($storage)->equals($expectedFirstStorage);
})
],
$this
);
$phraseBuilder = Stub::make(
PhraseBuilder::class,
[
'build' => Stub\Expected::once(function() use ($phrase) { return $phrase; }),
],
$this
);
$testee = new CaptchaPhrase($session, $phraseBuilder);
$phraseTypeA = $testee->getPhraseForType('type-a', null);
$phraseTypeB = $testee->getPhraseForType('type-b', null);
expect($phraseTypeA)->equals($phraseTypeB);
}
}

View File

@ -0,0 +1,306 @@
<?php
namespace MailPoet\Subscription\Captcha\Validator;
use Codeception\Stub;
use MailPoet\Subscribers\SubscriberIPsRepository;
use MailPoet\Subscription\Captcha\CaptchaPhrase;
use MailPoet\Subscription\Captcha\CaptchaSession;
use MailPoet\Subscription\SubscriptionUrlFactory;
use MailPoet\WP\Functions as WPFunctions;
class BuiltInCaptchaValidatorTest extends \MailPoetUnitTest
{
/**
* @var WPFunctions
*/
private $wp;
public function _before()
{
$this->wp = Stub::make(
WPFunctions::class,
[
'isUserLoggedIn' => false,
'applyFilters' => function($filter, $value) {
return $value;
},
'__' => function($string) { return $string; },
],
$this
);
}
public function testHashIsValid() {
$phrase = 'abc';
$subscriptionUrlFactory = Stub::makeEmpty(SubscriptionUrlFactory::class);
$captchaPhrase = Stub::make(
CaptchaPhrase::class,
[
'getPhrase' => $phrase,
],
$this
);
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$subscriberRepository = Stub::makeEmpty(SubscriberIPsRepository::class);
$testee = new BuiltInCaptchaValidator(
$subscriptionUrlFactory,
$captchaPhrase,
$captchaSession,
$this->wp,
$subscriberRepository
);
$data = [
'captcha' => $phrase,
];
expect($testee->validate($data))->true();
}
/**
* @dataProvider dataForTestSomeRolesCanBypassCaptcha
*/
public function testSomeRolesCanBypassCaptcha($wp) {
$phrase = 'abc';
$subscriptionUrlFactory = Stub::makeEmpty(SubscriptionUrlFactory::class);
$captchaPhrase = Stub::make(
CaptchaPhrase::class,
[
'getPhrase' => 'something.else.' . $phrase,
],
$this
);
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$subscriberRepository = Stub::makeEmpty(SubscriberIPsRepository::class);
$testee = new BuiltInCaptchaValidator(
$subscriptionUrlFactory,
$captchaPhrase,
$captchaSession,
$wp,
$subscriberRepository
);
$data = [
'captcha' => $phrase,
];
expect($testee->validate($data))->true();
}
public function dataForTestSomeRolesCanBypassCaptcha() {
return [
'administrator_bypass_captcha' => [
'wp' => Stub::make(
WPFunctions::class,
[
'isUserLoggedIn' => true,
'applyFilters' => function($filter, $value) {
return $value;
},
'__' => function($string) { return $string; },
'wpGetCurrentUser' => (object) [
'roles' => ['administrator'],
],
],
$this
),
],
'editor_bypass_captcha' => [
'wp' => Stub::make(
WPFunctions::class,
[
'isUserLoggedIn' => true,
'applyFilters' => function($filter, $value) {
return $value;
},
'__' => function($string) { return $string; },
'wpGetCurrentUser' => (object) [
'roles' => ['editor'],
],
],
$this
),
],
'custom_role_can_bypass_with_filter' => [
'wp' => Stub::make(
WPFunctions::class,
[
'isUserLoggedIn' => true,
'applyFilters' => function($filter, $value) {
if ($filter === 'mailpoet_subscription_captcha_exclude_roles') {
return ['custom-role'];
}
return $value;
},
'__' => function($string) { return $string; },
'wpGetCurrentUser' => (object) [
'roles' => ['custom-role'],
],
],
$this
),
],
];
}
public function testEditorsBypassCaptcha() {
$phrase = 'abc';
$subscriptionUrlFactory = Stub::makeEmpty(SubscriptionUrlFactory::class);
$captchaPhrase = Stub::make(
CaptchaPhrase::class,
[
'getPhrase' => 'something.else.' . $phrase,
],
$this
);
$currentUser = (object) [
'roles' => ['editor'],
];
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$wp = Stub::make(
WPFunctions::class,
[
'isUserLoggedIn' => true,
'applyFilters' => function($filter, $value) {
return $value;
},
'__' => function($string) { return $string; },
'wpGetCurrentUser' => $currentUser,
],
$this
);
$subscriberRepository = Stub::makeEmpty(SubscriberIPsRepository::class);
$testee = new BuiltInCaptchaValidator(
$subscriptionUrlFactory,
$captchaPhrase,
$captchaSession,
$wp,
$subscriberRepository
);
$data = [
'captcha' => $phrase,
];
expect($testee->validate($data))->true();
}
public function testNoCaptchaFound() {
$phrase = 'abc';
$newUrl = 'https://example.com';
$subscriptionUrlFactory = Stub::make(
SubscriptionUrlFactory::class,
[
'getCaptchaUrl' => $newUrl,
],
$this
);
$captchaPhrase = Stub::make(
CaptchaPhrase::class,
[
'getPhrase' => null,
],
$this
);
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$subscriberRepository = Stub::makeEmpty(SubscriberIPsRepository::class);
$testee = new BuiltInCaptchaValidator(
$subscriptionUrlFactory,
$captchaPhrase,
$captchaSession,
$this->wp,
$subscriberRepository
);
$data = [
'captcha' => $phrase,
];
$error = null;
try {
$testee->validate($data);
} catch(ValidationError $error) {
expect($error->getMessage())->equals('Please regenerate the CAPTCHA.');
expect($error->getMeta()['redirect_url'])->equals($newUrl);
}
expect($error)->isInstanceOf(ValidationError::class);
}
public function testCaptchaMissmatch() {
$phrase = 'abc';
$subscriptionUrlFactory = Stub::makeEmpty(SubscriptionUrlFactory::class);
$captchaPhrase = Stub::make(
CaptchaPhrase::class,
[
'getPhrase' => $phrase . 'd',
'resetPhrase' => null,
],
$this
);
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$subscriberRepository = Stub::makeEmpty(SubscriberIPsRepository::class);
$testee = new BuiltInCaptchaValidator(
$subscriptionUrlFactory,
$captchaPhrase,
$captchaSession,
$this->wp,
$subscriberRepository
);
$data = [
'captcha' => $phrase,
];
$error = null;
try {
$testee->validate($data);
} catch(ValidationError $error) {
expect($error->getMessage())->equals('The characters entered do not match with the previous CAPTCHA.');
expect($error->getMeta()['refresh_captcha'])->true();
}
expect($error)->isInstanceOf(ValidationError::class);
}
public function testNoCaptchaIsSend() {
$phrase = 'abc';
$newUrl = 'https://example.com';
$subscriptionUrlFactory = Stub::make(
SubscriptionUrlFactory::class,
[
'getCaptchaUrl' => $newUrl,
],
$this
);
$captchaPhrase = Stub::make(
CaptchaPhrase::class,
[
'getPhrase' => $phrase,
],
$this
);
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$subscriberRepository = Stub::makeEmpty(SubscriberIPsRepository::class);
$testee = new BuiltInCaptchaValidator(
$subscriptionUrlFactory,
$captchaPhrase,
$captchaSession,
$this->wp,
$subscriberRepository
);
$data = [
'captcha' => '',
];
$error = null;
try {
$testee->validate($data);
} catch(ValidationError $error) {
expect($error->getMessage())->equals('Please fill in the CAPTCHA.');
expect($error->getMeta()['redirect_url'])->equals($newUrl);
}
expect($error)->isInstanceOf(ValidationError::class);
}
}

View File

@ -0,0 +1,193 @@
<?php
namespace MailPoet\Subscription\Captcha\Validator;
use Codeception\Stub;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscription\Captcha\CaptchaConstants;
use MailPoet\WP\Functions as WPFunctions;
class RecaptchaValidatorTest extends \MailPoetUnitTest
{
public function testSuccessfulInvisibleValidation() {
$captchaSettings = [
'type' => CaptchaConstants::TYPE_RECAPTCHA_INVISIBLE,
'recaptcha_invisible_secret_token' => 'recaptcha_invisible_secret_token',
'recaptcha_secret_token' => 'recaptcha_secret_token'
];
$recaptchaResponseToken = 'recaptchaResponseToken';
$response = json_encode(['success' => true]);
$settings = Stub::make(
SettingsController::class,
[
'get' => function($key) use ($captchaSettings) {
if ($key === 'captcha') {
return $captchaSettings;
}
},
],
$this
);
$wp = Stub::make(
WPFunctions::class,
[
'wpRemotePost' => function($url, $args) use ($recaptchaResponseToken, $captchaSettings, $response) {
expect($url)->equals('https://www.google.com/recaptcha/api/siteverify');
expect($args['body']['secret'])->equals($captchaSettings['recaptcha_invisible_secret_token']);
expect($args['body']['response'])->equals($recaptchaResponseToken);
return $response;
},
'isWpError' => false,
'wpRemoteRetrieveBody' => function($data) use ($response) {
expect($data)->equals($response);
return $response;
}
],
$this
);
$testee = new RecaptchaValidator($settings, $wp);
$data = [
'recaptchaResponseToken' => $recaptchaResponseToken,
];
expect($testee->validate($data))->true();
}
public function testSuccessfulValidation() {
$captchaSettings = [
'type' => CaptchaConstants::TYPE_RECAPTCHA,
'recaptcha_invisible_secret_token' => 'recaptcha_invisible_secret_token',
'recaptcha_secret_token' => 'recaptcha_secret_token'
];
$recaptchaResponseToken = 'recaptchaResponseToken';
$response = json_encode(['success' => true]);
$settings = Stub::make(
SettingsController::class,
[
'get' => function($key) use ($captchaSettings) {
if ($key === 'captcha') {
return $captchaSettings;
}
},
],
$this
);
$wp = Stub::make(
WPFunctions::class,
[
'wpRemotePost' => function($url, $args) use ($recaptchaResponseToken, $captchaSettings, $response) {
expect($url)->equals('https://www.google.com/recaptcha/api/siteverify');
expect($args['body']['secret'])->equals($captchaSettings['recaptcha_secret_token']);
expect($args['body']['response'])->equals($recaptchaResponseToken);
return $response;
},
'isWpError' => false,
'wpRemoteRetrieveBody' => function($data) use ($response) {
expect($data)->equals($response);
return $response;
}
],
$this
);
$testee = new RecaptchaValidator($settings, $wp);
$data = [
'recaptchaResponseToken' => $recaptchaResponseToken,
];
expect($testee->validate($data))->true();
}
public function testFailingValidation() {
$captchaSettings = [
'type' => CaptchaConstants::TYPE_RECAPTCHA_INVISIBLE,
'recaptcha_invisible_secret_token' => 'recaptcha_invisible_secret_token',
'recaptcha_secret_token' => 'recaptcha_secret_token'
];
$recaptchaResponseToken = 'recaptchaResponseToken';
$response = json_encode(['success' => false]);
$settings = Stub::make(
SettingsController::class,
[
'get' => function($key) use ($captchaSettings) {
if ($key === 'captcha') {
return $captchaSettings;
}
},
],
$this
);
$wp = Stub::make(
WPFunctions::class,
[
'wpRemotePost' => function() use ($response) {
return $response;
},
'isWpError' => false,
'wpRemoteRetrieveBody' => function() use ($response) {
return $response;
}
],
$this
);
$testee = new RecaptchaValidator($settings, $wp);
$data = [
'recaptchaResponseToken' => $recaptchaResponseToken,
];
$error = null;
try {
$testee->validate($data);
} catch (ValidationError $error) {
expect($error->getMessage())->equals('Error while validating the CAPTCHA.');
}
expect($error)->isInstanceOf(ValidationError::class);
}
public function testConnectionError() {
$captchaSettings = [
'type' => CaptchaConstants::TYPE_RECAPTCHA_INVISIBLE,
'recaptcha_invisible_secret_token' => 'recaptcha_invisible_secret_token',
'recaptcha_secret_token' => 'recaptcha_secret_token'
];
$recaptchaResponseToken = 'recaptchaResponseToken';
$response = (object) ['wp-error'];
$settings = Stub::make(
SettingsController::class,
[
'get' => function($key) use ($captchaSettings) {
if ($key === 'captcha') {
return $captchaSettings;
}
},
],
$this
);
$wp = Stub::make(
WPFunctions::class,
[
'wpRemotePost' => function() use ($response) {
return $response;
},
'isWpError' => true,
],
$this
);
$testee = new RecaptchaValidator($settings, $wp);
$data = [
'recaptchaResponseToken' => $recaptchaResponseToken,
];
$error = null;
try {
$testee->validate($data);
} catch (ValidationError $error) {
expect($error->getMessage())->equals('Error while validating the CAPTCHA.');
}
expect($error)->isInstanceOf(ValidationError::class);
}
}