| @@ -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 |
| @@ -66,12 +64,13 @@ | ||
| 66 | 64 | 'wpbc_bfb_preview_token' => array( 'validate' => 'strong', 'default' => '' ), |
| 67 | 65 | 'wpbc_bfb_preview_form_id' => array( 'validate' => 'd', 'default' => 0 ), |
| 68 | 66 | 'wpbc_bfb_preview_nonce' => array( 'validate' => 'strong', 'default' => '' ), |
| 69 | 67 | 'wpbc_time_override_enabled' => array( 'validate' => 'd', 'default' => 0 ), |
| 70 | - 'wpbc_time_override_source' => array( 'validate' => 'strong', 'default' => '' ), | |
| 71 | - 'wpbc_time_override_start' => array( 'validate' => 'strong', 'default' => '' ), | |
| 72 | - 'wpbc_time_override_end' => array( 'validate' => 'strong', 'default' => '' ), | |
| 73 | - ), $workflow_request_rules ) | |
| 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 ) | |
| 74 | 73 | )); |
| 75 | 74 | |
| 76 | 75 | // Escape of request params in Ajax Post. We use prefix 'calendar_request_params', if Ajax sent - $_REQUEST['calendar_request_params']['resource_id'], ... |
| 77 | 76 | $request_prefix = 'calendar_request_params'; |
| @@ -77,15 +76,16 @@ | ||
| 77 | 76 | $request_prefix = 'calendar_request_params'; |
| 78 | 77 | |
| 79 | 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"; |
| 80 | 79 | |
| 81 | - $request_params = $user_request->get_sanitized__in_request__value_or_default( $request_prefix ); // NOT Direct: $_REQUEST['calendar_request_params']['resource_id'] | |
| 82 | - $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 */ | |
| 83 | - $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'] ); | |
| 84 | 84 | |
| 85 | 85 | // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- CAPTCHA " > |
| 86 | 86 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 87 | - 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 ] ); | |
| 88 | 88 | // </editor-fold> |
| 89 | 89 | |
| 90 | 90 | // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- BOOKING_RESOURCE ID " > |
| 91 | 91 | if ( $request_params['resource_id'] <= 0 ) { |
| @@ -92,16 +92,13 @@ | ||
| 92 | 92 | $ajx_data_arr['status'] = 'error'; |
| 93 | 93 | $ajx_data_arr['status_error'] = 'resource_id_incorrect'; |
| 94 | 94 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 95 | 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'] . ' ]'; |
| 96 | - $ajx_data_arr['ajx_after_action_message_status'] = 'error'; | |
| 97 | - wp_send_json( array( | |
| 98 | - 'ajx_data' => $ajx_data_arr, | |
| 99 | - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized | |
| 100 | - 'ajx_search_params' => $_REQUEST[ $request_prefix ], | |
| 101 | - 'ajx_cleaned_params' => $request_params, | |
| 102 | - 'resource_id' => $request_params['resource_id'], | |
| 103 | - ) ); | |
| 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 | + ) ); | |
| 104 | 101 | } |
| 105 | 102 | // </editor-fold> |
| 106 | 103 | |
| 107 | 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 */ |
| @@ -124,11 +121,12 @@ | ||
| 124 | 121 | 'wpbc_bfb_preview_token' => $request_params['wpbc_bfb_preview_token'], |
| 125 | 122 | 'wpbc_bfb_preview_form_id' => $request_params['wpbc_bfb_preview_form_id'], |
| 126 | 123 | 'wpbc_bfb_preview_nonce' => $request_params['wpbc_bfb_preview_nonce'], |
| 127 | 124 | 'wpbc_time_override_enabled' => $request_params['wpbc_time_override_enabled'], |
| 128 | - 'wpbc_time_override_source' => $request_params['wpbc_time_override_source'], | |
| 129 | - 'wpbc_time_override_start' => $request_params['wpbc_time_override_start'], | |
| 125 | + 'wpbc_time_override_source' => $request_params['wpbc_time_override_source'], | |
| 126 | + 'wpbc_time_override_start' => $request_params['wpbc_time_override_start'], | |
| 130 | 127 | 'wpbc_time_override_end' => $request_params['wpbc_time_override_end'], |
| 128 | + 'wpbc_admin_cost_correction' => $request_params['wpbc_admin_cost_correction'], | |
| 131 | 129 | ); |
| 132 | 130 | $request_save_params['service_id'] = $request_params['service_id']; |
| 133 | 131 | $request_save_params['appointment_service_required'] = $request_params['appointment_service_required']; |
| 134 | 132 | $request_save_params['appointment_context_token'] = $request_params['appointment_context_token']; |
| @@ -133,19 +131,20 @@ | ||
| 133 | 131 | $request_save_params['appointment_service_required'] = $request_params['appointment_service_required']; |
| 134 | 132 | $request_save_params['appointment_context_token'] = $request_params['appointment_context_token']; |
| 135 | 133 | $request_save_params['resource_selector_required'] = $request_params['resource_selector_required']; |
| 136 | 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']; | |
| 137 | 136 | $booking_save_arr = wpbc_booking_save( $request_save_params ); |
| 138 | 137 | |
| 139 | 138 | // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- BOOKING " > |
| 140 | 139 | if ( 'ok' !== $booking_save_arr['ajx_data']['status'] ) { |
| 141 | 140 | |
| 142 | - wp_send_json( array( 'ajx_data' => $booking_save_arr['ajx_data'], | |
| 143 | - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized | |
| 144 | - 'ajx_search_params' => $_REQUEST[ $request_prefix ], | |
| 145 | - 'ajx_cleaned_params' => $request_params, | |
| 146 | - 'resource_id' => $request_params['resource_id'] | |
| 147 | - )); | |
| 141 | + wp_send_json( | |
| 142 | + array( | |
| 143 | + 'ajx_data' => $booking_save_arr['ajx_data'], | |
| 144 | + 'resource_id' => $request_params['resource_id'], | |
| 145 | + ) | |
| 146 | + ); | |
| 148 | 147 | } |
| 149 | 148 | // </editor-fold> |
| 150 | 149 | |
| 151 | 150 | $ajx_data_arr = $booking_save_arr['ajx_data']; |
| @@ -214,10 +213,94 @@ | ||
| 214 | 213 | // --------------------------------------------------------------------------------------------------------------------- |
| 215 | 214 | // == Save Booking |
| 216 | 215 | // --------------------------------------------------------------------------------------------------------------------- |
| 217 | 216 | |
| 218 | -/** | |
| 219 | - * 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 | |
| 220 | 303 | * |
| 221 | 304 | * @param $request_params = [ |
| 222 | 305 | * resource_id = 2 REQUIRED Default: 1 |
| 223 | 306 | * dates_ddmmyy_csv = '27.10.2023, 28.10.2023, 29.10.2023' REQUIRED |
| @@ -294,18 +377,22 @@ | ||
| 294 | 377 | 'wpbc_bfb_preview_token' => array( 'validate' => 'strong', 'default' => '' ), |
| 295 | 378 | 'wpbc_bfb_preview_form_id' => array( 'validate' => 'd', 'default' => 0 ), |
| 296 | 379 | 'wpbc_bfb_preview_nonce' => array( 'validate' => 'strong', 'default' => '' ), |
| 297 | 380 | 'wpbc_time_override_enabled' => array( 'validate' => 'd', 'default' => 0 ), |
| 298 | - 'wpbc_time_override_source' => array( 'validate' => 'strong', 'default' => '' ), | |
| 299 | - 'wpbc_time_override_start' => array( 'validate' => 'strong', 'default' => '' ), | |
| 381 | + 'wpbc_time_override_source' => array( 'validate' => 'strong', 'default' => '' ), | |
| 382 | + 'wpbc_time_override_start' => array( 'validate' => 'strong', 'default' => '' ), | |
| 300 | 383 | 'wpbc_time_override_end' => array( 'validate' => 'strong', 'default' => '' ), |
| 301 | - ); | |
| 384 | + 'wpbc_admin_cost_correction' => array( 'validate' => 'strong', 'default' => '' ), | |
| 385 | + ); | |
| 302 | 386 | $validate_arr_rules['service_id'] = array( 'validate' => 'd', 'default' => 0 ); |
| 303 | 387 | $validate_arr_rules['appointment_service_required'] = array( 'validate' => 'd', 'default' => 0 ); |
| 304 | 388 | $validate_arr_rules['appointment_context_token'] = array( 'validate' => 'strong', 'default' => '' ); |
| 305 | 389 | $validate_arr_rules['resource_selector_required'] = array( 'validate' => 'd', 'default' => 0 ); |
| 306 | 390 | $validate_arr_rules['resource_selector_context_token'] = array( 'validate' => 'strong', 'default' => '' ); |
| 391 | + $validate_arr_rules['wpbc_admin_booking_nonce'] = array( 'validate' => 'strong', 'default' => '' ); | |
| 307 | 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; | |
| 308 | 395 | if ( ! empty( $re_cleaned_params['appointment_service_required'] ) && empty( $re_cleaned_params['service_id'] ) ) { |
| 309 | 396 | $ajx_data_arr['status'] = 'error'; |
| 310 | 397 | $ajx_data_arr['status_error'] = 'appointment_service_required'; |
| 311 | 398 | $ajx_data_arr['ajx_after_action_message'] = __( 'Please select a Service.', 'booking' ); |
| @@ -328,13 +415,14 @@ | ||
| 328 | 415 | $ajx_data_arr['ajx_after_action_message'] = $appointment_context_check->get_error_message(); |
| 329 | 416 | $ajx_data_arr['ajx_after_action_message_status'] = 'warning'; |
| 330 | 417 | return array( 'ajx_data' => $ajx_data_arr ); |
| 331 | 418 | } |
| 419 | + $has_verified_appointment_context = true; | |
| 332 | 420 | |
| 333 | 421 | // A client value cannot enable past Appointment creation; trust only the site-authored signed context. |
| 334 | 422 | $re_cleaned_params['allow_past'] = wpbc_booking_appointment_is_past_booking_enabled( $appointment_context_check ) ? 1 : 0; |
| 335 | 423 | } |
| 336 | - 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'] ) ) { | |
| 337 | 425 | if ( ! function_exists( 'wpbc_booking_resource_selector_validate_submission_context' ) ) { |
| 338 | 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' ) ); |
| 339 | 427 | } else { |
| 340 | 428 | $resource_selector_context_check = wpbc_booking_resource_selector_validate_submission_context( |
| @@ -348,16 +436,15 @@ | ||
| 348 | 436 | $ajx_data_arr['ajx_after_action_message'] = $resource_selector_context_check->get_error_message(); |
| 349 | 437 | $ajx_data_arr['ajx_after_action_message_status'] = 'warning'; |
| 350 | 438 | return array( 'ajx_data' => $ajx_data_arr ); |
| 351 | 439 | } |
| 440 | + $has_verified_resource_selector_context = true; | |
| 352 | 441 | |
| 353 | 442 | // Trust only the site-authored signed selector context for public past bookings. |
| 354 | 443 | $re_cleaned_params['allow_past'] = wpbc_booking_resource_selector_is_past_booking_enabled( $resource_selector_context_check ) ? 1 : 0; |
| 355 | 444 | } |
| 356 | 445 | |
| 357 | - $admin_uri = ltrim( str_replace( get_site_url( null, '', 'admin' ), '', admin_url( 'admin.php?' ) ), '/' ); // wp-admin/admin.php? | |
| 358 | - | |
| 359 | - $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'] ); | |
| 360 | 447 | if ( 'preview' !== $re_cleaned_params['form_status'] ) { |
| 361 | 448 | $re_cleaned_params['form_status'] = 'published'; |
| 362 | 449 | } |
| 363 | 450 | // FixIn: 2026-02-05 - make preview/published available to form parsing/templates during this request. |
| @@ -374,10 +461,11 @@ | ||
| 374 | 461 | |
| 375 | 462 | // ----------------------------------------------------------------------------------------------------------------- |
| 376 | 463 | // Local parameters |
| 377 | 464 | // ----------------------------------------------------------------------------------------------------------------- |
| 378 | - $local_params = array(); | |
| 379 | - $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; | |
| 380 | 468 | $local_params['user_id'] = $re_cleaned_params['user_id']; // 1 |
| 381 | 469 | $local_params['sync_gid'] = $re_cleaned_params['sync_gid']; // '' |
| 382 | 470 | $local_params['is_approve_booking'] = $re_cleaned_params['is_approve_booking']; // 0 | 1 |
| 383 | 471 | $local_params['is_use_booking_recurrent_time'] = ( 1 === $re_cleaned_params['is_use_booking_recurrent_time'] ); // false | true |
| @@ -383,13 +471,8 @@ | ||
| 383 | 471 | $local_params['is_use_booking_recurrent_time'] = ( 1 === $re_cleaned_params['is_use_booking_recurrent_time'] ); // false | true |
| 384 | 472 | $request_action = isset( $_REQUEST['action'] ) && is_scalar( $_REQUEST['action'] ) |
| 385 | 473 | ? sanitize_key( (string) wp_unslash( $_REQUEST['action'] ) ) |
| 386 | 474 | : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 387 | - $is_authorized_admin_booking_request = $local_params['is_from_admin_panel'] | |
| 388 | - && is_user_logged_in() | |
| 389 | - && class_exists( 'WPBC_Add_Booking_Component' ) | |
| 390 | - && WPBC_Add_Booking_Component::current_user_can_add_booking() | |
| 391 | - && wpbc_is_mu_user_can_be_here( 'activated_user' ); | |
| 392 | 475 | $is_public_booking_create_request = wp_doing_ajax() |
| 393 | 476 | && 'wpbc_ajx_booking__create' === strtolower( $request_action ) |
| 394 | 477 | && ! $is_authorized_admin_booking_request; |
| 395 | 478 | |
| @@ -394,8 +477,10 @@ | ||
| 394 | 477 | && ! $is_authorized_admin_booking_request; |
| 395 | 478 | |
| 396 | 479 | // Time overrides belong exclusively to the capability-protected Add Booking administration workflow. |
| 397 | 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 ); | |
| 398 | 483 | |
| 399 | 484 | // ----------------------------------------------------------------------------------------------------------------- |
| 400 | 485 | // Parse Local parameters for later use |
| 401 | 486 | // ----------------------------------------------------------------------------------------------------------------- |
| @@ -502,8 +587,9 @@ | ||
| 502 | 587 | // [ '2023-09-10', '2023-09-11' ] |
| 503 | 588 | $local_params['dates_only_sql_arr'] = wpbc_convert_dates_str__dd_mm_yyyy__to__yyyy_mm_dd( $re_cleaned_params["dates_ddmmyy_csv"] ); |
| 504 | 589 | $local_params['dates_only_sql_arr'] = explode( ',', $local_params['dates_only_sql_arr'] ); |
| 505 | 590 | |
| 591 | + $classic_context = array(); | |
| 506 | 592 | $has_verified_classic_context = false; |
| 507 | 593 | if ( ! empty( $re_cleaned_params['classic_booking_context_token'] ) && function_exists( 'wpbc_classic_booking_context_validate_submission' ) ) { |
| 508 | 594 | $classic_context = wpbc_classic_booking_context_validate_submission( |
| 509 | 595 | $re_cleaned_params['classic_booking_context_token'], |
| @@ -519,22 +605,38 @@ | ||
| 519 | 605 | $ajx_data_arr['ajx_after_action_message_status'] = 'warning'; |
| 520 | 606 | return array( 'ajx_data' => $ajx_data_arr ); |
| 521 | 607 | } |
| 522 | 608 | |
| 523 | - $has_verified_classic_context = true; | |
| 609 | + $has_verified_classic_context = true; | |
| 524 | 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'] ); | |
| 525 | 613 | } |
| 526 | 614 | |
| 527 | - $has_verified_workflow_context = ( | |
| 528 | - ( ! empty( $re_cleaned_params['service_id'] ) && ! empty( $re_cleaned_params['appointment_context_token'] ) ) | |
| 529 | - || ( ! 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 | |
| 530 | 628 | ); |
| 531 | - if ( $is_public_booking_create_request && ! $has_verified_classic_context && ! $has_verified_workflow_context ) { | |
| 532 | - $re_cleaned_params['allow_past'] = 0; | |
| 533 | - $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 | + } | |
| 534 | 636 | } |
| 535 | - | |
| 536 | - if ( | |
| 637 | + | |
| 638 | + if ( | |
| 537 | 639 | ( ! empty( $local_params['time_override_arr'] ) ) |
| 538 | 640 | && ( 'times_availability' === $local_params['time_override_arr']['source'] ) |
| 539 | 641 | && ( count( array_filter( $local_params['dates_only_sql_arr'] ) ) > 1 ) |
| 540 | 642 | ) { |
| @@ -543,13 +645,16 @@ | ||
| 543 | 645 | |
| 544 | 646 | $local_params['is_show_payment_form'] = $re_cleaned_params["is_show_payment_form"]; |
| 545 | 647 | |
| 546 | 648 | // FixIn: 9.9.0.35. |
| 547 | - if ( $local_params['is_show_payment_form'] ) { | |
| 548 | - $local_params['is_show_payment_form'] = ( false !== strpos( $re_cleaned_params['request_uri'], 'is_show_payment_form=Off' ) ) | |
| 549 | - ? 0 | |
| 550 | - : $local_params['is_show_payment_form']; // 1|0 | |
| 551 | - } | |
| 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 | + } | |
| 552 | 657 | |
| 553 | 658 | // Get EDIT booking data |
| 554 | 659 | $local_params['edit_resource_id'] = ''; |
| 555 | 660 | $local_params['skip_booking_id'] = ''; |
| @@ -598,143 +703,108 @@ | ||
| 598 | 703 | // ----------------------------------------------------------------------------------------------------------------- |
| 599 | 704 | // Here GO |
| 600 | 705 | // ----------------------------------------------------------------------------------------------------------------- |
| 601 | 706 | |
| 602 | - // Force - resource saving parameters, instead of wpbc__where_to_save_booking() | |
| 603 | - if ( ! empty( $re_cleaned_params["save_booking_even_if_unavailable"] ) ) { | |
| 604 | - | |
| 605 | - $local_params['how_many_items_to_book'] = 1; | |
| 606 | - | |
| 607 | - $dates_keys_arr = array_values( $local_params['dates_only_sql_arr'] ); // [ '2023-09-23', '2023-09-24' ] | |
| 608 | - | |
| 609 | - $resources_in_dates = array_fill_keys( $dates_keys_arr , array( $local_params['initial_resource_id'] ) ); // [ 2023-09-23 = [ 2 ], 2023-09-24 = [ 2 ] ] | |
| 610 | - | |
| 611 | - $where_to_save_booking = array(); | |
| 612 | - $where_to_save_booking['result'] = 'ok'; | |
| 613 | - $where_to_save_booking['resources_in_dates'] = $resources_in_dates; // [ 2023-09-23 = [ 2, 10, 11 ], 2023-09-24 = [ 2, 10, 11 ] | |
| 614 | - $where_to_save_booking['time_to_book'] = $local_params['time_as_his_arr']; // [ "00:00:00", "24:00:00" ] | |
| 615 | - $where_to_save_booking['main__resource_id'] = $local_params['initial_resource_id']; // here edit or request (parent/single) resource | |
| 616 | - | |
| 617 | - } else { | |
| 618 | - // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " > | |
| 619 | - $php_performance = wpbc_php_performance_START( 'wpbc__where_to_save_booking' , $php_performance ); | |
| 620 | - // </editor-fold> | |
| 621 | - | |
| 622 | - /** | |
| 623 | - * Get slots [] where we can save booking = [ 'resources_in_dates' => [ 2023-10-18 = [ 2, 12, 10, 11 ] | |
| 624 | - * 2023-10-19 = [ 2, 12, 10, 11 ] | |
| 625 | - * 2023-10-20 = [ 2, 12, 10, 11 ] | |
| 626 | - * ], | |
| 627 | - * 'time_to_book' => [ "14:00:01" , "12:00:01" ], | |
| 628 | - * 'result' => 'ok' | |
| 629 | - * 'main__resource_id' => 2 | |
| 630 | - * ] | |
| 631 | - * OR | |
| 632 | - * [ 'result' => 'error', 'message' => 'Booking can not be saved ...' ] | |
| 633 | - */ | |
| 634 | - $where_to_save_booking = wpbc__where_to_save_booking( array( | |
| 635 | - 'resource_id' => $local_params['initial_resource_id'], // 2 //TODO: If edit booking. What to pass 'edit' or 'parent' resource ID? | |
| 636 | - 'skip_booking_id' => $local_params['skip_booking_id'], // '', | 125 if edit booking | |
| 637 | - 'dates_only_sql_arr' => $local_params['dates_only_sql_arr'], // [ "2023-10-18", "2023-10-25", "2023-11-25" ] | |
| 638 | - 'time_as_seconds_arr' => $local_params['time_as_seconds_arr'], // [ 36000, 39600 ] | |
| 639 | - 'how_many_items_to_book' => $local_params['how_many_items_to_book'], // 1 | |
| 640 | - 'request_uri' => $re_cleaned_params['request_uri'], // 'http://beta/resource-id2/' | |
| 641 | - 'allow_past' => ! empty( $re_cleaned_params['allow_past'] ), | |
| 642 | - 'is_use_booking_recurrent_time' => $local_params['is_use_booking_recurrent_time'], // true | false | |
| 643 | - 'time_override_source' => ! empty( $local_params['time_override_arr']['source'] ) ? $local_params['time_override_arr']['source'] : '', | |
| 644 | - 'as_single_resource' => false, // false | |
| 645 | - 'aggregate_resource_id_arr' => $local_params['aggregate_resource_id_arr'], // Optional can be '' | |
| 646 | - '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. | |
| 647 | - 'custom_form' => $re_cleaned_params['custom_form'] // FixIn: 10.0.0.10. | |
| 648 | - )); | |
| 649 | - // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- NO SLOTS TO SAVE " > | |
| 650 | - if ( 'error' == $where_to_save_booking['result'] ) { | |
| 651 | - $ajx_data_arr['status'] = 'error'; | |
| 652 | - $ajx_data_arr['status_error'] = 'booking_can_not_save'; | |
| 653 | - $ajx_data_arr['ajx_after_action_message'] = $where_to_save_booking['message']; | |
| 654 | - $ajx_data_arr['ajx_after_action_message_status'] = 'warning'; | |
| 655 | - return array( 'ajx_data' => $ajx_data_arr ); | |
| 656 | - } | |
| 657 | - // </editor-fold> | |
| 658 | - | |
| 659 | - // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " > | |
| 660 | - $php_performance = wpbc_php_performance_END( 'wpbc__where_to_save_booking' , $php_performance ); | |
| 661 | - // </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 ); | |
| 662 | 715 | } |
| 663 | 716 | |
| 664 | - if ( ! empty( $local_params['appointment_service'] ) && function_exists( 'wpbc_appointment_services_check_buffer_conflicts' ) ) { | |
| 665 | - $buffer_check = wpbc_appointment_services_check_buffer_conflicts( | |
| 666 | - $local_params['appointment_service'], | |
| 667 | - $where_to_save_booking['main__resource_id'], | |
| 668 | - array_keys( $where_to_save_booking['resources_in_dates'] ), | |
| 669 | - $local_params['time_as_seconds_arr'], | |
| 670 | - $local_params['skip_booking_id'] | |
| 671 | - ); | |
| 672 | - if ( is_wp_error( $buffer_check ) ) { | |
| 673 | - $ajx_data_arr['status'] = 'error'; | |
| 674 | - $ajx_data_arr['status_error'] = 'appointment_service_buffer_conflict'; | |
| 675 | - $ajx_data_arr['ajx_after_action_message'] = $buffer_check->get_error_message(); | |
| 676 | - $ajx_data_arr['ajx_after_action_message_status'] = 'warning'; | |
| 677 | - return array( 'ajx_data' => $ajx_data_arr ); | |
| 678 | - } | |
| 679 | - } | |
| 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 ); | |
| 680 | 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'; | |
| 681 | 728 | |
| 682 | - // Get parameters, from REQUEST | |
| 683 | - $create_params = $local_params; | |
| 684 | - $create_params['resource_id'] = ( ! empty( $local_params['edit_resource_id'] ) ) | |
| 685 | - ? $local_params['edit_resource_id'] // If we edit, then use original resource ??? | |
| 686 | - : $where_to_save_booking['main__resource_id']; // Here is important TIP, resource can be where is free, and not where we submit | |
| 687 | - /** | |
| 688 | - * 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'] | |
| 689 | - * but what, if $where_to_save_booking do not contain this $local_params['edit_resource_id'] as available resource. | |
| 690 | - * or even we have $local_params['edit_resource_id'] = 2 and $where_to_save_booking contain resources like [ 1, 2, 3, 4 ] | |
| 691 | - * we make booking for 3 slots | |
| 692 | - * in this case, main resource will be 2 | |
| 693 | - * 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 ) | |
| 694 | - * "(205, '2023-10-04 00:00:00', 0, NULL)" <- main resource '2' e.g. $local_params['edit_resource_id'] = 2 | |
| 695 | - * "(205, '2023-10-04 00:00:00', 0, 2)" ? <- child resource '2' e.g. [ .., 2, .. ] in $where_to_save_booking WHICH IS WRONG | |
| 696 | - */ | |
| 697 | - $create_params['is_emails_send'] = $re_cleaned_params['is_emails_send']; | |
| 698 | - $create_params['custom_form'] = $re_cleaned_params['custom_form']; | |
| 699 | - | |
| 700 | - make_bk_action( 'check_multiuser_params_for_client_side', $create_params['resource_id'] ); // Activate working with specific user in WP MU | |
| 701 | - | |
| 702 | - // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " > | |
| 703 | - $php_performance = wpbc_php_performance_START( 'wpbc_db__booking_save' , $php_performance ); | |
| 704 | - // </editor-fold> | |
| 705 | - | |
| 706 | - // ----------------------------------------------------------------------------------------------------------------- | |
| 707 | - // == CREATE_THE 'NEW_BOOKING' == | |
| 708 | - // ----------------------------------------------------------------------------------------------------------------- | |
| 709 | - $create_booking_params = array( | |
| 710 | - 'resource_id' => $create_params['resource_id'], | |
| 711 | - 'custom_form' => $create_params['custom_form'], | |
| 712 | - 'all_booking_data_arr' => $create_params['all_booking_data_arr'], | |
| 713 | - 'dates_only_sql_arr' => $create_params['dates_only_sql_arr'], | |
| 714 | - 'time_as_his_arr' => $create_params['time_as_his_arr'], | |
| 715 | - 'is_from_admin_panel' => $create_params['is_from_admin_panel'], | |
| 716 | - 'is_edit_booking' => $create_params['is_edit_booking'], | |
| 717 | - 'is_duplicate_booking' => $create_params['is_duplicate_booking'], | |
| 718 | - 'is_approve_booking' => $create_params['is_approve_booking'], | |
| 719 | - 'how_many_items_to_book' => $create_params['how_many_items_to_book'], | |
| 720 | - 'is_use_booking_recurrent_time' => $create_params['is_use_booking_recurrent_time'] // true | false | |
| 721 | - ); | |
| 722 | - if ( ! empty( $create_params['appointment_service'] ) ) { $create_booking_params['appointment_service'] = $create_params['appointment_service']; } | |
| 723 | - if ( ! empty( $create_params['sync_gid'] ) ) { $create_booking_params['sync_gid'] = $create_params['sync_gid']; } | |
| 724 | - | |
| 725 | - $booking_new_arr = wpbc_db__booking_save( $create_booking_params, $where_to_save_booking ); | |
| 726 | - | |
| 727 | - // <editor-fold defaultstate="collapsed" desc=" :: ERROR :: <- BOOKING CREATION " > | |
| 728 | - if ( 'ok' !== $booking_new_arr['status'] ) { | |
| 729 | - $ajx_data_arr['status'] = $booking_new_arr['status']; | |
| 730 | - $ajx_data_arr['status_error'] = 'booking_can_not_save'; | |
| 731 | - $ajx_data_arr['ajx_after_action_message'] = $booking_new_arr['message']; | |
| 732 | - $ajx_data_arr['ajx_after_action_message_status'] = 'error'; | |
| 733 | - 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 ); | |
| 734 | 804 | } |
| 735 | - // </editor-fold> | |
| 736 | 805 | |
| 806 | + // Released compatibility hook: arbitrary callbacks must not extend the database lock duration. | |
| 737 | 807 | do_action( 'wpbc_booking_after_save', $booking_new_arr['booking_id'], $create_booking_params, $where_to_save_booking ); |
| 738 | 808 | |
| 739 | 809 | // FixIn: 9.9.0.36. |
| 740 | 810 | if ( |
| @@ -776,9 +846,10 @@ | ||
| 776 | 846 | $payment_params['is_edit_booking'] = $create_params['is_edit_booking']; // => 0 0 | int - ID of the booking |
| 777 | 847 | $payment_params['custom_form'] = $create_params['custom_form']; // => '' '' | 'some_name' |
| 778 | 848 | $payment_params['is_duplicate_booking'] = $create_params['is_duplicate_booking']; // => 0 0 | 1 |
| 779 | 849 | $payment_params['is_from_admin_panel'] = $create_params['is_from_admin_panel']; // => false true | false |
| 780 | - $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']; | |
| 781 | 852 | if ( $payment_params['is_from_admin_panel'] ) { |
| 782 | 853 | // $payment_params['is_show_payment_form'] = 0; // FixIn: 9.9.0.21. |
| 783 | 854 | } |
| 784 | 855 | // <editor-fold defaultstate="collapsed" desc=" = PERFORMANCE = " > |
| @@ -1244,9 +1315,9 @@ | ||
| 1244 | 1315 | $sql_field_arr[] = array( 'name' => 'form', 'type' => '%s', 'value' => $form_data ); |
| 1245 | 1316 | $sql_field_arr[] = array( 'name' => 'booking_type', 'type' => '%d', 'value' => $create_params['resource_id'] ); |
| 1246 | 1317 | $sql_field_arr[] = array( 'name' => 'modification_date', 'type' => '%s', 'value' => gmdate( 'Y-m-d H:i:s' ) ); |
| 1247 | 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] ); |
| 1248 | - $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() ); | |
| 1249 | 1320 | |
| 1250 | 1321 | |
| 1251 | 1322 | if ( |
| 1252 | 1323 | ( 0 == $create_params['is_edit_booking'] ) || // If not edit, then INSERT. |
| @@ -1267,12 +1338,15 @@ | ||
| 1267 | 1338 | $sql_prepare_arr['name'] = implode( ', ', $sql_prepare_arr['name'] ); |
| 1268 | 1339 | $sql_prepare_arr['type'] = implode( ', ', $sql_prepare_arr['type'] ); |
| 1269 | 1340 | /* phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare */ |
| 1270 | 1341 | $sql = $wpdb->prepare( "INSERT INTO {$wpdb->prefix}booking " . " ( {$sql_prepare_arr['name']} )" . " VALUES ( {$sql_prepare_arr['type']} )", $sql_prepare_arr['value'] ); |
| 1271 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter | |
| 1272 | - if ( false === $wpdb->query( $sql ) ) { | |
| 1273 | - return array( 'status' => 'error', 'message' => 'Error. INSERT New Data in DB.' . ' FILE:' . __FILE__ . ' LINE:' . __LINE__ . ' SQL:' . $sql ); | |
| 1274 | - } | |
| 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 | + } | |
| 1275 | 1349 | // Get ID of booking |
| 1276 | 1350 | $booking_id = (int) $wpdb->insert_id; |
| 1277 | 1351 | |
| 1278 | 1352 | } else { // Edit - UPDATE |
| @@ -1286,14 +1360,15 @@ | ||
| 1286 | 1360 | $sql_prepare_arr['set'] = implode( ', ', $sql_prepare_arr['set'] ); |
| 1287 | 1361 | |
| 1288 | 1362 | // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare |
| 1289 | 1363 | $sql = $wpdb->prepare( "UPDATE {$wpdb->prefix}booking SET {$sql_prepare_arr['set']} WHERE booking_id={$booking_id};", $sql_prepare_arr['value'] ); |
| 1290 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter | |
| 1291 | - if ( false === $wpdb->query( $sql ) ) { | |
| 1292 | - return array( 'status' => 'error', | |
| 1293 | - 'message' => 'Error. UPDATE Exist Data in DB.' . ' FILE:' . __FILE__ . ' LINE:' . __LINE__ . ' SQL:' . $sql, | |
| 1294 | - ); | |
| 1295 | - } | |
| 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 | + } | |
| 1296 | 1371 | |
| 1297 | 1372 | // Check if dates previously was approved. |
| 1298 | 1373 | $slct_sql = "SELECT approved FROM {$wpdb->prefix}bookingdates WHERE booking_id IN ({$booking_id}) LIMIT 0,1"; |
| 1299 | 1374 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| @@ -1584,9 +1659,9 @@ | ||
| 1584 | 1659 | * |
| 1585 | 1660 | * // Now get start/end times as seconds: [ 64800, 72000 ] |
| 1586 | 1661 | * $time_as_seconds_arr = wpbc_get_in_booking_form__time_to_book_as_seconds_arr( $structured_booking_data_arr ); |
| 1587 | 1662 | */ |
| 1588 | - 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 ){ | |
| 1589 | 1664 | |
| 1590 | 1665 | $selected_time_fields = wpbc_get__selected_time_fields__in_booking_form__as_arr( $booking_form_data__arr ); |
| 1591 | 1666 | |
| 1592 | 1667 | // 2.2 Get selected SECONDS to book --------------------------------------------------------------------------- |
| @@ -1625,13 +1700,70 @@ | ||
| 1625 | 1700 | } |
| 1626 | 1701 | } |
| 1627 | 1702 | } |
| 1628 | 1703 | |
| 1629 | - return $time_as_seconds_arr; | |
| 1630 | - } | |
| 1631 | - | |
| 1632 | - | |
| 1704 | + return $time_as_seconds_arr; | |
| 1705 | + } | |
| 1706 | + | |
| 1707 | + | |
| 1633 | 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 | + /** | |
| 1634 | 1766 | * Remove administrator time-override values from an unauthorized booking request. |
| 1635 | 1767 | * |
| 1636 | 1768 | * The public booking endpoint intentionally accepts unauthenticated requests, so |
| 1637 | 1769 | * sanitizing these values is not sufficient authorization. Clearing every related |
| @@ -1655,8 +1787,67 @@ | ||
| 1655 | 1787 | $request_params['wpbc_time_override_start'] = ''; |
| 1656 | 1788 | $request_params['wpbc_time_override_end'] = ''; |
| 1657 | 1789 | |
| 1658 | 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; | |
| 1659 | 1850 | } |
| 1660 | 1851 | |
| 1661 | 1852 | |
| 1662 | 1853 | /** |