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
booking / includes / booking-resource-selector / booking-resource-selector__render.php

booking-resource-selector__render.php in Booking Calendar 11.8.4, at includes/booking-resource-selector/booking-resource-selector__render.php

363 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Server-side Booking Resource selection and native form rendering.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Build the form action used by the non-JavaScript selection fallback.
14 *
15 * @param array<string,mixed> $config Selector configuration containing the
16 * original public URL during AJAX.
17 *
18 * @return string Public URL without selector query parameters.
19 */
20 function wpbc_booking_resource_selector_get_fallback_url( $config = array() ) {
21 $fallback_url = ! empty( $config['return_url'] ) ? esc_url_raw( $config['return_url'] ) : '';
22 if ( ! $fallback_url ) {
23 $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( (string) $_SERVER['REQUEST_URI'] ) : '/'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
24 $fallback_url = home_url( $request_uri );
25 }
26 $fallback_url = remove_query_arg( array( 'wpbc_resource_selector_resource', 'wpbc_resource_selector_start_over' ), $fallback_url );
27
28 return wp_validate_redirect( $fallback_url, home_url( '/' ) );
29 }
30
31 /**
32 * Build a non-JavaScript URL that explicitly returns to Resource selection.
33 *
34 * @param array<string,mixed> $config Selector configuration.
35 *
36 * @return string Validated public Start over URL.
37 */
38 function wpbc_booking_resource_selector_get_start_over_url( $config = array() ) {
39 return add_query_arg( 'wpbc_resource_selector_start_over', '1', wpbc_booking_resource_selector_get_fallback_url( $config ) );
40 }
41
42 /**
43 * Render the two-step Booking Resource selection progress indicator.
44 *
45 * @param int $active_step Current step from 1 to 2.
46 * @param array<string,mixed> $config Normalized selector configuration.
47 *
48 * @return string Progress markup, or an empty string when disabled.
49 */
50 function wpbc_booking_resource_selector_render_progress( $active_step, $config = array() ) {
51 if ( isset( $config['show_progress'] ) && ! $config['show_progress'] ) {
52 return '';
53 }
54
55 $steps = array(
56 1 => array(
57 'number' => isset( $config['progress_item_1_number'] ) ? $config['progress_item_1_number'] : '1',
58 'title' => isset( $config['progress_item_1_title'] ) ? $config['progress_item_1_title'] : __( 'Resource', 'booking' ),
59 ),
60 2 => array(
61 'number' => isset( $config['progress_item_2_number'] ) ? $config['progress_item_2_number'] : '2',
62 'title' => isset( $config['progress_item_2_title'] ) ? $config['progress_item_2_title'] : __( 'Date & Details', 'booking' ),
63 ),
64 );
65 $html = '<ol class="wpbc_booking_resource_selector__progress" aria-label="' . esc_attr__( 'Booking progress', 'booking' ) . '">';
66 foreach ( $steps as $step_number => $step ) {
67 $step_class = $step_number < $active_step ? ' is-complete' : ( $step_number === $active_step ? ' is-active' : '' );
68 $html .= '<li class="wpbc_booking_resource_selector__progress_step' . esc_attr( $step_class ) . '"' . ( $step_number === $active_step ? ' aria-current="step"' : '' ) . '>';
69 $html .= '<span class="wpbc_booking_resource_selector__progress_number">' . esc_html( $step['number'] ) . '</span><span class="wpbc_booking_resource_selector__progress_label">' . esc_html( $step['title'] ) . '</span></li>';
70 }
71 $html .= '</ol>';
72
73 return $html;
74 }
75
76 /**
77 * Render the configurable Booking Resource screen heading.
78 *
79 * Omitted values use translated defaults. Explicit empty values hide their
80 * element, preserving the same omitted-versus-empty contract as Appointments.
81 *
82 * @param array<string,mixed> $config Normalized configuration.
83 * @param string $default_title Translated default title.
84 * @param string $default_description Translated default description.
85 *
86 * @return string Escaped heading markup, or an empty string.
87 */
88 function wpbc_booking_resource_selector_render_screen_heading( $config, $default_title, $default_description ) {
89 $screen_title = isset( $config['screen_1_title'] ) ? $config['screen_1_title'] : $default_title;
90 $screen_description = isset( $config['screen_1_description'] ) ? $config['screen_1_description'] : $default_description;
91 if ( '' === $screen_title && '' === $screen_description ) {
92 return '';
93 }
94
95 $html = '<div class="wpbc_booking_resource_selector__heading">';
96 if ( '' !== $screen_title ) {
97 $html .= '<h3>' . esc_html( $screen_title ) . '</h3>';
98 }
99 if ( '' !== $screen_description ) {
100 $html .= '<p>' . esc_html( $screen_description ) . '</p>';
101 }
102 $html .= '</div>';
103
104 return $html;
105 }
106
107 /**
108 * Render a consistent public selector notice.
109 *
110 * @param string $message Notice text.
111 * @param string $type Notice type, such as error or empty.
112 *
113 * @return string Escaped notice markup.
114 */
115 function wpbc_booking_resource_selector_render_notice( $message, $type = 'error' ) {
116 return '<div class="wpbc_booking_resource_selector__notice wpbc_booking_resource_selector__notice--' . esc_attr( sanitize_html_class( $type ) ) . '" role="status" tabindex="-1"><span>' . esc_html( $message ) . '</span></div>';
117 }
118
119 /**
120 * Render a structured selector error without allowing arbitrary HTML.
121 *
122 * @param WP_Error $error Selector workflow error.
123 *
124 * @return string Escaped notice markup.
125 */
126 function wpbc_booking_resource_selector_render_error_notice( $error ) {
127 return wpbc_booking_resource_selector_render_notice( $error->get_error_message(), 'error' );
128 }
129
130 /**
131 * Build a compact two-letter fallback for a Booking Resource image.
132 *
133 * @param string $resource_title Public Booking Resource title.
134 *
135 * @return string Uppercase one- or two-letter fallback.
136 */
137 function wpbc_booking_resource_selector_get_resource_initials( $resource_title ) {
138 $initials = '';
139 $words = array_slice( array_filter( preg_split( '/\s+/u', trim( wp_strip_all_tags( (string) $resource_title ) ) ) ), 0, 2 );
140 foreach ( $words as $word ) {
141 $initials .= function_exists( 'mb_substr' ) ? mb_substr( $word, 0, 1 ) : substr( $word, 0, 1 );
142 }
143
144 if ( '' === $initials ) {
145 $initials = 'R';
146 }
147
148 return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $initials ) : strtoupper( $initials );
149 }
150
151 /**
152 * Render the Booking Resource choice stage.
153 *
154 * @param array<int,array<string,mixed>> $catalog Public resource catalogue.
155 * @param string $config_token Signed AJAX configuration.
156 * @param array<string,mixed> $config Selector configuration.
157 *
158 * @return string Resource picker markup.
159 */
160 function wpbc_booking_resource_selector_render_resources( $catalog, $config_token, $config = array() ) {
161 if ( empty( $catalog ) ) {
162 return wpbc_booking_resource_selector_render_notice( __( 'No Booking Resources are currently available.', 'booking' ), 'empty' );
163 }
164
165 $selected_resource_id = wpbc_booking_resource_selector_get_default_resource_id( $config );
166 $html = wpbc_booking_resource_selector_render_progress( 1, $config );
167 $html .= wpbc_booking_resource_selector_render_screen_heading( $config, __( 'Choose a Booking Resource', 'booking' ), __( 'Select what you would like to book.', 'booking' ) );
168 $html .= '<form class="wpbc_booking_resource_selector__selection_form" method="get" action="' . esc_url( wpbc_booking_resource_selector_get_fallback_url( $config ) ) . '">';
169 if ( function_exists( 'wpbc_booking_resource_catalog_presenter' ) ) {
170 $html .= wpbc_booking_resource_catalog_presenter()->render_selectable_catalog(
171 $catalog,
172 array(
173 'layout' => $config['catalog_layout'],
174 'show_filters' => $config['show_resource_filters'],
175 'show_image' => $config['show_resource_image'],
176 'show_title' => $config['show_resource_title'],
177 'show_description' => $config['show_resource_description'],
178 'item_width' => $config['catalog_item_width'],
179 'item_max_width' => $config['catalog_item_max_width'],
180 'grid_items_per_row' => $config['catalog_grid_items_per_row'],
181 'list_items_per_row' => $config['catalog_list_items_per_row'],
182 'show_hierarchy' => $config['show_resource_hierarchy'],
183 'show_availability' => $config['show_availability'],
184 'show_price' => $config['show_starting_price'],
185 'selected_resource_id' => $selected_resource_id,
186 )
187 );
188 } else {
189 $html .= '<div class="wpbc_booking_resource_selector__choices" role="radiogroup" aria-label="' . esc_attr__( 'Booking Resources', 'booking' ) . '">';
190 foreach ( $catalog as $resource ) {
191 $resource_id = absint( $resource['resource_id'] );
192 $input_id = 'wpbc_resource_selector_' . $resource_id . '_' . wp_rand( 1000, 9999 );
193 $is_selected = $resource_id === $selected_resource_id;
194 $html .= '<label class="wpbc_booking_resource_selector__choice' . ( $is_selected ? ' is-selected' : '' ) . '" for="' . esc_attr( $input_id ) . '">';
195 $html .= '<input id="' . esc_attr( $input_id ) . '" type="radio" name="wpbc_resource_selector_resource" value="' . $resource_id . '"' . checked( $is_selected, true, false ) . ' required>';
196 $html .= '<span class="wpbc_booking_resource_selector__resource_icon" aria-hidden="true">';
197 if ( ! empty( $resource['image_url'] ) ) {
198 $html .= '<img src="' . esc_url( $resource['image_url'] ) . '" alt="" loading="lazy" decoding="async">';
199 } else {
200 $html .= esc_html( wpbc_booking_resource_selector_get_resource_initials( $resource['title'] ) );
201 }
202 $html .= '</span>';
203 $html .= '<span class="wpbc_booking_resource_selector__choice_body"><strong>' . esc_html( $resource['title'] ) . '</strong>';
204 if ( ! empty( $resource['description'] ) ) {
205 $html .= '<span class="wpbc_booking_resource_selector__choice_description">' . esc_html( $resource['description'] ) . '</span>';
206 }
207 $html .= '</span><span class="wpbc_booking_resource_selector__choice_mark" aria-hidden="true"></span></label>';
208 }
209 $html .= '</div>';
210 }
211 $html .= '<input type="hidden" name="wpbc_resource_selector_config" value="' . esc_attr( $config_token ) . '">';
212 $html .= '<div class="wpbc_booking_resource_selector__actions"><button type="submit" class="wpbc_button wpbc_button_primary wpbc_booking_resource_selector__continue">' . esc_html__( 'Continue', 'booking' ) . '</button></div></form>';
213
214 return $html;
215 }
216
217 /**
218 * Resolve the published Booking Form slug for one selected resource.
219 *
220 * An explicit form_type wins. Otherwise the established resource-specific
221 * default form filter is used, followed by the standard form.
222 *
223 * @param int $resource_id Selected Booking Resource ID.
224 * @param array<string,mixed> $config Selector configuration.
225 *
226 * @return string Safe Booking Form slug.
227 */
228 function wpbc_booking_resource_selector_resolve_form_slug( $resource_id, $config ) {
229 $form_slug = ! empty( $config['form_type'] ) ? sanitize_text_field( $config['form_type'] ) : '';
230 if ( '' === $form_slug ) {
231 $form_slug = sanitize_text_field( (string) apply_bk_filter( 'wpbc_get_default_custom_form', '', absint( $resource_id ) ) );
232 }
233 if ( '' === $form_slug ) {
234 $form_slug = 'standard';
235 }
236
237 $form_slug = apply_filters( 'wpbc_booking_resource_selector_form_slug', $form_slug, absint( $resource_id ), $config );
238
239 return sanitize_text_field( (string) $form_slug );
240 }
241
242 /**
243 * Build the native aggregate resource expression for one primary resource.
244 *
245 * @param int $resource_id Primary Booking Resource ID.
246 * @param array<string,mixed> $config Selector configuration.
247 *
248 * @return string Primary ID or a semicolon-delimited aggregate expression.
249 */
250 function wpbc_booking_resource_selector_get_render_resource_id( $resource_id, $config ) {
251 $resource_id = absint( $resource_id );
252 $aggregate_ids = array_values( array_diff( wpbc_booking_resource_selector_normalize_ids( $config['aggregate_resource_ids'] ), array( $resource_id ) ) );
253
254 return implode( ';', array_merge( array( $resource_id ), $aggregate_ids ) );
255 }
256
257 /**
258 * Render one native Booking Calendar form for the selected resource.
259 *
260 * @param array<string,mixed> $resource_record Selected public resource.
261 * @param array<string,mixed> $config Selector configuration.
262 * @param string $config_token Signed AJAX configuration.
263 * @param bool $can_change_resource Whether another Resource can be selected.
264 *
265 * @return string|WP_Error Native form markup or a controlled error.
266 */
267 function wpbc_booking_resource_selector_render_booking_form( $resource_record, $config, $config_token, $can_change_resource = false ) {
268 $resource_id = absint( $resource_record['resource_id'] );
269 $form_slug = wpbc_booking_resource_selector_resolve_form_slug( $resource_id, $config );
270 $allow_past = wpbc_booking_resource_selector_is_past_booking_enabled( $config );
271
272 static $rendered_resource_ids = array();
273 if ( isset( $rendered_resource_ids[ $resource_id ] ) ) {
274 return new WP_Error( 'resource_selector_duplicate_form', __( 'This Booking Resource already has an open booking form on this page. Complete or restart that booking before opening another form for the same resource.', 'booking' ) );
275 }
276 $rendered_resource_ids[ $resource_id ] = true;
277
278 $render_params = array(
279 'is_echo' => 0,
280 'resource_id' => wpbc_booking_resource_selector_get_render_resource_id( $resource_id, $config ),
281 'cal_count' => absint( $config['cal_count'] ),
282 'custom_booking_form' => $form_slug,
283 'selected_dates_without_calendar' => $config['selected_dates'],
284 'shortcode_param__options' => $config['options'],
285 'calendar_dates_start' => $config['calendar_dates_start'],
286 'calendar_dates_end' => $config['calendar_dates_end'],
287 'booking_workflow' => 'resource_selector',
288 'calendar_request_overrides' => array(
289 'allow_past' => $allow_past ? 1 : 0,
290 ),
291 );
292 if ( ! empty( $config['start_month_calendar'] ) && is_array( $config['start_month_calendar'] ) ) {
293 $render_params['start_month_calendar'] = array(
294 absint( $config['start_month_calendar'][0] ),
295 absint( $config['start_month_calendar'][1] ),
296 );
297 }
298
299 $form_html = WPBC_FE_Render::render_booking_form( $render_params );
300 $submission_context = wpbc_booking_resource_selector_encode_submission_context( $config, $resource_id );
301 $summary_label = wpbc_frontend_messages__get( 'message_resource_selector_summary_label', array(), $resource_id );
302 $start_over_label = wpbc_frontend_messages__get( 'message_resource_selector_start_over', array(), $resource_id );
303 $html = wpbc_booking_resource_selector_render_progress( 2, $config );
304 $html .= '<div class="wpbc_booking_resource_selector__booking_header"><div><span>' . esc_html( $summary_label ) . '</span><strong>' . esc_html( $resource_record['title'] ) . '</strong></div>';
305 if ( $can_change_resource ) {
306 $html .= '<a class="wpbc_button wpbc_button_secondary wpbc_booking_resource_selector__change" data-wpbc-resource-selector-action="start-over" href="' . esc_url( wpbc_booking_resource_selector_get_start_over_url( $config ) ) . '">' . esc_html( $start_over_label ) . '</a>';
307 }
308 $html .= '</div>';
309 $html .= '<div class="wpbc_booking_resource_selector__native_form" data-resource-id="' . $resource_id . '" data-resource-title="' . esc_attr( $resource_record['title'] ) . '" data-form-slug="' . esc_attr( $form_slug ) . '" data-config-token="' . esc_attr( $config_token ) . '" data-resource-selector-context-token="' . esc_attr( $submission_context ) . '" data-allow-past="' . ( $allow_past ? '1' : '0' ) . '">';
310 $html .= $form_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
311 $html .= '</div>';
312
313 return $html;
314 }
315
316 /**
317 * Resolve and render the current Booking Resource selector stage.
318 *
319 * @param array<string,mixed> $config Normalized selector configuration.
320 * @param int $resource_id Selected Booking Resource ID.
321 * @param bool $allow_auto_select Whether this request may skip Resource selection.
322 *
323 * @return array{stage:string,html:string,resource_id:int}|WP_Error Stage response or validation error.
324 */
325 function wpbc_booking_resource_selector_resolve_stage( $config, $resource_id = 0, $allow_auto_select = true ) {
326 $config = wpbc_booking_resource_selector_normalize_config( $config );
327 $config_token = wpbc_booking_resource_selector_encode_config( $config );
328 $catalog = wpbc_booking_resource_selector_get_catalog( $config );
329 $resource_id = absint( $resource_id );
330
331 if ( ! $resource_id && $allow_auto_select && $config['auto_select_resource'] ) {
332 $default_resource_id = wpbc_booking_resource_selector_get_default_resource_id( $config );
333 if ( $default_resource_id && isset( $catalog[ $default_resource_id ] ) ) {
334 $resource_id = $default_resource_id;
335 }
336 if ( ! $resource_id && 1 === count( $catalog ) ) {
337 $resource_id = absint( key( $catalog ) );
338 }
339 }
340 if ( ! $resource_id ) {
341 return array(
342 'stage' => 'resource',
343 'html' => wpbc_booking_resource_selector_render_resources( $catalog, $config_token, $config ),
344 'resource_id' => 0,
345 );
346 }
347
348 $resource = wpbc_booking_resource_selector_get_resource( $catalog, $resource_id );
349 if ( is_wp_error( $resource ) ) {
350 return $resource;
351 }
352 $form_html = wpbc_booking_resource_selector_render_booking_form( $resource, $config, $config_token, count( $catalog ) > 1 );
353 if ( is_wp_error( $form_html ) ) {
354 return $form_html;
355 }
356
357 return array(
358 'stage' => 'booking',
359 'html' => $form_html,
360 'resource_id' => absint( $resource['resource_id'] ),
361 );
362 }
363