PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.0
4.11.0 4.10.0 4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Backup / BackupScheduler.php
wp-staging / Backup Last commit date
Ajax 1 day ago BackgroundProcessing 1 day ago Dto 1 day ago Entity 1 day ago Exceptions 1 day ago FileHeader 1 day ago Interfaces 1 day ago Job 1 day ago Request 1 day ago Service 1 day ago Storage 1 day ago Task 1 day ago Traits 1 day ago Utils 1 day ago AfterRestore.php 1 day ago BackupDeleter.php 1 day ago BackupDownload.php 1 day ago BackupFileIndex.php 1 day ago BackupGlitchReason.php 1 day ago BackupHeader.php 1 day ago BackupNextOffer.php 1 day ago BackupRepairer.php 1 day ago BackupRetentionHandler.php 1 day ago BackupScheduler.php 1 day ago BackupServiceProvider.php 1 day ago BackupValidator.php 1 day ago BeforeUpdateRowStatus.php 1 day ago FileHeader.php 1 day ago FileHeaderAttribute.php 1 day ago UpdateProtectionPausedNotice.php 1 day ago WithBackupIdentifier.php 1 day ago
BackupScheduler.php
1054 lines
1 <?php
2
3 namespace WPStaging\Backup;
4
5 use DateTime;
6 use WPStaging\Backup\BackgroundProcessing\Backup\PrepareBackup;
7 use WPStaging\Backup\Dto\Job\JobBackupDataDto;
8 use WPStaging\Backup\Service\BackupsFinder;
9 use WPStaging\Backup\Task\Tasks\JobBackup\FinishBackupTask;
10 use WPStaging\Core\Cron\Cron;
11 use WPStaging\Core\WPStaging;
12 use WPStaging\Framework\Facades\Sanitize;
13 use WPStaging\Framework\Job\ProcessLock;
14 use WPStaging\Framework\Security\Capabilities;
15 use WPStaging\Framework\Security\Nonce;
16 use WPStaging\Notifications\Notifications;
17
18 use function WPStaging\functions\debug_log;
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 class BackupScheduler
34 {
35
36 const OPTION_BACKUP_SCHEDULE_ERROR_REPORT = 'wpstg_backup_schedules_send_error_report';
37
38
39 const OPTION_BACKUP_SCHEDULE_WARNING_REPORT = 'wpstg_backup_schedules_send_warning_report';
40
41
42 const OPTION_BACKUP_SCHEDULE_GENERAL_REPORT = 'wpstg_backup_schedules_send_general_report';
43
44
45 const OPTION_BACKUP_SCHEDULE_SLACK_ERROR_REPORT = 'wpstg_backup_schedules_send_slack_error_report';
46
47
48 const OPTION_BACKUP_SCHEDULE_REPORT_SLACK_WEBHOOK = 'wpstg_backup_schedules_report_slack_webhook';
49
50
51 const OPTION_BACKUP_SCHEDULES = 'wpstg_backup_schedules';
52
53
54 const OPTION_LAST_BACKUP_FAILURE = 'wpstg_last_backup_failure';
55
56
57 const CRON_WARNING_TYPE_FAILURE = 'failure';
58
59
60 const CRON_WARNING_TYPE_OVERDUE = 'overdue';
61
62
63 const OVERDUE_GRACE_PERIOD = 30 * MINUTE_IN_SECONDS;
64
65
66 const TRANSIENT_BACKUP_SCHEDULE_ERROR_REPORT_SENT = 'wpstg.backup.schedules.error_report_sent';
67
68
69 const TRANSIENT_BACKUP_SCHEDULE_WARNING_REPORT_SENT = 'wpstg.backup.schedules.warning_report_sent';
70
71
72 const TRANSIENT_BACKUP_SCHEDULE_GENERAL_REPORT_SENT = 'wpstg.backup.schedules.general_report_sent';
73
74
75 const TRANSIENT_BACKUP_SCHEDULE_SLACK_REPORT_SENT = 'wpstg.backup.schedules.slack_report_sent';
76
77
78 const REPORT_TYPE_ERROR = 'error';
79
80
81 const REPORT_TYPE_WARNING = 'warning';
82
83
84 const REPORT_TYPE_GENERAL = 'general';
85
86
87 const FILTER_SCHEDULES_BACKUP_INTERVAL = 'wpstg.schedulesBackup.interval';
88
89
90 protected $backupsFinder;
91
92
93 protected $processLock;
94
95
96 protected $backupDeleter;
97
98
99
100
101 protected $notifications;
102
103
104 protected $numberOverdueCronjobs = 0;
105
106
107
108
109
110 protected $cronWarningType = '';
111
112
113
114
115
116 protected $lastBackupFailureMessage = '';
117
118
119
120
121
122
123
124 public function __construct(BackupsFinder $backupsFinder, ProcessLock $processLock, BackupDeleter $backupDeleter, Notifications $notifications)
125 {
126 $this->backupsFinder = $backupsFinder;
127 $this->processLock = $processLock;
128 $this->backupDeleter = $backupDeleter;
129 $this->notifications = $notifications;
130
131 $this->countOverdueCronjobs();
132 }
133
134
135
136
137 public function getSchedules(): array
138 {
139 $schedules = get_option(static::OPTION_BACKUP_SCHEDULES, []);
140 if (is_array($schedules)) {
141 return $schedules;
142 }
143
144 return [];
145 }
146
147
148
149
150
151 public function maybeDeleteOldBackups(JobBackupDataDto $jobBackupDataDto)
152 {
153 $scheduleId = $jobBackupDataDto->getScheduleId();
154
155
156 if (empty($scheduleId)) {
157 return;
158 }
159
160 $schedules = get_option(static::OPTION_BACKUP_SCHEDULES, []);
161
162 $schedule = array_filter($schedules, function ($schedule) use ($scheduleId) {
163 return $schedule['scheduleId'] == $scheduleId;
164 });
165
166 if (empty($schedule)) {
167 debug_log("Could not delete old backups for schedule ID $scheduleId as the schedule rotation plan was not found in the database.");
168 return;
169 }
170
171 $schedule = array_shift($schedule);
172
173 $maxAllowedBackupFiles = absint($schedule['rotation']);
174
175 $backupFiles = $this->backupsFinder->findBackupByScheduleId($scheduleId);
176
177
178 if (count($backupFiles) < $maxAllowedBackupFiles) {
179 return;
180 }
181
182
183 uasort($backupFiles, function ($backup1, $backup2) {
184
185
186
187
188 if ($backup1->getMTime() === $backup2->getMTime()) {
189 return 0;
190 }
191
192 return $backup1->getMTime() < $backup2->getMTime() ? -1 : 1;
193 });
194
195
196 $backupFiles = array_values($backupFiles);
197
198
199 $backupFiles = array_slice($backupFiles, 0, max(1, count($backupFiles) - $maxAllowedBackupFiles + 1));
200
201 array_map(function ($file) {
202 $this->backupDeleter->clearErrors();
203 $this->backupDeleter->deleteBackup($file);
204 $errors = $this->backupDeleter->getErrors();
205 foreach ($errors as $error) {
206 debug_log('Tried to cleanup old backups for backup plan rotation, but couldn\'t delete file: ' . $error);
207 }
208 }, $backupFiles);
209 }
210
211
212
213
214
215
216
217 public function scheduleBackup(JobBackupDataDto $jobBackupDataDto, string $scheduleId)
218 {
219 if (!isset(wp_get_schedules()[$jobBackupDataDto->getScheduleRecurrence()])) {
220 debug_log("Tried to schedule a backup, but schedule '" . $jobBackupDataDto->getScheduleRecurrence() . "' is not registered as a WordPress cron schedule. Data DTO: " . wp_json_encode($jobBackupDataDto));
221
222 return;
223 }
224
225 $firstSchedule = new \DateTime('now', wp_timezone());
226 $time = $jobBackupDataDto->getScheduleTime();
227 $recurrence = $jobBackupDataDto->getScheduleRecurrence();
228 $dayOfWeek = Cron::extractDayFromSchedule($recurrence);
229 $this->setUpcomingDateTime($firstSchedule, $time, $dayOfWeek, $recurrence);
230
231 $backupSchedule = [
232 'scheduleId' => $scheduleId,
233 'schedule' => $jobBackupDataDto->getScheduleRecurrence(),
234 'backupType' => $jobBackupDataDto->getBackupType(),
235 'subsiteBlogId' => $jobBackupDataDto->getSubsiteBlogId(),
236 'time' => $time,
237 'name' => $jobBackupDataDto->getName(),
238 'rotation' => $jobBackupDataDto->getScheduleRotation(),
239 'isExportingPlugins' => $jobBackupDataDto->getIsExportingPlugins(),
240 'isExportingMuPlugins' => $jobBackupDataDto->getIsExportingMuPlugins(),
241 'isExportingThemes' => $jobBackupDataDto->getIsExportingThemes(),
242 'isExportingUploads' => $jobBackupDataDto->getIsExportingUploads(),
243 'isExportingOtherWpContentFiles' => $jobBackupDataDto->getIsExportingOtherWpContentFiles(),
244 'isExportingOtherWpRootFiles' => $jobBackupDataDto->getIsExportingOtherWpRootFiles(),
245 'isExportingDatabase' => $jobBackupDataDto->getIsExportingDatabase(),
246 'sitesToBackup' => $jobBackupDataDto->getSitesToBackup(),
247 'storages' => $jobBackupDataDto->getStorages(),
248 'firstSchedule' => $firstSchedule->getTimestamp(),
249 'isSmartExclusion' => $jobBackupDataDto->getIsSmartExclusion(),
250 'isExcludingSpamComments' => $jobBackupDataDto->getIsExcludingSpamComments(),
251 'isExcludingPostRevision' => $jobBackupDataDto->getIsExcludingPostRevision(),
252 'isExcludingDeactivatedPlugins' => $jobBackupDataDto->getIsExcludingDeactivatedPlugins(),
253 'isExcludingUnusedThemes' => $jobBackupDataDto->getIsExcludingUnusedThemes(),
254 'isExcludingLogs' => $jobBackupDataDto->getIsExcludingLogs(),
255 'isExcludingCaches' => $jobBackupDataDto->getIsExcludingCaches(),
256 'isWpCliRequest' => true,
257 'backupExcludedDirectories' => $jobBackupDataDto->getBackupExcludedDirectories(),
258 ];
259
260 if (wp_next_scheduled(Cron::ACTION_CREATE_CRON_BACKUP, [$backupSchedule])) {
261 debug_log('[Schedule Backup Cron] Early bailed when registering the cron to create a backup on a schedule, because it already exists');
262
263 return;
264 }
265
266 $this->registerScheduleInDb($backupSchedule);
267 $this->reCreateCron();
268 }
269
270
271
272
273
274
275 protected function registerScheduleInDb(array $backupSchedule): bool
276 {
277 $backupSchedules = get_option(static::OPTION_BACKUP_SCHEDULES, []);
278 if (!is_array($backupSchedules)) {
279 $backupSchedules = [];
280 }
281
282 $backupSchedules[] = $backupSchedule;
283
284 if (!update_option(static::OPTION_BACKUP_SCHEDULES, $backupSchedules, false)) {
285 debug_log('[Schedule Backup Cron] Could not update BackupSchedules DB option');
286 return false;
287 }
288
289 return true;
290 }
291
292
293
294
295
296
297
298 public function createCronBackup(array $backupData)
299 {
300
301 $logId = wp_generate_password(4, false);
302
303 debug_log(sprintf("[Schedule Backup Cron - %s] Received request to create a backup using Cron. Backup Data: %s", $logId, wp_json_encode($backupData)), 'info', false);
304
305 try {
306 debug_log(sprintf("[Schedule Backup Cron - %s] Preparing job", $logId), 'info', false);
307 $jobId = WPStaging::make(PrepareBackup::class)->prepare($backupData);
308 if ($jobId instanceof \WP_Error) {
309 debug_log(sprintf("[Schedule Backup Cron - %s] Failed to create backup: %s", $logId, $jobId->get_error_message()));
310 $this->saveBackupFailure($jobId->get_error_message());
311 return;
312 }
313
314 debug_log(sprintf("[Schedule Backup Cron - %s] Successfully received a Job ID: %s", $logId, $jobId), 'info', false);
315 } catch (\Exception $e) {
316 debug_log("[Schedule Backup Cron - $logId] Exception thrown while preparing the Backup: " . $e->getMessage());
317 $this->saveBackupFailure($e->getMessage());
318 }
319 }
320
321
322
323
324
325 public function dismissSchedule()
326 {
327 if (!current_user_can((new Capabilities())->manageWPSTG())) {
328 return;
329 }
330
331 if (!(new Nonce())->requestHasValidNonce(Nonce::WPSTG_NONCE)) {
332 return;
333 }
334
335 if (empty($_POST['scheduleId'])) {
336 return;
337 }
338
339 try {
340 $this->deleteSchedule(Sanitize::sanitizeString($_POST['scheduleId']));
341 wp_send_json_success();
342 } catch (\Exception $e) {
343 wp_send_json_error($e->getMessage());
344 }
345 }
346
347
348
349
350
351
352
353 public function deleteSchedule(string $scheduleId, $reCreateCron = true)
354 {
355 $schedules = $this->getSchedules();
356
357 $newSchedules = array_filter($schedules, function ($schedule) use ($scheduleId) {
358 return $schedule['scheduleId'] != $scheduleId;
359 });
360
361 if (!update_option(static::OPTION_BACKUP_SCHEDULES, $newSchedules, false)) {
362 debug_log('[Schedule Backup Cron] Could not update BackupSchedules DB option after removing schedule.');
363 throw new \RuntimeException('Could not unschedule event from Db.');
364 }
365
366
367
368
369 if (empty($newSchedules)) {
370 delete_option(static::OPTION_LAST_BACKUP_FAILURE);
371 }
372
373 if ($reCreateCron === false) {
374 return;
375 }
376
377 $this->reCreateCron();
378 }
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393 public function reCreateCron($scheduleBeingEdit = null): bool
394 {
395 $schedules = $this->getSchedules();
396 static::removeBackupSchedulesFromCron();
397
398 $errors = [];
399
400 foreach ($schedules as $schedule) {
401 $timeToSchedule = new \DateTime('now', wp_timezone());
402
403
404
405
406 if (isset(wp_get_schedules()[$schedule['schedule']]) && isset($schedule['firstSchedule']) && ($schedule['scheduleId'] !== $scheduleBeingEdit)) {
407 $this->setNextSchedulingDate($timeToSchedule, $schedule);
408 } else {
409 $dayOfWeek = Cron::extractDayFromSchedule($schedule['schedule']);
410 $this->setUpcomingDateTime($timeToSchedule, $schedule['time'], $dayOfWeek, $schedule['schedule']);
411 }
412
413
414 $result = wp_schedule_event($timeToSchedule->format('U'), $schedule['schedule'], Cron::ACTION_CREATE_CRON_BACKUP, [$schedule]);
415
416
417
418 if ($result === false || $result instanceof \WP_Error) {
419 if ($result instanceof \WP_Error) {
420 $details = $result->get_error_message();
421 } else {
422 $details = '';
423 }
424
425 $error = '[Schedule Backup Cron] Failed to register the cron event wpstg_create_cron_backup. ' . $schedule['schedule'] . ' ' . $details;
426
427 $errors[] = $error;
428
429 debug_log($error);
430 }
431 }
432
433 if (!empty($errors)) {
434 return false;
435 }
436
437 return true;
438 }
439
440
441
442
443
444
445
446
447
448 public function reCreateCronIfSchedulesExist(): bool
449 {
450 if (empty($this->getSchedules())) {
451 return true;
452 }
453
454 return $this->reCreateCron();
455 }
456
457
458
459
460
461
462
463
464
465
466 public static function removeBackupSchedulesFromCron(): bool
467 {
468 $cron = get_option('cron');
469
470
471 if (!is_array($cron)) {
472 return false;
473 }
474
475
476 foreach ($cron as $timestamp => &$events) {
477 if (is_array($events)) {
478 foreach ($events as $callback => &$args) {
479 if ($callback === Cron::ACTION_CREATE_CRON_BACKUP) {
480 unset($cron[$timestamp][$callback]);
481 }
482 }
483 }
484 }
485
486
487
488
489 $cron = array_filter($cron, function ($timestamps) {
490 return !empty($timestamps);
491 });
492
493 update_option('cron', $cron);
494
495 return true;
496 }
497
498
499
500
501
502
503
504
505 public function checkCronStatus(): bool
506 {
507 $this->cronWarningType = '';
508 $this->lastBackupFailureMessage = '';
509
510 if ($this->isSchedulesEmpty()) {
511 return true;
512 }
513
514 $this->detectScheduledBackupWarning();
515
516 return $this->cronWarningType === '';
517 }
518
519
520
521
522
523
524 private function detectScheduledBackupWarning()
525 {
526 $lastFailure = get_option(self::OPTION_LAST_BACKUP_FAILURE);
527 if (is_array($lastFailure) && !empty($lastFailure['time']) && (int)$lastFailure['time'] > $this->getLastScheduledBackupSuccessTime()) {
528 $this->cronWarningType = self::CRON_WARNING_TYPE_FAILURE;
529 $this->lastBackupFailureMessage = $lastFailure['message'] ?? '';
530 return;
531 }
532
533 if ($this->hasOverdueOrMissingBackupCronJob()) {
534 $this->cronWarningType = self::CRON_WARNING_TYPE_OVERDUE;
535 }
536 }
537
538
539
540
541
542
543 private function getLastScheduledBackupSuccessTime(): int
544 {
545 $lastBackupInfo = $this->getLastBackupInfo();
546 if (empty($lastBackupInfo['endTime'])) {
547 return 0;
548 }
549
550 $jobDataDto = isset($lastBackupInfo['JobBackupDataDto']) ? $lastBackupInfo['JobBackupDataDto'] : null;
551 if (!($jobDataDto instanceof JobBackupDataDto) || !$jobDataDto->isScheduledBackup()) {
552 return 0;
553 }
554
555 return (int)$lastBackupInfo['endTime'];
556 }
557
558
559
560
561 private function getLastBackupInfo(): array
562 {
563 $lastBackupInfo = get_option(FinishBackupTask::OPTION_LAST_BACKUP, []);
564
565 return is_array($lastBackupInfo) ? $lastBackupInfo : [];
566 }
567
568
569 public function getOverdueCronJobsCount(): int
570 {
571 return $this->numberOverdueCronjobs;
572 }
573
574
575 public function isWpCronDisabled(): bool
576 {
577 return defined('DISABLE_WP_CRON') && DISABLE_WP_CRON;
578 }
579
580
581 public function hasOverdueCronJobs(): bool
582 {
583 return $this->numberOverdueCronjobs > 4;
584 }
585
586
587
588
589 public function getWarningType(): string
590 {
591 return $this->cronWarningType;
592 }
593
594
595
596
597 public function getLastBackupFailureMessage(): string
598 {
599 return $this->lastBackupFailureMessage;
600 }
601
602
603
604
605
606 public function getNextBackupSchedule(): array
607 {
608 $cron = get_option('cron');
609
610
611 if (!is_array($cron)) {
612 throw new \UnexpectedValueException();
613 }
614
615 ksort($cron, SORT_NUMERIC);
616
617
618 foreach ($cron as $timestamp => &$events) {
619 if (is_array($events)) {
620 foreach ($events as $callback => &$args) {
621 if ($callback === Cron::ACTION_CREATE_CRON_BACKUP) {
622 return [$timestamp, $cron[$timestamp][$callback]];
623 }
624 }
625 }
626 }
627
628
629 throw new \OutOfBoundsException();
630 }
631
632
633
634
635
636
637
638
639
640
641 protected function setUpcomingDateTime(DateTime &$datetime, $time, $dayOfWeek = null, $scheduleRecurrence = null)
642 {
643 if (is_array($time)) {
644 $hourAndMinute = $time;
645 } else {
646 $hourAndMinute = explode(':', $time);
647 }
648
649
650 $isWeeklySchedule = $scheduleRecurrence === Cron::WEEKLY ||
651 $scheduleRecurrence === Cron::EVERY_TWO_WEEKS ||
652 strpos($scheduleRecurrence, Cron::WEEKLY . '_') === 0;
653
654 if ($dayOfWeek !== null && $isWeeklySchedule) {
655
656
657 $currentDayOfWeek = (int)$datetime->format('N');
658 $targetDayOfWeek = (int)$dayOfWeek;
659
660
661 $targetTimeInt = (int) sprintf('%02d%02d', $hourAndMinute[0], $hourAndMinute[1]);
662 $currentTimeInt = (int) $datetime->format('Hi');
663 $daysUntilTarget = $targetDayOfWeek - $currentDayOfWeek;
664
665 if ($daysUntilTarget < 0) {
666 $daysUntilTarget += 7;
667 }
668
669
670 if ($daysUntilTarget === 0 && $targetTimeInt <= $currentTimeInt) {
671 $daysUntilTarget = 7;
672 }
673
674
675 if ($daysUntilTarget > 0) {
676 $datetime->add(new \DateInterval("P{$daysUntilTarget}D"));
677 }
678 } else {
679
680 if ((int)sprintf('%s%s', $hourAndMinute[0], $hourAndMinute[1]) < (int)$datetime->format('Hi')) {
681 $datetime->add(new \DateInterval('P1D'));
682 }
683 }
684
685 $datetime->setTime($hourAndMinute[0], $hourAndMinute[1]);
686 }
687
688
689
690
691
692
693
694
695 protected function setNextSchedulingDate(DateTime &$datetime, array $schedule)
696 {
697 $next = $schedule['firstSchedule'];
698 $now = $datetime->getTimestamp();
699 if ($next >= $now) {
700 $dayOfWeek = Cron::extractDayFromSchedule($schedule['schedule']);
701 $this->setUpcomingDateTime($datetime, $schedule['time'], $dayOfWeek, $schedule['schedule']);
702 return;
703 }
704
705 $recurrance = wp_get_schedules()[$schedule['schedule']];
706 while ($next < $now) {
707 $next += $recurrance['interval'];
708 }
709
710 $datetime->setTimestamp($next);
711 }
712
713
714
715
716
717
718
719
720
721
722 public function sendErrorReport(string $message, string $title = ''): bool
723 {
724 if (get_option(self::OPTION_BACKUP_SCHEDULE_ERROR_REPORT) !== 'true') {
725 return false;
726 }
727
728 if (empty($message)) {
729 return false;
730 }
731
732 if (strpos($message, 'index resource') !== false) {
733 $message .= "\r\n \r\n" . esc_html__("This can happen if another process deleted the backup while it was created. Please report this to support@wp-staging.com if it happens often. Otherwise you can ignore it.", 'wp-staging');
734 }
735
736 if (empty($title)) {
737 $title = esc_html__('WP Staging - Backup Error Report', 'wp-staging');
738 }
739
740 $this->sendEmailReport($message, $title);
741 $this->sendSlackReport($message, $title);
742
743 return true;
744 }
745
746
747
748
749
750
751
752
753
754
755 public function sendWarningReport(string $message, string $title = ''): bool
756 {
757 if (get_option(self::OPTION_BACKUP_SCHEDULE_WARNING_REPORT) !== 'true') {
758 return false;
759 }
760
761 if (empty($message)) {
762 return false;
763 }
764
765 if (empty($title)) {
766 $title = esc_html__('WP Staging - Backup Warning Report', 'wp-staging');
767 }
768
769 $this->sendEmailReport($message, $title, self::REPORT_TYPE_WARNING);
770
771 return true;
772 }
773
774
775
776
777
778
779
780
781
782
783 public function sendGeneralReport(string $message, string $title = ''): bool
784 {
785 if (get_option(self::OPTION_BACKUP_SCHEDULE_GENERAL_REPORT) !== 'true') {
786 return false;
787 }
788
789 if (empty($message)) {
790 return false;
791 }
792
793 if (empty($title)) {
794 $title = esc_html__('WP Staging - Backup General Report', 'wp-staging');
795 }
796
797 $this->sendEmailReport($message, $title, self::REPORT_TYPE_GENERAL);
798
799 return true;
800 }
801
802
803
804
805
806
807
808
809
810 public function sendEmailReport(string $message, string $title = '', string $reportType = self::REPORT_TYPE_ERROR): bool
811 {
812 $optionName = $this->getReportOptionName($reportType);
813
814 if (get_option($optionName) !== 'true') {
815 return false;
816 }
817
818 $reportEmail = get_option(Notifications::OPTION_BACKUP_SCHEDULE_REPORT_EMAIL);
819 if (!filter_var($reportEmail, FILTER_VALIDATE_EMAIL)) {
820 return false;
821 }
822
823
824 $transientName = $this->getReportTransientName($reportType);
825 if (get_transient($transientName) !== false) {
826 return false;
827 }
828
829 if (empty($message)) {
830 return false;
831 }
832
833 if (empty($title)) {
834 $title = $this->getDefaultReportTitle($reportType);
835 }
836
837
838 $transientName = $this->getReportTransientName($reportType);
839 set_transient($transientName, true, 5 * 60);
840
841 if (get_option(Notifications::OPTION_SEND_EMAIL_AS_HTML, false) === 'true') {
842 return $this->notifications->sendEmailAsHTML($reportEmail, $title, $message);
843 }
844
845 return $this->notifications->sendEmail($reportEmail, $title, $message);
846 }
847
848
849
850
851
852
853
854
855
856 public function sendSlackReport(string $message, string $title = ''): bool
857 {
858 if (!WPStaging::isPro()) {
859 return false;
860 }
861
862 if (get_option(self::OPTION_BACKUP_SCHEDULE_SLACK_ERROR_REPORT) !== 'true') {
863 return false;
864 }
865
866 $webhook = get_option(self::OPTION_BACKUP_SCHEDULE_REPORT_SLACK_WEBHOOK);
867 if (!filter_var($webhook, FILTER_VALIDATE_URL)) {
868 return false;
869 }
870
871
872 if (get_transient(self::TRANSIENT_BACKUP_SCHEDULE_SLACK_REPORT_SENT) !== false) {
873 return false;
874 }
875
876 if (empty($message)) {
877 return false;
878 }
879
880 if (empty($title)) {
881 $title = esc_html__('WP Staging - Backup Report', 'wp-staging');
882 }
883
884
885 set_transient(self::TRANSIENT_BACKUP_SCHEDULE_SLACK_REPORT_SENT, true, 5 * 60);
886 return $this->notifications->sendSlack($webhook, $title, $message);
887 }
888
889
890
891
892
893
894
895 private function getReportOptionName(string $reportType): string
896 {
897 switch ($reportType) {
898 case self::REPORT_TYPE_WARNING:
899 return self::OPTION_BACKUP_SCHEDULE_WARNING_REPORT;
900 case self::REPORT_TYPE_GENERAL:
901 return self::OPTION_BACKUP_SCHEDULE_GENERAL_REPORT;
902 default:
903 return self::OPTION_BACKUP_SCHEDULE_ERROR_REPORT;
904 }
905 }
906
907
908
909
910
911
912
913 private function getReportTransientName(string $reportType): string
914 {
915 switch ($reportType) {
916 case self::REPORT_TYPE_WARNING:
917 return self::TRANSIENT_BACKUP_SCHEDULE_WARNING_REPORT_SENT;
918 case self::REPORT_TYPE_GENERAL:
919 return self::TRANSIENT_BACKUP_SCHEDULE_GENERAL_REPORT_SENT;
920 case self::REPORT_TYPE_ERROR:
921 default:
922 return self::TRANSIENT_BACKUP_SCHEDULE_ERROR_REPORT_SENT;
923 }
924 }
925
926
927
928
929
930
931
932 private function getDefaultReportTitle(string $reportType): string
933 {
934 switch ($reportType) {
935 case self::REPORT_TYPE_WARNING:
936 return esc_html__('WP Staging - Backup Warning Report', 'wp-staging');
937 case self::REPORT_TYPE_GENERAL:
938 return esc_html__('WP Staging - Backup General Report', 'wp-staging');
939 default:
940 return esc_html__('WP Staging - Backup Error Report', 'wp-staging');
941 }
942 }
943
944
945
946
947 private function isSchedulesEmpty(): bool
948 {
949 $schedules = get_option(static::OPTION_BACKUP_SCHEDULES, []);
950 if (empty($schedules)) {
951 return true;
952 }
953
954 return false;
955 }
956
957
958
959
960 private function getCronJobs(): array
961 {
962 $cron = get_option('cron');
963 if (!is_array($cron)) {
964 return [];
965 }
966
967 $cronJobs = [];
968 foreach ($cron as $timestamp => $hooks) {
969 if (!is_numeric($timestamp) || !is_array($hooks)) {
970 continue;
971 }
972
973 $cronJobs[(int)$timestamp] = $hooks;
974 }
975
976 return $cronJobs;
977 }
978
979
980
981
982 private function countOverdueCronjobs()
983 {
984 $cronJobs = $this->getCronJobs();
985 $timeNow = time();
986 foreach ($cronJobs as $expectedExecutionTime => $cronJob) {
987 if (($expectedExecutionTime + self::OVERDUE_GRACE_PERIOD) < $timeNow) {
988 $this->numberOverdueCronjobs++;
989 }
990 }
991 }
992
993
994
995
996
997
998
999
1000
1001 public function onBackgroundJobFailure(array $args)
1002 {
1003 $jobDataDto = isset($args['jobDataDto']) ? $args['jobDataDto'] : null;
1004 if (!($jobDataDto instanceof JobBackupDataDto)) {
1005 return;
1006 }
1007
1008 if (!$jobDataDto->isScheduledBackup()) {
1009 return;
1010 }
1011
1012 $errorMessage = isset($args['errorMessage']) ? (string)$args['errorMessage'] : '';
1013 $this->saveBackupFailure($errorMessage);
1014 }
1015
1016
1017
1018
1019
1020 private function saveBackupFailure(string $message)
1021 {
1022 update_option(self::OPTION_LAST_BACKUP_FAILURE, [
1023 'time' => time(),
1024 'message' => $message,
1025 ], false);
1026 }
1027
1028
1029
1030
1031
1032
1033
1034
1035 private function hasOverdueOrMissingBackupCronJob(): bool
1036 {
1037 $eventExists = false;
1038 $now = time();
1039
1040 foreach ($this->getCronJobs() as $timestamp => $hooks) {
1041 if (!isset($hooks[Cron::ACTION_CREATE_CRON_BACKUP])) {
1042 continue;
1043 }
1044
1045 $eventExists = true;
1046 if (($timestamp + self::OVERDUE_GRACE_PERIOD) < $now) {
1047 return true;
1048 }
1049 }
1050
1051 return !$eventExists;
1052 }
1053 }
1054