PluginProbe
Booking Calendar / 11.6
Booking Calendar v11.6
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 10.11.2 All 203 releases
booking / core / wpbc-frontend-messages.php

wpbc-frontend-messages.php in Booking Calendar 11.6, at core/wpbc-frontend-messages.php

369 lines 17.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Visitor-facing booking form message registry and resolver.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Get the visitor-facing message registry.
14 *
15 * Defaults intentionally remain in PHP/gettext. Only administrator overrides are stored in the database.
16 *
17 * @return array
18 */
19 function wpbc_frontend_messages__get_registry() {
20
21 $registry = array(
22 'message_check_no_selected_dates' => array(
23 'default' => __( 'Please, select booking date(s) at Calendar.', 'booking' ),
24 'title' => __( 'No booking dates selected', 'booking' ),
25 'description' => __( 'Shown when the booking form is submitted without selected calendar dates.', 'booking' ),
26 'group' => 'date_selection', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
27 ),
28 'message_range_selection_click_last_date' => array(
29 'default' => __( 'Start date selected. Now click the last day of your booking to complete the date range.', 'booking' ),
30 'title' => __( 'Complete date range', 'booking' ),
31 'description' => __( 'Informational note shown after the first click in two-click range selection mode.', 'booking' ),
32 'group' => 'date_selection', 'severity' => 'info', 'optional' => true, 'placeholders' => array(),
33 ),
34 'message_range_selection_has_unavailable_dates' => array(
35 'default' => __( 'The selected date range includes unavailable or already booked dates, so the selection was cleared. Please choose a range that contains only available dates.', 'booking' ),
36 'title' => __( 'Unavailable date inside range', 'booking' ),
37 'description' => __( 'Shown when a selected range contains an unavailable date and must be cleared.', 'booking' ),
38 'group' => 'date_selection', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
39 ),
40 'message_dates_times_unavailable' => array(
41 'default' => __( 'These dates and times in this calendar are already booked or unavailable.', 'booking' ),
42 'title' => __( 'Dates or times unavailable', 'booking' ),
43 'description' => __( 'Shown when the requested dates or times are no longer available.', 'booking' ),
44 'group' => 'date_selection', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
45 ),
46 'message_choose_alternative_dates' => array(
47 'default' => __( 'Please choose alternative date(s), times, or adjust the number of slots booked.', 'booking' ),
48 'title' => __( 'Choose alternative dates', 'booking' ),
49 'description' => __( 'Follow-up instruction for an unavailable date, time, or capacity selection.', 'booking' ),
50 'group' => 'date_selection', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
51 ),
52 'message_cannot_save_in_one_resource' => array(
53 'default' => __( 'It is not possible to store this sequence of the dates into the one same resource.', 'booking' ),
54 'title' => __( 'Cannot use one booking resource', 'booking' ),
55 'description' => __( 'Shown when the selected sequence cannot be stored in one booking resource.', 'booking' ),
56 'group' => 'date_selection', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
57 ),
58 'message_check_required' => array(
59 'default' => __( 'This field is required', 'booking' ),
60 'title' => __( 'Required field', 'booking' ),
61 'description' => __( 'Shown for an empty required text, select, or other standard field.', 'booking' ),
62 'group' => 'form_validation', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
63 ),
64 'message_check_required_for_check_box' => array(
65 'default' => __( 'This checkbox must be checked', 'booking' ),
66 'title' => __( 'Required checkbox', 'booking' ),
67 'description' => __( 'Shown when a required checkbox is not checked.', 'booking' ),
68 'group' => 'form_validation', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
69 ),
70 'message_check_required_for_radio_box' => array(
71 'default' => __( 'At least one option must be selected', 'booking' ),
72 'title' => __( 'Required option', 'booking' ),
73 'description' => __( 'Shown when no option is selected in a required radio group.', 'booking' ),
74 'group' => 'form_validation', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
75 ),
76 'message_check_email' => array(
77 'default' => __( 'Incorrect email address', 'booking' ),
78 'title' => __( 'Invalid email address', 'booking' ),
79 'description' => __( 'Shown when an email field does not contain a valid email address.', 'booking' ),
80 'group' => 'form_validation', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
81 ),
82 'message_check_same_email' => array(
83 'default' => __( 'Your emails do not match', 'booking' ),
84 'title' => __( 'Email addresses do not match', 'booking' ),
85 'description' => __( 'Shown when email confirmation does not match the original email field.', 'booking' ),
86 'group' => 'form_validation', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
87 ),
88 'message_check_valid_date' => array(
89 /* translators: Keep the {date_example} placeholder unchanged. */
90 'default' => __( 'This field must be a valid date like {date_example}', 'booking' ),
91 'title' => __( 'Invalid date format', 'booking' ),
92 'description' => __( 'Shown for a field using the validate_as_date CSS class.', 'booking' ),
93 'group' => 'form_validation', 'severity' => 'warning', 'optional' => false, 'placeholders' => array( '{date_example}' ),
94 ),
95 'message_check_digits_only' => array(
96 'default' => __( 'This field must contain only digits', 'booking' ),
97 'title' => __( 'Digits only', 'booking' ),
98 'description' => __( 'Shown for a field using the validate_as_digit CSS class.', 'booking' ),
99 'group' => 'form_validation', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
100 ),
101 'message_check_digits_count' => array(
102 /* translators: Keep the {digits} placeholder unchanged. */
103 'default' => __( 'This field must contain {digits} digits', 'booking' ),
104 'title' => __( 'Required number of digits', 'booking' ),
105 'description' => __( 'Shown for validate_digit_N fields. Keep the {digits} placeholder.', 'booking' ),
106 'group' => 'form_validation', 'severity' => 'warning', 'optional' => false, 'placeholders' => array( '{digits}' ),
107 ),
108 'message_error_start_time' => array(
109 'default' => __( 'Start Time is invalid. The date or time may be booked, or already in the past! Please choose another date or time.', 'booking' ),
110 'title' => __( 'Invalid start time', 'booking' ),
111 'description' => __( 'Shown when a selected start time cannot be booked.', 'booking' ),
112 'group' => 'time_validation', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
113 ),
114 'message_error_end_time' => array(
115 'default' => __( 'End Time is invalid. The date or time may be booked, or already in the past. The End Time may also be earlier that the start time, if only 1 day was selected! Please choose another date or time.', 'booking' ),
116 'title' => __( 'Invalid end time', 'booking' ),
117 'description' => __( 'Shown when a selected end time cannot be booked.', 'booking' ),
118 'group' => 'time_validation', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
119 ),
120 'message_error_range_time' => array(
121 'default' => __( 'The time(s) may be booked, or already in the past!', 'booking' ),
122 'title' => __( 'Invalid time range', 'booking' ),
123 'description' => __( 'Shown when a range-time selection is unavailable or in the past.', 'booking' ),
124 'group' => 'time_validation', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
125 ),
126 'message_error_duration_time' => array(
127 'default' => __( 'The time(s) may be booked, or already in the past!', 'booking' ),
128 'title' => __( 'Invalid duration', 'booking' ),
129 'description' => __( 'Shown when a duration-time selection is unavailable or in the past.', 'booking' ),
130 'group' => 'time_validation', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
131 ),
132 'message_booking_saving' => array(
133 'default' => __( 'Saving', 'booking' ),
134 'title' => __( 'Saving booking', 'booking' ),
135 'description' => __( 'Short progress text shown while the booking is being submitted.', 'booking' ),
136 'group' => 'submission', 'severity' => 'info', 'optional' => false, 'placeholders' => array(),
137 ),
138 'message_unexpected_server_response' => array(
139 'default' => __( 'The server returned an unexpected response. Please reload the page and try again. If the problem continues, contact the website administrator.', 'booking' ),
140 'title' => __( 'Unexpected server response', 'booking' ),
141 'description' => __( 'Shown when calendar loading or booking submission does not return valid Booking Calendar data.', 'booking' ),
142 'group' => 'submission', 'severity' => 'error', 'optional' => false, 'placeholders' => array(),
143 ),
144 'message_resource_selector_summary_label' => array(
145 'default' => __( 'Booking Resource', 'booking' ),
146 'title' => __( 'Selected Booking Resource label', 'booking' ),
147 'description' => __( 'Shown above the selected Booking Resource title on the booking form step.', 'booking' ),
148 'group' => 'booking_summaries', 'severity' => 'info', 'optional' => false, 'placeholders' => array(),
149 ),
150 'message_resource_selector_start_over' => array(
151 'default' => __( 'Start over', 'booking' ),
152 'title' => __( 'Booking Resource Start over button', 'booking' ),
153 'description' => __( 'Button text that returns a visitor to Booking Resource selection.', 'booking' ),
154 'group' => 'booking_summaries', 'severity' => 'info', 'optional' => false, 'placeholders' => array(),
155 ),
156 'message_appointment_summary_label' => array(
157 'default' => __( 'Your appointment', 'booking' ),
158 'title' => __( 'Appointment summary label', 'booking' ),
159 'description' => __( 'Shown above the selected Service title on the Appointment booking form step.', 'booking' ),
160 'group' => 'booking_summaries', 'severity' => 'info', 'optional' => false, 'placeholders' => array(),
161 ),
162 'message_appointment_start_over' => array(
163 'default' => __( 'Start over', 'booking' ),
164 'title' => __( 'Appointment Start over button', 'booking' ),
165 'description' => __( 'Button text that returns a visitor to Service selection.', 'booking' ),
166 'group' => 'booking_summaries', 'severity' => 'info', 'optional' => false, 'placeholders' => array(),
167 ),
168 'message_captcha_incorrect' => array(
169 'default' => __( 'The code you entered is incorrect', 'booking' ),
170 'title' => __( 'Incorrect CAPTCHA', 'booking' ),
171 'description' => __( 'Shown after an incorrect CAPTCHA response.', 'booking' ),
172 'group' => 'captcha', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
173 ),
174 );
175
176 /**
177 * Filters the visitor-facing message registry.
178 *
179 * @param array $registry Message definitions keyed by stable message ID.
180 */
181 return apply_filters( 'wpbc_frontend_message_registry', $registry );
182 }
183
184 /**
185 * Get normalized saved message settings.
186 *
187 * @return array
188 */
189 function wpbc_frontend_messages__get_settings() {
190
191 $settings = get_bk_option( 'booking_frontend_messages' );
192 if ( ! is_array( $settings ) ) {
193 $settings = array();
194 }
195
196 // Accept an early flat override array as a compatibility fallback.
197 if ( ! isset( $settings['messages'] ) ) {
198 $flat = array_intersect_key( $settings, wpbc_frontend_messages__get_registry() );
199 $settings = array( 'version' => 1, 'messages' => $flat, 'enabled' => array() );
200 }
201
202 return array(
203 'version' => 1,
204 'messages' => ( isset( $settings['messages'] ) && is_array( $settings['messages'] ) ) ? $settings['messages'] : array(),
205 'enabled' => ( isset( $settings['enabled'] ) && is_array( $settings['enabled'] ) ) ? $settings['enabled'] : array(),
206 );
207 }
208
209 /**
210 * Resolve a message for the active Booking Calendar locale.
211 *
212 * @param string $message_key Stable registry key.
213 * @param array $replacements Optional literal placeholder replacements.
214 * @param int $resource_id Optional resource ID reserved for future scoped overrides.
215 * @return string
216 */
217 function wpbc_frontend_messages__get( $message_key, $replacements = array(), $resource_id = 0 ) {
218
219 $registry = wpbc_frontend_messages__get_registry();
220 if ( ! isset( $registry[ $message_key ] ) ) {
221 return '';
222 }
223
224 $settings = wpbc_frontend_messages__get_settings();
225 if ( isset( $settings['messages'][ $message_key ] ) && is_string( $settings['messages'][ $message_key ] ) && '' !== trim( $settings['messages'][ $message_key ] ) ) {
226 $override = trim( $settings['messages'][ $message_key ] );
227 // Translation-only overrides inherit the live gettext default instead of freezing the administrator's locale.
228 if ( 0 === strpos( $override, '[lang=' ) ) {
229 $override = $registry[ $message_key ]['default'] . "\n" . $override;
230 }
231 $value = wpbc_lang( $override );
232 } else {
233 $value = $registry[ $message_key ]['default'];
234 }
235
236 if ( is_array( $replacements ) && ! empty( $replacements ) ) {
237 $safe_replacements = array();
238 foreach ( $replacements as $placeholder => $replacement ) {
239 $safe_replacements[ (string) $placeholder ] = sanitize_text_field( (string) $replacement );
240 }
241 $value = strtr( $value, $safe_replacements );
242 }
243
244 /**
245 * Filters a resolved visitor-facing message before final plain-text sanitization.
246 *
247 * @param string $value Message text.
248 * @param string $message_key Registry key.
249 * @param int $resource_id Booking resource ID, or zero for global context.
250 */
251 $value = apply_filters( 'wpbc_frontend_message_value', $value, $message_key, absint( $resource_id ) );
252 if ( ! is_scalar( $value ) ) {
253 $value = $registry[ $message_key ]['default'];
254 }
255
256 return sanitize_textarea_field( (string) $value );
257 }
258
259 /**
260 * Check whether an optional message is enabled.
261 *
262 * @param string $message_key Registry key.
263 * @return bool
264 */
265 function wpbc_frontend_messages__is_enabled( $message_key ) {
266
267 $registry = wpbc_frontend_messages__get_registry();
268 if ( ! isset( $registry[ $message_key ] ) ) {
269 return false;
270 }
271 if ( empty( $registry[ $message_key ]['optional'] ) ) {
272 return true;
273 }
274
275 $settings = wpbc_frontend_messages__get_settings();
276 return ! isset( $settings['enabled'][ $message_key ] ) || 'Off' !== $settings['enabled'][ $message_key ];
277 }
278
279 /**
280 * Split a validated [lang=LOCALE] message into editable sections.
281 *
282 * @param string $value Stored message value.
283 * @return array
284 */
285 function wpbc_frontend_messages__parse_language_sections( $value ) {
286
287 $result = array( 'default' => '', 'translations' => array() );
288 $value = (string) $value;
289 if ( '' === $value ) {
290 return $result;
291 }
292
293 if ( ! preg_match_all( '/\[lang=([A-Za-z0-9_@.\-]+)\]/', $value, $matches, PREG_OFFSET_CAPTURE ) ) {
294 $result['default'] = trim( $value );
295 return $result;
296 }
297
298 $result['default'] = trim( substr( $value, 0, $matches[0][0][1] ) );
299 $count = count( $matches[0] );
300 for ( $index = 0; $index < $count; $index++ ) {
301 $locale = $matches[1][ $index ][0];
302 $start = $matches[0][ $index ][1] + strlen( $matches[0][ $index ][0] );
303 $end = ( $index + 1 < $count ) ? $matches[0][ $index + 1 ][1] : strlen( $value );
304 $result['translations'][ $locale ] = trim( substr( $value, $start, $end - $start ) );
305 }
306
307 return $result;
308 }
309
310 /**
311 * Validate and sanitize one raw multilingual override.
312 *
313 * @param string $message_key Registry key.
314 * @param mixed $raw_value Posted value.
315 * @return string|WP_Error
316 */
317 function wpbc_frontend_messages__validate_value( $message_key, $raw_value ) {
318
319 $registry = wpbc_frontend_messages__get_registry();
320 if ( ! isset( $registry[ $message_key ] ) || ! is_scalar( $raw_value ) ) {
321 return new WP_Error( 'invalid_message', __( 'Invalid message value.', 'booking' ) );
322 }
323
324 $value = str_replace( array( "\r\n", "\r" ), "\n", wp_unslash( (string) $raw_value ) );
325 $value = trim( $value );
326 if ( '' === $value ) {
327 return '';
328 }
329 if ( 4000 < strlen( $value ) ) {
330 return new WP_Error( 'message_too_long', __( 'A form message cannot exceed 4000 characters.', 'booking' ) );
331 }
332 if ( false !== strpos( $value, '<' ) || false !== strpos( $value, '>' ) ) {
333 return new WP_Error( 'message_html_not_allowed', __( 'HTML is not allowed in form messages.', 'booking' ) );
334 }
335
336 $sanitized = sanitize_textarea_field( $value );
337 $without_valid_tags = preg_replace( '/\[lang=([A-Za-z0-9_@.\-]+)\]/', '', $sanitized );
338 if ( false !== stripos( $without_valid_tags, '[lang' ) ) {
339 return new WP_Error( 'invalid_language_tag', __( 'A language section contains an invalid [lang=LOCALE] tag.', 'booking' ) );
340 }
341
342 $sections = wpbc_frontend_messages__parse_language_sections( $sanitized );
343 if ( preg_match_all( '/\[lang=([A-Za-z0-9_@.\-]+)\]/', $sanitized, $locale_matches ) ) {
344 if ( count( $locale_matches[1] ) !== count( array_unique( $locale_matches[1] ) ) ) {
345 return new WP_Error( 'duplicate_language', __( 'Each locale can be used only once in a message.', 'booking' ) );
346 }
347 foreach ( $sections['translations'] as $translation ) {
348 if ( '' === trim( $translation ) ) {
349 return new WP_Error( 'empty_translation', __( 'Language sections cannot be empty.', 'booking' ) );
350 }
351 }
352 }
353
354 $placeholder_default = '' !== $sections['default'] ? $sections['default'] : $registry[ $message_key ]['default'];
355 $all_sections = array_merge( array( $placeholder_default ), array_values( $sections['translations'] ) );
356 foreach ( $registry[ $message_key ]['placeholders'] as $placeholder ) {
357 foreach ( $all_sections as $section_text ) {
358 if ( false === strpos( $section_text, $placeholder ) ) {
359 return new WP_Error(
360 'missing_placeholder',
361 sprintf( __( 'The message must contain the %s placeholder in every language.', 'booking' ), $placeholder )
362 );
363 }
364 }
365 }
366
367 return $sanitized;
368 }
369