PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 3.1.3
WP STAGING – WordPress Backup, Restore, Migration & Clone v3.1.3
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 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
693 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 protected $backupsFinder;
28
29 protected $processLock;
30
31 /** @var BackupDeleter */
32 protected $backupDeleter;
33
34 /**
35 * Store cron related message
36 * @var string
37 */
38 protected $cronMessage;
39
40 /** @var int */
41 protected $numberOverdueCronjobs = 0;
42
43 /**
44 * @param BackupsFinder $backupsFinder
45 * @param BackupProcessLock $processLock
46 * @param BackupDeleter $backupDeleter
47 */
48 public function __construct(BackupsFinder $backupsFinder, BackupProcessLock $processLock, BackupDeleter $backupDeleter)
49 {
50 $this->backupsFinder = $backupsFinder;
51 $this->processLock = $processLock;
52 $this->backupDeleter = $backupDeleter;
53 $this->countOverdueCronjobs();
54 }
55
56 const OPTION_BACKUP_SCHEDULES = 'wpstg_backup_schedules';
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 ];
169
170 if (wp_next_scheduled('wpstg_create_cron_backup', [$backupSchedule])) {
171 debug_log('[Schedule Backup Cron] Early bailed when registering the cron to create a backup on a schedule, because it already exists');
172
173 return;
174 }
175
176 $this->registerScheduleInDb($backupSchedule);
177 $this->reCreateCron();
178 }
179
180 /**
181 * Registers a schedule in the Db.
182 * @param $backupSchedule
183 * @return bool false on error or if nothing would be updated
184 */
185 protected function registerScheduleInDb($backupSchedule): bool
186 {
187 $backupSchedules = get_option(static::OPTION_BACKUP_SCHEDULES, []);
188 if (!is_array($backupSchedules)) {
189 $backupSchedules = [];
190 }
191
192 $backupSchedules[] = $backupSchedule;
193
194 if (!update_option(static::OPTION_BACKUP_SCHEDULES, $backupSchedules, false)) {
195 debug_log('[Schedule Backup Cron] Could not update BackupSchedules DB option');
196 return false;
197 }
198
199 return true;
200 }
201
202 /**
203 * AJAX callback that processes the backup schedule.
204 *
205 * @param $backupData
206 */
207 public function createCronBackup($backupData)
208 {
209 // Cron is hell to debug, so let's log everything that happens.
210 $logId = wp_generate_password(4, false);
211
212 debug_log("[Schedule Backup Cron - $logId] Received request to create a backup using Cron. Backup Data: " . wp_json_encode($backupData));
213
214 try {
215 debug_log("[Schedule Backup Cron - $logId] Preparing job");
216 $jobId = WPStaging::make(PrepareBackup::class)->prepare($backupData);
217 debug_log("[Schedule Backup Cron - $logId] Successfully received a Job ID: $jobId");
218
219 if ($jobId instanceof \WP_Error) {
220 debug_log("[Schedule Backup Cron - $logId] Failed to create backup: " . $jobId->get_error_message());
221 }
222 } catch (\Exception $e) {
223 debug_log("[Schedule Backup Cron - $logId] Exception thrown while preparing the Backup: " . $e->getMessage());
224 }
225 }
226
227 /**
228 * Ajax callback to dismiss a schedule.
229 */
230 public function dismissSchedule()
231 {
232 if (!current_user_can((new Capabilities())->manageWPSTG())) {
233 return;
234 }
235
236 if (!(new Nonce())->requestHasValidNonce(Nonce::WPSTG_NONCE)) {
237 return;
238 }
239
240 if (empty($_POST['scheduleId'])) {
241 return;
242 }
243
244 try {
245 $this->deleteSchedule(Sanitize::sanitizeString($_POST['scheduleId']));
246 wp_send_json_success();
247 } catch (\Exception $e) {
248 wp_send_json_error($e->getMessage());
249 }
250 }
251
252 /**
253 * Deletes a backup schedule.
254 *
255 * @param string $scheduleId The schedule ID to delete.
256 */
257 public function deleteSchedule(string $scheduleId, $reCreateCron = true)
258 {
259 $schedules = $this->getSchedules();
260
261 $newSchedules = array_filter($schedules, function ($schedule) use ($scheduleId) {
262 return $schedule['scheduleId'] != $scheduleId;
263 });
264
265 if (!update_option(static::OPTION_BACKUP_SCHEDULES, $newSchedules, false)) {
266 debug_log('[Schedule Backup Cron] Could not update BackupSchedules DB option after removing schedule.');
267 throw new \RuntimeException('Could not unschedule event from Db.');
268 }
269
270 if ($reCreateCron === false) {
271 return;
272 }
273
274 $this->reCreateCron();
275 }
276
277 /**
278 * @return bool
279 * @throws \Exception
280 * @see OPTION_BACKUP_SCHEDULES The Db option that is the source of truth for Cron events.
281 * The backup schedule cron events are deleted and re-created
282 * based on what is in this db option.
283 *
284 * This way, we only care about preserving this option on Backup
285 * Restore or Push, and we don't have to worry about re-scheduling
286 * the Cron events or removing leftover schedules.
287 *
288 */
289
290 public function reCreateCron(): bool
291 {
292 $schedules = $this->getSchedules();
293 static::removeBackupSchedulesFromCron();
294
295 $errors = [];
296
297 foreach ($schedules as $schedule) {
298 $timeToSchedule = new \DateTime('now', wp_timezone());
299
300 /**
301 * New mechanism for recroning old jobs
302 */
303 if (isset(wp_get_schedules()[$schedule['schedule']]) && isset($schedule['firstSchedule'])) {
304 $this->setNextSchedulingDate($timeToSchedule, $schedule);
305 } else {
306 $this->setUpcomingDateTime($timeToSchedule, $schedule['time']);
307 }
308
309 /** @see BackupServiceProvider::enqueueAjaxListeners */
310 $result = wp_schedule_event($timeToSchedule->format('U'), $schedule['schedule'], 'wpstg_create_cron_backup', [$schedule]);
311
312 // Could not register Cron event.
313 // Log errors but keep trying for the other cron events or all of them will be lost
314 if ($result === false || $result instanceof \WP_Error) {
315 if ($result instanceof \WP_Error) {
316 $details = $result->get_error_message();
317 } else {
318 $details = '';
319 }
320
321 $error = '[Schedule Backup Cron] Failed to register the cron event wpstg_create_cron_backup. ' . $schedule['schedule'] . ' ' . $details;
322
323 $errors[] = $error;
324
325 debug_log($error);
326 }
327 }
328
329 if (!empty($errors)) {
330 return false;
331 }
332
333 return true;
334 }
335
336 /**
337 * Removes all backup schedule events from WordPress Cron array.
338 *
339 * This is static so that it can be called from WP STAGING deactivation hook
340 * without having to bootstrap the entire plugin.
341 *
342 * This is a low-level function that can run when WP STAGING has not been
343 * bootstrapped, so there's no autoload nor Container available.
344 */
345 public static function removeBackupSchedulesFromCron(): bool
346 {
347 $cron = get_option('cron');
348
349 // Bail: Unexpected value - should never happen.
350 if (!is_array($cron)) {
351 return false;
352 }
353
354 // Remove any backup schedules from Cron
355 foreach ($cron as $timestamp => &$events) {
356 if (is_array($events)) {
357 foreach ($events as $callback => &$args) {
358 if ($callback === 'wpstg_create_cron_backup') {
359 unset($cron[$timestamp][$callback]);
360 }
361 }
362 }
363 }
364
365 // After removing the backup schedule events,
366 // we might have timestamps with no events.
367 // So we remove any leftover timestamps that don't have any events.
368 $cron = array_filter($cron, function ($timestamps) {
369 return !empty($timestamps);
370 });
371
372 update_option('cron', $cron);
373
374 return true;
375 }
376
377 /**
378 * Check cron status whether it is working or not
379 * Logic is adopted from WP Crontrol plugin
380 *
381 * @return bool
382 */
383 public function checkCronStatus(): bool
384 {
385 global $wp_version;
386
387 $this->cronMessage = '';
388
389 if ($this->isCronjobsOverdue()) {
390 if (WPStaging::isPro()) {
391 $this->cronMessage .= sprintf(
392 __('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'),
393 $this->numberOverdueCronjobs,
394 'https://wp-staging.com/docs/wp-cron-is-not-working-correctly/'
395 );
396
397 if (WPStaging::make(ServerVars::class)->isLitespeed()) {
398 $this->cronMessage .= sprintf(
399 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')),
400 'https://wp-staging.com/docs/scheduled-backups-do-not-work-hosting-company-uses-the-litespeed-webserver-fix-wp-cron/'
401 );
402 }
403 } else {
404 $this->cronMessage .= sprintf(
405 __('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'),
406 $this->numberOverdueCronjobs,
407 'https://wordpress.org/support/plugin/wp-staging/'
408 );
409
410 if (WPStaging::make(ServerVars::class)->isLitespeed()) {
411 $this->cronMessage .= sprintf(
412 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')),
413 'https://wordpress.org/support/plugin/wp-staging/'
414 );
415 }
416 }
417 }
418
419 // Third party plugins that handle crons
420 $thirdPartyCronPlugins = [
421 '\HM\Cavalcade\Plugin\Job' => 'Cavalcade',
422 '\Automattic\WP\Cron_Control\Main' => 'Cron Control',
423 '\KMM\KRoN\Core' => 'KMM KRoN',
424 ];
425
426 foreach ($thirdPartyCronPlugins as $class => $plugin) {
427 if (class_exists($class)) {
428 $this->cronMessage .= sprintf(
429 __('WP Cron is being managed by a third party plugin: %s plugin.', 'wp-staging'),
430 $plugin
431 );
432
433 return true;
434 }
435 }
436
437 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
438 if (WPStaging::isPro()) {
439 $this->cronMessage .= sprintf(
440 __('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'),
441 '<code>DISABLE_WP_CRON</code>',
442 '<code>true</code>',
443 '<code>false</code>'
444 );
445 } else {
446 $this->cronMessage .= sprintf(
447 __('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'),
448 '<code>DISABLE_WP_CRON</code>',
449 '<code>true</code>',
450 '<code>false</code>',
451 'https://wordpress.org/support/plugin/wp-staging/'
452 );
453 }
454
455 return true;
456 }
457
458 if (defined('ALTERNATE_WP_CRON') && ALTERNATE_WP_CRON) {
459 $this->cronMessage .= sprintf(
460 __('The constant %s is set to true.', 'wp-staging'),
461 'ALTERNATE_WP_CRON'
462 );
463
464 return true;
465 }
466
467 // Don't do the next time expensive checking if no schedules are set
468 if ($this->isSchedulesEmpty()) {
469 return true;
470 }
471
472 $sslverify = version_compare($wp_version, '4.0', '<');
473 $doingWpCron = sprintf('%.22F', microtime(true));
474 $urlEndpoint = add_query_arg('doing_wp_cron', $doingWpCron, site_url('wp-cron.php'));
475
476 $cronRequest = apply_filters('cron_request', [
477 'url' => $urlEndpoint,
478 'key' => $doingWpCron,
479 'args' => [
480 'timeout' => 10,
481 'blocking' => true,
482 'sslverify' => apply_filters('https_local_ssl_verify', $sslverify),
483 ],
484 ]);
485
486 $cronRequest['args']['blocking'] = true;
487
488 $result = wp_remote_post($cronRequest['url'], $cronRequest['args']);
489
490 if (is_wp_error($result)) {
491 $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);
492 // Only send the error report mail if error is caused by WP STAGING
493 if ($this->isWpstgError()) {
494 $this->sendErrorReport($this->cronMessage);
495 }
496
497 return false;
498 }
499
500 if (wp_remote_retrieve_response_code($result) >= 300) {
501 $this->cronMessage .= sprintf(
502 __('Unexpected HTTP response code: %s. Cron jobs and backup schedule might still work, but we recommend checking the HTTP response of %s', 'wp-staging'),
503 intval(wp_remote_retrieve_response_code($result)),
504 esc_url($urlEndpoint)
505 );
506
507 return false;
508 }
509
510 return true;
511 }
512
513 /**
514 * @return bool
515 */
516 private function isCronjobsOverdue(): bool
517 {
518 return $this->numberOverdueCronjobs > 4;
519 }
520
521 /** @return string */
522 public function getCronMessage(): string
523 {
524 return $this->cronMessage;
525 }
526
527 /**
528 * @return array An array where the first item is the timestamp, and the second is the backup callback.
529 * @throws \Exception When there is no backup scheduled or one could not be found.
530 */
531 public function getNextBackupSchedule(): array
532 {
533 $cron = get_option('cron');
534
535 // Bail: Unexpected value - should never happen.
536 if (!is_array($cron)) {
537 throw new \UnexpectedValueException();
538 }
539
540 ksort($cron, SORT_NUMERIC);
541
542 // Remove any backup schedules from Cron
543 foreach ($cron as $timestamp => &$events) {
544 if (is_array($events)) {
545 foreach ($events as $callback => &$args) {
546 if ($callback === 'wpstg_create_cron_backup') {
547 return [$timestamp, $cron[$timestamp][$callback]];
548 }
549 }
550 }
551 }
552
553 // No results found
554 throw new \OutOfBoundsException();
555 }
556
557 /**
558 * Set date today or tomorrow for given DateTime object according to time
559 *
560 * @param \DateTime $datetime
561 * @param string|array $time
562 */
563 protected function setUpcomingDateTime(&$datetime, $time)
564 {
565 if (is_array($time)) {
566 $hourAndMinute = $time;
567 } else {
568 $hourAndMinute = explode(':', $time);
569 }
570
571 // The event should be scheduled later today or tomorrow? Compares "Hi (Hourminute)" timestamps to figure out.
572 if ((int)sprintf('%s%s', $hourAndMinute[0], $hourAndMinute[1]) < (int)$datetime->format('Hi')) {
573 $datetime->add(new \DateInterval('P1D'));
574 }
575
576 $datetime->setTime($hourAndMinute[0], $hourAndMinute[1]);
577 }
578
579 /**
580 * Set the next scheduling date for the schedule
581 *
582 * @param \DateTime $datetime
583 * @param array $schedule
584 */
585 protected function setNextSchedulingDate(&$datetime, array $schedule)
586 {
587 $next = $schedule['firstSchedule'];
588 $now = $datetime->getTimestamp();
589 if ($next >= $now) {
590 $this->setUpcomingDateTime($datetime, $schedule['time']);
591 return;
592 }
593
594 $recurrance = wp_get_schedules()[$schedule['schedule']];
595 while ($next < $now) {
596 $next += $recurrance['interval'];
597 }
598
599 $datetime->setTimestamp($next);
600 }
601
602 /**
603 * Detect whether the last error is caused by WP STAGING
604 *
605 * @return bool
606 */
607 protected function isWpstgError(): bool
608 {
609 $error = error_get_last();
610 if (!is_array($error)) {
611 return false;
612 }
613
614 return strpos($error['file'], WPSTG_PLUGIN_SLUG) !== false;
615 }
616
617 /**
618 * @param string $message
619 */
620 public function sendErrorReport(string $message)
621 {
622 if (get_option(self::BACKUP_SCHEDULE_ERROR_REPORT_OPTION) !== 'true') {
623 return;
624 }
625
626 $reportEmail = get_option(self::BACKUP_SCHEDULE_REPORT_EMAIL_OPTION);
627 if (!filter_var($reportEmail, FILTER_VALIDATE_EMAIL)) {
628 return;
629 }
630
631 // Only send the error report mail once every 5 minutes
632 if (get_transient(self::BACKUP_SCHEDULE_REPORT_SENT_TRANSIENT) !== false) {
633 return;
634 }
635
636 if (empty($message)) {
637 return;
638 }
639
640 if (strpos($message, 'index resource') !== false) {
641 $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');
642 }
643
644 $message .= "\r\n" . "--" . "\r\n" . __('This message was sent by the WP Staging plugin from the website', 'wp-staging') . ' ' . get_site_url();
645 $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';
646 $message .= "\r\n" . __('Please do not reply to this email.', 'wp-staging');
647
648 // Set the transient to prevent sending the error report mail again for 5 minutes
649 set_transient(self::BACKUP_SCHEDULE_REPORT_SENT_TRANSIENT, true, 5 * 60);
650 wp_mail($reportEmail, __('WP Staging - Backup Error Report', 'wp-staging'), $message, [], []);
651 }
652
653 /**
654 * @return bool
655 */
656 private function isSchedulesEmpty(): bool
657 {
658 $schedules = get_option(static::OPTION_BACKUP_SCHEDULES, []);
659 if (empty($schedules)) {
660 return true;
661 }
662
663 return false;
664 }
665
666 /**
667 * @return array
668 */
669 private function getCronJobs(): array
670 {
671 $cron = get_option('cron');
672 if (!is_array($cron)) {
673 return [];
674 }
675
676 return $cron;
677 }
678
679 /**
680 * @return void
681 */
682 private function countOverdueCronjobs()
683 {
684 $cronJobs = $this->getCronJobs();
685 $timeNow = time();
686 foreach ($cronJobs as $expectedExecutionTime => $cronJob) {
687 if ($expectedExecutionTime < $timeNow) {
688 $this->numberOverdueCronjobs++;
689 }
690 }
691 }
692 }
693