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 +759 -205 10.15.7 → 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,14 +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' ),
61 + 'allow_past' => array( 'validate' => 'd', 'default' => 0 ),
62 + 'classic_booking_context_token' => array( 'validate' => 'strong', 'default' => '' ),
55 63 'wpbc_bfb_preview' => array( 'validate' => 'd', 'default' => 0 ),
56 64 'wpbc_bfb_preview_token' => array( 'validate' => 'strong', 'default' => '' ),
57 65 'wpbc_bfb_preview_form_id' => array( 'validate' => 'd', 'default' => 0 ),
58 66 'wpbc_bfb_preview_nonce' => array( 'validate' => 'strong', 'default' => '' ),
59 - )
60 - ));
67 + 'wpbc_time_override_enabled' => array( 'validate' => 'd', 'default' => 0 ),
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 + ));
61 74
62 75 // Escape of request params in Ajax Post. We use prefix 'calendar_request_params', if Ajax sent - $_REQUEST['calendar_request_params']['resource_id'], ...
63 76 $request_prefix = 'calendar_request_params';
64 77
@@ -63,15 +76,16 @@
63 76 $request_prefix = 'calendar_request_params';
64 77
65 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";
66 79
67 - $request_params = $user_request->get_sanitized__in_request__value_or_default( $request_prefix ); // NOT Direct: $_REQUEST['calendar_request_params']['resource_id']
68 - $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 */
69 - $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'] );
70 84
71 85 // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- CAPTCHA " >
72 86 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
73 - 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 ] );
74 88 // </editor-fold>
75 89
76 90 // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- BOOKING_RESOURCE ID " >
77 91 if ( $request_params['resource_id'] <= 0 ) {
@@ -78,24 +92,21 @@
78 92 $ajx_data_arr['status'] = 'error';
79 93 $ajx_data_arr['status_error'] = 'resource_id_incorrect';
80 94 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
81 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'] . ' ]';
82 - $ajx_data_arr['ajx_after_action_message_status'] = 'error';
83 - wp_send_json( array(
84 - 'ajx_data' => $ajx_data_arr,
85 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
86 - 'ajx_search_params' => $_REQUEST[ $request_prefix ],
87 - 'ajx_cleaned_params' => $request_params,
88 - 'resource_id' => $request_params['resource_id'],
89 - ) );
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 + ) );
90 101 }
91 102 // </editor-fold>
92 103
93 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 */
94 105
95 - $request_save_params = array(
96 - 'resource_id' => $request_params['resource_id'],
97 - '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'],
98 109 'form_data' => $request_params['formdata'],
99 110 'aggregate_resource_id_arr' => $request_params['aggregate_resource_id_arr'], // Optional can be ''.
100 111 'booking_hash' => $request_params['booking_hash'],
101 112 'custom_form' => $request_params['custom_form'],
@@ -103,24 +114,37 @@
103 114 'is_show_payment_form' => 1,
104 115 'user_id' => $local_params['user_id'],
105 116 'request_uri' => $server_http_referer_uri,
106 117 'form_status' => $request_params['form_status'],
118 + 'allow_past' => $request_params['allow_past'],
119 + 'classic_booking_context_token' => $request_params['classic_booking_context_token'],
107 120 'wpbc_bfb_preview' => $request_params['wpbc_bfb_preview'],
108 121 'wpbc_bfb_preview_token' => $request_params['wpbc_bfb_preview_token'],
109 122 'wpbc_bfb_preview_form_id' => $request_params['wpbc_bfb_preview_form_id'],
110 123 'wpbc_bfb_preview_nonce' => $request_params['wpbc_bfb_preview_nonce'],
111 - );
124 + 'wpbc_time_override_enabled' => $request_params['wpbc_time_override_enabled'],
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'];
112 136 $booking_save_arr = wpbc_booking_save( $request_save_params );
113 137
114 138 // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- BOOKING " >
115 139 if ( 'ok' !== $booking_save_arr['ajx_data']['status'] ) {
116 140
117 - wp_send_json( array( 'ajx_data' => $booking_save_arr['ajx_data'],
118 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
119 - 'ajx_search_params' => $_REQUEST[ $request_prefix ],
120 - 'ajx_cleaned_params' => $request_params,
121 - 'resource_id' => $request_params['resource_id']
122 - ));
141 + wp_send_json(
142 + array(
143 + 'ajx_data' => $booking_save_arr['ajx_data'],
144 + 'resource_id' => $request_params['resource_id'],
145 + )
146 + );
123 147 }
124 148 // </editor-fold>
125 149
126 150 $ajx_data_arr = $booking_save_arr['ajx_data'];
@@ -189,10 +213,94 @@
189 213 // ---------------------------------------------------------------------------------------------------------------------
190 214 // == Save Booking
191 215 // ---------------------------------------------------------------------------------------------------------------------
192 216
193 -/**
194 - * 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
195 303 *
196 304 * @param $request_params = [
197 305 * resource_id = 2 REQUIRED Default: 1
198 306 * dates_ddmmyy_csv = '27.10.2023, 28.10.2023, 29.10.2023' REQUIRED
@@ -242,11 +350,11 @@
242 350 // 1. Direct Clean Params
243 351 // -----------------------------------------------------------------------------------------------------------------
244 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 */
245 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 */
246 - $validate_arr_rules = array(
247 - 'resource_id' => array( 'validate' => 'd', 'default' => 1 ), // INT
248 - '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.
249 357 'form_data' => array( 'validate' => 'strong', 'default' => '' ),
250 358 'booking_hash' => array( 'validate' => 'strong', 'default' => '' ),
251 359 'custom_form' => array( 'validate' => 'strong', 'default' => '' ),
252 360 'is_emails_send' => array( 'validate' => 'd', 'default' => 1 ), // 0 | 1
@@ -251,8 +359,10 @@
251 359 'custom_form' => array( 'validate' => 'strong', 'default' => '' ),
252 360 'is_emails_send' => array( 'validate' => 'd', 'default' => 1 ), // 0 | 1
253 361 'is_show_payment_form' => array( 'validate' => 'd', 'default' => 1 ), // 0 | 1
254 362 'user_id' => array( 'validate' => 'd', 'default' => wpbc_get_current_user_id() ), // INT
363 + 'allow_past' => array( 'validate' => 'd', 'default' => 0 ),
364 + 'classic_booking_context_token' => array( 'validate' => 'strong', 'default' => '' ),
255 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
256 366 // Really Optional:
257 367 'aggregate_resource_id_arr' => array( 'validate' => 'digit_or_csd', 'default' => '' ),
258 368 //TODO: this parameter does not transfer during saving, so here will be always default value 'bookings_only' // FixIn: 10.0.0.7.
@@ -266,14 +376,75 @@
266 376 'wpbc_bfb_preview' => array( 'validate' => 'd', 'default' => 0 ),
267 377 'wpbc_bfb_preview_token' => array( 'validate' => 'strong', 'default' => '' ),
268 378 'wpbc_bfb_preview_form_id' => array( 'validate' => 'd', 'default' => 0 ),
269 379 'wpbc_bfb_preview_nonce' => array( 'validate' => 'strong', 'default' => '' ),
270 - );
271 - $re_cleaned_params = wpbc_sanitize_params_in_arr( $request_params, $validate_arr_rules );
272 -
273 - $admin_uri = ltrim( str_replace( get_site_url( null, '', 'admin' ), '', admin_url( 'admin.php?' ) ), '/' ); // wp-admin/admin.php?
274 -
275 - $re_cleaned_params['form_status'] = sanitize_key( $re_cleaned_params['form_status'] );
380 + 'wpbc_time_override_enabled' => array( 'validate' => 'd', 'default' => 0 ),
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'] );
276 447 if ( 'preview' !== $re_cleaned_params['form_status'] ) {
277 448 $re_cleaned_params['form_status'] = 'published';
278 449 }
279 450 // FixIn: 2026-02-05 - make preview/published available to form parsing/templates during this request.
@@ -290,14 +461,26 @@
290 461
291 462 // -----------------------------------------------------------------------------------------------------------------
292 463 // Local parameters
293 464 // -----------------------------------------------------------------------------------------------------------------
294 - $local_params = array();
295 - $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;
296 468 $local_params['user_id'] = $re_cleaned_params['user_id']; // 1
297 - $local_params['sync_gid'] = $re_cleaned_params['sync_gid']; // ''
298 - $local_params['is_approve_booking'] = $re_cleaned_params['is_approve_booking']; // 0 | 1
299 - $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 );
300 483
301 484 // -----------------------------------------------------------------------------------------------------------------
302 485 // Parse Local parameters for later use
303 486 // -----------------------------------------------------------------------------------------------------------------
@@ -305,10 +488,91 @@
305 488 * Get parsed booking form: = [ name = "John", secondname = "Smith", email = "[email protected]", visitors = "2",... ]
306 489 */
307 490 $local_params['structured_booking_data_arr'] = wpbc_get_parsed_booking_data_arr( $re_cleaned_params["form_data"], $re_cleaned_params["resource_id"], array( 'get' => 'value' ) );
308 491 $local_params['all_booking_data_arr'] = wpbc_get_parsed_booking_data_arr( $re_cleaned_params["form_data"], $re_cleaned_params["resource_id"] );
309 - // Important! : [ 64800, 72000 ]
310 - $local_params['time_as_seconds_arr'] = wpbc_get_in_booking_form__time_to_book_as_seconds_arr( $local_params['structured_booking_data_arr'] );
492 + $local_params['time_override_arr'] = wpbc_get_booking_time_override__as_arr( $re_cleaned_params );
493 + if ( ! empty( $local_params['time_override_arr'] ) ) {
494 + unset( $local_params['structured_booking_data_arr']['rangetime'], $local_params['structured_booking_data_arr']['durationtime'] );
495 + $local_params['structured_booking_data_arr']['starttime'] = $local_params['time_override_arr']['start'];
496 + $local_params['structured_booking_data_arr']['endtime'] = $local_params['time_override_arr']['end'];
497 +
498 + unset( $local_params['all_booking_data_arr']['rangetime'], $local_params['all_booking_data_arr']['durationtime'] );
499 + $local_params['all_booking_data_arr']['starttime'] = array(
500 + 'type' => 'text',
501 + 'original_name' => 'starttime' . $re_cleaned_params['resource_id'],
502 + 'name' => 'starttime',
503 + 'value' => $local_params['time_override_arr']['start'],
504 + );
505 + $local_params['all_booking_data_arr']['endtime'] = array(
506 + 'type' => 'text',
507 + 'original_name' => 'endtime' . $re_cleaned_params['resource_id'],
508 + 'name' => 'endtime',
509 + 'value' => $local_params['time_override_arr']['end'],
510 + );
511 + }
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 + }
311 575 // [ "18:00:00", "20:00:00" ]
312 576 $time_as_seconds_arr = $local_params['time_as_seconds_arr'];
313 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
314 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
@@ -320,19 +584,77 @@
320 584 wpbc_transform__seconds__in__24_hours_his( $time_as_seconds_arr[0] ),
321 585 wpbc_transform__seconds__in__24_hours_his( $time_as_seconds_arr[1] )
322 586 );
323 587 // [ '2023-09-10', '2023-09-11' ]
324 - $local_params['dates_only_sql_arr'] = wpbc_convert_dates_str__dd_mm_yyyy__to__yyyy_mm_dd( $re_cleaned_params["dates_ddmmyy_csv"] );
325 - $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 + }
326 645
327 646 $local_params['is_show_payment_form'] = $re_cleaned_params["is_show_payment_form"];
328 647
329 648 // FixIn: 9.9.0.35.
330 - if ( $local_params['is_show_payment_form'] ) {
331 - $local_params['is_show_payment_form'] = ( false !== strpos( $re_cleaned_params['request_uri'], 'is_show_payment_form=Off' ) )
332 - ? 0
333 - : $local_params['is_show_payment_form']; // 1|0
334 - }
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 + }
335 657
336 658 // Get EDIT booking data
337 659 $local_params['edit_resource_id'] = '';
338 660 $local_params['skip_booking_id'] = '';
@@ -338,27 +660,44 @@
338 660 $local_params['skip_booking_id'] = '';
339 661 $local_params['is_edit_booking'] = 0;
340 662 $local_params['is_duplicate_booking'] = 0;
341 663 $is_edit_booking = wpbc_get_data__if_edit_booking( $re_cleaned_params['booking_hash'], $re_cleaned_params['request_uri'] );
342 - if ( false !== $is_edit_booking ) {
343 - $local_params['edit_resource_id'] = $is_edit_booking['resource_id']; // can be parent booking resource, where we edit the booking
344 - $local_params['skip_booking_id'] = $is_edit_booking['booking_id']; // booking ID
345 - $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
346 668 if (
347 669 ( ! empty( $local_params['structured_booking_data_arr']['wpbc_other_action'] ) )
348 670 && ( 'duplicate_booking' === $local_params['structured_booking_data_arr']['wpbc_other_action'] )
349 671 ){
350 - $local_params['is_duplicate_booking'] = 1;
351 - }
352 - }
353 - // 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)
354 693 $local_params['initial_resource_id'] = ( ! empty( $local_params['edit_resource_id'] ) ) ? $local_params['edit_resource_id'] : $re_cleaned_params['resource_id'];
355 694
356 - // 2
357 - $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'] );
358 -
359 -
360 - $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'] );
361 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.
362 701 $local_params['aggregate_resource_id_arr'] = array_unique( $local_params['aggregate_resource_id_arr'] ); // Erase duplicates
363 702
364 703 // -----------------------------------------------------------------------------------------------------------------
@@ -364,123 +703,110 @@
364 703 // -----------------------------------------------------------------------------------------------------------------
365 704 // Here GO
366 705 // -----------------------------------------------------------------------------------------------------------------
367 706
368 - // Force - resource saving parameters, instead of wpbc__where_to_save_booking()
369 - 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 );
370 808
371 - $local_params['how_many_items_to_book'] = 1;
372 -
373 - $dates_keys_arr = array_values( $local_params['dates_only_sql_arr'] ); // [ '2023-09-23', '2023-09-24' ]
374 -
375 - $resources_in_dates = array_fill_keys( $dates_keys_arr , array( $local_params['initial_resource_id'] ) ); // [ 2023-09-23 = [ 2 ], 2023-09-24 = [ 2 ] ]
376 -
377 - $where_to_save_booking = array();
378 - $where_to_save_booking['result'] = 'ok';
379 - $where_to_save_booking['resources_in_dates'] = $resources_in_dates; // [ 2023-09-23 = [ 2, 10, 11 ], 2023-09-24 = [ 2, 10, 11 ]
380 - $where_to_save_booking['time_to_book'] = $local_params['time_as_his_arr']; // [ "00:00:00", "24:00:00" ]
381 - $where_to_save_booking['main__resource_id'] = $local_params['initial_resource_id']; // here edit or request (parent/single) resource
382 -
383 - } else {
384 - // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " >
385 - $php_performance = wpbc_php_performance_START( 'wpbc__where_to_save_booking' , $php_performance );
386 - // </editor-fold>
387 -
388 - /**
389 - * Get slots [] where we can save booking = [ 'resources_in_dates' => [ 2023-10-18 = [ 2, 12, 10, 11 ]
390 - * 2023-10-19 = [ 2, 12, 10, 11 ]
391 - * 2023-10-20 = [ 2, 12, 10, 11 ]
392 - * ],
393 - * 'time_to_book' => [ "14:00:01" , "12:00:01" ],
394 - * 'result' => 'ok'
395 - * 'main__resource_id' => 2
396 - * ]
397 - * OR
398 - * [ 'result' => 'error', 'message' => 'Booking can not be saved ...' ]
399 - */
400 - $where_to_save_booking = wpbc__where_to_save_booking( array(
401 - 'resource_id' => $local_params['initial_resource_id'], // 2 //TODO: If edit booking. What to pass 'edit' or 'parent' resource ID?
402 - 'skip_booking_id' => $local_params['skip_booking_id'], // '', | 125 if edit booking
403 - 'dates_only_sql_arr' => $local_params['dates_only_sql_arr'], // [ "2023-10-18", "2023-10-25", "2023-11-25" ]
404 - 'time_as_seconds_arr' => $local_params['time_as_seconds_arr'], // [ 36000, 39600 ]
405 - 'how_many_items_to_book' => $local_params['how_many_items_to_book'], // 1
406 - 'request_uri' => $re_cleaned_params['request_uri'], // 'http://beta/resource-id2/'
407 - 'is_use_booking_recurrent_time' => $local_params['is_use_booking_recurrent_time'], // true | false
408 - 'as_single_resource' => false, // false
409 - 'aggregate_resource_id_arr' => $local_params['aggregate_resource_id_arr'], // Optional can be ''
410 - '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.
411 - 'custom_form' => $re_cleaned_params['custom_form'] // FixIn: 10.0.0.10.
412 - ));
413 - // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- NO SLOTS TO SAVE " >
414 - if ( 'error' == $where_to_save_booking['result'] ) {
415 - $ajx_data_arr['status'] = 'error';
416 - $ajx_data_arr['status_error'] = 'booking_can_not_save';
417 - $ajx_data_arr['ajx_after_action_message'] = $where_to_save_booking['message'];
418 - $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
419 - return array( 'ajx_data' => $ajx_data_arr );
420 - }
421 - // </editor-fold>
422 -
423 - // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " >
424 - $php_performance = wpbc_php_performance_END( 'wpbc__where_to_save_booking' , $php_performance );
425 - // </editor-fold>
426 - }
427 -
428 -
429 - // Get parameters, from REQUEST
430 - $create_params = $local_params;
431 - $create_params['resource_id'] = ( ! empty( $local_params['edit_resource_id'] ) )
432 - ? $local_params['edit_resource_id'] // If we edit, then use original resource ???
433 - : $where_to_save_booking['main__resource_id']; // Here is important TIP, resource can be where is free, and not where we submit
434 - /**
435 - * 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']
436 - * but what, if $where_to_save_booking do not contain this $local_params['edit_resource_id'] as available resource.
437 - * or even we have $local_params['edit_resource_id'] = 2 and $where_to_save_booking contain resources like [ 1, 2, 3, 4 ]
438 - * we make booking for 3 slots
439 - * in this case, main resource will be 2
440 - * 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 )
441 - * "(205, '2023-10-04 00:00:00', 0, NULL)" <- main resource '2' e.g. $local_params['edit_resource_id'] = 2
442 - * "(205, '2023-10-04 00:00:00', 0, 2)" ? <- child resource '2' e.g. [ .., 2, .. ] in $where_to_save_booking WHICH IS WRONG
443 - */
444 - $create_params['is_emails_send'] = $re_cleaned_params['is_emails_send'];
445 - $create_params['custom_form'] = $re_cleaned_params['custom_form'];
446 -
447 - make_bk_action( 'check_multiuser_params_for_client_side', $create_params['resource_id'] ); // Activate working with specific user in WP MU
448 -
449 - // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " >
450 - $php_performance = wpbc_php_performance_START( 'wpbc_db__booking_save' , $php_performance );
451 - // </editor-fold>
452 -
453 - // -----------------------------------------------------------------------------------------------------------------
454 - // == CREATE_THE 'NEW_BOOKING' ==
455 - // -----------------------------------------------------------------------------------------------------------------
456 - $create_booking_params = array(
457 - 'resource_id' => $create_params['resource_id'],
458 - 'custom_form' => $create_params['custom_form'],
459 - 'all_booking_data_arr' => $create_params['all_booking_data_arr'],
460 - 'dates_only_sql_arr' => $create_params['dates_only_sql_arr'],
461 - 'time_as_his_arr' => $create_params['time_as_his_arr'],
462 - 'is_from_admin_panel' => $create_params['is_from_admin_panel'],
463 - 'is_edit_booking' => $create_params['is_edit_booking'],
464 - 'is_duplicate_booking' => $create_params['is_duplicate_booking'],
465 - 'is_approve_booking' => $create_params['is_approve_booking'],
466 - 'how_many_items_to_book' => $create_params['how_many_items_to_book'],
467 - 'is_use_booking_recurrent_time' => $create_params['is_use_booking_recurrent_time'] // true | false
468 - );
469 - if ( ! empty( $create_params['sync_gid'] ) ) { $create_booking_params['sync_gid'] = $create_params['sync_gid']; }
470 -
471 - $booking_new_arr = wpbc_db__booking_save( $create_booking_params, $where_to_save_booking );
472 -
473 - // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- BOOKING CREATION " >
474 - if ( 'ok' !== $booking_new_arr['status'] ) {
475 - $ajx_data_arr['status'] = $booking_new_arr['status'];
476 - $ajx_data_arr['status_error'] = 'booking_can_not_save';
477 - $ajx_data_arr['ajx_after_action_message'] = $booking_new_arr['message'];
478 - $ajx_data_arr['ajx_after_action_message_status'] = 'error';
479 - return array( 'ajx_data' => $ajx_data_arr );
480 - }
481 - // </editor-fold>
482 -
483 809 // FixIn: 9.9.0.36.
484 810 if (
485 811 ( 0 !== $create_params['is_edit_booking'] ) // If edit booking
486 812 && ( 1 != $create_params['is_duplicate_booking'] ) // If not duplicate
@@ -507,9 +833,10 @@
507 833 );
508 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']
509 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'
510 836 $payment_params['booking_id'] = $booking_new_arr['booking_id']; // REQUIRED -- '2'
511 - $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;
512 839 $payment_params['initial_resource_id'] = $local_params['initial_resource_id']; // REQUIRED -- '2' initial calendar - parent resource
513 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...'
514 841 $payment_params['times_array'] = array(
515 842 explode( ':', $where_to_save_booking['time_to_book'][0] ), // ["10","00","00"]
@@ -519,9 +846,10 @@
519 846 $payment_params['is_edit_booking'] = $create_params['is_edit_booking']; // => 0 0 | int - ID of the booking
520 847 $payment_params['custom_form'] = $create_params['custom_form']; // => '' '' | 'some_name'
521 848 $payment_params['is_duplicate_booking'] = $create_params['is_duplicate_booking']; // => 0 0 | 1
522 849 $payment_params['is_from_admin_panel'] = $create_params['is_from_admin_panel']; // => false true | false
523 - $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'];
524 852 if ( $payment_params['is_from_admin_panel'] ) {
525 853 // $payment_params['is_show_payment_form'] = 0; // FixIn: 9.9.0.21.
526 854 }
527 855 // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " >
@@ -895,9 +1223,13 @@
895 1223 return array( 'status' => 'error', 'message' => 'Sent request with no dates.' );
896 1224 }
897 1225
898 1226 // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- CHECK_IN_DATE_OLDER_THAN_CHECK_OUT " >
899 - 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 ?
900 1232
901 1233 // Is 'check in' date/time older than 'check out' date/time when SINGLE day for booking? Then show error.
902 1234
903 1235 /**
@@ -983,9 +1315,9 @@
983 1315 $sql_field_arr[] = array( 'name' => 'form', 'type' => '%s', 'value' => $form_data );
984 1316 $sql_field_arr[] = array( 'name' => 'booking_type', 'type' => '%d', 'value' => $create_params['resource_id'] );
985 1317 $sql_field_arr[] = array( 'name' => 'modification_date', 'type' => '%s', 'value' => gmdate( 'Y-m-d H:i:s' ) );
986 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] );
987 - $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() );
988 1320
989 1321
990 1322 if (
991 1323 ( 0 == $create_params['is_edit_booking'] ) || // If not edit, then INSERT.
@@ -1006,12 +1338,15 @@
1006 1338 $sql_prepare_arr['name'] = implode( ', ', $sql_prepare_arr['name'] );
1007 1339 $sql_prepare_arr['type'] = implode( ', ', $sql_prepare_arr['type'] );
1008 1340 /* phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare */
1009 1341 $sql = $wpdb->prepare( "INSERT INTO {$wpdb->prefix}booking " . " ( {$sql_prepare_arr['name']} )" . " VALUES ( {$sql_prepare_arr['type']} )", $sql_prepare_arr['value'] );
1010 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
1011 - if ( false === $wpdb->query( $sql ) ) {
1012 - return array( 'status' => 'error', 'message' => 'Error. INSERT New Data in DB.' . ' FILE:' . __FILE__ . ' LINE:' . __LINE__ . ' SQL:' . $sql );
1013 - }
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 + }
1014 1349 // Get ID of booking
1015 1350 $booking_id = (int) $wpdb->insert_id;
1016 1351
1017 1352 } else { // Edit - UPDATE
@@ -1025,14 +1360,15 @@
1025 1360 $sql_prepare_arr['set'] = implode( ', ', $sql_prepare_arr['set'] );
1026 1361
1027 1362 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
1028 1363 $sql = $wpdb->prepare( "UPDATE {$wpdb->prefix}booking SET {$sql_prepare_arr['set']} WHERE booking_id={$booking_id};", $sql_prepare_arr['value'] );
1029 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
1030 - if ( false === $wpdb->query( $sql ) ) {
1031 - return array( 'status' => 'error',
1032 - 'message' => 'Error. UPDATE Exist Data in DB.' . ' FILE:' . __FILE__ . ' LINE:' . __LINE__ . ' SQL:' . $sql,
1033 - );
1034 - }
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 + }
1035 1371
1036 1372 // Check if dates previously was approved.
1037 1373 $slct_sql = "SELECT approved FROM {$wpdb->prefix}bookingdates WHERE booking_id IN ({$booking_id}) LIMIT 0,1";
1038 1374 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
@@ -1323,9 +1659,9 @@
1323 1659 *
1324 1660 * // Now get start/end times as seconds: [ 64800, 72000 ]
1325 1661 * $time_as_seconds_arr = wpbc_get_in_booking_form__time_to_book_as_seconds_arr( $structured_booking_data_arr );
1326 1662 */
1327 - 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 ){
1328 1664
1329 1665 $selected_time_fields = wpbc_get__selected_time_fields__in_booking_form__as_arr( $booking_form_data__arr );
1330 1666
1331 1667 // 2.2 Get selected SECONDS to book ---------------------------------------------------------------------------
@@ -1364,9 +1700,227 @@
1364 1700 }
1365 1701 }
1366 1702 }
1367 1703
1368 - return $time_as_seconds_arr;
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.
1855 + *
1856 + * @param array $request_params Sanitized booking request params.
1857 + *
1858 + * @return array Empty array or array with start/end HH:MM values.
1859 + */
1860 + function wpbc_get_booking_time_override__as_arr( $request_params ) {
1861 +
1862 + if ( empty( $request_params['wpbc_time_override_enabled'] ) ) {
1863 + return array();
1864 + }
1865 +
1866 + $start_time = wpbc_sanitize_booking_time_override__hm( isset( $request_params['wpbc_time_override_start'] ) ? $request_params['wpbc_time_override_start'] : '' );
1867 + $end_time = wpbc_sanitize_booking_time_override__hm( isset( $request_params['wpbc_time_override_end'] ) ? $request_params['wpbc_time_override_end'] : '' );
1868 +
1869 + if (
1870 + ( '' === $start_time )
1871 + || ( '' === $end_time )
1872 + || ( wpbc_booking_time_override__hm_to_seconds( $start_time ) >= wpbc_booking_time_override__hm_to_seconds( $end_time ) )
1873 + ) {
1874 + return array();
1875 + }
1876 +
1877 + return array(
1878 + 'start' => $start_time,
1879 + 'end' => $end_time,
1880 + 'source' => isset( $request_params['wpbc_time_override_source'] ) ? sanitize_key( $request_params['wpbc_time_override_source'] ) : '',
1881 + );
1882 + }
1883 +
1884 +
1885 + /**
1886 + * Sanitize HH:MM value for admin booking time override.
1887 + *
1888 + * @param string $time_value Time value.
1889 + *
1890 + * @return string
1891 + */
1892 + function wpbc_sanitize_booking_time_override__hm( $time_value ) {
1893 +
1894 + $time_value = trim( sanitize_text_field( (string) $time_value ) );
1895 +
1896 + if ( ! preg_match( '/^([0-9]{1,2}):([0-9]{2})$/', $time_value, $matches ) ) {
1897 + return '';
1898 + }
1899 +
1900 + $hour = absint( $matches[1] );
1901 + $minute = absint( $matches[2] );
1902 +
1903 + if ( $hour > 24 || $minute > 59 || ( 24 === $hour && 0 !== $minute ) ) {
1904 + return '';
1905 + }
1906 +
1907 + return sprintf( '%02d:%02d', $hour, $minute );
1908 + }
1909 +
1910 +
1911 + /**
1912 + * Convert HH:MM to seconds.
1913 + *
1914 + * @param string $time_value Time value.
1915 + *
1916 + * @return int
1917 + */
1918 + function wpbc_booking_time_override__hm_to_seconds( $time_value ) {
1919 +
1920 + $time_arr = explode( ':', (string) $time_value );
1921 +
1922 + return ( absint( $time_arr[0] ) * 60 * 60 ) + ( absint( $time_arr[1] ) * 60 );
1369 1923 }
1370 1924
1371 1925
1372 1926 /**