PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
booking / includes / page-setup-wizard / class-wpbc-setup-wizard-progressive-save-coordinator.php

class-wpbc-setup-wizard-progressive-save-coordinator.php in Booking Calendar 11.9, at includes/page-setup-wizard/class-wpbc-setup-wizard-progressive-save-coordinator.php

183 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Shared Setup Wizard progressive-save coordinator.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Coordinate validation, locking, idempotency, domain saving, and checkpointing.
14 */
15 final class WPBC_Setup_Wizard_Progressive_Save_Coordinator {
16
17 /** @var WPBC_Setup_Wizard_Checkpoint_Store */
18 private $checkpoint_store;
19
20 /** @var WPBC_Setup_Wizard_Draft_Validator */
21 private $draft_validator;
22
23 /** @var WPBC_Setup_Wizard_Step_Save_Registry */
24 private $save_registry;
25
26 /** @var WPBC_Setup_Wizard_Operation_Lock */
27 private $operation_lock;
28
29 /**
30 * Build the shared save boundary from explicit collaborators.
31 *
32 * @param WPBC_Setup_Wizard_Checkpoint_Store $checkpoint_store Checkpoint persistence.
33 * @param WPBC_Setup_Wizard_Draft_Validator $draft_validator Step validator.
34 * @param WPBC_Setup_Wizard_Step_Save_Registry $save_registry Domain handler registry.
35 * @param WPBC_Setup_Wizard_Operation_Lock $operation_lock Per-step lock.
36 */
37 public function __construct( WPBC_Setup_Wizard_Checkpoint_Store $checkpoint_store, WPBC_Setup_Wizard_Draft_Validator $draft_validator, WPBC_Setup_Wizard_Step_Save_Registry $save_registry, WPBC_Setup_Wizard_Operation_Lock $operation_lock ) {
38 $this->checkpoint_store = $checkpoint_store;
39 $this->draft_validator = $draft_validator;
40 $this->save_registry = $save_registry;
41 $this->operation_lock = $operation_lock;
42 }
43
44 /**
45 * Save the current step once and advance only after a verified result.
46 *
47 * @param string $client_step_id Client-observed current step.
48 * @param int $expected_revision Client-observed revision.
49 * @param array<string,mixed> $submitted_fields Current-step fields.
50 * @param string $operation_id Stable idempotency identifier.
51 *
52 * @return array{checkpoint:array<string,mixed>,idempotent_replay:bool}|WP_Error Save result envelope or error.
53 */
54 public function save_and_continue( $client_step_id, $expected_revision, array $submitted_fields, $operation_id ) {
55 $client_step_id = sanitize_key( is_scalar( $client_step_id ) ? (string) $client_step_id : '' );
56 $raw_operation_id = is_scalar( $operation_id ) ? (string) $operation_id : '';
57 $operation_id = sanitize_key( $raw_operation_id );
58 if ( '' === $operation_id || $operation_id !== $raw_operation_id || 100 < strlen( $operation_id ) ) {
59 return new WP_Error( 'wpbc_setup_wizard_operation_invalid', __( 'The Setup Wizard save operation is invalid. Reload the page and try again.', 'booking' ) );
60 }
61
62 $checkpoint = $this->checkpoint_store->load();
63 if ( $this->is_completed_operation( $checkpoint, $client_step_id, $operation_id, $expected_revision ) ) {
64 return array(
65 'checkpoint' => $checkpoint,
66 'idempotent_replay' => true,
67 );
68 }
69
70 $request_state = $this->checkpoint_store->validate_request_state( $checkpoint, $client_step_id, $expected_revision );
71 if ( is_wp_error( $request_state ) ) {
72 return $request_state;
73 }
74
75 $handler = $this->save_registry->get_handler( $client_step_id );
76 if ( null === $handler ) {
77 return new WP_Error( 'wpbc_setup_wizard_save_handler_missing', __( 'This Setup Wizard step cannot be saved yet.', 'booking' ) );
78 }
79
80 $lock_scope = $this->checkpoint_store->get_lock_scope();
81 $lock_token = $this->operation_lock->acquire( $lock_scope, $client_step_id, $operation_id );
82 if ( is_wp_error( $lock_token ) ) {
83 return $lock_token;
84 }
85
86 try {
87 $checkpoint = $this->checkpoint_store->load();
88 if ( $this->is_completed_operation( $checkpoint, $client_step_id, $operation_id, $expected_revision ) ) {
89 return array(
90 'checkpoint' => $checkpoint,
91 'idempotent_replay' => true,
92 );
93 }
94
95 $request_state = $this->checkpoint_store->validate_request_state( $checkpoint, $client_step_id, $expected_revision );
96 if ( is_wp_error( $request_state ) ) {
97 return $request_state;
98 }
99
100 $validated_fields = $this->draft_validator->validate_step( $client_step_id, $submitted_fields, true, $checkpoint['values'] );
101 if ( is_wp_error( $validated_fields ) ) {
102 return $validated_fields;
103 }
104
105 if ( $handler->is_terminal() ) {
106 $route_values = $checkpoint['values'];
107 if ( ! empty( $validated_fields ) ) {
108 $current_values = isset( $route_values[ $client_step_id ] ) && is_array( $route_values[ $client_step_id ] ) ? $route_values[ $client_step_id ] : array();
109 $route_values[ $client_step_id ] = array_merge( $current_values, $validated_fields );
110 }
111
112 $route_checkpoint = $checkpoint;
113 $route_checkpoint['values'] = $route_values;
114 $active_step_ids = $this->checkpoint_store->get_active_step_ids( $route_checkpoint );
115 $revalidated_values = $this->draft_validator->validate_active_route( $route_values, $active_step_ids );
116 if ( is_wp_error( $revalidated_values ) ) {
117 return $revalidated_values;
118 }
119 $validated_fields = isset( $revalidated_values[ $client_step_id ] ) ? $revalidated_values[ $client_step_id ] : $validated_fields;
120 }
121
122 $handler_result = $handler->save(
123 $validated_fields,
124 $checkpoint,
125 array(
126 'operation_id' => $operation_id,
127 'step_id' => $client_step_id,
128 'expected_revision' => (int) $expected_revision,
129 )
130 );
131 if ( is_wp_error( $handler_result ) ) {
132 return $handler_result;
133 }
134
135 $handler_result = $this->save_registry->normalize_result( $handler_result );
136 if ( is_wp_error( $handler_result ) ) {
137 return $handler_result;
138 }
139
140 $updated_checkpoint = $this->checkpoint_store->commit_step_save(
141 $client_step_id,
142 $expected_revision,
143 $validated_fields,
144 $operation_id,
145 $handler_result,
146 $handler->is_terminal()
147 );
148 if ( is_wp_error( $updated_checkpoint ) ) {
149 return $updated_checkpoint;
150 }
151
152 return array(
153 'checkpoint' => $updated_checkpoint,
154 'idempotent_replay' => false,
155 );
156 } finally {
157 $this->operation_lock->release( $lock_scope, $client_step_id, $lock_token );
158 }
159 }
160
161 /**
162 * Determine whether a stored step result already completed an operation.
163 *
164 * @param array<string,mixed> $checkpoint Current checkpoint.
165 * @param string $step_id Stable step identifier.
166 * @param string $operation_id Stable operation identifier.
167 * @param int $source_revision Revision observed by the original save.
168 *
169 * @return bool True when the exact operation already succeeded.
170 */
171 private function is_completed_operation( array $checkpoint, $step_id, $operation_id, $source_revision ) {
172 $step_result = isset( $checkpoint['step_results'][ $step_id ] ) && is_array( $checkpoint['step_results'][ $step_id ] )
173 ? $checkpoint['step_results'][ $step_id ]
174 : array();
175
176 return 'saved' === ( isset( $step_result['status'] ) ? $step_result['status'] : '' )
177 && isset( $step_result['operation_id'] )
178 && hash_equals( (string) $step_result['operation_id'], (string) $operation_id )
179 && isset( $step_result['source_revision'] )
180 && (int) $step_result['source_revision'] === (int) $source_revision;
181 }
182 }
183