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