PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.2
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.2
4.11.2 4.11.1 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 week ago BackgroundProcessing 2 weeks ago Dto 1 week ago Entity 2 weeks ago Exceptions 4 days ago FileHeader 2 weeks ago Interfaces 2 weeks ago Job 4 days ago Request 2 weeks ago Service 4 days ago Storage 2 weeks ago Task 4 days ago Traits 2 weeks ago Utils 2 weeks ago AdminMenuBadge.php 4 days ago AfterRestore.php 2 weeks ago BackupDeleter.php 2 weeks ago BackupDownload.php 2 weeks ago BackupFileIndex.php 2 weeks ago BackupGlitchReason.php 2 weeks ago BackupHeader.php 1 week ago BackupNextOffer.php 4 days ago BackupRepairer.php 2 weeks ago BackupRetentionHandler.php 2 weeks ago BackupScheduler.php 4 days ago BackupServiceProvider.php 2 weeks ago BackupValidator.php 1 week ago BeforeUpdateRowStatus.php 2 weeks ago FileHeader.php 2 weeks ago FileHeaderAttribute.php 2 weeks ago UpdateProtectionPausedNotice.php 2 weeks ago WithBackupIdentifier.php 2 weeks ago
BackupScheduler.php
1065 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
607
608 public function shouldShowMenuBadge(): bool
609 {
610 return !$this->checkCronStatus();
611 }
612
613
614
615
616
617 public function getNextBackupSchedule(): array
618 {
619 $cron = get_option('cron');
620
621
622 if (!is_array($cron)) {
623 throw new \UnexpectedValueException();
624 }
625
626 ksort($cron, SORT_NUMERIC);
627
628
629 foreach ($cron as $timestamp => &$events) {
630 if (is_array($events)) {
631 foreach ($events as $callback => &$args) {
632 if ($callback === Cron::ACTION_CREATE_CRON_BACKUP) {
633 return [$timestamp, $cron[$timestamp][$callback]];
634 }
635 }
636 }
637 }
638
639
640 throw new \OutOfBoundsException();
641 }
642
643
644
645
646
647
648
649
650
651
652 protected function setUpcomingDateTime(DateTime &$datetime, $time, $dayOfWeek = null, $scheduleRecurrence = null)
653 {
654 if (is_array($time)) {
655 $hourAndMinute = $time;
656 } else {
657 $hourAndMinute = explode(':', $time);
658 }
659
660
661 $isWeeklySchedule = $scheduleRecurrence === Cron::WEEKLY ||
662 $scheduleRecurrence === Cron::EVERY_TWO_WEEKS ||
663 strpos($scheduleRecurrence, Cron::WEEKLY . '_') === 0;
664
665 if ($dayOfWeek !== null && $isWeeklySchedule) {
666
667
668 $currentDayOfWeek = (int)$datetime->format('N');
669 $targetDayOfWeek = (int)$dayOfWeek;
670
671
672 $targetTimeInt = (int) sprintf('%02d%02d', $hourAndMinute[0], $hourAndMinute[1]);
673 $currentTimeInt = (int) $datetime->format('Hi');
674 $daysUntilTarget = $targetDayOfWeek - $currentDayOfWeek;
675
676 if ($daysUntilTarget < 0) {
677 $daysUntilTarget += 7;
678 }
679
680
681 if ($daysUntilTarget === 0 && $targetTimeInt <= $currentTimeInt) {
682 $daysUntilTarget = 7;
683 }
684
685
686 if ($daysUntilTarget > 0) {
687 $datetime->add(new \DateInterval("P{$daysUntilTarget}D"));
688 }
689 } else {
690
691 if ((int)sprintf('%02d%02d', $hourAndMinute[0], $hourAndMinute[1]) <= (int)$datetime->format('Hi')) {
692 $datetime->add(new \DateInterval('P1D'));
693 }
694 }
695
696 $datetime->setTime($hourAndMinute[0], $hourAndMinute[1]);
697 }
698
699
700
701
702
703
704
705
706 protected function setNextSchedulingDate(DateTime &$datetime, array $schedule)
707 {
708 $next = $schedule['firstSchedule'];
709 $now = $datetime->getTimestamp();
710 if ($next >= $now) {
711 $dayOfWeek = Cron::extractDayFromSchedule($schedule['schedule']);
712 $this->setUpcomingDateTime($datetime, $schedule['time'], $dayOfWeek, $schedule['schedule']);
713 return;
714 }
715
716 $recurrance = wp_get_schedules()[$schedule['schedule']];
717 while ($next < $now) {
718 $next += $recurrance['interval'];
719 }
720
721 $datetime->setTimestamp($next);
722 }
723
724
725
726
727
728
729
730
731
732
733 public function sendErrorReport(string $message, string $title = ''): bool
734 {
735 if (get_option(self::OPTION_BACKUP_SCHEDULE_ERROR_REPORT) !== 'true') {
736 return false;
737 }
738
739 if (empty($message)) {
740 return false;
741 }
742
743 if (strpos($message, 'index resource') !== false) {
744 $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');
745 }
746
747 if (empty($title)) {
748 $title = esc_html__('WP Staging - Backup Error Report', 'wp-staging');
749 }
750
751 $this->sendEmailReport($message, $title);
752 $this->sendSlackReport($message, $title);
753
754 return true;
755 }
756
757
758
759
760
761
762
763
764
765
766 public function sendWarningReport(string $message, string $title = ''): bool
767 {
768 if (get_option(self::OPTION_BACKUP_SCHEDULE_WARNING_REPORT) !== 'true') {
769 return false;
770 }
771
772 if (empty($message)) {
773 return false;
774 }
775
776 if (empty($title)) {
777 $title = esc_html__('WP Staging - Backup Warning Report', 'wp-staging');
778 }
779
780 $this->sendEmailReport($message, $title, self::REPORT_TYPE_WARNING);
781
782 return true;
783 }
784
785
786
787
788
789
790
791
792
793
794 public function sendGeneralReport(string $message, string $title = ''): bool
795 {
796 if (get_option(self::OPTION_BACKUP_SCHEDULE_GENERAL_REPORT) !== 'true') {
797 return false;
798 }
799
800 if (empty($message)) {
801 return false;
802 }
803
804 if (empty($title)) {
805 $title = esc_html__('WP Staging - Backup General Report', 'wp-staging');
806 }
807
808 $this->sendEmailReport($message, $title, self::REPORT_TYPE_GENERAL);
809
810 return true;
811 }
812
813
814
815
816
817
818
819
820
821 public function sendEmailReport(string $message, string $title = '', string $reportType = self::REPORT_TYPE_ERROR): bool
822 {
823 $optionName = $this->getReportOptionName($reportType);
824
825 if (get_option($optionName) !== 'true') {
826 return false;
827 }
828
829 $reportEmail = get_option(Notifications::OPTION_BACKUP_SCHEDULE_REPORT_EMAIL);
830 if (!filter_var($reportEmail, FILTER_VALIDATE_EMAIL)) {
831 return false;
832 }
833
834
835 $transientName = $this->getReportTransientName($reportType);
836 if (get_transient($transientName) !== false) {
837 return false;
838 }
839
840 if (empty($message)) {
841 return false;
842 }
843
844 if (empty($title)) {
845 $title = $this->getDefaultReportTitle($reportType);
846 }
847
848
849 $transientName = $this->getReportTransientName($reportType);
850 set_transient($transientName, true, 5 * 60);
851
852 if (get_option(Notifications::OPTION_SEND_EMAIL_AS_HTML, false) === 'true') {
853 return $this->notifications->sendEmailAsHTML($reportEmail, $title, $message);
854 }
855
856 return $this->notifications->sendEmail($reportEmail, $title, $message);
857 }
858
859
860
861
862
863
864
865
866
867 public function sendSlackReport(string $message, string $title = ''): bool
868 {
869 if (!WPStaging::isPro()) {
870 return false;
871 }
872
873 if (get_option(self::OPTION_BACKUP_SCHEDULE_SLACK_ERROR_REPORT) !== 'true') {
874 return false;
875 }
876
877 $webhook = get_option(self::OPTION_BACKUP_SCHEDULE_REPORT_SLACK_WEBHOOK);
878 if (!filter_var($webhook, FILTER_VALIDATE_URL)) {
879 return false;
880 }
881
882
883 if (get_transient(self::TRANSIENT_BACKUP_SCHEDULE_SLACK_REPORT_SENT) !== false) {
884 return false;
885 }
886
887 if (empty($message)) {
888 return false;
889 }
890
891 if (empty($title)) {
892 $title = esc_html__('WP Staging - Backup Report', 'wp-staging');
893 }
894
895
896 set_transient(self::TRANSIENT_BACKUP_SCHEDULE_SLACK_REPORT_SENT, true, 5 * 60);
897 return $this->notifications->sendSlack($webhook, $title, $message);
898 }
899
900
901
902
903
904
905
906 private function getReportOptionName(string $reportType): string
907 {
908 switch ($reportType) {
909 case self::REPORT_TYPE_WARNING:
910 return self::OPTION_BACKUP_SCHEDULE_WARNING_REPORT;
911 case self::REPORT_TYPE_GENERAL:
912 return self::OPTION_BACKUP_SCHEDULE_GENERAL_REPORT;
913 default:
914 return self::OPTION_BACKUP_SCHEDULE_ERROR_REPORT;
915 }
916 }
917
918
919
920
921
922
923
924 private function getReportTransientName(string $reportType): string
925 {
926 switch ($reportType) {
927 case self::REPORT_TYPE_WARNING:
928 return self::TRANSIENT_BACKUP_SCHEDULE_WARNING_REPORT_SENT;
929 case self::REPORT_TYPE_GENERAL:
930 return self::TRANSIENT_BACKUP_SCHEDULE_GENERAL_REPORT_SENT;
931 case self::REPORT_TYPE_ERROR:
932 default:
933 return self::TRANSIENT_BACKUP_SCHEDULE_ERROR_REPORT_SENT;
934 }
935 }
936
937
938
939
940
941
942
943 private function getDefaultReportTitle(string $reportType): string
944 {
945 switch ($reportType) {
946 case self::REPORT_TYPE_WARNING:
947 return esc_html__('WP Staging - Backup Warning Report', 'wp-staging');
948 case self::REPORT_TYPE_GENERAL:
949 return esc_html__('WP Staging - Backup General Report', 'wp-staging');
950 default:
951 return esc_html__('WP Staging - Backup Error Report', 'wp-staging');
952 }
953 }
954
955
956
957
958 private function isSchedulesEmpty(): bool
959 {
960 $schedules = get_option(static::OPTION_BACKUP_SCHEDULES, []);
961 if (empty($schedules)) {
962 return true;
963 }
964
965 return false;
966 }
967
968
969
970
971 private function getCronJobs(): array
972 {
973 $cron = get_option('cron');
974 if (!is_array($cron)) {
975 return [];
976 }
977
978 $cronJobs = [];
979 foreach ($cron as $timestamp => $hooks) {
980 if (!is_numeric($timestamp) || !is_array($hooks)) {
981 continue;
982 }
983
984 $cronJobs[(int)$timestamp] = $hooks;
985 }
986
987 return $cronJobs;
988 }
989
990
991
992
993 private function countOverdueCronjobs()
994 {
995 $cronJobs = $this->getCronJobs();
996 $timeNow = time();
997 foreach ($cronJobs as $expectedExecutionTime => $cronJob) {
998 if (($expectedExecutionTime + self::OVERDUE_GRACE_PERIOD) < $timeNow) {
999 $this->numberOverdueCronjobs++;
1000 }
1001 }
1002 }
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012 public function onBackgroundJobFailure(array $args)
1013 {
1014 $jobDataDto = isset($args['jobDataDto']) ? $args['jobDataDto'] : null;
1015 if (!($jobDataDto instanceof JobBackupDataDto)) {
1016 return;
1017 }
1018
1019 if (!$jobDataDto->isScheduledBackup()) {
1020 return;
1021 }
1022
1023 $errorMessage = isset($args['errorMessage']) ? (string)$args['errorMessage'] : '';
1024 $this->saveBackupFailure($errorMessage);
1025 }
1026
1027
1028
1029
1030
1031 private function saveBackupFailure(string $message)
1032 {
1033 update_option(self::OPTION_LAST_BACKUP_FAILURE, [
1034 'time' => time(),
1035 'message' => $message,
1036 ], false);
1037 }
1038
1039
1040
1041
1042
1043
1044
1045
1046 private function hasOverdueOrMissingBackupCronJob(): bool
1047 {
1048 $eventExists = false;
1049 $now = time();
1050
1051 foreach ($this->getCronJobs() as $timestamp => $hooks) {
1052 if (!isset($hooks[Cron::ACTION_CREATE_CRON_BACKUP])) {
1053 continue;
1054 }
1055
1056 $eventExists = true;
1057 if (($timestamp + self::OVERDUE_GRACE_PERIOD) < $now) {
1058 return true;
1059 }
1060 }
1061
1062 return !$eventExists;
1063 }
1064 }
1065