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