Check validity on activating newsletters

This prevents users from activating automatic emails from listing pages
that are not valid.

This also adds more checks for the content of a newsletter, requiring
that a newsletter have at least one content block in order to be valid.
This change makes the server side validation check match what we're
checking in the editor in mailpoet/assets/js/src/newsletter_editor
/components/save.js and mailpoet/assets/js/src/newsletters/send.jsx

[MAILPOET-4236]
This commit is contained in:
John Oleksowicz
2022-04-13 16:28:06 -05:00
committed by Veljko V
parent fdaf22d46b
commit 27a86d2ca6
4 changed files with 63 additions and 10 deletions

View File

@ -22,6 +22,7 @@ use MailPoet\Newsletter\Preview\SendPreviewException;
use MailPoet\Newsletter\Scheduler\PostNotificationScheduler; use MailPoet\Newsletter\Scheduler\PostNotificationScheduler;
use MailPoet\Newsletter\Scheduler\Scheduler; use MailPoet\Newsletter\Scheduler\Scheduler;
use MailPoet\Newsletter\Url as NewsletterUrl; use MailPoet\Newsletter\Url as NewsletterUrl;
use MailPoet\Newsletter\Validator;
use MailPoet\Settings\SettingsController; use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig; use MailPoet\Settings\TrackingConfig;
use MailPoet\UnexpectedValueException; use MailPoet\UnexpectedValueException;
@ -79,6 +80,9 @@ class Newsletters extends APIEndpoint {
/** @var TrackingConfig */ /** @var TrackingConfig */
private $trackingConfig; private $trackingConfig;
/** @var Validator */
private $validator;
/** @var Scheduler */ /** @var Scheduler */
private $scheduler; private $scheduler;
@ -97,7 +101,8 @@ class Newsletters extends APIEndpoint {
NewsletterSaveController $newsletterSaveController, NewsletterSaveController $newsletterSaveController,
NewsletterUrl $newsletterUrl, NewsletterUrl $newsletterUrl,
TrackingConfig $trackingConfig, TrackingConfig $trackingConfig,
Scheduler $scheduler Scheduler $scheduler,
Validator $validator
) { ) {
$this->listingHandler = $listingHandler; $this->listingHandler = $listingHandler;
$this->wp = $wp; $this->wp = $wp;
@ -114,6 +119,7 @@ class Newsletters extends APIEndpoint {
$this->newsletterUrl = $newsletterUrl; $this->newsletterUrl = $newsletterUrl;
$this->trackingConfig = $trackingConfig; $this->trackingConfig = $trackingConfig;
$this->scheduler = $scheduler; $this->scheduler = $scheduler;
$this->validator = $validator;
} }
public function get($data = []) { public function get($data = []) {
@ -184,6 +190,13 @@ class Newsletters extends APIEndpoint {
]); ]);
} }
if ($status === NewsletterEntity::STATUS_ACTIVE) {
$validationError = $this->validator->validate($newsletter);
if ($validationError !== null) {
return $this->errorResponse([APIError::FORBIDDEN => $validationError], [], Response::STATUS_FORBIDDEN);
}
}
// if the re-engagement email doesn't contain the re-engage link, it can't be activated // if the re-engagement email doesn't contain the re-engage link, it can't be activated
if ($newsletter->getType() === NewsletterEntity::TYPE_RE_ENGAGEMENT && $status === NewsletterEntity::STATUS_ACTIVE) { if ($newsletter->getType() === NewsletterEntity::TYPE_RE_ENGAGEMENT && $status === NewsletterEntity::STATUS_ACTIVE) {
if (strpos($newsletter->getContent(), '[link:subscription_re_engage_url]') === false) { if (strpos($newsletter->getContent(), '[link:subscription_re_engage_url]') === false) {

View File

@ -22,21 +22,31 @@ class Validator {
&& is_array($newsletterEntity->getBody()) && is_array($newsletterEntity->getBody())
&& $newsletterEntity->getBody()['content'] && $newsletterEntity->getBody()['content']
) { ) {
$body = json_encode($newsletterEntity->getBody()['content']); $content = $newsletterEntity->getBody()['content'];
if ($body === false) { $encodedBody = json_encode($content);
return __('Poet, please add prose to your masterpiece before you send it to your followers.'); if ($encodedBody === false) {
return $this->emptyContentErrorMessage();
} else {
$blocks = $content['blocks'] ?? [];
if (empty($blocks)) {
return $this->emptyContentErrorMessage();
}
} }
if ( if (
$this->bridge->isMailpoetSendingServiceEnabled() $this->bridge->isMailpoetSendingServiceEnabled()
&& (strpos($body, '[link:subscription_unsubscribe_url]') === false) && (strpos($encodedBody, '[link:subscription_unsubscribe_url]') === false)
&& (strpos($body, '[link:subscription_unsubscribe]') === false) && (strpos($encodedBody, '[link:subscription_unsubscribe]') === false)
) { ) {
return __('All emails must include an "Unsubscribe" link. Add a footer widget to your email to continue.'); return __('All emails must include an "Unsubscribe" link. Add a footer widget to your email to continue.');
} }
} else { } else {
return __('Poet, please add prose to your masterpiece before you send it to your followers.'); return $this->emptyContentErrorMessage();
} }
return null; return null;
} }
private function emptyContentErrorMessage(): string {
return __('Poet, please add prose to your masterpiece before you send it to your followers.');
}
} }

View File

@ -36,6 +36,7 @@ use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository; use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Newsletter\Statistics\NewsletterStatisticsRepository; use MailPoet\Newsletter\Statistics\NewsletterStatisticsRepository;
use MailPoet\Newsletter\Url; use MailPoet\Newsletter\Url;
use MailPoet\Newsletter\Validator;
use MailPoet\Router\Router; use MailPoet\Router\Router;
use MailPoet\Segments\SegmentsRepository; use MailPoet\Segments\SegmentsRepository;
use MailPoet\Settings\SettingsController; use MailPoet\Settings\SettingsController;
@ -695,14 +696,15 @@ class NewslettersTest extends \MailPoetTest {
$this->diContainer->get(NewsletterSaveController::class), $this->diContainer->get(NewsletterSaveController::class),
$this->diContainer->get(Url::class), $this->diContainer->get(Url::class),
$this->diContainer->get(TrackingConfig::class), $this->diContainer->get(TrackingConfig::class),
$this->scheduler $this->scheduler,
$this->diContainer->get(Validator::class)
); );
} }
private function createNewsletter(string $subject, string $type): NewsletterEntity { private function createNewsletter(string $subject, string $type): NewsletterEntity {
$newsletter = new NewsletterEntity(); $newsletter = new NewsletterEntity();
$newsletter->setSubject($subject); $newsletter->setSubject($subject);
$newsletter->setBody(Fixtures::get('newsletter_body_template')); $newsletter->setBody((array)json_decode(Fixtures::get('newsletter_body_template'), true));
$newsletter->setType($type); $newsletter->setType($type);
$newsletter->setHash(Security::generateHash()); $newsletter->setHash(Security::generateHash());
$this->newsletterRepository->persist($newsletter); $this->newsletterRepository->persist($newsletter);

View File

@ -143,7 +143,21 @@ class SendingQueueTest extends \MailPoetTest {
$newsletter = new NewsletterEntity(); $newsletter = new NewsletterEntity();
$newsletter->setSubject('subject'); $newsletter->setSubject('subject');
$newsletter->setType(NewsletterEntity::TYPE_STANDARD); $newsletter->setType(NewsletterEntity::TYPE_STANDARD);
$newsletter->setBody(['content' => ['type' => 'container', 'columnLayout' => false, 'orientation' => 'vertical']]); $newsletter->setBody([
'content' =>
[
'type' => 'container',
'columnLayout' => false,
'orientation' => 'vertical',
'blocks' => [
[
'type' => 'header',
'link' => '',
'text' => 'Hello!'
]
]
]
]);
$this->entityManager->persist($newsletter); $this->entityManager->persist($newsletter);
$this->entityManager->flush(); $this->entityManager->flush();
$sendingQueue = new SendingQueueAPI( $sendingQueue = new SendingQueueAPI(
@ -165,6 +179,20 @@ class SendingQueueTest extends \MailPoetTest {
expect($response['errors'][0]['error'])->stringContainsString('bad_request'); expect($response['errors'][0]['error'])->stringContainsString('bad_request');
} }
public function testItRejectsNewslettersWithoutContentBlocks() {
$newsletter = new NewsletterEntity();
$newsletter->setSubject('subject');
$newsletter->setType(NewsletterEntity::TYPE_STANDARD);
$newsletter->setBody(['content' => ['type' => 'container', 'columnLayout' => false, 'orientation' => 'vertical']]);
$this->entityManager->persist($newsletter);
$this->entityManager->flush();
$sendingQueue = $this->diContainer->get(SendingQueueAPI::class);
$response = $sendingQueue->add(['newsletter_id' => $newsletter->getId()]);
$result = $response->getData();
expect($result['errors'][0])->array();
expect($result['errors'][0]['message'])->stringContainsString('Poet, please add prose to your masterpiece before you send it to your followers');
}
private function _createOrUpdateNewsletterOptions(NewsletterEntity $newsletter, $newsletterType, $options) { private function _createOrUpdateNewsletterOptions(NewsletterEntity $newsletter, $newsletterType, $options) {
$newsletterOptionFieldRepository = $this->diContainer->get(NewsletterOptionFieldsRepository::class); $newsletterOptionFieldRepository = $this->diContainer->get(NewsletterOptionFieldsRepository::class);
$newsletterOptionRepository = $this->diContainer->get(NewsletterOptionsRepository::class); $newsletterOptionRepository = $this->diContainer->get(NewsletterOptionsRepository::class);