PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.0
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 / Framework / Onboarding / OnboardingJourney.php
wp-staging / Framework / Onboarding Last commit date
BackupPluginsDetector.php 1 day ago FirstInstall.php 1 day ago FreeOnboarding.php 1 day ago NextStepRenderer.php 1 day ago OnboardingAjax.php 1 day ago OnboardingJourney.php 1 day ago QueuedBackup.php 1 day ago
OnboardingJourney.php
531 lines
1 <?php
2
3 namespace WPStaging\Framework\Onboarding;
4
5 use WPStaging\Core\WPStaging;
6 use WPStaging\Framework\Analytics\Actions\AnalyticsGenericEvent;
7 use WPStaging\Framework\Security\Auth;
8
9
10
11
12
13
14
15 class OnboardingJourney
16 {
17 const OPTION_STATE = 'wpstg_onboarding_journey';
18
19
20 const OPTION_COMPLETED = 'wpstg_onboarding_completed';
21
22
23
24
25
26
27
28 const OPTION_RESTARTED = 'wpstg_onboarding_restarted';
29
30 const STEP_SELECT = 'select';
31 const STEP_RUNNING = 'running';
32 const STEP_NEXT = 'next';
33 const STEP_DONE = 'done';
34
35 const POSITION_FIRST = 'first';
36 const POSITION_SECOND = 'second';
37
38 const CAPABILITY_STAGING = 'staging';
39 const CAPABILITY_BACKUP = 'backup';
40 const CAPABILITIES = [self::CAPABILITY_STAGING, self::CAPABILITY_BACKUP];
41
42 const REASON_SKIPPED_INITIAL = 'skipped_initial';
43 const REASON_DONE_AFTER_FIRST = 'done_after_first_capability';
44 const REASON_TWO_CAPABILITIES = 'two_capabilities_completed';
45 const REASON_OTHER_EXPLICIT_EXIT = 'other_explicit_exit';
46
47
48 const SURFACE_POST_SUCCESS = 'post_success';
49
50
51 const SURFACE_STAGING_PROGRESS = 'staging_progress';
52
53 const EVENT_ACTION_STARTED = 'onboarding_action_started';
54 const EVENT_ACTION_COMPLETED = 'onboarding_action_completed';
55 const EVENT_ACTION_CANCELLED = 'onboarding_action_cancelled';
56 const EVENT_NEXT_OFFER_SHOWN = 'onboarding_next_offer_shown';
57 const EVENT_COMPLETED = 'onboarding_completed';
58
59
60 const MAX_RUNNING_AGE_IN_SECONDS = DAY_IN_SECONDS;
61
62
63 private $state = null;
64
65
66 private $completionReason = null;
67
68
69
70
71 public function getStep(): string
72 {
73 if ($this->isCompleted()) {
74 return self::STEP_DONE;
75 }
76
77 if ($this->getAction(self::POSITION_FIRST) === '') {
78 return self::STEP_SELECT;
79 }
80
81 return $this->activePosition() === '' ? self::STEP_NEXT : self::STEP_RUNNING;
82 }
83
84
85
86
87
88 public function getAction(string $position): string
89 {
90 return $this->read($position, 'action');
91 }
92
93
94
95
96 public function getNextCapability(): string
97 {
98 $first = $this->getAction(self::POSITION_FIRST);
99
100 if ($first === '') {
101 return '';
102 }
103
104 return $first === self::CAPABILITY_BACKUP ? self::CAPABILITY_STAGING : self::CAPABILITY_BACKUP;
105 }
106
107
108
109
110
111 public function getActiveCapability(): string
112 {
113 return $this->getAction($this->activePosition());
114 }
115
116
117
118
119
120 public function getRunningCapability(): string
121 {
122 $position = $this->activePosition();
123
124 return $this->read($position, 'started_at') === '' ? '' : $this->getAction($position);
125 }
126
127
128
129
130
131
132 public function isUnderWay(): bool
133 {
134 return !$this->isCompleted() && $this->getAction(self::POSITION_FIRST) !== '';
135 }
136
137 public function isFirstCapabilityCompleted(): bool
138 {
139 return $this->hasSucceeded(self::POSITION_FIRST);
140 }
141
142
143
144
145
146 public function selectAction(string $capability): string
147 {
148 if (!in_array($capability, self::CAPABILITIES, true) || $this->isCompleted()) {
149 return '';
150 }
151
152 $position = $this->positionFor($capability);
153
154 if ($this->getAction($position) === '') {
155 $this->stamp($position, 'selected_at', ['action' => $capability]);
156 }
157
158 return $position;
159 }
160
161 public function startAction()
162 {
163 $position = $this->activePosition();
164
165 if ($position === '' || $this->read($position, 'started_at') !== '') {
166 return;
167 }
168
169 $this->stamp($position, 'started_at');
170 $this->logStep(self::EVENT_ACTION_STARTED, $position);
171 }
172
173
174
175
176 public function completeStaging()
177 {
178 $this->completeCapability(self::CAPABILITY_STAGING);
179 }
180
181
182
183
184
185
186
187
188
189 public function completeBackup($jobDataDto = null)
190 {
191 $position = $this->activePosition();
192
193 $this->completeCapability(self::CAPABILITY_BACKUP);
194
195 if ($position === '' || !is_object($jobDataDto) || !method_exists($jobDataDto, 'getBackupFilePath')) {
196 return;
197 }
198
199 $path = (string)$jobDataDto->getBackupFilePath();
200
201 if ($path === '' || !is_readable($path)) {
202 return;
203 }
204
205 $state = $this->readState();
206 $state[$position]['backup_file'] = basename($path);
207 $state[$position]['backup_size'] = (int)filesize($path);
208
209 $this->write($state);
210 }
211
212
213
214
215
216 public function getCompletedBackupDetails(): array
217 {
218 foreach ([self::POSITION_FIRST, self::POSITION_SECOND] as $position) {
219 $name = $this->read($position, 'backup_file');
220
221 if ($name !== '') {
222 return ['name' => $name, 'size' => (int)$this->read($position, 'backup_size')];
223 }
224 }
225
226 return [];
227 }
228
229
230
231
232
233
234
235 public function completeCapability(string $capability)
236 {
237 $position = $this->activePosition();
238
239 if ($position === '' || $this->getAction($position) !== $capability) {
240 return;
241 }
242
243 $this->stamp($position, 'completed_at');
244 $this->logStep(self::EVENT_ACTION_COMPLETED, $position);
245
246 if ($position === self::POSITION_SECOND) {
247 $this->complete(self::REASON_TWO_CAPABILITIES);
248 }
249 }
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264 public function abandonActionOnRequest()
265 {
266 if (!WPStaging::make(Auth::class)->isAuthenticatedRequest('', 'manage_options')) {
267 return;
268 }
269
270 $this->abandonAction();
271 }
272
273 public function abandonAction()
274 {
275 $position = $this->activePosition();
276
277 if ($position === '' || $this->isCompleted()) {
278 return;
279 }
280
281 $this->logStep(self::EVENT_ACTION_CANCELLED, $position);
282 $this->forget($position);
283 }
284
285
286
287
288
289 public function cancelAction()
290 {
291 $position = $this->activePosition();
292
293 if ($position === '' || $this->read($position, 'started_at') !== '') {
294 return;
295 }
296
297 $this->forget($position);
298 }
299
300
301
302
303
304 public function cancelSecondAction()
305 {
306 if ($this->getAction(self::POSITION_SECOND) === '' || $this->hasSucceeded(self::POSITION_SECOND)) {
307 return;
308 }
309
310 $this->forget(self::POSITION_SECOND);
311 }
312
313
314
315
316
317
318
319
320 public function recordNextOfferShown(string $surface = self::SURFACE_POST_SUCCESS)
321 {
322 $capability = $this->getNextCapability();
323 $stamp = $surface === self::SURFACE_STAGING_PROGRESS ? 'offered_progress_at' : 'offered_at';
324
325 if ($capability === '' || $this->read(self::POSITION_SECOND, $stamp) !== '') {
326 return;
327 }
328
329 $this->stamp(self::POSITION_SECOND, $stamp);
330
331 AnalyticsGenericEvent::logEvent(self::EVENT_NEXT_OFFER_SHOWN, FreeOnboarding::ANALYTICS_GROUP, [
332 'action' => $capability,
333 'surface' => $surface,
334 ]);
335 }
336
337
338
339
340 public function completeOnExplicitExit()
341 {
342 $reasons = [
343 self::STEP_SELECT => self::REASON_SKIPPED_INITIAL,
344 self::STEP_NEXT => self::REASON_DONE_AFTER_FIRST,
345 ];
346
347 $step = $this->getStep();
348
349 $this->complete(isset($reasons[$step]) ? $reasons[$step] : self::REASON_OTHER_EXPLICIT_EXIT);
350 }
351
352
353
354
355 public function complete(string $reason)
356 {
357 if ($this->isCompleted()) {
358 return;
359 }
360
361 AnalyticsGenericEvent::logEvent(self::EVENT_COMPLETED, FreeOnboarding::ANALYTICS_GROUP, [
362 'reason' => $reason,
363 'first_action' => $this->getAction(self::POSITION_FIRST),
364 'second_action' => $this->getAction(self::POSITION_SECOND),
365 ]);
366
367 update_option(self::OPTION_COMPLETED, $reason, false);
368 $this->completionReason = $reason;
369
370 delete_option(self::OPTION_STATE);
371 delete_option(self::OPTION_RESTARTED);
372 delete_option(QueuedBackup::OPTION_STATE);
373 $this->state = null;
374 }
375
376 public function isCompleted(): bool
377 {
378 return $this->getCompletionReason() !== '';
379 }
380
381
382
383
384 public function restart()
385 {
386 delete_option(self::OPTION_COMPLETED);
387 delete_option(self::OPTION_STATE);
388 delete_option(FreeOnboarding::OPTION_EXPOSURE);
389 delete_option(QueuedBackup::OPTION_STATE);
390 update_option(self::OPTION_RESTARTED, (string)time(), false);
391
392 $this->state = null;
393 $this->completionReason = null;
394 }
395
396 public function wasRestarted(): bool
397 {
398 return get_option(self::OPTION_RESTARTED) !== false;
399 }
400
401
402
403
404
405 public function getCompletionReason(): string
406 {
407 if ($this->completionReason === null) {
408 $this->completionReason = (string)get_option(self::OPTION_COMPLETED, '');
409 }
410
411 return $this->completionReason;
412 }
413
414 private function hasSucceeded(string $position): bool
415 {
416 return $this->read($position, 'completed_at') !== '';
417 }
418
419
420
421
422 private function activePosition(): string
423 {
424 foreach ([self::POSITION_FIRST, self::POSITION_SECOND] as $position) {
425 if ($this->getAction($position) !== '' && !$this->hasSucceeded($position)) {
426 return $position;
427 }
428 }
429
430 return '';
431 }
432
433
434
435
436
437 private function positionFor(string $capability): string
438 {
439 $first = $this->getAction(self::POSITION_FIRST);
440
441 return ($first === '' || $first === $capability) ? self::POSITION_FIRST : self::POSITION_SECOND;
442 }
443
444 private function logStep(string $event, string $position)
445 {
446 AnalyticsGenericEvent::logEvent($event, FreeOnboarding::ANALYTICS_GROUP, [
447 'action' => $this->getAction($position),
448 'position' => $position,
449 ]);
450 }
451
452 private function stamp(string $position, string $key, array $extra = [])
453 {
454 $state = $this->readState();
455 $state[$position] = array_merge($state[$position], [$key => time()], $extra);
456
457 $this->write($state);
458 }
459
460 private function forget(string $position)
461 {
462 $state = $this->readState();
463 $state[$position] = [];
464
465 $this->write($state);
466 }
467
468 private function write(array $state)
469 {
470 $this->state = $state;
471 update_option(self::OPTION_STATE, $state, false);
472 }
473
474
475
476
477
478
479 private function dropStaleAction()
480 {
481 foreach ([self::POSITION_FIRST, self::POSITION_SECOND] as $position) {
482 $startedAt = empty($this->state[$position]['started_at']) ? 0 : (int)$this->state[$position]['started_at'];
483
484 if ($startedAt === 0 || !empty($this->state[$position]['completed_at'])) {
485 continue;
486 }
487
488 if ($startedAt < time() - self::MAX_RUNNING_AGE_IN_SECONDS) {
489 $this->state[$position] = [];
490 update_option(self::OPTION_STATE, $this->state, false);
491 }
492 }
493 }
494
495
496
497
498 private function read(string $position, string $key): string
499 {
500 $state = $this->readState();
501
502 return empty($state[$position][$key]) ? '' : (string)$state[$position][$key];
503 }
504
505
506
507
508 private function readState(): array
509 {
510 if ($this->state !== null) {
511 return $this->state;
512 }
513
514
515
516
517
518 $stored = $this->isCompleted() ? [] : get_option(self::OPTION_STATE, []);
519 $stored = is_array($stored) ? $stored : [];
520 $this->state = [];
521
522 foreach ([self::POSITION_FIRST, self::POSITION_SECOND] as $position) {
523 $this->state[$position] = isset($stored[$position]) && is_array($stored[$position]) ? $stored[$position] : [];
524 }
525
526 $this->dropStaleAction();
527
528 return $this->state;
529 }
530 }
531