oont-contents/plugins/mailpoet/lib/Cron/Workers/SendingQueue/Tasks/Newsletter.php
2025-02-08 15:10:23 +01:00

373 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Links as LinksTask;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Posts as PostsTask;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Shortcodes as ShortcodesTask;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Mailer\MailerLog;
use MailPoet\Newsletter\Links\Links as NewsletterLinks;
use MailPoet\Newsletter\NewsletterDeleteController;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Renderer\PostProcess\OpenTracking;
use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\RuntimeException;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Statistics\GATracking;
use MailPoet\Util\Helpers;
use MailPoet\Util\pQuery\DomNode;
use MailPoet\Util\pQuery\pQuery;
use MailPoet\WP\Emoji;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
class Newsletter {
public $trackingEnabled;
public $trackingImageInserted;
/** @var WPFunctions */
private $wp;
/** @var PostsTask */
private $postsTask;
/** @var GATracking */
private $gaTracking;
/** @var LoggerFactory */
private $loggerFactory;
/** @var Renderer */
private $renderer;
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var NewsletterDeleteController */
private $newsletterDeleteController;
/** @var Emoji */
private $emoji;
/** @var LinksTask */
private $linksTask;
/** @var NewsletterLinks */
private $newsletterLinks;
/** @var SendingQueuesRepository */
private $sendingQueuesRepository;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var ScheduledTasksRepository */
private $scheduledTasksRepository;
public function __construct(
WPFunctions $wp = null,
PostsTask $postsTask = null,
GATracking $gaTracking = null,
Emoji $emoji = null
) {
$trackingConfig = ContainerWrapper::getInstance()->get(TrackingConfig::class);
$this->trackingEnabled = $trackingConfig->isEmailTrackingEnabled();
if ($wp === null) {
$wp = new WPFunctions;
}
$this->wp = $wp;
if ($postsTask === null) {
$postsTask = new PostsTask;
}
$this->postsTask = $postsTask;
if ($gaTracking === null) {
$gaTracking = ContainerWrapper::getInstance()->get(GATracking::class);
}
$this->gaTracking = $gaTracking;
$this->loggerFactory = LoggerFactory::getInstance();
if ($emoji === null) {
$emoji = new Emoji();
}
$this->emoji = $emoji;
$this->renderer = ContainerWrapper::getInstance()->get(Renderer::class);
$this->newslettersRepository = ContainerWrapper::getInstance()->get(NewslettersRepository::class);
$this->newsletterDeleteController = ContainerWrapper::getInstance()->get(NewsletterDeleteController::class);
$this->linksTask = ContainerWrapper::getInstance()->get(LinksTask::class);
$this->newsletterLinks = ContainerWrapper::getInstance()->get(NewsletterLinks::class);
$this->sendingQueuesRepository = ContainerWrapper::getInstance()->get(SendingQueuesRepository::class);
$this->segmentsRepository = ContainerWrapper::getInstance()->get(SegmentsRepository::class);
$this->scheduledTasksRepository = ContainerWrapper::getInstance()->get(ScheduledTasksRepository::class);
}
public function getNewsletterFromQueue(ScheduledTaskEntity $task): ?NewsletterEntity {
// get existing active or sending newsletter
$queue = $task->getSendingQueue();
$newsletter = $queue ? $queue->getNewsletter() : null;
if (
is_null($newsletter)
|| $newsletter->getDeletedAt() !== null
|| !in_array($newsletter->getStatus(), [NewsletterEntity::STATUS_ACTIVE, NewsletterEntity::STATUS_SENDING])
|| $newsletter->getStatus() === NewsletterEntity::STATUS_CORRUPT
) {
$this->recoverFromInvalidState($task);
return null;
}
// if this is a notification history, get existing active or sending parent newsletter
if ($newsletter->getType() == NewsletterEntity::TYPE_NOTIFICATION_HISTORY) {
$parentNewsletter = $newsletter->getParent();
if (
is_null($parentNewsletter)
|| $parentNewsletter->getDeletedAt() !== null
|| !in_array($parentNewsletter->getStatus(), [NewsletterEntity::STATUS_ACTIVE, NewsletterEntity::STATUS_SENDING])
) {
return null;
}
}
return $newsletter;
}
/**
* Pre-processes the newsletter before sending.
* - Renders the newsletter
* - Adds tracking
* - Extracts links
* - Checks if the newsletter is a post notification and if it contains at least 1 ALC post.
* If not it deletes the notification history record and all associate entities.
*
* @return NewsletterEntity|false - Returns false only if the newsletter is a post notification history and was deleted.
*
*/
public function preProcessNewsletter(NewsletterEntity $newsletter, ScheduledTaskEntity $task) {
// return the newsletter if it was previously rendered
$queue = $task->getSendingQueue();
if (!$queue) {
throw new RuntimeException('Cant pre-process newsletter without queue.');
}
if ($queue->getNewsletterRenderedBody() !== null) {
return $newsletter;
}
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
'pre-processing newsletter',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()]
);
$campaignId = null;
// if tracking is enabled, do additional processing
if ($this->trackingEnabled) {
// hook to the newsletter post-processing filter and add tracking image
$this->trackingImageInserted = OpenTracking::addTrackingImage();
// render newsletter
$renderedNewsletter = $this->renderer->render($newsletter, $queue);
$renderedNewsletter = $this->wp->applyFilters(
'mailpoet_sending_newsletter_render_after_pre_process',
$renderedNewsletter,
$newsletter
);
if (is_array($renderedNewsletter)) {
$campaignId = $this->calculateCampaignId($newsletter, $renderedNewsletter);
}
$renderedNewsletter = $this->gaTracking->applyGATracking($renderedNewsletter, $newsletter);
// hash and save all links
$renderedNewsletter = $this->linksTask->process($renderedNewsletter, $newsletter, $queue);
} else {
// render newsletter
$renderedNewsletter = $this->renderer->render($newsletter, $queue);
$renderedNewsletter = $this->wp->applyFilters(
'mailpoet_sending_newsletter_render_after_pre_process',
$renderedNewsletter,
$newsletter
);
if (is_array($renderedNewsletter)) {
$campaignId = $this->calculateCampaignId($newsletter, $renderedNewsletter);
}
$renderedNewsletter = $this->gaTracking->applyGATracking($renderedNewsletter, $newsletter);
}
// check if this is a post notification and if it contains at least 1 ALC post
if (
$newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION_HISTORY &&
$this->postsTask->getAlcPostsCount($renderedNewsletter, $newsletter) === 0
) {
// delete notification history record since it will never be sent
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info(
'no posts in post notification, deleting it',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()]
);
$this->newsletterDeleteController->bulkDelete([(int)$newsletter->getId()]);
return false;
}
// extract and save newsletter posts
$this->postsTask->extractAndSave($renderedNewsletter, $newsletter);
if ($campaignId !== null) {
$this->sendingQueuesRepository->saveCampaignId($queue, $campaignId);
}
$filterSegmentId = $newsletter->getFilterSegmentId();
if ($filterSegmentId) {
$filterSegment = $this->segmentsRepository->findOneById($filterSegmentId);
if ($filterSegment instanceof SegmentEntity && $filterSegment->getType() === SegmentEntity::TYPE_DYNAMIC) {
$this->sendingQueuesRepository->saveFilterSegmentMeta($queue, $filterSegment);
}
}
// update queue with the rendered and pre-processed newsletter
$queue->setNewsletterRenderedSubject(
ShortcodesTask::process(
$newsletter->getSubject(),
$renderedNewsletter['html'],
$newsletter,
null,
$queue
)
);
// if the rendered subject is empty, use a default subject,
// having no subject in a newsletter is considered spammy
if (empty(trim((string)$queue->getNewsletterRenderedSubject()))) {
$queue->setNewsletterRenderedSubject(__('No subject', 'mailpoet'));
}
$renderedNewsletter = $this->emoji->encodeEmojisInBody($renderedNewsletter);
$queue->setNewsletterRenderedBody($renderedNewsletter);
try {
$this->sendingQueuesRepository->flush();
} catch (\Throwable $e) {
$this->stopNewsletterPreProcessing(sprintf('QUEUE-%d-SAVE', $queue->getId()));
}
return $newsletter;
}
/**
* Shortcodes and links will be replaced in the subject, html and text body
* to speed the processing, join content into a continuous string.
*/
public function prepareNewsletterForSending(NewsletterEntity $newsletter, SubscriberEntity $subscriber, SendingQueueEntity $queue): array {
$renderedNewsletter = $queue->getNewsletterRenderedBody();
$renderedNewsletter = $this->emoji->decodeEmojisInBody($renderedNewsletter);
$preparedNewsletter = Helpers::joinObject(
[
$queue->getNewsletterRenderedSubject(),
$renderedNewsletter['html'],
$renderedNewsletter['text'],
]
);
$preparedNewsletter = ShortcodesTask::process(
$preparedNewsletter,
null,
$newsletter,
$subscriber,
$queue
);
if ($this->trackingEnabled) {
$preparedNewsletter = $this->newsletterLinks->replaceSubscriberData(
$subscriber->getId(),
$queue->getId(),
$preparedNewsletter
);
}
$preparedNewsletter = Helpers::splitObject($preparedNewsletter);
return [
'id' => $newsletter->getId(),
'subject' => $preparedNewsletter[0],
'body' => [
'html' => $preparedNewsletter[1],
'text' => $preparedNewsletter[2],
],
];
}
public function markNewsletterAsSent(NewsletterEntity $newsletter) {
// if it's a standard or notification history newsletter, update its status
if (
$newsletter->getType() === NewsletterEntity::TYPE_STANDARD ||
$newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION_HISTORY
) {
$newsletter->setStatus(NewsletterEntity::STATUS_SENT);
$newsletter->setSentAt(Carbon::now()->millisecond(0));
$this->newslettersRepository->persist($newsletter);
$this->newslettersRepository->flush();
}
}
public function stopNewsletterPreProcessing($errorCode = null) {
MailerLog::processError(
'queue_save',
__('There was an error processing your newsletter during sending. If possible, please contact us and report this issue.', 'mailpoet'),
$errorCode
);
}
/**
* @param NewsletterEntity $newsletter
* @param array $renderedNewsletters - The pre-processed renderered newsletters, before link tracking has been added or shortcodes have been processed.
*
* @return string
*/
public function calculateCampaignId(NewsletterEntity $newsletter, array $renderedNewsletters): string {
$relevantContent = [
$newsletter->getId(),
$newsletter->getSubject(),
];
if (isset($renderedNewsletters['text'])) {
$relevantContent[] = $renderedNewsletters['text'];
}
// The text version of emails contains just the alt text of images, which could be the same for multiple images. In order to ensure
// campaign IDs change when images change, we should consider all image URLs.
if (isset($renderedNewsletters['html'])) {
$html = pQuery::parseStr($renderedNewsletters['html']);
if ($html instanceof DomNode) {
foreach ($html->query('img') as $imageNode) {
$src = $imageNode->getAttribute('src');
if (is_string($src)) {
$relevantContent[] = $src;
}
}
}
}
return substr(md5(implode('|', $relevantContent)), 0, 16);
}
/**
* This method recovers the scheduled task and newsletter from a state when sending cannot proceed.
*/
private function recoverFromInvalidState(ScheduledTaskEntity $task): void {
// When newsletter does not exist, we need to remove the scheduled task and sending queue.
$queue = $task->getSendingQueue();
$newsletter = $queue ? $queue->getNewsletter() : null;
if (!$newsletter) {
$this->scheduledTasksRepository->remove($task);
if ($queue) {
$this->sendingQueuesRepository->remove($queue);
}
$this->sendingQueuesRepository->flush();
return;
}
// Only deleted newsletter or newsletter with unexpected state should pass here.
// Because this state cannot proceed with sending, we need to pause the scheduled task.
$task->setStatus(ScheduledTaskEntity::STATUS_PAUSED);
$this->scheduledTasksRepository->flush();
}
}