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/_front_end/class-fe-booking-context.php +98 -13 11.8 → 11.9 View file →
@@ -22,8 +22,45 @@
22 22 return 2;
23 23 }
24 24
25 25 /**
26 + * Resolve an actionable visitor message for a Booking Form context failure.
27 + *
28 + * The frontend message registry makes these notices translatable and editable
29 + * with the other Form Messages. The local defaults keep this security boundary
30 + * usable in isolated tests and integrations that load the context API before
31 + * the central message registry.
32 + *
33 + * @param string $message_key Stable frontend message key.
34 + * @param mixed $resource_id Booking Resource ID associated with the form.
35 + *
36 + * @return string Plain-text visitor message, or an empty string for an unsupported key.
37 + */
38 +function wpbc_classic_booking_context_get_visitor_message( $message_key, $resource_id = 0 ) {
39 + $resource_id = absint( $resource_id );
40 + $fallbacks = array(
41 + /* translators: Keep the {resource_id} placeholder unchanged. */
42 + 'message_booking_form_context_required' => __( 'This booking form is not connected to a valid calendar for Booking Resource ID {resource_id}. This can happen when the same Booking Resource is used more than once on the page, including in hidden content, or when the page cache is outdated. Reload the page and try again. If the problem continues, ask the site administrator to remove duplicate forms for this Booking Resource and clear the page cache.', 'booking' ),
43 + /* translators: Keep the {resource_id} placeholder unchanged. */
44 + 'message_booking_form_context_expired' => __( 'This booking form was generated by an older or cached version of the page for Booking Resource ID {resource_id}. Reload the page and try again. If the problem continues, ask the site administrator to clear the page cache and remove any duplicate calendar or form for this Booking Resource.', 'booking' ),
45 + );
46 +
47 + if ( ! isset( $fallbacks[ $message_key ] ) ) {
48 + return '';
49 + }
50 +
51 + $replacements = array( '{resource_id}' => (string) $resource_id );
52 + if ( function_exists( 'wpbc_frontend_messages__get' ) ) {
53 + $resolved_message = wpbc_frontend_messages__get( $message_key, $replacements, $resource_id );
54 + if ( '' !== $resolved_message ) {
55 + return $resolved_message;
56 + }
57 + }
58 +
59 + return strtr( $fallbacks[ $message_key ], $replacements );
60 +}
61 +
62 +/**
26 63 * Normalize one YYYY-MM-DD value and reject impossible calendar dates.
27 64 *
28 65 * @param mixed $date_value Candidate date value.
29 66 *
@@ -82,8 +119,50 @@
82 119 return '' === $custom_form ? 'standard' : $custom_form;
83 120 }
84 121
85 122 /**
123 + * Normalize additional aggregate Booking Resource IDs for signed contexts.
124 + *
125 + * The legacy shortcode renderer represents an aggregate form as the primary
126 + * Resource followed by its additional Resources. Calendar runtime state
127 + * intentionally stores only the additional Resources because the primary is
128 + * already carried separately as resource_id. Removing that separately bound
129 + * primary gives both established shapes one canonical representation while
130 + * preserving an exact-set security comparison for every additional Resource.
131 + *
132 + * @param array|string|int $aggregate_resource_ids Candidate Resource IDs.
133 + * @param mixed $primary_resource_id Separately bound primary Resource ID.
134 + *
135 + * @return int[] Sorted unique positive IDs excluding the primary Resource.
136 + */
137 +function wpbc_classic_booking_context_normalize_aggregate_resource_ids( $aggregate_resource_ids, $primary_resource_id = 0 ) {
138 + $aggregate_resource_ids = is_array( $aggregate_resource_ids ) ? $aggregate_resource_ids : array( $aggregate_resource_ids );
139 + $primary_resource_id = absint( $primary_resource_id );
140 + $normalized_resource_ids = array();
141 +
142 + foreach ( $aggregate_resource_ids as $aggregate_resource_id ) {
143 + if ( ! is_int( $aggregate_resource_id ) && ! is_float( $aggregate_resource_id ) && ! is_string( $aggregate_resource_id ) ) {
144 + continue;
145 + }
146 +
147 + $resource_id_parts = preg_split( '/[;,\s]+/', (string) $aggregate_resource_id, -1, PREG_SPLIT_NO_EMPTY );
148 + foreach ( (array) $resource_id_parts as $resource_id_part ) {
149 + $resource_id = absint( $resource_id_part );
150 + if ( ! $resource_id || $resource_id === $primary_resource_id ) {
151 + continue;
152 + }
153 +
154 + $normalized_resource_ids[ $resource_id ] = $resource_id;
155 + }
156 + }
157 +
158 + $normalized_resource_ids = array_values( $normalized_resource_ids );
159 + sort( $normalized_resource_ids, SORT_NUMERIC );
160 +
161 + return $normalized_resource_ids;
162 +}
163 +
164 +/**
86 165 * Normalize the native Booking Form context before it is signed or consumed.
87 166 *
88 167 * @param mixed $context Raw context values.
89 168 *
@@ -103,13 +182,12 @@
103 182 'aggregate_resource_ids' => array(),
104 183 'allow_past' => false,
105 184 )
106 185 );
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'] );
186 + $calendar_dates_start = wpbc_classic_booking_context_normalize_date( $context['calendar_dates_start'] );
187 + $resource_id = absint( $context['resource_id'] );
188 + $aggregate_resource_ids = wpbc_classic_booking_context_normalize_aggregate_resource_ids( $context['aggregate_resource_ids'], $resource_id );
189 + $booking_workflow = sanitize_key( (string) $context['booking_workflow'] );
112 190 if ( ! in_array( $booking_workflow, array( 'classic', 'appointment', 'resource_selector' ), true ) ) {
113 191 $booking_workflow = 'classic';
114 192 }
115 193
@@ -116,9 +194,9 @@
116 194 // Derive permission from the site-authored date boundary; never trust a caller-supplied allow_past flag.
117 195 return array(
118 196 'context_version' => absint( $context['context_version'] ),
119 197 'booking_workflow' => $booking_workflow,
120 - 'resource_id' => absint( $context['resource_id'] ),
198 + 'resource_id' => $resource_id,
121 199 'calendar_dates_start' => $calendar_dates_start,
122 200 'calendar_dates_end' => wpbc_classic_booking_context_normalize_date( $context['calendar_dates_end'] ),
123 201 'custom_form' => wpbc_classic_booking_context_normalize_form( $context['custom_form'] ),
124 202 'aggregate_resource_ids' => $aggregate_resource_ids,
@@ -212,9 +290,12 @@
212 290 }
213 291
214 292 $context = wpbc_classic_booking_context_normalize( $context );
215 293 if ( wpbc_classic_booking_context_get_version() !== $context['context_version'] ) {
216 - return new WP_Error( 'classic_booking_context_expired', __( 'The booking form context has expired. Please reload the page and try again.', 'booking' ) );
294 + return new WP_Error(
295 + 'classic_booking_context_expired',
296 + wpbc_classic_booking_context_get_visitor_message( 'message_booking_form_context_expired', $context['resource_id'] )
297 + );
217 298 }
218 299 if (
219 300 0 === $context['resource_id']
220 301 || ( ( '' === $context['calendar_dates_start'] ) !== ( '' === $context['calendar_dates_end'] ) )
@@ -254,15 +335,19 @@
254 335 if ( $custom_form !== $context['custom_form'] ) {
255 336 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 337 }
257 338
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 );
339 + $aggregate_resource_ids = wpbc_classic_booking_context_normalize_aggregate_resource_ids( $aggregate_resource_ids, $context['resource_id'] );
263 340 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' ) );
341 + $troubleshooting_url = 'https://wpbookingcalendar.com/faq/troubleshooting-the-booking-resources-do-not-match-this-calendar/';
342 + $aggregate_mismatch_message = esc_html__( 'The booking resources do not match this calendar. Please reload the page and try again.', 'booking' );
343 + $aggregate_mismatch_message .= sprintf(
344 + '<br><a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>',
345 + esc_url( $troubleshooting_url ),
346 + esc_html__( 'Open the troubleshooting guide.', 'booking' )
347 + );
348 +
349 + return new WP_Error( 'classic_booking_context_aggregate_mismatch', $aggregate_mismatch_message );
265 350 }
266 351
267 352 if ( is_string( $submitted_dates ) ) {
268 353 $submitted_dates = preg_split( '/\s*,\s*/', $submitted_dates, -1, PREG_SPLIT_NO_EMPTY );