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 / ajax / class-wpbc-setup-wizard-ajax.php

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

306 lines 11.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AJAX endpoints for Setup Wizard checkpoint saving and navigation.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Orchestrate nonce-protected, authorized, data-only checkpoint requests.
14 */
15 final class WPBC_Setup_Wizard_Ajax {
16
17 const ACTION_SAVE_CONTINUE = 'WPBC_AJX_SETUP_WIZARD_SAVE_CONTINUE';
18 const ACTION_BACK = 'WPBC_AJX_SETUP_WIZARD_BACK';
19 const ACTION_EDIT = 'WPBC_AJX_SETUP_WIZARD_EDIT';
20 const ACTION_RESTART = 'WPBC_AJX_SETUP_WIZARD_RESTART';
21 const ACTION_SKIP = 'WPBC_AJX_SETUP_WIZARD_SKIP';
22 const NONCE_ACTION = 'wpbc_setup_wizard_nonce';
23
24 /**
25 * Register authenticated endpoints while the module gate is enabled.
26 *
27 * @return void
28 */
29 public static function register() {
30 add_action( 'wp_ajax_' . self::ACTION_SAVE_CONTINUE, array( __CLASS__, 'save_and_continue' ) );
31 add_action( 'wp_ajax_' . self::ACTION_BACK, array( __CLASS__, 'navigate_back' ) );
32 add_action( 'wp_ajax_' . self::ACTION_EDIT, array( __CLASS__, 'edit_step' ) );
33 add_action( 'wp_ajax_' . self::ACTION_RESTART, array( __CLASS__, 'restart' ) );
34 add_action( 'wp_ajax_' . self::ACTION_SKIP, array( __CLASS__, 'skip_setup' ) );
35 }
36
37 /**
38 * Verify the production Setup Wizard nonce.
39 *
40 * @return bool True when the request contains a recognized valid nonce.
41 */
42 public static function verify_request_nonce() {
43 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This method performs the nonce verification.
44 if ( ! isset( $_REQUEST['nonce'] ) || ! is_scalar( $_REQUEST['nonce'] ) ) {
45 return false;
46 }
47
48 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This method performs the nonce verification.
49 $nonce = sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) );
50 return (bool) wp_verify_nonce( $nonce, self::NONCE_ACTION );
51 }
52
53 /**
54 * Save one validated step through the progressive coordinator and continue.
55 *
56 * @return void Sends a WordPress JSON response and terminates the request.
57 */
58 public static function save_and_continue() {
59 self::authorize_request();
60
61 $current_step_id = self::get_request_string( 'current_step' );
62 $expected_revision = self::get_request_integer( 'expected_revision' );
63 $operation_id = self::get_request_string( 'operation_id' );
64 $request_id = self::get_request_string( 'request_id' );
65 $submitted_fields = self::get_request_fields();
66 $services = self::get_services();
67 $result = $services['coordinator']->save_and_continue( $current_step_id, $expected_revision, $submitted_fields, $operation_id );
68
69 self::send_result( $result, $request_id, self::get_page_url() );
70 }
71
72 /**
73 * Navigate backward without saving or validating current form fields.
74 *
75 * @return void Sends a WordPress JSON response and terminates the request.
76 */
77 public static function navigate_back() {
78 self::authorize_request();
79
80 $current_step_id = self::get_request_string( 'current_step' );
81 $expected_revision = self::get_request_integer( 'expected_revision' );
82 $request_id = self::get_request_string( 'request_id' );
83 $updated_draft = self::get_draft_store()->navigate_back( $current_step_id, $expected_revision );
84
85 self::send_result( $updated_draft, $request_id, self::get_page_url() );
86 }
87
88 /**
89 * Navigate from the journey rail or review plan to one earlier active step.
90 *
91 * @return void Sends a WordPress JSON response and terminates the request.
92 */
93 public static function edit_step() {
94 self::authorize_request();
95
96 $target_step_id = self::get_request_string( 'target_step' );
97 $current_step_id = self::get_request_string( 'current_step' );
98 $expected_revision = self::get_request_integer( 'expected_revision' );
99 $request_id = self::get_request_string( 'request_id' );
100 $updated_draft = self::get_draft_store()->navigate_to_step( $target_step_id, $current_step_id, $expected_revision );
101
102 self::send_result( $updated_draft, $request_id, self::get_page_url() );
103 }
104
105 /**
106 * Restart wizard navigation without claiming a settings rollback.
107 *
108 * @return void Sends a WordPress JSON response and terminates the request.
109 */
110 public static function restart() {
111 self::authorize_request();
112
113 if ( 'restart' !== self::get_request_string( 'confirmation' ) ) {
114 wp_send_json_error( array( 'message' => __( 'Confirm that you want to restart Setup Wizard navigation.', 'booking' ) ), 400 );
115 }
116
117 $expected_revision = self::get_request_integer( 'expected_revision' );
118 $request_id = self::get_request_string( 'request_id' );
119 $updated_draft = self::get_draft_store()->restart( $expected_revision );
120
121 self::send_result( $updated_draft, $request_id, self::get_page_url() );
122 }
123
124 /**
125 * Skip remaining steps, complete the checkpoint, and open Setup overview.
126 *
127 * This action intentionally does not save unsaved current-page fields or run
128 * step mutation handlers. Previously completed progressive saves remain live.
129 *
130 * @return void Sends a WordPress JSON response and terminates the request.
131 */
132 public static function skip_setup() {
133 self::authorize_request();
134
135 if ( 'skip' !== self::get_request_string( 'confirmation' ) ) {
136 wp_send_json_error( array( 'message' => __( 'Confirm that you want to skip the remaining steps and exit the Setup Wizard.', 'booking' ) ), 400 );
137 }
138
139 $current_step_id = self::get_request_string( 'current_step' );
140 $expected_revision = self::get_request_integer( 'expected_revision' );
141 $request_id = self::get_request_string( 'request_id' );
142 $updated_draft = self::get_draft_store()->complete_with_skipped_steps( $current_step_id, $expected_revision );
143
144 self::send_result( $updated_draft, $request_id, self::get_page_url() );
145 }
146
147 /**
148 * Verify the fixed nonce and the complete server-side access boundary.
149 *
150 * @return void Sends an error response when authorization fails.
151 */
152 private static function authorize_request() {
153 if ( ! self::verify_request_nonce() ) {
154 wp_send_json_error( array( 'message' => __( 'Your Setup Wizard session expired. Reload the page and try again.', 'booking' ) ), 403 );
155 }
156
157 if ( ! WPBC_Setup_Wizard_Access::current_user_can_access() ) {
158 wp_send_json_error( array( 'message' => __( 'You are not allowed to use this Setup Wizard.', 'booking' ) ), 403 );
159 }
160 }
161
162 /**
163 * Create a checkpoint store bound to the current authorized request context.
164 *
165 * @return WPBC_Setup_Wizard_Draft_Store Draft persistence service.
166 */
167 private static function get_draft_store() {
168 $services = self::get_services();
169
170 return $services['checkpoint_store'];
171 }
172
173 /**
174 * Build shared request services around one registry and validator instance.
175 *
176 * @return array{checkpoint_store:WPBC_Setup_Wizard_Draft_Store,coordinator:WPBC_Setup_Wizard_Progressive_Save_Coordinator} Request services.
177 */
178 private static function get_services() {
179 $module_registry = new WPBC_Setup_Wizard_Step_Module_Registry();
180 $step_data = new WPBC_Setup_Wizard_Step_Data( $module_registry );
181 $step_registry = new WPBC_Setup_Wizard_Step_Registry( $module_registry );
182 $draft_validator = new WPBC_Setup_Wizard_Draft_Validator( $step_data );
183 $checkpoint_store = new WPBC_Setup_Wizard_Draft_Store( $step_registry, $draft_validator );
184
185 return array(
186 'checkpoint_store' => $checkpoint_store,
187 'coordinator' => WPBC_Setup_Wizard_Progressive_Save_Factory::create_coordinator(
188 $checkpoint_store,
189 $draft_validator,
190 $step_registry
191 ),
192 );
193 }
194
195 /**
196 * Read the current step field collection before domain validation.
197 *
198 * The draft validator owns sanitization and the exact per-step allow-list.
199 * Non-array input becomes empty and cannot bypass forward validation.
200 *
201 * @return array<string,mixed> Unslashed request fields.
202 */
203 private static function get_request_fields() {
204 if ( ! isset( $_POST['fields'] ) || ! is_array( $_POST['fields'] ) ) {
205 return array();
206 }
207
208 return wp_unslash( $_POST['fields'] );
209 }
210
211 /**
212 * Read and sanitize one scalar request string.
213 *
214 * @param string $key Request field name.
215 *
216 * @return string Sanitized string, or an empty string when absent or invalid.
217 */
218 private static function get_request_string( $key ) {
219 if ( ! isset( $_POST[ $key ] ) || ! is_scalar( $_POST[ $key ] ) ) {
220 return '';
221 }
222
223 return sanitize_text_field( wp_unslash( $_POST[ $key ] ) );
224 }
225
226 /**
227 * Read one non-negative integer request value.
228 *
229 * @param string $key Request field name.
230 *
231 * @return int Sanitized integer.
232 */
233 private static function get_request_integer( $key ) {
234 if ( ! isset( $_POST[ $key ] ) || ! is_scalar( $_POST[ $key ] ) ) {
235 return -1;
236 }
237
238 return absint( wp_unslash( $_POST[ $key ] ) );
239 }
240
241 /**
242 * Build the canonical Setup Wizard URL without client input.
243 *
244 * @return string Administration URL for the Setup Wizard.
245 */
246 private static function get_page_url() {
247 return wpbc_get_setup_wizard_page_url();
248 }
249
250 /**
251 * Send a normalized data-only success or error response.
252 *
253 * @param array<string,mixed>|WP_Error $result Service result.
254 * @param string $request_id Client request correlation identifier.
255 * @param string $redirect_url Server-owned destination URL.
256 * @return void Sends a WordPress JSON response and terminates the request.
257 */
258 private static function send_result( $result, $request_id, $redirect_url ) {
259 if ( is_wp_error( $result ) ) {
260 $status = 400;
261 if ( 0 === strpos( $result->get_error_code(), 'wpbc_setup_wizard_stale_' ) || 'wpbc_setup_wizard_operation_locked' === $result->get_error_code() ) {
262 $status = 409;
263 } elseif ( in_array( $result->get_error_code(), array( 'wpbc_setup_wizard_validation_failed', 'wpbc_setup_wizard_review_invalid' ), true ) ) {
264 $status = 422;
265 }
266
267 $error_data = $result->get_error_data();
268 $field_errors = is_array( $error_data ) && isset( $error_data['field_errors'] ) && is_array( $error_data['field_errors'] )
269 ? $error_data['field_errors']
270 : array();
271 $invalid_step_id = is_array( $error_data ) && isset( $error_data['invalid_step_id'] )
272 ? sanitize_key( (string) $error_data['invalid_step_id'] )
273 : '';
274
275 wp_send_json_error(
276 array(
277 'code' => $result->get_error_code(),
278 'message' => $result->get_error_message(),
279 'field_errors' => $field_errors,
280 'invalid_step_id' => $invalid_step_id,
281 'request_id' => sanitize_key( $request_id ),
282 ),
283 $status
284 );
285 }
286
287 $idempotent_replay = is_array( $result ) && isset( $result['checkpoint'], $result['idempotent_replay'] );
288 $checkpoint = $idempotent_replay ? $result['checkpoint'] : $result;
289
290 wp_send_json_success(
291 array(
292 'checkpoint' => array(
293 'status' => $checkpoint['status'],
294 'current_step' => $checkpoint['current_step'],
295 'completed_steps' => $checkpoint['completed_steps'],
296 'needs_review_steps' => $checkpoint['needs_review_steps'],
297 'revision' => (int) $checkpoint['revision'],
298 ),
299 'redirect_url' => esc_url_raw( $redirect_url ),
300 'idempotent_replay' => $idempotent_replay ? (bool) $result['idempotent_replay'] : false,
301 'request_id' => sanitize_key( $request_id ),
302 )
303 );
304 }
305 }
306