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
← All changes | includes/_capacity/create_booking.php +668 -223 11.1 → 11.9 View file →
@@ -1,4 +1,4 @@
1 1 <?php
2 2
3 3 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly // FixIn: 9.8.0.4.
4 4
@@ -28,22 +28,28 @@
28 28 // Response AJAX parameters
29 29 $ajx_data_arr = array();
30 30 $ajx_data_arr['status'] = 'ok';
31 31
32 - $admin_uri = ltrim( str_replace( get_site_url( null, '', 'admin' ), '', admin_url( 'admin.php?' ) ), '/' ); // 'wp-admin/admin.php?'
33 - $server_http_referer_uri = ( ( isset( $_SERVER['HTTP_REFERER'] ) ) ? sanitize_text_field( $_SERVER['HTTP_REFERER'] ) : '' ); /* phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash */ /* FixIn: sanitize_unslash */
34 - // Local parameters
35 - $local_params = array();
36 - $local_params['is_from_admin_panel'] = ( false !== strpos( $server_http_referer_uri, $admin_uri ) ); // true | false
37 - $local_params['user_id'] = ( isset( $_REQUEST['wpbc_ajx_user_id'] ) ) ? intval( $_REQUEST['wpbc_ajx_user_id'] ) : wpbc_get_current_user_id(); // 1
32 + // Local parameters
33 + $local_params = array();
34 + $local_params['user_id'] = ( isset( $_REQUEST['wpbc_ajx_user_id'] ) ) ? intval( $_REQUEST['wpbc_ajx_user_id'] ) : wpbc_get_current_user_id(); // 1
38 35
39 - // Request parameters
40 - $user_request = new WPBC_AJX__REQUEST( array( // Using this class here only for escaping variables
41 - 'db_option_name' => 'booking__wpbc_booking_create__request_params', // Not necessary, because we not save request, only sanitize it
42 - 'user_id' => $local_params['user_id'], // Not necessary, because we not save request, only sanitize it
43 - 'request_rules_structure' => array(
44 - 'resource_id' => array( 'validate' => 'd', 'default' => 1 ), // 'digit_or_csd'.
45 - 'aggregate_resource_id_arr' => array( 'validate' => 'digit_or_csd', 'default' => '' ),
36 + // Request parameters for the released Appointment and Resource Selector workflows.
37 + $workflow_request_rules = array(
38 + 'service_id' => array( 'validate' => 'd', 'default' => 0 ),
39 + 'appointment_service_required' => array( 'validate' => 'd', 'default' => 0 ),
40 + 'appointment_context_token' => array( 'validate' => 'strong', 'default' => '' ),
41 + 'resource_selector_required' => array( 'validate' => 'd', 'default' => 0 ),
42 + 'resource_selector_context_token' => array( 'validate' => 'strong', 'default' => '' ),
43 + 'wpbc_admin_booking_nonce' => array( 'validate' => 'strong', 'default' => '' ),
44 + );
45 +
46 + $user_request = new WPBC_AJX__REQUEST( array( // Using this class here only for escaping variables
47 + 'db_option_name' => 'booking__wpbc_booking_create__request_params', // Not necessary, because we not save request, only sanitize it
48 + 'user_id' => $local_params['user_id'], // Not necessary, because we not save request, only sanitize it
49 + 'request_rules_structure' => array_merge( array(
50 + 'resource_id' => array( 'validate' => 'd', 'default' => 1 ), // 'digit_or_csd'.
51 + 'aggregate_resource_id_arr' => array( 'validate' => 'digit_or_csd', 'default' => '' ),
46 52 'dates_ddmmyy_csv' => array( 'validate' => 'csv_dates', 'default' => '' ), // FixIn: 9.9.1.1.
47 53 'formdata' => array( 'validate' => 'strong', 'default' => '' ),
48 54 'booking_hash' => array( 'validate' => 'strong', 'default' => '' ),
49 55 'custom_form' => array( 'validate' => 'strong', 'default' => '' ),
@@ -51,19 +57,21 @@
51 57 'captcha_user_input' => array( 'validate' => 'strong', 'default' => '' ),
52 58 'is_emails_send' => array( 'validate' => 'd', 'default' => 1 ),
53 59 'active_locale' => array( 'validate' => 'strong', 'default' => '' ),
54 60 'form_status' => array( 'validate' => 'strong', 'default' => 'published' ),
55 - 'allow_past' => array( 'validate' => 'd', 'default' => 0 ),
61 + 'allow_past' => array( 'validate' => 'd', 'default' => 0 ),
62 + 'classic_booking_context_token' => array( 'validate' => 'strong', 'default' => '' ),
56 63 'wpbc_bfb_preview' => array( 'validate' => 'd', 'default' => 0 ),
57 64 'wpbc_bfb_preview_token' => array( 'validate' => 'strong', 'default' => '' ),
58 65 'wpbc_bfb_preview_form_id' => array( 'validate' => 'd', 'default' => 0 ),
59 66 'wpbc_bfb_preview_nonce' => array( 'validate' => 'strong', 'default' => '' ),
60 67 'wpbc_time_override_enabled' => array( 'validate' => 'd', 'default' => 0 ),
61 - 'wpbc_time_override_source' => array( 'validate' => 'strong', 'default' => '' ),
62 - 'wpbc_time_override_start' => array( 'validate' => 'strong', 'default' => '' ),
63 - 'wpbc_time_override_end' => array( 'validate' => 'strong', 'default' => '' ),
64 - )
65 - ));
68 + 'wpbc_time_override_source' => array( 'validate' => 'strong', 'default' => '' ),
69 + 'wpbc_time_override_start' => array( 'validate' => 'strong', 'default' => '' ),
70 + 'wpbc_time_override_end' => array( 'validate' => 'strong', 'default' => '' ),
71 + 'wpbc_admin_cost_correction' => array( 'validate' => 'strong', 'default' => '' ),
72 + ), $workflow_request_rules )
73 + ));
66 74
67 75 // Escape of request params in Ajax Post. We use prefix 'calendar_request_params', if Ajax sent - $_REQUEST['calendar_request_params']['resource_id'], ...
68 76 $request_prefix = 'calendar_request_params';
69 77
@@ -68,15 +76,16 @@
68 76 $request_prefix = 'calendar_request_params';
69 77
70 78 //$_REQUEST['calendar_request_params']['dates_ddmmyy_csv'] .= "'%2b(select+'box'+from(select+sleep(2)+from+dual+where+1=1*)a)%2b'-02-21+00:00:00";
71 79
72 - $request_params = $user_request->get_sanitized__in_request__value_or_default( $request_prefix ); // NOT Direct: $_REQUEST['calendar_request_params']['resource_id']
73 - $server_http_referer_uri = ( ( isset( $_SERVER['HTTP_REFERER'] ) ) ? sanitize_text_field( $_SERVER['HTTP_REFERER'] ) : '' ); /* phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash */ /* FixIn: sanitize_unslash */
74 - $request_params['request_uri'] = $server_http_referer_uri; // Parameter needed for Error in booking saving and reloading calendar again with these actual parameters.
80 + $request_params = $user_request->get_sanitized__in_request__value_or_default( $request_prefix ); // NOT Direct: $_REQUEST['calendar_request_params']['resource_id']
81 + $server_http_referer_uri = ( ( isset( $_SERVER['HTTP_REFERER'] ) ) ? sanitize_text_field( $_SERVER['HTTP_REFERER'] ) : '' ); /* phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash */ /* FixIn: sanitize_unslash */
82 + $request_params['request_uri'] = $server_http_referer_uri; // Parameter needed for Error in booking saving and reloading calendar again with these actual parameters.
83 + $is_authorized_admin_booking_request = wpbc_is_authorized_admin_booking_request( $request_params['wpbc_admin_booking_nonce'] );
75 84
76 85 // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- CAPTCHA " >
77 86 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
78 - wpbc_captcha__in_ajx__check( $request_params, $local_params['is_from_admin_panel'], $_REQUEST[ $request_prefix ] );
87 + wpbc_captcha__in_ajx__check( $request_params, $is_authorized_admin_booking_request, $_REQUEST[ $request_prefix ] );
79 88 // </editor-fold>
80 89
81 90 // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- BOOKING_RESOURCE ID " >
82 91 if ( $request_params['resource_id'] <= 0 ) {
@@ -83,24 +92,21 @@
83 92 $ajx_data_arr['status'] = 'error';
84 93 $ajx_data_arr['status_error'] = 'resource_id_incorrect';
85 94 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
86 95 $ajx_data_arr['ajx_after_action_message'] = 'Wrong ID of booking resource: ' . ' [ request ID: ' . $_REQUEST['calendar_request_params']['resource_id'] . ' | parsed ID: ' . $request_params['resource_id'] . ' ]';
87 - $ajx_data_arr['ajx_after_action_message_status'] = 'error';
88 - wp_send_json( array(
89 - 'ajx_data' => $ajx_data_arr,
90 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
91 - 'ajx_search_params' => $_REQUEST[ $request_prefix ],
92 - 'ajx_cleaned_params' => $request_params,
93 - 'resource_id' => $request_params['resource_id'],
94 - ) );
96 + $ajx_data_arr['ajx_after_action_message_status'] = 'error';
97 + wp_send_json( array(
98 + 'ajx_data' => $ajx_data_arr,
99 + 'resource_id' => $request_params['resource_id'],
100 + ) );
95 101 }
96 102 // </editor-fold>
97 103
98 104 $server_http_referer_uri = ( ( isset( $_SERVER['HTTP_REFERER'] ) ) ? sanitize_text_field( $_SERVER['HTTP_REFERER'] ) : '' ); /* phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash */ /* FixIn: sanitize_unslash */
99 105
100 - $request_save_params = array(
101 - 'resource_id' => $request_params['resource_id'],
102 - 'dates_ddmmyy_csv' => $request_params['dates_ddmmyy_csv'],
106 + $request_save_params = array(
107 + 'resource_id' => $request_params['resource_id'],
108 + 'dates_ddmmyy_csv' => $request_params['dates_ddmmyy_csv'],
103 109 'form_data' => $request_params['formdata'],
104 110 'aggregate_resource_id_arr' => $request_params['aggregate_resource_id_arr'], // Optional can be ''.
105 111 'booking_hash' => $request_params['booking_hash'],
106 112 'custom_form' => $request_params['custom_form'],
@@ -108,29 +114,37 @@
108 114 'is_show_payment_form' => 1,
109 115 'user_id' => $local_params['user_id'],
110 116 'request_uri' => $server_http_referer_uri,
111 117 'form_status' => $request_params['form_status'],
112 - 'allow_past' => $request_params['allow_past'],
118 + 'allow_past' => $request_params['allow_past'],
119 + 'classic_booking_context_token' => $request_params['classic_booking_context_token'],
113 120 'wpbc_bfb_preview' => $request_params['wpbc_bfb_preview'],
114 121 'wpbc_bfb_preview_token' => $request_params['wpbc_bfb_preview_token'],
115 122 'wpbc_bfb_preview_form_id' => $request_params['wpbc_bfb_preview_form_id'],
116 123 'wpbc_bfb_preview_nonce' => $request_params['wpbc_bfb_preview_nonce'],
117 124 'wpbc_time_override_enabled' => $request_params['wpbc_time_override_enabled'],
118 - 'wpbc_time_override_source' => $request_params['wpbc_time_override_source'],
119 - 'wpbc_time_override_start' => $request_params['wpbc_time_override_start'],
120 - 'wpbc_time_override_end' => $request_params['wpbc_time_override_end'],
121 - );
125 + 'wpbc_time_override_source' => $request_params['wpbc_time_override_source'],
126 + 'wpbc_time_override_start' => $request_params['wpbc_time_override_start'],
127 + 'wpbc_time_override_end' => $request_params['wpbc_time_override_end'],
128 + 'wpbc_admin_cost_correction' => $request_params['wpbc_admin_cost_correction'],
129 + );
130 + $request_save_params['service_id'] = $request_params['service_id'];
131 + $request_save_params['appointment_service_required'] = $request_params['appointment_service_required'];
132 + $request_save_params['appointment_context_token'] = $request_params['appointment_context_token'];
133 + $request_save_params['resource_selector_required'] = $request_params['resource_selector_required'];
134 + $request_save_params['resource_selector_context_token'] = $request_params['resource_selector_context_token'];
135 + $request_save_params['wpbc_admin_booking_nonce'] = $request_params['wpbc_admin_booking_nonce'];
122 136 $booking_save_arr = wpbc_booking_save( $request_save_params );
123 137
124 138 // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- BOOKING " >
125 139 if ( 'ok' !== $booking_save_arr['ajx_data']['status'] ) {
126 140
127 - wp_send_json( array( 'ajx_data' => $booking_save_arr['ajx_data'],
128 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
129 - 'ajx_search_params' => $_REQUEST[ $request_prefix ],
130 - 'ajx_cleaned_params' => $request_params,
131 - 'resource_id' => $request_params['resource_id']
132 - ));
141 + wp_send_json(
142 + array(
143 + 'ajx_data' => $booking_save_arr['ajx_data'],
144 + 'resource_id' => $request_params['resource_id'],
145 + )
146 + );
133 147 }
134 148 // </editor-fold>
135 149
136 150 $ajx_data_arr = $booking_save_arr['ajx_data'];
@@ -199,10 +213,94 @@
199 213 // ---------------------------------------------------------------------------------------------------------------------
200 214 // == Save Booking
201 215 // ---------------------------------------------------------------------------------------------------------------------
202 216
203 -/**
204 - * Save Booking - ADD NEW or UPDATE exist booking
217 +/**
218 + * Resolve and validate the final booking destination against current storage.
219 + *
220 + * This function contains the availability, Appointment working-time, and
221 + * Appointment buffer checks that must be repeated if the database connection
222 + * loses its advisory lock before persistence. The caller clears the relevant
223 + * request-local cache before every invocation.
224 + *
225 + * @param array $local_params Parsed booking parameters, passed by reference because force-save mode fixes capacity at one.
226 + * @param array $cleaned_params Sanitized booking request parameters.
227 + * @param array $php_performance Performance measurements, passed by reference.
228 + *
229 + * @return array|WP_Error Validated storage destination, or a visitor-safe validation error.
230 + */
231 +function wpbc_booking_validate_save_availability( &$local_params, $cleaned_params, &$php_performance ) {
232 +
233 + // Privileged imports and other established integrations may intentionally force a save.
234 + if ( ! empty( $cleaned_params['save_booking_even_if_unavailable'] ) ) {
235 + $local_params['how_many_items_to_book'] = 1;
236 + $dates_keys_arr = array_values( $local_params['dates_only_sql_arr'] );
237 + $resources_in_dates = array_fill_keys( $dates_keys_arr, array( $local_params['initial_resource_id'] ) );
238 + $where_to_save_booking = array(
239 + 'result' => 'ok',
240 + 'resources_in_dates' => $resources_in_dates,
241 + 'time_to_book' => $local_params['time_as_his_arr'],
242 + 'main__resource_id' => $local_params['initial_resource_id'],
243 + );
244 + } else {
245 + $php_performance = wpbc_php_performance_START( 'wpbc__where_to_save_booking', $php_performance );
246 +
247 + $where_to_save_booking = wpbc__where_to_save_booking(
248 + array(
249 + 'resource_id' => $local_params['initial_resource_id'],
250 + 'skip_booking_id' => $local_params['skip_booking_id'],
251 + 'dates_only_sql_arr' => $local_params['dates_only_sql_arr'],
252 + 'time_as_seconds_arr' => $local_params['time_as_seconds_arr'],
253 + 'how_many_items_to_book' => $local_params['how_many_items_to_book'],
254 + 'request_uri' => $cleaned_params['request_uri'],
255 + 'allow_past' => ! empty( $cleaned_params['allow_past'] ),
256 + 'is_use_booking_recurrent_time' => $local_params['is_use_booking_recurrent_time'],
257 + 'time_override_source' => ! empty( $local_params['time_override_arr']['source'] ) ? $local_params['time_override_arr']['source'] : '',
258 + 'as_single_resource' => false,
259 + 'aggregate_resource_id_arr' => $local_params['aggregate_resource_id_arr'],
260 + 'aggregate_type' => $cleaned_params['aggregate_type'],
261 + 'custom_form' => $cleaned_params['custom_form'],
262 + )
263 + );
264 +
265 + if ( 'error' === $where_to_save_booking['result'] ) {
266 + return new WP_Error( 'booking_can_not_save', $where_to_save_booking['message'] );
267 + }
268 +
269 + $php_performance = wpbc_php_performance_END( 'wpbc__where_to_save_booking', $php_performance );
270 + }
271 +
272 + if ( ! empty( $local_params['appointment_service'] ) && function_exists( 'wpbc_appointment_services_check_working_time' ) ) {
273 + $working_time_check = wpbc_appointment_services_check_working_time(
274 + $local_params['appointment_service'],
275 + $where_to_save_booking['main__resource_id'],
276 + array_keys( $where_to_save_booking['resources_in_dates'] ),
277 + $local_params['time_as_seconds_arr']
278 + );
279 + if ( is_wp_error( $working_time_check ) ) {
280 + return $working_time_check;
281 + }
282 + }
283 +
284 + if ( ! empty( $local_params['appointment_service'] ) && function_exists( 'wpbc_appointment_services_check_buffer_conflicts' ) ) {
285 + $buffer_check = wpbc_appointment_services_check_buffer_conflicts(
286 + $local_params['appointment_service'],
287 + $where_to_save_booking['main__resource_id'],
288 + array_keys( $where_to_save_booking['resources_in_dates'] ),
289 + $local_params['time_as_seconds_arr'],
290 + $local_params['skip_booking_id']
291 + );
292 + if ( is_wp_error( $buffer_check ) ) {
293 + return $buffer_check;
294 + }
295 + }
296 +
297 + return $where_to_save_booking;
298 +}
299 +
300 +
301 +/**
302 + * Save Booking - ADD NEW or UPDATE exist booking
205 303 *
206 304 * @param $request_params = [
207 305 * resource_id = 2 REQUIRED Default: 1
208 306 * dates_ddmmyy_csv = '27.10.2023, 28.10.2023, 29.10.2023' REQUIRED
@@ -252,11 +350,11 @@
252 350 // 1. Direct Clean Params
253 351 // -----------------------------------------------------------------------------------------------------------------
254 352 $server_request_uri = ( ( isset( $_SERVER['REQUEST_URI'] ) ) ? sanitize_text_field( $_SERVER['REQUEST_URI'] ) : '' ); /* phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash */ /* FixIn: sanitize_unslash */
255 353 $server_http_referer_uri = ( ( isset( $_SERVER['HTTP_REFERER'] ) ) ? sanitize_text_field( $_SERVER['HTTP_REFERER'] ) : '' ); /* phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash */ /* FixIn: sanitize_unslash */
256 - $validate_arr_rules = array(
257 - 'resource_id' => array( 'validate' => 'd', 'default' => 1 ), // INT
258 - 'dates_ddmmyy_csv' => array( 'validate' => 'csv_dates', 'default' => '' ), // FixIn: 9.9.1.1.
354 + $validate_arr_rules = array(
355 + 'resource_id' => array( 'validate' => 'd', 'default' => 1 ), // INT
356 + 'dates_ddmmyy_csv' => array( 'validate' => 'csv_dates', 'default' => '' ), // FixIn: 9.9.1.1.
259 357 'form_data' => array( 'validate' => 'strong', 'default' => '' ),
260 358 'booking_hash' => array( 'validate' => 'strong', 'default' => '' ),
261 359 'custom_form' => array( 'validate' => 'strong', 'default' => '' ),
262 360 'is_emails_send' => array( 'validate' => 'd', 'default' => 1 ), // 0 | 1
@@ -261,9 +359,10 @@
261 359 'custom_form' => array( 'validate' => 'strong', 'default' => '' ),
262 360 'is_emails_send' => array( 'validate' => 'd', 'default' => 1 ), // 0 | 1
263 361 'is_show_payment_form' => array( 'validate' => 'd', 'default' => 1 ), // 0 | 1
264 362 'user_id' => array( 'validate' => 'd', 'default' => wpbc_get_current_user_id() ), // INT
265 - 'allow_past' => array( 'validate' => 'd', 'default' => 0 ),
363 + 'allow_past' => array( 'validate' => 'd', 'default' => 0 ),
364 + 'classic_booking_context_token' => array( 'validate' => 'strong', 'default' => '' ),
266 365 'request_uri' => array( 'validate' => 'strong', 'default' => ( ( defined( 'DOING_AJAX' ) ) && ( DOING_AJAX ) ) ? $server_http_referer_uri : $server_request_uri ), // front-end: $server_request_uri | ajax: $server_http_referer_uri
267 366 // Really Optional:
268 367 'aggregate_resource_id_arr' => array( 'validate' => 'digit_or_csd', 'default' => '' ),
269 368 //TODO: this parameter does not transfer during saving, so here will be always default value 'bookings_only' // FixIn: 10.0.0.7.
@@ -278,17 +377,74 @@
278 377 'wpbc_bfb_preview_token' => array( 'validate' => 'strong', 'default' => '' ),
279 378 'wpbc_bfb_preview_form_id' => array( 'validate' => 'd', 'default' => 0 ),
280 379 'wpbc_bfb_preview_nonce' => array( 'validate' => 'strong', 'default' => '' ),
281 380 'wpbc_time_override_enabled' => array( 'validate' => 'd', 'default' => 0 ),
282 - 'wpbc_time_override_source' => array( 'validate' => 'strong', 'default' => '' ),
283 - 'wpbc_time_override_start' => array( 'validate' => 'strong', 'default' => '' ),
284 - 'wpbc_time_override_end' => array( 'validate' => 'strong', 'default' => '' ),
285 - );
286 - $re_cleaned_params = wpbc_sanitize_params_in_arr( $request_params, $validate_arr_rules );
287 -
288 - $admin_uri = ltrim( str_replace( get_site_url( null, '', 'admin' ), '', admin_url( 'admin.php?' ) ), '/' ); // wp-admin/admin.php?
289 -
290 - $re_cleaned_params['form_status'] = sanitize_key( $re_cleaned_params['form_status'] );
381 + 'wpbc_time_override_source' => array( 'validate' => 'strong', 'default' => '' ),
382 + 'wpbc_time_override_start' => array( 'validate' => 'strong', 'default' => '' ),
383 + 'wpbc_time_override_end' => array( 'validate' => 'strong', 'default' => '' ),
384 + 'wpbc_admin_cost_correction' => array( 'validate' => 'strong', 'default' => '' ),
385 + );
386 + $validate_arr_rules['service_id'] = array( 'validate' => 'd', 'default' => 0 );
387 + $validate_arr_rules['appointment_service_required'] = array( 'validate' => 'd', 'default' => 0 );
388 + $validate_arr_rules['appointment_context_token'] = array( 'validate' => 'strong', 'default' => '' );
389 + $validate_arr_rules['resource_selector_required'] = array( 'validate' => 'd', 'default' => 0 );
390 + $validate_arr_rules['resource_selector_context_token'] = array( 'validate' => 'strong', 'default' => '' );
391 + $validate_arr_rules['wpbc_admin_booking_nonce'] = array( 'validate' => 'strong', 'default' => '' );
392 + $re_cleaned_params = wpbc_sanitize_params_in_arr( $request_params, $validate_arr_rules );
393 + $has_verified_appointment_context = false;
394 + $has_verified_resource_selector_context = false;
395 + if ( ! empty( $re_cleaned_params['appointment_service_required'] ) && empty( $re_cleaned_params['service_id'] ) ) {
396 + $ajx_data_arr['status'] = 'error';
397 + $ajx_data_arr['status_error'] = 'appointment_service_required';
398 + $ajx_data_arr['ajx_after_action_message'] = __( 'Please select a Service.', 'booking' );
399 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
400 + return array( 'ajx_data' => $ajx_data_arr );
401 + }
402 + if ( ! empty( $re_cleaned_params['service_id'] ) ) {
403 + if ( ! function_exists( 'wpbc_booking_appointment_validate_submission_context' ) ) {
404 + $appointment_context_check = new WP_Error( 'appointment_context_unavailable', __( 'The Appointment selection cannot be verified. Please reload the page and try again.', 'booking' ) );
405 + } else {
406 + $appointment_context_check = wpbc_booking_appointment_validate_submission_context(
407 + $re_cleaned_params['appointment_context_token'],
408 + $re_cleaned_params['service_id'],
409 + $re_cleaned_params['resource_id']
410 + );
411 + }
412 + if ( is_wp_error( $appointment_context_check ) ) {
413 + $ajx_data_arr['status'] = 'error';
414 + $ajx_data_arr['status_error'] = $appointment_context_check->get_error_code();
415 + $ajx_data_arr['ajx_after_action_message'] = $appointment_context_check->get_error_message();
416 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
417 + return array( 'ajx_data' => $ajx_data_arr );
418 + }
419 + $has_verified_appointment_context = true;
420 +
421 + // A client value cannot enable past Appointment creation; trust only the site-authored signed context.
422 + $re_cleaned_params['allow_past'] = wpbc_booking_appointment_is_past_booking_enabled( $appointment_context_check ) ? 1 : 0;
423 + }
424 + if ( ! empty( $re_cleaned_params['resource_selector_required'] ) || ! empty( $re_cleaned_params['resource_selector_context_token'] ) ) {
425 + if ( ! function_exists( 'wpbc_booking_resource_selector_validate_submission_context' ) ) {
426 + $resource_selector_context_check = new WP_Error( 'resource_selector_context_unavailable', __( 'The Booking Resource selection cannot be verified. Please reload the page and try again.', 'booking' ) );
427 + } else {
428 + $resource_selector_context_check = wpbc_booking_resource_selector_validate_submission_context(
429 + $re_cleaned_params['resource_selector_context_token'],
430 + $re_cleaned_params['resource_id']
431 + );
432 + }
433 + if ( is_wp_error( $resource_selector_context_check ) ) {
434 + $ajx_data_arr['status'] = 'error';
435 + $ajx_data_arr['status_error'] = $resource_selector_context_check->get_error_code();
436 + $ajx_data_arr['ajx_after_action_message'] = $resource_selector_context_check->get_error_message();
437 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
438 + return array( 'ajx_data' => $ajx_data_arr );
439 + }
440 + $has_verified_resource_selector_context = true;
441 +
442 + // Trust only the site-authored signed selector context for public past bookings.
443 + $re_cleaned_params['allow_past'] = wpbc_booking_resource_selector_is_past_booking_enabled( $resource_selector_context_check ) ? 1 : 0;
444 + }
445 +
446 + $re_cleaned_params['form_status'] = sanitize_key( $re_cleaned_params['form_status'] );
291 447 if ( 'preview' !== $re_cleaned_params['form_status'] ) {
292 448 $re_cleaned_params['form_status'] = 'published';
293 449 }
294 450 // FixIn: 2026-02-05 - make preview/published available to form parsing/templates during this request.
@@ -305,14 +461,26 @@
305 461
306 462 // -----------------------------------------------------------------------------------------------------------------
307 463 // Local parameters
308 464 // -----------------------------------------------------------------------------------------------------------------
309 - $local_params = array();
310 - $local_params['is_from_admin_panel'] = ( false !== strpos( $re_cleaned_params['request_uri'], $admin_uri ) ); // true | false
465 + $local_params = array();
466 + $is_authorized_admin_booking_request = wpbc_is_authorized_admin_booking_request( $re_cleaned_params['wpbc_admin_booking_nonce'] );
467 + $local_params['is_from_admin_panel'] = $is_authorized_admin_booking_request;
311 468 $local_params['user_id'] = $re_cleaned_params['user_id']; // 1
312 - $local_params['sync_gid'] = $re_cleaned_params['sync_gid']; // ''
313 - $local_params['is_approve_booking'] = $re_cleaned_params['is_approve_booking']; // 0 | 1
314 - $local_params['is_use_booking_recurrent_time'] = ( 1 === $re_cleaned_params['is_use_booking_recurrent_time'] ); // false | true
469 + $local_params['sync_gid'] = $re_cleaned_params['sync_gid']; // ''
470 + $local_params['is_approve_booking'] = $re_cleaned_params['is_approve_booking']; // 0 | 1
471 + $local_params['is_use_booking_recurrent_time'] = ( 1 === $re_cleaned_params['is_use_booking_recurrent_time'] ); // false | true
472 + $request_action = isset( $_REQUEST['action'] ) && is_scalar( $_REQUEST['action'] )
473 + ? sanitize_key( (string) wp_unslash( $_REQUEST['action'] ) )
474 + : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
475 + $is_public_booking_create_request = wp_doing_ajax()
476 + && 'wpbc_ajx_booking__create' === strtolower( $request_action )
477 + && ! $is_authorized_admin_booking_request;
478 +
479 + // Time overrides belong exclusively to the capability-protected Add Booking administration workflow.
480 + $re_cleaned_params = wpbc_restrict_booking_time_override_to_authorized_admin( $re_cleaned_params, $is_authorized_admin_booking_request );
481 + // Cost corrections belong exclusively to capability-protected administrator booking workflows.
482 + $re_cleaned_params = wpbc_restrict_booking_cost_correction_to_authorized_admin( $re_cleaned_params, $is_authorized_admin_booking_request );
315 483
316 484 // -----------------------------------------------------------------------------------------------------------------
317 485 // Parse Local parameters for later use
318 486 // -----------------------------------------------------------------------------------------------------------------
@@ -340,10 +508,71 @@
340 508 'name' => 'endtime',
341 509 'value' => $local_params['time_override_arr']['end'],
342 510 );
343 511 }
344 - // Important! : [ 64800, 72000 ]
345 - $local_params['time_as_seconds_arr'] = wpbc_get_in_booking_form__time_to_book_as_seconds_arr( $local_params['structured_booking_data_arr'] );
512 + // Important! : [ 64800, 72000 ]
513 + $local_params['time_as_seconds_arr'] = wpbc_get_in_booking_form__time_to_book_as_seconds_arr( $local_params['structured_booking_data_arr'] );
514 + $local_params['appointment_service'] = array();
515 + if ( ! empty( $re_cleaned_params['service_id'] ) && function_exists( 'wpbc_appointment_services_repository' ) ) {
516 + $range_time_value = isset( $local_params['structured_booking_data_arr']['rangetime'] ) ? $local_params['structured_booking_data_arr']['rangetime'] : '';
517 + $start_time_value = isset( $local_params['structured_booking_data_arr']['starttime'] ) ? $local_params['structured_booking_data_arr']['starttime'] : '';
518 + $range_time_value = is_array( $range_time_value ) ? implode( '', $range_time_value ) : $range_time_value;
519 + $start_time_value = is_array( $start_time_value ) ? implode( '', $start_time_value ) : $start_time_value;
520 + $has_appointment_time = ! empty( $local_params['time_override_arr'] )
521 + || '' !== trim( (string) $range_time_value )
522 + || '' !== trim( (string) $start_time_value );
523 + if ( ! $has_appointment_time ) {
524 + $ajx_data_arr['status'] = 'error';
525 + $ajx_data_arr['status_error'] = 'appointment_service_time_required';
526 + $ajx_data_arr['ajx_after_action_message'] = __( 'A Service appointment requires a start time. Add a time field to the Booking Form and select a time.', 'booking' );
527 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
528 + return array( 'ajx_data' => $ajx_data_arr );
529 + }
530 + $appointment_service = wpbc_appointment_services_repository()->find_active_for_resource( $re_cleaned_params['service_id'], $re_cleaned_params['resource_id'] );
531 + if ( is_wp_error( $appointment_service ) ) {
532 + $ajx_data_arr['status'] = 'error';
533 + $ajx_data_arr['status_error'] = 'appointment_service_unavailable';
534 + $ajx_data_arr['ajx_after_action_message'] = $appointment_service->get_error_message();
535 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
536 + return array( 'ajx_data' => $ajx_data_arr );
537 + }
538 + if ( count( $local_params['time_as_seconds_arr'] ) < 2 || ! function_exists( 'wpbc_appointment_services_resolve_end_seconds' ) ) {
539 + $ajx_data_arr['status'] = 'error';
540 + $ajx_data_arr['status_error'] = 'appointment_service_duration_invalid';
541 + $ajx_data_arr['ajx_after_action_message'] = __( 'The selected Service duration is invalid. Please contact the website administrator.', 'booking' );
542 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
543 + return array( 'ajx_data' => $ajx_data_arr );
544 + }
545 + $maximum_duration_minutes = absint( apply_filters( 'wpbc_booking_appointment_maximum_duration_minutes', 24 * 60, array() ) );
546 + $service_end_second = wpbc_appointment_services_resolve_end_seconds( $appointment_service, $local_params['time_as_seconds_arr'][0], $maximum_duration_minutes );
547 + if ( is_wp_error( $service_end_second ) ) {
548 + $ajx_data_arr['status'] = 'error';
549 + $ajx_data_arr['status_error'] = $service_end_second->get_error_code();
550 + $ajx_data_arr['ajx_after_action_message'] = $service_end_second->get_error_message();
551 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
552 + return array( 'ajx_data' => $ajx_data_arr );
553 + }
554 + $local_params['time_as_seconds_arr'][1] = $service_end_second;
555 + $local_params['appointment_service'] = $appointment_service;
556 + $service_start_time = wpbc_transform__seconds__in__24_hours_his( $local_params['time_as_seconds_arr'][0] );
557 + $service_end_time = wpbc_transform__seconds__in__24_hours_his( $local_params['time_as_seconds_arr'][1] );
558 + unset( $local_params['structured_booking_data_arr']['rangetime'], $local_params['structured_booking_data_arr']['durationtime'] );
559 + $local_params['structured_booking_data_arr']['starttime'] = $service_start_time;
560 + $local_params['structured_booking_data_arr']['endtime'] = $service_end_time;
561 + unset( $local_params['all_booking_data_arr']['rangetime'], $local_params['all_booking_data_arr']['durationtime'] );
562 + $local_params['all_booking_data_arr']['starttime'] = array( 'type' => 'text', 'original_name' => 'starttime' . $re_cleaned_params['resource_id'], 'name' => 'starttime', 'value' => $service_start_time );
563 + $local_params['all_booking_data_arr']['endtime'] = array( 'type' => 'text', 'original_name' => 'endtime' . $re_cleaned_params['resource_id'], 'name' => 'endtime', 'value' => $service_end_time );
564 + }
565 + if ( function_exists( 'wpbc_appointment_services_sync_service_hint_booking_data' ) ) {
566 + $service_hint_booking_data = wpbc_appointment_services_sync_service_hint_booking_data(
567 + $local_params['structured_booking_data_arr'],
568 + $local_params['all_booking_data_arr'],
569 + $local_params['appointment_service'],
570 + $re_cleaned_params['resource_id']
571 + );
572 + $local_params['structured_booking_data_arr'] = $service_hint_booking_data['structured_booking_data'];
573 + $local_params['all_booking_data_arr'] = $service_hint_booking_data['all_booking_data'];
574 + }
346 575 // [ "18:00:00", "20:00:00" ]
347 576 $time_as_seconds_arr = $local_params['time_as_seconds_arr'];
348 577 $time_as_seconds_arr[0] = ( 0 != $time_as_seconds_arr[0] ) ? $time_as_seconds_arr[0] + 1 : $time_as_seconds_arr[0]; // set check in time with ended 1 second
349 578 $time_as_seconds_arr[1] = ( ( 24 * 60 * 60 ) != $time_as_seconds_arr[1] ) ? $time_as_seconds_arr[1] + 2 : $time_as_seconds_arr[1]; // set check out time with ended 2 seconds
@@ -355,19 +584,77 @@
355 584 wpbc_transform__seconds__in__24_hours_his( $time_as_seconds_arr[0] ),
356 585 wpbc_transform__seconds__in__24_hours_his( $time_as_seconds_arr[1] )
357 586 );
358 587 // [ '2023-09-10', '2023-09-11' ]
359 - $local_params['dates_only_sql_arr'] = wpbc_convert_dates_str__dd_mm_yyyy__to__yyyy_mm_dd( $re_cleaned_params["dates_ddmmyy_csv"] );
360 - $local_params['dates_only_sql_arr'] = explode( ',', $local_params['dates_only_sql_arr'] );
588 + $local_params['dates_only_sql_arr'] = wpbc_convert_dates_str__dd_mm_yyyy__to__yyyy_mm_dd( $re_cleaned_params["dates_ddmmyy_csv"] );
589 + $local_params['dates_only_sql_arr'] = explode( ',', $local_params['dates_only_sql_arr'] );
590 +
591 + $classic_context = array();
592 + $has_verified_classic_context = false;
593 + if ( ! empty( $re_cleaned_params['classic_booking_context_token'] ) && function_exists( 'wpbc_classic_booking_context_validate_submission' ) ) {
594 + $classic_context = wpbc_classic_booking_context_validate_submission(
595 + $re_cleaned_params['classic_booking_context_token'],
596 + $re_cleaned_params['resource_id'],
597 + $local_params['dates_only_sql_arr'],
598 + $re_cleaned_params['custom_form'],
599 + $re_cleaned_params['aggregate_resource_id_arr']
600 + );
601 + if ( is_wp_error( $classic_context ) ) {
602 + $ajx_data_arr['status'] = 'error';
603 + $ajx_data_arr['status_error'] = $classic_context->get_error_code();
604 + $ajx_data_arr['ajx_after_action_message'] = $classic_context->get_error_message();
605 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
606 + return array( 'ajx_data' => $ajx_data_arr );
607 + }
608 +
609 + $has_verified_classic_context = true;
610 + $re_cleaned_params['allow_past'] = ! empty( $classic_context['allow_past'] ) ? 1 : 0;
611 + // Pass only the signed canonical set into final availability and persistence decisions.
612 + $re_cleaned_params['aggregate_resource_id_arr'] = implode( ',', $classic_context['aggregate_resource_ids'] );
613 + }
614 +
615 + if ( $is_public_booking_create_request && ! $has_verified_classic_context ) {
616 + $ajx_data_arr['status'] = 'error';
617 + $ajx_data_arr['status_error'] = 'classic_booking_context_required';
618 + $ajx_data_arr['ajx_after_action_message'] = wpbc_classic_booking_context_get_visitor_message( 'message_booking_form_context_required', $re_cleaned_params['resource_id'] );
619 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
620 + return array( 'ajx_data' => $ajx_data_arr );
621 + }
622 +
623 + if ( $has_verified_classic_context ) {
624 + $workflow_context_error = wpbc_booking_create_validate_required_workflow(
625 + $classic_context,
626 + $has_verified_appointment_context,
627 + $has_verified_resource_selector_context
628 + );
629 + if ( is_wp_error( $workflow_context_error ) ) {
630 + $ajx_data_arr['status'] = 'error';
631 + $ajx_data_arr['status_error'] = $workflow_context_error->get_error_code();
632 + $ajx_data_arr['ajx_after_action_message'] = $workflow_context_error->get_error_message();
633 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
634 + return array( 'ajx_data' => $ajx_data_arr );
635 + }
636 + }
637 +
638 + if (
639 + ( ! empty( $local_params['time_override_arr'] ) )
640 + && ( 'times_availability' === $local_params['time_override_arr']['source'] )
641 + && ( count( array_filter( $local_params['dates_only_sql_arr'] ) ) > 1 )
642 + ) {
643 + $local_params['is_use_booking_recurrent_time'] = true;
644 + }
361 645
362 646 $local_params['is_show_payment_form'] = $re_cleaned_params["is_show_payment_form"];
363 647
364 648 // FixIn: 9.9.0.35.
365 - if ( $local_params['is_show_payment_form'] ) {
366 - $local_params['is_show_payment_form'] = ( false !== strpos( $re_cleaned_params['request_uri'], 'is_show_payment_form=Off' ) )
367 - ? 0
368 - : $local_params['is_show_payment_form']; // 1|0
369 - }
649 + if ( $local_params['is_show_payment_form'] ) {
650 + $local_params['is_show_payment_form'] = (
651 + $is_authorized_admin_booking_request
652 + && false !== strpos( $re_cleaned_params['request_uri'], 'is_show_payment_form=Off' )
653 + )
654 + ? 0
655 + : $local_params['is_show_payment_form']; // 1|0
656 + }
370 657
371 658 // Get EDIT booking data
372 659 $local_params['edit_resource_id'] = '';
373 660 $local_params['skip_booking_id'] = '';
@@ -373,27 +660,44 @@
373 660 $local_params['skip_booking_id'] = '';
374 661 $local_params['is_edit_booking'] = 0;
375 662 $local_params['is_duplicate_booking'] = 0;
376 663 $is_edit_booking = wpbc_get_data__if_edit_booking( $re_cleaned_params['booking_hash'], $re_cleaned_params['request_uri'] );
377 - if ( false !== $is_edit_booking ) {
378 - $local_params['edit_resource_id'] = $is_edit_booking['resource_id']; // can be parent booking resource, where we edit the booking
379 - $local_params['skip_booking_id'] = $is_edit_booking['booking_id']; // booking ID
380 - $local_params['is_edit_booking'] = $is_edit_booking['booking_id']; // booking ID
664 + if ( false !== $is_edit_booking ) {
665 + $local_params['edit_resource_id'] = $is_edit_booking['resource_id']; // can be parent booking resource, where we edit the booking
666 + $local_params['skip_booking_id'] = $is_edit_booking['booking_id']; // booking ID
667 + $local_params['is_edit_booking'] = $is_edit_booking['booking_id']; // booking ID
381 668 if (
382 669 ( ! empty( $local_params['structured_booking_data_arr']['wpbc_other_action'] ) )
383 670 && ( 'duplicate_booking' === $local_params['structured_booking_data_arr']['wpbc_other_action'] )
384 671 ){
385 - $local_params['is_duplicate_booking'] = 1;
386 - }
387 - }
388 - // It can be request resource ID or if we edit booking, it can be 'edit resource' - (e.g. child resource)
672 + $local_params['is_duplicate_booking'] = 1;
673 + }
674 + }
675 +
676 + $is_frontend_ajax_edit = wp_doing_ajax()
677 + && 'wpbc_ajx_booking__create' === strtolower( $request_action )
678 + && 0 !== $local_params['is_edit_booking'];
679 + $is_authorized_admin_edit = $is_authorized_admin_booking_request;
680 +
681 + if (
682 + $is_frontend_ajax_edit
683 + && ! $is_authorized_admin_edit
684 + && ! wpbc_is_visitor_booking_action_allowed( $local_params['is_edit_booking'] )
685 + ) {
686 + $ajx_data_arr['status'] = 'error';
687 + $ajx_data_arr['status_error'] = 'visitor_booking_dates_in_past';
688 + $ajx_data_arr['ajx_after_action_message'] = __( 'This booking can no longer be edited because its dates have already passed.', 'booking' );
689 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
690 + return array( 'ajx_data' => $ajx_data_arr );
691 + }
692 + // It can be request resource ID or if we edit booking, it can be 'edit resource' - (e.g. child resource)
389 693 $local_params['initial_resource_id'] = ( ! empty( $local_params['edit_resource_id'] ) ) ? $local_params['edit_resource_id'] : $re_cleaned_params['resource_id'];
390 694
391 - // 2
392 - $local_params['how_many_items_to_book'] = wpbc_get__how_many_items_to_book__in_booking_form( $local_params['structured_booking_data_arr'], $local_params['initial_resource_id'] );
393 -
394 -
395 - $local_params['aggregate_resource_id_arr'] = explode( ',', $re_cleaned_params['aggregate_resource_id_arr'] );
695 + // 2
696 + $local_params['how_many_items_to_book'] = wpbc_get__how_many_items_to_book__in_booking_form( $local_params['structured_booking_data_arr'], $local_params['initial_resource_id'] );
697 +
698 +
699 + $local_params['aggregate_resource_id_arr'] = explode( ',', $re_cleaned_params['aggregate_resource_id_arr'] );
396 700 $local_params['aggregate_resource_id_arr'] = array_filter( $local_params['aggregate_resource_id_arr'] ); // All entries of array equal to FALSE (0, '', '0' ) will be removed.
397 701 $local_params['aggregate_resource_id_arr'] = array_unique( $local_params['aggregate_resource_id_arr'] ); // Erase duplicates
398 702
399 703 // -----------------------------------------------------------------------------------------------------------------
@@ -399,124 +703,110 @@
399 703 // -----------------------------------------------------------------------------------------------------------------
400 704 // Here GO
401 705 // -----------------------------------------------------------------------------------------------------------------
402 706
403 - // Force - resource saving parameters, instead of wpbc__where_to_save_booking()
404 - if ( ! empty( $re_cleaned_params["save_booking_even_if_unavailable"] ) ) {
707 + $availability_guard = wpbc_booking_availability_guard_acquire();
708 + if ( is_wp_error( $availability_guard ) ) {
709 + $ajx_data_arr['status'] = 'error';
710 + $ajx_data_arr['status_error'] = $availability_guard->get_error_code();
711 + $ajx_data_arr['ajx_after_action_message'] = $availability_guard->get_error_message();
712 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
713 +
714 + return array( 'ajx_data' => $ajx_data_arr );
715 + }
716 +
717 + $guard_revalidation_attempts = 0;
718 + try {
719 + while ( true ) {
720 + wpbc_cache__clear( 'wpbc__sql__get_booking_dates' );
721 + $where_to_save_booking = wpbc_booking_validate_save_availability( $local_params, $re_cleaned_params, $php_performance );
722 +
723 + if ( is_wp_error( $where_to_save_booking ) ) {
724 + $ajx_data_arr['status'] = 'error';
725 + $ajx_data_arr['status_error'] = $where_to_save_booking->get_error_code();
726 + $ajx_data_arr['ajx_after_action_message'] = $where_to_save_booking->get_error_message();
727 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
728 +
729 + return array( 'ajx_data' => $ajx_data_arr );
730 + }
731 +
732 + // Get parameters, from REQUEST.
733 + $create_params = $local_params;
734 + $create_params['resource_id'] = ( ! empty( $local_params['edit_resource_id'] ) )
735 + ? $local_params['edit_resource_id']
736 + : $where_to_save_booking['main__resource_id'];
737 + $create_params['is_emails_send'] = $re_cleaned_params['is_emails_send'];
738 + $create_params['custom_form'] = $re_cleaned_params['custom_form'];
739 +
740 + make_bk_action( 'check_multiuser_params_for_client_side', $create_params['resource_id'] );
741 +
742 + $create_booking_params = array(
743 + 'resource_id' => $create_params['resource_id'],
744 + 'custom_form' => $create_params['custom_form'],
745 + 'all_booking_data_arr' => $create_params['all_booking_data_arr'],
746 + 'dates_only_sql_arr' => $create_params['dates_only_sql_arr'],
747 + 'time_as_his_arr' => $create_params['time_as_his_arr'],
748 + 'is_from_admin_panel' => $create_params['is_from_admin_panel'],
749 + 'is_edit_booking' => $create_params['is_edit_booking'],
750 + 'is_duplicate_booking' => $create_params['is_duplicate_booking'],
751 + 'is_approve_booking' => $create_params['is_approve_booking'],
752 + 'how_many_items_to_book' => $create_params['how_many_items_to_book'],
753 + 'is_use_booking_recurrent_time' => $create_params['is_use_booking_recurrent_time'],
754 + );
755 + if ( ! empty( $create_params['appointment_service'] ) ) {
756 + $create_booking_params['appointment_service'] = $create_params['appointment_service'];
757 + }
758 + if ( ! empty( $create_params['sync_gid'] ) ) {
759 + $create_booking_params['sync_gid'] = $create_params['sync_gid'];
760 + }
761 +
762 + if ( ! wpbc_booking_availability_guard_is_owned( $availability_guard ) ) {
763 + wpbc_booking_availability_guard_release( $availability_guard );
764 + if ( 1 <= $guard_revalidation_attempts ) {
765 + $availability_guard = wpbc_booking_availability_guard_get_busy_error();
766 + } else {
767 + ++$guard_revalidation_attempts;
768 + $availability_guard = wpbc_booking_availability_guard_acquire();
769 + }
770 +
771 + if ( is_wp_error( $availability_guard ) ) {
772 + $ajx_data_arr['status'] = 'error';
773 + $ajx_data_arr['status_error'] = $availability_guard->get_error_code();
774 + $ajx_data_arr['ajx_after_action_message'] = $availability_guard->get_error_message();
775 + $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
776 +
777 + return array( 'ajx_data' => $ajx_data_arr );
778 + }
779 +
780 + continue;
781 + }
782 +
783 + $php_performance = wpbc_php_performance_START( 'wpbc_db__booking_save', $php_performance );
784 + $booking_new_arr = wpbc_db__booking_save( $create_booking_params, $where_to_save_booking );
785 + if ( 'ok' !== $booking_new_arr['status'] ) {
786 + $ajx_data_arr['status'] = $booking_new_arr['status'];
787 + $ajx_data_arr['status_error'] = 'booking_can_not_save';
788 + $ajx_data_arr['ajx_after_action_message'] = $booking_new_arr['message'];
789 + $ajx_data_arr['ajx_after_action_message_status'] = 'error';
790 +
791 + return array( 'ajx_data' => $ajx_data_arr );
792 + }
793 +
794 + // Appointment buffers must become visible before the serialized availability section ends.
795 + if ( function_exists( 'wpbc_appointment_services_after_booking_save' ) ) {
796 + wpbc_appointment_services_after_booking_save( $booking_new_arr['booking_id'], $create_booking_params, $where_to_save_booking );
797 + }
798 +
799 + break;
800 + }
801 + } finally {
802 + wpbc_cache__clear( 'wpbc__sql__get_booking_dates' );
803 + wpbc_booking_availability_guard_release( $availability_guard );
804 + }
805 +
806 + // Released compatibility hook: arbitrary callbacks must not extend the database lock duration.
807 + do_action( 'wpbc_booking_after_save', $booking_new_arr['booking_id'], $create_booking_params, $where_to_save_booking );
405 808
406 - $local_params['how_many_items_to_book'] = 1;
407 -
408 - $dates_keys_arr = array_values( $local_params['dates_only_sql_arr'] ); // [ '2023-09-23', '2023-09-24' ]
409 -
410 - $resources_in_dates = array_fill_keys( $dates_keys_arr , array( $local_params['initial_resource_id'] ) ); // [ 2023-09-23 = [ 2 ], 2023-09-24 = [ 2 ] ]
411 -
412 - $where_to_save_booking = array();
413 - $where_to_save_booking['result'] = 'ok';
414 - $where_to_save_booking['resources_in_dates'] = $resources_in_dates; // [ 2023-09-23 = [ 2, 10, 11 ], 2023-09-24 = [ 2, 10, 11 ]
415 - $where_to_save_booking['time_to_book'] = $local_params['time_as_his_arr']; // [ "00:00:00", "24:00:00" ]
416 - $where_to_save_booking['main__resource_id'] = $local_params['initial_resource_id']; // here edit or request (parent/single) resource
417 -
418 - } else {
419 - // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " >
420 - $php_performance = wpbc_php_performance_START( 'wpbc__where_to_save_booking' , $php_performance );
421 - // </editor-fold>
422 -
423 - /**
424 - * Get slots [] where we can save booking = [ 'resources_in_dates' => [ 2023-10-18 = [ 2, 12, 10, 11 ]
425 - * 2023-10-19 = [ 2, 12, 10, 11 ]
426 - * 2023-10-20 = [ 2, 12, 10, 11 ]
427 - * ],
428 - * 'time_to_book' => [ "14:00:01" , "12:00:01" ],
429 - * 'result' => 'ok'
430 - * 'main__resource_id' => 2
431 - * ]
432 - * OR
433 - * [ 'result' => 'error', 'message' => 'Booking can not be saved ...' ]
434 - */
435 - $where_to_save_booking = wpbc__where_to_save_booking( array(
436 - 'resource_id' => $local_params['initial_resource_id'], // 2 //TODO: If edit booking. What to pass 'edit' or 'parent' resource ID?
437 - 'skip_booking_id' => $local_params['skip_booking_id'], // '', | 125 if edit booking
438 - 'dates_only_sql_arr' => $local_params['dates_only_sql_arr'], // [ "2023-10-18", "2023-10-25", "2023-11-25" ]
439 - 'time_as_seconds_arr' => $local_params['time_as_seconds_arr'], // [ 36000, 39600 ]
440 - 'how_many_items_to_book' => $local_params['how_many_items_to_book'], // 1
441 - 'request_uri' => $re_cleaned_params['request_uri'], // 'http://beta/resource-id2/'
442 - 'allow_past' => ! empty( $re_cleaned_params['allow_past'] ),
443 - 'is_use_booking_recurrent_time' => $local_params['is_use_booking_recurrent_time'], // true | false
444 - 'as_single_resource' => false, // false
445 - 'aggregate_resource_id_arr' => $local_params['aggregate_resource_id_arr'], // Optional can be ''
446 - 'aggregate_type' => $re_cleaned_params['aggregate_type'], //TODO: this parameter does not transfer during saving, so here will be always default value 'bookings_only' // FixIn: 10.0.0.7.
447 - 'custom_form' => $re_cleaned_params['custom_form'] // FixIn: 10.0.0.10.
448 - ));
449 - // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- NO SLOTS TO SAVE " >
450 - if ( 'error' == $where_to_save_booking['result'] ) {
451 - $ajx_data_arr['status'] = 'error';
452 - $ajx_data_arr['status_error'] = 'booking_can_not_save';
453 - $ajx_data_arr['ajx_after_action_message'] = $where_to_save_booking['message'];
454 - $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
455 - return array( 'ajx_data' => $ajx_data_arr );
456 - }
457 - // </editor-fold>
458 -
459 - // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " >
460 - $php_performance = wpbc_php_performance_END( 'wpbc__where_to_save_booking' , $php_performance );
461 - // </editor-fold>
462 - }
463 -
464 -
465 - // Get parameters, from REQUEST
466 - $create_params = $local_params;
467 - $create_params['resource_id'] = ( ! empty( $local_params['edit_resource_id'] ) )
468 - ? $local_params['edit_resource_id'] // If we edit, then use original resource ???
469 - : $where_to_save_booking['main__resource_id']; // Here is important TIP, resource can be where is free, and not where we submit
470 - /**
471 - * TODO: I think it's resolved! Just test about this situation, when we edit the booking - and it's means that we have $local_params['edit_resource_id']
472 - * but what, if $where_to_save_booking do not contain this $local_params['edit_resource_id'] as available resource.
473 - * or even we have $local_params['edit_resource_id'] = 2 and $where_to_save_booking contain resources like [ 1, 2, 3, 4 ]
474 - * we make booking for 3 slots
475 - * in this case, main resource will be 2
476 - * but then when we loop resources in wpbc_db__booking_save() we will save child booking resources for dates like: 2, 3, 4 ( and it's wrong )
477 - * "(205, '2023-10-04 00:00:00', 0, NULL)" <- main resource '2' e.g. $local_params['edit_resource_id'] = 2
478 - * "(205, '2023-10-04 00:00:00', 0, 2)" ? <- child resource '2' e.g. [ .., 2, .. ] in $where_to_save_booking WHICH IS WRONG
479 - */
480 - $create_params['is_emails_send'] = $re_cleaned_params['is_emails_send'];
481 - $create_params['custom_form'] = $re_cleaned_params['custom_form'];
482 -
483 - make_bk_action( 'check_multiuser_params_for_client_side', $create_params['resource_id'] ); // Activate working with specific user in WP MU
484 -
485 - // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " >
486 - $php_performance = wpbc_php_performance_START( 'wpbc_db__booking_save' , $php_performance );
487 - // </editor-fold>
488 -
489 - // -----------------------------------------------------------------------------------------------------------------
490 - // == CREATE_THE 'NEW_BOOKING' ==
491 - // -----------------------------------------------------------------------------------------------------------------
492 - $create_booking_params = array(
493 - 'resource_id' => $create_params['resource_id'],
494 - 'custom_form' => $create_params['custom_form'],
495 - 'all_booking_data_arr' => $create_params['all_booking_data_arr'],
496 - 'dates_only_sql_arr' => $create_params['dates_only_sql_arr'],
497 - 'time_as_his_arr' => $create_params['time_as_his_arr'],
498 - 'is_from_admin_panel' => $create_params['is_from_admin_panel'],
499 - 'is_edit_booking' => $create_params['is_edit_booking'],
500 - 'is_duplicate_booking' => $create_params['is_duplicate_booking'],
501 - 'is_approve_booking' => $create_params['is_approve_booking'],
502 - 'how_many_items_to_book' => $create_params['how_many_items_to_book'],
503 - 'is_use_booking_recurrent_time' => $create_params['is_use_booking_recurrent_time'] // true | false
504 - );
505 - if ( ! empty( $create_params['sync_gid'] ) ) { $create_booking_params['sync_gid'] = $create_params['sync_gid']; }
506 -
507 - $booking_new_arr = wpbc_db__booking_save( $create_booking_params, $where_to_save_booking );
508 -
509 - // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- BOOKING CREATION " >
510 - if ( 'ok' !== $booking_new_arr['status'] ) {
511 - $ajx_data_arr['status'] = $booking_new_arr['status'];
512 - $ajx_data_arr['status_error'] = 'booking_can_not_save';
513 - $ajx_data_arr['ajx_after_action_message'] = $booking_new_arr['message'];
514 - $ajx_data_arr['ajx_after_action_message_status'] = 'error';
515 - return array( 'ajx_data' => $ajx_data_arr );
516 - }
517 - // </editor-fold>
518 -
519 809 // FixIn: 9.9.0.36.
520 810 if (
521 811 ( 0 !== $create_params['is_edit_booking'] ) // If edit booking
522 812 && ( 1 != $create_params['is_duplicate_booking'] ) // If not duplicate
@@ -543,9 +833,10 @@
543 833 );
544 834 $str_dates__dd_mm_yyyy = wpbc_convert_dates_arr__yyyy_mm_dd__to__dd_mm_yyyy( $payment_params['booked_dates_times_arr']['dates_ymd_arr'] ); // ['2023-10-20','2023-10-25'] => ['20.10.2023','25.10.2023']
545 835 $payment_params['str_dates__dd_mm_yyyy'] = implode( ',', $str_dates__dd_mm_yyyy ); // REQUIRED -- '14.11.2023, 15.11.2023, 16.11.2023, 17.11.2023'
546 836 $payment_params['booking_id'] = $booking_new_arr['booking_id']; // REQUIRED -- '2'
547 - $payment_params['resource_id'] = $create_params['resource_id']; // REQUIRED -- '2' can be child resource (changed in wpbc_where_to_save() )
837 + $payment_params['resource_id'] = $create_params['resource_id']; // REQUIRED -- '2' can be child resource (changed in wpbc_where_to_save() )
838 + $payment_params['service_id'] = ! empty( $create_params['appointment_service']['service_id'] ) ? absint( $create_params['appointment_service']['service_id'] ) : 0;
548 839 $payment_params['initial_resource_id'] = $local_params['initial_resource_id']; // REQUIRED -- '2' initial calendar - parent resource
549 840 $payment_params['form_data'] = $booking_new_arr['form_data']; // we re-save it, because here can be sync_guid and custom form new data from wpbc_db__booking_save(..) // REQUIRED -- 'text^selected_short_timedates_hint4^06/11/2018 14:00...'
550 841 $payment_params['times_array'] = array(
551 842 explode( ':', $where_to_save_booking['time_to_book'][0] ), // ["10","00","00"]
@@ -555,9 +846,10 @@
555 846 $payment_params['is_edit_booking'] = $create_params['is_edit_booking']; // => 0 0 | int - ID of the booking
556 847 $payment_params['custom_form'] = $create_params['custom_form']; // => '' '' | 'some_name'
557 848 $payment_params['is_duplicate_booking'] = $create_params['is_duplicate_booking']; // => 0 0 | 1
558 849 $payment_params['is_from_admin_panel'] = $create_params['is_from_admin_panel']; // => false true | false
559 - $payment_params['is_show_payment_form'] = $create_params['is_show_payment_form']; // => 1 0 | 1
850 + $payment_params['is_show_payment_form'] = $create_params['is_show_payment_form']; // => 1 0 | 1
851 + $payment_params['wpbc_admin_cost_correction'] = $re_cleaned_params['wpbc_admin_cost_correction'];
560 852 if ( $payment_params['is_from_admin_panel'] ) {
561 853 // $payment_params['is_show_payment_form'] = 0; // FixIn: 9.9.0.21.
562 854 }
563 855 // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " >
@@ -931,9 +1223,13 @@
931 1223 return array( 'status' => 'error', 'message' => 'Sent request with no dates.' );
932 1224 }
933 1225
934 1226 // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- CHECK_IN_DATE_OLDER_THAN_CHECK_OUT " >
935 - if ( count( $create_params['dates_only_sql_arr'] ) == 1 ) { // Is it single selected date ?
1227 + $is_no_dates_booking = (
1228 + function_exists( 'wpbc_is_these_dates__for__no_dates' )
1229 + && wpbc_is_these_dates__for__no_dates( $create_params['dates_only_sql_arr'] )
1230 + );
1231 + if ( ( count( $create_params['dates_only_sql_arr'] ) == 1 ) && ( ! $is_no_dates_booking ) ) { // Is it single selected date ?
936 1232
937 1233 // Is 'check in' date/time older than 'check out' date/time when SINGLE day for booking? Then show error.
938 1234
939 1235 /**
@@ -1019,9 +1315,9 @@
1019 1315 $sql_field_arr[] = array( 'name' => 'form', 'type' => '%s', 'value' => $form_data );
1020 1316 $sql_field_arr[] = array( 'name' => 'booking_type', 'type' => '%d', 'value' => $create_params['resource_id'] );
1021 1317 $sql_field_arr[] = array( 'name' => 'modification_date', 'type' => '%s', 'value' => gmdate( 'Y-m-d H:i:s' ) );
1022 1318 $sql_field_arr[] = array( 'name' => 'sort_date', 'type' => '%s', 'value' => $create_params['dates_only_sql_arr'][0] . ' ' . $create_params['time_as_his_arr'][0] );
1023 - $sql_field_arr[] = array( 'name' => 'hash', 'type' => 'MD5(%s)', 'value' => time() . '_' . wp_rand( 1000, 1000000 ) );
1319 + $sql_field_arr[] = array( 'name' => 'hash', 'type' => '%s', 'value' => wpbc_hash__generate_booking_hash() );
1024 1320
1025 1321
1026 1322 if (
1027 1323 ( 0 == $create_params['is_edit_booking'] ) || // If not edit, then INSERT.
@@ -1042,12 +1338,15 @@
1042 1338 $sql_prepare_arr['name'] = implode( ', ', $sql_prepare_arr['name'] );
1043 1339 $sql_prepare_arr['type'] = implode( ', ', $sql_prepare_arr['type'] );
1044 1340 /* phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare */
1045 1341 $sql = $wpdb->prepare( "INSERT INTO {$wpdb->prefix}booking " . " ( {$sql_prepare_arr['name']} )" . " VALUES ( {$sql_prepare_arr['type']} )", $sql_prepare_arr['value'] );
1046 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
1047 - if ( false === $wpdb->query( $sql ) ) {
1048 - return array( 'status' => 'error', 'message' => 'Error. INSERT New Data in DB.' . ' FILE:' . __FILE__ . ' LINE:' . __LINE__ . ' SQL:' . $sql );
1049 - }
1342 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
1343 + if ( false === $wpdb->query( $sql ) ) {
1344 + return array(
1345 + 'status' => 'error',
1346 + 'message' => __( 'The booking could not be saved because of a database error. Please try again or contact the website administrator.', 'booking' ),
1347 + );
1348 + }
1050 1349 // Get ID of booking
1051 1350 $booking_id = (int) $wpdb->insert_id;
1052 1351
1053 1352 } else { // Edit - UPDATE
@@ -1061,14 +1360,15 @@
1061 1360 $sql_prepare_arr['set'] = implode( ', ', $sql_prepare_arr['set'] );
1062 1361
1063 1362 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
1064 1363 $sql = $wpdb->prepare( "UPDATE {$wpdb->prefix}booking SET {$sql_prepare_arr['set']} WHERE booking_id={$booking_id};", $sql_prepare_arr['value'] );
1065 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
1066 - if ( false === $wpdb->query( $sql ) ) {
1067 - return array( 'status' => 'error',
1068 - 'message' => 'Error. UPDATE Exist Data in DB.' . ' FILE:' . __FILE__ . ' LINE:' . __LINE__ . ' SQL:' . $sql,
1069 - );
1070 - }
1364 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
1365 + if ( false === $wpdb->query( $sql ) ) {
1366 + return array(
1367 + 'status' => 'error',
1368 + 'message' => __( 'The booking could not be updated because of a database error. Please try again or contact the website administrator.', 'booking' ),
1369 + );
1370 + }
1071 1371
1072 1372 // Check if dates previously was approved.
1073 1373 $slct_sql = "SELECT approved FROM {$wpdb->prefix}bookingdates WHERE booking_id IN ({$booking_id}) LIMIT 0,1";
1074 1374 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
@@ -1359,9 +1659,9 @@
1359 1659 *
1360 1660 * // Now get start/end times as seconds: [ 64800, 72000 ]
1361 1661 * $time_as_seconds_arr = wpbc_get_in_booking_form__time_to_book_as_seconds_arr( $structured_booking_data_arr );
1362 1662 */
1363 - function wpbc_get_in_booking_form__time_to_book_as_seconds_arr( $booking_form_data__arr ){
1663 + function wpbc_get_in_booking_form__time_to_book_as_seconds_arr( $booking_form_data__arr ){
1364 1664
1365 1665 $selected_time_fields = wpbc_get__selected_time_fields__in_booking_form__as_arr( $booking_form_data__arr );
1366 1666
1367 1667 // 2.2 Get selected SECONDS to book ---------------------------------------------------------------------------
@@ -1400,14 +1700,159 @@
1400 1700 }
1401 1701 }
1402 1702 }
1403 1703
1404 - return $time_as_seconds_arr;
1405 - }
1406 -
1407 -
1408 - /**
1409 - * Get explicit admin-selected time override from Add Booking modal request.
1704 + return $time_as_seconds_arr;
1705 + }
1706 +
1707 +
1708 + /**
1709 + * Determine whether a booking-create request is an authorized administration workflow.
1710 + *
1711 + * The public booking action is intentionally available to signed-out visitors. A
1712 + * Referer, request path, or caller-supplied Boolean therefore cannot establish an
1713 + * administrator security context. The Add Booking UI supplies this user-bound nonce,
1714 + * and the server independently rechecks login, capability, and MultiUser access.
1715 + *
1716 + * @param mixed $admin_booking_nonce Candidate Add Booking administration nonce.
1717 + *
1718 + * @return bool True only for an authorized Add Booking administration request.
1719 + */
1720 + function wpbc_is_authorized_admin_booking_request( $admin_booking_nonce ) {
1721 +
1722 + if (
1723 + ! is_scalar( $admin_booking_nonce )
1724 + || '' === trim( (string) $admin_booking_nonce )
1725 + || ! is_user_logged_in()
1726 + || ! wp_verify_nonce( sanitize_text_field( (string) $admin_booking_nonce ), 'wpbc_admin_booking_create' )
1727 + || ! class_exists( 'WPBC_Add_Booking_Component' )
1728 + || ! WPBC_Add_Booking_Component::current_user_can_add_booking()
1729 + || ! wpbc_is_mu_user_can_be_here( 'activated_user' )
1730 + ) {
1731 + return false;
1732 + }
1733 +
1734 + return true;
1735 + }
1736 +
1737 +
1738 + /**
1739 + * Require the signed workflow proof declared by a verified Booking Form context.
1740 + *
1741 + * Appointment and Resource Selector JavaScript flags are presentation hints only.
1742 + * The signed Booking Form context identifies the server-rendered workflow, so removing
1743 + * a flag or domain token cannot downgrade that form to a different workflow.
1744 + *
1745 + * @param array $classic_context Verified Booking Form context.
1746 + * @param bool $has_verified_appointment_context Whether Service and Provider proof passed validation.
1747 + * @param bool $has_verified_resource_selector_context Whether Resource Selector proof passed validation.
1748 + *
1749 + * @return true|WP_Error True when the required proof is present, otherwise a safe validation error.
1750 + */
1751 + function wpbc_booking_create_validate_required_workflow( $classic_context, $has_verified_appointment_context, $has_verified_resource_selector_context ) {
1752 +
1753 + $booking_workflow = isset( $classic_context['booking_workflow'] ) ? sanitize_key( $classic_context['booking_workflow'] ) : '';
1754 + if ( 'appointment' === $booking_workflow && ! $has_verified_appointment_context ) {
1755 + return new WP_Error( 'appointment_context_required', __( 'The Appointment selection has expired. Please start over and try again.', 'booking' ) );
1756 + }
1757 + if ( 'resource_selector' === $booking_workflow && ! $has_verified_resource_selector_context ) {
1758 + return new WP_Error( 'resource_selector_context_required', __( 'The Booking Resource selection has expired. Please start over and try again.', 'booking' ) );
1759 + }
1760 +
1761 + return true;
1762 + }
1763 +
1764 +
1765 + /**
1766 + * Remove administrator time-override values from an unauthorized booking request.
1767 + *
1768 + * The public booking endpoint intentionally accepts unauthenticated requests, so
1769 + * sanitizing these values is not sufficient authorization. Clearing every related
1770 + * value here prevents a public client from replacing the Booking Form's configured
1771 + * time while preserving the capability-protected Add Booking workflow.
1772 + *
1773 + * @param array $request_params Sanitized booking request parameters.
1774 + * @param bool $is_authorized_admin_booking_request Whether the current request is an authorized Add Booking administration request.
1775 + *
1776 + * @return array Booking request parameters with unauthorized override values removed.
1777 + */
1778 + function wpbc_restrict_booking_time_override_to_authorized_admin( $request_params, $is_authorized_admin_booking_request ) {
1779 +
1780 + $request_params = is_array( $request_params ) ? $request_params : array();
1781 + if ( $is_authorized_admin_booking_request ) {
1782 + return $request_params;
1783 + }
1784 +
1785 + $request_params['wpbc_time_override_enabled'] = 0;
1786 + $request_params['wpbc_time_override_source'] = '';
1787 + $request_params['wpbc_time_override_start'] = '';
1788 + $request_params['wpbc_time_override_end'] = '';
1789 +
1790 + return $request_params;
1791 + }
1792 +
1793 +
1794 + /**
1795 + * Authorize and normalize an administrator cost-correction request value.
1796 + *
1797 + * Booking creation is intentionally public, so a sanitized numeric value is
1798 + * not sufficient authorization. Only capability-protected Add Booking and
1799 + * Add Appointment workflows in Business Small or higher may retain this value.
1800 + * Missing, malformed, out-of-range, public, and unsupported-edition values
1801 + * are reduced to an empty sentinel, which preserves automatic calculation.
1802 + *
1803 + * @param array $request_params Sanitized booking request parameters.
1804 + * @param bool $is_authorized_admin_booking_request Whether this is an authorized administrator booking request.
1805 + *
1806 + * @return array Booking request parameters with a normalized or empty cost correction.
1807 + */
1808 + function wpbc_restrict_booking_cost_correction_to_authorized_admin( $request_params, $is_authorized_admin_booking_request ) {
1809 +
1810 + $request_params = is_array( $request_params ) ? $request_params : array();
1811 + $raw_cost = isset( $request_params['wpbc_admin_cost_correction'] ) ? $request_params['wpbc_admin_cost_correction'] : '';
1812 +
1813 + $request_params['wpbc_admin_cost_correction'] = '';
1814 + if ( ! $is_authorized_admin_booking_request || ! class_exists( 'wpdev_bk_biz_s' ) ) {
1815 + return $request_params;
1816 + }
1817 +
1818 + $request_params['wpbc_admin_cost_correction'] = wpbc_sanitize_booking_cost_correction( $raw_cost );
1819 +
1820 + return $request_params;
1821 + }
1822 +
1823 +
1824 + /**
1825 + * Sanitize one exact administrator-entered Booking total.
1826 + *
1827 + * @param mixed $raw_cost Raw request value.
1828 + *
1829 + * @return string Normalized decimal without trailing zeroes, or an empty string when invalid.
1830 + */
1831 + function wpbc_sanitize_booking_cost_correction( $raw_cost ) {
1832 +
1833 + if ( ! is_scalar( $raw_cost ) ) {
1834 + return '';
1835 + }
1836 +
1837 + $raw_cost = trim( sanitize_text_field( (string) $raw_cost ) );
1838 + if ( '' === $raw_cost || ! preg_match( '/^[0-9]{1,10}(?:\.[0-9]{1,8})?$/', $raw_cost ) ) {
1839 + return '';
1840 + }
1841 +
1842 + $normalized_cost = (float) $raw_cost;
1843 + if ( ! is_finite( $normalized_cost ) || $normalized_cost < 0 || $normalized_cost > 1000000000 ) {
1844 + return '';
1845 + }
1846 +
1847 + $normalized_cost = rtrim( rtrim( number_format( $normalized_cost, 8, '.', '' ), '0' ), '.' );
1848 +
1849 + return '' === $normalized_cost ? '0' : $normalized_cost;
1850 + }
1851 +
1852 +
1853 + /**
1854 + * Get explicit admin-selected time override from Add Booking modal request.
1410 1855 *
1411 1856 * @param array $request_params Sanitized booking request params.
1412 1857 *
1413 1858 * @return array Empty array or array with start/end HH:MM values.