PluginProbe
Booking Calendar / 11.7
Booking Calendar v11.7
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
booking / includes / booking-resource-selector / booking-resource-selector__config.php

booking-resource-selector__config.php in Booking Calendar 11.7, at includes/booking-resource-selector/booking-resource-selector__config.php

362 lines 15.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Booking Resource selector configuration normalization and signing.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Convert a delimited value or array to unique positive Booking Resource IDs.
14 *
15 * @param mixed $resource_ids Raw Booking Resource ID collection.
16 *
17 * @return int[] Normalized Booking Resource IDs.
18 */
19 function wpbc_booking_resource_selector_normalize_ids( $resource_ids ) {
20 if ( is_string( $resource_ids ) ) {
21 $resource_ids = preg_split( '/[;,\s]+/', $resource_ids, -1, PREG_SPLIT_NO_EMPTY );
22 }
23
24 return array_values( array_unique( array_filter( array_map( 'absint', (array) $resource_ids ) ) ) );
25 }
26
27 /**
28 * Convert a shortcode-style value to a strict Boolean.
29 *
30 * @param mixed $raw_value Raw Boolean-like value.
31 * @param bool $default_value Value used when the raw value is null.
32 *
33 * @return bool Normalized Boolean.
34 */
35 function wpbc_booking_resource_selector_normalize_boolean( $raw_value, $default_value = false ) {
36 if ( null === $raw_value ) {
37 return (bool) $default_value;
38 }
39
40 if ( is_string( $raw_value ) ) {
41 $raw_value = strtolower( trim( $raw_value ) );
42 }
43
44 return ! in_array( $raw_value, array( false, 0, '0', 'false', 'off', 'no', '' ), true );
45 }
46
47 /**
48 * Normalize a safe public catalog item width.
49 *
50 * Bare numbers are treated as pixels for shortcode convenience. Only simple
51 * dimensions are accepted; CSS functions and arbitrary declarations are
52 * rejected before the value can reach an inline custom property.
53 *
54 * @param mixed $raw_width Raw shortcode width.
55 *
56 * @return string Normalized CSS width or an empty string for automatic width.
57 */
58 function wpbc_booking_resource_selector_normalize_css_width( $raw_width ) {
59 if ( is_int( $raw_width ) || is_float( $raw_width ) ) {
60 $raw_width = (string) $raw_width . 'px';
61 }
62
63 $raw_width = strtolower( trim( (string) $raw_width ) );
64 if ( '' === $raw_width || 'auto' === $raw_width ) {
65 return '';
66 }
67 if ( preg_match( '/^\d+(?:\.\d+)?$/', $raw_width ) ) {
68 $raw_width .= 'px';
69 }
70 if ( ! preg_match( '/^(\d+(?:\.\d+)?)(px|%|rem|em|vw)$/', $raw_width, $matches ) ) {
71 return '';
72 }
73
74 $numeric_width = (float) $matches[1];
75 $width_unit = $matches[2];
76 $maximum_width = in_array( $width_unit, array( '%', 'vw' ), true ) ? 100 : ( 'px' === $width_unit ? 2000 : 100 );
77 if ( $numeric_width <= 0 || $numeric_width > $maximum_width ) {
78 return '';
79 }
80
81 $normalized_width = rtrim( rtrim( number_format( $numeric_width, 4, '.', '' ), '0' ), '.' );
82
83 return $normalized_width . $width_unit;
84 }
85
86 /**
87 * Return the Booking Resource that should be checked on the selection screen.
88 *
89 * The public `resource_id` attribute is the primary default-selection
90 * parameter. `selected_resource_id` remains as a compatibility fallback for
91 * shortcodes created before `resource_id` adopted that behavior.
92 *
93 * @param array<string,mixed> $config Normalized selector configuration.
94 *
95 * @return int Default Booking Resource ID or zero.
96 */
97 function wpbc_booking_resource_selector_get_default_resource_id( $config ) {
98 if ( ! empty( $config['resource_id'] ) ) {
99 return absint( $config['resource_id'] );
100 }
101
102 return ! empty( $config['selected_resource_id'] ) ? absint( $config['selected_resource_id'] ) : 0;
103 }
104
105 /**
106 * Normalize public shortcode attributes into the signed AJAX contract.
107 *
108 * The legacy-compatible aliases are accepted only at this boundary. AJAX and
109 * submission requests carry one stable normalized representation.
110 *
111 * @param mixed $attributes Raw shortcode attributes or decoded configuration.
112 *
113 * @return array<string,mixed> Safe Booking Resource selector configuration.
114 */
115 function wpbc_booking_resource_selector_normalize_config( $attributes ) {
116 $attributes = is_array( $attributes ) ? $attributes : array();
117 $defaults = array(
118 'resource_id' => 0,
119 'selected_resource_id' => 0,
120 'resource_ids' => array(),
121 'aggregate_resource_ids' => array(),
122 'cal_count' => 1,
123 'start_month_calendar' => false,
124 'calendar_dates_start' => '',
125 'calendar_dates_end' => '',
126 'selected_dates' => '',
127 'options' => '',
128 'form_type' => '',
129 'auto_select_resource' => false,
130 'catalog_layout' => 'grid',
131 'show_resource_filters' => false,
132 'show_resource_image' => true,
133 'show_resource_title' => true,
134 'show_resource_description' => true,
135 'catalog_item_width' => '',
136 'catalog_item_max_width' => 0,
137 'catalog_grid_items_per_row' => 0,
138 'catalog_list_items_per_row' => 0,
139 'show_resource_hierarchy' => true,
140 'show_availability' => true,
141 'show_starting_price' => true,
142 'show_progress' => true,
143 'progress_item_1_title' => null,
144 'progress_item_1_number' => null,
145 'progress_item_2_title' => null,
146 'progress_item_2_number' => null,
147 'screen_1_title' => null,
148 'screen_1_description' => null,
149 'allow_past' => false,
150 'return_url' => '',
151 );
152
153 $attribute_aliases = array(
154 'resources' => 'resource_ids',
155 'type' => 'resource_ids',
156 'aggregate' => 'aggregate_resource_ids',
157 'nummonths' => 'cal_count',
158 'startmonth' => 'start_month_calendar',
159 'selected_type' => 'selected_resource_id',
160 'label' => 'screen_1_title',
161 );
162 foreach ( $attribute_aliases as $public_attribute => $normalized_attribute ) {
163 if ( array_key_exists( $public_attribute, $attributes ) && ! array_key_exists( $normalized_attribute, $attributes ) ) {
164 $attributes[ $normalized_attribute ] = $attributes[ $public_attribute ];
165 }
166 unset( $attributes[ $public_attribute ] );
167 }
168
169 $attributes = array_intersect_key( $attributes, $defaults );
170 $config = wp_parse_args( $attributes, $defaults );
171 $config['resource_id'] = absint( $config['resource_id'] );
172 $config['selected_resource_id'] = absint( $config['selected_resource_id'] );
173 $config['resource_ids'] = wpbc_booking_resource_selector_normalize_ids( $config['resource_ids'] );
174 $config['aggregate_resource_ids'] = wpbc_booking_resource_selector_normalize_ids( $config['aggregate_resource_ids'] );
175 $config['cal_count'] = min( 24, max( 1, absint( $config['cal_count'] ) ) );
176
177 $start_month = $config['start_month_calendar'];
178 if ( is_array( $start_month ) ) {
179 $year = isset( $start_month[0] ) ? absint( $start_month[0] ) : 0;
180 $month = isset( $start_month[1] ) ? absint( $start_month[1] ) : 0;
181 $start_month = ( $year && $month >= 1 && $month <= 12 ) ? array( $year, $month ) : false;
182 } elseif ( is_string( $start_month ) && preg_match( '/^(\d{4})[-\/]?(\d{1,2})$/', $start_month, $matches ) ) {
183 $month = absint( $matches[2] );
184 $start_month = ( $month >= 1 && $month <= 12 ) ? array( absint( $matches[1] ), $month ) : false;
185 } else {
186 $start_month = false;
187 }
188 $config['start_month_calendar'] = $start_month;
189
190 foreach ( array( 'calendar_dates_start', 'calendar_dates_end' ) as $date_key ) {
191 $date_value = sanitize_text_field( (string) $config[ $date_key ] );
192 $config[ $date_key ] = preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_value ) ? $date_value : '';
193 }
194
195 $config['selected_dates'] = sanitize_text_field( (string) $config['selected_dates'] );
196 $config['options'] = sanitize_text_field( (string) $config['options'] );
197 $config['form_type'] = sanitize_text_field( (string) $config['form_type'] );
198 $config['return_url'] = esc_url_raw( (string) $config['return_url'] );
199 $config['auto_select_resource'] = wpbc_booking_resource_selector_normalize_boolean( $config['auto_select_resource'] );
200 $config['catalog_layout'] = 'list' === sanitize_key( (string) $config['catalog_layout'] ) ? 'list' : 'grid';
201 $config['show_resource_filters'] = wpbc_booking_resource_selector_normalize_boolean( $config['show_resource_filters'] );
202 $config['show_resource_image'] = wpbc_booking_resource_selector_normalize_boolean( $config['show_resource_image'], true );
203 $config['show_resource_title'] = wpbc_booking_resource_selector_normalize_boolean( $config['show_resource_title'], true );
204 $config['show_resource_description'] = wpbc_booking_resource_selector_normalize_boolean( $config['show_resource_description'], true );
205 $config['catalog_item_width'] = wpbc_booking_resource_selector_normalize_css_width( $config['catalog_item_width'] );
206 $config['catalog_item_max_width'] = absint( $config['catalog_item_max_width'] );
207 if ( $config['catalog_item_max_width'] > 0 ) {
208 $config['catalog_item_max_width'] = min( 1200, max( 280, $config['catalog_item_max_width'] ) );
209 }
210 $config['catalog_grid_items_per_row'] = min( 12, absint( $config['catalog_grid_items_per_row'] ) );
211 $config['catalog_list_items_per_row'] = min( 12, absint( $config['catalog_list_items_per_row'] ) );
212 $config['show_resource_hierarchy'] = wpbc_booking_resource_selector_normalize_boolean( $config['show_resource_hierarchy'], true );
213 $config['show_availability'] = wpbc_booking_resource_selector_normalize_boolean( $config['show_availability'], true );
214 $config['show_starting_price'] = wpbc_booking_resource_selector_normalize_boolean( $config['show_starting_price'], true );
215 $config['show_progress'] = wpbc_booking_resource_selector_normalize_boolean( $config['show_progress'], true );
216 $config['allow_past'] = wpbc_booking_resource_selector_normalize_boolean( $config['allow_past'] );
217
218 $display_text_keys = array(
219 'progress_item_1_title',
220 'progress_item_1_number',
221 'progress_item_2_title',
222 'progress_item_2_number',
223 'screen_1_title',
224 'screen_1_description',
225 );
226 foreach ( $display_text_keys as $display_text_key ) {
227 if ( null !== $config[ $display_text_key ] ) {
228 $config[ $display_text_key ] = sanitize_text_field( (string) $config[ $display_text_key ] );
229 }
230 }
231
232 return (array) apply_filters( 'wpbc_booking_resource_selector_normalized_config', $config, $attributes );
233 }
234
235 /**
236 * Check whether signed selector configuration enables past bookings.
237 *
238 * @param array<string,mixed> $config Normalized or decoded configuration.
239 *
240 * @return bool True when the signed shortcode explicitly enables past bookings.
241 */
242 function wpbc_booking_resource_selector_is_past_booking_enabled( $config ) {
243 return ! empty( $config['allow_past'] );
244 }
245
246 /**
247 * Base64-url encode a binary or text value without padding.
248 *
249 * @param string $raw_value Value to encode.
250 *
251 * @return string URL-safe encoded value.
252 */
253 function wpbc_booking_resource_selector_base64url_encode( $raw_value ) {
254 return rtrim( strtr( base64_encode( (string) $raw_value ), '+/', '-_' ), '=' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
255 }
256
257 /**
258 * Decode a base64-url value with strict validation.
259 *
260 * @param string $encoded_value Encoded value.
261 *
262 * @return string|false Decoded value or false.
263 */
264 function wpbc_booking_resource_selector_base64url_decode( $encoded_value ) {
265 $encoded_value = strtr( (string) $encoded_value, '-_', '+/' );
266 $padding = strlen( $encoded_value ) % 4;
267 if ( $padding ) {
268 $encoded_value .= str_repeat( '=', 4 - $padding );
269 }
270
271 return base64_decode( $encoded_value, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
272 }
273
274 /**
275 * Sign normalized shortcode configuration for public AJAX round trips.
276 *
277 * @param array<string,mixed> $config Normalized configuration.
278 *
279 * @return string Signed opaque configuration token.
280 */
281 function wpbc_booking_resource_selector_encode_config( $config ) {
282 $payload = wpbc_booking_resource_selector_base64url_encode( wp_json_encode( wpbc_booking_resource_selector_normalize_config( $config ) ) );
283 $signature = hash_hmac( 'sha256', $payload, wp_salt( 'auth' ), true );
284
285 return $payload . '.' . wpbc_booking_resource_selector_base64url_encode( $signature );
286 }
287
288 /**
289 * Verify and decode a public AJAX configuration token.
290 *
291 * @param string $config_token Signed token.
292 *
293 * @return array<string,mixed>|WP_Error Normalized configuration or validation error.
294 */
295 function wpbc_booking_resource_selector_decode_config( $config_token ) {
296 $parts = explode( '.', (string) $config_token, 2 );
297 if ( 2 !== count( $parts ) ) {
298 return new WP_Error( 'resource_selector_config_invalid', __( 'The Booking Resource selection configuration is invalid. Reload the page and try again.', 'booking' ) );
299 }
300
301 $expected_signature = hash_hmac( 'sha256', $parts[0], wp_salt( 'auth' ), true );
302 $actual_signature = wpbc_booking_resource_selector_base64url_decode( $parts[1] );
303 if ( false === $actual_signature || ! hash_equals( $expected_signature, $actual_signature ) ) {
304 return new WP_Error( 'resource_selector_config_invalid', __( 'The Booking Resource selection configuration is invalid. Reload the page and try again.', 'booking' ) );
305 }
306
307 $json_data = wpbc_booking_resource_selector_base64url_decode( $parts[0] );
308 $config_data = false !== $json_data ? json_decode( $json_data, true ) : null;
309 if ( ! is_array( $config_data ) ) {
310 return new WP_Error( 'resource_selector_config_invalid', __( 'The Booking Resource selection configuration is invalid. Reload the page and try again.', 'booking' ) );
311 }
312
313 return wpbc_booking_resource_selector_normalize_config( $config_data );
314 }
315
316 /**
317 * Sign one server-validated Booking Resource for final booking submission.
318 *
319 * @param array<string,mixed> $config Original normalized configuration.
320 * @param int $resource_id Selected Booking Resource ID.
321 *
322 * @return string Signed selection token, or an empty string for an invalid ID.
323 */
324 function wpbc_booking_resource_selector_encode_submission_context( $config, $resource_id ) {
325 $resource_id = absint( $resource_id );
326 if ( ! $resource_id ) {
327 return '';
328 }
329
330 $context = wpbc_booking_resource_selector_normalize_config( $config );
331 $context['resource_id'] = $resource_id;
332 $context['resource_ids'] = array( $resource_id );
333 $context['selected_resource_id'] = 0;
334
335 return wpbc_booking_resource_selector_encode_config( $context );
336 }
337
338 /**
339 * Verify that a signed selector submission context matches a resource.
340 *
341 * @param string $context_token Signed selector context token.
342 * @param int $resource_id Submitted Booking Resource ID.
343 *
344 * @return array<string,mixed>|WP_Error Verified context or controlled error.
345 */
346 function wpbc_booking_resource_selector_validate_submission_context( $context_token, $resource_id ) {
347 $resource_id = absint( $resource_id );
348 if ( '' === trim( (string) $context_token ) ) {
349 return new WP_Error( 'resource_selector_context_required', __( 'The Booking Resource selection has expired. Please start over and try again.', 'booking' ) );
350 }
351
352 $context = wpbc_booking_resource_selector_decode_config( $context_token );
353 if ( is_wp_error( $context ) ) {
354 return new WP_Error( 'resource_selector_context_invalid', __( 'The Booking Resource selection is invalid. Please start over and try again.', 'booking' ) );
355 }
356 if ( absint( $context['resource_id'] ) !== $resource_id ) {
357 return new WP_Error( 'resource_selector_context_mismatch', __( 'The selected Booking Resource does not match this booking form. Please start over and try again.', 'booking' ) );
358 }
359
360 return $context;
361 }
362