PluginProbe
Booking Calendar / 11.4.2
Booking Calendar v11.4.2
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.4.2, at core/wpbc-frontend-messages.php

339 lines 15.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_captcha_incorrect' => array(
139 'default' => __( 'The code you entered is incorrect', 'booking' ),
140 'title' => __( 'Incorrect CAPTCHA', 'booking' ),
141 'description' => __( 'Shown after an incorrect CAPTCHA response.', 'booking' ),
142 'group' => 'captcha', 'severity' => 'warning', 'optional' => false, 'placeholders' => array(),
143 ),
144 );
145
146 /**
147 * Filters the visitor-facing message registry.
148 *
149 * @param array $registry Message definitions keyed by stable message ID.
150 */
151 return apply_filters( 'wpbc_frontend_message_registry', $registry );
152 }
153
154 /**
155 * Get normalized saved message settings.
156 *
157 * @return array
158 */
159 function wpbc_frontend_messages__get_settings() {
160
161 $settings = get_bk_option( 'booking_frontend_messages' );
162 if ( ! is_array( $settings ) ) {
163 $settings = array();
164 }
165
166 // Accept an early flat override array as a compatibility fallback.
167 if ( ! isset( $settings['messages'] ) ) {
168 $flat = array_intersect_key( $settings, wpbc_frontend_messages__get_registry() );
169 $settings = array( 'version' => 1, 'messages' => $flat, 'enabled' => array() );
170 }
171
172 return array(
173 'version' => 1,
174 'messages' => ( isset( $settings['messages'] ) && is_array( $settings['messages'] ) ) ? $settings['messages'] : array(),
175 'enabled' => ( isset( $settings['enabled'] ) && is_array( $settings['enabled'] ) ) ? $settings['enabled'] : array(),
176 );
177 }
178
179 /**
180 * Resolve a message for the active Booking Calendar locale.
181 *
182 * @param string $message_key Stable registry key.
183 * @param array $replacements Optional literal placeholder replacements.
184 * @param int $resource_id Optional resource ID reserved for future scoped overrides.
185 * @return string
186 */
187 function wpbc_frontend_messages__get( $message_key, $replacements = array(), $resource_id = 0 ) {
188
189 $registry = wpbc_frontend_messages__get_registry();
190 if ( ! isset( $registry[ $message_key ] ) ) {
191 return '';
192 }
193
194 $settings = wpbc_frontend_messages__get_settings();
195 if ( isset( $settings['messages'][ $message_key ] ) && is_string( $settings['messages'][ $message_key ] ) && '' !== trim( $settings['messages'][ $message_key ] ) ) {
196 $override = trim( $settings['messages'][ $message_key ] );
197 // Translation-only overrides inherit the live gettext default instead of freezing the administrator's locale.
198 if ( 0 === strpos( $override, '[lang=' ) ) {
199 $override = $registry[ $message_key ]['default'] . "\n" . $override;
200 }
201 $value = wpbc_lang( $override );
202 } else {
203 $value = $registry[ $message_key ]['default'];
204 }
205
206 if ( is_array( $replacements ) && ! empty( $replacements ) ) {
207 $safe_replacements = array();
208 foreach ( $replacements as $placeholder => $replacement ) {
209 $safe_replacements[ (string) $placeholder ] = sanitize_text_field( (string) $replacement );
210 }
211 $value = strtr( $value, $safe_replacements );
212 }
213
214 /**
215 * Filters a resolved visitor-facing message before final plain-text sanitization.
216 *
217 * @param string $value Message text.
218 * @param string $message_key Registry key.
219 * @param int $resource_id Booking resource ID, or zero for global context.
220 */
221 $value = apply_filters( 'wpbc_frontend_message_value', $value, $message_key, absint( $resource_id ) );
222 if ( ! is_scalar( $value ) ) {
223 $value = $registry[ $message_key ]['default'];
224 }
225
226 return sanitize_textarea_field( (string) $value );
227 }
228
229 /**
230 * Check whether an optional message is enabled.
231 *
232 * @param string $message_key Registry key.
233 * @return bool
234 */
235 function wpbc_frontend_messages__is_enabled( $message_key ) {
236
237 $registry = wpbc_frontend_messages__get_registry();
238 if ( ! isset( $registry[ $message_key ] ) ) {
239 return false;
240 }
241 if ( empty( $registry[ $message_key ]['optional'] ) ) {
242 return true;
243 }
244
245 $settings = wpbc_frontend_messages__get_settings();
246 return ! isset( $settings['enabled'][ $message_key ] ) || 'Off' !== $settings['enabled'][ $message_key ];
247 }
248
249 /**
250 * Split a validated [lang=LOCALE] message into editable sections.
251 *
252 * @param string $value Stored message value.
253 * @return array
254 */
255 function wpbc_frontend_messages__parse_language_sections( $value ) {
256
257 $result = array( 'default' => '', 'translations' => array() );
258 $value = (string) $value;
259 if ( '' === $value ) {
260 return $result;
261 }
262
263 if ( ! preg_match_all( '/\[lang=([A-Za-z0-9_@.\-]+)\]/', $value, $matches, PREG_OFFSET_CAPTURE ) ) {
264 $result['default'] = trim( $value );
265 return $result;
266 }
267
268 $result['default'] = trim( substr( $value, 0, $matches[0][0][1] ) );
269 $count = count( $matches[0] );
270 for ( $index = 0; $index < $count; $index++ ) {
271 $locale = $matches[1][ $index ][0];
272 $start = $matches[0][ $index ][1] + strlen( $matches[0][ $index ][0] );
273 $end = ( $index + 1 < $count ) ? $matches[0][ $index + 1 ][1] : strlen( $value );
274 $result['translations'][ $locale ] = trim( substr( $value, $start, $end - $start ) );
275 }
276
277 return $result;
278 }
279
280 /**
281 * Validate and sanitize one raw multilingual override.
282 *
283 * @param string $message_key Registry key.
284 * @param mixed $raw_value Posted value.
285 * @return string|WP_Error
286 */
287 function wpbc_frontend_messages__validate_value( $message_key, $raw_value ) {
288
289 $registry = wpbc_frontend_messages__get_registry();
290 if ( ! isset( $registry[ $message_key ] ) || ! is_scalar( $raw_value ) ) {
291 return new WP_Error( 'invalid_message', __( 'Invalid message value.', 'booking' ) );
292 }
293
294 $value = str_replace( array( "\r\n", "\r" ), "\n", wp_unslash( (string) $raw_value ) );
295 $value = trim( $value );
296 if ( '' === $value ) {
297 return '';
298 }
299 if ( 4000 < strlen( $value ) ) {
300 return new WP_Error( 'message_too_long', __( 'A form message cannot exceed 4000 characters.', 'booking' ) );
301 }
302 if ( false !== strpos( $value, '<' ) || false !== strpos( $value, '>' ) ) {
303 return new WP_Error( 'message_html_not_allowed', __( 'HTML is not allowed in form messages.', 'booking' ) );
304 }
305
306 $sanitized = sanitize_textarea_field( $value );
307 $without_valid_tags = preg_replace( '/\[lang=([A-Za-z0-9_@.\-]+)\]/', '', $sanitized );
308 if ( false !== stripos( $without_valid_tags, '[lang' ) ) {
309 return new WP_Error( 'invalid_language_tag', __( 'A language section contains an invalid [lang=LOCALE] tag.', 'booking' ) );
310 }
311
312 $sections = wpbc_frontend_messages__parse_language_sections( $sanitized );
313 if ( preg_match_all( '/\[lang=([A-Za-z0-9_@.\-]+)\]/', $sanitized, $locale_matches ) ) {
314 if ( count( $locale_matches[1] ) !== count( array_unique( $locale_matches[1] ) ) ) {
315 return new WP_Error( 'duplicate_language', __( 'Each locale can be used only once in a message.', 'booking' ) );
316 }
317 foreach ( $sections['translations'] as $translation ) {
318 if ( '' === trim( $translation ) ) {
319 return new WP_Error( 'empty_translation', __( 'Language sections cannot be empty.', 'booking' ) );
320 }
321 }
322 }
323
324 $placeholder_default = '' !== $sections['default'] ? $sections['default'] : $registry[ $message_key ]['default'];
325 $all_sections = array_merge( array( $placeholder_default ), array_values( $sections['translations'] ) );
326 foreach ( $registry[ $message_key ]['placeholders'] as $placeholder ) {
327 foreach ( $all_sections as $section_text ) {
328 if ( false === strpos( $section_text, $placeholder ) ) {
329 return new WP_Error(
330 'missing_placeholder',
331 sprintf( __( 'The message must contain the %s placeholder in every language.', 'booking' ), $placeholder )
332 );
333 }
334 }
335 }
336
337 return $sanitized;
338 }
339