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 +322 -196 11.7 → 11.9 View file →
@@ -28,14 +28,11 @@
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 36 // Request parameters for the released Appointment and Resource Selector workflows.
40 37 $workflow_request_rules = array(
41 38 'service_id' => array( 'validate' => 'd', 'default' => 0 ),
@@ -42,8 +39,9 @@
42 39 'appointment_service_required' => array( 'validate' => 'd', 'default' => 0 ),
43 40 'appointment_context_token' => array( 'validate' => 'strong', 'default' => '' ),
44 41 'resource_selector_required' => array( 'validate' => 'd', 'default' => 0 ),
45 42 'resource_selector_context_token' => array( 'validate' => 'strong', 'default' => '' ),
43 + 'wpbc_admin_booking_nonce' => array( 'validate' => 'strong', 'default' => '' ),
46 44 );
47 45
48 46 $user_request = new WPBC_AJX__REQUEST( array( // Using this class here only for escaping variables
49 47 'db_option_name' => 'booking__wpbc_booking_create__request_params', // Not necessary, because we not save request, only sanitize it
@@ -78,15 +76,16 @@
78 76 $request_prefix = 'calendar_request_params';
79 77
80 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";
81 79
82 - $request_params = $user_request->get_sanitized__in_request__value_or_default( $request_prefix ); // NOT Direct: $_REQUEST['calendar_request_params']['resource_id']
83 - $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 */
84 - $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'] );
85 84
86 85 // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- CAPTCHA " >
87 86 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
88 - 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 ] );
89 88 // </editor-fold>
90 89
91 90 // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- BOOKING_RESOURCE ID " >
92 91 if ( $request_params['resource_id'] <= 0 ) {
@@ -93,16 +92,13 @@
93 92 $ajx_data_arr['status'] = 'error';
94 93 $ajx_data_arr['status_error'] = 'resource_id_incorrect';
95 94 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
96 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'] . ' ]';
97 - $ajx_data_arr['ajx_after_action_message_status'] = 'error';
98 - wp_send_json( array(
99 - 'ajx_data' => $ajx_data_arr,
100 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
101 - 'ajx_search_params' => $_REQUEST[ $request_prefix ],
102 - 'ajx_cleaned_params' => $request_params,
103 - 'resource_id' => $request_params['resource_id'],
104 - ) );
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 + ) );
105 101 }
106 102 // </editor-fold>
107 103
108 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 */
@@ -135,19 +131,20 @@
135 131 $request_save_params['appointment_service_required'] = $request_params['appointment_service_required'];
136 132 $request_save_params['appointment_context_token'] = $request_params['appointment_context_token'];
137 133 $request_save_params['resource_selector_required'] = $request_params['resource_selector_required'];
138 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'];
139 136 $booking_save_arr = wpbc_booking_save( $request_save_params );
140 137
141 138 // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- BOOKING " >
142 139 if ( 'ok' !== $booking_save_arr['ajx_data']['status'] ) {
143 140
144 - wp_send_json( array( 'ajx_data' => $booking_save_arr['ajx_data'],
145 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
146 - 'ajx_search_params' => $_REQUEST[ $request_prefix ],
147 - 'ajx_cleaned_params' => $request_params,
148 - 'resource_id' => $request_params['resource_id']
149 - ));
141 + wp_send_json(
142 + array(
143 + 'ajx_data' => $booking_save_arr['ajx_data'],
144 + 'resource_id' => $request_params['resource_id'],
145 + )
146 + );
150 147 }
151 148 // </editor-fold>
152 149
153 150 $ajx_data_arr = $booking_save_arr['ajx_data'];
@@ -216,10 +213,94 @@
216 213 // ---------------------------------------------------------------------------------------------------------------------
217 214 // == Save Booking
218 215 // ---------------------------------------------------------------------------------------------------------------------
219 216
220 -/**
221 - * 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
222 303 *
223 304 * @param $request_params = [
224 305 * resource_id = 2 REQUIRED Default: 1
225 306 * dates_ddmmyy_csv = '27.10.2023, 28.10.2023, 29.10.2023' REQUIRED
@@ -306,9 +387,12 @@
306 387 $validate_arr_rules['appointment_service_required'] = array( 'validate' => 'd', 'default' => 0 );
307 388 $validate_arr_rules['appointment_context_token'] = array( 'validate' => 'strong', 'default' => '' );
308 389 $validate_arr_rules['resource_selector_required'] = array( 'validate' => 'd', 'default' => 0 );
309 390 $validate_arr_rules['resource_selector_context_token'] = array( 'validate' => 'strong', 'default' => '' );
391 + $validate_arr_rules['wpbc_admin_booking_nonce'] = array( 'validate' => 'strong', 'default' => '' );
310 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;
311 395 if ( ! empty( $re_cleaned_params['appointment_service_required'] ) && empty( $re_cleaned_params['service_id'] ) ) {
312 396 $ajx_data_arr['status'] = 'error';
313 397 $ajx_data_arr['status_error'] = 'appointment_service_required';
314 398 $ajx_data_arr['ajx_after_action_message'] = __( 'Please select a Service.', 'booking' );
@@ -331,13 +415,14 @@
331 415 $ajx_data_arr['ajx_after_action_message'] = $appointment_context_check->get_error_message();
332 416 $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
333 417 return array( 'ajx_data' => $ajx_data_arr );
334 418 }
419 + $has_verified_appointment_context = true;
335 420
336 421 // A client value cannot enable past Appointment creation; trust only the site-authored signed context.
337 422 $re_cleaned_params['allow_past'] = wpbc_booking_appointment_is_past_booking_enabled( $appointment_context_check ) ? 1 : 0;
338 423 }
339 - if ( ! empty( $re_cleaned_params['resource_selector_required'] ) ) {
424 + if ( ! empty( $re_cleaned_params['resource_selector_required'] ) || ! empty( $re_cleaned_params['resource_selector_context_token'] ) ) {
340 425 if ( ! function_exists( 'wpbc_booking_resource_selector_validate_submission_context' ) ) {
341 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' ) );
342 427 } else {
343 428 $resource_selector_context_check = wpbc_booking_resource_selector_validate_submission_context(
@@ -351,16 +436,15 @@
351 436 $ajx_data_arr['ajx_after_action_message'] = $resource_selector_context_check->get_error_message();
352 437 $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
353 438 return array( 'ajx_data' => $ajx_data_arr );
354 439 }
440 + $has_verified_resource_selector_context = true;
355 441
356 442 // Trust only the site-authored signed selector context for public past bookings.
357 443 $re_cleaned_params['allow_past'] = wpbc_booking_resource_selector_is_past_booking_enabled( $resource_selector_context_check ) ? 1 : 0;
358 444 }
359 445
360 - $admin_uri = ltrim( str_replace( get_site_url( null, '', 'admin' ), '', admin_url( 'admin.php?' ) ), '/' ); // wp-admin/admin.php?
361 -
362 - $re_cleaned_params['form_status'] = sanitize_key( $re_cleaned_params['form_status'] );
446 + $re_cleaned_params['form_status'] = sanitize_key( $re_cleaned_params['form_status'] );
363 447 if ( 'preview' !== $re_cleaned_params['form_status'] ) {
364 448 $re_cleaned_params['form_status'] = 'published';
365 449 }
366 450 // FixIn: 2026-02-05 - make preview/published available to form parsing/templates during this request.
@@ -377,10 +461,11 @@
377 461
378 462 // -----------------------------------------------------------------------------------------------------------------
379 463 // Local parameters
380 464 // -----------------------------------------------------------------------------------------------------------------
381 - $local_params = array();
382 - $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;
383 468 $local_params['user_id'] = $re_cleaned_params['user_id']; // 1
384 469 $local_params['sync_gid'] = $re_cleaned_params['sync_gid']; // ''
385 470 $local_params['is_approve_booking'] = $re_cleaned_params['is_approve_booking']; // 0 | 1
386 471 $local_params['is_use_booking_recurrent_time'] = ( 1 === $re_cleaned_params['is_use_booking_recurrent_time'] ); // false | true
@@ -386,13 +471,8 @@
386 471 $local_params['is_use_booking_recurrent_time'] = ( 1 === $re_cleaned_params['is_use_booking_recurrent_time'] ); // false | true
387 472 $request_action = isset( $_REQUEST['action'] ) && is_scalar( $_REQUEST['action'] )
388 473 ? sanitize_key( (string) wp_unslash( $_REQUEST['action'] ) )
389 474 : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
390 - $is_authorized_admin_booking_request = $local_params['is_from_admin_panel']
391 - && is_user_logged_in()
392 - && class_exists( 'WPBC_Add_Booking_Component' )
393 - && WPBC_Add_Booking_Component::current_user_can_add_booking()
394 - && wpbc_is_mu_user_can_be_here( 'activated_user' );
395 475 $is_public_booking_create_request = wp_doing_ajax()
396 476 && 'wpbc_ajx_booking__create' === strtolower( $request_action )
397 477 && ! $is_authorized_admin_booking_request;
398 478
@@ -507,8 +587,9 @@
507 587 // [ '2023-09-10', '2023-09-11' ]
508 588 $local_params['dates_only_sql_arr'] = wpbc_convert_dates_str__dd_mm_yyyy__to__yyyy_mm_dd( $re_cleaned_params["dates_ddmmyy_csv"] );
509 589 $local_params['dates_only_sql_arr'] = explode( ',', $local_params['dates_only_sql_arr'] );
510 590
591 + $classic_context = array();
511 592 $has_verified_classic_context = false;
512 593 if ( ! empty( $re_cleaned_params['classic_booking_context_token'] ) && function_exists( 'wpbc_classic_booking_context_validate_submission' ) ) {
513 594 $classic_context = wpbc_classic_booking_context_validate_submission(
514 595 $re_cleaned_params['classic_booking_context_token'],
@@ -524,22 +605,38 @@
524 605 $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
525 606 return array( 'ajx_data' => $ajx_data_arr );
526 607 }
527 608
528 - $has_verified_classic_context = true;
609 + $has_verified_classic_context = true;
529 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'] );
530 613 }
531 614
532 - $has_verified_workflow_context = (
533 - ( ! empty( $re_cleaned_params['service_id'] ) && ! empty( $re_cleaned_params['appointment_context_token'] ) )
534 - || ( ! empty( $re_cleaned_params['resource_selector_required'] ) && ! empty( $re_cleaned_params['resource_selector_context_token'] ) )
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
535 628 );
536 - if ( $is_public_booking_create_request && ! $has_verified_classic_context && ! $has_verified_workflow_context ) {
537 - $re_cleaned_params['allow_past'] = 0;
538 - $re_cleaned_params['request_uri'] = remove_query_arg( 'allow_past', $re_cleaned_params['request_uri'] );
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 + }
539 636 }
540 -
541 - if (
637 +
638 + if (
542 639 ( ! empty( $local_params['time_override_arr'] ) )
543 640 && ( 'times_availability' === $local_params['time_override_arr']['source'] )
544 641 && ( count( array_filter( $local_params['dates_only_sql_arr'] ) ) > 1 )
545 642 ) {
@@ -548,13 +645,16 @@
548 645
549 646 $local_params['is_show_payment_form'] = $re_cleaned_params["is_show_payment_form"];
550 647
551 648 // FixIn: 9.9.0.35.
552 - if ( $local_params['is_show_payment_form'] ) {
553 - $local_params['is_show_payment_form'] = ( false !== strpos( $re_cleaned_params['request_uri'], 'is_show_payment_form=Off' ) )
554 - ? 0
555 - : $local_params['is_show_payment_form']; // 1|0
556 - }
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 + }
557 657
558 658 // Get EDIT booking data
559 659 $local_params['edit_resource_id'] = '';
560 660 $local_params['skip_booking_id'] = '';
@@ -603,143 +703,108 @@
603 703 // -----------------------------------------------------------------------------------------------------------------
604 704 // Here GO
605 705 // -----------------------------------------------------------------------------------------------------------------
606 706
607 - // Force - resource saving parameters, instead of wpbc__where_to_save_booking()
608 - if ( ! empty( $re_cleaned_params["save_booking_even_if_unavailable"] ) ) {
609 -
610 - $local_params['how_many_items_to_book'] = 1;
611 -
612 - $dates_keys_arr = array_values( $local_params['dates_only_sql_arr'] ); // [ '2023-09-23', '2023-09-24' ]
613 -
614 - $resources_in_dates = array_fill_keys( $dates_keys_arr , array( $local_params['initial_resource_id'] ) ); // [ 2023-09-23 = [ 2 ], 2023-09-24 = [ 2 ] ]
615 -
616 - $where_to_save_booking = array();
617 - $where_to_save_booking['result'] = 'ok';
618 - $where_to_save_booking['resources_in_dates'] = $resources_in_dates; // [ 2023-09-23 = [ 2, 10, 11 ], 2023-09-24 = [ 2, 10, 11 ]
619 - $where_to_save_booking['time_to_book'] = $local_params['time_as_his_arr']; // [ "00:00:00", "24:00:00" ]
620 - $where_to_save_booking['main__resource_id'] = $local_params['initial_resource_id']; // here edit or request (parent/single) resource
621 -
622 - } else {
623 - // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " >
624 - $php_performance = wpbc_php_performance_START( 'wpbc__where_to_save_booking' , $php_performance );
625 - // </editor-fold>
626 -
627 - /**
628 - * Get slots [] where we can save booking = [ 'resources_in_dates' => [ 2023-10-18 = [ 2, 12, 10, 11 ]
629 - * 2023-10-19 = [ 2, 12, 10, 11 ]
630 - * 2023-10-20 = [ 2, 12, 10, 11 ]
631 - * ],
632 - * 'time_to_book' => [ "14:00:01" , "12:00:01" ],
633 - * 'result' => 'ok'
634 - * 'main__resource_id' => 2
635 - * ]
636 - * OR
637 - * [ 'result' => 'error', 'message' => 'Booking can not be saved ...' ]
638 - */
639 - $where_to_save_booking = wpbc__where_to_save_booking( array(
640 - 'resource_id' => $local_params['initial_resource_id'], // 2 //TODO: If edit booking. What to pass 'edit' or 'parent' resource ID?
641 - 'skip_booking_id' => $local_params['skip_booking_id'], // '', | 125 if edit booking
642 - 'dates_only_sql_arr' => $local_params['dates_only_sql_arr'], // [ "2023-10-18", "2023-10-25", "2023-11-25" ]
643 - 'time_as_seconds_arr' => $local_params['time_as_seconds_arr'], // [ 36000, 39600 ]
644 - 'how_many_items_to_book' => $local_params['how_many_items_to_book'], // 1
645 - 'request_uri' => $re_cleaned_params['request_uri'], // 'http://beta/resource-id2/'
646 - 'allow_past' => ! empty( $re_cleaned_params['allow_past'] ),
647 - 'is_use_booking_recurrent_time' => $local_params['is_use_booking_recurrent_time'], // true | false
648 - 'time_override_source' => ! empty( $local_params['time_override_arr']['source'] ) ? $local_params['time_override_arr']['source'] : '',
649 - 'as_single_resource' => false, // false
650 - 'aggregate_resource_id_arr' => $local_params['aggregate_resource_id_arr'], // Optional can be ''
651 - '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.
652 - 'custom_form' => $re_cleaned_params['custom_form'] // FixIn: 10.0.0.10.
653 - ));
654 - // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- NO SLOTS TO SAVE " >
655 - if ( 'error' == $where_to_save_booking['result'] ) {
656 - $ajx_data_arr['status'] = 'error';
657 - $ajx_data_arr['status_error'] = 'booking_can_not_save';
658 - $ajx_data_arr['ajx_after_action_message'] = $where_to_save_booking['message'];
659 - $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
660 - return array( 'ajx_data' => $ajx_data_arr );
661 - }
662 - // </editor-fold>
663 -
664 - // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " >
665 - $php_performance = wpbc_php_performance_END( 'wpbc__where_to_save_booking' , $php_performance );
666 - // </editor-fold>
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 );
667 715 }
668 716
669 - if ( ! empty( $local_params['appointment_service'] ) && function_exists( 'wpbc_appointment_services_check_buffer_conflicts' ) ) {
670 - $buffer_check = wpbc_appointment_services_check_buffer_conflicts(
671 - $local_params['appointment_service'],
672 - $where_to_save_booking['main__resource_id'],
673 - array_keys( $where_to_save_booking['resources_in_dates'] ),
674 - $local_params['time_as_seconds_arr'],
675 - $local_params['skip_booking_id']
676 - );
677 - if ( is_wp_error( $buffer_check ) ) {
678 - $ajx_data_arr['status'] = 'error';
679 - $ajx_data_arr['status_error'] = 'appointment_service_buffer_conflict';
680 - $ajx_data_arr['ajx_after_action_message'] = $buffer_check->get_error_message();
681 - $ajx_data_arr['ajx_after_action_message_status'] = 'warning';
682 - return array( 'ajx_data' => $ajx_data_arr );
683 - }
684 - }
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 );
685 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';
686 728
687 - // Get parameters, from REQUEST
688 - $create_params = $local_params;
689 - $create_params['resource_id'] = ( ! empty( $local_params['edit_resource_id'] ) )
690 - ? $local_params['edit_resource_id'] // If we edit, then use original resource ???
691 - : $where_to_save_booking['main__resource_id']; // Here is important TIP, resource can be where is free, and not where we submit
692 - /**
693 - * 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']
694 - * but what, if $where_to_save_booking do not contain this $local_params['edit_resource_id'] as available resource.
695 - * or even we have $local_params['edit_resource_id'] = 2 and $where_to_save_booking contain resources like [ 1, 2, 3, 4 ]
696 - * we make booking for 3 slots
697 - * in this case, main resource will be 2
698 - * 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 )
699 - * "(205, '2023-10-04 00:00:00', 0, NULL)" <- main resource '2' e.g. $local_params['edit_resource_id'] = 2
700 - * "(205, '2023-10-04 00:00:00', 0, 2)" ? <- child resource '2' e.g. [ .., 2, .. ] in $where_to_save_booking WHICH IS WRONG
701 - */
702 - $create_params['is_emails_send'] = $re_cleaned_params['is_emails_send'];
703 - $create_params['custom_form'] = $re_cleaned_params['custom_form'];
704 -
705 - make_bk_action( 'check_multiuser_params_for_client_side', $create_params['resource_id'] ); // Activate working with specific user in WP MU
706 -
707 - // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " >
708 - $php_performance = wpbc_php_performance_START( 'wpbc_db__booking_save' , $php_performance );
709 - // </editor-fold>
710 -
711 - // -----------------------------------------------------------------------------------------------------------------
712 - // == CREATE_THE 'NEW_BOOKING' ==
713 - // -----------------------------------------------------------------------------------------------------------------
714 - $create_booking_params = array(
715 - 'resource_id' => $create_params['resource_id'],
716 - 'custom_form' => $create_params['custom_form'],
717 - 'all_booking_data_arr' => $create_params['all_booking_data_arr'],
718 - 'dates_only_sql_arr' => $create_params['dates_only_sql_arr'],
719 - 'time_as_his_arr' => $create_params['time_as_his_arr'],
720 - 'is_from_admin_panel' => $create_params['is_from_admin_panel'],
721 - 'is_edit_booking' => $create_params['is_edit_booking'],
722 - 'is_duplicate_booking' => $create_params['is_duplicate_booking'],
723 - 'is_approve_booking' => $create_params['is_approve_booking'],
724 - 'how_many_items_to_book' => $create_params['how_many_items_to_book'],
725 - 'is_use_booking_recurrent_time' => $create_params['is_use_booking_recurrent_time'] // true | false
726 - );
727 - if ( ! empty( $create_params['appointment_service'] ) ) { $create_booking_params['appointment_service'] = $create_params['appointment_service']; }
728 - if ( ! empty( $create_params['sync_gid'] ) ) { $create_booking_params['sync_gid'] = $create_params['sync_gid']; }
729 -
730 - $booking_new_arr = wpbc_db__booking_save( $create_booking_params, $where_to_save_booking );
731 -
732 - // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- BOOKING CREATION " >
733 - if ( 'ok' !== $booking_new_arr['status'] ) {
734 - $ajx_data_arr['status'] = $booking_new_arr['status'];
735 - $ajx_data_arr['status_error'] = 'booking_can_not_save';
736 - $ajx_data_arr['ajx_after_action_message'] = $booking_new_arr['message'];
737 - $ajx_data_arr['ajx_after_action_message_status'] = 'error';
738 - return array( 'ajx_data' => $ajx_data_arr );
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 );
739 804 }
740 - // </editor-fold>
741 805
806 + // Released compatibility hook: arbitrary callbacks must not extend the database lock duration.
742 807 do_action( 'wpbc_booking_after_save', $booking_new_arr['booking_id'], $create_booking_params, $where_to_save_booking );
743 808
744 809 // FixIn: 9.9.0.36.
745 810 if (
@@ -1250,9 +1315,9 @@
1250 1315 $sql_field_arr[] = array( 'name' => 'form', 'type' => '%s', 'value' => $form_data );
1251 1316 $sql_field_arr[] = array( 'name' => 'booking_type', 'type' => '%d', 'value' => $create_params['resource_id'] );
1252 1317 $sql_field_arr[] = array( 'name' => 'modification_date', 'type' => '%s', 'value' => gmdate( 'Y-m-d H:i:s' ) );
1253 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] );
1254 - $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() );
1255 1320
1256 1321
1257 1322 if (
1258 1323 ( 0 == $create_params['is_edit_booking'] ) || // If not edit, then INSERT.
@@ -1273,12 +1338,15 @@
1273 1338 $sql_prepare_arr['name'] = implode( ', ', $sql_prepare_arr['name'] );
1274 1339 $sql_prepare_arr['type'] = implode( ', ', $sql_prepare_arr['type'] );
1275 1340 /* phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare */
1276 1341 $sql = $wpdb->prepare( "INSERT INTO {$wpdb->prefix}booking " . " ( {$sql_prepare_arr['name']} )" . " VALUES ( {$sql_prepare_arr['type']} )", $sql_prepare_arr['value'] );
1277 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
1278 - if ( false === $wpdb->query( $sql ) ) {
1279 - return array( 'status' => 'error', 'message' => 'Error. INSERT New Data in DB.' . ' FILE:' . __FILE__ . ' LINE:' . __LINE__ . ' SQL:' . $sql );
1280 - }
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 + }
1281 1349 // Get ID of booking
1282 1350 $booking_id = (int) $wpdb->insert_id;
1283 1351
1284 1352 } else { // Edit - UPDATE
@@ -1292,14 +1360,15 @@
1292 1360 $sql_prepare_arr['set'] = implode( ', ', $sql_prepare_arr['set'] );
1293 1361
1294 1362 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
1295 1363 $sql = $wpdb->prepare( "UPDATE {$wpdb->prefix}booking SET {$sql_prepare_arr['set']} WHERE booking_id={$booking_id};", $sql_prepare_arr['value'] );
1296 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
1297 - if ( false === $wpdb->query( $sql ) ) {
1298 - return array( 'status' => 'error',
1299 - 'message' => 'Error. UPDATE Exist Data in DB.' . ' FILE:' . __FILE__ . ' LINE:' . __LINE__ . ' SQL:' . $sql,
1300 - );
1301 - }
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 + }
1302 1371
1303 1372 // Check if dates previously was approved.
1304 1373 $slct_sql = "SELECT approved FROM {$wpdb->prefix}bookingdates WHERE booking_id IN ({$booking_id}) LIMIT 0,1";
1305 1374 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
@@ -1590,9 +1659,9 @@
1590 1659 *
1591 1660 * // Now get start/end times as seconds: [ 64800, 72000 ]
1592 1661 * $time_as_seconds_arr = wpbc_get_in_booking_form__time_to_book_as_seconds_arr( $structured_booking_data_arr );
1593 1662 */
1594 - 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 ){
1595 1664
1596 1665 $selected_time_fields = wpbc_get__selected_time_fields__in_booking_form__as_arr( $booking_form_data__arr );
1597 1666
1598 1667 // 2.2 Get selected SECONDS to book ---------------------------------------------------------------------------
@@ -1631,12 +1700,69 @@
1631 1700 }
1632 1701 }
1633 1702 }
1634 1703
1635 - return $time_as_seconds_arr;
1636 - }
1637 -
1638 -
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 +
1639 1765 /**
1640 1766 * Remove administrator time-override values from an unauthorized booking request.
1641 1767 *
1642 1768 * The public booking endpoint intentionally accepts unauthenticated requests, so