init
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -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('Can‘t pre-process newsletter without queue.');
|
||||
}
|
||||
if ($queue->getNewsletterRenderedBody() !== null) {
|
||||
return $newsletter;
|
||||
}
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info(
|
||||
'pre-processing newsletter',
|
||||
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()]
|
||||
);
|
||||
|
||||
$campaignId = null;
|
||||
|
||||
// if tracking is enabled, do additional processing
|
||||
if ($this->trackingEnabled) {
|
||||
// hook to the newsletter post-processing filter and add tracking image
|
||||
$this->trackingImageInserted = OpenTracking::addTrackingImage();
|
||||
// render newsletter
|
||||
$renderedNewsletter = $this->renderer->render($newsletter, $queue);
|
||||
$renderedNewsletter = $this->wp->applyFilters(
|
||||
'mailpoet_sending_newsletter_render_after_pre_process',
|
||||
$renderedNewsletter,
|
||||
$newsletter
|
||||
);
|
||||
if (is_array($renderedNewsletter)) {
|
||||
$campaignId = $this->calculateCampaignId($newsletter, $renderedNewsletter);
|
||||
}
|
||||
$renderedNewsletter = $this->gaTracking->applyGATracking($renderedNewsletter, $newsletter);
|
||||
// hash and save all links
|
||||
$renderedNewsletter = $this->linksTask->process($renderedNewsletter, $newsletter, $queue);
|
||||
} else {
|
||||
// render newsletter
|
||||
$renderedNewsletter = $this->renderer->render($newsletter, $queue);
|
||||
$renderedNewsletter = $this->wp->applyFilters(
|
||||
'mailpoet_sending_newsletter_render_after_pre_process',
|
||||
$renderedNewsletter,
|
||||
$newsletter
|
||||
);
|
||||
if (is_array($renderedNewsletter)) {
|
||||
$campaignId = $this->calculateCampaignId($newsletter, $renderedNewsletter);
|
||||
}
|
||||
$renderedNewsletter = $this->gaTracking->applyGATracking($renderedNewsletter, $newsletter);
|
||||
}
|
||||
|
||||
// check if this is a post notification and if it contains at least 1 ALC post
|
||||
if (
|
||||
$newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION_HISTORY &&
|
||||
$this->postsTask->getAlcPostsCount($renderedNewsletter, $newsletter) === 0
|
||||
) {
|
||||
// delete notification history record since it will never be sent
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info(
|
||||
'no posts in post notification, deleting it',
|
||||
['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()]
|
||||
);
|
||||
$this->newsletterDeleteController->bulkDelete([(int)$newsletter->getId()]);
|
||||
return false;
|
||||
}
|
||||
// extract and save newsletter posts
|
||||
$this->postsTask->extractAndSave($renderedNewsletter, $newsletter);
|
||||
|
||||
if ($campaignId !== null) {
|
||||
$this->sendingQueuesRepository->saveCampaignId($queue, $campaignId);
|
||||
}
|
||||
|
||||
$filterSegmentId = $newsletter->getFilterSegmentId();
|
||||
if ($filterSegmentId) {
|
||||
$filterSegment = $this->segmentsRepository->findOneById($filterSegmentId);
|
||||
if ($filterSegment instanceof SegmentEntity && $filterSegment->getType() === SegmentEntity::TYPE_DYNAMIC) {
|
||||
$this->sendingQueuesRepository->saveFilterSegmentMeta($queue, $filterSegment);
|
||||
}
|
||||
}
|
||||
|
||||
// update queue with the rendered and pre-processed newsletter
|
||||
$queue->setNewsletterRenderedSubject(
|
||||
ShortcodesTask::process(
|
||||
$newsletter->getSubject(),
|
||||
$renderedNewsletter['html'],
|
||||
$newsletter,
|
||||
null,
|
||||
$queue
|
||||
)
|
||||
);
|
||||
|
||||
// if the rendered subject is empty, use a default subject,
|
||||
// having no subject in a newsletter is considered spammy
|
||||
if (empty(trim((string)$queue->getNewsletterRenderedSubject()))) {
|
||||
$queue->setNewsletterRenderedSubject(__('No subject', 'mailpoet'));
|
||||
}
|
||||
$renderedNewsletter = $this->emoji->encodeEmojisInBody($renderedNewsletter);
|
||||
$queue->setNewsletterRenderedBody($renderedNewsletter);
|
||||
|
||||
try {
|
||||
$this->sendingQueuesRepository->flush();
|
||||
} catch (\Throwable $e) {
|
||||
$this->stopNewsletterPreProcessing(sprintf('QUEUE-%d-SAVE', $queue->getId()));
|
||||
}
|
||||
return $newsletter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcodes and links will be replaced in the subject, html and text body
|
||||
* to speed the processing, join content into a continuous string.
|
||||
*/
|
||||
public function prepareNewsletterForSending(NewsletterEntity $newsletter, SubscriberEntity $subscriber, SendingQueueEntity $queue): array {
|
||||
$renderedNewsletter = $queue->getNewsletterRenderedBody();
|
||||
$renderedNewsletter = $this->emoji->decodeEmojisInBody($renderedNewsletter);
|
||||
$preparedNewsletter = Helpers::joinObject(
|
||||
[
|
||||
$queue->getNewsletterRenderedSubject(),
|
||||
$renderedNewsletter['html'],
|
||||
$renderedNewsletter['text'],
|
||||
]
|
||||
);
|
||||
|
||||
$preparedNewsletter = ShortcodesTask::process(
|
||||
$preparedNewsletter,
|
||||
null,
|
||||
$newsletter,
|
||||
$subscriber,
|
||||
$queue
|
||||
);
|
||||
if ($this->trackingEnabled) {
|
||||
$preparedNewsletter = $this->newsletterLinks->replaceSubscriberData(
|
||||
$subscriber->getId(),
|
||||
$queue->getId(),
|
||||
$preparedNewsletter
|
||||
);
|
||||
}
|
||||
$preparedNewsletter = Helpers::splitObject($preparedNewsletter);
|
||||
if ($newsletter->getWpPostId() !== null) {
|
||||
$this->personalizer->set_context([
|
||||
'recipient_email' => $subscriber->getEmail(),
|
||||
'newsletter_id' => $newsletter->getId(),
|
||||
'queue_id' => $queue->getId(),
|
||||
]);
|
||||
foreach ($preparedNewsletter as $key => $content) {
|
||||
$preparedNewsletter[$key] = $this->personalizer->personalize_content($content);
|
||||
}
|
||||
}
|
||||
return [
|
||||
'id' => $newsletter->getId(),
|
||||
'subject' => $preparedNewsletter[0],
|
||||
'body' => [
|
||||
'html' => $preparedNewsletter[1],
|
||||
'text' => $preparedNewsletter[2],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function markNewsletterAsSent(NewsletterEntity $newsletter) {
|
||||
// if it's a standard or notification history newsletter, update its status
|
||||
if (
|
||||
$newsletter->getType() === NewsletterEntity::TYPE_STANDARD ||
|
||||
$newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION_HISTORY
|
||||
) {
|
||||
$newsletter->setStatus(NewsletterEntity::STATUS_SENT);
|
||||
$newsletter->setSentAt(Carbon::now()->millisecond(0));
|
||||
$this->newslettersRepository->persist($newsletter);
|
||||
$this->newslettersRepository->flush();
|
||||
}
|
||||
}
|
||||
|
||||
public function stopNewsletterPreProcessing($errorCode = null) {
|
||||
MailerLog::processError(
|
||||
'queue_save',
|
||||
__('There was an error processing your newsletter during sending. If possible, please contact us and report this issue.', 'mailpoet'),
|
||||
$errorCode
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NewsletterEntity $newsletter
|
||||
* @param array $renderedNewsletters - The pre-processed renderered newsletters, before link tracking has been added or shortcodes have been processed.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function calculateCampaignId(NewsletterEntity $newsletter, array $renderedNewsletters): string {
|
||||
$relevantContent = [
|
||||
$newsletter->getId(),
|
||||
$newsletter->getSubject(),
|
||||
];
|
||||
|
||||
if (isset($renderedNewsletters['text'])) {
|
||||
$relevantContent[] = $renderedNewsletters['text'];
|
||||
}
|
||||
|
||||
// The text version of emails contains just the alt text of images, which could be the same for multiple images. In order to ensure
|
||||
// campaign IDs change when images change, we should consider all image URLs.
|
||||
if (isset($renderedNewsletters['html'])) {
|
||||
$html = pQuery::parseStr($renderedNewsletters['html']);
|
||||
if ($html instanceof DomNode) {
|
||||
foreach ($html->query('img') as $imageNode) {
|
||||
$src = $imageNode->getAttribute('src');
|
||||
if (is_string($src)) {
|
||||
$relevantContent[] = $src;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return substr(md5(implode('|', $relevantContent)), 0, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method recovers the scheduled task and newsletter from a state when sending cannot proceed.
|
||||
*/
|
||||
private function recoverFromInvalidState(ScheduledTaskEntity $task): void {
|
||||
// When newsletter does not exist, we need to remove the scheduled task and sending queue.
|
||||
$queue = $task->getSendingQueue();
|
||||
$newsletter = $queue ? $queue->getNewsletter() : null;
|
||||
if (!$newsletter) {
|
||||
$this->scheduledTasksRepository->remove($task);
|
||||
if ($queue) {
|
||||
$this->sendingQueuesRepository->remove($queue);
|
||||
}
|
||||
$this->sendingQueuesRepository->flush();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only deleted newsletter or newsletter with unexpected state should pass here.
|
||||
// Because this state cannot proceed with sending, we need to pause the scheduled task.
|
||||
$task->setStatus(ScheduledTaskEntity::STATUS_PAUSED);
|
||||
$this->scheduledTasksRepository->flush();
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
+62
@@ -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;
|
||||
}
|
||||
}
|
||||
+87
@@ -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 @@
|
||||
<?php
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user