| @@ -1,4 +1,4 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | 3 | * @version 1.0 |
| 4 | 4 | * @package Booking Calendar |
| @@ -135,10 +135,40 @@ | ||
| 135 | 135 | return $time_m_h . ':' . $time_m_m; |
| 136 | 136 | } |
| 137 | 137 | |
| 138 | 138 | |
| 139 | -/** | |
| 140 | - * Get Times from booking Form, if these times fields exist | |
| 139 | +/** | |
| 140 | + * Parse a duration field value into numeric hours and minutes. | |
| 141 | + * | |
| 142 | + * Duration values come from serialized front-end form data. Placeholder or | |
| 143 | + * otherwise malformed values must not reach arithmetic because PHP 8 throws | |
| 144 | + * a TypeError when a non-numeric string is multiplied by an integer. | |
| 145 | + * | |
| 146 | + * @param string $duration_time_value Duration in `HH:MM` format. | |
| 147 | + * | |
| 148 | + * @return array|false Numeric hours and minutes, or false for an invalid duration. | |
| 149 | + */ | |
| 150 | +function wpbc_parse_duration_time_value( $duration_time_value ) { | |
| 151 | + | |
| 152 | + $duration_time_value = trim( (string) $duration_time_value ); | |
| 153 | + | |
| 154 | + if ( ! preg_match( '/^([0-9]+):([0-9]{1,2})$/', $duration_time_value, $duration_time_matches ) ) { | |
| 155 | + return false; | |
| 156 | + } | |
| 157 | + | |
| 158 | + $duration_hours = intval( $duration_time_matches[1] ); | |
| 159 | + $duration_minutes = intval( $duration_time_matches[2] ); | |
| 160 | + | |
| 161 | + if ( 59 < $duration_minutes ) { | |
| 162 | + return false; | |
| 163 | + } | |
| 164 | + | |
| 165 | + return array( $duration_hours, $duration_minutes ); | |
| 166 | +} | |
| 167 | + | |
| 168 | + | |
| 169 | +/** | |
| 170 | + * Get Times from booking Form, if these times fields exist | |
| 141 | 171 | * |
| 142 | 172 | * @param type $booking_form_data |
| 143 | 173 | * @param type $booking_type |
| 144 | 174 | * @return mixed |
| @@ -233,34 +263,36 @@ | ||
| 233 | 263 | } else { |
| 234 | 264 | $end_time = explode( ':', $end_time ); |
| 235 | 265 | } |
| 236 | 266 | |
| 237 | - if ( strpos( $booking_form_data, 'durationtime' . $booking_type ) !== false ) { // Get END TIME From form request | |
| 238 | - $pos1 = strpos( $booking_form_data, 'durationtime' . $booking_type ); // Find start time pos | |
| 239 | - $pos1 = strpos( $booking_form_data, '^', $pos1 ) + 1; // Find TIME pos | |
| 240 | - $pos2 = strpos( $booking_form_data, '~', $pos1 ); // Find TIME length | |
| 267 | + if ( strpos( $booking_form_data, 'durationtime' . $booking_type ) !== false ) { // Get END TIME From form request | |
| 268 | + $pos1 = strpos( $booking_form_data, 'durationtime' . $booking_type ); // Find start time pos | |
| 269 | + $pos1 = strpos( $booking_form_data, '^', $pos1 ) + 1; // Find TIME pos | |
| 270 | + $pos2 = strpos( $booking_form_data, '~', $pos1 ); // Find TIME length | |
| 241 | 271 | if ( $pos2 === false ) { |
| 242 | 272 | $pos2 = strlen( $booking_form_data ); |
| 243 | 273 | } |
| 244 | - $pos2 = $pos2 - $pos1; | |
| 245 | - $end_time = substr( $booking_form_data, $pos1, $pos2 ); | |
| 274 | + $pos2 = $pos2 - $pos1; | |
| 275 | + $duration_time_value = substr( $booking_form_data, $pos1, $pos2 ); | |
| 276 | + $duration_time_parts = wpbc_parse_duration_time_value( $duration_time_value ); | |
| 277 | + | |
| 278 | + if ( false !== $duration_time_parts ) { | |
| 279 | + $is_time_exist = true; | |
| 280 | + | |
| 281 | + // Get the selected start time and add the validated duration to calculate the end time. | |
| 282 | + $new_end_time = mktime( intval( $start_time[0] ), intval( $start_time[1] ) ); | |
| 283 | + $new_end_time += $duration_time_parts[0] * 60 * 60; | |
| 284 | + $new_end_time += $duration_time_parts[1] * 60; | |
| 285 | + $end_time = gmdate( 'H:i', $new_end_time ); | |
| 286 | + | |
| 287 | + if ( '00:00' === $end_time ) { | |
| 288 | + $end_time = '23:59'; | |
| 289 | + } | |
| 290 | + $end_time = explode( ':', $end_time ); | |
| 291 | + $end_time[2] = '02'; | |
| 292 | + } | |
| 293 | + } | |
| 246 | 294 | |
| 247 | - $is_time_exist = true; | |
| 248 | - | |
| 249 | - $end_time = explode( ':', $end_time ); | |
| 250 | - | |
| 251 | - // Here we are get start time and add duration for end time | |
| 252 | - $new_end_time = mktime( intval( $start_time[0] ), intval( $start_time[1] ) ); | |
| 253 | - $new_end_time = $new_end_time + $end_time[0] * 60 * 60 + $end_time[1] * 60; | |
| 254 | - $end_time = gmdate( 'H:i', $new_end_time ); | |
| 255 | - | |
| 256 | - if ( $end_time == '00:00' ) { | |
| 257 | - $end_time = '23:59'; | |
| 258 | - } | |
| 259 | - $end_time = explode( ':', $end_time ); | |
| 260 | - $end_time[2] = '02'; | |
| 261 | - } | |
| 262 | - | |
| 263 | 295 | } |
| 264 | 296 | |
| 265 | 297 | if ( $is_time_exist ) { |
| 266 | 298 | return array( $start_time, $end_time ); |
| @@ -318,9 +350,9 @@ | ||
| 318 | 350 | function wpbc_db__get_sql_dates__in_booking__as_str( $booking_id_str ) { |
| 319 | 351 | |
| 320 | 352 | global $wpdb; |
| 321 | 353 | |
| 322 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 354 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 323 | 355 | $dates_result = $wpdb->get_results( "SELECT DISTINCT booking_date FROM {$wpdb->prefix}bookingdates WHERE booking_id IN ({$booking_id_str}) ORDER BY booking_date" ); |
| 324 | 356 | |
| 325 | 357 | $dates_str = array(); |
| 326 | 358 | |
| @@ -651,9 +683,9 @@ | ||
| 651 | 683 | * |
| 652 | 684 | * @param string $some_day : '2015-05-29' |
| 653 | 685 | * @return boolean : true | false |
| 654 | 686 | */ |
| 655 | -function wpbc_is_date_in_past( $some_day ) { | |
| 687 | +function wpbc_is_date_in_past( $some_day ) { | |
| 656 | 688 | |
| 657 | 689 | $some_day_d = gmdate( 'm.d.Y', mysql2date( 'U', $some_day ) ); |
| 658 | 690 | $some_array = explode( '.', $some_day_d ); |
| 659 | 691 | $some_day = mktime( 0, 0, 0, intval($some_array[0]), ( intval($some_array[1]) + 1 ), intval($some_array[2]) ); |
| @@ -664,12 +696,74 @@ | ||
| 664 | 696 | return true; |
| 665 | 697 | } else { |
| 666 | 698 | return false; |
| 667 | 699 | } |
| 700 | +} | |
| 701 | + | |
| 702 | + | |
| 703 | +/** | |
| 704 | + * Check whether a booking can still be changed by a visitor. | |
| 705 | + * | |
| 706 | + * Visitor edit and cancellation actions must fail closed when the booking does | |
| 707 | + * not exist, is in the trash, has no dates, or its final booked date has | |
| 708 | + * already passed. The comparison deliberately uses wpbc_is_date_in_past() so | |
| 709 | + * the rule remains compatible with the established date-based front-end edit | |
| 710 | + * restriction and does not unexpectedly expire time-slot bookings mid-day. | |
| 711 | + * | |
| 712 | + * @param int $booking_id Booking ID resolved from a verified visitor hash. | |
| 713 | + * | |
| 714 | + * @return bool True when at least one booked date is current or future. | |
| 715 | + */ | |
| 716 | +function wpbc_is_visitor_booking_action_allowed( $booking_id ) { | |
| 717 | + global $wpdb; | |
| 718 | + | |
| 719 | + $booking_id = absint( $booking_id ); | |
| 720 | + if ( empty( $booking_id ) ) { | |
| 721 | + return false; | |
| 722 | + } | |
| 723 | + | |
| 724 | + $last_booking_date = $wpdb->get_var( | |
| 725 | + $wpdb->prepare( | |
| 726 | + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 727 | + "SELECT MAX(dt.booking_date) | |
| 728 | + FROM {$wpdb->prefix}bookingdates AS dt | |
| 729 | + INNER JOIN {$wpdb->prefix}booking AS bk ON bk.booking_id = dt.booking_id | |
| 730 | + WHERE bk.booking_id = %d AND bk.trash = 0", | |
| 731 | + $booking_id | |
| 732 | + ) | |
| 733 | + ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching | |
| 734 | + | |
| 735 | + if ( empty( $last_booking_date ) || ! is_string( $last_booking_date ) ) { | |
| 736 | + return false; | |
| 737 | + } | |
| 738 | + | |
| 739 | + return ! wpbc_is_date_in_past( $last_booking_date ); | |
| 740 | +} | |
| 741 | + | |
| 742 | + | |
| 743 | +/** | |
| 744 | + * Check if the booking was made without calendar ( no booking date submited.) | |
| 745 | + * | |
| 746 | + * @param $is_no_date | |
| 747 | + * @param $date | |
| 748 | + * | |
| 749 | + * @return mixed|true | |
| 750 | + */ | |
| 751 | +function wpbc_maybe_no_booking_date( $is_no_date, $date_ymd ) { | |
| 752 | + | |
| 753 | + if ( ( ! empty( $date_ymd ) ) && ( is_array( $date_ymd ) ) ) { | |
| 754 | + $date_ymd = $date_ymd[0]; | |
| 755 | + } | |
| 756 | + // FixIn: 2981-01-13 13 Jan 2981. | |
| 757 | + if ( '2981-01-13' === gmdate( 'Y-m-d', mysql2date( 'U', $date_ymd ) ) ) { | |
| 758 | + $is_no_date = true; | |
| 759 | + } | |
| 760 | + | |
| 761 | + return $is_no_date; | |
| 668 | 762 | } |
| 763 | +add_filter( 'wpbc_maybe_no_booking_date', 'wpbc_maybe_no_booking_date', 10, 2 ); | |
| 669 | 764 | |
| 670 | 765 | |
| 671 | - | |
| 672 | 766 | //TODO: refactor it, by replacig date_i18n to wp_loc_date... (check depndencies of this function in other usage functions...) |
| 673 | 767 | /** |
| 674 | 768 | * Change date / time format |
| 675 | 769 | * |
| @@ -679,8 +773,13 @@ | ||
| 679 | 773 | * @return array( 'DATE in custom Format', 'TIME in custom Format' ) |
| 680 | 774 | */ |
| 681 | 775 | function wpbc_get_date_in_correct_format( $dt, $date_format = false, $time_format = false ) { |
| 682 | 776 | |
| 777 | + $is_no_date = apply_filters( 'wpbc_maybe_no_booking_date', false, $dt ); | |
| 778 | + if ( $is_no_date ) { | |
| 779 | + return array( '---', '' ); | |
| 780 | + } | |
| 781 | + | |
| 683 | 782 | if ( $date_format === false ) $date_format = get_bk_option( 'booking_date_format'); |
| 684 | 783 | if ( empty( $date_format ) ) $date_format = "m / d / Y, D"; |
| 685 | 784 | |
| 686 | 785 | if ( $time_format === false ) $time_format = get_bk_option( 'booking_time_format'); |
| @@ -806,9 +905,9 @@ | ||
| 806 | 905 | |
| 807 | 906 | // W H E R E |
| 808 | 907 | $sql_where = ''; |
| 809 | 908 | $sql_where .= ( '' != $params['approved'] ) ? " AND ( dt.approved = {$params['approved']} ) " : ''; // Approved (1) or Pending (0) or All // int |
| 810 | - $sql_where .= " AND dt.booking_date >= CURDATE() "; // Only actual bookings | |
| 909 | + $sql_where .= " AND dt.booking_date >= " . wpbc_sql_date_math_expr_explicit('', 'curdate') . " "; // Only actual bookings | |
| 811 | 910 | $sql_where .= " AND bk.trash != 1 "; // Not in Trash // int |
| 812 | 911 | $sql_where .= " AND bk.booking_type IN ( {$params['resource_id']} ) "; // For specific calendar (booking resource) // int |
| 813 | 912 | $sql_where .= ( '' != $params['skip_booking_id'] ) ? " AND dt.booking_id NOT IN ( {$params['skip_booking_id']} ) " : '' ; // Skip some bookings ? Usually, during booking edit. |
| 814 | 913 | |
| @@ -821,9 +920,9 @@ | ||
| 821 | 920 | [1] => stdClass Object ( [booking_date] => 2022-12-28 00:00:00, [approved] => 1, [booking_id] => 26 ) |
| 822 | 921 | ... |
| 823 | 922 | */ |
| 824 | 923 | |
| 825 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 924 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 826 | 925 | $result_arr = $wpdb->get_results( $sql . $sql_where . $sql_order ); |
| 827 | 926 | |
| 828 | 927 | // P A R S E |
| 829 | 928 | $prior_check_out_date = false; |
| @@ -982,5 +1081,5 @@ | ||
| 982 | 1081 | |
| 983 | 1082 | } |
| 984 | 1083 | |
| 985 | 1084 | return $days_availability; |
| 986 | - } | |
| 1085 | + } | |