This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,34 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Services\Bridge;
class AuthorizedSendingEmailsCheck extends SimpleWorker {
const TASK_TYPE = 'authorized_email_addresses_check';
const AUTOMATIC_SCHEDULING = false;
/** @var AuthorizedEmailsController */
private $authorizedEmailsController;
public function __construct(
AuthorizedEmailsController $authorizedEmailsController
) {
$this->authorizedEmailsController = $authorizedEmailsController;
parent::__construct();
}
public function checkProcessingRequirements() {
return Bridge::isMPSendingServiceEnabled();
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$this->authorizedEmailsController->checkAuthorizedEmailAddresses();
return true;
}
}
@@ -0,0 +1,71 @@
<?php declare(strict_types = 1);
namespace MailPoet\Cron\Workers\Automations;
if (!defined('ABSPATH')) exit;
use MailPoet\Automation\Engine\Data\Automation;
use MailPoet\Automation\Engine\Storage\AutomationStorage;
use MailPoet\Cron\Workers\SimpleWorker;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\WP\Functions as WPFunctions;
class AbandonedCartWorker extends SimpleWorker {
const TASK_TYPE = 'automation_abandoned_cart';
const ACTION = 'abandoned_cart';
const AUTOMATIC_SCHEDULING = false;
const BATCH_SIZE = 1000;
private AutomationStorage $automationStorage;
private WPFunctions $wp;
public function __construct(
AutomationStorage $automationStorage,
WPFunctions $wp
) {
parent::__construct();
$this->automationStorage = $automationStorage;
$this->wp = $wp;
}
public function checkProcessingRequirements() {
return true;
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$productIds = $task->getMeta()['product_ids'] ?? [];
$automationId = $task->getMeta()['automation_id'] ?? 0;
$automationVersion = $task->getMeta()['automation_version'] ?? 0;
if (!$productIds || !$automationId || !$automationVersion) {
return true;
}
$lastActivityAt = $task->getCreatedAt();
$subscribers = $task->getSubscribers();
if ($subscribers->count() !== 1) {
return true;
}
$subscriber = isset($subscribers[0]) ? $subscribers[0]->getSubscriber() : null;
if (!$subscriber) {
return true;
}
$automation = $this->automationStorage->getAutomation((int)$automationId, (int)$automationVersion);
if (!$automation || $automation->getStatus() !== Automation::STATUS_ACTIVE) {
return true;
}
$this->wp->doAction(
self::ACTION,
$subscriber,
$productIds,
$lastActivityAt
);
return true;
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,48 @@
<?php declare(strict_types = 1);
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Subscribers\EngagementDataBackfiller;
class BackfillEngagementData extends SimpleWorker {
const TASK_TYPE = 'backfill_engagement_data';
const BATCH_SIZE = 100;
const AUTOMATIC_SCHEDULING = false;
const SUPPORT_MULTIPLE_INSTANCES = false;
/** @var EngagementDataBackfiller */
private $engagementDataBackfiller;
public function __construct(
EngagementDataBackfiller $engagementDataBackfiller
) {
parent::__construct();
$this->engagementDataBackfiller = $engagementDataBackfiller;
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$meta = $task->getMeta();
$lastSubscriberId = $meta['last_subscriber_id'] ?? 0;
do {
$this->cronHelper->enforceExecutionLimit($timer);
$batch = $this->engagementDataBackfiller->getBatch($lastSubscriberId, self::BATCH_SIZE);
if (empty($batch)) {
break;
}
$this->engagementDataBackfiller->updateBatch($batch);
$lastSubscriberId = $this->engagementDataBackfiller->getLastProcessedSubscriberId();
$meta['last_subscriber_id'] = $lastSubscriberId;
$task->setMeta($meta);
$this->scheduledTasksRepository->persist($task);
$this->scheduledTasksRepository->flush();
} while (count($batch) === self::BATCH_SIZE);
return true;
}
}
@@ -0,0 +1,161 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
use MailPoet\Entities\StatisticsBounceEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Mailer\Mailer;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Services\Bridge;
use MailPoet\Services\Bridge\API;
use MailPoet\Settings\SettingsController;
use MailPoet\Statistics\StatisticsBouncesRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Tasks\Subscribers\BatchIterator;
use MailPoetVendor\Carbon\Carbon;
class Bounce extends SimpleWorker {
const TASK_TYPE = 'bounce';
const BATCH_SIZE = 100;
const BOUNCED_HARD = 'hard';
const BOUNCED_SOFT = 'soft';
const NOT_BOUNCED = null;
public $api;
/** @var SettingsController */
private $settings;
/** @var Bridge */
private $bridge;
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var SendingQueuesRepository */
private $sendingQueuesRepository;
/** @var StatisticsBouncesRepository */
private $statisticsBouncesRepository;
/** @var ScheduledTaskSubscribersRepository */
private $scheduledTaskSubscribersRepository;
public function __construct(
SettingsController $settings,
SubscribersRepository $subscribersRepository,
SendingQueuesRepository $sendingQueuesRepository,
StatisticsBouncesRepository $statisticsBouncesRepository,
ScheduledTaskSubscribersRepository $scheduledTaskSubscribersRepository,
Bridge $bridge
) {
$this->settings = $settings;
$this->bridge = $bridge;
parent::__construct();
$this->subscribersRepository = $subscribersRepository;
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->statisticsBouncesRepository = $statisticsBouncesRepository;
$this->scheduledTaskSubscribersRepository = $scheduledTaskSubscribersRepository;
}
public function init() {
if (!$this->api) {
$this->api = new API($this->settings->get(Mailer::MAILER_CONFIG_SETTING_NAME)['mailpoet_api_key']);
}
}
public function checkProcessingRequirements() {
return $this->bridge->isMailpoetSendingServiceEnabled();
}
public function prepareTaskStrategy(ScheduledTaskEntity $task, $timer) {
$this->scheduledTaskSubscribersRepository->createSubscribersForBounceWorker($task);
if (!$this->scheduledTaskSubscribersRepository->countBy(['task' => $task, 'processed' => ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED])) {
$this->scheduledTaskSubscribersRepository->deleteByScheduledTask($task);
return false;
}
return true;
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$subscriberBatches = new BatchIterator($task->getId(), self::BATCH_SIZE);
if (count($subscriberBatches) === 0) {
$this->scheduledTaskSubscribersRepository->deleteByScheduledTask($task);
return true; // mark completed
}
/** @var int[] $subscribersToProcessIds - it's required for PHPStan */
foreach ($subscriberBatches as $subscribersToProcessIds) {
// abort if execution limit is reached
$this->cronHelper->enforceExecutionLimit($timer);
$subscriberEmails = $this->subscribersRepository->getUndeletedSubscribersEmailsByIds($subscribersToProcessIds);
$subscriberEmails = array_column($subscriberEmails, 'email');
$this->processEmails($task, $subscriberEmails);
$this->scheduledTaskSubscribersRepository->updateProcessedSubscribers($task, $subscribersToProcessIds);
}
return true;
}
public function processEmails(ScheduledTaskEntity $task, array $subscriberEmails) {
$checkedEmails = $this->api->checkBounces($subscriberEmails);
$this->processApiResponse($task, (array)$checkedEmails);
}
public function processApiResponse(ScheduledTaskEntity $task, array $checkedEmails) {
$previousTask = $this->findPreviousTask($task);
foreach ($checkedEmails as $email) {
if (!isset($email['address'], $email['bounce'])) {
continue;
}
if ($email['bounce'] === self::BOUNCED_HARD) {
$subscriber = $this->subscribersRepository->findOneBy(['email' => $email['address']]);
if (!$subscriber instanceof SubscriberEntity) continue;
$subscriber->setStatus(SubscriberEntity::STATUS_BOUNCED);
$this->saveBouncedStatistics($subscriber, $task, $previousTask);
}
}
$this->subscribersRepository->flush();
}
public function getNextRunDate() {
$date = Carbon::now()->millisecond(0);
return $date->startOfDay()
->addDay()
->addHours(rand(0, 5))
->addMinutes(rand(0, 59))
->addSeconds(rand(0, 59));
}
private function findPreviousTask(ScheduledTaskEntity $task): ?ScheduledTaskEntity {
return $this->scheduledTasksRepository->findPreviousTask($task);
}
private function saveBouncedStatistics(SubscriberEntity $subscriber, ScheduledTaskEntity $task, ?ScheduledTaskEntity $previousTask): void {
$dateFrom = null;
if ($previousTask instanceof ScheduledTaskEntity) {
$dateFrom = $previousTask->getScheduledAt();
}
$queues = $this->sendingQueuesRepository->findAllForSubscriberSentBetween($subscriber, $task->getScheduledAt(), $dateFrom);
foreach ($queues as $queue) {
$newsletter = $queue->getNewsletter();
if ($newsletter instanceof NewsletterEntity) {
$statistics = new StatisticsBounceEntity($newsletter, $queue, $subscriber);
$this->statisticsBouncesRepository->persist($statistics);
}
}
}
}
@@ -0,0 +1,31 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Subscribers\ImportExport\Export\Export;
use MailPoetVendor\Carbon\Carbon;
class ExportFilesCleanup extends SimpleWorker {
const TASK_TYPE = 'export_files_cleanup';
const DELETE_FILES_AFTER_X_DAYS = 1;
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$iterator = new \GlobIterator(Export::getExportPath() . '/' . Export::getFilePrefix() . '*.*');
foreach ($iterator as $file) {
if (is_string($file)) {
continue;
}
$name = $file->getPathname();
$created = $file->getMTime();
$now = new Carbon();
if (Carbon::createFromTimestamp((int)$created)->lessThan($now->subDays(self::DELETE_FILES_AFTER_X_DAYS))) {
unlink($name);
};
}
return true;
}
}
@@ -0,0 +1,83 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Subscribers\InactiveSubscribersController;
use MailPoet\Subscribers\SubscribersRepository;
class InactiveSubscribers extends SimpleWorker {
const TASK_TYPE = 'inactive_subscribers';
const BATCH_SIZE = 1000;
const SUPPORT_MULTIPLE_INSTANCES = false;
/** @var InactiveSubscribersController */
private $inactiveSubscribersController;
/** @var SettingsController */
private $settings;
/** @var TrackingConfig */
private $trackingConfig;
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
InactiveSubscribersController $inactiveSubscribersController,
SettingsController $settings,
TrackingConfig $trackingConfig,
SubscribersRepository $subscribersRepository
) {
$this->inactiveSubscribersController = $inactiveSubscribersController;
$this->settings = $settings;
$this->trackingConfig = $trackingConfig;
$this->subscribersRepository = $subscribersRepository;
parent::__construct();
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
if (!$this->trackingConfig->isEmailTrackingEnabled()) {
$this->schedule();
return true;
}
$daysToInactive = (int)$this->settings->get('deactivate_subscriber_after_inactive_days');
// Activate all inactive subscribers in case the feature is turned off
if ($daysToInactive === 0) {
$this->inactiveSubscribersController->reactivateInactiveSubscribers();
$this->schedule();
return true;
}
// Handle activation/deactivation within interval
$meta = $task->getMeta();
$lastSubscriberId = isset($meta['last_subscriber_id']) ? $meta['last_subscriber_id'] : 0;
if (isset($meta['max_subscriber_id'])) {
$maxSubscriberId = $meta['max_subscriber_id'];
} else {
$maxSubscriberId = $this->subscribersRepository->getMaxSubscriberId();
}
while ($lastSubscriberId <= $maxSubscriberId) {
$count = $this->inactiveSubscribersController->markInactiveSubscribers($daysToInactive, self::BATCH_SIZE, $lastSubscriberId);
if ($count === false) {
break;
}
$lastSubscriberId += self::BATCH_SIZE;
$task->setMeta(['last_subscriber_id' => $lastSubscriberId]);
$this->scheduledTasksRepository->persist($task);
$this->scheduledTasksRepository->flush();
$this->cronHelper->enforceExecutionLimit($timer);
};
while ($this->inactiveSubscribersController->markActiveSubscribers($daysToInactive, self::BATCH_SIZE) === self::BATCH_SIZE) {
$this->cronHelper->enforceExecutionLimit($timer);
};
$this->schedule();
return true;
}
}
@@ -0,0 +1,59 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\KeyCheck;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\CronWorkerScheduler;
use MailPoet\Cron\Workers\SimpleWorker;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Services\Bridge;
use MailPoetVendor\Carbon\Carbon;
abstract class KeyCheckWorker extends SimpleWorker {
/** @var Bridge|null */
public $bridge;
/** @var CronWorkerScheduler */
protected $cronWorkerScheduler;
public function __construct(
CronWorkerScheduler $cronWorkerScheduler
) {
parent::__construct();
$this->cronWorkerScheduler = $cronWorkerScheduler;
}
public function init() {
if (!$this->bridge) {
$this->bridge = new Bridge();
}
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
try {
$result = $this->checkKey();
} catch (\Exception $e) {
$result = false;
}
if (empty($result['code']) || $result['code'] == Bridge::CHECK_ERROR_UNAVAILABLE) {
$this->cronWorkerScheduler->rescheduleProgressively($task);
return false;
}
return true;
}
public function getNextRunDate() {
$date = Carbon::now()->millisecond(0);
return $date->startOfDay()
->addDay()
->addHours(rand(0, 5))
->addMinutes(rand(0, 59))
->addSeconds(rand(0, 59));
}
public abstract function checkKey();
}
@@ -0,0 +1,42 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\KeyCheck;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\CronWorkerScheduler;
use MailPoet\InvalidStateException;
use MailPoet\Services\Bridge;
use MailPoet\Settings\SettingsController;
class PremiumKeyCheck extends KeyCheckWorker {
const TASK_TYPE = 'premium_key_check';
/** @var SettingsController */
private $settings;
public function __construct(
SettingsController $settings,
CronWorkerScheduler $cronWorkerScheduler
) {
$this->settings = $settings;
parent::__construct($cronWorkerScheduler);
}
public function checkProcessingRequirements() {
return Bridge::isPremiumKeySpecified();
}
public function checkKey() {
// for phpstan because we set bridge property in the init function
if (!$this->bridge) {
throw new InvalidStateException('The class was not initialized properly. Please call the Init method before.');
};
$premiumKey = $this->settings->get(Bridge::PREMIUM_KEY_SETTING_NAME);
$result = $this->bridge->checkPremiumKey($premiumKey);
$this->bridge->storePremiumKeyAndState($premiumKey, $result);
return $result;
}
}
@@ -0,0 +1,70 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\KeyCheck;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\ServicesChecker;
use MailPoet\Cron\CronWorkerScheduler;
use MailPoet\InvalidStateException;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\MailerLog;
use MailPoet\Services\Bridge;
use MailPoet\Settings\SettingsController;
use MailPoetVendor\Carbon\Carbon;
class SendingServiceKeyCheck extends KeyCheckWorker {
const TASK_TYPE = 'sending_service_key_check';
/** @var SettingsController */
private $settings;
/** @var ServicesChecker */
private $servicesChecker;
public function __construct(
SettingsController $settings,
ServicesChecker $servicesChecker,
CronWorkerScheduler $cronWorkerScheduler
) {
$this->settings = $settings;
$this->servicesChecker = $servicesChecker;
parent::__construct($cronWorkerScheduler);
}
public function checkProcessingRequirements() {
return Bridge::isMPSendingServiceEnabled();
}
/**
* @return \DateTimeInterface|Carbon
*/
public function getNextRunDate() {
// when key pending approval, check key sate every hour
if ($this->servicesChecker->isMailPoetAPIKeyPendingApproval()) {
$date = Carbon::now()->millisecond(0);
return $date->addHour();
}
return parent::getNextRunDate();
}
public function checkKey() {
// for phpstan because we set bridge property in the init function
if (!$this->bridge) {
throw new InvalidStateException('The class was not initialized properly. Please call the Init method before.');
};
$wasPendingApproval = $this->servicesChecker->isMailPoetAPIKeyPendingApproval();
$mssKey = $this->settings->get(Mailer::MAILER_CONFIG_SETTING_NAME)['mailpoet_api_key'];
$result = $this->bridge->checkMSSKey($mssKey);
$this->bridge->storeMSSKeyAndState($mssKey, $result);
$isPendingApproval = $this->servicesChecker->isMailPoetAPIKeyPendingApproval();
if ($wasPendingApproval && !$isPendingApproval) {
MailerLog::resumeSending();
}
return $result;
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,64 @@
<?php declare(strict_types = 1);
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Analytics\Analytics;
use MailPoet\Entities\ScheduledTaskEntity;
use Mixpanel as MixpanelLibrary;
class Mixpanel extends SimpleWorker {
const PRODUCTION_PROJECT_ID = '8cce373b255e5a76fb22d57b85db0c92';
/** @var Analytics */
private $analytics;
const TASK_TYPE = 'mixpanel';
private MixpanelLibrary $mixpanel;
public function __construct(
Analytics $analytics
) {
parent::__construct();
$this->analytics = $analytics;
$this->mixpanel = MixpanelLibrary::getInstance(self::PRODUCTION_PROJECT_ID);
$this->mixpanel->register('Platform', 'Plugin');
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
return $this->maybeReportAnalyticsToMixpanel();
}
public function maybeReportAnalyticsToMixpanel(): bool {
if (!$this->analytics->shouldSend()) {
return true;
}
return $this->reportAnalyticsToMixpanel();
}
public function reportAnalyticsToMixpanel(): bool {
$publicId = $this->analytics->getPublicId();
if (strlen($publicId) < 1) {
return true;
}
$data = $this->analytics->getAnalyticsData();
$this->mixpanel->identify($publicId);
$this->mixpanel->people->set($publicId, $data);
$this->mixpanel->track('User Properties', $data);
$this->analytics->recordDataSent();
return true;
}
public function getNextRunDate() {
return $this->analytics->getNextSendDate()->addMinutes(rand(0, 59));
}
}
@@ -0,0 +1,30 @@
<?php declare(strict_types = 1);
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\NewsletterTemplates\ThumbnailSaver;
class NewsletterTemplateThumbnails extends SimpleWorker {
const TASK_TYPE = 'newsletter_templates_thumbnails';
const AUTOMATIC_SCHEDULING = false;
const SUPPORT_MULTIPLE_INSTANCES = false;
/** @var ThumbnailSaver */
private $thumbnailSaver;
public function __construct(
ThumbnailSaver $thumbnailSaver
) {
parent::__construct();
$this->thumbnailSaver = $thumbnailSaver;
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$this->thumbnailSaver->ensureTemplateThumbnailsForAll();
return true;
}
}
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Newsletter\Scheduler\ReEngagementScheduler;
use MailPoetVendor\Carbon\Carbon;
class ReEngagementEmailsScheduler extends SimpleWorker {
const TASK_TYPE = 'schedule_re_engagement_email';
/** @var ReEngagementScheduler */
private $reEngagementEmailsScheduler;
public function __construct(
ReEngagementScheduler $reEngagementEmailsScheduler
) {
parent::__construct();
$this->reEngagementEmailsScheduler = $reEngagementEmailsScheduler;
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$this->reEngagementEmailsScheduler->scheduleAll();
return true;
}
public function getNextRunDate() {
return Carbon::now()->millisecond(0)->addDay();
}
}
@@ -0,0 +1,455 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\CronHelper;
use MailPoet\Cron\CronWorkerScheduler;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterSegmentEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Scheduler\PostNotificationScheduler;
use MailPoet\Newsletter\Scheduler\Scheduler as NewsletterScheduler;
use MailPoet\Newsletter\Scheduler\WelcomeScheduler;
use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Segments\SubscribersFinder;
use MailPoet\Subscribers\SubscriberSegmentRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Util\Security;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\ORM\EntityNotFoundException;
class Scheduler {
const TASK_BATCH_SIZE = 5;
/** @var SubscribersFinder */
private $subscribersFinder;
/** @var LoggerFactory */
private $loggerFactory;
/** @var CronHelper */
private $cronHelper;
/** @var CronWorkerScheduler */
private $cronWorkerScheduler;
/** @var ScheduledTasksRepository */
private $scheduledTasksRepository;
/** @var ScheduledTaskSubscribersRepository */
private $scheduledTaskSubscribersRepository;
/** @var SendingQueuesRepository */
private $sendingQueuesRepository;
/** @var NewslettersRepository */
private $newslettersRepository;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var NewsletterSegmentRepository */
private $newsletterSegmentRepository;
/** @var Security */
private $security;
/** @var NewsletterScheduler */
private $scheduler;
/** @var SubscriberSegmentRepository */
private $subscriberSegmentRepository;
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
SubscribersFinder $subscribersFinder,
LoggerFactory $loggerFactory,
CronHelper $cronHelper,
CronWorkerScheduler $cronWorkerScheduler,
ScheduledTasksRepository $scheduledTasksRepository,
ScheduledTaskSubscribersRepository $scheduledTaskSubscribersRepository,
SendingQueuesRepository $sendingQueuesRepository,
NewslettersRepository $newslettersRepository,
SegmentsRepository $segmentsRepository,
NewsletterSegmentRepository $newsletterSegmentRepository,
Security $security,
NewsletterScheduler $scheduler,
SubscriberSegmentRepository $subscriberSegmentRepository,
SubscribersRepository $subscribersRepository
) {
$this->cronHelper = $cronHelper;
$this->subscribersFinder = $subscribersFinder;
$this->loggerFactory = $loggerFactory;
$this->cronWorkerScheduler = $cronWorkerScheduler;
$this->scheduledTasksRepository = $scheduledTasksRepository;
$this->scheduledTaskSubscribersRepository = $scheduledTaskSubscribersRepository;
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->newslettersRepository = $newslettersRepository;
$this->segmentsRepository = $segmentsRepository;
$this->newsletterSegmentRepository = $newsletterSegmentRepository;
$this->security = $security;
$this->scheduler = $scheduler;
$this->subscriberSegmentRepository = $subscriberSegmentRepository;
$this->subscribersRepository = $subscribersRepository;
}
public function process($timer = false) {
$timer = $timer ?: microtime(true);
// abort if execution limit is reached
$this->cronHelper->enforceExecutionLimit($timer);
$scheduledTasks = $this->getScheduledSendingTasks();
$this->updateTasks($scheduledTasks);
foreach ($scheduledTasks as $task) {
$queue = $task->getSendingQueue();
if (!$queue) {
$this->deleteByTask($task);
continue;
}
$newsletter = $queue->getNewsletter();
try {
if (!$newsletter instanceof NewsletterEntity || $newsletter->getDeletedAt() !== null) {
$this->deleteByTask($task);
} elseif ($newsletter->getStatus() !== NewsletterEntity::STATUS_ACTIVE && $newsletter->getStatus() !== NewsletterEntity::STATUS_SCHEDULED) {
$task->setStatus(ScheduledTaskEntity::STATUS_PAUSED);
$this->scheduledTasksRepository->flush();
continue;
} elseif ($newsletter->getType() === NewsletterEntity::TYPE_WELCOME) {
$this->processWelcomeNewsletter($newsletter, $task);
} elseif ($newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION) {
$this->processPostNotificationNewsletter($newsletter, $task);
} elseif ($newsletter->getType() === NewsletterEntity::TYPE_STANDARD) {
$this->processScheduledStandardNewsletter($newsletter, $task);
} elseif ($newsletter->getType() === NewsletterEntity::TYPE_AUTOMATIC) {
$this->processScheduledAutomaticEmail($newsletter, $task);
} elseif ($newsletter->getType() === NewsletterEntity::TYPE_RE_ENGAGEMENT) {
$this->processReEngagementEmail($task);
} elseif ($newsletter->getType() === NewsletterEntity::TYPE_AUTOMATION) {
$this->processScheduledAutomationEmail($task);
} elseif ($newsletter->getType() === NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL) {
$this->processScheduledTransactionalEmail($task);
}
} catch (EntityNotFoundException $e) {
// Doctrine throws this exception when newsletter doesn't exist but is referenced in a scheduled task.
// This was added while refactoring this method to use Doctrine instead of Paris. We have to handle this case
// for the SchedulerTest::testItDeletesQueueDuringProcessingWhenNewsletterNotFound() test. I'm not sure
// if this problem could happen in production or not.
$this->deleteByTask($task);
}
$this->cronHelper->enforceExecutionLimit($timer);
}
}
public function processWelcomeNewsletter(NewsletterEntity $newsletter, ScheduledTaskEntity $task) {
$subscribers = $task->getSubscribers();
if (empty($subscribers[0])) {
$this->deleteByTask($task);
return false;
}
$subscriberId = (int)$subscribers[0]->getSubscriberId();
if ($newsletter->getOptionValue('event') === 'segment') {
if ($this->verifyMailpoetSubscriber($subscriberId, $newsletter, $task) === false) {
return false;
}
} else {
if ($newsletter->getOptionValue('event') === 'user') {
if ($this->verifyWPSubscriber($subscriberId, $newsletter, $task) === false) {
return false;
}
}
}
$task->setStatus(null);
$this->scheduledTasksRepository->flush();
return true;
}
public function processPostNotificationNewsletter(NewsletterEntity $newsletter, ScheduledTaskEntity $task) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info(
'process post notification in scheduler',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()]
);
// ensure that segments exist
$segments = $newsletter->getSegmentIds();
if (empty($segments)) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info(
'post notification no segments',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()]
);
$this->deleteQueueOrUpdateNextRunDate($task, $newsletter);
return false;
}
// ensure that subscribers are in segments
$this->subscribersFinder->addSubscribersToTaskFromSegments($task, $segments, $newsletter->getFilterSegmentId());
$subscribersCount = $task->getSubscribers()->count();
if (empty($subscribersCount)) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info(
'post notification no subscribers',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId(), 'segment_ids' => $segments]
);
$this->deleteQueueOrUpdateNextRunDate($task, $newsletter);
return false;
}
// create a duplicate newsletter that acts as a history record
try {
$notificationHistory = $this->createPostNotificationHistory($newsletter);
} catch (\Exception $exception) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->error(
'creating post notification history failed',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId(), 'error' => $exception->getMessage()]
);
return false;
}
// queue newsletter for delivery
$queue = $task->getSendingQueue();
if (!$queue) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->error(
'post notification no queue',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()]
);
return false;
}
$queue->setNewsletter($notificationHistory);
$this->sendingQueuesRepository->updateCounts($queue);
$task->setStatus(null);
$this->scheduledTasksRepository->flush();
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info(
'post notification set status to sending',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()]
);
return true;
}
public function processScheduledAutomaticEmail(NewsletterEntity $newsletter, ScheduledTaskEntity $task) {
if ($newsletter->getOptionValue('sendTo') === 'segment') {
$segment = $this->segmentsRepository->findOneById($newsletter->getOptionValue('segment'));
if ($segment instanceof SegmentEntity) {
$this->subscribersFinder->addSubscribersToTaskFromSegments($task, [(int)$segment->getId()]);
if (!$task->getSubscribers()->count()) {
$this->deleteByTask($task);
return false;
}
}
} else {
$subscribers = $task->getSubscribers();
$subscriber = isset($subscribers[0]) ? $subscribers[0]->getSubscriber() : null;
if (!$subscriber) {
$this->deleteByTask($task);
return false;
}
if ($this->verifySubscriber($subscriber, $task) === false) {
return false;
}
}
$task->setStatus(null);
$this->scheduledTasksRepository->flush();
return true;
}
public function processScheduledAutomationEmail(ScheduledTaskEntity $task): bool {
$subscribers = $task->getSubscribers();
$subscriber = isset($subscribers[0]) ? $subscribers[0]->getSubscriber() : null;
if (!$subscriber) {
$this->deleteByTask($task);
return false;
}
if (!$this->verifySubscriber($subscriber, $task)) {
return false;
}
$task->setStatus(null);
$this->scheduledTasksRepository->flush();
return true;
}
public function processScheduledTransactionalEmail(ScheduledTaskEntity $task): bool {
$subscribers = $task->getSubscribers();
$subscriber = isset($subscribers[0]) ? $subscribers[0]->getSubscriber() : null;
if (!$subscriber) {
$this->deleteByTask($task);
return false;
}
if (!$this->verifySubscriber($subscriber, $task)) {
$this->deleteByTask($task);
return false;
}
$task->setStatus(null);
$this->scheduledTasksRepository->flush();
return true;
}
public function processScheduledStandardNewsletter(NewsletterEntity $newsletter, ScheduledTaskEntity $task) {
$segments = $newsletter->getSegmentIds();
$this->subscribersFinder->addSubscribersToTaskFromSegments($task, $segments, $newsletter->getFilterSegmentId());
$task->setStatus(null);
$queue = $task->getSendingQueue();
if ($queue) {
$this->sendingQueuesRepository->updateCounts($queue);
}
$newsletter->setStatus(NewsletterEntity::STATUS_SENDING);
$this->scheduledTasksRepository->flush();
return true;
}
private function processReEngagementEmail(ScheduledTaskEntity $task) {
$task->setStatus(null);
$this->scheduledTasksRepository->flush();
return true;
}
public function verifyMailpoetSubscriber(int $subscriberId, NewsletterEntity $newsletter, ScheduledTaskEntity $task): bool {
$subscriber = $this->subscribersRepository->findOneById($subscriberId);
// check if subscriber is in proper segment
$subscriberInSegment = $this->subscriberSegmentRepository->findOneBy(
[
'subscriber' => $subscriberId,
'segment' => $newsletter->getOptionValue('segment'),
'status' => SubscriberEntity::STATUS_SUBSCRIBED,
]
);
if (!$subscriber || !$subscriberInSegment) {
$this->deleteByTask($task);
return false;
}
return $this->verifySubscriber($subscriber, $task);
}
public function verifyWPSubscriber(int $subscriberId, NewsletterEntity $newsletter, ScheduledTaskEntity $task): bool {
// check if user has the proper role
$subscriber = $this->subscribersRepository->findOneById($subscriberId);
if (!$subscriber || $subscriber->isWPUser() === false || is_null($subscriber->getWpUserId())) {
$this->deleteByTask($task);
return false;
}
$wpUser = get_userdata($subscriber->getWpUserId());
if ($wpUser === false) {
$this->deleteByTask($task);
return false;
}
if (
$newsletter->getOptionValue('role') !== WelcomeScheduler::WORDPRESS_ALL_ROLES
&& !in_array($newsletter->getOptionValue('role'), ((array)$wpUser)['roles'])
) {
$this->deleteByTask($task);
return false;
}
return $this->verifySubscriber($subscriber, $task);
}
public function verifySubscriber(SubscriberEntity $subscriber, ScheduledTaskEntity $task): bool {
$queue = $task->getSendingQueue();
$newsletter = $queue ? $queue->getNewsletter() : null;
if ($newsletter && $newsletter->isTransactional()) {
return $subscriber->getStatus() !== SubscriberEntity::STATUS_BOUNCED;
}
if ($subscriber->getStatus() === SubscriberEntity::STATUS_UNCONFIRMED) {
// reschedule delivery
$this->cronWorkerScheduler->rescheduleProgressively($task);
return false;
} else if ($subscriber->getStatus() === SubscriberEntity::STATUS_UNSUBSCRIBED) {
$this->deleteByTask($task);
return false;
}
return true;
}
public function deleteQueueOrUpdateNextRunDate(ScheduledTaskEntity $task, NewsletterEntity $newsletter) {
if ($newsletter->getOptionValue('intervalType') === PostNotificationScheduler::INTERVAL_IMMEDIATELY) {
$this->deleteByTask($task);
} else {
$nextRunDate = $this->scheduler->getNextRunDateTime($newsletter->getOptionValue('schedule'));
if (!$nextRunDate) {
$this->deleteByTask($task);
return;
}
$task->setScheduledAt($nextRunDate);
$this->scheduledTasksRepository->flush();
}
}
public function createPostNotificationHistory(NewsletterEntity $newsletter): NewsletterEntity {
// clone newsletter
$notificationHistory = clone $newsletter;
$notificationHistory->setParent($newsletter);
$notificationHistory->setType(NewsletterEntity::TYPE_NOTIFICATION_HISTORY);
$notificationHistory->setStatus(NewsletterEntity::STATUS_SENDING);
$notificationHistory->setUnsubscribeToken($this->security->generateUnsubscribeTokenByEntity($notificationHistory));
// reset timestamps
$createdAt = Carbon::now()->millisecond(0);
$notificationHistory->setCreatedAt($createdAt);
$notificationHistory->setUpdatedAt($createdAt);
$notificationHistory->setDeletedAt(null);
// reset hash
$notificationHistory->setHash(Security::generateHash());
$this->newslettersRepository->persist($notificationHistory);
$this->newslettersRepository->flush();
// create relationships between notification history and segments
foreach ($newsletter->getNewsletterSegments() as $newsletterSegment) {
$segment = $newsletterSegment->getSegment();
if (!$segment) {
continue;
}
$duplicateSegment = new NewsletterSegmentEntity($notificationHistory, $segment);
$notificationHistory->getNewsletterSegments()->add($duplicateSegment);
$this->newsletterSegmentRepository->persist($duplicateSegment);
}
$this->newslettersRepository->flush();
return $notificationHistory;
}
/**
* @param ScheduledTaskEntity[] $scheduledTasks
*/
private function updateTasks(array $scheduledTasks): void {
$ids = array_map(function (ScheduledTaskEntity $scheduledTask): ?int {
return $scheduledTask->getId();
}, $scheduledTasks);
$ids = array_filter($ids);
$this->scheduledTasksRepository->touchAllByIds($ids);
}
/**
* @return ScheduledTaskEntity[]
*/
public function getScheduledSendingTasks(): array {
return $this->scheduledTasksRepository->findScheduledSendingTasks(self::TASK_BATCH_SIZE);
}
private function deleteByTask(ScheduledTaskEntity $task): void {
$queue = $task->getSendingQueue();
if ($queue) {
$this->sendingQueuesRepository->remove($queue);
}
$this->scheduledTaskSubscribersRepository->deleteByScheduledTask($task);
$this->scheduledTasksRepository->remove($task);
$this->scheduledTasksRepository->flush();
}
}
@@ -0,0 +1,86 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\SendingQueue;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\MailerLog;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
class SendingErrorHandler {
/** @var ScheduledTaskSubscribersRepository */
private $scheduledTaskSubscribersRepository;
/** @var SendingThrottlingHandler */
private $throttlingHandler;
/** @var SendingQueuesRepository */
private $sendingQueuesRepository;
/** @var LoggerFactory */
private $loggerFactory;
public function __construct(
ScheduledTaskSubscribersRepository $scheduledTaskSubscribersRepository,
SendingThrottlingHandler $throttlingHandler,
SendingQueuesRepository $sendingQueuesRepository,
LoggerFactory $loggerFactory
) {
$this->scheduledTaskSubscribersRepository = $scheduledTaskSubscribersRepository;
$this->throttlingHandler = $throttlingHandler;
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->loggerFactory = $loggerFactory;
}
public function processError(
MailerError $error,
ScheduledTaskEntity $task,
array $preparedSubscribersIds,
array $preparedSubscribers
) {
if ($error->getLevel() === MailerError::LEVEL_HARD) {
return $this->processHardError($error);
}
$this->processSoftError($error, $task, $preparedSubscribersIds, $preparedSubscribers);
}
private function processHardError(MailerError $error) {
if ($error->getRetryInterval() !== null) {
MailerLog::processNonBlockingError($error->getOperation(), $error->getMessageWithFailedSubscribers(), $error->getRetryInterval());
} else {
$throttledBatchSize = null;
if ($error->getOperation() === MailerError::OPERATION_CONNECT) {
$throttledBatchSize = $this->throttlingHandler->throttleBatchSize();
}
MailerLog::processError($error->getOperation(), $error->getMessageWithFailedSubscribers(), null, false, $throttledBatchSize);
}
}
private function processSoftError(MailerError $error, ScheduledTaskEntity $task, $preparedSubscribersIds, $preparedSubscribers) {
foreach ($error->getSubscriberErrors() as $subscriberError) {
$subscriberIdIndex = array_search($subscriberError->getEmail(), $preparedSubscribers);
$message = $subscriberError->getMessage() ?: $error->getMessage();
$this->scheduledTaskSubscribersRepository->saveError($task, $preparedSubscribersIds[$subscriberIdIndex], $message ?? '');
}
$queue = $task->getSendingQueue();
if ($queue instanceof SendingQueueEntity) {
if ($error->getOperation() === MailerError::OPERATION_DOMAIN_AUTHORIZATION) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
'Paused task in sending queue due to sender domain authorization error',
['task_id' => $task->getId()]
);
$this->sendingQueuesRepository->pause($queue);
return;
}
$this->sendingQueuesRepository->updateCounts($queue);
}
}
}
@@ -0,0 +1,686 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\SendingQueue;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\CronHelper;
use MailPoet\Cron\Workers\Bounce;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Links;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Mailer as MailerTask;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter as NewsletterTask;
use MailPoet\Cron\Workers\StatsNotifications\Scheduler as StatsNotificationsScheduler;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\InvalidStateException;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Mailer\MailerLog;
use MailPoet\Mailer\MetaInfo;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Segments\SubscribersFinder;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Statistics\StatisticsNewslettersRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Tasks\Subscribers\BatchIterator;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\ORM\EntityManager;
use Throwable;
class SendingQueue {
/** @var MailerTask */
public $mailerTask;
/** @var NewsletterTask */
public $newsletterTask;
const TASK_TYPE = 'sending';
const TASK_BATCH_SIZE = 5;
const EMAIL_WITH_INVALID_SEGMENT_OPTION = 'mailpoet_email_with_invalid_segment';
/** @var StatsNotificationsScheduler */
public $statsNotificationsScheduler;
/** @var SendingErrorHandler */
private $errorHandler;
/** @var SendingThrottlingHandler */
private $throttlingHandler;
/** @var MetaInfo */
private $mailerMetaInfo;
/** @var LoggerFactory */
private $loggerFactory;
/** @var CronHelper */
private $cronHelper;
/** @var SubscribersFinder */
private $subscribersFinder;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var WPFunctions */
private $wp;
/** @var Links */
private $links;
/** @var ScheduledTasksRepository */
private $scheduledTasksRepository;
/** @var ScheduledTaskSubscribersRepository */
private $scheduledTaskSubscribersRepository;
/** @var SubscribersRepository */
private $subscribersRepository;
/*** @var SendingQueuesRepository */
private $sendingQueuesRepository;
/** @var EntityManager */
private $entityManager;
/** @var StatisticsNewslettersRepository */
private $statisticsNewslettersRepository;
/** @var AuthorizedEmailsController */
private $authorizedEmailsController;
public function __construct(
SendingErrorHandler $errorHandler,
SendingThrottlingHandler $throttlingHandler,
StatsNotificationsScheduler $statsNotificationsScheduler,
LoggerFactory $loggerFactory,
CronHelper $cronHelper,
SubscribersFinder $subscriberFinder,
SegmentsRepository $segmentsRepository,
WPFunctions $wp,
Links $links,
ScheduledTasksRepository $scheduledTasksRepository,
ScheduledTaskSubscribersRepository $scheduledTaskSubscribersRepository,
MailerTask $mailerTask,
SubscribersRepository $subscribersRepository,
SendingQueuesRepository $sendingQueuesRepository,
EntityManager $entityManager,
StatisticsNewslettersRepository $statisticsNewslettersRepository,
AuthorizedEmailsController $authorizedEmailsController,
$newsletterTask = false
) {
$this->errorHandler = $errorHandler;
$this->throttlingHandler = $throttlingHandler;
$this->statsNotificationsScheduler = $statsNotificationsScheduler;
$this->subscribersFinder = $subscriberFinder;
$this->mailerTask = $mailerTask;
$this->newsletterTask = ($newsletterTask) ? $newsletterTask : new NewsletterTask();
$this->segmentsRepository = $segmentsRepository;
$this->mailerMetaInfo = new MetaInfo;
$this->wp = $wp;
$this->loggerFactory = $loggerFactory;
$this->cronHelper = $cronHelper;
$this->links = $links;
$this->scheduledTasksRepository = $scheduledTasksRepository;
$this->scheduledTaskSubscribersRepository = $scheduledTaskSubscribersRepository;
$this->subscribersRepository = $subscribersRepository;
$this->sendingQueuesRepository = $sendingQueuesRepository;
$this->entityManager = $entityManager;
$this->statisticsNewslettersRepository = $statisticsNewslettersRepository;
$this->authorizedEmailsController = $authorizedEmailsController;
}
public function process($timer = false) {
$timer = $timer ?: microtime(true);
$this->enforceSendingAndExecutionLimits($timer);
foreach ($this->scheduledTasksRepository->findRunningSendingTasks(self::TASK_BATCH_SIZE) as $task) {
$queue = $task->getSendingQueue();
if (!$queue) {
continue;
}
if ($task->getInProgress()) {
if ($this->isTimeout($task)) {
$this->stopProgress($task);
} else {
continue;
}
}
$this->startProgress($task);
try {
$this->scheduledTasksRepository->touchAllByIds([$task->getId()]);
$this->processSending($task, (int)$timer);
} catch (\Exception $e) {
$this->stopProgress($task);
throw $e;
}
$this->stopProgress($task);
}
}
private function processSending(ScheduledTaskEntity $task, int $timer): void {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
'sending queue processing',
['task_id' => $task->getId()]
);
$this->deleteTaskIfNewsletterDoesNotExist($task);
$queue = $task->getSendingQueue();
$newsletter = $this->newsletterTask->getNewsletterFromQueue($task);
if (!$queue || !$newsletter) {
$task->setStatus(ScheduledTaskEntity::STATUS_PAUSED);
$this->scheduledTasksRepository->flush();
return;
}
// pre-process newsletter (render, replace shortcodes/links, etc.)
$newsletter = $this->newsletterTask->preProcessNewsletter($newsletter, $task);
// During pre-processing we may find that the newsletter can't be sent and we delete it including all associated entities
// E.g. post notification history newsletter when there are no posts to send
if (!$newsletter) {
$task->setStatus(ScheduledTaskEntity::STATUS_PAUSED);
$this->scheduledTasksRepository->flush();
return;
}
// configure mailer
$this->mailerTask->configureMailer($newsletter);
// get newsletter segments
$newsletterSegmentsIds = $newsletter->getSegmentIds();
$segmentIdsToCheck = $newsletterSegmentsIds;
$filterSegmentId = $newsletter->getFilterSegmentId();
if (is_int($filterSegmentId)) {
$segmentIdsToCheck[] = $filterSegmentId;
}
// Pause task in case some of related segments was deleted or trashed
if ($newsletterSegmentsIds && !$this->checkDeletedSegments($segmentIdsToCheck)) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
'pause task in sending queue due deleted or trashed segment',
['task_id' => $task->getId()]
);
$task->setStatus(ScheduledTaskEntity::STATUS_PAUSED);
$this->scheduledTasksRepository->flush();
$this->wp->setTransient(self::EMAIL_WITH_INVALID_SEGMENT_OPTION, $newsletter->getSubject());
return;
}
// Pause task if sender domain requirements are not met
if (!$this->authorizedEmailsController->isSenderAddressValid($newsletter, 'sending')) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
'pause task in sending queue due to sender domain requirements',
['task_id' => $task->getId()]
);
$task->setStatus(ScheduledTaskEntity::STATUS_PAUSED);
$this->scheduledTasksRepository->flush();
return;
}
// get subscribers
$subscriberBatches = new BatchIterator($task->getId(), $this->getBatchSize());
// Set invalid state for sending task for non-campaign (no-bulk) newsletters with no subscribers (e.g. welcome emails, automatic emails).
// This cover cases when a welcome or automatic email was scheduled but before processing it the subscriber was deleted.
// The non-campaign emails are sent only to a single recipient, and we count stats based on sending tasks statues, so we can't mark them as completed.
// At the same time we want to keep a record abut processing them
if ($subscriberBatches->count() === 0 && !in_array($newsletter->getType(), NewsletterEntity::CAMPAIGN_TYPES, true)) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
'no subscribers to process',
['task_id' => $task->getId()]
);
$this->scheduledTasksRepository->invalidateTask($task);
return;
}
/** @var int[] $subscribersToProcessIds - it's required for PHPStan */
foreach ($subscriberBatches as $subscribersToProcessIds) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
'subscriber batch processing',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId(), 'subscriber_batch_count' => count($subscribersToProcessIds)]
);
if (!empty($newsletterSegmentsIds[0])) {
// Check that subscribers are in segments
try {
$foundSubscribersIds = $this->subscribersFinder->findSubscribersInSegments($subscribersToProcessIds, $newsletterSegmentsIds, $filterSegmentId);
} catch (InvalidStateException $exception) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
'paused task in sending queue due to problem finding subscribers: ' . $exception->getMessage(),
['task_id' => $task->getId()]
);
$task->setStatus(ScheduledTaskEntity::STATUS_PAUSED);
$this->scheduledTasksRepository->flush();
return;
}
$foundSubscribers = empty($foundSubscribersIds) ? [] : $this->subscribersRepository->findBy(['id' => $foundSubscribersIds, 'deletedAt' => null]);
} else {
// No segments = Welcome emails or some Automatic emails.
// Welcome emails or some Automatic emails use segments only for scheduling and store them as a newsletter option
$queryBuilder = $this->entityManager->createQueryBuilder();
$queryBuilder->select('s')
->from(SubscriberEntity::class, 's')
->where('s.id IN (:subscriberIds)')
->setParameter('subscriberIds', $subscribersToProcessIds)
->andWhere('s.deletedAt IS NULL');
if ($newsletter->isTransactional()) {
$queryBuilder->andWhere('s.status != :bouncedStatus')
->setParameter('bouncedStatus', SubscriberEntity::STATUS_BOUNCED);
} else {
$queryBuilder->andWhere('s.status = :subscribedStatus')
->setParameter('subscribedStatus', SubscriberEntity::STATUS_SUBSCRIBED);
}
$foundSubscribers = $queryBuilder->getQuery()->getResult();
$foundSubscribersIds = array_map(function(SubscriberEntity $subscriber) {
return $subscriber->getId();
}, $foundSubscribers);
}
// if some subscribers weren't found, remove them from the processing list
if (count($foundSubscribersIds) !== count($subscribersToProcessIds)) {
$subscribersToRemove = array_diff(
$subscribersToProcessIds,
$foundSubscribersIds
);
$this->scheduledTaskSubscribersRepository->deleteByScheduledTaskAndSubscriberIds($task, $subscribersToRemove);
$this->sendingQueuesRepository->updateCounts($queue);
// if there aren't any subscribers to process in the batch (e.g. all unsubscribed or were deleted), continue with the next batch
if (count($foundSubscribersIds) === 0) {
continue;
}
}
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
'before queue chunk processing',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId(), 'found_subscribers_count' => count($foundSubscribers)]
);
// reschedule bounce task to run sooner, if needed
$this->reScheduleBounceTask();
// Check task has not been paused before continue processing
// This is needed because the task can be paused in the middle of the batch processing,
// for example on API error ERROR_MESSAGE_BULK_EMAIL_FORBIDDEN
if ($task->getStatus() === ScheduledTaskEntity::STATUS_PAUSED) {
return;
}
if ($newsletter->getStatus() !== NewsletterEntity::STATUS_CORRUPT) {
$this->processQueue(
$task,
$newsletter,
$foundSubscribers,
$timer
);
if (!$newsletter->isTransactional()) {
$this->entityManager->wrapInTransaction(function() use ($foundSubscribersIds) {
$now = Carbon::now()->millisecond(0);
$this->subscribersRepository->bulkUpdateLastSendingAt($foundSubscribersIds, $now);
// We're nullifying this value so these subscribers' engagement score will be recalculated the next time the cron runs
$this->subscribersRepository->bulkUpdateEngagementScoreUpdatedAt($foundSubscribersIds, null);
});
}
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
'after queue chunk processing',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()]
);
// In case we finished end sending properly before enforcing sending and execution limits
// The limit enforcing throws and exception and the sending end wouldn't be processed properly (stats notification, newsletter marked as sent etc.)
if ($task->getStatus() === ScheduledTaskEntity::STATUS_COMPLETED) {
$this->endSending($task, $newsletter);
return;
}
$this->enforceSendingAndExecutionLimits($timer);
} else {
$this->sendingQueuesRepository->pause($queue);
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->error(
'Can\'t send corrupt newsletter',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()]
);
return;
}
}
// At this point all batches were processed or there are no batches to process
// Also none of the checks above paused or invalidated the task
$this->endSending($task, $newsletter);
}
public function getBatchSize(): int {
return $this->throttlingHandler->getBatchSize();
}
/**
* @param SubscriberEntity[] $subscribers
*/
public function processQueue(ScheduledTaskEntity $task, NewsletterEntity $newsletter, array $subscribers, $timer) {
// determine if processing is done in bulk or individually
$processingMethod = $this->mailerTask->getProcessingMethod();
$preparedNewsletters = [];
$preparedSubscribers = [];
$preparedSubscribersIds = [];
$unsubscribeUrls = [];
$statistics = [];
$metas = [];
$oneClickUnsubscribeUrls = [];
$sendingQueueEntity = $task->getSendingQueue();
if (!$sendingQueueEntity) {
return;
}
$sendingQueueMeta = $sendingQueueEntity->getMeta() ?? [];
$campaignId = $sendingQueueMeta['campaignId'] ?? null;
foreach ($subscribers as $subscriber) {
// render shortcodes and replace subscriber data in tracked links
$preparedNewsletters[] =
$this->newsletterTask->prepareNewsletterForSending(
$newsletter,
$subscriber,
$sendingQueueEntity
);
// format subscriber name/address according to mailer settings
$preparedSubscribers[] = $this->mailerTask->prepareSubscriberForSending(
$subscriber
);
$preparedSubscribersIds[] = $subscriber->getId();
// create personalized instant unsubsribe link
$unsubscribeUrls[] = $this->links->getUnsubscribeUrl($sendingQueueEntity->getId(), $subscriber);
$oneClickUnsubscribeUrls[] = $this->links->getOneClickUnsubscribeUrl($sendingQueueEntity->getId(), $subscriber);
$metasForSubscriber = $this->mailerMetaInfo->getNewsletterMetaInfo($newsletter, $subscriber);
if ($campaignId) {
$metasForSubscriber['campaign_id'] = $campaignId;
}
$metas[] = $metasForSubscriber;
// keep track of values for statistics purposes
$statistics[] = [
'newsletter_id' => $newsletter->getId(),
'subscriber_id' => $subscriber->getId(),
'queue_id' => $sendingQueueEntity->getId(),
];
if ($processingMethod === 'individual') {
$this->sendNewsletter(
$task,
$preparedSubscribersIds[0],
$preparedNewsletters[0],
$preparedSubscribers[0],
$statistics[0],
$timer,
[
'unsubscribe_url' => $unsubscribeUrls[0],
'meta' => $metas[0],
'one_click_unsubscribe' => $oneClickUnsubscribeUrls[0],
]
);
$preparedNewsletters = [];
$preparedSubscribers = [];
$preparedSubscribersIds = [];
$unsubscribeUrls = [];
$oneClickUnsubscribeUrls = [];
$statistics = [];
$metas = [];
}
}
if ($processingMethod === 'bulk') {
$this->sendNewsletters(
$task,
$preparedSubscribersIds,
$preparedNewsletters,
$preparedSubscribers,
$statistics,
$timer,
[
'unsubscribe_url' => $unsubscribeUrls,
'meta' => $metas,
'one_click_unsubscribe' => $oneClickUnsubscribeUrls,
]
);
}
}
public function sendNewsletter(
ScheduledTaskEntity $task, $preparedSubscriberId, $preparedNewsletter,
$preparedSubscriber, $statistics, $timer, $extraParams = []
) {
// send newsletter
$sendResult = $this->mailerTask->send(
$preparedNewsletter,
$preparedSubscriber,
$extraParams
);
$this->processSendResult(
$task,
$sendResult,
[$preparedSubscriber],
[$preparedSubscriberId],
[$statistics],
$timer
);
}
public function sendNewsletters(
ScheduledTaskEntity $task, $preparedSubscribersIds, $preparedNewsletters,
$preparedSubscribers, $statistics, $timer, $extraParams = []
) {
// send newsletters
$sendResult = $this->mailerTask->sendBulk(
$preparedNewsletters,
$preparedSubscribers,
$extraParams
);
$this->processSendResult(
$task,
$sendResult,
$preparedSubscribers,
$preparedSubscribersIds,
$statistics,
$timer
);
}
/**
* Checks whether some of segments was deleted or trashed
* @param int[] $segmentIds
*/
private function checkDeletedSegments(array $segmentIds): bool {
if (count($segmentIds) === 0) {
return true;
}
$segmentIds = array_unique($segmentIds);
$segments = $this->segmentsRepository->findBy(['id' => $segmentIds]);
// Some segment was deleted from DB
if (count($segmentIds) > count($segments)) {
return false;
}
foreach ($segments as $segment) {
if ($segment->getDeletedAt() !== null) {
return false;
}
}
return true;
}
private function processSendResult(
ScheduledTaskEntity $task,
$sendResult,
array $preparedSubscribers,
array $preparedSubscribersIds,
array $statistics,
$timer
) {
// log error message and schedule retry/pause sending
if ($sendResult['response'] === false) {
$error = $sendResult['error'];
$this->errorHandler->processError($error, $task, $preparedSubscribersIds, $preparedSubscribers);
} else {
$queue = $task->getSendingQueue();
if (!$queue) {
return;
}
try {
$this->scheduledTaskSubscribersRepository->updateProcessedSubscribers($task, $preparedSubscribersIds);
$this->sendingQueuesRepository->updateCounts($queue);
} catch (Throwable $e) {
MailerLog::processError(
'processed_list_update',
sprintf('QUEUE-%d-PROCESSED-LIST-UPDATE', $queue->getId()),
null,
true
);
}
}
// log statistics
$this->statisticsNewslettersRepository->createMultiple($statistics);
// update the sent count
$this->mailerTask->updateSentCount();
// enforce execution limits if queue is still being processed
if ($task->getStatus() !== ScheduledTaskEntity::STATUS_COMPLETED) {
$this->enforceSendingAndExecutionLimits($timer);
}
// trigger automation email sent hook for automation emails
if (
$task->getStatus() === ScheduledTaskEntity::STATUS_COMPLETED
&& isset($task->getMeta()['automation'])
) {
try {
$this->wp->doAction('mailpoet_automation_email_sent', $task->getMeta()['automation']);
} catch (Throwable $e) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->error(
'Error while executing "mailpoet_automation_email_sent action" hook',
['task_id' => $task->getId(), 'error' => $e->getMessage()]
);
}
}
$this->throttlingHandler->processSuccess();
}
public function enforceSendingAndExecutionLimits($timer) {
// abort if execution limit is reached
$this->cronHelper->enforceExecutionLimit($timer);
// abort if sending limit has been reached
MailerLog::enforceExecutionRequirements();
}
private function reScheduleBounceTask() {
$bounceTasks = $this->scheduledTasksRepository->findFutureScheduledByType(Bounce::TASK_TYPE);
if (count($bounceTasks)) {
$bounceTask = reset($bounceTasks);
if (Carbon::now()->millisecond(0)->addHours(42)->lessThan($bounceTask->getScheduledAt())) {
$randomOffset = rand(-6 * 60 * 60, 6 * 60 * 60);
$bounceTask->setScheduledAt(Carbon::now()->millisecond(0)->addSeconds((36 * 60 * 60) + $randomOffset));
$this->scheduledTasksRepository->persist($bounceTask);
$this->scheduledTasksRepository->flush();
}
}
}
private function startProgress(ScheduledTaskEntity $task): void {
$task->setInProgress(true);
$this->scheduledTasksRepository->flush();
}
private function stopProgress(ScheduledTaskEntity $task): void {
// if task is not managed by entity manager, it's already deleted and detached
// it can be deleted in self::processSending method
if (!$this->entityManager->contains($task)) {
return;
}
$task->setInProgress(false);
$this->scheduledTasksRepository->flush();
}
private function isTimeout(ScheduledTaskEntity $task): bool {
$currentTime = Carbon::now()->millisecond(0);
$updatedAt = new Carbon($task->getUpdatedAt());
if ($updatedAt->diffInSeconds($currentTime, false) > $this->getExecutionLimit()) {
return true;
}
return false;
}
private function getExecutionLimit(): int {
return $this->cronHelper->getDaemonExecutionLimit() * 3;
}
private function deleteTaskIfNewsletterDoesNotExist(ScheduledTaskEntity $task) {
$queue = $task->getSendingQueue();
$newsletter = $queue ? $queue->getNewsletter() : null;
if ($newsletter !== null) {
return;
}
$this->deleteTask($task);
}
private function deleteTask(ScheduledTaskEntity $task) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
'delete task in sending queue',
['task_id' => $task->getId()]
);
$queue = $task->getSendingQueue();
if ($queue) {
$this->sendingQueuesRepository->remove($queue);
}
$this->scheduledTaskSubscribersRepository->deleteByScheduledTask($task);
$this->scheduledTasksRepository->remove($task);
$this->scheduledTasksRepository->flush();
}
private function endSending(ScheduledTaskEntity $task, NewsletterEntity $newsletter): void {
// We should handle all transitions into these states in the processSending method and end processing there or we throw an exception
// This might theoretically happen when multiple cron workers are running in parallel which we don't support and try to prevent
$unexpectedStates = [
ScheduledTaskEntity::STATUS_PAUSED,
ScheduledTaskEntity::STATUS_INVALID,
ScheduledTaskEntity::STATUS_SCHEDULED,
];
if (in_array($task->getStatus(), $unexpectedStates)) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->error(
'Sending task reached end of processing in sending queue worker in an unexpected state.',
['task_id' => $task->getId(), 'status' => $task->getStatus()]
);
return;
}
// The task is running but there is no one to send to.
// This may happen when we send to all but the execution is interrupted (e.g. by PHP time limit) and we don't update the task status
// or if we trigger sending to a newsletter without any subscriber (e.g. scheduled for long time but all were deleted)
// Lets set status to completed and update the queue counts
if ($task->getStatus() === null && $this->scheduledTaskSubscribersRepository->countUnprocessed($task) === 0) {
$task->setStatus(ScheduledTaskEntity::STATUS_COMPLETED);
$queue = $task->getSendingQueue();
if ($queue) {
$this->sendingQueuesRepository->updateCounts($queue);
}
$this->scheduledTasksRepository->flush();
}
// Task is completed let's do all the stuff for the completed task
if ($task->getStatus() === ScheduledTaskEntity::STATUS_COMPLETED) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
'completed newsletter sending',
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()]
);
$this->newsletterTask->markNewsletterAsSent($newsletter);
$this->statsNotificationsScheduler->schedule($newsletter);
}
}
}
@@ -0,0 +1,88 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\SendingQueue;
if (!defined('ABSPATH')) exit;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Settings\SettingsController;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Monolog\Logger;
class SendingThrottlingHandler {
public const BATCH_SIZE = 20;
public const SETTINGS_KEY = 'mta_throttling';
public const SUCCESS_THRESHOLD_TO_INCREASE = 10;
/** @var Logger */
private $logger;
/** @var SettingsController */
private $settings;
/** @var WPFunctions */
private $wp;
public function __construct(
LoggerFactory $loggerFactory,
SettingsController $settings,
WPFunctions $wp
) {
$this->logger = $loggerFactory->getLogger(LoggerFactory::TOPIC_SENDING);
$this->settings = $settings;
$this->wp = $wp;
}
public function getBatchSize(): int {
$throttlingSettings = $this->loadSettings();
if (isset($throttlingSettings['batch_size'])) {
return $throttlingSettings['batch_size'];
}
return $this->getMaxBatchSize();
}
private function getMaxBatchSize(): int {
return $this->wp->applyFilters('mailpoet_cron_worker_sending_queue_batch_size', self::BATCH_SIZE);
}
public function throttleBatchSize(): int {
$batchSize = $this->getBatchSize();
if ($batchSize > 1) {
$batchSize = (int)ceil($this->getBatchSize() / 2);
$throttlingSettings = $this->loadSettings();
$throttlingSettings['batch_size'] = $batchSize;
unset($throttlingSettings['success_count']);
$this->logger->error("MailPoet throttling: decrease batch_size to: {$batchSize}");
$this->saveSettings($throttlingSettings);
}
return $batchSize;
}
public function processSuccess(): void {
$throttlingSettings = $this->loadSettings();
if (!isset($throttlingSettings['batch_size'])) {
return;
}
$throttlingSettings['success_count'] = isset($throttlingSettings['success_count']) ? ++$throttlingSettings['success_count'] : 1;
$this->logger->info("MailPoet throttling: increase success_count to: {$throttlingSettings['success_count']}");
if ($throttlingSettings['success_count'] >= self::SUCCESS_THRESHOLD_TO_INCREASE) {
unset($throttlingSettings['success_count']);
$throttlingSettings['batch_size'] = min($this->getMaxBatchSize(), $throttlingSettings['batch_size'] * 2);
$this->logger->info("MailPoet throttling: increase batch_size to: {$throttlingSettings['batch_size']}");
if ($this->getMaxBatchSize() === $throttlingSettings['batch_size']) {
unset($throttlingSettings['batch_size']);
}
}
$this->saveSettings($throttlingSettings);
}
private function loadSettings(): ?array {
return $this->settings->get(self::SETTINGS_KEY);
}
private function saveSettings(array $settings): void {
$this->settings->set(self::SETTINGS_KEY, $settings);
}
}
@@ -0,0 +1,105 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\Workers\StatsNotifications\NewsletterLinkRepository;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterLinkEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Newsletter\Links\Links as NewsletterLinks;
use MailPoet\Router\Endpoints\Track;
use MailPoet\Router\Router;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Subscribers\LinkTokens;
use MailPoet\Subscription\SubscriptionUrlFactory;
use MailPoet\Util\Helpers;
class Links {
/** @var LinkTokens */
private $linkTokens;
/** @var NewsletterLinks */
private $newsletterLinks;
/** @var NewsletterLinkRepository */
private $newsletterLinkRepository;
/** @var TrackingConfig */
private $trackingConfig;
public function __construct(
LinkTokens $linkTokens,
NewsletterLinks $newsletterLinks,
NewsletterLinkRepository $newsletterLinkRepository,
TrackingConfig $trackingConfig
) {
$this->linkTokens = $linkTokens;
$this->newsletterLinks = $newsletterLinks;
$this->newsletterLinkRepository = $newsletterLinkRepository;
$this->trackingConfig = $trackingConfig;
}
public function process($renderedNewsletter, NewsletterEntity $newsletter, SendingQueueEntity $queue) {
[$renderedNewsletter, $links] = $this->hashAndReplaceLinks($renderedNewsletter, $newsletter->getId(), $queue->getId());
$this->saveLinks($links, $newsletter, $queue);
return $renderedNewsletter;
}
public function hashAndReplaceLinks($renderedNewsletter, $newsletterId, $queueId) {
// join HTML and TEXT rendered body into a text string
$content = Helpers::joinObject($renderedNewsletter);
[$content, $links] = $this->newsletterLinks->process($content, $newsletterId, $queueId);
$links = $this->newsletterLinks->ensureInstantUnsubscribeLink($links);
// split the processed body with hashed links back to HTML and TEXT
list($renderedNewsletter['html'], $renderedNewsletter['text'])
= Helpers::splitObject($content);
return [
$renderedNewsletter,
$links,
];
}
public function saveLinks($links, NewsletterEntity $newsletter, SendingQueueEntity $queue) {
return $this->newsletterLinks->save($links, $newsletter->getId(), $queue->getId());
}
public function getUnsubscribeUrl($queueId, SubscriberEntity $subscriber = null) {
if ($this->trackingConfig->isEmailTrackingEnabled() && $subscriber) {
$linkHash = $this->newsletterLinkRepository->findOneBy(
[
'queue' => $queueId,
'url' => NewsletterLinkEntity::INSTANT_UNSUBSCRIBE_LINK_SHORT_CODE,
]
);
if (!$linkHash instanceof NewsletterLinkEntity) {
return '';
}
$data = $this->newsletterLinks->createUrlDataObject(
$subscriber->getId(),
$this->linkTokens->getToken($subscriber),
$queueId,
$linkHash->getHash(),
false
);
$url = Router::buildRequest(
Track::ENDPOINT,
Track::ACTION_CLICK,
$data
);
} else {
$subscriptionUrlFactory = SubscriptionUrlFactory::getInstance();
$url = $subscriptionUrlFactory->getUnsubscribeUrl($subscriber, $queueId);
}
return $url;
}
public function getOneClickUnsubscribeUrl($queueId, SubscriberEntity $subscriber): string {
$subscriptionUrlFactory = SubscriptionUrlFactory::getInstance();
return $subscriptionUrlFactory->getUnsubscribeUrl($subscriber, $queueId);
}
}
@@ -0,0 +1,91 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Mailer\Mailer as MailerInstance;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Mailer\MailerLog;
use MailPoet\Mailer\Methods\MailPoet;
class Mailer {
/** @var MailerFactory */
private $mailerFactory;
/** @var MailerInstance */
private $mailer;
public function __construct(
MailerFactory $mailerFactory
) {
$this->mailerFactory = $mailerFactory;
$this->mailer = $this->configureMailer();
}
public function configureMailer(NewsletterEntity $newsletter = null) {
$sender['address'] = ($newsletter && !empty($newsletter->getSenderAddress())) ?
$newsletter->getSenderAddress() :
null;
$sender['name'] = ($newsletter && !empty($newsletter->getSenderName())) ?
$newsletter->getSenderName() :
null;
$replyTo['address'] = ($newsletter && !empty($newsletter->getReplyToAddress())) ?
$newsletter->getReplyToAddress() :
null;
$replyTo['name'] = ($newsletter && !empty($newsletter->getReplyToName())) ?
$newsletter->getReplyToName() :
null;
if (!$sender['address']) {
$sender = null;
}
if (!$replyTo['address']) {
$replyTo = null;
}
$this->mailer = $this->mailerFactory->buildMailer(null, $sender, $replyTo);
return $this->mailer;
}
public function getMailerLog() {
return MailerLog::getMailerLog();
}
public function updateSentCount() {
return MailerLog::incrementSentCount();
}
public function getProcessingMethod() {
return ($this->mailer->mailerMethod instanceof MailPoet) ?
'bulk' :
'individual';
}
public function prepareSubscriberForSending(SubscriberEntity $subscriber) {
return $this->mailer->formatSubscriberNameAndEmailAddress($subscriber);
}
public function sendBulk($preparedNewsletters, $preparedSubscribers, $extraParams = []) {
if ($this->getProcessingMethod() === 'individual') {
throw new \LogicException('Trying to send a batch with individual processing method');
}
return $this->mailer->mailerMethod->send(
$preparedNewsletters,
$preparedSubscribers,
$extraParams
);
}
public function send($preparedNewsletter, $preparedSubscriber, $extraParams = []) {
if ($this->getProcessingMethod() === 'bulk') {
throw new \LogicException('Trying to send an individual email with a bulk processing method');
}
return $this->mailer->mailerMethod->send(
$preparedNewsletter,
$preparedSubscriber,
$extraParams
);
}
}
@@ -0,0 +1,388 @@
<?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('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);
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();
}
}
@@ -0,0 +1,68 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
if (!defined('ABSPATH')) exit;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterPostEntity;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Newsletter\NewsletterPostsRepository;
class Posts {
/** @var LoggerFactory */
private $loggerFactory;
/** @var NewsletterPostsRepository */
private $newsletterPostRepository;
public function __construct() {
$this->loggerFactory = LoggerFactory::getInstance();
$this->newsletterPostRepository = ContainerWrapper::getInstance()->get(NewsletterPostsRepository::class);
}
public function extractAndSave($renderedNewsletter, NewsletterEntity $newsletter): bool {
if ($newsletter->getType() !== NewsletterEntity::TYPE_NOTIFICATION_HISTORY) {
return false;
}
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info(
'extract and save posts - before',
['newsletter_id' => $newsletter->getId()]
);
preg_match_all(
'/data-post-id="(\d+)"/ism',
$renderedNewsletter['html'],
$matchedPostsIds
);
$matchedPostsIds = $matchedPostsIds[1];
if (!count($matchedPostsIds)) {
return false;
}
$parent = $newsletter->getParent(); // parent post notification
if (!$parent instanceof NewsletterEntity) {
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info(
'parent post has not been found',
['newsletter_id' => $newsletter->getId()]
);
return false;
}
foreach ($matchedPostsIds as $postId) {
$newsletterPost = new NewsletterPostEntity($parent, $postId);
$this->newsletterPostRepository->persist($newsletterPost);
}
$this->newsletterPostRepository->flush();
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info(
'extract and save posts - after',
['newsletter_id' => $newsletter->getId(), 'matched_posts_ids' => $matchedPostsIds]
);
return true;
}
public function getAlcPostsCount($renderedNewsletter, NewsletterEntity $newsletter) {
$templatePostsCount = substr_count($newsletter->getContent(), 'data-post-id');
$newsletterPostsCount = substr_count($renderedNewsletter['html'], 'data-post-id');
return $newsletterPostsCount - $templatePostsCount;
}
}
@@ -0,0 +1,45 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
if (!defined('ABSPATH')) exit;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Newsletter\Shortcodes\Shortcodes as NewsletterShortcodes;
class Shortcodes {
/**
* @param string $content
* @param string|null $contentSource
* @param NewsletterEntity|null $newsletter
* @param SubscriberEntity|null $subscriber
* @param SendingQueueEntity|null $queue
*/
public static function process($content, $contentSource = null, NewsletterEntity $newsletter = null, SubscriberEntity $subscriber = null, SendingQueueEntity $queue = null) {
/** @var NewsletterShortcodes $shortcodes */
$shortcodes = ContainerWrapper::getInstance()->get(NewsletterShortcodes::class);
if ($queue instanceof SendingQueueEntity) {
$shortcodes->setQueue($queue);
} else {
$shortcodes->setQueue(null);
}
if ($newsletter instanceof NewsletterEntity) {
$shortcodes->setNewsletter($newsletter);
} else {
$shortcodes->setNewsletter(null);
}
if ($subscriber instanceof SubscriberEntity) {
$shortcodes->setSubscriber($subscriber);
} else {
$shortcodes->setSubscriber(null);
}
return $shortcodes->replace($content, $contentSource);
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,92 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\CronHelper;
use MailPoet\Cron\CronWorkerInterface;
use MailPoet\Cron\CronWorkerRunner;
use MailPoet\Cron\CronWorkerScheduler;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
use MailPoetVendor\Carbon\Carbon;
abstract class SimpleWorker implements CronWorkerInterface {
const TASK_TYPE = null;
const AUTOMATIC_SCHEDULING = true;
const SUPPORT_MULTIPLE_INSTANCES = true;
public $timer;
/** @var CronHelper */
protected $cronHelper;
/** @var CronWorkerScheduler */
protected $cronWorkerScheduler;
/** @var ScheduledTasksRepository */
protected $scheduledTasksRepository;
public function __construct() {
if (static::TASK_TYPE === null) {
throw new \Exception('Constant TASK_TYPE is not defined on subclass ' . get_class($this));
}
$this->cronHelper = ContainerWrapper::getInstance()->get(CronHelper::class);
$this->cronWorkerScheduler = ContainerWrapper::getInstance()->get(CronWorkerScheduler::class);
$this->scheduledTasksRepository = ContainerWrapper::getInstance()->get(ScheduledTasksRepository::class);
}
public function getTaskType() {
return static::TASK_TYPE;
}
public function supportsMultipleInstances() {
return static::SUPPORT_MULTIPLE_INSTANCES;
}
public function schedule() {
$this->cronWorkerScheduler->schedule(static::TASK_TYPE, $this->getNextRunDate());
}
protected function scheduleImmediately(): void {
$this->cronWorkerScheduler->schedule(static::TASK_TYPE, $this->getNextRunDateImmediately());
}
public function checkProcessingRequirements() {
return true;
}
public function init() {
}
public function prepareTaskStrategy(ScheduledTaskEntity $task, $timer) {
return true;
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
return true;
}
public function getNextRunDate() {
// random day of the next week
$date = Carbon::now()->millisecond(0);
$date->setISODate((int)$date->format('o'), ((int)$date->format('W')) + 1, mt_rand(1, 7));
$date->startOfDay();
return $date;
}
protected function getNextRunDateImmediately(): Carbon {
return Carbon::now()->millisecond(0);
}
public function scheduleAutomatically() {
return static::AUTOMATIC_SCHEDULING;
}
protected function getCompletedTasks() {
return $this->scheduledTasksRepository->findCompletedByType(static::TASK_TYPE, CronWorkerRunner::TASK_BATCH_SIZE);
}
}
@@ -0,0 +1,171 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\StatsNotifications;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Renderer;
use MailPoet\Cron\Workers\SimpleWorker;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Mailer\MetaInfo;
use MailPoet\Newsletter\NewslettersRepository;
use MailPoet\Newsletter\Statistics\NewsletterStatistics;
use MailPoet\Newsletter\Statistics\NewsletterStatisticsRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
class AutomatedEmails extends SimpleWorker {
const TASK_TYPE = 'stats_notification_automated_emails';
/** @var MailerFactory */
private $mailerFactory;
/** @var SettingsController */
private $settings;
/** @var Renderer */
private $renderer;
/** @var MetaInfo */
private $mailerMetaInfo;
/** @var NewslettersRepository */
private $repository;
/** @var NewsletterStatisticsRepository */
private $newsletterStatisticsRepository;
/** @var TrackingConfig */
private $trackingConfig;
public function __construct(
MailerFactory $mailerFactory,
Renderer $renderer,
SettingsController $settings,
NewslettersRepository $repository,
NewsletterStatisticsRepository $newsletterStatisticsRepository,
MetaInfo $mailerMetaInfo,
TrackingConfig $trackingConfig
) {
parent::__construct();
$this->mailerFactory = $mailerFactory;
$this->settings = $settings;
$this->renderer = $renderer;
$this->mailerMetaInfo = $mailerMetaInfo;
$this->repository = $repository;
$this->newsletterStatisticsRepository = $newsletterStatisticsRepository;
$this->trackingConfig = $trackingConfig;
}
public function checkProcessingRequirements() {
$settings = $this->settings->get(Worker::SETTINGS_KEY);
if (!is_array($settings)) {
return false;
}
if (!isset($settings['automated'])) {
return false;
}
if (!isset($settings['address'])) {
return false;
}
if (empty(trim($settings['address']))) {
return false;
}
if (!$this->trackingConfig->isEmailTrackingEnabled()) {
return false;
}
return (bool)$settings['automated'];
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
try {
$settings = $this->settings->get(Worker::SETTINGS_KEY);
$newsletters = $this->getNewsletters();
if ($newsletters) {
$extraParams = [
'meta' => $this->mailerMetaInfo->getStatsNotificationMetaInfo(),
];
$this->mailerFactory->getDefaultMailer()->send($this->constructNewsletter($newsletters), $settings['address'], $extraParams);
}
} catch (\Exception $e) {
if (WP_DEBUG) {
throw $e;
}
}
return true;
}
/**
* @param array<int, array{newsletter: NewsletterEntity, statistics: NewsletterStatistics}> $newsletters
*/
private function constructNewsletter(array $newsletters): array {
$context = $this->prepareContext($newsletters);
return [
'subject' => __('Your monthly stats are in!', 'mailpoet'),
'body' => [
'html' => $this->renderer->render('emails/statsNotificationAutomatedEmails.html', $context),
'text' => $this->renderer->render('emails/statsNotificationAutomatedEmails.txt', $context),
],
];
}
/**
* @return array<int, array{newsletter: NewsletterEntity, statistics: NewsletterStatistics}>
*/
protected function getNewsletters(): array {
$result = [];
$newsletters = $this->repository->findActiveByTypes(
[NewsletterEntity::TYPE_AUTOMATIC, NewsletterEntity::TYPE_WELCOME]
);
foreach ($newsletters as $newsletter) {
$statistics = $this->newsletterStatisticsRepository->getStatistics($newsletter);
if ($statistics->getTotalSentCount()) {
$result[] = [
'statistics' => $statistics,
'newsletter' => $newsletter,
];
}
}
return $result;
}
/**
* @param array<int, array{newsletter: NewsletterEntity, statistics: NewsletterStatistics}> $newsletters
* @return array
*/
private function prepareContext(array $newsletters): array {
$context = [
'linkSettings' => WPFunctions::get()->getSiteUrl(null, '/wp-admin/admin.php?page=mailpoet-settings#basics'),
'newsletters' => [],
];
foreach ($newsletters as $row) {
$statistics = $row['statistics'];
$newsletter = $row['newsletter'];
$clicked = ($statistics->getClickCount() * 100) / $statistics->getTotalSentCount();
$opened = ($statistics->getOpenCount() * 100) / $statistics->getTotalSentCount();
$machineOpened = ($statistics->getMachineOpenCount() * 100) / $statistics->getTotalSentCount();
$unsubscribed = ($statistics->getUnsubscribeCount() * 100) / $statistics->getTotalSentCount();
$bounced = ($statistics->getBounceCount() * 100) / $statistics->getTotalSentCount();
$context['newsletters'][] = [
'linkStats' => WPFunctions::get()->getSiteUrl(null, '/wp-admin/admin.php?page=mailpoet-newsletters#/stats/' . $newsletter->getId()),
'clicked' => $clicked,
'opened' => $opened,
'machineOpened' => $machineOpened,
'unsubscribed' => $unsubscribed,
'bounced' => $bounced,
'subject' => $newsletter->getSubject(),
];
}
return $context;
}
public function getNextRunDate() {
$date = Carbon::now()->millisecond(0);
return $date->endOfMonth()->next(Carbon::MONDAY)->midDay();
}
}
@@ -0,0 +1,62 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\StatsNotifications;
if (!defined('ABSPATH')) exit;
use MailPoet\Doctrine\Repository;
use MailPoet\Entities\NewsletterLinkEntity;
use MailPoet\Entities\StatisticsClickEntity;
use MailPoetVendor\Doctrine\DBAL\Result;
/**
* @extends Repository<NewsletterLinkEntity>
*/
class NewsletterLinkRepository extends Repository {
protected function getEntityClassName() {
return NewsletterLinkEntity::class;
}
/**
* @param int $newsletterId
* @return NewsletterLinkEntity|null
*/
public function findTopLinkForNewsletter($newsletterId) {
$statisticsClicksTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName();
$topIdQuery = $this->entityManager->getConnection()->createQueryBuilder()
->select('c.link_id')
->addSelect('count(c.id) AS counter')
->from($statisticsClicksTable, 'c')
->where('c.newsletter_id = :newsletterId')
->setParameter('newsletterId', $newsletterId)
->groupBy('c.link_id')
->orderBy('counter', 'desc')
->setMaxResults(1)
->execute();
if (!$topIdQuery instanceof Result) {
return null;
}
$topId = $topIdQuery->fetch();
if (is_array($topId) && isset($topId['link_id'])) {
return $this->findOneById((int)$topId['link_id']);
}
return null;
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(NewsletterLinkEntity::class, 'l')
->where('l.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (NewsletterLinkEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}
@@ -0,0 +1,113 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\StatsNotifications;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\StatsNotificationEntity;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class Scheduler {
/**
* How many hours after the newsletter will be the stats notification sent
* @var int
*/
const HOURS_TO_SEND_AFTER_NEWSLETTER = 24;
/** @var SettingsController */
private $settings;
private $supportedTypes = [
NewsletterEntity::TYPE_NOTIFICATION_HISTORY,
NewsletterEntity::TYPE_STANDARD,
];
/** @var EntityManager */
private $entityManager;
/** @var StatsNotificationsRepository */
private $repository;
/** @var TrackingConfig */
private $trackingConfig;
public function __construct(
SettingsController $settings,
EntityManager $entityManager,
StatsNotificationsRepository $repository,
TrackingConfig $trackingConfig
) {
$this->settings = $settings;
$this->entityManager = $entityManager;
$this->repository = $repository;
$this->trackingConfig = $trackingConfig;
}
public function schedule(NewsletterEntity $newsletter) {
if (!$this->shouldSchedule($newsletter)) {
return false;
}
$task = new ScheduledTaskEntity();
$task->setType(Worker::TASK_TYPE);
$task->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED);
$task->setScheduledAt($this->getNextRunDate());
$this->entityManager->persist($task);
$this->entityManager->flush();
$statsNotifications = new StatsNotificationEntity($newsletter, $task);
$this->entityManager->persist($statsNotifications);
$this->entityManager->flush();
}
private function shouldSchedule(NewsletterEntity $newsletter) {
if ($this->isDisabled()) {
return false;
}
if (!in_array($newsletter->getType(), $this->supportedTypes)) {
return false;
}
if ($this->hasTaskBeenScheduled($newsletter->getId())) {
return false;
}
return true;
}
private function isDisabled() {
$settings = $this->settings->get(Worker::SETTINGS_KEY);
if (!is_array($settings)) {
return true;
}
if (!isset($settings['enabled'])) {
return true;
}
if (!isset($settings['address'])) {
return true;
}
if (empty(trim($settings['address']))) {
return true;
}
if (!$this->trackingConfig->isEmailTrackingEnabled()) {
return true;
}
return !(bool)$settings['enabled'];
}
private function hasTaskBeenScheduled($newsletterId) {
$existing = $this->repository->findOneByNewsletterId($newsletterId);
return $existing instanceof StatsNotificationEntity;
}
private function getNextRunDate() {
$date = new Carbon();
$date->addHours(self::HOURS_TO_SEND_AFTER_NEWSLETTER);
return $date;
}
}
@@ -0,0 +1,87 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\StatsNotifications;
if (!defined('ABSPATH')) exit;
use MailPoet\Doctrine\Repository;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\StatsNotificationEntity;
use MailPoetVendor\Carbon\Carbon;
/**
* @extends Repository<StatsNotificationEntity>
*/
class StatsNotificationsRepository extends Repository {
protected function getEntityClassName() {
return StatsNotificationEntity::class;
}
/**
* @param int $newsletterId
* @return StatsNotificationEntity|null
*/
public function findOneByNewsletterId($newsletterId) {
return $this->doctrineRepository
->createQueryBuilder('sn')
->andWhere('sn.newsletter = :newsletterId')
->setParameter('newsletterId', $newsletterId)
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
/**
* @param int|null $limit
* @return StatsNotificationEntity[]
*/
public function findScheduled($limit = null) {
$date = new Carbon();
$query = $this->doctrineRepository
->createQueryBuilder('sn')
->join('sn.task', 'tasks')
->join('sn.newsletter', 'n')
->addSelect('tasks')
->addSelect('n')
->addOrderBy('tasks.priority')
->addOrderBy('tasks.updatedAt')
->where('tasks.deletedAt IS NULL')
->andWhere('tasks.status = :status')
->setParameter('status', ScheduledTaskEntity::STATUS_SCHEDULED)
->andWhere('tasks.scheduledAt < :date')
->setParameter('date', $date)
->andWhere('tasks.type = :workerType')
->setParameter('workerType', Worker::TASK_TYPE);
if (is_int($limit)) {
$query->setMaxResults($limit);
}
return $query->getQuery()->getResult();
}
public function deleteOrphanedScheduledTasks() {
$scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
$statsNotificationsTable = $this->entityManager->getClassMetadata(StatsNotificationEntity::class)->getTableName();
$this->entityManager->getConnection()->executeStatement("
DELETE st FROM $scheduledTasksTable st
LEFT JOIN $statsNotificationsTable sn ON sn.task_id = st.id
WHERE sn.id IS NULL AND st.type = :taskType;
", ['taskType' => Worker::TASK_TYPE]);
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(StatsNotificationEntity::class, 'n')
->where('n.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (StatsNotificationEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}
@@ -0,0 +1,205 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers\StatsNotifications;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\Renderer;
use MailPoet\Config\ServicesChecker;
use MailPoet\Cron\CronHelper;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\NewsletterLinkEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\StatsNotificationEntity;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Mailer\MetaInfo;
use MailPoet\Newsletter\Statistics\NewsletterStatisticsRepository;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class Worker {
const TASK_TYPE = 'stats_notification';
const SETTINGS_KEY = 'stats_notifications';
const BATCH_SIZE = 5;
/** @var Renderer */
private $renderer;
/** @var MailerFactory */
private $mailerFactory;
/** @var SettingsController */
private $settings;
/** @var CronHelper */
private $cronHelper;
/** @var MetaInfo */
private $mailerMetaInfo;
/** @var StatsNotificationsRepository */
private $repository;
/** @var EntityManager */
private $entityManager;
/** @var NewsletterLinkRepository */
private $newsletterLinkRepository;
/** @var NewsletterStatisticsRepository */
private $newsletterStatisticsRepository;
/** @var SubscribersFeature */
private $subscribersFeature;
/** @var SubscribersRepository */
private $subscribersRepository;
/** @var ServicesChecker */
private $servicesChecker;
public function __construct(
MailerFactory $mailerFactory,
Renderer $renderer,
SettingsController $settings,
CronHelper $cronHelper,
MetaInfo $mailerMetaInfo,
StatsNotificationsRepository $repository,
NewsletterLinkRepository $newsletterLinkRepository,
NewsletterStatisticsRepository $newsletterStatisticsRepository,
EntityManager $entityManager,
SubscribersFeature $subscribersFeature,
SubscribersRepository $subscribersRepository,
ServicesChecker $servicesChecker
) {
$this->renderer = $renderer;
$this->mailerFactory = $mailerFactory;
$this->settings = $settings;
$this->cronHelper = $cronHelper;
$this->mailerMetaInfo = $mailerMetaInfo;
$this->repository = $repository;
$this->entityManager = $entityManager;
$this->newsletterLinkRepository = $newsletterLinkRepository;
$this->newsletterStatisticsRepository = $newsletterStatisticsRepository;
$this->subscribersFeature = $subscribersFeature;
$this->subscribersRepository = $subscribersRepository;
$this->servicesChecker = $servicesChecker;
}
/** @throws \Exception */
public function process($timer = false) {
$timer = $timer ?: microtime(true);
$settings = $this->settings->get(self::SETTINGS_KEY);
// Cleanup potential orphaned task created due bug MAILPOET-3015
$this->repository->deleteOrphanedScheduledTasks();
foreach ($this->repository->findScheduled(self::BATCH_SIZE) as $statsNotificationEntity) {
try {
$extraParams = [
'meta' => $this->mailerMetaInfo->getStatsNotificationMetaInfo(),
];
$this->mailerFactory->getDefaultMailer()->send($this->constructNewsletter($statsNotificationEntity), $settings['address'], $extraParams);
} catch (\Exception $e) {
if (WP_DEBUG) {
throw $e;
}
} finally {
$task = $statsNotificationEntity->getTask();
if ($task instanceof ScheduledTaskEntity) {
$this->markTaskAsFinished($task);
}
}
$this->cronHelper->enforceExecutionLimit($timer);
}
}
private function constructNewsletter(StatsNotificationEntity $statsNotificationEntity) {
$newsletter = $statsNotificationEntity->getNewsletter();
if (!$newsletter instanceof NewsletterEntity) {
throw new \RuntimeException('Missing newsletter entity for statistic notification.');
}
$link = $this->newsletterLinkRepository->findTopLinkForNewsletter((int)$newsletter->getId());
$sendingQueue = $newsletter->getLatestQueue();
if (!$sendingQueue instanceof SendingQueueEntity) {
throw new \RuntimeException('Missing sending queue entity for statistic notification.');
}
$context = $this->prepareContext($newsletter, $sendingQueue, $link);
$subject = $sendingQueue->getNewsletterRenderedSubject();
return [
// translators: %s is the subject of the email.
'subject' => sprintf(_x('Stats for email %s', 'title of an automatic email containing statistics (newsletter open rate, click rate, etc)', 'mailpoet'), $subject),
'body' => [
'html' => $this->renderer->render('emails/statsNotification.html', $context),
'text' => $this->renderer->render('emails/statsNotification.txt', $context),
],
];
}
private function prepareContext(NewsletterEntity $newsletter, SendingQueueEntity $sendingQueue, NewsletterLinkEntity $link = null) {
$statistics = $this->newsletterStatisticsRepository->getStatistics($newsletter);
$clicked = ($statistics->getClickCount() * 100) / $statistics->getTotalSentCount();
$opened = ($statistics->getOpenCount() * 100) / $statistics->getTotalSentCount();
$machineOpened = ($statistics->getMachineOpenCount() * 100) / $statistics->getTotalSentCount();
$unsubscribed = ($statistics->getUnsubscribeCount() * 100) / $statistics->getTotalSentCount();
$bounced = ($statistics->getBounceCount() * 100) / $statistics->getTotalSentCount();
$subject = $sendingQueue->getNewsletterRenderedSubject();
$subscribersCount = $this->subscribersRepository->getTotalSubscribers();
$hasValidApiKey = $this->subscribersFeature->hasValidApiKey();
$context = [
'subject' => $subject,
'preheader' => sprintf(
// translators: %1$s is the percentage of clicks, %2$s the percentage of opens and %3$s the number of unsubscribes.
_x(
'%1$s%% clicks, %2$s%% opens, %3$s%% unsubscribes in a nutshell.',
'newsletter open rate, click rate and unsubscribe rate',
'mailpoet'
),
number_format($clicked, 2),
number_format($opened, 2),
number_format($unsubscribed, 2)
),
'topLinkClicks' => 0,
'linkSettings' => WPFunctions::get()->getSiteUrl(null, '/wp-admin/admin.php?page=mailpoet-settings#basics'),
'linkStats' => WPFunctions::get()->getSiteUrl(null, '/wp-admin/admin.php?page=mailpoet-newsletters&stats=' . $newsletter->getId()),
'clicked' => $clicked,
'opened' => $opened,
'machineOpened' => $machineOpened,
'unsubscribed' => $unsubscribed,
'bounced' => $bounced,
'subscribersLimitReached' => $this->subscribersFeature->check(),
'hasValidApiKey' => $hasValidApiKey,
'subscribersLimit' => $this->subscribersFeature->getSubscribersLimit(),
'upgradeNowLink' => $hasValidApiKey
? 'https://account.mailpoet.com/orders/upgrade/' . $this->servicesChecker->generatePartialApiKey()
: 'https://account.mailpoet.com/?s=' . ($subscribersCount + 1),
];
if ($link) {
$context['topLinkClicks'] = $link->getTotalClicksCount();
$mappings = self::getShortcodeLinksMapping();
$context['topLink'] = isset($mappings[$link->getUrl()]) ? $mappings[$link->getUrl()] : $link->getUrl();
}
return $context;
}
private function markTaskAsFinished(ScheduledTaskEntity $task) {
$task->setStatus(ScheduledTaskEntity::STATUS_COMPLETED);
$task->setProcessedAt(Carbon::now()->millisecond(0));
$task->setScheduledAt(null);
$this->entityManager->flush();
}
public static function getShortcodeLinksMapping() {
return [
NewsletterLinkEntity::UNSUBSCRIBE_LINK_SHORT_CODE => __('Unsubscribe link', 'mailpoet'),
NewsletterLinkEntity::INSTANT_UNSUBSCRIBE_LINK_SHORT_CODE => __('Unsubscribe link (without confirmation)', 'mailpoet'),
'[link:subscription_manage_url]' => __('Manage subscription link', 'mailpoet'),
'[link:newsletter_view_in_browser_url]' => __('View in browser link', 'mailpoet'),
];
}
}
@@ -0,0 +1,48 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\DI\ContainerWrapper;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\DBAL\ParameterType;
use MailPoetVendor\Doctrine\ORM\EntityManager;
if (!defined('ABSPATH')) exit;
class SubscriberLinkTokens extends SimpleWorker {
const TASK_TYPE = 'subscriber_link_tokens';
const BATCH_SIZE = 10000;
const AUTOMATIC_SCHEDULING = false;
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$entityManager = ContainerWrapper::getInstance()->get(EntityManager::class);
$subscribersRepository = ContainerWrapper::getInstance()->get(SubscribersRepository::class);
$subscribersTable = $entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
$connection = $entityManager->getConnection();
$count = $subscribersRepository->countBy(['linkToken' => null]);
if ($count) {
$authKey = defined('AUTH_KEY') ? AUTH_KEY : '';
$connection->executeStatement(
"UPDATE {$subscribersTable} SET link_token = SUBSTRING(MD5(CONCAT(:authKey, email)), 1, :tokenLength) WHERE link_token IS NULL LIMIT :limit",
['authKey' => $authKey, 'tokenLength' => SubscriberEntity::OBSOLETE_LINK_TOKEN_LENGTH, 'limit' => self::BATCH_SIZE],
['authKey' => ParameterType::STRING, 'tokenLength' => ParameterType::INTEGER, 'limit' => ParameterType::INTEGER]
);
$this->schedule();
}
return true;
}
public function getNextRunDate() {
return Carbon::now()->millisecond(0);
}
}
@@ -0,0 +1,95 @@
<?php declare(strict_types = 1);
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Cache\TransientCache;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Subscribers\SubscribersCountsController;
use MailPoetVendor\Carbon\Carbon;
class SubscribersCountCacheRecalculation extends SimpleWorker {
private const EXPIRATION_IN_MINUTES = 30;
const TASK_TYPE = 'subscribers_count_cache_recalculation';
const AUTOMATIC_SCHEDULING = false;
const SUPPORT_MULTIPLE_INSTANCES = false;
/** @var TransientCache */
private $transientCache;
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var SubscribersCountsController */
private $subscribersCountsController;
public function __construct(
TransientCache $transientCache,
SegmentsRepository $segmentsRepository,
SubscribersCountsController $subscribersCountsController
) {
parent::__construct();
$this->transientCache = $transientCache;
$this->segmentsRepository = $segmentsRepository;
$this->subscribersCountsController = $subscribersCountsController;
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$segments = $this->segmentsRepository->findAll();
foreach ($segments as $segment) {
$this->recalculateSegmentCache($timer, (int)$segment->getId(), $segment);
}
// update cache for subscribers without segment
$this->recalculateSegmentCache($timer, 0);
$this->recalculateHomepageCache($timer);
// remove redundancies from cache
$this->cronHelper->enforceExecutionLimit($timer);
$this->subscribersCountsController->removeRedundancyFromStatisticsCache();
return true;
}
private function recalculateSegmentCache($timer, int $segmentId, ?SegmentEntity $segment = null): void {
$this->cronHelper->enforceExecutionLimit($timer);
$now = Carbon::now();
$item = $this->transientCache->getItem(TransientCache::SUBSCRIBERS_STATISTICS_COUNT_KEY, $segmentId);
if ($item === null || !isset($item['created_at']) || $now->diffInMinutes($item['created_at']) > self::EXPIRATION_IN_MINUTES) {
if ($segment) {
$this->subscribersCountsController->recalculateSegmentStatisticsCache($segment);
} else {
$this->subscribersCountsController->recalculateSubscribersWithoutSegmentStatisticsCache();
}
}
}
private function recalculateHomepageCache($timer): void {
$this->cronHelper->enforceExecutionLimit($timer);
$now = Carbon::now();
$item = $this->transientCache->getItem(TransientCache::SUBSCRIBERS_HOMEPAGE_STATISTICS_COUNT_KEY, 0);
if ($item === null || !isset($item['created_at']) || $now->diffInMinutes($item['created_at']) > self::EXPIRATION_IN_MINUTES) {
$this->cronHelper->enforceExecutionLimit($timer);
$this->subscribersCountsController->recalculateHomepageStatisticsCache();
}
}
public function getNextRunDate() {
return Carbon::now()->millisecond(0);
}
public function shouldBeScheduled(): bool {
$scheduledOrRunningTask = $this->scheduledTasksRepository->findScheduledOrRunningTask(self::TASK_TYPE);
if ($scheduledOrRunningTask) {
return false;
}
$now = Carbon::now();
$oldestCreatedAt = $this->transientCache->getOldestCreatedAt(TransientCache::SUBSCRIBERS_STATISTICS_COUNT_KEY);
return $oldestCreatedAt === null || $now->diffInMinutes($oldestCreatedAt) > self::EXPIRATION_IN_MINUTES;
}
}
@@ -0,0 +1,98 @@
<?php declare(strict_types = 1);
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Subscribers\SubscribersEmailCountsController;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class SubscribersEmailCount extends SimpleWorker {
const TASK_TYPE = 'subscribers_email_count';
const BATCH_SIZE = 1000;
const SUPPORT_MULTIPLE_INSTANCES = false;
/** @var SubscribersEmailCountsController */
private $subscribersEmailCountsController;
/** @var EntityManager */
private $entityManager;
/** @var SettingsController */
private $settings;
/** @var TrackingConfig */
private $trackingConfig;
public function __construct(
SubscribersEmailCountsController $subscribersEmailCountsController,
EntityManager $entityManager,
SettingsController $settings,
TrackingConfig $trackingConfig
) {
$this->subscribersEmailCountsController = $subscribersEmailCountsController;
$this->entityManager = $entityManager;
$this->settings = $settings;
$this->trackingConfig = $trackingConfig;
parent::__construct();
}
public function checkProcessingRequirements() {
if (!$this->trackingConfig->isEmailTrackingEnabled()) {
return false;
}
$daysToInactive = (int)$this->settings->get('deactivate_subscriber_after_inactive_days');
if ($daysToInactive === 0) {
return false;
}
return true;
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$previousTask = $this->findPreviousTask($task);
$dateFromLastRun = null;
if ($previousTask instanceof ScheduledTaskEntity) {
$dateFromLastRun = $previousTask->getScheduledAt();
}
$meta = $task->getMeta();
$lastSubscriberId = isset($meta['last_subscriber_id']) ? (int)$meta['last_subscriber_id'] : 0;
$highestSubscriberId = isset($meta['highest_subscriber_id']) ? (int)$meta['highest_subscriber_id'] : $this->getHighestSubscriberId();
$meta['highest_subscriber_id'] = $highestSubscriberId;
$task->setMeta($meta);
while ($lastSubscriberId <= $highestSubscriberId) {
[$count, $lastSubscriberId] = $this->subscribersEmailCountsController->updateSubscribersEmailCounts($dateFromLastRun, self::BATCH_SIZE, intval($lastSubscriberId));
if ($count === 0) {
break;
}
$meta['last_subscriber_id'] = $lastSubscriberId++;
$task->setMeta($meta);
$this->scheduledTasksRepository->persist($task);
$this->scheduledTasksRepository->flush();
$this->cronHelper->enforceExecutionLimit($timer);
};
$this->schedule();
return true;
}
private function findPreviousTask(ScheduledTaskEntity $task): ?ScheduledTaskEntity {
return $this->scheduledTasksRepository->findPreviousTask($task);
}
private function getHighestSubscriberId(): int {
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
$result = $this->entityManager->getConnection()->executeQuery("SELECT MAX(id) FROM $subscribersTable LIMIT 1;")->fetchNumeric();
/** @var int[] $result - it's required for PHPStan */
return is_array($result) && isset($result[0]) ? (int)$result[0] : 0;
}
}
@@ -0,0 +1,79 @@
<?php declare(strict_types = 1);
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Statistics\StatisticsOpensRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoetVendor\Carbon\Carbon;
class SubscribersEngagementScore extends SimpleWorker {
const AUTOMATIC_SCHEDULING = true;
const BATCH_SIZE = 60;
const TASK_TYPE = 'subscribers_engagement_score';
/** @var SegmentsRepository */
private $segmentsRepository;
/** @var StatisticsOpensRepository */
private $statisticsOpensRepository;
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
SegmentsRepository $segmentsRepository,
StatisticsOpensRepository $statisticsOpensRepository,
SubscribersRepository $subscribersRepository
) {
parent::__construct();
$this->segmentsRepository = $segmentsRepository;
$this->statisticsOpensRepository = $statisticsOpensRepository;
$this->subscribersRepository = $subscribersRepository;
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$recalculatedSubscribersCount = $this->recalculateSubscribers();
if ($recalculatedSubscribersCount > 0) {
$this->scheduleImmediately();
return true;
}
$recalculatedSegmentsCount = $this->recalculateSegments();
if ($recalculatedSegmentsCount > 0) {
$this->scheduleImmediately();
return true;
}
$this->schedule();
return true;
}
private function recalculateSubscribers(): int {
$subscribers = $this->subscribersRepository->findByUpdatedScoreNotInLastMonth(self::BATCH_SIZE);
foreach ($subscribers as $subscriber) {
$this->statisticsOpensRepository->recalculateSubscriberScore($subscriber);
}
return count($subscribers);
}
private function recalculateSegments(): int {
$segments = $this->segmentsRepository->findByUpdatedScoreNotInLastDay(self::BATCH_SIZE);
foreach ($segments as $segment) {
$this->statisticsOpensRepository->recalculateSegmentScore($segment);
}
return count($segments);
}
public function getNextRunDate() {
// random day of the next week
$date = Carbon::now()->millisecond(0);
$date->addDay();
$date->setTime(mt_rand(0, 23), mt_rand(0, 59));
return $date;
}
}
@@ -0,0 +1,67 @@
<?php declare(strict_types = 1);
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\StatisticsClickEntity;
use MailPoet\Entities\StatisticsOpenEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class SubscribersLastEngagement extends SimpleWorker {
const AUTOMATIC_SCHEDULING = false;
const SUPPORT_MULTIPLE_INSTANCES = false;
const BATCH_SIZE = 2000;
const TASK_TYPE = 'subscribers_last_engagement';
/** @var EntityManager */
private $entityManager;
public function __construct(
EntityManager $entityManager
) {
parent::__construct();
$this->entityManager = $entityManager;
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer): bool {
$meta = $task->getMeta();
$minId = $meta['nextId'] ?? 1;
$highestId = $this->getHighestSubscriberId();
while ($minId <= $highestId) {
$maxId = $minId + self::BATCH_SIZE;
$this->processBatch($minId, $maxId);
$task->setMeta(['nextId' => $maxId]);
$this->scheduledTasksRepository->persist($task);
$this->scheduledTasksRepository->flush();
$this->cronHelper->enforceExecutionLimit($timer); // Throws exception and interrupts process if over execution limit
$minId = $maxId;
}
return true;
}
private function processBatch(int $minSubscriberId, int $maxSubscriberId): void {
$statisticsClicksTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName();
$statisticsOpensTable = $this->entityManager->getClassMetadata(StatisticsOpenEntity::class)->getTableName();
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
$query = "
UPDATE $subscribersTable as mps
LEFT JOIN (SELECT max(created_at) as created_at, subscriber_id FROM $statisticsOpensTable as mpsoinner GROUP BY mpsoinner.subscriber_id) as mpso ON mpso.subscriber_id = mps.id
LEFT JOIN (SELECT max(created_at) as created_at, subscriber_id FROM $statisticsClicksTable as mpscinner GROUP BY mpscinner.subscriber_id) as mpsc ON mpsc.subscriber_id = mps.id
SET mps.last_engagement_at = NULLIF(GREATEST(COALESCE(mpso.created_at, '0'), COALESCE(mpsc.created_at, '0')), '0')
WHERE mps.last_engagement_at IS NULL AND mps.id >= $minSubscriberId AND mps.id < $maxSubscriberId;
";
$this->entityManager->getConnection()->executeStatement($query);
}
private function getHighestSubscriberId(): int {
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
$result = $this->entityManager->getConnection()->executeQuery("SELECT MAX(id) FROM $subscribersTable LIMIT 1;")->fetchNumeric();
return is_array($result) && isset($result[0]) ? (int)$result[0] : 0;
}
}
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\ServicesChecker;
use MailPoet\Cron\CronWorkerScheduler;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Services\SubscribersCountReporter;
use MailPoetVendor\Carbon\Carbon;
class SubscribersStatsReport extends SimpleWorker {
const TASK_TYPE = 'subscribers_stats_report';
/** @var SubscribersCountReporter */
private $subscribersCountReporter;
/** @var ServicesChecker */
private $serviceChecker;
/** @var CronWorkerScheduler */
private $workerScheduler;
public function __construct(
SubscribersCountReporter $subscribersCountReporter,
ServicesChecker $servicesChecker,
CronWorkerScheduler $workerScheduler
) {
parent::__construct();
$this->subscribersCountReporter = $subscribersCountReporter;
$this->serviceChecker = $servicesChecker;
$this->workerScheduler = $workerScheduler;
}
public function checkProcessingRequirements() {
return (bool)$this->serviceChecker->getValidAccountKey();
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer): bool {
$key = $this->serviceChecker->getValidAccountKey();
if ($key === null) {
return false;
}
$result = $this->subscribersCountReporter->report($key);
// We have a valid key, but request failed
if ($result === false) {
$this->workerScheduler->rescheduleProgressively($task);
}
return $result;
}
public function getNextRunDate() {
$date = Carbon::now()->millisecond(0);
// Spread the check within 6 hours after midnight so that all plugins don't ping the service at the same time
return $date->startOfDay()
->addDay()
->addHours(rand(0, 5))
->addMinutes(rand(0, 59))
->addSeconds(rand(0, 59));
}
}
@@ -0,0 +1,99 @@
<?php declare(strict_types = 1);
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\InvalidStateException;
use MailPoet\Util\Security;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class UnsubscribeTokens extends SimpleWorker {
const TASK_TYPE = 'unsubscribe_tokens';
const BATCH_SIZE = 1000;
const AUTOMATIC_SCHEDULING = false;
/** @var Security */
private $security;
/** @var EntityManager */
private $entityManager;
public function __construct(
Security $security,
EntityManager $entityManager
) {
parent::__construct();
$this->security = $security;
$this->entityManager = $entityManager;
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$meta = $task->getMeta();
if (!isset($meta['last_subscriber_id'])) {
$meta['last_subscriber_id'] = 0;
}
if (!isset($meta['last_newsletter_id'])) {
$meta['last_newsletter_id'] = 0;
}
do {
$this->cronHelper->enforceExecutionLimit($timer);
$subscribersCount = $this->addTokens(SubscriberEntity::class, $meta['last_subscriber_id']);
$task->setMeta($meta);
$this->scheduledTasksRepository->persist($task);
$this->scheduledTasksRepository->flush();
} while ($subscribersCount === self::BATCH_SIZE);
do {
$this->cronHelper->enforceExecutionLimit($timer);
$newslettersCount = $this->addTokens(NewsletterEntity::class, $meta['last_newsletter_id']);
$task->setMeta($meta);
$this->scheduledTasksRepository->persist($task);
$this->scheduledTasksRepository->flush();
} while ($newslettersCount === self::BATCH_SIZE);
if ($subscribersCount > 0 || $newslettersCount > 0) {
return false;
}
return true;
}
private function addTokens($entityClass, &$lastProcessedId = 0) {
$queryBuilder = $this->entityManager->createQueryBuilder();
$entities = $queryBuilder
->select('PARTIAL e.{id}')
->from($entityClass, 'e')
->where('e.unsubscribeToken IS NULL')
->andWhere('e.id > :lastProcessedId')
->orderBy('e.id', 'ASC')
->setMaxResults(self::BATCH_SIZE)
->setParameter('lastProcessedId', $lastProcessedId)
->getQuery()
->getResult();
if (!is_iterable($entities) || !is_countable($entities)) {
throw new InvalidStateException('Entities must be iterable');
}
foreach ($entities as $entity) {
$lastProcessedId = $entity->getId();
$entity->setUnsubscribeToken($this->security->generateUnsubscribeTokenByEntity($entity));
$this->entityManager->persist($entity);
}
$this->entityManager->flush();
return count($entities);
}
public function getNextRunDate() {
return Carbon::now()->millisecond(0);
}
}
@@ -0,0 +1,83 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Entities\StatisticsClickEntity;
use MailPoet\Statistics\StatisticsClicksRepository;
use MailPoet\Statistics\Track\WooCommercePurchases;
use MailPoet\WooCommerce\Helper as WCHelper;
use MailPoetVendor\Carbon\Carbon;
class WooCommercePastOrders extends SimpleWorker {
const TASK_TYPE = 'woocommerce_past_orders';
const BATCH_SIZE = 20;
/** @var WCHelper */
private $woocommerceHelper;
/** @var WooCommercePurchases */
private $woocommercePurchases;
/** @var StatisticsClicksRepository */
private $statisticsClicksRepository;
public function __construct(
WCHelper $woocommerceHelper,
StatisticsClicksRepository $statisticsClicksRepository,
WooCommercePurchases $woocommercePurchases
) {
$this->woocommerceHelper = $woocommerceHelper;
$this->woocommercePurchases = $woocommercePurchases;
$this->statisticsClicksRepository = $statisticsClicksRepository;
parent::__construct();
}
public function checkProcessingRequirements() {
return $this->woocommerceHelper->isWooCommerceActive() && empty($this->getCompletedTasks()); // run only once
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$oldestClick = $this->statisticsClicksRepository->findOneBy([], ['createdAt' => 'asc']);
if (!$oldestClick instanceof StatisticsClickEntity) {
return true;
}
// continue from 'last_processed_id' from previous run
$meta = $task->getMeta();
$lastId = isset($meta['last_processed_id']) ? $meta['last_processed_id'] : 0;
add_filter('posts_where', function ($where = '') use ($lastId) {
global $wpdb;
return $where . " AND {$wpdb->prefix}posts.ID > " . $lastId;
}, 10, 1);
$orderIds = $this->woocommerceHelper->wcGetOrders([
'date_completed' => '>=' . (($createdAt = $oldestClick->getCreatedAt()) ? $createdAt->format('Y-m-d H:i:s') : null),
'orderby' => 'ID',
'order' => 'ASC',
'limit' => self::BATCH_SIZE,
'return' => 'ids',
]);
if (empty($orderIds)) {
return true;
}
foreach ($orderIds as $orderId) {
$this->woocommercePurchases->trackPurchase($orderId, false);
}
$task->setMeta(['last_processed_id' => end($orderIds)]);
$this->scheduledTasksRepository->persist($task);
$this->scheduledTasksRepository->flush();
return false;
}
public function getNextRunDate() {
return Carbon::now()->millisecond(0); // schedule immediately
}
}
@@ -0,0 +1,74 @@
<?php declare(strict_types = 1);
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\ScheduledTaskEntity;
use MailPoet\Segments\WooCommerce as WooCommerceSegment;
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
class WooCommerceSync extends SimpleWorker {
const TASK_TYPE = 'woocommerce_sync';
const SUPPORT_MULTIPLE_INSTANCES = false;
const AUTOMATIC_SCHEDULING = false;
const BATCH_SIZE = 1000;
/** @var WooCommerceSegment */
private $woocommerceSegment;
/** @var WooCommerceHelper */
private $woocommerceHelper;
public function __construct(
WooCommerceSegment $woocommerceSegment,
WooCommerceHelper $woocommerceHelper
) {
$this->woocommerceSegment = $woocommerceSegment;
$this->woocommerceHelper = $woocommerceHelper;
parent::__construct();
}
public function checkProcessingRequirements() {
return $this->woocommerceHelper->isWooCommerceActive();
}
public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
$meta = $task->getMeta();
$highestOrderId = $this->getHighestOrderId();
if (!isset($meta['last_checked_order_id'])) {
$meta['last_checked_order_id'] = 0;
}
do {
$this->cronHelper->enforceExecutionLimit($timer);
$meta['last_checked_order_id'] = $this->woocommerceSegment->synchronizeCustomers(
$meta['last_checked_order_id'],
$highestOrderId,
self::BATCH_SIZE
);
$task->setMeta($meta);
$this->scheduledTasksRepository->persist($task);
$this->scheduledTasksRepository->flush();
} while ($meta['last_checked_order_id'] < $highestOrderId);
return true;
}
private function getHighestOrderId(): int {
$orders = $this->woocommerceHelper->wcGetOrders(
[
'status' => 'all',
'type' => 'shop_order',
'limit' => 1,
'orderby' => 'ID',
'order' => 'DESC',
'return' => 'ids',
]
);
return (!empty($orders)) ? $orders[0] : 0;
}
}
@@ -0,0 +1,167 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Cron\Workers;
if (!defined('ABSPATH')) exit;
use MailPoet\Cron\Workers\Automations\AbandonedCartWorker;
use MailPoet\Cron\Workers\Bounce as BounceWorker;
use MailPoet\Cron\Workers\KeyCheck\PremiumKeyCheck as PremiumKeyCheckWorker;
use MailPoet\Cron\Workers\KeyCheck\SendingServiceKeyCheck as SendingServiceKeyCheckWorker;
use MailPoet\Cron\Workers\Scheduler as SchedulerWorker;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Cron\Workers\StatsNotifications\AutomatedEmails as StatsNotificationsWorkerForAutomatedEmails;
use MailPoet\Cron\Workers\StatsNotifications\Worker as StatsNotificationsWorker;
use MailPoet\Cron\Workers\WooCommerceSync as WooCommerceSyncWorker;
use MailPoet\DI\ContainerWrapper;
class WorkersFactory {
public const SIMPLE_WORKER_TYPES = [
SubscribersCountCacheRecalculation::TASK_TYPE,
NewsletterTemplateThumbnails::TASK_TYPE,
ReEngagementEmailsScheduler::TASK_TYPE,
SubscribersLastEngagement::TASK_TYPE,
SubscribersEngagementScore::TASK_TYPE,
WooCommercePastOrders::TASK_TYPE,
AuthorizedSendingEmailsCheck::TASK_TYPE,
WooCommerceSyncWorker::TASK_TYPE,
SubscriberLinkTokens::TASK_TYPE,
UnsubscribeTokens::TASK_TYPE,
InactiveSubscribers::TASK_TYPE,
SubscribersEmailCount::TASK_TYPE,
StatsNotificationsWorkerForAutomatedEmails::TASK_TYPE,
StatsNotificationsWorker::TASK_TYPE,
BackfillEngagementData::TASK_TYPE,
Mixpanel::TASK_TYPE,
AbandonedCartWorker::TASK_TYPE,
];
/** @var ContainerWrapper */
private $container;
public function __construct(
ContainerWrapper $container
) {
$this->container = $container;
}
/** @return SchedulerWorker */
public function createScheduleWorker() {
return $this->container->get(SchedulerWorker::class);
}
/** @return SendingQueueWorker */
public function createQueueWorker() {
return $this->container->get(SendingQueueWorker::class);
}
/** @return StatsNotificationsWorker */
public function createStatsNotificationsWorker() {
return $this->container->get(StatsNotificationsWorker::class);
}
/** @return StatsNotificationsWorkerForAutomatedEmails */
public function createStatsNotificationsWorkerForAutomatedEmails() {
return $this->container->get(StatsNotificationsWorkerForAutomatedEmails::class);
}
/** @return SendingServiceKeyCheckWorker */
public function createSendingServiceKeyCheckWorker() {
return $this->container->get(SendingServiceKeyCheckWorker::class);
}
/** @return PremiumKeyCheckWorker */
public function createPremiumKeyCheckWorker() {
return $this->container->get(PremiumKeyCheckWorker::class);
}
/** @return BounceWorker */
public function createBounceWorker() {
return $this->container->get(BounceWorker::class);
}
/** @return WooCommerceSyncWorker */
public function createWooCommerceSyncWorker() {
return $this->container->get(WooCommerceSyncWorker::class);
}
/** @return ExportFilesCleanup */
public function createExportFilesCleanupWorker() {
return $this->container->get(ExportFilesCleanup::class);
}
/** @return InactiveSubscribers */
public function createInactiveSubscribersWorker() {
return $this->container->get(InactiveSubscribers::class);
}
/** @return UnsubscribeTokens */
public function createUnsubscribeTokensWorker() {
return $this->container->get(UnsubscribeTokens::class);
}
/** @return SubscriberLinkTokens */
public function createSubscriberLinkTokensWorker() {
return $this->container->get(SubscriberLinkTokens::class);
}
/** @return SubscribersEngagementScore */
public function createSubscribersEngagementScoreWorker() {
return $this->container->get(SubscribersEngagementScore::class);
}
/** @return SubscribersLastEngagement */
public function createSubscribersLastEngagementWorker() {
return $this->container->get(SubscribersLastEngagement::class);
}
/** @return AuthorizedSendingEmailsCheck */
public function createAuthorizedSendingEmailsCheckWorker() {
return $this->container->get(AuthorizedSendingEmailsCheck::class);
}
/** @return WooCommercePastOrders */
public function createWooCommercePastOrdersWorker() {
return $this->container->get(WooCommercePastOrders::class);
}
/** @return SubscribersCountCacheRecalculation */
public function createSubscribersCountCacheRecalculationWorker() {
return $this->container->get(SubscribersCountCacheRecalculation::class);
}
/** @return ReEngagementEmailsScheduler */
public function createReEngagementEmailsSchedulerWorker() {
return $this->container->get(ReEngagementEmailsScheduler::class);
}
/** @return SubscribersStatsReport */
public function createSubscribersStatsReportWorker() {
return $this->container->get(SubscribersStatsReport::class);
}
/** @return NewsletterTemplateThumbnails */
public function createNewsletterTemplateThumbnailsWorker() {
return $this->container->get(NewsletterTemplateThumbnails::class);
}
/** @return SubscribersEmailCount */
public function createSubscribersEmailCountsWorker() {
return $this->container->get(SubscribersEmailCount::class);
}
/** @return AbandonedCartWorker */
public function createAbandonedCartWorker() {
return $this->container->get(AbandonedCartWorker::class);
}
/** @return BackfillEngagementData */
public function createBackfillEngagementDataWorker() {
return $this->container->get(BackfillEngagementData::class);
}
public function createMixpanelWorker() {
return $this->container->get(Mixpanel::class);
}
}
@@ -0,0 +1 @@
<?php