Make captcha session stateless

[MAILPOET-6038]
This commit is contained in:
Jan Jakes
2024-07-10 09:32:07 +02:00
committed by Aschepikov
parent e8cf3d61ef
commit 71d7f46718
15 changed files with 133 additions and 142 deletions

View File

@@ -65,12 +65,18 @@ class Subscription {
public function captchaImage($data) { public function captchaImage($data) {
$width = !empty($data['width']) ? (int)$data['width'] : null; $width = !empty($data['width']) ? (int)$data['width'] : null;
$height = !empty($data['height']) ? (int)$data['height'] : null; $height = !empty($data['height']) ? (int)$data['height'] : null;
$sessionId = !empty($data['captcha_session_id']) ? $data['captcha_session_id'] : null; $sessionId = $data['captcha_session_id'] ?? null;
return $this->captchaRenderer->renderImage($width, $height, $sessionId); if (!$sessionId) {
return false;
}
return $this->captchaRenderer->renderImage($sessionId, $width, $height);
} }
public function captchaAudio($data) { public function captchaAudio($data) {
$sessionId = !empty($data['captcha_session_id']) ? $data['captcha_session_id'] : null; $sessionId = $data['captcha_session_id'] ?? null;
if (!$sessionId) {
return false;
}
return $this->captchaRenderer->renderAudio($sessionId); return $this->captchaRenderer->renderAudio($sessionId);
} }

View File

@@ -153,9 +153,9 @@ class SubscriberSubscribeController {
[$subscriber, $subscriptionMeta] = $this->subscriberActions->subscribe($data, $segmentIds); [$subscriber, $subscriptionMeta] = $this->subscriberActions->subscribe($data, $segmentIds);
if (!empty($captchaSettings['type']) && $captchaSettings['type'] === CaptchaConstants::TYPE_BUILTIN) { if (!empty($captchaSettings['type']) && $captchaSettings['type'] === CaptchaConstants::TYPE_BUILTIN && isset($data['captcha_session_id'])) {
// Captcha has been verified, invalidate the session vars // Captcha has been verified, invalidate the session vars
$this->captchaSession->reset(); $this->captchaSession->reset($data['captcha_session_id']);
} }
// record form statistics // record form statistics
@@ -213,14 +213,18 @@ class SubscriberSubscribeController {
return $data; return $data;
} }
$captchaSessionId = isset($data['captcha_session_id']) ? $data['captcha_session_id'] : null; // When serving the built-in CAPTCHA for the first time, generate a new session ID.
$this->captchaSession->init($captchaSessionId); if (!isset($data['captcha_session_id'])) {
$data['captcha_session_id'] = $this->captchaSession->generateSessionId();
}
$sessionId = $data['captcha_session_id'];
if (!isset($data['captcha'])) { if (!isset($data['captcha'])) {
// Save form data to session // Save form data to session
$this->captchaSession->setFormData(array_merge($data, ['form_id' => $form->getId()])); $this->captchaSession->setFormData($sessionId, array_merge($data, ['form_id' => $form->getId()]));
} elseif ($this->captchaSession->getFormData()) { } elseif ($this->captchaSession->getFormData($sessionId)) {
// Restore form data from session // Restore form data from session
$data = array_merge($this->captchaSession->getFormData(), ['captcha' => $data['captcha']]); $data = array_merge($this->captchaSession->getFormData($sessionId), ['captcha' => $data['captcha']]);
} }
return $data; return $data;
} }

View File

@@ -17,15 +17,13 @@ class CaptchaPhrase {
} }
public function createPhrase(string $sessionId): string { public function createPhrase(string $sessionId): string {
$this->session->init($sessionId);
$storage = ['phrase' => $this->phraseBuilder->build()]; $storage = ['phrase' => $this->phraseBuilder->build()];
$this->session->setCaptchaHash($storage); $this->session->setCaptchaHash($sessionId, $storage);
return $storage['phrase']; return $storage['phrase'];
} }
public function getPhrase(string $sessionId): ?string { public function getPhrase(string $sessionId): ?string {
$this->session->init($sessionId); $storage = $this->session->getCaptchaHash($sessionId);
$storage = $this->session->getCaptchaHash();
return (isset($storage['phrase']) && is_string($storage['phrase'])) ? $storage['phrase'] : null; return (isset($storage['phrase']) && is_string($storage['phrase'])) ? $storage['phrase'] : null;
} }
} }

View File

@@ -1,4 +1,4 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing <?php declare(strict_types = 1);
namespace MailPoet\Subscription\Captcha; namespace MailPoet\Subscription\Captcha;
@@ -6,11 +6,10 @@ use MailPoet\Config\Env;
use MailPoetVendor\Gregwar\Captcha\CaptchaBuilder; use MailPoetVendor\Gregwar\Captcha\CaptchaBuilder;
class CaptchaRenderer { class CaptchaRenderer {
const DEFAULT_WIDTH = 220; const DEFAULT_WIDTH = 220;
const DEFAULT_HEIGHT = 60; const DEFAULT_HEIGHT = 60;
private $phrase; private CaptchaPhrase $phrase;
public function __construct( public function __construct(
CaptchaPhrase $phrase CaptchaPhrase $phrase
@@ -18,11 +17,11 @@ class CaptchaRenderer {
$this->phrase = $phrase; $this->phrase = $phrase;
} }
public function isSupported() { public function isSupported(): bool {
return extension_loaded('gd') && function_exists('imagettftext'); return extension_loaded('gd') && function_exists('imagettftext');
} }
public function renderAudio($sessionId, $return = false) { public function renderAudio(string $sessionId, $return = false) {
if (!$this->isSupported()) { if (!$this->isSupported()) {
return false; return false;
} }
@@ -50,7 +49,7 @@ class CaptchaRenderer {
exit; exit;
} }
public function renderImage($width = null, $height = null, $sessionId = null, $return = false) { public function renderImage(string $sessionId, $width = null, $height = null, $return = false) {
if (!$this->isSupported()) { if (!$this->isSupported()) {
return false; return false;
} }
@@ -89,8 +88,8 @@ class CaptchaRenderer {
exit; exit;
} }
private function getPhrase(string $sessionId = null): string { private function getPhrase(string $sessionId): string {
$phrase = $sessionId ? $this->phrase->getPhrase($sessionId) : null; $phrase = $this->phrase->getPhrase($sessionId);
if (!$phrase) { if (!$phrase) {
throw new \RuntimeException("No CAPTCHA phrase was generated."); throw new \RuntimeException("No CAPTCHA phrase was generated.");
} }

View File

@@ -1,4 +1,4 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing <?php declare(strict_types = 1);
namespace MailPoet\Subscription\Captcha; namespace MailPoet\Subscription\Captcha;
@@ -12,11 +12,7 @@ class CaptchaSession {
const SESSION_HASH_KEY = 'hash'; const SESSION_HASH_KEY = 'hash';
const SESSION_FORM_KEY = 'form'; const SESSION_FORM_KEY = 'form';
/** @var WPFunctions */ private WPFunctions $wp;
private $wp;
/** @var string */
private $id = '';
public function __construct( public function __construct(
WPFunctions $wp WPFunctions $wp
@@ -24,39 +20,38 @@ class CaptchaSession {
$this->wp = $wp; $this->wp = $wp;
} }
public function init(string $id = null) { public function generateSessionId(): string {
$this->id = $id ?: Security::generateRandomString(self::ID_LENGTH); return Security::generateRandomString(self::ID_LENGTH);
} }
public function getId(): string { public function reset(string $sessionId): void {
if (!$this->id) { $formKey = $this->getKey($sessionId, self::SESSION_FORM_KEY);
$this->init(); $hashKey = $this->getKey($sessionId, self::SESSION_HASH_KEY);
} $this->wp->deleteTransient($formKey);
return $this->id; $this->wp->deleteTransient($hashKey);
} }
public function reset() { public function setFormData(string $sessionId, array $data): void {
$this->wp->deleteTransient($this->getKey(self::SESSION_FORM_KEY)); $key = $this->getKey($sessionId, self::SESSION_FORM_KEY);
$this->wp->deleteTransient($this->getKey(self::SESSION_HASH_KEY)); $this->wp->setTransient($key, $data, self::EXPIRATION);
} }
public function setFormData(array $data) { public function getFormData(string $sessionId) {
$this->wp->setTransient($this->getKey(self::SESSION_FORM_KEY), $data, self::EXPIRATION); $key = $this->getKey($sessionId, self::SESSION_FORM_KEY);
return $this->wp->getTransient($key);
} }
public function getFormData() { public function setCaptchaHash(string $sessionId, $hash): void {
return $this->wp->getTransient($this->getKey(self::SESSION_FORM_KEY)); $key = $this->getKey($sessionId, self::SESSION_HASH_KEY);
$this->wp->setTransient($key, $hash, self::EXPIRATION);
} }
public function setCaptchaHash($hash) { public function getCaptchaHash(string $sessionId) {
$this->wp->setTransient($this->getKey(self::SESSION_HASH_KEY), $hash, self::EXPIRATION); $key = $this->getKey($sessionId, self::SESSION_HASH_KEY);
return $this->wp->getTransient($key);
} }
public function getCaptchaHash() { private function getKey(string $sessionId, string $type): string {
return $this->wp->getTransient($this->getKey(self::SESSION_HASH_KEY)); return implode('_', ['MAILPOET', $sessionId, $type]);
}
private function getKey($type) {
return \implode('_', ['MAILPOET', $this->getId(), $type]);
} }
} }

View File

@@ -5,23 +5,17 @@ namespace MailPoet\Subscription\Captcha\Validator;
use MailPoet\Subscribers\SubscriberIPsRepository; use MailPoet\Subscribers\SubscriberIPsRepository;
use MailPoet\Subscribers\SubscribersRepository; use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Subscription\Captcha\CaptchaPhrase; use MailPoet\Subscription\Captcha\CaptchaPhrase;
use MailPoet\Subscription\Captcha\CaptchaSession;
use MailPoet\Subscription\SubscriptionUrlFactory; use MailPoet\Subscription\SubscriptionUrlFactory;
use MailPoet\Util\Helpers; use MailPoet\Util\Helpers;
use MailPoet\WP\Functions as WPFunctions; use MailPoet\WP\Functions as WPFunctions;
class BuiltInCaptchaValidator implements CaptchaValidator { class BuiltInCaptchaValidator implements CaptchaValidator {
/** @var SubscriptionUrlFactory */ /** @var SubscriptionUrlFactory */
private $subscriptionUrlFactory; private $subscriptionUrlFactory;
/** @var CaptchaPhrase */ /** @var CaptchaPhrase */
private $captchaPhrase; private $captchaPhrase;
/** @var CaptchaSession */
private $captchaSession;
/** @var WPFunctions */ /** @var WPFunctions */
private $wp; private $wp;
@@ -34,14 +28,12 @@ class BuiltInCaptchaValidator implements CaptchaValidator {
public function __construct( public function __construct(
SubscriptionUrlFactory $subscriptionUrlFactory, SubscriptionUrlFactory $subscriptionUrlFactory,
CaptchaPhrase $captchaPhrase, CaptchaPhrase $captchaPhrase,
CaptchaSession $captchaSession,
WPFunctions $wp, WPFunctions $wp,
SubscriberIPsRepository $subscriberIPsRepository, SubscriberIPsRepository $subscriberIPsRepository,
SubscribersRepository $subscribersRepository SubscribersRepository $subscribersRepository
) { ) {
$this->subscriptionUrlFactory = $subscriptionUrlFactory; $this->subscriptionUrlFactory = $subscriptionUrlFactory;
$this->captchaPhrase = $captchaPhrase; $this->captchaPhrase = $captchaPhrase;
$this->captchaSession = $captchaSession;
$this->wp = $wp; $this->wp = $wp;
$this->subscriberIPsRepository = $subscriberIPsRepository; $this->subscriberIPsRepository = $subscriberIPsRepository;
$this->subscribersRepository = $subscribersRepository; $this->subscribersRepository = $subscribersRepository;
@@ -52,26 +44,34 @@ class BuiltInCaptchaValidator implements CaptchaValidator {
if (!$isBuiltinCaptchaRequired) { if (!$isBuiltinCaptchaRequired) {
return true; return true;
} }
// session ID must be set at this point
$sessionId = $data['captcha_session_id'] ?? null;
if (!$sessionId) {
throw new ValidationError(__('CAPTCHA verification failed.', 'mailpoet'));
}
if (empty($data['captcha'])) { if (empty($data['captcha'])) {
throw new ValidationError( throw new ValidationError(
__('Please fill in the CAPTCHA.', 'mailpoet'), __('Please fill in the CAPTCHA.', 'mailpoet'),
[ [
'redirect_url' => $this->subscriptionUrlFactory->getCaptchaUrl($this->captchaSession->getId()), 'redirect_url' => $this->subscriptionUrlFactory->getCaptchaUrl($sessionId),
] ]
); );
} }
$captchaHash = $this->captchaPhrase->getPhrase($this->captchaSession->getId());
$captchaHash = $this->captchaPhrase->getPhrase($sessionId);
if (empty($captchaHash)) { if (empty($captchaHash)) {
throw new ValidationError( throw new ValidationError(
__('Please regenerate the CAPTCHA.', 'mailpoet'), __('Please regenerate the CAPTCHA.', 'mailpoet'),
[ [
'redirect_url' => $this->subscriptionUrlFactory->getCaptchaUrl($this->captchaSession->getId()), 'redirect_url' => $this->subscriptionUrlFactory->getCaptchaUrl($sessionId),
] ]
); );
} }
if (!hash_equals(strtolower($data['captcha']), strtolower($captchaHash))) { if (!hash_equals(strtolower($data['captcha']), strtolower($captchaHash))) {
$this->captchaPhrase->createPhrase($this->captchaSession->getId()); $this->captchaPhrase->createPhrase($sessionId);
throw new ValidationError( throw new ValidationError(
__('The characters entered do not match with the previous CAPTCHA.', 'mailpoet'), __('The characters entered do not match with the previous CAPTCHA.', 'mailpoet'),
[ [

View File

@@ -55,9 +55,9 @@ class CaptchaFormRenderer {
return __("Confirm youre not a robot", 'mailpoet'); return __("Confirm youre not a robot", 'mailpoet');
} }
public function getCaptchaPageContent($sessionId) { public function getCaptchaPageContent(string $sessionId) {
$this->captchaPhrase->createPhrase($sessionId); $this->captchaPhrase->createPhrase($sessionId);
$captchaSessionForm = $this->captchaSession->getFormData(); $captchaSessionForm = $this->captchaSession->getFormData($sessionId);
$showSuccessMessage = !empty($_GET['mailpoet_success']); $showSuccessMessage = !empty($_GET['mailpoet_success']);
$showErrorMessage = !empty($_GET['mailpoet_error']); $showErrorMessage = !empty($_GET['mailpoet_error']);
$formId = 0; $formId = 0;
@@ -112,7 +112,7 @@ class CaptchaFormRenderer {
'id="mailpoet_captcha_form" ' . 'id="mailpoet_captcha_form" ' .
'novalidate>'; 'novalidate>';
$formHtml .= '<input type="hidden" name="data[form_id]" value="' . $formId . '" />'; $formHtml .= '<input type="hidden" name="data[form_id]" value="' . $formId . '" />';
$formHtml .= '<input type="hidden" name="data[captcha_session_id]" value="' . htmlspecialchars((string)$this->captchaSession->getId()) . '" />'; $formHtml .= '<input type="hidden" name="data[captcha_session_id]" value="' . htmlspecialchars($sessionId) . '" />';
$formHtml .= '<input type="hidden" name="api_version" value="v1" />'; $formHtml .= '<input type="hidden" name="api_version" value="v1" />';
$formHtml .= '<input type="hidden" name="endpoint" value="subscribers" />'; $formHtml .= '<input type="hidden" name="endpoint" value="subscribers" />';
$formHtml .= '<input type="hidden" name="mailpoet_method" value="subscribe" />'; $formHtml .= '<input type="hidden" name="mailpoet_method" value="subscribe" />';
@@ -121,8 +121,8 @@ class CaptchaFormRenderer {
$width = 220; $width = 220;
$height = 60; $height = 60;
$captchaUrl = $this->subscriptionUrlFactory->getCaptchaImageUrl($width, $height, $this->captchaSession->getId()); $captchaUrl = $this->subscriptionUrlFactory->getCaptchaImageUrl($width, $height, $sessionId);
$mp3CaptchaUrl = $this->subscriptionUrlFactory->getCaptchaAudioUrl($this->captchaSession->getId()); $mp3CaptchaUrl = $this->subscriptionUrlFactory->getCaptchaAudioUrl($sessionId);
$reloadIcon = Env::$assetsUrl . '/img/icons/image-rotate.svg'; $reloadIcon = Env::$assetsUrl . '/img/icons/image-rotate.svg';
$playIcon = Env::$assetsUrl . '/img/icons/controls-volumeon.svg'; $playIcon = Env::$assetsUrl . '/img/icons/controls-volumeon.svg';

View File

@@ -328,8 +328,10 @@ class Pages {
switch ($this->action) { switch ($this->action) {
case self::ACTION_CAPTCHA: case self::ACTION_CAPTCHA:
$captchaSessionId = $this->data['captcha_session_id'] ?? null;
$captchaSessionId = isset($this->data['captcha_session_id']) ? $this->data['captcha_session_id'] : null; if (!$captchaSessionId) {
return false;
}
$content = $this->captchaRenderer->getCaptchaPageContent($captchaSessionId); $content = $this->captchaRenderer->getCaptchaPageContent($captchaSessionId);
break; break;
case self::ACTION_CONFIRM: case self::ACTION_CONFIRM:

View File

@@ -765,8 +765,7 @@ class SubscribersTest extends \MailPoetTest {
->create(); ->create();
$captchaValue = ['phrase' => 'ihG5W']; $captchaValue = ['phrase' => 'ihG5W'];
$captchaSessionId = 'abcdfgh'; $captchaSessionId = 'abcdfgh';
$this->captchaSession->init($captchaSessionId); $this->captchaSession->setCaptchaHash($captchaSessionId, $captchaValue);
$this->captchaSession->setCaptchaHash($captchaValue);
$response = $this->endpoint->subscribe([ $response = $this->endpoint->subscribe([
$this->obfuscatedEmail => $email, $this->obfuscatedEmail => $email,
'form_id' => $this->form->getId(), 'form_id' => $this->form->getId(),

View File

@@ -15,15 +15,15 @@ class CaptchaRendererTest extends \MailPoetTest {
} }
public function testItRendersImage(): void { public function testItRendersImage(): void {
$sessionId = $this->session->getId(); $sessionId = '123';
$this->session->setCaptchaHash(['phrase' => 'a']); $this->session->setCaptchaHash($sessionId, ['phrase' => 'a']);
$result = $this->testee->renderImage(null, null, $sessionId, true); $result = $this->testee->renderImage($sessionId, null, null, true);
$this->assertStringContainsString('JPEG', $result); $this->assertStringContainsString('JPEG', $result);
} }
public function testItRendersAudio(): void { public function testItRendersAudio(): void {
$sessionId = $this->session->getId(); $sessionId = '123';
$this->session->setCaptchaHash(['phrase' => 'a']); $this->session->setCaptchaHash($sessionId, ['phrase' => 'a']);
$result = $this->testee->renderAudio($sessionId, true); $result = $this->testee->renderAudio($sessionId, true);
$partOfAudio = '(-1166::BBKKQQVZZ^^bbggkkoosxx|'; $partOfAudio = '(-1166::BBKKQQVZZ^^bbggkkoosxx|';
$this->assertStringContainsString($partOfAudio, $result); $this->assertStringContainsString($partOfAudio, $result);

View File

@@ -8,41 +8,29 @@ use MailPoet\WP\Functions as WPFunctions;
class CaptchaSessionTest extends \MailPoetTest { class CaptchaSessionTest extends \MailPoetTest {
const SESSION_ID = 'ABCD'; const SESSION_ID = 'ABCD';
/** @var CaptchaSession */ private CaptchaSession $captchaSession;
private $captchaSession;
public function _before() { public function _before() {
$this->captchaSession = new CaptchaSession(new WPFunctions); $this->captchaSession = new CaptchaSession(new WPFunctions);
$this->captchaSession->init(self::SESSION_ID);
} }
public function testItCanStoreAndRetrieveFormData() { public function testItCanStoreAndRetrieveFormData() {
$formData = ['email' => 'email@example.com']; $formData = ['email' => 'email@example.com'];
$this->captchaSession->setFormData($formData); $this->captchaSession->setFormData(self::SESSION_ID, $formData);
verify($this->captchaSession->getFormData())->equals($formData); verify($this->captchaSession->getFormData(self::SESSION_ID))->equals($formData);
} }
public function testItCanStoreAndRetrieveCaptchaHash() { public function testItCanStoreAndRetrieveCaptchaHash() {
$hash = '1234'; $hash = '1234';
$this->captchaSession->setCaptchaHash($hash); $this->captchaSession->setCaptchaHash(self::SESSION_ID, $hash);
verify($this->captchaSession->getCaptchaHash())->equals($hash); verify($this->captchaSession->getCaptchaHash(self::SESSION_ID))->equals($hash);
} }
public function testItCanResetSessionData() { public function testItCanResetSessionData() {
$this->captchaSession->setFormData(['email' => 'email@example.com']); $this->captchaSession->setFormData(self::SESSION_ID, ['email' => 'email@example.com']);
$this->captchaSession->setCaptchaHash('hash123'); $this->captchaSession->setCaptchaHash(self::SESSION_ID, 'hash123');
$this->captchaSession->reset(); $this->captchaSession->reset(self::SESSION_ID);
verify($this->captchaSession->getFormData())->false(); verify($this->captchaSession->getFormData(self::SESSION_ID))->false();
verify($this->captchaSession->getCaptchaHash())->false(); verify($this->captchaSession->getCaptchaHash(self::SESSION_ID))->false();
}
public function testItAssociatesDataWithSession() {
$hash = '1234';
$this->captchaSession->setCaptchaHash($hash);
verify($this->captchaSession->getCaptchaHash())->equals($hash);
$this->captchaSession->init();
verify($this->captchaSession->getCaptchaHash())->false();
$this->captchaSession->init(self::SESSION_ID);
verify($this->captchaSession->getCaptchaHash())->equals($hash);
} }
} }

View File

@@ -11,23 +11,26 @@ use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon; use MailPoetVendor\Carbon\Carbon;
class BuiltInCaptchaValidatorTest extends \MailPoetTest { class BuiltInCaptchaValidatorTest extends \MailPoetTest {
private BuiltInCaptchaValidator $testee;
private CaptchaSession $session;
/** @var BuiltInCaptchaValidator */
private $testee;
/** @var CaptchaSession */
private $session;
public function _before() { public function _before() {
$this->testee = $this->diContainer->get(BuiltInCaptchaValidator::class); $this->testee = $this->diContainer->get(BuiltInCaptchaValidator::class);
$this->session = $this->diContainer->get(CaptchaSession::class); $this->session = $this->diContainer->get(CaptchaSession::class);
} }
public function testEmptyCaptchaThrowsError() { public function testMissingCaptchaSessionIdThrowsError() {
try { try {
$this->testee->validate([]); $this->testee->validate([]);
} catch (ValidationError $error) {
$meta = $error->getMeta();
$this->assertEquals('CAPTCHA verification failed.', $meta['error']);
}
}
public function testEmptyCaptchaThrowsError() {
try {
$this->testee->validate(['captcha_session_id' => '123']);
} catch (ValidationError $error) { } catch (ValidationError $error) {
$meta = $error->getMeta(); $meta = $error->getMeta();
$this->assertEquals('Please fill in the CAPTCHA.', $meta['error']); $this->assertEquals('Please fill in the CAPTCHA.', $meta['error']);
@@ -36,11 +39,10 @@ class BuiltInCaptchaValidatorTest extends \MailPoetTest {
} }
public function testWrongCaptchaThrowsError() { public function testWrongCaptchaThrowsError() {
$sessionId = '123';
$this->session->init(); $this->session->setCaptchaHash($sessionId, ['phrase' => 'abc']);
$this->session->setCaptchaHash(['phrase' => 'abc']);
try { try {
$this->testee->validate(['captcha' => '123']); $this->testee->validate(['captcha' => 'xyz', 'captcha_session_id' => $sessionId]);
} catch (ValidationError $error) { } catch (ValidationError $error) {
$meta = $error->getMeta(); $meta = $error->getMeta();
$this->assertEquals('The characters entered do not match with the previous CAPTCHA.', $meta['error']); $this->assertEquals('The characters entered do not match with the previous CAPTCHA.', $meta['error']);
@@ -48,11 +50,10 @@ class BuiltInCaptchaValidatorTest extends \MailPoetTest {
} }
public function testThrowsErrorWhenCaptchaHasTimedOut() { public function testThrowsErrorWhenCaptchaHasTimedOut() {
$sessionId = '123';
$this->session->init(); $this->session->setCaptchaHash($sessionId, ['phrase' => null]);
$this->session->setCaptchaHash(['phrase' => null]);
try { try {
$this->testee->validate(['captcha' => '123']); $this->testee->validate(['captcha' => 'xyz', 'captcha_session_id' => $sessionId]);
} catch (ValidationError $error) { } catch (ValidationError $error) {
$meta = $error->getMeta(); $meta = $error->getMeta();
$this->assertEquals('Please regenerate the CAPTCHA.', $meta['error']); $this->assertEquals('Please regenerate the CAPTCHA.', $meta['error']);
@@ -61,10 +62,9 @@ class BuiltInCaptchaValidatorTest extends \MailPoetTest {
} }
public function testReturnsTrueWhenCaptchaIsSolved() { public function testReturnsTrueWhenCaptchaIsSolved() {
$sessionId = '123';
$this->session->init(); $this->session->setCaptchaHash($sessionId, ['phrase' => 'abc']);
$this->session->setCaptchaHash(['phrase' => 'abc']); $this->assertTrue($this->testee->validate(['captcha' => 'abc', 'captcha_session_id' => $sessionId]));
$this->assertTrue($this->testee->validate(['captcha' => 'abc']));
} }
public function testItRequiresCaptchaForFirstSubscription() { public function testItRequiresCaptchaForFirstSubscription() {

View File

@@ -9,7 +9,6 @@ use MailPoet\Subscription\CaptchaFormRenderer;
class CaptchaFormRendererTest extends \MailPoetTest { class CaptchaFormRendererTest extends \MailPoetTest {
public function testCaptchaSubmitTextIsConfigurable() { public function testCaptchaSubmitTextIsConfigurable() {
$expectedLabel = 'EXPECTED_LABEL'; $expectedLabel = 'EXPECTED_LABEL';
$formRepository = $this->diContainer->get(FormsRepository::class); $formRepository = $this->diContainer->get(FormsRepository::class);
$form = new FormEntity('captcha-render-test-form'); $form = new FormEntity('captcha-render-test-form');
@@ -31,17 +30,17 @@ class CaptchaFormRendererTest extends \MailPoetTest {
$form->setId(1); $form->setId(1);
$formRepository->persist($form); $formRepository->persist($form);
$formRepository->flush(); $formRepository->flush();
$sessionId = '123';
$captchaSession = $this->diContainer->get(CaptchaSession::class); $captchaSession = $this->diContainer->get(CaptchaSession::class);
$captchaSession->init(); $captchaSession->setFormData($sessionId, ['form_id' => $form->getId()]);
$captchaSession->setFormData(['form_id' => $form->getId()]);
$testee = $this->diContainer->get(CaptchaFormRenderer::class); $testee = $this->diContainer->get(CaptchaFormRenderer::class);
$result = $testee->getCaptchaPageContent($captchaSession->getId()); $result = $testee->getCaptchaPageContent($sessionId);
$this->assertStringContainsString('value="' . $expectedLabel . '"', $result); $this->assertStringContainsString('value="' . $expectedLabel . '"', $result);
} }
public function testCaptchaSubmitTextHasDefault() { public function testCaptchaSubmitTextHasDefault() {
$formRepository = $this->diContainer->get(FormsRepository::class); $formRepository = $this->diContainer->get(FormsRepository::class);
$form = new FormEntity('captcha-render-test-form'); $form = new FormEntity('captcha-render-test-form');
$form->setBody([ $form->setBody([
@@ -62,12 +61,13 @@ class CaptchaFormRendererTest extends \MailPoetTest {
$form->setId(1); $form->setId(1);
$formRepository->persist($form); $formRepository->persist($form);
$formRepository->flush(); $formRepository->flush();
$sessionId = '123';
$captchaSession = $this->diContainer->get(CaptchaSession::class); $captchaSession = $this->diContainer->get(CaptchaSession::class);
$captchaSession->init(); $captchaSession->setFormData($sessionId, ['form_id' => $form->getId()]);
$captchaSession->setFormData(['form_id' => $form->getId()]);
$testee = $this->diContainer->get(CaptchaFormRenderer::class); $testee = $this->diContainer->get(CaptchaFormRenderer::class);
$result = $testee->getCaptchaPageContent($captchaSession->getId()); $result = $testee->getCaptchaPageContent($sessionId);
$this->assertStringContainsString('value="Subscribe"', $result); $this->assertStringContainsString('value="Subscribe"', $result);
} }

View File

@@ -7,32 +7,36 @@ use MailPoetVendor\Gregwar\Captcha\PhraseBuilder;
class CaptchaPhraseTest extends \MailPoetUnitTest { class CaptchaPhraseTest extends \MailPoetUnitTest {
public function testItCreatesPhrase(): void { public function testItCreatesPhrase(): void {
$expectedSessionId = '123';
$expectedPhrase = 'abc'; $expectedPhrase = 'abc';
$session = $this->make(CaptchaSession::class, [ $session = $this->make(CaptchaSession::class, [
'setCaptchaHash' => Stub\Expected::once(function ($data) use ($expectedPhrase) { 'setCaptchaHash' => Stub\Expected::once(function ($sessionId, $data) use ($expectedSessionId, $expectedPhrase) {
$this->assertSame($expectedSessionId, $sessionId);
$this->assertSame($expectedPhrase, $data['phrase']); $this->assertSame($expectedPhrase, $data['phrase']);
}), }),
]); ]);
$phraseBuilder = $this->make(PhraseBuilder::class, ['build' => $expectedPhrase]); $phraseBuilder = $this->make(PhraseBuilder::class, ['build' => $expectedPhrase]);
$captchaPhrase = new CaptchaPhrase($session, $phraseBuilder); $captchaPhrase = new CaptchaPhrase($session, $phraseBuilder);
$phrase = $captchaPhrase->createPhrase('123'); $phrase = $captchaPhrase->createPhrase($expectedSessionId);
$this->assertSame($expectedPhrase, $phrase); $this->assertSame($expectedPhrase, $phrase);
} }
public function testItReturnsPhrase(): void { public function testItReturnsPhrase(): void {
$expectedSessionId = '123';
$expectedPhrase = 'abc'; $expectedPhrase = 'abc';
$session = $this->make(CaptchaSession::class, [ $session = $this->make(CaptchaSession::class, [
'getCaptchaHash' => Stub\Expected::once(function () use ($expectedPhrase) { 'getCaptchaHash' => Stub\Expected::once(function ($sessionId) use ($expectedSessionId, $expectedPhrase) {
$this->assertSame($expectedSessionId, $sessionId);
return ['phrase' => $expectedPhrase]; return ['phrase' => $expectedPhrase];
}), }),
]); ]);
$phraseBuilder = $this->make(PhraseBuilder::class, ['build' => $expectedPhrase]); $phraseBuilder = $this->make(PhraseBuilder::class, ['build' => $expectedPhrase]);
$captchaPhrase = new CaptchaPhrase($session, $phraseBuilder); $captchaPhrase = new CaptchaPhrase($session, $phraseBuilder);
$phrase = $captchaPhrase->getPhrase('123'); $phrase = $captchaPhrase->getPhrase($expectedSessionId);
$this->assertSame($expectedPhrase, $phrase); $this->assertSame($expectedPhrase, $phrase);
} }
} }

View File

@@ -44,13 +44,11 @@ class BuiltInCaptchaValidatorTest extends \MailPoetUnitTest {
], ],
$this $this
); );
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$subscriberIpRepository = Stub::makeEmpty(SubscriberIPsRepository::class); $subscriberIpRepository = Stub::makeEmpty(SubscriberIPsRepository::class);
$subscriberRepository = Stub::makeEmpty(SubscribersRepository::class); $subscriberRepository = Stub::makeEmpty(SubscribersRepository::class);
$testee = new BuiltInCaptchaValidator( $testee = new BuiltInCaptchaValidator(
$subscriptionUrlFactory, $subscriptionUrlFactory,
$captchaPhrase, $captchaPhrase,
$captchaSession,
$this->wp, $this->wp,
$subscriberIpRepository, $subscriberIpRepository,
$subscriberRepository $subscriberRepository
@@ -58,6 +56,7 @@ class BuiltInCaptchaValidatorTest extends \MailPoetUnitTest {
$data = [ $data = [
'captcha' => $phrase, 'captcha' => $phrase,
'captcha_session_id' => '123',
]; ];
verify($testee->validate($data))->true(); verify($testee->validate($data))->true();
@@ -76,14 +75,12 @@ class BuiltInCaptchaValidatorTest extends \MailPoetUnitTest {
], ],
$this $this
); );
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$subscriberIpRepository = Stub::makeEmpty(SubscriberIPsRepository::class); $subscriberIpRepository = Stub::makeEmpty(SubscriberIPsRepository::class);
$subscriberRepository = Stub::makeEmpty(SubscribersRepository::class); $subscriberRepository = Stub::makeEmpty(SubscribersRepository::class);
$testee = new BuiltInCaptchaValidator( $testee = new BuiltInCaptchaValidator(
$subscriptionUrlFactory, $subscriptionUrlFactory,
$captchaPhrase, $captchaPhrase,
$captchaSession,
$wp, $wp,
$subscriberIpRepository, $subscriberIpRepository,
$subscriberRepository $subscriberRepository
@@ -91,6 +88,7 @@ class BuiltInCaptchaValidatorTest extends \MailPoetUnitTest {
$data = [ $data = [
'captcha' => $phrase, 'captcha' => $phrase,
'captcha_session_id' => '123',
]; ];
verify($testee->validate($data))->true(); verify($testee->validate($data))->true();
} }
@@ -186,14 +184,14 @@ class BuiltInCaptchaValidatorTest extends \MailPoetUnitTest {
$testee = new BuiltInCaptchaValidator( $testee = new BuiltInCaptchaValidator(
$subscriptionUrlFactory, $subscriptionUrlFactory,
$captchaPhrase, $captchaPhrase,
$captchaSession,
$wp, $wp,
$subscriberIpRepository, $subscriberIpRepository,
$subscriberRepository $subscriberRepository
); );
$data = [ $data = [
'captcha' => $phrase, 'captcha' => $phrase,
'captcha_session_id' => '123',
]; ];
verify($testee->validate($data))->true(); verify($testee->validate($data))->true();
@@ -223,7 +221,6 @@ class BuiltInCaptchaValidatorTest extends \MailPoetUnitTest {
$testee = new BuiltInCaptchaValidator( $testee = new BuiltInCaptchaValidator(
$subscriptionUrlFactory, $subscriptionUrlFactory,
$captchaPhrase, $captchaPhrase,
$captchaSession,
$this->wp, $this->wp,
$subscriberIpRepository, $subscriberIpRepository,
$subscriberRepository $subscriberRepository
@@ -231,6 +228,7 @@ class BuiltInCaptchaValidatorTest extends \MailPoetUnitTest {
$data = [ $data = [
'captcha' => $phrase, 'captcha' => $phrase,
'captcha_session_id' => '123',
]; ];
$error = null; $error = null;
try { try {
@@ -254,13 +252,11 @@ class BuiltInCaptchaValidatorTest extends \MailPoetUnitTest {
], ],
$this $this
); );
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$subscriberIpRepository = Stub::makeEmpty(SubscriberIPsRepository::class); $subscriberIpRepository = Stub::makeEmpty(SubscriberIPsRepository::class);
$subscriberRepository = Stub::makeEmpty(SubscribersRepository::class); $subscriberRepository = Stub::makeEmpty(SubscribersRepository::class);
$testee = new BuiltInCaptchaValidator( $testee = new BuiltInCaptchaValidator(
$subscriptionUrlFactory, $subscriptionUrlFactory,
$captchaPhrase, $captchaPhrase,
$captchaSession,
$this->wp, $this->wp,
$subscriberIpRepository, $subscriberIpRepository,
$subscriberRepository $subscriberRepository
@@ -268,6 +264,7 @@ class BuiltInCaptchaValidatorTest extends \MailPoetUnitTest {
$data = [ $data = [
'captcha' => $phrase, 'captcha' => $phrase,
'captcha_session_id' => '123',
]; ];
$error = null; $error = null;
try { try {
@@ -297,13 +294,11 @@ class BuiltInCaptchaValidatorTest extends \MailPoetUnitTest {
], ],
$this $this
); );
$captchaSession = Stub::makeEmpty(CaptchaSession::class);
$subscriberIpRepository = Stub::makeEmpty(SubscriberIPsRepository::class); $subscriberIpRepository = Stub::makeEmpty(SubscriberIPsRepository::class);
$subscriberRepository = Stub::makeEmpty(SubscribersRepository::class); $subscriberRepository = Stub::makeEmpty(SubscribersRepository::class);
$testee = new BuiltInCaptchaValidator( $testee = new BuiltInCaptchaValidator(
$subscriptionUrlFactory, $subscriptionUrlFactory,
$captchaPhrase, $captchaPhrase,
$captchaSession,
$this->wp, $this->wp,
$subscriberIpRepository, $subscriberIpRepository,
$subscriberRepository $subscriberRepository
@@ -311,6 +306,7 @@ class BuiltInCaptchaValidatorTest extends \MailPoetUnitTest {
$data = [ $data = [
'captcha' => '', 'captcha' => '',
'captcha_session_id' => '123',
]; ];
$error = null; $error = null;
try { try {