PluginProbe
Booking Calendar / 11.6
Booking Calendar v11.6
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-appointment / booking-appointment__config.php

booking-appointment__config.php in Booking Calendar 11.6, at includes/booking-appointment/booking-appointment__config.php

296 lines 12.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Appointment shortcode configuration normalization and signing.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Convert a comma-separated value or array to unique positive IDs.
14 *
15 * @param mixed $value Raw ID collection.
16 *
17 * @return int[] Normalized IDs.
18 */
19 function wpbc_booking_appointment_normalize_ids( $value ) {
20 if ( is_string( $value ) ) {
21 $value = preg_split( '/[;,\s]+/', $value, -1, PREG_SPLIT_NO_EMPTY );
22 }
23
24 return array_values( array_unique( array_filter( array_map( 'absint', (array) $value ) ) ) );
25 }
26
27 /**
28 * Normalize shortcode attributes into the stable AJAX configuration contract.
29 *
30 * @param mixed $attributes Raw shortcode attributes or decoded configuration.
31 *
32 * @return array<string,mixed> Safe Appointment configuration.
33 */
34 function wpbc_booking_appointment_normalize_config( $attributes ) {
35 $attributes = is_array( $attributes ) ? $attributes : array();
36 $defaults = array(
37 'service_id' => 0,
38 'provider_id' => 0,
39 'service_ids' => array(),
40 'provider_ids' => array(),
41 'cal_count' => 1,
42 'start_month_calendar' => false,
43 'calendar_dates_start' => '',
44 'calendar_dates_end' => '',
45 'options' => '',
46 'form_type' => '',
47 'auto_select_provider' => false,
48 'show_progress' => true,
49 'progress_item_1_title' => null,
50 'progress_item_1_number' => null,
51 'progress_item_2_title' => null,
52 'progress_item_2_number' => null,
53 'progress_item_3_title' => null,
54 'progress_item_3_number' => null,
55 'screen_1_title' => null,
56 'screen_1_description' => null,
57 'screen_2_title' => null,
58 'screen_2_description' => null,
59 'allow_past' => false,
60 'return_url' => '',
61 );
62
63 // Decode the public shortcode aliases only before values enter the signed token.
64 if ( isset( $attributes['services'] ) && ! isset( $attributes['service_ids'] ) ) {
65 $attributes['service_ids'] = $attributes['services'];
66 }
67 if ( isset( $attributes['providers'] ) && ! isset( $attributes['provider_ids'] ) ) {
68 $attributes['provider_ids'] = $attributes['providers'];
69 }
70 if ( isset( $attributes['nummonths'] ) && ! isset( $attributes['cal_count'] ) ) {
71 $attributes['cal_count'] = $attributes['nummonths'];
72 }
73 if ( isset( $attributes['startmonth'] ) && ! isset( $attributes['start_month_calendar'] ) ) {
74 $attributes['start_month_calendar'] = $attributes['startmonth'];
75 }
76
77 // Normalize earlier descriptive names before signing one indexed contract.
78 $progress_attribute_aliases = array(
79 'progress_service_title' => 'progress_item_1_title',
80 'progress_service_number' => 'progress_item_1_number',
81 'progress_provider_title' => 'progress_item_2_title',
82 'progress_provider_number' => 'progress_item_2_number',
83 'progress_details_title' => 'progress_item_3_title',
84 'progress_details_number' => 'progress_item_3_number',
85 );
86 foreach ( $progress_attribute_aliases as $legacy_attribute => $normalized_attribute ) {
87 if ( array_key_exists( $legacy_attribute, $attributes ) && ! array_key_exists( $normalized_attribute, $attributes ) ) {
88 $attributes[ $normalized_attribute ] = $attributes[ $legacy_attribute ];
89 }
90 unset( $attributes[ $legacy_attribute ] );
91 }
92
93 $config = wp_parse_args( $attributes, $defaults );
94
95 $config['service_id'] = absint( $config['service_id'] );
96 $config['provider_id'] = absint( $config['provider_id'] );
97 $config['service_ids'] = wpbc_booking_appointment_normalize_ids( $config['service_ids'] );
98 $config['provider_ids'] = wpbc_booking_appointment_normalize_ids( $config['provider_ids'] );
99 $config['cal_count'] = min( 24, max( 1, absint( $config['cal_count'] ) ) );
100
101 if ( $config['service_id'] && ! in_array( $config['service_id'], $config['service_ids'], true ) ) {
102 $config['service_ids'][] = $config['service_id'];
103 }
104 if ( $config['provider_id'] && ! in_array( $config['provider_id'], $config['provider_ids'], true ) ) {
105 $config['provider_ids'][] = $config['provider_id'];
106 }
107
108 $start_month = $config['start_month_calendar'];
109 if ( is_array( $start_month ) ) {
110 $year = isset( $start_month[0] ) ? absint( $start_month[0] ) : 0;
111 $month = isset( $start_month[1] ) ? absint( $start_month[1] ) : 0;
112 $start_month = ( $year && $month >= 1 && $month <= 12 ) ? array( $year, $month ) : false;
113 } elseif ( is_string( $start_month ) && preg_match( '/^(\d{4})[-\/]?(\d{1,2})$/', $start_month, $matches ) ) {
114 $month = absint( $matches[2] );
115 $start_month = ( $month >= 1 && $month <= 12 ) ? array( absint( $matches[1] ), $month ) : false;
116 } else {
117 $start_month = false;
118 }
119 $config['start_month_calendar'] = $start_month;
120
121 foreach ( array( 'calendar_dates_start', 'calendar_dates_end' ) as $date_key ) {
122 $date_value = sanitize_text_field( (string) $config[ $date_key ] );
123 $config[ $date_key ] = preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_value ) ? $date_value : '';
124 }
125
126 $config['options'] = sanitize_text_field( (string) $config['options'] );
127 $config['form_type'] = sanitize_text_field( (string) $config['form_type'] );
128 $config['return_url'] = esc_url_raw( (string) $config['return_url'] );
129 $auto_select_provider = is_string( $config['auto_select_provider'] ) ? strtolower( trim( $config['auto_select_provider'] ) ) : $config['auto_select_provider'];
130 $config['auto_select_provider'] = ! in_array( $auto_select_provider, array( false, 0, '0', 'false', 'off', 'no' ), true );
131 $show_progress = is_string( $config['show_progress'] ) ? strtolower( trim( $config['show_progress'] ) ) : $config['show_progress'];
132 $config['show_progress'] = ! in_array( $show_progress, array( false, 0, '0', 'false', 'off', 'no', '' ), true );
133 $display_text_keys = array(
134 'progress_item_1_title',
135 'progress_item_1_number',
136 'progress_item_2_title',
137 'progress_item_2_number',
138 'progress_item_3_title',
139 'progress_item_3_number',
140 'screen_1_title',
141 'screen_1_description',
142 'screen_2_title',
143 'screen_2_description',
144 );
145 foreach ( $display_text_keys as $display_text_key ) {
146 if ( null !== $config[ $display_text_key ] ) {
147 $config[ $display_text_key ] = sanitize_text_field( (string) $config[ $display_text_key ] );
148 }
149 }
150 $allow_past = is_string( $config['allow_past'] ) ? strtolower( trim( $config['allow_past'] ) ) : $config['allow_past'];
151 $config['allow_past'] = ! in_array( $allow_past, array( false, 0, '0', 'false', 'off', 'no', '' ), true );
152
153 return (array) apply_filters( 'wpbc_booking_appointment_normalized_config', $config, $attributes );
154 }
155
156 /**
157 * Check whether signed Appointment configuration enables past bookings.
158 *
159 * The site author explicitly opts in through the shortcode. The normalized
160 * value is included in the signed Appointment context and verified again by
161 * the save handler, so a visitor cannot enable it by modifying AJAX data.
162 *
163 * @param array<string,mixed> $config Normalized or decoded Appointment configuration.
164 *
165 * @return bool True when the signed configuration explicitly enables past bookings.
166 */
167 function wpbc_booking_appointment_is_past_booking_enabled( $config ) {
168 return ! empty( $config['allow_past'] );
169 }
170
171 /**
172 * Base64-url encode a binary or text value without padding.
173 *
174 * @param string $value Value to encode.
175 *
176 * @return string URL-safe encoded value.
177 */
178 function wpbc_booking_appointment_base64url_encode( $value ) {
179 return rtrim( strtr( base64_encode( (string) $value ), '+/', '-_' ), '=' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
180 }
181
182 /**
183 * Decode a base64-url value with strict validation.
184 *
185 * @param string $value Encoded value.
186 *
187 * @return string|false Decoded value or false.
188 */
189 function wpbc_booking_appointment_base64url_decode( $value ) {
190 $value = strtr( (string) $value, '-_', '+/' );
191 $padding = strlen( $value ) % 4;
192 if ( $padding ) {
193 $value .= str_repeat( '=', 4 - $padding );
194 }
195
196 return base64_decode( $value, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
197 }
198
199 /**
200 * Sign normalized shortcode configuration for public AJAX round trips.
201 *
202 * @param array<string,mixed> $config Normalized configuration.
203 *
204 * @return string Signed opaque configuration token.
205 */
206 function wpbc_booking_appointment_encode_config( $config ) {
207 $payload = wpbc_booking_appointment_base64url_encode( wp_json_encode( wpbc_booking_appointment_normalize_config( $config ) ) );
208 $signature = hash_hmac( 'sha256', $payload, wp_salt( 'auth' ), true );
209
210 return $payload . '.' . wpbc_booking_appointment_base64url_encode( $signature );
211 }
212
213 /**
214 * Verify and decode a public AJAX configuration token.
215 *
216 * @param string $token Signed token.
217 *
218 * @return array<string,mixed>|WP_Error Normalized configuration or validation error.
219 */
220 function wpbc_booking_appointment_decode_config( $token ) {
221 $parts = explode( '.', (string) $token, 2 );
222 if ( 2 !== count( $parts ) ) {
223 return new WP_Error( 'appointment_config_invalid', __( 'The Appointment configuration is invalid. Reload the page and try again.', 'booking' ) );
224 }
225
226 $expected_signature = hash_hmac( 'sha256', $parts[0], wp_salt( 'auth' ), true );
227 $actual_signature = wpbc_booking_appointment_base64url_decode( $parts[1] );
228 if ( false === $actual_signature || ! hash_equals( $expected_signature, $actual_signature ) ) {
229 return new WP_Error( 'appointment_config_invalid', __( 'The Appointment configuration is invalid. Reload the page and try again.', 'booking' ) );
230 }
231
232 $json = wpbc_booking_appointment_base64url_decode( $parts[0] );
233 $data = false !== $json ? json_decode( $json, true ) : null;
234 if ( ! is_array( $data ) ) {
235 return new WP_Error( 'appointment_config_invalid', __( 'The Appointment configuration is invalid. Reload the page and try again.', 'booking' ) );
236 }
237
238 return wpbc_booking_appointment_normalize_config( $data );
239 }
240
241 /**
242 * Sign one server-validated Service/Provider selection for booking submission.
243 *
244 * The selection is narrowed to exactly one Service and Provider. The token is
245 * attached to the rendered native form and verified again by the core booking
246 * save path, preventing another Appointment block from supplying its context.
247 *
248 * @param array<string,mixed> $config Original normalized shortcode configuration.
249 * @param int $service_id Selected Service ID.
250 * @param int $provider_id Selected Provider resource ID.
251 *
252 * @return string Signed selection token, or an empty string for invalid IDs.
253 */
254 function wpbc_booking_appointment_encode_submission_context( $config, $service_id, $provider_id ) {
255 $service_id = absint( $service_id );
256 $provider_id = absint( $provider_id );
257 if ( ! $service_id || ! $provider_id ) {
258 return '';
259 }
260
261 $context = wpbc_booking_appointment_normalize_config( $config );
262 $context['service_id'] = $service_id;
263 $context['provider_id'] = $provider_id;
264 $context['service_ids'] = array( $service_id );
265 $context['provider_ids'] = array( $provider_id );
266
267 return wpbc_booking_appointment_encode_config( $context );
268 }
269
270 /**
271 * Verify that a signed submission context matches the submitted booking pair.
272 *
273 * @param string $token Signed Appointment selection token.
274 * @param int $service_id Submitted Service ID.
275 * @param int $provider_id Submitted Provider resource ID.
276 *
277 * @return array<string,mixed>|WP_Error Verified context or controlled error.
278 */
279 function wpbc_booking_appointment_validate_submission_context( $token, $service_id, $provider_id ) {
280 $service_id = absint( $service_id );
281 $provider_id = absint( $provider_id );
282 if ( '' === trim( (string) $token ) ) {
283 return new WP_Error( 'appointment_context_required', __( 'The Appointment selection has expired. Please start over and try again.', 'booking' ) );
284 }
285
286 $context = wpbc_booking_appointment_decode_config( $token );
287 if ( is_wp_error( $context ) ) {
288 return new WP_Error( 'appointment_context_invalid', __( 'The Appointment selection is invalid. Please start over and try again.', 'booking' ) );
289 }
290 if ( $service_id !== absint( $context['service_id'] ) || $provider_id !== absint( $context['provider_id'] ) ) {
291 return new WP_Error( 'appointment_context_mismatch', __( 'The selected Service and Provider do not match this Appointment form. Please start over and try again.', 'booking' ) );
292 }
293
294 return $context;
295 }
296