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

296 lines 11.7 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 * Return the Booking Resource that should be checked on the selection screen.
49 *
50 * The public `resource_id` attribute is the primary default-selection
51 * parameter. `selected_resource_id` remains as a compatibility fallback for
52 * shortcodes created before `resource_id` adopted that behavior.
53 *
54 * @param array<string,mixed> $config Normalized selector configuration.
55 *
56 * @return int Default Booking Resource ID or zero.
57 */
58 function wpbc_booking_resource_selector_get_default_resource_id( $config ) {
59 if ( ! empty( $config['resource_id'] ) ) {
60 return absint( $config['resource_id'] );
61 }
62
63 return ! empty( $config['selected_resource_id'] ) ? absint( $config['selected_resource_id'] ) : 0;
64 }
65
66 /**
67 * Normalize public shortcode attributes into the signed AJAX contract.
68 *
69 * The legacy-compatible aliases are accepted only at this boundary. AJAX and
70 * submission requests carry one stable normalized representation.
71 *
72 * @param mixed $attributes Raw shortcode attributes or decoded configuration.
73 *
74 * @return array<string,mixed> Safe Booking Resource selector configuration.
75 */
76 function wpbc_booking_resource_selector_normalize_config( $attributes ) {
77 $attributes = is_array( $attributes ) ? $attributes : array();
78 $defaults = array(
79 'resource_id' => 0,
80 'selected_resource_id' => 0,
81 'resource_ids' => array(),
82 'aggregate_resource_ids' => array(),
83 'cal_count' => 1,
84 'start_month_calendar' => false,
85 'calendar_dates_start' => '',
86 'calendar_dates_end' => '',
87 'selected_dates' => '',
88 'options' => '',
89 'form_type' => '',
90 'auto_select_resource' => false,
91 'show_progress' => true,
92 'progress_item_1_title' => null,
93 'progress_item_1_number' => null,
94 'progress_item_2_title' => null,
95 'progress_item_2_number' => null,
96 'screen_1_title' => null,
97 'screen_1_description' => null,
98 'allow_past' => false,
99 'return_url' => '',
100 );
101
102 $attribute_aliases = array(
103 'resources' => 'resource_ids',
104 'type' => 'resource_ids',
105 'aggregate' => 'aggregate_resource_ids',
106 'nummonths' => 'cal_count',
107 'startmonth' => 'start_month_calendar',
108 'selected_type' => 'selected_resource_id',
109 'label' => 'screen_1_title',
110 );
111 foreach ( $attribute_aliases as $public_attribute => $normalized_attribute ) {
112 if ( array_key_exists( $public_attribute, $attributes ) && ! array_key_exists( $normalized_attribute, $attributes ) ) {
113 $attributes[ $normalized_attribute ] = $attributes[ $public_attribute ];
114 }
115 unset( $attributes[ $public_attribute ] );
116 }
117
118 $attributes = array_intersect_key( $attributes, $defaults );
119 $config = wp_parse_args( $attributes, $defaults );
120 $config['resource_id'] = absint( $config['resource_id'] );
121 $config['selected_resource_id'] = absint( $config['selected_resource_id'] );
122 $config['resource_ids'] = wpbc_booking_resource_selector_normalize_ids( $config['resource_ids'] );
123 $config['aggregate_resource_ids'] = wpbc_booking_resource_selector_normalize_ids( $config['aggregate_resource_ids'] );
124 $config['cal_count'] = min( 24, max( 1, absint( $config['cal_count'] ) ) );
125
126 $start_month = $config['start_month_calendar'];
127 if ( is_array( $start_month ) ) {
128 $year = isset( $start_month[0] ) ? absint( $start_month[0] ) : 0;
129 $month = isset( $start_month[1] ) ? absint( $start_month[1] ) : 0;
130 $start_month = ( $year && $month >= 1 && $month <= 12 ) ? array( $year, $month ) : false;
131 } elseif ( is_string( $start_month ) && preg_match( '/^(\d{4})[-\/]?(\d{1,2})$/', $start_month, $matches ) ) {
132 $month = absint( $matches[2] );
133 $start_month = ( $month >= 1 && $month <= 12 ) ? array( absint( $matches[1] ), $month ) : false;
134 } else {
135 $start_month = false;
136 }
137 $config['start_month_calendar'] = $start_month;
138
139 foreach ( array( 'calendar_dates_start', 'calendar_dates_end' ) as $date_key ) {
140 $date_value = sanitize_text_field( (string) $config[ $date_key ] );
141 $config[ $date_key ] = preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_value ) ? $date_value : '';
142 }
143
144 $config['selected_dates'] = sanitize_text_field( (string) $config['selected_dates'] );
145 $config['options'] = sanitize_text_field( (string) $config['options'] );
146 $config['form_type'] = sanitize_text_field( (string) $config['form_type'] );
147 $config['return_url'] = esc_url_raw( (string) $config['return_url'] );
148 $config['auto_select_resource'] = wpbc_booking_resource_selector_normalize_boolean( $config['auto_select_resource'] );
149 $config['show_progress'] = wpbc_booking_resource_selector_normalize_boolean( $config['show_progress'], true );
150 $config['allow_past'] = wpbc_booking_resource_selector_normalize_boolean( $config['allow_past'] );
151
152 $display_text_keys = array(
153 'progress_item_1_title',
154 'progress_item_1_number',
155 'progress_item_2_title',
156 'progress_item_2_number',
157 'screen_1_title',
158 'screen_1_description',
159 );
160 foreach ( $display_text_keys as $display_text_key ) {
161 if ( null !== $config[ $display_text_key ] ) {
162 $config[ $display_text_key ] = sanitize_text_field( (string) $config[ $display_text_key ] );
163 }
164 }
165
166 return (array) apply_filters( 'wpbc_booking_resource_selector_normalized_config', $config, $attributes );
167 }
168
169 /**
170 * Check whether signed selector configuration enables past bookings.
171 *
172 * @param array<string,mixed> $config Normalized or decoded configuration.
173 *
174 * @return bool True when the signed shortcode explicitly enables past bookings.
175 */
176 function wpbc_booking_resource_selector_is_past_booking_enabled( $config ) {
177 return ! empty( $config['allow_past'] );
178 }
179
180 /**
181 * Base64-url encode a binary or text value without padding.
182 *
183 * @param string $raw_value Value to encode.
184 *
185 * @return string URL-safe encoded value.
186 */
187 function wpbc_booking_resource_selector_base64url_encode( $raw_value ) {
188 return rtrim( strtr( base64_encode( (string) $raw_value ), '+/', '-_' ), '=' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
189 }
190
191 /**
192 * Decode a base64-url value with strict validation.
193 *
194 * @param string $encoded_value Encoded value.
195 *
196 * @return string|false Decoded value or false.
197 */
198 function wpbc_booking_resource_selector_base64url_decode( $encoded_value ) {
199 $encoded_value = strtr( (string) $encoded_value, '-_', '+/' );
200 $padding = strlen( $encoded_value ) % 4;
201 if ( $padding ) {
202 $encoded_value .= str_repeat( '=', 4 - $padding );
203 }
204
205 return base64_decode( $encoded_value, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
206 }
207
208 /**
209 * Sign normalized shortcode configuration for public AJAX round trips.
210 *
211 * @param array<string,mixed> $config Normalized configuration.
212 *
213 * @return string Signed opaque configuration token.
214 */
215 function wpbc_booking_resource_selector_encode_config( $config ) {
216 $payload = wpbc_booking_resource_selector_base64url_encode( wp_json_encode( wpbc_booking_resource_selector_normalize_config( $config ) ) );
217 $signature = hash_hmac( 'sha256', $payload, wp_salt( 'auth' ), true );
218
219 return $payload . '.' . wpbc_booking_resource_selector_base64url_encode( $signature );
220 }
221
222 /**
223 * Verify and decode a public AJAX configuration token.
224 *
225 * @param string $config_token Signed token.
226 *
227 * @return array<string,mixed>|WP_Error Normalized configuration or validation error.
228 */
229 function wpbc_booking_resource_selector_decode_config( $config_token ) {
230 $parts = explode( '.', (string) $config_token, 2 );
231 if ( 2 !== count( $parts ) ) {
232 return new WP_Error( 'resource_selector_config_invalid', __( 'The Booking Resource selection configuration is invalid. Reload the page and try again.', 'booking' ) );
233 }
234
235 $expected_signature = hash_hmac( 'sha256', $parts[0], wp_salt( 'auth' ), true );
236 $actual_signature = wpbc_booking_resource_selector_base64url_decode( $parts[1] );
237 if ( false === $actual_signature || ! hash_equals( $expected_signature, $actual_signature ) ) {
238 return new WP_Error( 'resource_selector_config_invalid', __( 'The Booking Resource selection configuration is invalid. Reload the page and try again.', 'booking' ) );
239 }
240
241 $json_data = wpbc_booking_resource_selector_base64url_decode( $parts[0] );
242 $config_data = false !== $json_data ? json_decode( $json_data, true ) : null;
243 if ( ! is_array( $config_data ) ) {
244 return new WP_Error( 'resource_selector_config_invalid', __( 'The Booking Resource selection configuration is invalid. Reload the page and try again.', 'booking' ) );
245 }
246
247 return wpbc_booking_resource_selector_normalize_config( $config_data );
248 }
249
250 /**
251 * Sign one server-validated Booking Resource for final booking submission.
252 *
253 * @param array<string,mixed> $config Original normalized configuration.
254 * @param int $resource_id Selected Booking Resource ID.
255 *
256 * @return string Signed selection token, or an empty string for an invalid ID.
257 */
258 function wpbc_booking_resource_selector_encode_submission_context( $config, $resource_id ) {
259 $resource_id = absint( $resource_id );
260 if ( ! $resource_id ) {
261 return '';
262 }
263
264 $context = wpbc_booking_resource_selector_normalize_config( $config );
265 $context['resource_id'] = $resource_id;
266 $context['resource_ids'] = array( $resource_id );
267 $context['selected_resource_id'] = 0;
268
269 return wpbc_booking_resource_selector_encode_config( $context );
270 }
271
272 /**
273 * Verify that a signed selector submission context matches a resource.
274 *
275 * @param string $context_token Signed selector context token.
276 * @param int $resource_id Submitted Booking Resource ID.
277 *
278 * @return array<string,mixed>|WP_Error Verified context or controlled error.
279 */
280 function wpbc_booking_resource_selector_validate_submission_context( $context_token, $resource_id ) {
281 $resource_id = absint( $resource_id );
282 if ( '' === trim( (string) $context_token ) ) {
283 return new WP_Error( 'resource_selector_context_required', __( 'The Booking Resource selection has expired. Please start over and try again.', 'booking' ) );
284 }
285
286 $context = wpbc_booking_resource_selector_decode_config( $context_token );
287 if ( is_wp_error( $context ) ) {
288 return new WP_Error( 'resource_selector_context_invalid', __( 'The Booking Resource selection is invalid. Please start over and try again.', 'booking' ) );
289 }
290 if ( absint( $context['resource_id'] ) !== $resource_id ) {
291 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' ) );
292 }
293
294 return $context;
295 }
296