externalSendingActivated]);
$callType = acym_getVar('string', 'type', '');
$this->startQueue = acym_getVar('int', 'startqueue', 0);
$this->externalSendingRepeat = !empty(acym_getVar('int', 'external_sending_repeat', 0));
if ($callType === 'sending' || !empty($this->startQueue) || !empty($this->externalSendingRepeat)) {
$this->skip = array_diff(self::ALL_STEPS, [self::STEP_SEND]);
$this->isSendingCall = true;
}
}
public function addSkipFromString(string $skipVar): void
{
if (empty($skipVar)) {
return;
}
$skipVar = explode(',', $skipVar);
if (empty($skipVar)) {
return;
}
$this->skip = array_unique(array_merge($this->skip, $skipVar));
}
public function cron(): void
{
$this->handleUnlinkLicenseCalls();
$this->triggeredMessage();
if (!$this->checkCronFrequency()) {
return;
}
$this->queueScheduledCampaigns();
$this->cleanQueue();
$this->handleQueue();
$this->handleBounceMessages();
$this->handleAutomations();
$this->handleAutomaticCampaigns();
$this->handleSpecificEmails();
$this->handleFollowups();
$this->handleMailboxActions();
$this->cleanData();
$this->handleABTestCampaigns();
$this->handleScenario();
$this->continueCronCall();
$this->handleCronReport();
}
public function saveReport(array $messages = [], array $detailMessages = []): void
{
$saveReport = $this->config->get('cron_savereport');
$reportPath = $this->config->get('cron_savepath');
if (empty($saveReport) || empty($reportPath)) {
return;
}
if (!(($saveReport == 2 && $this->processed) || $saveReport == 1 || ($saveReport == 3 && $this->errorDetected))) {
return;
}
if (!empty($messages)) {
$this->messages = $messages;
}
if (!empty($detailMessages)) {
$this->detailMessages = $detailMessages;
}
// Prepare the cron file path
$reportPath = str_replace(['{year}', '{month}'], [gmdate('Y'), gmdate('m')], $reportPath);
$reportPath = acym_cleanPath(ACYM_ROOT.trim(html_entity_decode($reportPath)));
acym_createDir(dirname($reportPath), true, true);
$lr = "\r\n";
// Catch warnings
ob_start();
file_put_contents(
$reportPath,
$lr.$lr.'******************** '.acym_getDate(time()).' UTC ********************'.$lr.implode($lr, $this->messages),
FILE_APPEND
);
if ($saveReport == 2 && !empty($this->detailMessages)) {
file_put_contents(
$reportPath,
$lr.'---- Details ----'.$lr.implode($lr, $this->detailMessages),
FILE_APPEND
);
}
$potentialWarnings = ob_get_clean();
if (!empty($potentialWarnings)) {
$this->messages[] = $potentialWarnings;
}
}
/**
* Sets the types of emails that will be sent by the cron
*/
public function setEmailTypes(array $emailTypes): void
{
$this->emailTypes = $emailTypes;
}
public function addMessage(string $message): void
{
$this->messages[] = $message;
}
public function handleCronReport(): void
{
$sendReport = (int)$this->config->get('cron_sendreport');
if (
($sendReport === self::SEND_REPORT_ONLY_ON_ACTION && $this->processed)
|| $sendReport === self::SEND_REPORT_EACH_TIME
|| ($sendReport === self::SEND_REPORT_ONLY_ON_ERROR && $this->errorDetected)
) {
$mailerHelper = new MailerHelper();
$mailerHelper->report = false;
$mailerHelper->autoAddUser = true;
$mailerHelper->addParam('report', implode('
', $this->messages));
$mailerHelper->addParam('mainreport', $this->mainMessage);
$mailerHelper->addParam('detailreport', implode('
', $this->detailMessages));
$receiverString = $this->config->get('cron_sendto');
$receivers = [];
if (substr_count($receiverString, '@') > 1) {
$receivers = explode(' ', trim(preg_replace('# +#', ' ', str_replace([';', ','], ' ', $receiverString))));
} else {
$receivers[] = trim($receiverString);
}
if (!empty($receivers)) {
$mailClass = new MailClass();
foreach ($receivers as $oneReceiver) {
if (empty($oneReceiver)) {
continue;
}
try {
$reportEmail = $mailClass->getOneByName('acy_report');
if (!empty($reportEmail)) {
$mailerHelper->sendOne($reportEmail->id, $oneReceiver);
}
} catch (\Exception $e) {
acym_logError('Error while sending the cron report to '.$oneReceiver.' : '.$e->getMessage());
}
}
}
}
$this->saveReport();
$newConfig = [
'cron_report' => implode("\n", $this->messages),
];
if (strlen($newConfig['cron_report']) > 800) {
$newConfig['cron_report'] = substr($newConfig['cron_report'], 0, 795).'...';
}
$this->config->saveConfig($newConfig);
}
/**
* Call made by the API to remove the API key when the user unlinks the website from their account page on our website
*/
private function handleUnlinkLicenseCalls(): void
{
if (acym_getVar('int', 'unlink', 0) !== 1) {
return;
}
$callLicenseKey = acym_getVar('string', 'licenseKey');
$configLicenseKey = $this->config->get('license_key');
if (!empty($configLicenseKey) && $configLicenseKey === $callLicenseKey) {
$this->config->saveConfig(['license_key' => '', 'active_cron' => 0]);
}
exit;
}
/**
* Displays the message shown on the cron URL page
*/
private function triggeredMessage(): void
{
$firstMessage = acym_translationSprintf('ACYM_CRON_TRIGGERED', acym_date('now', 'd F Y H:i'));
$this->messages[] = $firstMessage;
acym_display($firstMessage, 'info');
}
/**
* Checks the cron frequency in the configuration to make sure the call is wanted
*/
private function checkCronFrequency(): bool
{
if ($this->isSendingCall) {
return true;
}
$time = time();
$nextCronTime = $this->config->get('cron_next', 0);
$cronFrequency = $this->config->get('cron_frequency', 900);
if ($nextCronTime > $time) {
if ($nextCronTime > ($time + $cronFrequency)) {
// The next cron time is too far in the future, should not happen but we'll handle the case, so we reset the next cron time
$this->config->saveConfig(['cron_next' => $time + $cronFrequency]);
}
$notTimeMessage = acym_translationSprintf('ACYM_CRON_NEXT', acym_date($this->config->get('cron_next'), 'd F Y H:i'));
$this->messages[] = $notTimeMessage;
// We don't need to trigger anything, it's not time yet
acym_display($notTimeMessage, 'info');
return false;
}
// We update the next cron and the last cron dates
$newConfig = [
'cron_last' => $time,
'cron_fromip' => acym_getIP(true),
'cron_next' => $nextCronTime + $cronFrequency,
];
if ($newConfig['cron_next'] <= $time || $newConfig['cron_next'] > $time + $cronFrequency) {
$newConfig['cron_next'] = $time + $cronFrequency;
}
$this->config->saveConfig($newConfig);
return true;
}
/**
* Adds the scheduled emails to the queue when the date is reached
*/
private function queueScheduledCampaigns(): void
{
if (in_array(self::STEP_SCHEDULE, $this->skip)) {
return;
}
$queueClass = new QueueClass();
$nbScheduled = $queueClass->scheduleReady();
if (!empty($nbScheduled)) {
$this->messages[] = acym_translationSprintf('ACYM_NB_SCHEDULED', $nbScheduled);
$this->detailMessages = array_merge($this->detailMessages, $queueClass->messages);
$this->processed = true;
}
}
/**
* Cleans the emails that are stuck in the queue for too long (disabled/unconfirmed recipients)
*/
private function cleanQueue(): void
{
if (in_array(self::STEP_CLEAN_QUEUE, $this->skip)) {
return;
}
$queueClass = new QueueClass();
$deletedNb = $queueClass->cleanQueue();
if (!empty($deletedNb)) {
$this->messages[] = acym_translationSprintf('ACYM_EMAILS_REMOVED_QUEUE_CLEAN', $deletedNb);
$this->processed = true;
}
}
private function handleQueue(): void
{
if (in_array(self::STEP_SEND, $this->skip) || !$this->canSendQueuedEmails()) {
return;
}
if (!$this->isSendingCall && acym_level(ACYM_ENTERPRISE) && function_exists('curl_multi_exec')) {
$this->handleMultiCron();
}
if ($this->isSendingCall || $this->config->get('dedicated_send_process', 0) != 1) {
$this->sendQueuedEmails();
}
}
/**
* Makes sure the current process doesn't reach the server time limit
*/
private function checkTimeRemaining(): void
{
$time = time();
if (empty($this->cronTimeLimit) || $time < $this->cronTimeLimit) {
return;
}
$this->messages[] = acym_translation('ACYM_CRON_TIME_LIMIT_REACHED');
$this->cronTimeLimitReached = true;
}
/**
* If the configuration is set to send multiple batches of emails at the same time
*/
private function handleMultiCron(): void
{
$emailsPerBatches = $this->config->get('queue_nbmail_auto', 70);
if (empty($emailsPerBatches)) {
return;
}
$nbBatches = $this->config->get('queue_batch_auto', 1);
$nbBatches = intval($nbBatches);
if ($this->config->get('dedicated_send_process', 0) == 1) {
$startCron = 0;
} else {
if ($nbBatches < 2) {
return;
}
$startCron = 1;
}
$cronParams = '&t='.time();
if (!empty($this->config->get('cron_security', 0)) && !empty($this->config->get('cron_key'))) {
$cronParams .= '&cronKey='.$this->config->get('cron_key');
}
$urls = [];
for ($i = $startCron; $i < $nbBatches; $i++) {
$urls[] = acym_frontendLink('cron&task=cron&type=sending&startqueue='.($emailsPerBatches * $i).$cronParams);
}
acym_asyncUrlCalls($urls);
}
private function handleBounceMessages(): void
{
$this->checkTimeRemaining();
$time = time();
$autoBounceHandlingActive = (int)$this->config->get('auto_bounce', 0) === 1;
$autoBounceHandlingNextTime = (int)$this->config->get('auto_bounce_next', 0);
$autoBounceHandlingFrequency = (int)$this->config->get('auto_bounce_frequency', 0);
$isEnterprise = acym_level(ACYM_ENTERPRISE);
if (in_array(self::STEP_BOUNCE, $this->skip) || $this->cronTimeLimitReached || !$isEnterprise || !$autoBounceHandlingActive || $time < $autoBounceHandlingNextTime) {
return;
}
$this->config->saveConfig(
[
'auto_bounce_last' => $time,
'auto_bounce_next' => $time + $autoBounceHandlingFrequency,
]
);
$bounceHelper = new BounceHelper();
$bounceHelper->report = false;
$bounceHelper->stoptime = $this->cronTimeLimit;
$newConfig = [];
if ($bounceHelper->init() && $bounceHelper->connect()) {
$nbMessages = $bounceHelper->getNBMessages();
$nbMessagesReport = acym_translationSprintf('ACYM_NB_MAIL_MAILBOX', $nbMessages);
$this->messages[] = $nbMessagesReport;
$newConfig['auto_bounce_report'] = $nbMessagesReport;
$this->detailMessages[] = $nbMessagesReport;
if (!empty($nbMessages)) {
$bounceHelper->handleMessages();
$this->processed = true;
}
$this->detailMessages = array_merge($this->detailMessages, $bounceHelper->messages);
} else {
$bounceErrors = $bounceHelper->getErrors();
$newConfig['auto_bounce_report'] = implode('
', $bounceErrors);
//We add "bounce handling" just before the error so the user knows where it comes from...
if (!empty($bounceErrors[0])) {
$bounceErrors[0] = acym_translation('ACYM_BOUNCE_HANDLING').' : '.$bounceErrors[0];
}
$this->messages = array_merge($this->messages, $bounceErrors);
$this->processed = true;
$this->errorDetected = true;
}
$this->config->saveConfig($newConfig);
}
/**
* Triggers the automations based on time frequency
*/
private function handleAutomations(): void
{
$this->checkTimeRemaining();
if (in_array(self::STEP_AUTOMATION, $this->skip) || $this->cronTimeLimitReached || !acym_level(ACYM_ENTERPRISE)) {
return;
}
$automationClass = new AutomationClass();
$automationClass->trigger('classic');
$userStatusCheckTriggers = [];
acym_trigger('onAcymDefineUserStatusCheckTriggers', [&$userStatusCheckTriggers]);
$automationClass->trigger($userStatusCheckTriggers);
if (!empty($automationClass->report)) {
if ($automationClass->didAnAction) {
$this->processed = true;
}
$this->messages = array_merge($this->messages, $automationClass->report);
}
}
private function handleAutomaticCampaigns(): void
{
$this->checkTimeRemaining();
if (in_array(self::STEP_CAMPAIGN, $this->skip) || $this->cronTimeLimitReached || !acym_level(ACYM_ENTERPRISE)) {
return;
}
$campaignClass = new CampaignClass();
$campaignClass->triggerAutoCampaign();
if (!empty($campaignClass->messages)) {
$this->messages = array_merge($this->messages, $campaignClass->messages);
$this->processed = true;
}
}
/**
* Sends special emails such as birthday or WooCommerce reminders
*/
private function handleSpecificEmails(): void
{
$this->checkTimeRemaining();
if (in_array(self::STEP_SPECIFIC, $this->skip) || $this->cronTimeLimitReached) {
return;
}
$campaignClass = new CampaignClass();
$specialTypes = [];
acym_trigger('getCampaignTypes', [&$specialTypes]);
$specialMail = $campaignClass->getCampaignsByTypes($specialTypes, true);
$time = time();
acym_trigger('filterSpecificMailsToSend', [&$specialMail, $time]);
foreach ($specialMail as $onespecialMail) {
$campaignClass->send($onespecialMail->id);
}
}
/**
* Sends follow-ups based on time frequency like birthdays
*/
private function handleFollowups(): void
{
$this->checkTimeRemaining();
if (in_array(self::STEP_FOLLOWUP, $this->skip) || $this->cronTimeLimitReached) {
return;
}
$followupClass = new FollowupClass();
$followups = $followupClass->getFollowupDailyBases();
// Only once a day
$dailyHour = $this->config->get('daily_hour', '12');
$dailyMinute = $this->config->get('daily_minute', '00');
// The day it is currently based on the timezone specified in the CMS configuration
$dayBasedOnCMSTimezone = acym_date('now', 'Y-m-d');
// The UTC timestamp of the current day based on the CMS timezone, at the specified hour
$dayBasedOnCMSTimezoneAtSpecifiedHour = acym_getTimeFromCMSDate($dayBasedOnCMSTimezone.' '.$dailyHour.':'.$dailyMinute);
$time = time();
foreach ($followups as $followup) {
$lastTrigger = empty($followup->last_trigger) ? '' : acym_date($followup->last_trigger, 'Y-m-d');
if ($time >= $dayBasedOnCMSTimezoneAtSpecifiedHour && $lastTrigger != $dayBasedOnCMSTimezone) {
if (!empty($followup->condition)) {
$followup->condition = json_decode($followup->condition, true);
}
acym_trigger('onAcymFollowupDailyBasesNeedToBeTriggered', [$followup]);
}
}
}
/**
* Checks messages received on mailboxes and triggers actions accordingly
*/
private function handleMailboxActions(): void
{
$this->checkTimeRemaining();
if (in_array(self::STEP_MAILBOX_ACTION, $this->skip) || $this->cronTimeLimitReached || !acym_level(ACYM_ENTERPRISE)) {
return;
}
$mailboxActionClass = new MailboxClass();
$mailboxes = $mailboxActionClass->getAllActiveReadyWithActions();
if (empty($mailboxes)) {
return;
}
$mailboxHelper = new MailboxHelper();
$mailboxHelper->report = false;
foreach ($mailboxes as $oneMailboxAction) {
$this->processed = true;
if (!$mailboxHelper->isConnectionValid($oneMailboxAction, false)) {
$this->messages[] = acym_translationSprintf('ACYM_CONNECTION_FAILED_MAILBOX_X', $oneMailboxAction->id.'-'.$oneMailboxAction->name);
$this->errorDetected = true;
continue;
}
$nbMessages = $mailboxHelper->getNBMessages();
if (!$nbMessages) {
$this->messages[] = acym_translationSprintf('ACYM_NO_MESSAGE_IN_MAILBOX_X', $oneMailboxAction->id.'-'.$oneMailboxAction->name);
$mailboxHelper->close();
continue;
}
$mailboxHelper->handleAction();
$mailboxHelper->close();
// Update next trigger
$oneMailboxAction->nextdate = time() + $oneMailboxAction->frequency;
unset($oneMailboxAction->conditions);
unset($oneMailboxAction->actions);
$mailboxActionClass->save($oneMailboxAction);
}
}
private function cleanData(): void
{
// Clean the history and detailed stats based on the configuration
if (!in_array(self::STEP_DELETE_HISTORY, $this->skip) && $this->isDailyCron()) {
$userStatClass = new UserStatClass();
$userDetailedStatsDeleted = $userStatClass->deleteDetailedStatsPeriod();
if (!empty($userDetailedStatsDeleted['message'])) {
$this->messages[] = $userDetailedStatsDeleted['message'];
$this->processed = true;
}
$userClass = new UserClass();
$userHistoryDeleted = $userClass->deleteHistoryPeriod();
if (!empty($userHistoryDeleted['message'])) {
$this->messages[] = $userHistoryDeleted['message'];
$this->processed = true;
}
$mailArchiveClass = new MailArchiveClass();
$archiveDeleted = $mailArchiveClass->deleteArchivePeriod();
if (!empty($archiveDeleted['message'])) {
$this->messages[] = $archiveDeleted['message'];
$this->processed = true;
}
}
// Clean data on external sending method
if (!in_array(self::STEP_CLEAN_DATA_EXTERNAL_SENDING_METHOD, $this->skip) && $this->isDailyCron()) {
acym_trigger('onAcymCleanDataExternalSendingMethod');
}
// Clean export changes
if (!in_array(self::STEP_CLEAN_EXPORT_CHANGES, $this->skip) && $this->isDailyCron()) {
$exportHelper = new ExportHelper();
$exportHelper->cleanExportChangesFile();
}
if ($this->isDailyCron()) {
$time = time();
$this->config->saveConfig(['cron_last_daily' => $time]);
}
}
private function isDailyCron(): bool
{
// Only once a day
$dailyHour = $this->config->get('daily_hour', '12');
$dailyMinute = $this->config->get('daily_minute', '00');
// The day it is currently based on the timezone specified in the CMS configuration
$dayBasedOnCMSTimezone = acym_date('now', 'Y-m-d');
// The UTC timestamp of the current day based on the CMS timezone, at the specified hour
$dayBasedOnCMSTimezoneAtSpecifiedHour = acym_getTimeFromCMSDate($dayBasedOnCMSTimezone.' '.$dailyHour.':'.$dailyMinute);
$time = time();
//If we do not have run the cron we set it to yesterday so it will be trigger
$lastCronDayBasedOnCMSTimezone = acym_date($this->config->get('cron_last_daily', $time - 86400), 'Y-m-d');
return $time >= $dayBasedOnCMSTimezoneAtSpecifiedHour && $lastCronDayBasedOnCMSTimezone != $dayBasedOnCMSTimezone;
}
private function handleABTestCampaigns(): void
{
$this->checkTimeRemaining();
if (in_array(self::STEP_ABTEST, $this->skip) || $this->cronTimeLimitReached) {
return;
}
$campaignClass = new CampaignClass();
$abTestCampaigns = $campaignClass->getAllAbTestCampaignsToFinishSending();
foreach ($abTestCampaigns as $campaign) {
if ($campaignClass->finishAbTestCampaign($campaign)) {
$this->messages[] = acym_translationSprintf('ACYM_ABTEST_CAMPAIGN_X_FINAL_VERSION_SENT', $campaign->id);
$this->processed = true;
}
}
}
private function handleScenario(): void
{
$this->checkTimeRemaining();
if (in_array(self::STEP_SCENARIO, $this->skip) || $this->cronTimeLimitReached) {
return;
}
$scenarioHelper = new ScenarioHelper();
$scenarioHelper->executeAvailableSteps();
$scenarioHelper->triggerTimeScenarios();
}
/**
* We reached the maximum time allowed so the tasks are not finished, we begin a new process to finish it up
*/
private function continueCronCall(): void
{
if ($this->externalSendingNotFinished) {
$cronKey = '';
if (!empty($this->config->get('cron_security', 0)) && !empty($this->config->get('cron_key'))) {
$cronKey = '&cronKey='.$this->config->get('cron_key');
}
acym_makeCurlCall(acym_frontendLink('cron&task=cron&external_sending_repeat=1&t='.time().$cronKey), ['verifySsl' => false]);
}
}
private function canSendQueuedEmails(): bool
{
if ($this->config->get('queue_type') === 'manual') {
return false;
}
$dayOfWeek = acym_date('now', 'N');
if ($this->config->get('queue_stop_weekend', 0) && $dayOfWeek >= 6) {
return false;
}
// Make sure we are within the sending hours defined in the configuration
$fromHour = $this->config->get('queue_send_from_hour', '00');
$fromMinute = $this->config->get('queue_send_from_minute', '00');
$toHour = $this->config->get('queue_send_to_hour', '23');
$toMinute = $this->config->get('queue_send_to_minute', '59');
$time = time();
if ($fromHour != '00' || $fromMinute != '00' || $toHour != '23' || $toMinute != '59') {
// The day it is currently based on the timezone specified in the CMS configuration
$dayBasedOnCMSTimezone = acym_date('now', 'Y-m-d');
// The UTC timestamp of the current day based on the CMS timezone, at the specified hour
$fromBasedOnCMSTimezoneAtSpecifiedHour = acym_getTimeFromCMSDate($dayBasedOnCMSTimezone.' '.$fromHour.':'.$fromMinute);
$toBasedOnCMSTimezoneAtSpecifiedHour = acym_getTimeFromCMSDate($dayBasedOnCMSTimezone.' '.$toHour.':'.$toMinute);
// In case we want to send during the night and the FROM is superior to the TO (from 8pm to 4am), we should change the day of ones of the limits
if ($fromBasedOnCMSTimezoneAtSpecifiedHour > $toBasedOnCMSTimezoneAtSpecifiedHour) {
// TO becomes tomorrow as we are not passed midnight
if ($time > $fromBasedOnCMSTimezoneAtSpecifiedHour) {
$toBasedOnCMSTimezoneAtSpecifiedHour = acym_getTimeFromCMSDate(acym_date('tomorrow', 'Y-m-d').' '.$toHour.':'.$toMinute);
} elseif ($time < $toBasedOnCMSTimezoneAtSpecifiedHour) {
// FROM becomes yesterday as we are passed midnight
$fromBasedOnCMSTimezoneAtSpecifiedHour = acym_getTimeFromCMSDate(acym_date('yesterday', 'Y-m-d').' '.$fromHour.':'.$fromMinute);
}
}
if ($time < $fromBasedOnCMSTimezoneAtSpecifiedHour || $time > $toBasedOnCMSTimezoneAtSpecifiedHour) {
return false;
}
}
return true;
}
private function sendQueuedEmails(): void
{
$queueHelper = new QueueHelper();
$queueHelper->send_limit = (int)$this->config->get('queue_nbmail_auto');
$queueHelper->report = false;
$queueHelper->emailTypes = $this->emailTypes;
$queueHelper->startSendingFrom = $this->startQueue;
$queueHelper->process();
if (!empty($queueHelper->messages)) {
$this->detailMessages = array_merge($this->detailMessages, $queueHelper->messages);
}
if (!empty($queueHelper->nbprocess)) {
$this->processed = true;
if (!$queueHelper->finish && $this->externalSendingActivated) {
$this->externalSendingNotFinished = true;
}
}
$this->mainMessage = acym_translationSprintf(
'ACYM_CRON_PROCESS',
$queueHelper->nbprocess,
$queueHelper->successSend,
$queueHelper->errorSend
);
$this->messages[] = $this->mainMessage;
if (!empty($queueHelper->errorSend)) {
$this->errorDetected = true;
}
$this->cronTimeLimit = $queueHelper->stoptime;
}
}