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