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 / core / wpbc-frontend-messages.php

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

396 lines 20.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_duplicate_booking_resource_on_page' => array(
139 /* translators: Keep the {resource_id} placeholder unchanged. */
140 'default' => __( 'More than one calendar or booking form for Booking Resource ID {resource_id} is present on this page. A hidden copy still conflicts with the visible form and can prevent booking submission. The site administrator must keep only one calendar or form for this Booking Resource. After the page is corrected, reload it and try again.', 'booking' ),
141 'title' => __( 'Duplicate Booking Resource on page', 'booking' ),
142 'description' => __( 'Shown when more than one calendar or booking form uses the same Booking Resource ID and one copy cannot initialize.', 'booking' ),
143 'group' => 'submission', 'severity' => 'warning', 'optional' => false, 'placeholders' => array( '{resource_id}' ),
144 ),
145 'message_booking_form_context_required' => array(
146 /* translators: Keep the {resource_id} placeholder unchanged. */
147 'default' => __( 'This booking form is not connected to a valid calendar for Booking Resource ID {resource_id}. This can happen when the same Booking Resource is used more than once on the page, including in hidden content, or when the page cache is outdated. Reload the page and try again. If the problem continues, ask the site administrator to remove duplicate forms for this Booking Resource and clear the page cache.', 'booking' ),
148 'title' => __( 'Booking form is not connected to its calendar', 'booking' ),
149 'description' => __( 'Shown when a public booking submission is missing its server-signed Booking Form context.', 'booking' ),
150 'group' => 'submission', 'severity' => 'warning', 'optional' => false, 'placeholders' => array( '{resource_id}' ),
151 ),
152 'message_booking_form_context_expired' => array(
153 /* translators: Keep the {resource_id} placeholder unchanged. */
154 'default' => __( 'This booking form was generated by an older or cached version of the page for Booking Resource ID {resource_id}. Reload the page and try again. If the problem continues, ask the site administrator to clear the page cache and remove any duplicate calendar or form for this Booking Resource.', 'booking' ),
155 'title' => __( 'Booking form context is outdated', 'booking' ),
156 'description' => __( 'Shown when a signed Booking Form context uses an older unsupported contract version.', 'booking' ),
157 'group' => 'submission', 'severity' => 'warning', 'optional' => false, 'placeholders' => array( '{resource_id}' ),
158 ),
159 'message_unexpected_server_response' => array(
160 'default' => __( 'The server returned an unexpected response. Please reload the page and try again. If the problem continues, contact the website administrator.', 'booking' ),
161 'title' => __( 'Unexpected server response', 'booking' ),
162 'description' => __( 'Shown when calendar loading or booking submission does not return valid Booking Calendar data.', 'booking' ),
163 'group' => 'submission', 'severity' => 'error', 'optional' => false, 'placeholders' => array(),
164 ),
165 'message_http_conflict_cache_hint' => array(
166 'default' => __( 'A cached or optimized version of this page may be outdated. Clear the browser cache and reload the page. Website administrators should also purge page, server, and CDN caches and regenerate minified or optimized files.', 'booking' ),
167 'title' => __( 'HTTP conflict cache troubleshooting', 'booking' ),
168 'description' => __( 'Shown after an HTTP 409 calendar-loading failure when stale cached or optimized content may be involved.', 'booking' ),
169 'group' => 'submission', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
170 ),
171 'message_resource_selector_summary_label' => array(
172 'default' => __( 'Booking Resource', 'booking' ),
173 'title' => __( 'Selected Booking Resource label', 'booking' ),
174 'description' => __( 'Shown above the selected Booking Resource title on the booking form step.', 'booking' ),
175 'group' => 'booking_summaries', 'severity' => 'info', 'optional' => false, 'placeholders' => array(),
176 ),
177 'message_resource_selector_start_over' => array(
178 'default' => __( 'Start over', 'booking' ),
179 'title' => __( 'Booking Resource Start over button', 'booking' ),
180 'description' => __( 'Button text that returns a visitor to Booking Resource selection.', 'booking' ),
181 'group' => 'booking_summaries', 'severity' => 'info', 'optional' => false, 'placeholders' => array(),
182 ),
183 'message_appointment_summary_label' => array(
184 'default' => __( 'Your appointment', 'booking' ),
185 'title' => __( 'Appointment summary label', 'booking' ),
186 'description' => __( 'Shown above the selected Service title on the Appointment booking form step.', 'booking' ),
187 'group' => 'booking_summaries', 'severity' => 'info', 'optional' => false, 'placeholders' => array(),
188 ),
189 'message_appointment_start_over' => array(
190 'default' => __( 'Start over', 'booking' ),
191 'title' => __( 'Appointment Start over button', 'booking' ),
192 'description' => __( 'Button text that returns a visitor to Service selection.', 'booking' ),
193 'group' => 'booking_summaries', 'severity' => 'info', 'optional' => false, 'placeholders' => array(),
194 ),
195 'message_captcha_incorrect' => array(
196 'default' => __( 'The code you entered is incorrect', 'booking' ),
197 'title' => __( 'Incorrect CAPTCHA', 'booking' ),
198 'description' => __( 'Shown after an incorrect CAPTCHA response.', 'booking' ),
199 'group' => 'captcha', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
200 ),
201 );
202
203 /**
204 * Filters the visitor-facing message registry.
205 *
206 * @param array $registry Message definitions keyed by stable message ID.
207 */
208 return apply_filters( 'wpbc_frontend_message_registry', $registry );
209 }
210
211 /**
212 * Get normalized saved message settings.
213 *
214 * @return array
215 */
216 function wpbc_frontend_messages__get_settings() {
217
218 $settings = get_bk_option( 'booking_frontend_messages' );
219 if ( ! is_array( $settings ) ) {
220 $settings = array();
221 }
222
223 // Accept an early flat override array as a compatibility fallback.
224 if ( ! isset( $settings['messages'] ) ) {
225 $flat = array_intersect_key( $settings, wpbc_frontend_messages__get_registry() );
226 $settings = array( 'version' => 1, 'messages' => $flat, 'enabled' => array() );
227 }
228
229 return array(
230 'version' => 1,
231 'messages' => ( isset( $settings['messages'] ) && is_array( $settings['messages'] ) ) ? $settings['messages'] : array(),
232 'enabled' => ( isset( $settings['enabled'] ) && is_array( $settings['enabled'] ) ) ? $settings['enabled'] : array(),
233 );
234 }
235
236 /**
237 * Resolve a message for the active Booking Calendar locale.
238 *
239 * @param string $message_key Stable registry key.
240 * @param array $replacements Optional literal placeholder replacements.
241 * @param int $resource_id Optional resource ID reserved for future scoped overrides.
242 * @return string
243 */
244 function wpbc_frontend_messages__get( $message_key, $replacements = array(), $resource_id = 0 ) {
245
246 $registry = wpbc_frontend_messages__get_registry();
247 if ( ! isset( $registry[ $message_key ] ) ) {
248 return '';
249 }
250
251 $settings = wpbc_frontend_messages__get_settings();
252 if ( isset( $settings['messages'][ $message_key ] ) && is_string( $settings['messages'][ $message_key ] ) && '' !== trim( $settings['messages'][ $message_key ] ) ) {
253 $override = trim( $settings['messages'][ $message_key ] );
254 // Translation-only overrides inherit the live gettext default instead of freezing the administrator's locale.
255 if ( 0 === strpos( $override, '[lang=' ) ) {
256 $override = $registry[ $message_key ]['default'] . "\n" . $override;
257 }
258 $value = wpbc_lang( $override );
259 } else {
260 $value = $registry[ $message_key ]['default'];
261 }
262
263 if ( is_array( $replacements ) && ! empty( $replacements ) ) {
264 $safe_replacements = array();
265 foreach ( $replacements as $placeholder => $replacement ) {
266 $safe_replacements[ (string) $placeholder ] = sanitize_text_field( (string) $replacement );
267 }
268 $value = strtr( $value, $safe_replacements );
269 }
270
271 /**
272 * Filters a resolved visitor-facing message before final plain-text sanitization.
273 *
274 * @param string $value Message text.
275 * @param string $message_key Registry key.
276 * @param int $resource_id Booking resource ID, or zero for global context.
277 */
278 $value = apply_filters( 'wpbc_frontend_message_value', $value, $message_key, absint( $resource_id ) );
279 if ( ! is_scalar( $value ) ) {
280 $value = $registry[ $message_key ]['default'];
281 }
282
283 return sanitize_textarea_field( (string) $value );
284 }
285
286 /**
287 * Check whether an optional message is enabled.
288 *
289 * @param string $message_key Registry key.
290 * @return bool
291 */
292 function wpbc_frontend_messages__is_enabled( $message_key ) {
293
294 $registry = wpbc_frontend_messages__get_registry();
295 if ( ! isset( $registry[ $message_key ] ) ) {
296 return false;
297 }
298 if ( empty( $registry[ $message_key ]['optional'] ) ) {
299 return true;
300 }
301
302 $settings = wpbc_frontend_messages__get_settings();
303 return ! isset( $settings['enabled'][ $message_key ] ) || 'Off' !== $settings['enabled'][ $message_key ];
304 }
305
306 /**
307 * Split a validated [lang=LOCALE] message into editable sections.
308 *
309 * @param string $value Stored message value.
310 * @return array
311 */
312 function wpbc_frontend_messages__parse_language_sections( $value ) {
313
314 $result = array( 'default' => '', 'translations' => array() );
315 $value = (string) $value;
316 if ( '' === $value ) {
317 return $result;
318 }
319
320 if ( ! preg_match_all( '/\[lang=([A-Za-z0-9_@.\-]+)\]/', $value, $matches, PREG_OFFSET_CAPTURE ) ) {
321 $result['default'] = trim( $value );
322 return $result;
323 }
324
325 $result['default'] = trim( substr( $value, 0, $matches[0][0][1] ) );
326 $count = count( $matches[0] );
327 for ( $index = 0; $index < $count; $index++ ) {
328 $locale = $matches[1][ $index ][0];
329 $start = $matches[0][ $index ][1] + strlen( $matches[0][ $index ][0] );
330 $end = ( $index + 1 < $count ) ? $matches[0][ $index + 1 ][1] : strlen( $value );
331 $result['translations'][ $locale ] = trim( substr( $value, $start, $end - $start ) );
332 }
333
334 return $result;
335 }
336
337 /**
338 * Validate and sanitize one raw multilingual override.
339 *
340 * @param string $message_key Registry key.
341 * @param mixed $raw_value Posted value.
342 * @return string|WP_Error
343 */
344 function wpbc_frontend_messages__validate_value( $message_key, $raw_value ) {
345
346 $registry = wpbc_frontend_messages__get_registry();
347 if ( ! isset( $registry[ $message_key ] ) || ! is_scalar( $raw_value ) ) {
348 return new WP_Error( 'invalid_message', __( 'Invalid message value.', 'booking' ) );
349 }
350
351 $value = str_replace( array( "\r\n", "\r" ), "\n", wp_unslash( (string) $raw_value ) );
352 $value = trim( $value );
353 if ( '' === $value ) {
354 return '';
355 }
356 if ( 4000 < strlen( $value ) ) {
357 return new WP_Error( 'message_too_long', __( 'A form message cannot exceed 4000 characters.', 'booking' ) );
358 }
359 if ( false !== strpos( $value, '<' ) || false !== strpos( $value, '>' ) ) {
360 return new WP_Error( 'message_html_not_allowed', __( 'HTML is not allowed in form messages.', 'booking' ) );
361 }
362
363 $sanitized = sanitize_textarea_field( $value );
364 $without_valid_tags = preg_replace( '/\[lang=([A-Za-z0-9_@.\-]+)\]/', '', $sanitized );
365 if ( false !== stripos( $without_valid_tags, '[lang' ) ) {
366 return new WP_Error( 'invalid_language_tag', __( 'A language section contains an invalid [lang=LOCALE] tag.', 'booking' ) );
367 }
368
369 $sections = wpbc_frontend_messages__parse_language_sections( $sanitized );
370 if ( preg_match_all( '/\[lang=([A-Za-z0-9_@.\-]+)\]/', $sanitized, $locale_matches ) ) {
371 if ( count( $locale_matches[1] ) !== count( array_unique( $locale_matches[1] ) ) ) {
372 return new WP_Error( 'duplicate_language', __( 'Each locale can be used only once in a message.', 'booking' ) );
373 }
374 foreach ( $sections['translations'] as $translation ) {
375 if ( '' === trim( $translation ) ) {
376 return new WP_Error( 'empty_translation', __( 'Language sections cannot be empty.', 'booking' ) );
377 }
378 }
379 }
380
381 $placeholder_default = '' !== $sections['default'] ? $sections['default'] : $registry[ $message_key ]['default'];
382 $all_sections = array_merge( array( $placeholder_default ), array_values( $sections['translations'] ) );
383 foreach ( $registry[ $message_key ]['placeholders'] as $placeholder ) {
384 foreach ( $all_sections as $section_text ) {
385 if ( false === strpos( $section_text, $placeholder ) ) {
386 return new WP_Error(
387 'missing_placeholder',
388 sprintf( __( 'The message must contain the %s placeholder in every language.', 'booking' ), $placeholder )
389 );
390 }
391 }
392 }
393
394 return $sanitized;
395 }
396