| @@ -82,8 +82,50 @@ | ||
| 82 | 82 | return '' === $custom_form ? 'standard' : $custom_form; |
| 83 | 83 | } |
| 84 | 84 | |
| 85 | 85 | /** |
| 86 | + * Normalize additional aggregate Booking Resource IDs for signed contexts. | |
| 87 | + * | |
| 88 | + * The legacy shortcode renderer represents an aggregate form as the primary | |
| 89 | + * Resource followed by its additional Resources. Calendar runtime state | |
| 90 | + * intentionally stores only the additional Resources because the primary is | |
| 91 | + * already carried separately as resource_id. Removing that separately bound | |
| 92 | + * primary gives both established shapes one canonical representation while | |
| 93 | + * preserving an exact-set security comparison for every additional Resource. | |
| 94 | + * | |
| 95 | + * @param array|string|int $aggregate_resource_ids Candidate Resource IDs. | |
| 96 | + * @param mixed $primary_resource_id Separately bound primary Resource ID. | |
| 97 | + * | |
| 98 | + * @return int[] Sorted unique positive IDs excluding the primary Resource. | |
| 99 | + */ | |
| 100 | +function wpbc_classic_booking_context_normalize_aggregate_resource_ids( $aggregate_resource_ids, $primary_resource_id = 0 ) { | |
| 101 | + $aggregate_resource_ids = is_array( $aggregate_resource_ids ) ? $aggregate_resource_ids : array( $aggregate_resource_ids ); | |
| 102 | + $primary_resource_id = absint( $primary_resource_id ); | |
| 103 | + $normalized_resource_ids = array(); | |
| 104 | + | |
| 105 | + foreach ( $aggregate_resource_ids as $aggregate_resource_id ) { | |
| 106 | + if ( ! is_int( $aggregate_resource_id ) && ! is_float( $aggregate_resource_id ) && ! is_string( $aggregate_resource_id ) ) { | |
| 107 | + continue; | |
| 108 | + } | |
| 109 | + | |
| 110 | + $resource_id_parts = preg_split( '/[;,\s]+/', (string) $aggregate_resource_id, -1, PREG_SPLIT_NO_EMPTY ); | |
| 111 | + foreach ( (array) $resource_id_parts as $resource_id_part ) { | |
| 112 | + $resource_id = absint( $resource_id_part ); | |
| 113 | + if ( ! $resource_id || $resource_id === $primary_resource_id ) { | |
| 114 | + continue; | |
| 115 | + } | |
| 116 | + | |
| 117 | + $normalized_resource_ids[ $resource_id ] = $resource_id; | |
| 118 | + } | |
| 119 | + } | |
| 120 | + | |
| 121 | + $normalized_resource_ids = array_values( $normalized_resource_ids ); | |
| 122 | + sort( $normalized_resource_ids, SORT_NUMERIC ); | |
| 123 | + | |
| 124 | + return $normalized_resource_ids; | |
| 125 | +} | |
| 126 | + | |
| 127 | +/** | |
| 86 | 128 | * Normalize the native Booking Form context before it is signed or consumed. |
| 87 | 129 | * |
| 88 | 130 | * @param mixed $context Raw context values. |
| 89 | 131 | * |
| @@ -103,13 +145,12 @@ | ||
| 103 | 145 | 'aggregate_resource_ids' => array(), |
| 104 | 146 | 'allow_past' => false, |
| 105 | 147 | ) |
| 106 | 148 | ); |
| 107 | - $calendar_dates_start = wpbc_classic_booking_context_normalize_date( $context['calendar_dates_start'] ); | |
| 108 | - | |
| 109 | - $aggregate_resource_ids = array_values( array_unique( array_filter( array_map( 'absint', (array) $context['aggregate_resource_ids'] ) ) ) ); | |
| 110 | - sort( $aggregate_resource_ids, SORT_NUMERIC ); | |
| 111 | - $booking_workflow = sanitize_key( (string) $context['booking_workflow'] ); | |
| 149 | + $calendar_dates_start = wpbc_classic_booking_context_normalize_date( $context['calendar_dates_start'] ); | |
| 150 | + $resource_id = absint( $context['resource_id'] ); | |
| 151 | + $aggregate_resource_ids = wpbc_classic_booking_context_normalize_aggregate_resource_ids( $context['aggregate_resource_ids'], $resource_id ); | |
| 152 | + $booking_workflow = sanitize_key( (string) $context['booking_workflow'] ); | |
| 112 | 153 | if ( ! in_array( $booking_workflow, array( 'classic', 'appointment', 'resource_selector' ), true ) ) { |
| 113 | 154 | $booking_workflow = 'classic'; |
| 114 | 155 | } |
| 115 | 156 | |
| @@ -116,9 +157,9 @@ | ||
| 116 | 157 | // Derive permission from the site-authored date boundary; never trust a caller-supplied allow_past flag. |
| 117 | 158 | return array( |
| 118 | 159 | 'context_version' => absint( $context['context_version'] ), |
| 119 | 160 | 'booking_workflow' => $booking_workflow, |
| 120 | - 'resource_id' => absint( $context['resource_id'] ), | |
| 161 | + 'resource_id' => $resource_id, | |
| 121 | 162 | 'calendar_dates_start' => $calendar_dates_start, |
| 122 | 163 | 'calendar_dates_end' => wpbc_classic_booking_context_normalize_date( $context['calendar_dates_end'] ), |
| 123 | 164 | 'custom_form' => wpbc_classic_booking_context_normalize_form( $context['custom_form'] ), |
| 124 | 165 | 'aggregate_resource_ids' => $aggregate_resource_ids, |
| @@ -254,15 +295,19 @@ | ||
| 254 | 295 | if ( $custom_form !== $context['custom_form'] ) { |
| 255 | 296 | return new WP_Error( 'classic_booking_context_form_mismatch', __( 'The selected Booking Form does not match this calendar. Please reload the page and try again.', 'booking' ) ); |
| 256 | 297 | } |
| 257 | 298 | |
| 258 | - if ( is_string( $aggregate_resource_ids ) ) { | |
| 259 | - $aggregate_resource_ids = preg_split( '/[;,\s]+/', $aggregate_resource_ids, -1, PREG_SPLIT_NO_EMPTY ); | |
| 260 | - } | |
| 261 | - $aggregate_resource_ids = array_values( array_unique( array_filter( array_map( 'absint', (array) $aggregate_resource_ids ) ) ) ); | |
| 262 | - sort( $aggregate_resource_ids, SORT_NUMERIC ); | |
| 299 | + $aggregate_resource_ids = wpbc_classic_booking_context_normalize_aggregate_resource_ids( $aggregate_resource_ids, $context['resource_id'] ); | |
| 263 | 300 | if ( $aggregate_resource_ids !== $context['aggregate_resource_ids'] ) { |
| 264 | - return new WP_Error( 'classic_booking_context_aggregate_mismatch', __( 'The booking resources do not match this calendar. Please reload the page and try again.', 'booking' ) ); | |
| 301 | + $troubleshooting_url = 'https://wpbookingcalendar.com/faq/troubleshooting-the-booking-resources-do-not-match-this-calendar/'; | |
| 302 | + $aggregate_mismatch_message = esc_html__( 'The booking resources do not match this calendar. Please reload the page and try again.', 'booking' ); | |
| 303 | + $aggregate_mismatch_message .= sprintf( | |
| 304 | + '<br><a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>', | |
| 305 | + esc_url( $troubleshooting_url ), | |
| 306 | + esc_html__( 'Open the troubleshooting guide.', 'booking' ) | |
| 307 | + ); | |
| 308 | + | |
| 309 | + return new WP_Error( 'classic_booking_context_aggregate_mismatch', $aggregate_mismatch_message ); | |
| 265 | 310 | } |
| 266 | 311 | |
| 267 | 312 | if ( is_string( $submitted_dates ) ) { |
| 268 | 313 | $submitted_dates = preg_split( '/\s*,\s*/', $submitted_dates, -1, PREG_SPLIT_NO_EMPTY ); |