PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 3.3.1
WP STAGING – WordPress Backup, Restore, Migration & Clone v3.3.1
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 2 years ago BackgroundProcessing 2 years ago Dto 2 years ago Entity 2 years ago Exceptions 3 years ago Job 2 years ago Request 2 years ago Service 2 years ago Storage 2 years ago Task 2 years ago AfterRestore.php 3 years ago BackupDeleter.php 3 years ago BackupDownload.php 3 years ago BackupProcessLock.php 3 years ago BackupRepairer.php 3 years ago BackupRetentionHandler.php 2 years ago BackupScheduler.php 2 years ago BackupServiceProvider.php 2 years ago BackupValidator.php 2 years ago WithBackupIdentifier.php 2 years ago wpstgBackupHeader.txt 3 years ago
BackupScheduler.php
701 lines
1 <?php
2
3 namespace WPStaging\Backup;
4
5 use WPStaging\Core\WPStaging;
6 use WPStaging\Framework\Facades\Sanitize;
7 use WPStaging\Framework\Security\Capabilities;
8 use WPStaging\Framework\Security\Nonce;
9 use WPStaging\Framework\Utils\Times;
10 use WPStaging\Backup\BackgroundProcessing\Backup\PrepareBackup;
11 use WPStaging\Backup\Dto\Job\JobBackupDataDto;
12 use WPStaging\Backup\Service\BackupsFinder;
13 use WPStaging\Core\Cron\Cron;
14 use WPStaging\Framework\Facades\Escape;
15 use WPStaging\Framework\Utils\ServerVars;
16
17 use function WPStaging\functions\debug_log;
18
19 class BackupScheduler
20 {
21 const BACKUP_SCHEDULE_ERROR_REPORT_OPTION = 'wpstg_backup_schedules_send_error_report';
22
23 const BACKUP_SCHEDULE_REPORT_EMAIL_OPTION = 'wpstg_backup_schedules_report_email';
24
25 const BACKUP_SCHEDULE_REPORT_SENT_TRANSIENT = 'wpstg.backup.schedules.report_sent';
26
27 const OPTION_BACKUP_SCHEDULES = 'wpstg_backup_schedules';
28
29 protected $backupsFinder;
30
31 protected $processLock;
32
33 /** @var BackupDeleter */
34 protected $backupDeleter;
35
36 /**
37 * Store cron related message
38 * @var string
39 */
40 protected $cronMessage;
41
42 /** @var int */
43 protected $numberOverdueCronjobs = 0;
44
45 /**
46 * @param BackupsFinder $backupsFinder
47 * @param BackupProcessLock $processLock
48 * @param BackupDeleter $backupDeleter
49 */
50 public function __construct(BackupsFinder $backupsFinder, BackupProcessLock $processLock, BackupDeleter $backupDeleter)
51 {
52 $this->backupsFinder = $backupsFinder;
53 $this->processLock = $processLock;
54 $this->backupDeleter = $backupDeleter;
55 $this->countOverdueCronjobs();
56 }
57
58 /**
59 * @return array
60 */
61 public function getSchedules(): array
62 {
63 $schedules = get_option(static::OPTION_BACKUP_SCHEDULES, []);
64 if (is_array($schedules)) {
65 return $schedules;
66 }
67
68 return [];
69 }
70
71 /**
72 * @param JobBackupDataDto $jobBackupDataDto
73 * @return void
74 */
75 public function maybeDeleteOldBackups(JobBackupDataDto $jobBackupDataDto)
76 {
77 $scheduleId = $jobBackupDataDto->getScheduleId();
78
79 // Not a scheduled backup, nothing to do.
80 if (empty($scheduleId)) {
81 return;
82 }
83
84 $schedules = get_option(static::OPTION_BACKUP_SCHEDULES, []);
85
86 $schedule = array_filter($schedules, function ($schedule) use ($scheduleId) {
87 return $schedule['scheduleId'] == $scheduleId;
88 });
89
90 if (empty($schedule)) {
91 debug_log("Could not delete old backups for schedule ID $scheduleId as the schedule rotation plan was not found in the database.");
92 return;
93 }
94
95 $schedule = array_shift($schedule);
96
97 $maxAllowedBackupFiles = absint($schedule['rotation']);
98
99 $backupFiles = $this->backupsFinder->findBackupByScheduleId($scheduleId);
100
101 // Early bail: Not enough backups to trigger the rotation
102 if (count($backupFiles) < $maxAllowedBackupFiles) {
103 return;
104 }
105
106 // Sort backups, older first
107 uasort($backupFiles, function ($backup1, $backup2) {
108 /**
109 * @var \SplFileInfo $backup1
110 * @var \SplFileInfo $backup2
111 */
112 if ($backup1->getMTime() === $backup2->getMTime()) {
113 return 0;
114 }
115
116 return $backup1->getMTime() < $backup2->getMTime() ? -1 : 1;
117 });
118
119 // Make sure array indexes are correctly ordered.
120 $backupFiles = array_values($backupFiles);
121
122 // Get exceeding backups, including an extra one for the backup that will be created right now.
123 $backupFiles = array_slice($backupFiles, 0, max(1, count($backupFiles) - $maxAllowedBackupFiles + 1));
124
125 array_map(function ($file) {
126 $this->backupDeleter->clearErrors();
127 $this->backupDeleter->deleteBackup($file);
128 $errors = $this->backupDeleter->getErrors();
129 foreach ($errors as $error) {
130 debug_log('Tried to cleanup old backups for backup plan rotation, but couldn\'t delete file: ' . $error);
131 }
132 }, $backupFiles);
133 }
134
135 /**
136 * @param JobBackupDataDto $jobBackupDataDto
137 * @param $scheduleId
138 * @return void
139 * @throws \Exception
140 */
141 public function scheduleBackup(JobBackupDataDto $jobBackupDataDto, $scheduleId)
142 {
143 if (!isset(wp_get_schedules()[$jobBackupDataDto->getScheduleRecurrence()])) {
144 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));
145
146 return;
147 }
148
149 $firstSchedule = new \DateTime('now', wp_timezone());
150 $time = $jobBackupDataDto->getScheduleTime();
151 $this->setUpcomingDateTime($firstSchedule, $time);
152
153 $backupSchedule = [
154 'scheduleId' => $scheduleId,
155 'schedule' => $jobBackupDataDto->getScheduleRecurrence(),
156 'time' => $time,
157 'name' => $jobBackupDataDto->getName(),
158 'rotation' => $jobBackupDataDto->getScheduleRotation(),
159 'isExportingPlugins' => $jobBackupDataDto->getIsExportingPlugins(),
160 'isExportingMuPlugins' => $jobBackupDataDto->getIsExportingMuPlugins(),
161 'isExportingThemes' => $jobBackupDataDto->getIsExportingThemes(),
162 'isExportingUploads' => $jobBackupDataDto->getIsExportingUploads(),
163 'isExportingOtherWpContentFiles' => $jobBackupDataDto->getIsExportingOtherWpContentFiles(),
164 'isExportingDatabase' => $jobBackupDataDto->getIsExportingDatabase(),
165 'sitesToBackup' => $jobBackupDataDto->getSitesToBackup(),
166 'storages' => $jobBackupDataDto->getStorages(),
167 'firstSchedule' => $firstSchedule->getTimestamp(),
168 'isSmartExclusion' => $jobBackupDataDto->getIsSmartExclusion(),
169 'isExcludingSpamComments' => $jobBackupDataDto->getIsExcludingSpamComments(),
170 'isExcludingPostRevision' => $jobBackupDataDto->getIsExcludingPostRevision(),
171 'isExcludingDeactivatedPlugins' => $jobBackupDataDto->getIsExcludingDeactivatedPlugins(),
172 'isExcludingUnusedThemes' => $jobBackupDataDto->getIsExcludingUnusedThemes(),
173 'isExcludingLogs' => $jobBackupDataDto->getIsExcludingLogs(),
174 'isExcludingCaches' => $jobBackupDataDto->getIsExcludingCaches(),
175 'isWpCliRequest' => true, // should be true otherwise multisite backup will not work
176 ];
177
178 if (wp_next_scheduled('wpstg_create_cron_backup', [$backupSchedule])) {
179 debug_log('[Schedule Backup Cron] Early bailed when registering the cron to create a backup on a schedule, because it already exists');
180
181 return;
182 }
183
184 $this->registerScheduleInDb($backupSchedule);
185 $this->reCreateCron();
186 }
187
188 /**
189 * Registers a schedule in the Db.
190 * @param $backupSchedule
191 * @return bool false on error or if nothing would be updated
192 */
193 protected function registerScheduleInDb($backupSchedule): bool
194 {
195 $backupSchedules = get_option(static::OPTION_BACKUP_SCHEDULES, []);
196 if (!is_array($backupSchedules)) {
197 $backupSchedules = [];
198 }
199
200 $backupSchedules[] = $backupSchedule;
201
202 if (!update_option(static::OPTION_BACKUP_SCHEDULES, $backupSchedules, false)) {
203 debug_log('[Schedule Backup Cron] Could not update BackupSchedules DB option');
204 return false;
205 }
206
207 return true;
208 }
209
210 /**
211 * AJAX callback that processes the backup schedule.
212 *
213 * @param $backupData
214 */
215 public function createCronBackup($backupData)
216 {
217 // Cron is hell to debug, so let's log everything that happens.
218 $logId = wp_generate_password(4, false);
219
220 debug_log("[Schedule Backup Cron - $logId] Received request to create a backup using Cron. Backup Data: " . wp_json_encode($backupData));
221
222 try {
223 debug_log("[Schedule Backup Cron - $logId] Preparing job");
224 $jobId = WPStaging::make(PrepareBackup::class)->prepare($backupData);
225 debug_log("[Schedule Backup Cron - $logId] Successfully received a Job ID: $jobId");
226
227 if ($jobId instanceof \WP_Error) {
228 debug_log("[Schedule Backup Cron - $logId] Failed to create backup: " . $jobId->get_error_message());
229 }
230 } catch (\Exception $e) {
231 debug_log("[Schedule Backup Cron - $logId] Exception thrown while preparing the Backup: " . $e->getMessage());
232 }
233 }
234
235 /**
236 * Ajax callback to dismiss a schedule.
237 */
238 public function dismissSchedule()
239 {
240 if (!current_user_can((new Capabilities())->manageWPSTG())) {
241 return;
242 }
243
244 if (!(new Nonce())->requestHasValidNonce(Nonce::WPSTG_NONCE)) {
245 return;
246 }
247
248 if (empty($_POST['scheduleId'])) {
249 return;
250 }
251
252 try {
253 $this->deleteSchedule(Sanitize::sanitizeString($_POST['scheduleId']));
254 wp_send_json_success();
255 } catch (\Exception $e) {
256 wp_send_json_error($e->getMessage());
257 }
258 }
259
260 /**
261 * Deletes a backup schedule.
262 *
263 * @param string $scheduleId The schedule ID to delete.
264 */
265 public function deleteSchedule(string $scheduleId, $reCreateCron = true)
266 {
267 $schedules = $this->getSchedules();
268
269 $newSchedules = array_filter($schedules, function ($schedule) use ($scheduleId) {
270 return $schedule['scheduleId'] != $scheduleId;
271 });
272
273 if (!update_option(static::OPTION_BACKUP_SCHEDULES, $newSchedules, false)) {
274 debug_log('[Schedule Backup Cron] Could not update BackupSchedules DB option after removing schedule.');
275 throw new \RuntimeException('Could not unschedule event from Db.');
276 }
277
278 if ($reCreateCron === false) {
279 return;
280 }
281
282 $this->reCreateCron();
283 }
284
285 /**
286 * @return bool
287 * @throws \Exception
288 * @see OPTION_BACKUP_SCHEDULES The Db option that is the source of truth for Cron events.
289 * The backup schedule cron events are deleted and re-created
290 * based on what is in this db option.
291 *
292 * This way, we only care about preserving this option on Backup
293 * Restore or Push, and we don't have to worry about re-scheduling
294 * the Cron events or removing leftover schedules.
295 *
296 */
297
298 public function reCreateCron(): bool
299 {
300 $schedules = $this->getSchedules();
301 static::removeBackupSchedulesFromCron();
302
303 $errors = [];
304
305 foreach ($schedules as $schedule) {
306 $timeToSchedule = new \DateTime('now', wp_timezone());
307
308 /**
309 * New mechanism for recroning old jobs
310 */
311 if (isset(wp_get_schedules()[$schedule['schedule']]) && isset($schedule['firstSchedule'])) {
312 $this->setNextSchedulingDate($timeToSchedule, $schedule);
313 } else {
314 $this->setUpcomingDateTime($timeToSchedule, $schedule['time']);
315 }
316
317 /** @see BackupServiceProvider::enqueueAjaxListeners */
318 $result = wp_schedule_event($timeToSchedule->format('U'), $schedule['schedule'], 'wpstg_create_cron_backup', [$schedule]);
319
320 // Could not register Cron event.
321 // Log errors but keep trying for the other cron events or all of them will be lost
322 if ($result === false || $result instanceof \WP_Error) {
323 if ($result instanceof \WP_Error) {
324 $details = $result->get_error_message();
325 } else {
326 $details = '';
327 }
328
329 $error = '[Schedule Backup Cron] Failed to register the cron event wpstg_create_cron_backup. ' . $schedule['schedule'] . ' ' . $details;
330
331 $errors[] = $error;
332
333 debug_log($error);
334 }
335 }
336
337 if (!empty($errors)) {
338 return false;
339 }
340
341 return true;
342 }
343
344 /**
345 * Removes all backup schedule events from WordPress Cron array.
346 *
347 * This is static so that it can be called from WP STAGING deactivation hook
348 * without having to bootstrap the entire plugin.
349 *
350 * This is a low-level function that can run when WP STAGING has not been
351 * bootstrapped, so there's no autoload nor Container available.
352 */
353 public static function removeBackupSchedulesFromCron(): bool
354 {
355 $cron = get_option('cron');
356
357 // Bail: Unexpected value - should never happen.
358 if (!is_array($cron)) {
359 return false;
360 }
361
362 // Remove any backup schedules from Cron
363 foreach ($cron as $timestamp => &$events) {
364 if (is_array($events)) {
365 foreach ($events as $callback => &$args) {
366 if ($callback === 'wpstg_create_cron_backup') {
367 unset($cron[$timestamp][$callback]);
368 }
369 }
370 }
371 }
372
373 // After removing the backup schedule events,
374 // we might have timestamps with no events.
375 // So we remove any leftover timestamps that don't have any events.
376 $cron = array_filter($cron, function ($timestamps) {
377 return !empty($timestamps);
378 });
379
380 update_option('cron', $cron);
381
382 return true;
383 }
384
385 /**
386 * Check cron status whether it is working or not
387 * Logic is adopted from WP Crontrol plugin
388 *
389 * @return bool
390 */
391 public function checkCronStatus(): bool
392 {
393 global $wp_version;
394
395 $this->cronMessage = '';
396
397 if ($this->isCronjobsOverdue()) {
398 if (WPStaging::isPro()) {
399 $this->cronMessage .= sprintf(
400 __('There are %s scheduled WordPress tasks overdue. This means the WordPress cron jobs are not working properly, unless this a development site or no users are visiting this website. <a href="%s">Read this article</a> to find a solution.<br><br>', 'wp-staging'),
401 $this->numberOverdueCronjobs,
402 'https://wp-staging.com/docs/wp-cron-is-not-working-correctly/'
403 );
404
405 if (WPStaging::make(ServerVars::class)->isLitespeed()) {
406 $this->cronMessage .= sprintf(
407 Escape::escapeHtml(__('This site is using LiteSpeed server, this could prevent the scheduled backups from working properly. Please read <a href="%s" target="_blank">this article here</a> if the backup scheduling is not working properly.', 'wp-staging')),
408 'https://wp-staging.com/docs/scheduled-backups-do-not-work-hosting-company-uses-the-litespeed-webserver-fix-wp-cron/'
409 );
410 }
411 } else {
412 $this->cronMessage .= sprintf(
413 __('There are %s scheduled WordPress tasks overdue. This means the WordPress cron jobs are not working properly, unless this a development site or no users are visiting this website.<br> <a href="%s">Write to us in the forum</a> to get a solution for this issue from the WP STAGING support team.<br><br>', 'wp-staging'),
414 $this->numberOverdueCronjobs,
415 'https://wordpress.org/support/plugin/wp-staging/'
416 );
417
418 if (WPStaging::make(ServerVars::class)->isLitespeed()) {
419 $this->cronMessage .= sprintf(
420 Escape::escapeHtml(__('This site is using LiteSpeed server, this could prevent the scheduled backups from working properly. <a href="%s">Write to us in the forum</a> to get a solution for that issue.', 'wp-staging')),
421 'https://wordpress.org/support/plugin/wp-staging/'
422 );
423 }
424 }
425 }
426
427 // Third party plugins that handle crons
428 $thirdPartyCronPlugins = [
429 '\HM\Cavalcade\Plugin\Job' => 'Cavalcade',
430 '\Automattic\WP\Cron_Control\Main' => 'Cron Control',
431 '\KMM\KRoN\Core' => 'KMM KRoN',
432 ];
433
434 foreach ($thirdPartyCronPlugins as $class => $plugin) {
435 if (class_exists($class)) {
436 $this->cronMessage .= sprintf(
437 __('WP Cron is being managed by a third party plugin: %s plugin.', 'wp-staging'),
438 $plugin
439 );
440
441 return true;
442 }
443 }
444
445 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
446 if (WPStaging::isPro()) {
447 $this->cronMessage .= sprintf(
448 __('The background backup creation depends on WP-Cron but %s is set to %s in wp-config.php. Background processing might not work. Remove this constant or set its value to %s. Ignore this if you use an external cron job.', 'wp-staging'),
449 '<code>DISABLE_WP_CRON</code>',
450 '<code>true</code>',
451 '<code>false</code>'
452 );
453 } else {
454 $this->cronMessage .= sprintf(
455 __('The background backup creation depends on WP-Cron but %s is set to %s in wp-config.php. Background processing might not work. Remove this constant or set its value to %s. Ignore this if you use an external cron job. <a href="%s" target="_blank">Ask us in the forum</a> if you need more information.', 'wp-staging'),
456 '<code>DISABLE_WP_CRON</code>',
457 '<code>true</code>',
458 '<code>false</code>',
459 'https://wordpress.org/support/plugin/wp-staging/'
460 );
461 }
462
463 return true;
464 }
465
466 if (defined('ALTERNATE_WP_CRON') && ALTERNATE_WP_CRON) {
467 $this->cronMessage .= sprintf(
468 __('The constant %s is set to true.', 'wp-staging'),
469 'ALTERNATE_WP_CRON'
470 );
471
472 return true;
473 }
474
475 // Don't do the next time expensive checking if no schedules are set
476 if ($this->isSchedulesEmpty()) {
477 return true;
478 }
479
480 $sslverify = version_compare($wp_version, '4.0', '<');
481 $doingWpCron = sprintf('%.22F', microtime(true));
482 $urlEndpoint = add_query_arg('doing_wp_cron', $doingWpCron, site_url('wp-cron.php'));
483
484 $cronRequest = apply_filters('cron_request', [
485 'url' => $urlEndpoint,
486 'key' => $doingWpCron,
487 'args' => [
488 'timeout' => 10,
489 'blocking' => true,
490 'sslverify' => apply_filters('https_local_ssl_verify', $sslverify),
491 ],
492 ]);
493
494 $cronRequest['args']['blocking'] = true;
495
496 $result = wp_remote_post($cronRequest['url'], $cronRequest['args']);
497
498 if (is_wp_error($result)) {
499 $this->cronMessage .= "Can not create scheduled backups because cron jobs do not work on this site. Error: " . $result->get_error_message() . ". Can not reach endpoint: " . esc_url($urlEndpoint);
500 // Only send the error report mail if error is caused by WP STAGING
501 if ($this->isWpstgError()) {
502 $this->sendErrorReport($this->cronMessage);
503 }
504
505 return false;
506 }
507
508 if (wp_remote_retrieve_response_code($result) >= 300) {
509 $this->cronMessage .= sprintf(
510 __('Unexpected HTTP response code: %s. Cron jobs and backup schedule might still work, but we recommend checking the HTTP response of %s', 'wp-staging'),
511 intval(wp_remote_retrieve_response_code($result)),
512 esc_url($urlEndpoint)
513 );
514
515 return false;
516 }
517
518 return true;
519 }
520
521 /**
522 * @return bool
523 */
524 private function isCronjobsOverdue(): bool
525 {
526 return $this->numberOverdueCronjobs > 4;
527 }
528
529 /** @return string */
530 public function getCronMessage(): string
531 {
532 return $this->cronMessage;
533 }
534
535 /**
536 * @return array An array where the first item is the timestamp, and the second is the backup callback.
537 * @throws \Exception When there is no backup scheduled or one could not be found.
538 */
539 public function getNextBackupSchedule(): array
540 {
541 $cron = get_option('cron');
542
543 // Bail: Unexpected value - should never happen.
544 if (!is_array($cron)) {
545 throw new \UnexpectedValueException();
546 }
547
548 ksort($cron, SORT_NUMERIC);
549
550 // Remove any backup schedules from Cron
551 foreach ($cron as $timestamp => &$events) {
552 if (is_array($events)) {
553 foreach ($events as $callback => &$args) {
554 if ($callback === 'wpstg_create_cron_backup') {
555 return [$timestamp, $cron[$timestamp][$callback]];
556 }
557 }
558 }
559 }
560
561 // No results found
562 throw new \OutOfBoundsException();
563 }
564
565 /**
566 * Set date today or tomorrow for given DateTime object according to time
567 *
568 * @param \DateTime $datetime
569 * @param string|array $time
570 */
571 protected function setUpcomingDateTime(&$datetime, $time)
572 {
573 if (is_array($time)) {
574 $hourAndMinute = $time;
575 } else {
576 $hourAndMinute = explode(':', $time);
577 }
578
579 // The event should be scheduled later today or tomorrow? Compares "Hi (Hourminute)" timestamps to figure out.
580 if ((int)sprintf('%s%s', $hourAndMinute[0], $hourAndMinute[1]) < (int)$datetime->format('Hi')) {
581 $datetime->add(new \DateInterval('P1D'));
582 }
583
584 $datetime->setTime($hourAndMinute[0], $hourAndMinute[1]);
585 }
586
587 /**
588 * Set the next scheduling date for the schedule
589 *
590 * @param \DateTime $datetime
591 * @param array $schedule
592 */
593 protected function setNextSchedulingDate(&$datetime, array $schedule)
594 {
595 $next = $schedule['firstSchedule'];
596 $now = $datetime->getTimestamp();
597 if ($next >= $now) {
598 $this->setUpcomingDateTime($datetime, $schedule['time']);
599 return;
600 }
601
602 $recurrance = wp_get_schedules()[$schedule['schedule']];
603 while ($next < $now) {
604 $next += $recurrance['interval'];
605 }
606
607 $datetime->setTimestamp($next);
608 }
609
610 /**
611 * Detect whether the last error is caused by WP STAGING
612 *
613 * @return bool
614 */
615 protected function isWpstgError(): bool
616 {
617 $error = error_get_last();
618 if (!is_array($error)) {
619 return false;
620 }
621
622 return strpos($error['file'], WPSTG_PLUGIN_SLUG) !== false;
623 }
624
625 /**
626 * @param string $message
627 */
628 public function sendErrorReport(string $message)
629 {
630 if (get_option(self::BACKUP_SCHEDULE_ERROR_REPORT_OPTION) !== 'true') {
631 return;
632 }
633
634 $reportEmail = get_option(self::BACKUP_SCHEDULE_REPORT_EMAIL_OPTION);
635 if (!filter_var($reportEmail, FILTER_VALIDATE_EMAIL)) {
636 return;
637 }
638
639 // Only send the error report mail once every 5 minutes
640 if (get_transient(self::BACKUP_SCHEDULE_REPORT_SENT_TRANSIENT) !== false) {
641 return;
642 }
643
644 if (empty($message)) {
645 return;
646 }
647
648 if (strpos($message, 'index resource') !== false) {
649 $message .= "\r\n \r\n" . __("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');
650 }
651
652 $message .= "\r\n" . "--" . "\r\n" . __('This message was sent by the WP Staging plugin from the website', 'wp-staging') . ' ' . get_site_url();
653 $message .= "\r\n" . __('It was sent to the email address', 'wp-staging') . ' ' . $reportEmail . ' ' . __('which can be set up on ', 'wp-staging') . get_site_url() . '/wp-admin/admin.php?page=wpstg-settings';
654 $message .= "\r\n" . __('Please do not reply to this email.', 'wp-staging');
655
656 // Set the transient to prevent sending the error report mail again for 5 minutes
657 set_transient(self::BACKUP_SCHEDULE_REPORT_SENT_TRANSIENT, true, 5 * 60);
658 wp_mail($reportEmail, __('WP Staging - Backup Error Report', 'wp-staging'), $message, [], []);
659 }
660
661 /**
662 * @return bool
663 */
664 private function isSchedulesEmpty(): bool
665 {
666 $schedules = get_option(static::OPTION_BACKUP_SCHEDULES, []);
667 if (empty($schedules)) {
668 return true;
669 }
670
671 return false;
672 }
673
674 /**
675 * @return array
676 */
677 private function getCronJobs(): array
678 {
679 $cron = get_option('cron');
680 if (!is_array($cron)) {
681 return [];
682 }
683
684 return $cron;
685 }
686
687 /**
688 * @return void
689 */
690 private function countOverdueCronjobs()
691 {
692 $cronJobs = $this->getCronJobs();
693 $timeNow = time();
694 foreach ($cronJobs as $expectedExecutionTime => $cronJob) {
695 if ($expectedExecutionTime < $timeNow) {
696 $this->numberOverdueCronjobs++;
697 }
698 }
699 }
700 }
701