Current File : /var/www/pediatribu/wp-content/plugins/mailpoet/lib/Cron/Workers/SendingQueue/Tasks/Newsletter.php |
<?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\EmailEditor\Engine\Personalizer;
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;
/** @var Personalizer */
private $personalizer;
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);
$this->personalizer = ContainerWrapper::getInstance()->get(Personalizer::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('Can‘t 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);
if ($newsletter->getWpPostId() !== null) {
$this->personalizer->set_context([
'recipient_email' => $subscriber->getEmail(),
'newsletter_id' => $newsletter->getId(),
'queue_id' => $queue->getId(),
]);
foreach ($preparedNewsletter as $key => $content) {
$preparedNewsletter[$key] = $this->personalizer->personalize_content($content);
}
}
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();
}
}