PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.2
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.2
4.11.2 4.11.1 4.11.0 4.10.0 4.9.5 4.9.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 / Service / AbstractBackgroundBackupRequest.php
wp-staging / Backup / Service Last commit date
Compression 2 weeks ago Database 2 weeks ago DirectoryExplorer 2 weeks ago AbstractBackgroundBackupRequest.php 3 days ago AbstractBackupsFinder.php 2 weeks ago AbstractExtractor.php 2 weeks ago AbstractServiceProvider.php 2 weeks ago Archiver.php 1 week ago BackupAssets.php 2 weeks ago BackupContent.php 2 weeks ago BackupMetadataEditor.php 1 week ago BackupMetadataReader.php 2 weeks ago BackupSigner.php 2 weeks ago BackupsDirectoryResolver.php 2 weeks ago BackupsFinder.php 2 weeks ago BeforeUpdateBackupRequest.php 2 weeks ago BeforeUpdateBackupsService.php 2 weeks ago Extractor.php 2 weeks ago FileBackupService.php 2 weeks ago FileBackupServiceProvider.php 2 weeks ago ServiceInterface.php 2 weeks ago TmpBackupCleaner.php 2 weeks ago UpdateProtectionHealth.php 2 weeks ago UpdateProtectionSettings.php 3 days ago ZlibCompressor.php 2 weeks ago
AbstractBackgroundBackupRequest.php
407 lines
1 <?php
2
3 namespace WPStaging\Backup\Service;
4
5 use WPStaging\Backup\BackgroundProcessing\Backup\PrepareBackup;
6 use WPStaging\Core\WPStaging;
7 use WPStaging\Framework\Analytics\Actions\AnalyticsGenericEvent;
8 use WPStaging\Framework\BackgroundProcessing\QueueProcessor;
9 use WPStaging\Framework\Job\Ajax\PrepareCancel;
10 use WPStaging\Framework\Job\BackgroundProcessing\PrepareCancel as BackgroundPrepareCancel;
11 use WPStaging\Framework\Job\JobTransientCache;
12
13 use function WPStaging\functions\debug_log;
14
15
16
17
18
19
20
21
22
23
24
25 abstract class AbstractBackgroundBackupRequest
26 {
27
28 const STATUS_QUEUED = 'queued';
29
30
31 const STATUS_RUNNING = 'running';
32
33 const STATUS_COMPLETED = 'completed';
34
35
36 const STATUS_FAILED = 'failed';
37
38
39
40
41
42
43 const MAX_QUEUED_AGE_IN_SECONDS = 6 * HOUR_IN_SECONDS;
44
45
46
47
48
49 const MAX_RUNNING_AGE_IN_SECONDS = DAY_IN_SECONDS;
50
51
52
53
54
55 const MAX_TERMINAL_AGE_IN_SECONDS = HOUR_IN_SECONDS;
56
57
58 const NUDGE_LOCK_IN_SECONDS = 10;
59
60
61 private $state = null;
62
63
64
65
66 abstract protected function getOptionName(): string;
67
68
69
70
71 abstract protected function getNudgeTransientName(): string;
72
73
74
75
76 abstract protected function getEventNames(): array;
77
78
79
80
81 abstract protected function getAnalyticsGroup(): string;
82
83
84
85
86 abstract protected function getLogContext(): string;
87
88
89
90
91
92 public function getStatus(): string
93 {
94 $state = $this->read();
95
96 return isset($state['status']) ? $state['status'] : '';
97 }
98
99
100
101
102 public function isPending(): bool
103 {
104 return in_array($this->getStatus(), [self::STATUS_QUEUED, self::STATUS_RUNNING], true);
105 }
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123 public function runWaitingWork()
124 {
125
126
127 if (!$this->isPending() || get_transient($this->getNudgeTransientName()) !== false) {
128 return;
129 }
130
131 set_transient($this->getNudgeTransientName(), time(), self::NUDGE_LOCK_IN_SECONDS);
132
133 try {
134 WPStaging::make(QueueProcessor::class)->process();
135 } catch (\Throwable $e) {
136 debug_log($this->getLogContext() . ': could not run the queued backup. ' . $e->getMessage(), 'debug', false);
137 }
138 }
139
140
141
142
143 public function markCompleted()
144 {
145 if ($this->getStatus() !== self::STATUS_RUNNING || !$this->isOwnBackupJob()) {
146 return;
147 }
148
149 $this->transition(self::STATUS_COMPLETED, 'completed_at', $this->getEventName('completed'));
150 }
151
152
153
154
155
156
157
158
159
160 public function onBackgroundJobFailure($failure = [])
161 {
162 $reporter = is_array($failure) && isset($failure['jobTransientCache']) ? $failure['jobTransientCache'] : null;
163
164 if ($this->getStatus() !== self::STATUS_RUNNING || !$this->isOwnBackupJob($reporter)) {
165 return;
166 }
167
168 $this->markFailed(is_array($failure) && isset($failure['errorMessage']) ? (string)$failure['errorMessage'] : '');
169 }
170
171
172
173
174
175 public function markFailed(string $reason = '')
176 {
177 if (!$this->isPending()) {
178 return;
179 }
180
181 if ($reason !== '') {
182 debug_log($this->getLogContext() . ': queued backup failed. ' . $reason, 'debug', false);
183 }
184
185 $this->transition(self::STATUS_FAILED, 'failed_at', $this->getEventName('failed'), [], $this->getFailureReport($reason));
186 $this->afterFailed($reason);
187 }
188
189
190
191
192
193
194
195
196
197
198 protected function cancelRunningBackup(): bool
199 {
200 if ($this->getStatus() !== self::STATUS_RUNNING || !$this->isOwnBackupJob()) {
201 return false;
202 }
203
204 $cancelledJob = WPStaging::make(PrepareCancel::class)->prepare([]);
205
206 if ($cancelledJob instanceof \WP_Error) {
207 debug_log($this->getLogContext() . ': could not cancel the running backup. ' . $cancelledJob->get_error_message(), 'debug', false);
208
209 return false;
210 }
211
212 $cleanup = WPStaging::make(BackgroundPrepareCancel::class)->prepareCleanup($cancelledJob);
213
214 if ($cleanup instanceof \WP_Error) {
215 debug_log($this->getLogContext() . ': the cancelled backup could not be cleaned up. ' . $cleanup->get_error_message(), 'debug', false);
216 }
217
218 return true;
219 }
220
221
222
223
224 public function clear()
225 {
226 $this->state = [];
227 delete_option($this->getOptionName());
228 }
229
230
231
232
233
234
235
236 protected function start(array $backupData): bool
237 {
238 $jobId = WPStaging::make(PrepareBackup::class)->prepare($backupData);
239
240 if (is_wp_error($jobId)) {
241 $this->markFailed($jobId->get_error_message());
242
243 return false;
244 }
245
246 $this->transition(self::STATUS_RUNNING, 'started_at', $this->getEventName('started'), [
247 'backup_job_id' => (string)$jobId,
248 ]);
249
250 $this->afterStarted();
251
252 return true;
253 }
254
255
256
257
258 protected function afterStarted()
259 {
260 }
261
262
263
264
265
266 protected function afterFailed(string $reason = '')
267 {
268 }
269
270
271
272
273
274
275
276
277
278
279 protected function getFailureReport(string $reason): array
280 {
281 return [];
282 }
283
284
285
286
287
288
289
290
291
292
293 protected function read(): array
294 {
295 if ($this->state !== null) {
296 return $this->state;
297 }
298
299 $state = get_option($this->getOptionName(), []);
300 $this->state = (is_array($state) && !empty($state['status'])) ? $state : [];
301
302 if ($this->state !== [] && $this->isStale($this->state)) {
303 $this->clear();
304 }
305
306 return $this->state;
307 }
308
309
310
311
312
313 protected function write(array $state)
314 {
315 $this->state = $state;
316 update_option($this->getOptionName(), $state, false);
317 }
318
319
320
321
322
323
324
325
326
327 protected function transition(string $status, string $timeKey, string $event, array $extra = [], array $eventData = [])
328 {
329 $this->write(array_merge($this->read(), $extra, [
330 'status' => $status,
331 $timeKey => time(),
332 ]));
333
334 AnalyticsGenericEvent::logEvent($event, $this->getAnalyticsGroup(), $eventData);
335 }
336
337
338
339
340
341 private function getEventName(string $transition): string
342 {
343 $events = $this->getEventNames();
344
345 return isset($events[$transition]) ? $events[$transition] : '';
346 }
347
348
349
350
351
352 private function isStale(array $state): bool
353 {
354 if ($state['status'] === self::STATUS_QUEUED) {
355 return $this->isOlderThan($state, ['queued_at'], self::MAX_QUEUED_AGE_IN_SECONDS);
356 }
357
358 if ($state['status'] === self::STATUS_RUNNING) {
359 return $this->isOlderThan($state, ['started_at', 'queued_at'], self::MAX_RUNNING_AGE_IN_SECONDS);
360 }
361
362 return $this->isOlderThan($state, ['completed_at', 'failed_at', 'started_at', 'queued_at'], self::MAX_TERMINAL_AGE_IN_SECONDS);
363 }
364
365
366
367
368
369
370
371 private function isOlderThan(array $state, array $timeKeys, int $maxAgeInSeconds): bool
372 {
373 foreach ($timeKeys as $timeKey) {
374 if (!empty($state[$timeKey])) {
375 return (int)$state[$timeKey] < time() - $maxAgeInSeconds;
376 }
377 }
378
379 return true;
380 }
381
382
383
384
385
386
387
388
389
390
391
392
393
394 protected function isOwnBackupJob($reporter = null): bool
395 {
396 $state = $this->read();
397 if (empty($state['backup_job_id'])) {
398 return false;
399 }
400
401 $cache = $reporter instanceof JobTransientCache ? $reporter : WPStaging::make(JobTransientCache::class);
402 $job = $cache->getJob();
403
404 return is_array($job) && isset($job['queueId']) && $job['queueId'] === $state['backup_job_id'];
405 }
406 }
407