PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
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 / page-setup-wizard / step-appearance / class-wpbc-setup-wizard-appearance.php

class-wpbc-setup-wizard-appearance.php in Booking Calendar 11.9, at includes/page-setup-wizard/step-appearance/class-wpbc-setup-wizard-appearance.php

438 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Read-only Appearance choices for the isolated Setup Wizard.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Adapt canonical Booking Calendar appearance registries to wizard DTOs.
14 */
15 final class WPBC_Setup_Wizard_Appearance {
16
17 /**
18 * Renderer used by every Appearance preview.
19 *
20 * Appearance compares visual settings against the selected Form Builder
21 * template itself. It must not inherit the Booking Form page's optional
22 * Service/Provider entry point because that would hide the form being styled.
23 *
24 * @var string
25 */
26 const PREVIEW_BOOKING_FORM_USAGE = 'direct_booking_form';
27
28 /**
29 * Return the exact starting styles exposed by the wizard.
30 *
31 * @return array<int,array{id:string,label:string,description:string}> Style DTOs.
32 */
33 public function get_starting_styles() {
34 $descriptions = array(
35 'light_bordered' => __( 'A clean light form with clear field and container borders.', 'booking' ),
36 'light_soft' => __( 'A light form with a soft background and gentle contrast.', 'booking' ),
37 'dark_bordered' => __( 'A dark bordered form for high-contrast layouts.', 'booking' ),
38 );
39 $styles = array();
40 $presets = function_exists( 'wpbc_bfb_settings__get_form_style_presets' )
41 ? wpbc_bfb_settings__get_form_style_presets()
42 : array();
43
44 foreach ( array( 'light_bordered', 'light_soft', 'dark_bordered' ) as $style_id ) {
45 if ( empty( $presets[ $style_id ] ) || ! is_array( $presets[ $style_id ] ) ) {
46 continue;
47 }
48
49 $styles[] = array(
50 'id' => $style_id,
51 'label' => isset( $presets[ $style_id ]['title'] ) ? sanitize_text_field( (string) $presets[ $style_id ]['title'] ) : $style_id,
52 'description' => $descriptions[ $style_id ],
53 );
54 }
55
56 return $styles;
57 }
58
59 /**
60 * Return the preset accent shortcuts displayed beside the custom picker.
61 *
62 * The first shortcut follows the canonical project default so activation,
63 * Form Builder, and Setup Wizard defaults cannot drift independently. Every
64 * value is still submitted through the same validated hexadecimal field;
65 * these records are presentation shortcuts, not an alternate save contract.
66 *
67 * @return array<int,array{value:string,label:string}> Accent shortcut DTOs.
68 */
69 public function get_accent_colors() {
70 return array(
71 array(
72 'value' => $this->get_default_accent_color(),
73 'label' => __( 'Blue', 'booking' ),
74 ),
75 array(
76 'value' => '#0EA5E9',
77 'label' => __( 'Sky blue', 'booking' ),
78 ),
79 array(
80 'value' => '#14B8A6',
81 'label' => __( 'Teal', 'booking' ),
82 ),
83 array(
84 'value' => '#F59E0B',
85 'label' => __( 'Amber', 'booking' ),
86 ),
87 array(
88 'value' => '#DB2777',
89 'label' => __( 'Rose', 'booking' ),
90 ),
91 array(
92 'value' => '#7C3AED',
93 'label' => __( 'Violet', 'booking' ),
94 ),
95 );
96 }
97
98 /**
99 * Return grouped calendar skin choices from the canonical registry.
100 *
101 * @return array<int,array{label:string,options:array<int,array{value:string,label:string,url:string}>}> Skin groups.
102 */
103 public function get_calendar_skin_groups() {
104 if ( ! function_exists( 'wpbc_get_calendar_skin_options' ) ) {
105 return array();
106 }
107
108 $groups = array();
109 $current_group = array(
110 'label' => __( 'Calendar Skins', 'booking' ),
111 'options' => array(),
112 );
113
114 foreach ( wpbc_get_calendar_skin_options() as $skin_value => $skin_label ) {
115 if ( is_array( $skin_label ) && ! empty( $skin_label['optgroup'] ) ) {
116 if ( ! empty( $skin_label['close'] ) ) {
117 if ( ! empty( $current_group['options'] ) ) {
118 $groups[] = $current_group;
119 }
120 $current_group = array( 'label' => '', 'options' => array() );
121 continue;
122 }
123
124 $current_group = array(
125 'label' => isset( $skin_label['title'] ) ? $this->normalize_label( $skin_label['title'] ) : __( 'Calendar Skins', 'booking' ),
126 'options' => array(),
127 );
128 continue;
129 }
130
131 $normalized_skin = $this->normalize_calendar_skin( $skin_value );
132 $label = is_array( $skin_label ) && isset( $skin_label['title'] ) ? $skin_label['title'] : $skin_label;
133 if ( '' === $normalized_skin || '' === $this->normalize_label( $label ) ) {
134 continue;
135 }
136
137 $current_group['options'][] = array(
138 'value' => $normalized_skin,
139 'label' => $this->normalize_label( $label ),
140 'url' => esc_url_raw( $this->get_calendar_skin_url( $normalized_skin ) ),
141 );
142 }
143
144 if ( ! empty( $current_group['options'] ) ) {
145 $groups[] = $current_group;
146 }
147
148 return $groups;
149 }
150
151 /**
152 * Return current canonical values as safe initial wizard suggestions.
153 *
154 * @return array<string,string> Initial appearance values.
155 */
156 public function get_initial_values() {
157 $style_ids = wp_list_pluck( $this->get_starting_styles(), 'id' );
158 $current_style = function_exists( 'wpbc_bfb_settings__get_current_form_style' )
159 ? wpbc_bfb_settings__get_current_form_style()
160 : 'light_bordered';
161 $current_style = in_array( $current_style, $style_ids, true ) ? $current_style : 'light_bordered';
162 $accent_color = function_exists( 'wpbc_bfb_settings__get_form_accent_options' )
163 ? wpbc_bfb_settings__get_form_accent_options()
164 : array();
165 $accent_color = isset( $accent_color['booking_form_accent_color'] ) ? $accent_color['booking_form_accent_color'] : $this->get_default_accent_color();
166 $current_skin = $this->validate_calendar_skin( $this->normalize_calendar_skin( get_bk_option( 'booking_skin' ) ), false );
167
168 if ( is_wp_error( $current_skin ) || '' === $current_skin ) {
169 $current_skin = $this->get_first_calendar_skin();
170 }
171
172 return array(
173 'booking_form_style' => $current_style,
174 'booking_form_accent_color' => $this->sanitize_accent_color( $accent_color ),
175 'booking_skin' => $current_skin,
176 'booking_timeslot_picker' => 'On' === get_bk_option( 'booking_timeslot_picker' ) ? 'On' : 'Off',
177 );
178 }
179
180 /**
181 * Validate one starting style against the wizard subset.
182 *
183 * @param mixed $raw_style Candidate style identifier.
184 * @param bool $is_required Whether an empty value is invalid.
185 *
186 * @return string|WP_Error Valid style identifier or an error.
187 */
188 public function validate_style( $raw_style, $is_required ) {
189 return $this->validate_exact_choice(
190 $raw_style,
191 wp_list_pluck( $this->get_starting_styles(), 'id' ),
192 $is_required,
193 'wpbc_setup_wizard_appearance_style_invalid',
194 __( 'Choose one of the available starting styles.', 'booking' )
195 );
196 }
197
198 /**
199 * Validate one six-digit hexadecimal accent color.
200 *
201 * @param mixed $raw_color Candidate color.
202 * @param bool $is_required Whether an empty value is invalid.
203 *
204 * @return string|WP_Error Canonical color or an error.
205 */
206 public function validate_accent_color( $raw_color, $is_required ) {
207 if ( ! is_scalar( $raw_color ) ) {
208 return new WP_Error( 'wpbc_setup_wizard_appearance_color_invalid', __( 'Choose a valid accent color.', 'booking' ) );
209 }
210
211 $raw_color = trim( (string) $raw_color );
212 if ( '' === $raw_color && ! $is_required ) {
213 return '';
214 }
215 if ( ! preg_match( '/^#[0-9A-Fa-f]{6}$/', $raw_color ) ) {
216 return new WP_Error( 'wpbc_setup_wizard_appearance_color_invalid', __( 'Choose a valid six-digit accent color.', 'booking' ) );
217 }
218
219 return strtoupper( $raw_color );
220 }
221
222 /**
223 * Validate one calendar skin against the canonical current registry.
224 *
225 * @param mixed $raw_skin Candidate relative skin path.
226 * @param bool $is_required Whether an empty value is invalid.
227 *
228 * @return string|WP_Error Canonical relative path or an error.
229 */
230 public function validate_calendar_skin( $raw_skin, $is_required ) {
231 if ( ! is_scalar( $raw_skin ) ) {
232 return new WP_Error( 'wpbc_setup_wizard_appearance_skin_invalid', __( 'Choose a valid calendar skin.', 'booking' ) );
233 }
234
235 $raw_skin = trim( (string) $raw_skin );
236 $normalized_skin = $this->normalize_calendar_skin( $raw_skin );
237 if ( '' === $normalized_skin && ! $is_required ) {
238 return '';
239 }
240 if ( $normalized_skin !== $raw_skin || ! in_array( $normalized_skin, $this->get_calendar_skin_values(), true ) ) {
241 return new WP_Error( 'wpbc_setup_wizard_appearance_skin_invalid', __( 'Choose a calendar skin from the available list.', 'booking' ) );
242 }
243
244 return $normalized_skin;
245 }
246
247 /**
248 * Validate the canonical time-selection presentation value.
249 *
250 * @param mixed $raw_value Candidate `On` or `Off` value.
251 * @param bool $is_required Whether an empty value is invalid.
252 *
253 * @return string|WP_Error Canonical value or an error.
254 */
255 public function validate_timeslot_picker( $raw_value, $is_required ) {
256 return $this->validate_exact_choice(
257 $raw_value,
258 array( 'Off', 'On' ),
259 $is_required,
260 'wpbc_setup_wizard_appearance_time_invalid',
261 __( 'Choose how time slots are displayed.', 'booking' )
262 );
263 }
264
265 /**
266 * Build the exact preview style and global-option snapshot.
267 *
268 * @param array<string,mixed> $values Candidate appearance values.
269 *
270 * @return array<string,array<string,string>>|WP_Error Valid preview configuration or an error.
271 */
272 public function get_preview_configuration( array $values ) {
273 $style = $this->validate_style( isset( $values['booking_form_style'] ) ? $values['booking_form_style'] : '', true );
274 $color = $this->validate_accent_color( isset( $values['booking_form_accent_color'] ) ? $values['booking_form_accent_color'] : '', true );
275 $skin = $this->validate_calendar_skin( isset( $values['booking_skin'] ) ? $values['booking_skin'] : '', true );
276 $time = $this->validate_timeslot_picker( isset( $values['booking_timeslot_picker'] ) ? $values['booking_timeslot_picker'] : '', true );
277
278 foreach ( array( $style, $color, $skin, $time ) as $validated_value ) {
279 if ( is_wp_error( $validated_value ) ) {
280 return $validated_value;
281 }
282 }
283
284 return array(
285 'form_style' => array(
286 'booking_form_style' => $style,
287 'booking_form_accent_enabled' => 'On',
288 'booking_form_accent_color' => $color,
289 ),
290 'option_overrides' => array(
291 'booking_skin' => $skin,
292 'booking_timeslot_picker' => $time,
293 ),
294 );
295 }
296
297 /**
298 * Normalize a label that may contain legacy HTML entities.
299 *
300 * @param mixed $label Candidate label.
301 *
302 * @return string Plain translated label.
303 */
304 private function normalize_label( $label ) {
305 $label = is_scalar( $label ) ? (string) $label : '';
306
307 $charset = function_exists( 'get_bloginfo' ) ? get_bloginfo( 'charset' ) : 'UTF-8';
308
309 return trim( sanitize_text_field( wp_strip_all_tags( html_entity_decode( $label, ENT_QUOTES, $charset ) ) ) );
310 }
311
312 /**
313 * Normalize a calendar skin URL or filesystem path to a relative path.
314 *
315 * @param mixed $skin_value Candidate path.
316 *
317 * @return string Relative path.
318 */
319 private function normalize_calendar_skin( $skin_value ) {
320 $skin_value = is_scalar( $skin_value ) ? sanitize_text_field( (string) $skin_value ) : '';
321 $replace = array( WPBC_PLUGIN_DIR, WPBC_PLUGIN_URL );
322 $upload_dir = wp_upload_dir();
323
324 if ( ! empty( $upload_dir['basedir'] ) ) {
325 $replace[] = $upload_dir['basedir'];
326 }
327 if ( ! empty( $upload_dir['baseurl'] ) ) {
328 $replace[] = $upload_dir['baseurl'];
329 }
330
331 return str_replace( $replace, '', $skin_value );
332 }
333
334 /**
335 * Resolve one normalized calendar skin to its browser-loadable URL.
336 *
337 * Custom skins are stored below the WordPress uploads directory, while
338 * bundled skins are stored below the plugin directory. Returning an explicit
339 * server-resolved URL prevents browser code from guessing either location.
340 *
341 * @param string $relative_skin Normalized relative calendar skin path.
342 *
343 * @return string Absolute skin URL.
344 */
345 private function get_calendar_skin_url( $relative_skin ) {
346 $relative_skin = $this->normalize_calendar_skin( $relative_skin );
347 $upload_dir = wp_upload_dir();
348 $upload_url = ! empty( $upload_dir['baseurl'] ) ? untrailingslashit( $upload_dir['baseurl'] ) : '';
349
350 if ( 0 === strpos( $relative_skin, '/wpbc_skins/' ) && '' !== $upload_url ) {
351 return $upload_url . $relative_skin;
352 }
353
354 return untrailingslashit( WPBC_PLUGIN_URL ) . '/' . ltrim( $relative_skin, '/' );
355 }
356
357 /**
358 * Return every normalized skin path exposed by the registry DTO.
359 *
360 * @return string[] Skin allow-list.
361 */
362 private function get_calendar_skin_values() {
363 $values = array();
364 foreach ( $this->get_calendar_skin_groups() as $group ) {
365 foreach ( $group['options'] as $option ) {
366 $values[] = $option['value'];
367 }
368 }
369
370 return array_values( array_unique( $values ) );
371 }
372
373 /**
374 * Return the first registered calendar skin as a safe fallback.
375 *
376 * @return string Relative skin path or an empty string.
377 */
378 private function get_first_calendar_skin() {
379 $values = $this->get_calendar_skin_values();
380
381 return ! empty( $values ) ? (string) reset( $values ) : '';
382 }
383
384 /**
385 * Normalize a trusted initial accent color.
386 *
387 * @param mixed $accent_color Candidate accent color.
388 *
389 * @return string Canonical six-digit color.
390 */
391 private function sanitize_accent_color( $accent_color ) {
392 $validated = $this->validate_accent_color( $accent_color, true );
393
394 return is_wp_error( $validated ) ? $this->get_default_accent_color() : $validated;
395 }
396
397 /**
398 * Return the project accent default without assuming the constant is loaded.
399 *
400 * @return string Canonical uppercase six-digit color.
401 */
402 private function get_default_accent_color() {
403 $default_color = defined( 'WPBC_DEFAULT_FORM_ACCENT_COLOR' ) ? strtoupper( (string) WPBC_DEFAULT_FORM_ACCENT_COLOR ) : '#315EFB';
404 if ( ! preg_match( '/^#[0-9A-F]{6}$/', $default_color ) ) {
405 return '#315EFB';
406 }
407
408 return $default_color;
409 }
410
411 /**
412 * Validate one scalar against an exact server-owned allow-list.
413 *
414 * @param mixed $raw_value Candidate value.
415 * @param string[] $allowed Exact allowed values.
416 * @param bool $is_required Whether empty is invalid.
417 * @param string $error_code Stable error code.
418 * @param string $error_message Translated error message.
419 *
420 * @return string|WP_Error Valid choice or an error.
421 */
422 private function validate_exact_choice( $raw_value, array $allowed, $is_required, $error_code, $error_message ) {
423 if ( ! is_scalar( $raw_value ) ) {
424 return new WP_Error( $error_code, $error_message );
425 }
426
427 $raw_value = trim( (string) $raw_value );
428 if ( '' === $raw_value && ! $is_required ) {
429 return '';
430 }
431 if ( ! in_array( $raw_value, $allowed, true ) ) {
432 return new WP_Error( $error_code, $error_message );
433 }
434
435 return $raw_value;
436 }
437 }
438