PluginProbe
Booking Calendar / 11.8.4
Booking Calendar v11.8.4
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 10.11 All 204 releases
← All changes | includes/_front_end/class-fe-booking-context.php +103 -25 11.711.8.4 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Signed context for Classic Booking Calendar shortcode AJAX requests.
3 + * Signed context for native Booking Form AJAX requests.
4 4 *
5 5 * @package Booking Calendar
6 6 */
7 7
@@ -9,8 +9,21 @@
9 9 exit;
10 10 }
11 11
12 12 /**
13 + * Return the current signed Booking Form context contract version.
14 + *
15 + * Version 2 adds a server-authored workflow identity. Rejecting older tokens
16 + * prevents indefinitely cached pre-fix Appointment or Resource Selector forms
17 + * from being replayed as unsigned Classic bookings.
18 + *
19 + * @return int Current context contract version.
20 + */
21 +function wpbc_classic_booking_context_get_version() {
22 + return 2;
23 +}
24 +
25 +/**
13 26 * Normalize one YYYY-MM-DD value and reject impossible calendar dates.
14 27 *
15 28 * @param mixed $date_value Candidate date value.
16 29 *
@@ -69,10 +82,52 @@
69 82 return '' === $custom_form ? 'standard' : $custom_form;
70 83 }
71 84
72 85 /**
73 - * Normalize Classic shortcode context before it is signed or consumed.
86 + * Normalize additional aggregate Booking Resource IDs for signed contexts.
74 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 +/**
128 + * Normalize the native Booking Form context before it is signed or consumed.
129 + *
75 130 * @param mixed $context Raw context values.
76 131 *
77 132 * @return array<string,mixed> Stable context contract.
78 133 */
@@ -80,8 +135,10 @@
80 135 $context = is_array( $context ) ? $context : array();
81 136 $context = wp_parse_args(
82 137 $context,
83 138 array(
139 + 'context_version' => 0,
140 + 'booking_workflow' => 'classic',
84 141 'resource_id' => 0,
85 142 'calendar_dates_start' => '',
86 143 'calendar_dates_end' => '',
87 144 'custom_form' => 'standard',
@@ -88,16 +145,21 @@
88 145 'aggregate_resource_ids' => array(),
89 146 'allow_past' => false,
90 147 )
91 148 );
92 - $calendar_dates_start = wpbc_classic_booking_context_normalize_date( $context['calendar_dates_start'] );
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'] );
153 + if ( ! in_array( $booking_workflow, array( 'classic', 'appointment', 'resource_selector' ), true ) ) {
154 + $booking_workflow = 'classic';
155 + }
93 156
94 - $aggregate_resource_ids = array_values( array_unique( array_filter( array_map( 'absint', (array) $context['aggregate_resource_ids'] ) ) ) );
95 - sort( $aggregate_resource_ids, SORT_NUMERIC );
96 -
97 157 // Derive permission from the site-authored date boundary; never trust a caller-supplied allow_past flag.
98 158 return array(
99 - 'resource_id' => absint( $context['resource_id'] ),
159 + 'context_version' => absint( $context['context_version'] ),
160 + 'booking_workflow' => $booking_workflow,
161 + 'resource_id' => $resource_id,
100 162 'calendar_dates_start' => $calendar_dates_start,
101 163 'calendar_dates_end' => wpbc_classic_booking_context_normalize_date( $context['calendar_dates_end'] ),
102 164 'custom_form' => wpbc_classic_booking_context_normalize_form( $context['custom_form'] ),
103 165 'aggregate_resource_ids' => $aggregate_resource_ids,
@@ -133,9 +195,9 @@
133 195 return base64_decode( $encoded_value, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
134 196 }
135 197
136 198 /**
137 - * Sign normalized Classic shortcode context for cache-safe AJAX round trips.
199 + * Sign a normalized native Booking Form context for cache-safe AJAX round trips.
138 200 *
139 201 * The HMAC has no time component, so cached front-end pages remain usable until
140 202 * WordPress authentication salts change. No secret or raw signature key is
141 203 * exposed to the browser.
@@ -141,17 +203,21 @@
141 203 * exposed to the browser.
142 204 *
143 205 * @param mixed $context Raw or normalized context.
144 206 *
145 - * @return string Signed opaque token, or an empty string for incomplete context.
207 + * @return string Signed opaque token, or an empty string for invalid context.
146 208 */
147 209 function wpbc_classic_booking_context_encode( $context ) {
210 + $context = is_array( $context ) ? $context : array();
211 + $context['context_version'] = wpbc_classic_booking_context_get_version();
148 212 $context = wpbc_classic_booking_context_normalize( $context );
149 213 if (
150 214 0 === $context['resource_id']
151 - || '' === $context['calendar_dates_start']
152 - || '' === $context['calendar_dates_end']
153 - || $context['calendar_dates_start'] > $context['calendar_dates_end']
215 + || ( ( '' === $context['calendar_dates_start'] ) !== ( '' === $context['calendar_dates_end'] ) )
216 + || (
217 + '' !== $context['calendar_dates_start']
218 + && $context['calendar_dates_start'] > $context['calendar_dates_end']
219 + )
154 220 ) {
155 221 return '';
156 222 }
157 223
@@ -161,9 +227,9 @@
161 227 return $payload . '.' . wpbc_classic_booking_context_base64url_encode( $signature );
162 228 }
163 229
164 230 /**
165 - * Verify and decode a signed Classic shortcode context token.
231 + * Verify and decode a signed native Booking Form context token.
166 232 *
167 233 * @param string $context_token Signed token received through AJAX.
168 234 *
169 235 * @return array<string,mixed>|WP_Error Normalized context or a safe validation error.
@@ -186,13 +252,18 @@
186 252 return new WP_Error( 'classic_booking_context_invalid', __( 'The booking form context could not be verified. Please reload the page and try again.', 'booking' ) );
187 253 }
188 254
189 255 $context = wpbc_classic_booking_context_normalize( $context );
256 + if ( wpbc_classic_booking_context_get_version() !== $context['context_version'] ) {
257 + return new WP_Error( 'classic_booking_context_expired', __( 'The booking form context has expired. Please reload the page and try again.', 'booking' ) );
258 + }
190 259 if (
191 260 0 === $context['resource_id']
192 - || '' === $context['calendar_dates_start']
193 - || '' === $context['calendar_dates_end']
194 - || $context['calendar_dates_start'] > $context['calendar_dates_end']
261 + || ( ( '' === $context['calendar_dates_start'] ) !== ( '' === $context['calendar_dates_end'] ) )
262 + || (
263 + '' !== $context['calendar_dates_start']
264 + && $context['calendar_dates_start'] > $context['calendar_dates_end']
265 + )
195 266 ) {
196 267 return new WP_Error( 'classic_booking_context_invalid', __( 'The booking form context could not be verified. Please reload the page and try again.', 'booking' ) );
197 268 }
198 269
@@ -199,11 +270,11 @@
199 270 return $context;
200 271 }
201 272
202 273 /**
203 - * Validate a Classic AJAX request against its signed shortcode boundaries.
274 + * Validate a Booking Form AJAX request against its signed server-rendered boundaries.
204 275 *
205 - * @param string $context_token Signed Classic context token.
276 + * @param string $context_token Signed Booking Form context token.
206 277 * @param mixed $resource_id Submitted primary Booking Resource ID.
207 278 * @param array|string $submitted_dates Submitted YYYY-MM-DD dates.
208 279 * @param string $custom_form Submitted Booking Form identifier.
209 280 * @param array|string $aggregate_resource_ids Submitted aggregate Booking Resource IDs.
@@ -224,15 +295,19 @@
224 295 if ( $custom_form !== $context['custom_form'] ) {
225 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' ) );
226 297 }
227 298
228 - if ( is_string( $aggregate_resource_ids ) ) {
229 - $aggregate_resource_ids = preg_split( '/[;,\s]+/', $aggregate_resource_ids, -1, PREG_SPLIT_NO_EMPTY );
230 - }
231 - $aggregate_resource_ids = array_values( array_unique( array_filter( array_map( 'absint', (array) $aggregate_resource_ids ) ) ) );
232 - sort( $aggregate_resource_ids, SORT_NUMERIC );
299 + $aggregate_resource_ids = wpbc_classic_booking_context_normalize_aggregate_resource_ids( $aggregate_resource_ids, $context['resource_id'] );
233 300 if ( $aggregate_resource_ids !== $context['aggregate_resource_ids'] ) {
234 - 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 );
235 310 }
236 311
237 312 if ( is_string( $submitted_dates ) ) {
238 313 $submitted_dates = preg_split( '/\s*,\s*/', $submitted_dates, -1, PREG_SPLIT_NO_EMPTY );
@@ -246,9 +321,12 @@
246 321 $submitted_date = wpbc_classic_booking_context_normalize_date( $submitted_date );
247 322 if ( '' === $submitted_date ) {
248 323 return new WP_Error( 'classic_booking_context_date_invalid', __( 'The selected booking date is invalid. Please select the date again.', 'booking' ) );
249 324 }
250 - if ( $submitted_date < $context['calendar_dates_start'] || $submitted_date > $context['calendar_dates_end'] ) {
325 + if (
326 + '' !== $context['calendar_dates_start']
327 + && ( $submitted_date < $context['calendar_dates_start'] || $submitted_date > $context['calendar_dates_end'] )
328 + ) {
251 329 return new WP_Error( 'classic_booking_context_date_outside_range', __( 'The selected booking date is outside this calendar range. Please select another date.', 'booking' ) );
252 330 }
253 331 }
254 332