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__render.php

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

340 lines 16.1 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 $html .= '<div class="wpbc_booking_resource_selector__choices" role="radiogroup" aria-label="' . esc_attr__( 'Booking Resources', 'booking' ) . '">';
170 foreach ( $catalog as $resource ) {
171 $resource_id = absint( $resource['resource_id'] );
172 $input_id = 'wpbc_resource_selector_' . $resource_id . '_' . wp_rand( 1000, 9999 );
173 $is_selected = $resource_id === $selected_resource_id;
174 $html .= '<label class="wpbc_booking_resource_selector__choice' . ( $is_selected ? ' is-selected' : '' ) . '" for="' . esc_attr( $input_id ) . '">';
175 $html .= '<input id="' . esc_attr( $input_id ) . '" type="radio" name="wpbc_resource_selector_resource" value="' . $resource_id . '"' . checked( $is_selected, true, false ) . ' required>';
176 $html .= '<span class="wpbc_booking_resource_selector__resource_icon" aria-hidden="true">';
177 if ( ! empty( $resource['image_url'] ) ) {
178 $html .= '<img src="' . esc_url( $resource['image_url'] ) . '" alt="" loading="lazy" decoding="async">';
179 } else {
180 $html .= esc_html( wpbc_booking_resource_selector_get_resource_initials( $resource['title'] ) );
181 }
182 $html .= '</span>';
183 $html .= '<span class="wpbc_booking_resource_selector__choice_body"><strong>' . esc_html( $resource['title'] ) . '</strong>';
184 if ( ! empty( $resource['description'] ) ) {
185 $html .= '<span class="wpbc_booking_resource_selector__choice_description">' . esc_html( $resource['description'] ) . '</span>';
186 }
187 $html .= '</span><span class="wpbc_booking_resource_selector__choice_mark" aria-hidden="true"></span></label>';
188 }
189 $html .= '</div><input type="hidden" name="wpbc_resource_selector_config" value="' . esc_attr( $config_token ) . '">';
190 $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>';
191
192 return $html;
193 }
194
195 /**
196 * Resolve the published Booking Form slug for one selected resource.
197 *
198 * An explicit form_type wins. Otherwise the established resource-specific
199 * default form filter is used, followed by the standard form.
200 *
201 * @param int $resource_id Selected Booking Resource ID.
202 * @param array<string,mixed> $config Selector configuration.
203 *
204 * @return string Safe Booking Form slug.
205 */
206 function wpbc_booking_resource_selector_resolve_form_slug( $resource_id, $config ) {
207 $form_slug = ! empty( $config['form_type'] ) ? sanitize_text_field( $config['form_type'] ) : '';
208 if ( '' === $form_slug ) {
209 $form_slug = sanitize_text_field( (string) apply_bk_filter( 'wpbc_get_default_custom_form', '', absint( $resource_id ) ) );
210 }
211 if ( '' === $form_slug ) {
212 $form_slug = 'standard';
213 }
214
215 $form_slug = apply_filters( 'wpbc_booking_resource_selector_form_slug', $form_slug, absint( $resource_id ), $config );
216
217 return sanitize_text_field( (string) $form_slug );
218 }
219
220 /**
221 * Build the native aggregate resource expression for one primary resource.
222 *
223 * @param int $resource_id Primary Booking Resource ID.
224 * @param array<string,mixed> $config Selector configuration.
225 *
226 * @return string Primary ID or a semicolon-delimited aggregate expression.
227 */
228 function wpbc_booking_resource_selector_get_render_resource_id( $resource_id, $config ) {
229 $resource_id = absint( $resource_id );
230 $aggregate_ids = array_values( array_diff( wpbc_booking_resource_selector_normalize_ids( $config['aggregate_resource_ids'] ), array( $resource_id ) ) );
231
232 return implode( ';', array_merge( array( $resource_id ), $aggregate_ids ) );
233 }
234
235 /**
236 * Render one native Booking Calendar form for the selected resource.
237 *
238 * @param array<string,mixed> $resource_record Selected public resource.
239 * @param array<string,mixed> $config Selector configuration.
240 * @param string $config_token Signed AJAX configuration.
241 * @param bool $can_change_resource Whether another Resource can be selected.
242 *
243 * @return string|WP_Error Native form markup or a controlled error.
244 */
245 function wpbc_booking_resource_selector_render_booking_form( $resource_record, $config, $config_token, $can_change_resource = false ) {
246 $resource_id = absint( $resource_record['resource_id'] );
247 $form_slug = wpbc_booking_resource_selector_resolve_form_slug( $resource_id, $config );
248 $allow_past = wpbc_booking_resource_selector_is_past_booking_enabled( $config );
249
250 static $rendered_resource_ids = array();
251 if ( isset( $rendered_resource_ids[ $resource_id ] ) ) {
252 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' ) );
253 }
254 $rendered_resource_ids[ $resource_id ] = true;
255
256 $render_params = array(
257 'is_echo' => 0,
258 'resource_id' => wpbc_booking_resource_selector_get_render_resource_id( $resource_id, $config ),
259 'cal_count' => absint( $config['cal_count'] ),
260 'custom_booking_form' => $form_slug,
261 'selected_dates_without_calendar' => $config['selected_dates'],
262 'shortcode_param__options' => $config['options'],
263 'calendar_dates_start' => $config['calendar_dates_start'],
264 'calendar_dates_end' => $config['calendar_dates_end'],
265 'calendar_request_overrides' => array(
266 'allow_past' => $allow_past ? 1 : 0,
267 ),
268 );
269 if ( ! empty( $config['start_month_calendar'] ) && is_array( $config['start_month_calendar'] ) ) {
270 $render_params['start_month_calendar'] = array(
271 absint( $config['start_month_calendar'][0] ),
272 absint( $config['start_month_calendar'][1] ),
273 );
274 }
275
276 $form_html = WPBC_FE_Render::render_booking_form( $render_params );
277 $submission_context = wpbc_booking_resource_selector_encode_submission_context( $config, $resource_id );
278 $summary_label = wpbc_frontend_messages__get( 'message_resource_selector_summary_label', array(), $resource_id );
279 $start_over_label = wpbc_frontend_messages__get( 'message_resource_selector_start_over', array(), $resource_id );
280 $html = wpbc_booking_resource_selector_render_progress( 2, $config );
281 $html .= '<div class="wpbc_booking_resource_selector__booking_header"><div><span>' . esc_html( $summary_label ) . '</span><strong>' . esc_html( $resource_record['title'] ) . '</strong></div>';
282 if ( $can_change_resource ) {
283 $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>';
284 }
285 $html .= '</div>';
286 $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' ) . '">';
287 $html .= $form_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
288 $html .= '</div>';
289
290 return $html;
291 }
292
293 /**
294 * Resolve and render the current Booking Resource selector stage.
295 *
296 * @param array<string,mixed> $config Normalized selector configuration.
297 * @param int $resource_id Selected Booking Resource ID.
298 * @param bool $allow_auto_select Whether this request may skip Resource selection.
299 *
300 * @return array{stage:string,html:string,resource_id:int}|WP_Error Stage response or validation error.
301 */
302 function wpbc_booking_resource_selector_resolve_stage( $config, $resource_id = 0, $allow_auto_select = true ) {
303 $config = wpbc_booking_resource_selector_normalize_config( $config );
304 $config_token = wpbc_booking_resource_selector_encode_config( $config );
305 $catalog = wpbc_booking_resource_selector_get_catalog( $config );
306 $resource_id = absint( $resource_id );
307
308 if ( ! $resource_id && $allow_auto_select && $config['auto_select_resource'] ) {
309 $default_resource_id = wpbc_booking_resource_selector_get_default_resource_id( $config );
310 if ( $default_resource_id && isset( $catalog[ $default_resource_id ] ) ) {
311 $resource_id = $default_resource_id;
312 }
313 if ( ! $resource_id && 1 === count( $catalog ) ) {
314 $resource_id = absint( key( $catalog ) );
315 }
316 }
317 if ( ! $resource_id ) {
318 return array(
319 'stage' => 'resource',
320 'html' => wpbc_booking_resource_selector_render_resources( $catalog, $config_token, $config ),
321 'resource_id' => 0,
322 );
323 }
324
325 $resource = wpbc_booking_resource_selector_get_resource( $catalog, $resource_id );
326 if ( is_wp_error( $resource ) ) {
327 return $resource;
328 }
329 $form_html = wpbc_booking_resource_selector_render_booking_form( $resource, $config, $config_token, count( $catalog ) > 1 );
330 if ( is_wp_error( $form_html ) ) {
331 return $form_html;
332 }
333
334 return array(
335 'stage' => 'booking',
336 'html' => $form_html,
337 'resource_id' => absint( $resource['resource_id'] ),
338 );
339 }
340