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