| @@ -60,14 +60,18 @@ | ||
| 60 | 60 | private int $cronTimeLimit = 0; |
| 61 | 61 | private bool $cronTimeLimitReached = false; |
| 62 | 62 | private int $startQueue; |
| 63 | 63 | |
| 64 | + // Save the main message | |
| 64 | 65 | private string $mainMessage = ''; |
| 65 | 66 | |
| 67 | + // Did the cron process anything? | |
| 66 | 68 | private bool $processed = false; |
| 67 | 69 | |
| 70 | + // Is there any error in the process | |
| 68 | 71 | private bool $errorDetected = false; |
| 69 | 72 | |
| 73 | + // If we call the cron just to send a batch of emails | |
| 70 | 74 | private bool $externalSendingActivated = false; |
| 71 | 75 | private bool $externalSendingRepeat; |
| 72 | 76 | private bool $externalSendingNotFinished = false; |
| 73 | 77 | private bool $isSendingCall = false; |
| @@ -148,13 +152,15 @@ | ||
| 148 | 152 | if (!empty($detailMessages)) { |
| 149 | 153 | $this->detailMessages = $detailMessages; |
| 150 | 154 | } |
| 151 | 155 | |
| 152 | - $reportPath = str_replace(['{year}', '{month}'], [date('Y'), date('m')], $reportPath); | |
| 156 | + // Prepare the cron file path | |
| 157 | + $reportPath = str_replace(['{year}', '{month}'], [gmdate('Y'), gmdate('m')], $reportPath); | |
| 153 | 158 | $reportPath = acym_cleanPath(ACYM_ROOT.trim(html_entity_decode($reportPath))); |
| 154 | 159 | acym_createDir(dirname($reportPath), true, true); |
| 155 | 160 | |
| 156 | 161 | $lr = "\r\n"; |
| 162 | + // Catch warnings | |
| 157 | 163 | ob_start(); |
| 158 | 164 | file_put_contents( |
| 159 | 165 | $reportPath, |
| 160 | 166 | $lr.$lr.'******************** '.acym_getDate(time()).' UTC ********************'.$lr.implode($lr, $this->messages), |
| @@ -174,8 +180,11 @@ | ||
| 174 | 180 | $this->messages[] = $potentialWarnings; |
| 175 | 181 | } |
| 176 | 182 | } |
| 177 | 183 | |
| 184 | + /** | |
| 185 | + * Sets the types of emails that will be sent by the cron | |
| 186 | + */ | |
| 178 | 187 | public function setEmailTypes(array $emailTypes): void |
| 179 | 188 | { |
| 180 | 189 | $this->emailTypes = $emailTypes; |
| 181 | 190 | } |
| @@ -240,8 +249,11 @@ | ||
| 240 | 249 | |
| 241 | 250 | $this->config->saveConfig($newConfig); |
| 242 | 251 | } |
| 243 | 252 | |
| 253 | + /** | |
| 254 | + * Call made by the API to remove the API key when the user unlinks the website from their account page on our website | |
| 255 | + */ | |
| 244 | 256 | private function handleUnlinkLicenseCalls(): void |
| 245 | 257 | { |
| 246 | 258 | if (acym_getVar('int', 'unlink', 0) !== 1) { |
| 247 | 259 | return; |
| @@ -256,8 +268,11 @@ | ||
| 256 | 268 | |
| 257 | 269 | exit; |
| 258 | 270 | } |
| 259 | 271 | |
| 272 | + /** | |
| 273 | + * Displays the message shown on the cron URL page | |
| 274 | + */ | |
| 260 | 275 | private function triggeredMessage(): void |
| 261 | 276 | { |
| 262 | 277 | $firstMessage = acym_translationSprintf('ACYM_CRON_TRIGGERED', acym_date('now', 'd F Y H:i')); |
| 263 | 278 | $this->messages[] = $firstMessage; |
| @@ -263,8 +278,11 @@ | ||
| 263 | 278 | $this->messages[] = $firstMessage; |
| 264 | 279 | acym_display($firstMessage, 'info'); |
| 265 | 280 | } |
| 266 | 281 | |
| 282 | + /** | |
| 283 | + * Checks the cron frequency in the configuration to make sure the call is wanted | |
| 284 | + */ | |
| 267 | 285 | private function checkCronFrequency(): bool |
| 268 | 286 | { |
| 269 | 287 | if ($this->isSendingCall) { |
| 270 | 288 | return true; |
| @@ -275,21 +293,24 @@ | ||
| 275 | 293 | $cronFrequency = $this->config->get('cron_frequency', 900); |
| 276 | 294 | |
| 277 | 295 | if ($nextCronTime > $time) { |
| 278 | 296 | if ($nextCronTime > ($time + $cronFrequency)) { |
| 297 | + // 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 | |
| 279 | 298 | $this->config->saveConfig(['cron_next' => $time + $cronFrequency]); |
| 280 | 299 | } |
| 281 | 300 | |
| 282 | 301 | $notTimeMessage = acym_translationSprintf('ACYM_CRON_NEXT', acym_date($this->config->get('cron_next'), 'd F Y H:i')); |
| 283 | 302 | $this->messages[] = $notTimeMessage; |
| 303 | + // We don't need to trigger anything, it's not time yet | |
| 284 | 304 | acym_display($notTimeMessage, 'info'); |
| 285 | 305 | |
| 286 | 306 | return false; |
| 287 | 307 | } |
| 288 | 308 | |
| 309 | + // We update the next cron and the last cron dates | |
| 289 | 310 | $newConfig = [ |
| 290 | 311 | 'cron_last' => $time, |
| 291 | - 'cron_fromip' => acym_getIP(), | |
| 312 | + 'cron_fromip' => acym_getIP(true), | |
| 292 | 313 | 'cron_next' => $nextCronTime + $cronFrequency, |
| 293 | 314 | ]; |
| 294 | 315 | |
| 295 | 316 | if ($newConfig['cron_next'] <= $time || $newConfig['cron_next'] > $time + $cronFrequency) { |
| @@ -300,8 +321,11 @@ | ||
| 300 | 321 | |
| 301 | 322 | return true; |
| 302 | 323 | } |
| 303 | 324 | |
| 325 | + /** | |
| 326 | + * Adds the scheduled emails to the queue when the date is reached | |
| 327 | + */ | |
| 304 | 328 | private function queueScheduledCampaigns(): void |
| 305 | 329 | { |
| 306 | 330 | if (in_array(self::STEP_SCHEDULE, $this->skip)) { |
| 307 | 331 | return; |
| @@ -315,8 +339,11 @@ | ||
| 315 | 339 | $this->processed = true; |
| 316 | 340 | } |
| 317 | 341 | } |
| 318 | 342 | |
| 343 | + /** | |
| 344 | + * Cleans the emails that are stuck in the queue for too long (disabled/unconfirmed recipients) | |
| 345 | + */ | |
| 319 | 346 | private function cleanQueue(): void |
| 320 | 347 | { |
| 321 | 348 | if (in_array(self::STEP_CLEAN_QUEUE, $this->skip)) { |
| 322 | 349 | return; |
| @@ -345,8 +372,11 @@ | ||
| 345 | 372 | $this->sendQueuedEmails(); |
| 346 | 373 | } |
| 347 | 374 | } |
| 348 | 375 | |
| 376 | + /** | |
| 377 | + * Makes sure the current process doesn't reach the server time limit | |
| 378 | + */ | |
| 349 | 379 | private function checkTimeRemaining(): void |
| 350 | 380 | { |
| 351 | 381 | $time = time(); |
| 352 | 382 | if (empty($this->cronTimeLimit) || $time < $this->cronTimeLimit) { |
| @@ -357,8 +387,11 @@ | ||
| 357 | 387 | |
| 358 | 388 | $this->cronTimeLimitReached = true; |
| 359 | 389 | } |
| 360 | 390 | |
| 391 | + /** | |
| 392 | + * If the configuration is set to send multiple batches of emails at the same time | |
| 393 | + */ | |
| 361 | 394 | private function handleMultiCron(): void |
| 362 | 395 | { |
| 363 | 396 | $emailsPerBatches = $this->config->get('queue_nbmail_auto', 70); |
| 364 | 397 | if (empty($emailsPerBatches)) { |
| @@ -429,8 +462,9 @@ | ||
| 429 | 462 | $this->detailMessages = array_merge($this->detailMessages, $bounceHelper->messages); |
| 430 | 463 | } else { |
| 431 | 464 | $bounceErrors = $bounceHelper->getErrors(); |
| 432 | 465 | $newConfig['auto_bounce_report'] = implode('<br />', $bounceErrors); |
| 466 | + //We add "bounce handling" just before the error so the user knows where it comes from... | |
| 433 | 467 | if (!empty($bounceErrors[0])) { |
| 434 | 468 | $bounceErrors[0] = acym_translation('ACYM_BOUNCE_HANDLING').' : '.$bounceErrors[0]; |
| 435 | 469 | } |
| 436 | 470 | $this->messages = array_merge($this->messages, $bounceErrors); |
| @@ -440,8 +474,11 @@ | ||
| 440 | 474 | |
| 441 | 475 | $this->config->saveConfig($newConfig); |
| 442 | 476 | } |
| 443 | 477 | |
| 478 | + /** | |
| 479 | + * Triggers the automations based on time frequency | |
| 480 | + */ | |
| 444 | 481 | private function handleAutomations(): void |
| 445 | 482 | { |
| 446 | 483 | $this->checkTimeRemaining(); |
| 447 | 484 | if (in_array(self::STEP_AUTOMATION, $this->skip) || $this->cronTimeLimitReached || !acym_level(ACYM_ENTERPRISE)) { |
| @@ -478,8 +515,11 @@ | ||
| 478 | 515 | $this->processed = true; |
| 479 | 516 | } |
| 480 | 517 | } |
| 481 | 518 | |
| 519 | + /** | |
| 520 | + * Sends special emails such as birthday or WooCommerce reminders | |
| 521 | + */ | |
| 482 | 522 | private function handleSpecificEmails(): void |
| 483 | 523 | { |
| 484 | 524 | $this->checkTimeRemaining(); |
| 485 | 525 | if (in_array(self::STEP_SPECIFIC, $this->skip) || $this->cronTimeLimitReached) { |
| @@ -500,8 +540,11 @@ | ||
| 500 | 540 | $campaignClass->send($onespecialMail->id); |
| 501 | 541 | } |
| 502 | 542 | } |
| 503 | 543 | |
| 544 | + /** | |
| 545 | + * Sends follow-ups based on time frequency like birthdays | |
| 546 | + */ | |
| 504 | 547 | private function handleFollowups(): void |
| 505 | 548 | { |
| 506 | 549 | $this->checkTimeRemaining(); |
| 507 | 550 | if (in_array(self::STEP_FOLLOWUP, $this->skip) || $this->cronTimeLimitReached) { |
| @@ -510,11 +553,14 @@ | ||
| 510 | 553 | |
| 511 | 554 | $followupClass = new FollowupClass(); |
| 512 | 555 | $followups = $followupClass->getFollowupDailyBases(); |
| 513 | 556 | |
| 557 | + // Only once a day | |
| 514 | 558 | $dailyHour = $this->config->get('daily_hour', '12'); |
| 515 | 559 | $dailyMinute = $this->config->get('daily_minute', '00'); |
| 560 | + // The day it is currently based on the timezone specified in the CMS configuration | |
| 516 | 561 | $dayBasedOnCMSTimezone = acym_date('now', 'Y-m-d'); |
| 562 | + // The UTC timestamp of the current day based on the CMS timezone, at the specified hour | |
| 517 | 563 | $dayBasedOnCMSTimezoneAtSpecifiedHour = acym_getTimeFromCMSDate($dayBasedOnCMSTimezone.' '.$dailyHour.':'.$dailyMinute); |
| 518 | 564 | $time = time(); |
| 519 | 565 | |
| 520 | 566 | foreach ($followups as $followup) { |
| @@ -528,8 +574,11 @@ | ||
| 528 | 574 | } |
| 529 | 575 | } |
| 530 | 576 | } |
| 531 | 577 | |
| 578 | + /** | |
| 579 | + * Checks messages received on mailboxes and triggers actions accordingly | |
| 580 | + */ | |
| 532 | 581 | private function handleMailboxActions(): void |
| 533 | 582 | { |
| 534 | 583 | $this->checkTimeRemaining(); |
| 535 | 584 | if (in_array(self::STEP_MAILBOX_ACTION, $this->skip) || $this->cronTimeLimitReached || !acym_level(ACYM_ENTERPRISE)) { |
| @@ -564,8 +613,9 @@ | ||
| 564 | 613 | $mailboxHelper->handleAction(); |
| 565 | 614 | $mailboxHelper->close(); |
| 566 | 615 | |
| 567 | 616 | |
| 617 | + // Update next trigger | |
| 568 | 618 | $oneMailboxAction->nextdate = time() + $oneMailboxAction->frequency; |
| 569 | 619 | unset($oneMailboxAction->conditions); |
| 570 | 620 | unset($oneMailboxAction->actions); |
| 571 | 621 | $mailboxActionClass->save($oneMailboxAction); |
| @@ -573,8 +623,9 @@ | ||
| 573 | 623 | } |
| 574 | 624 | |
| 575 | 625 | private function cleanData(): void |
| 576 | 626 | { |
| 627 | + // Clean the history and detailed stats based on the configuration | |
| 577 | 628 | if (!in_array(self::STEP_DELETE_HISTORY, $this->skip) && $this->isDailyCron()) { |
| 578 | 629 | $userStatClass = new UserStatClass(); |
| 579 | 630 | $userDetailedStatsDeleted = $userStatClass->deleteDetailedStatsPeriod(); |
| 580 | 631 | if (!empty($userDetailedStatsDeleted['message'])) { |
| @@ -596,12 +647,14 @@ | ||
| 596 | 647 | $this->processed = true; |
| 597 | 648 | } |
| 598 | 649 | } |
| 599 | 650 | |
| 651 | + // Clean data on external sending method | |
| 600 | 652 | if (!in_array(self::STEP_CLEAN_DATA_EXTERNAL_SENDING_METHOD, $this->skip) && $this->isDailyCron()) { |
| 601 | 653 | acym_trigger('onAcymCleanDataExternalSendingMethod'); |
| 602 | 654 | } |
| 603 | 655 | |
| 656 | + // Clean export changes | |
| 604 | 657 | if (!in_array(self::STEP_CLEAN_EXPORT_CHANGES, $this->skip) && $this->isDailyCron()) { |
| 605 | 658 | $exportHelper = new ExportHelper(); |
| 606 | 659 | $exportHelper->cleanExportChangesFile(); |
| 607 | 660 | } |
| @@ -613,15 +666,19 @@ | ||
| 613 | 666 | } |
| 614 | 667 | |
| 615 | 668 | private function isDailyCron(): bool |
| 616 | 669 | { |
| 670 | + // Only once a day | |
| 617 | 671 | $dailyHour = $this->config->get('daily_hour', '12'); |
| 618 | 672 | $dailyMinute = $this->config->get('daily_minute', '00'); |
| 673 | + // The day it is currently based on the timezone specified in the CMS configuration | |
| 619 | 674 | $dayBasedOnCMSTimezone = acym_date('now', 'Y-m-d'); |
| 675 | + // The UTC timestamp of the current day based on the CMS timezone, at the specified hour | |
| 620 | 676 | $dayBasedOnCMSTimezoneAtSpecifiedHour = acym_getTimeFromCMSDate($dayBasedOnCMSTimezone.' '.$dailyHour.':'.$dailyMinute); |
| 621 | 677 | |
| 622 | 678 | $time = time(); |
| 623 | 679 | |
| 680 | + //If we do not have run the cron we set it to yesterday so it will be trigger | |
| 624 | 681 | $lastCronDayBasedOnCMSTimezone = acym_date($this->config->get('cron_last_daily', $time - 86400), 'Y-m-d'); |
| 625 | 682 | |
| 626 | 683 | return $time >= $dayBasedOnCMSTimezoneAtSpecifiedHour && $lastCronDayBasedOnCMSTimezone != $dayBasedOnCMSTimezone; |
| 627 | 684 | } |
| @@ -654,8 +711,11 @@ | ||
| 654 | 711 | $scenarioHelper->executeAvailableSteps(); |
| 655 | 712 | $scenarioHelper->triggerTimeScenarios(); |
| 656 | 713 | } |
| 657 | 714 | |
| 715 | + /** | |
| 716 | + * We reached the maximum time allowed so the tasks are not finished, we begin a new process to finish it up | |
| 717 | + */ | |
| 658 | 718 | private function continueCronCall(): void |
| 659 | 719 | { |
| 660 | 720 | if ($this->externalSendingNotFinished) { |
| 661 | 721 | $cronKey = ''; |
| @@ -676,8 +736,9 @@ | ||
| 676 | 736 | if ($this->config->get('queue_stop_weekend', 0) && $dayOfWeek >= 6) { |
| 677 | 737 | return false; |
| 678 | 738 | } |
| 679 | 739 | |
| 740 | + // Make sure we are within the sending hours defined in the configuration | |
| 680 | 741 | $fromHour = $this->config->get('queue_send_from_hour', '00'); |
| 681 | 742 | $fromMinute = $this->config->get('queue_send_from_minute', '00'); |
| 682 | 743 | $toHour = $this->config->get('queue_send_to_hour', '23'); |
| 683 | 744 | $toMinute = $this->config->get('queue_send_to_minute', '59'); |
| @@ -683,15 +744,20 @@ | ||
| 683 | 744 | $toMinute = $this->config->get('queue_send_to_minute', '59'); |
| 684 | 745 | $time = time(); |
| 685 | 746 | |
| 686 | 747 | if ($fromHour != '00' || $fromMinute != '00' || $toHour != '23' || $toMinute != '59') { |
| 748 | + // The day it is currently based on the timezone specified in the CMS configuration | |
| 687 | 749 | $dayBasedOnCMSTimezone = acym_date('now', 'Y-m-d'); |
| 750 | + // The UTC timestamp of the current day based on the CMS timezone, at the specified hour | |
| 688 | 751 | $fromBasedOnCMSTimezoneAtSpecifiedHour = acym_getTimeFromCMSDate($dayBasedOnCMSTimezone.' '.$fromHour.':'.$fromMinute); |
| 689 | 752 | $toBasedOnCMSTimezoneAtSpecifiedHour = acym_getTimeFromCMSDate($dayBasedOnCMSTimezone.' '.$toHour.':'.$toMinute); |
| 753 | + // 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 | |
| 690 | 754 | if ($fromBasedOnCMSTimezoneAtSpecifiedHour > $toBasedOnCMSTimezoneAtSpecifiedHour) { |
| 755 | + // TO becomes tomorrow as we are not passed midnight | |
| 691 | 756 | if ($time > $fromBasedOnCMSTimezoneAtSpecifiedHour) { |
| 692 | 757 | $toBasedOnCMSTimezoneAtSpecifiedHour = acym_getTimeFromCMSDate(acym_date('tomorrow', 'Y-m-d').' '.$toHour.':'.$toMinute); |
| 693 | 758 | } elseif ($time < $toBasedOnCMSTimezoneAtSpecifiedHour) { |
| 759 | + // FROM becomes yesterday as we are passed midnight | |
| 694 | 760 | $fromBasedOnCMSTimezoneAtSpecifiedHour = acym_getTimeFromCMSDate(acym_date('yesterday', 'Y-m-d').' '.$fromHour.':'.$fromMinute); |
| 695 | 761 | } |
| 696 | 762 | } |
| 697 | 763 | |