PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / trunk
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution vtrunk
2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 1.7.2 All 33 releases
← All changes | app/Services/BookingFieldService.php +181 -22 1.5.21trunk View file →
@@ -3,8 +3,9 @@
3 3 namespace FluentBooking\App\Services;
4 4
5 5 use FluentBooking\App\Models\Booking;
6 6 use FluentBooking\App\Models\CalendarSlot;
7 +use FluentBooking\App\Services\SanitizeService;
7 8 use FluentBooking\Framework\Support\Arr;
8 9
9 10 class BookingFieldService
10 11 {
@@ -17,22 +18,28 @@
17 18 $formattedValues = [];
18 19
19 20 foreach ($customFields as $fieldKey => $customField) {
20 21 $value = wp_unslash(Arr::get($postedData, $fieldKey));
21 - if (!$value && Arr::isTrue($customField, 'required')) {
22 - // translators: %s is the label of the required field
23 - $errors[$fieldKey . '.required'] = sprintf(__('%s is required', 'fluent-booking'), $customField['label']);
24 - continue;
22 + if (Arr::isTrue($customField, 'required')) {
23 + $isTerms = $customField['type'] === 'terms-and-conditions';
24 + $isCheckbox = $customField['type'] === 'checkbox';
25 + if (!$value || ($isCheckbox && $value !== 'Yes') || ($isTerms && $value !== 'Accepted')) {
26 + /* translators: %s: Field label */
27 + $errors[$fieldKey . '.required'] = sprintf(__('%s is required', 'fluent-booking'), $customField['label']);
28 + continue;
29 + }
25 30 }
26 31
27 32 if (is_array($value)) {
28 33 if ($customField['type'] === 'multi-select') {
29 - $value = array_map(
30 - function ($item) {
31 - return sanitize_text_field(Arr::get($item, 'value'));
32 - },
33 - $value
34 - );
34 + $value = array_map(function ($item) {
35 + $val = is_array($item) ? Arr::get($item, 'value') : $item;
36 + return sanitize_text_field($val);
37 + },$value);
38 + } else if ($customField['type'] === 'file') {
39 + $maxField = Arr::get($customField, 'max_file_allow', 1);
40 + $value = array_slice($value, 0, $maxField);
41 + $value = array_map('sanitize_text_field', $value);
35 42 } else {
36 43 $value = array_map('sanitize_text_field', $value);
37 44 }
38 45 } else if ($customField['type'] == 'textarea') {
@@ -40,8 +47,14 @@
40 47 } else {
41 48 $value = sanitize_text_field($value);
42 49 }
43 50
51 + if ($customField['type'] === 'phone' && $value && !Helper::isValidPhoneNumber($value)) {
52 + /* translators: %s: Field label */
53 + $errors[$fieldKey . '.valid_phone_number'] = sprintf(__('%s is not a valid phone number', 'fluent-booking'), $customField['label']);
54 + continue;
55 + }
56 +
44 57 $formattedValues[$fieldKey] = $value;
45 58 }
46 59
47 60 if ($errors) {
@@ -50,10 +63,16 @@
50 63
51 64 return $formattedValues;
52 65 }
53 66
54 - public static function getBookingFields(CalendarSlot $calendarSlot)
67 + public static function getBookingFields(CalendarSlot $calendarSlot, $cached = false)
55 68 {
69 + static $bookingFields = null;
70 +
71 + if ($cached && $bookingFields) {
72 + return $bookingFields;
73 + }
74 +
56 75 $requiredIndexes = ['name', 'email', 'message', 'cancellation_reason', 'rescheduling_reason'];
57 76
58 77 $defaultFields = [
59 78 'name' => [
@@ -132,8 +151,9 @@
132 151 'system_defined' => true,
133 152 'disable_alter' => false
134 153 ];
135 154 }
155 +
136 156 if ($calendarSlot->isLocationFieldRequired()) {
137 157 $requiredIndexes[] = 'location';
138 158 $defaultFields['location'] = [
139 159 'index' => 7,
@@ -222,16 +242,18 @@
222 242 }
223 243
224 244 $existingFields['email']['disabled'] = false;
225 245
226 - return array_values($existingFields);
246 + $bookingFields = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
247 +
248 + return $bookingFields;
227 249 }
228 250
229 251 public static function getBookingFieldLabels(CalendarSlot $calendarSlot, $enabledOnly = false)
230 252 {
231 - $fields = self::getBookingFields($calendarSlot);
253 + $fields = self::getBookingFields($calendarSlot, true);
254 +
232 255 $labels = [];
233 -
234 256 foreach ($fields as $field) {
235 257 if ($enabledOnly && !Arr::isTrue($field, 'enabled')) {
236 258 continue;
237 259 }
@@ -243,9 +265,10 @@
243 265 }
244 266
245 267 public static function generateFieldName($calendarEvent, $fieldLabel)
246 268 {
247 - $fieldName = 'custom_' . sanitize_title($fieldLabel);
269 + $fieldLabel = preg_replace('/[^A-Za-z0-9]/', '_', $fieldLabel);
270 + $fieldName = 'custom_' . strtolower($fieldLabel);
248 271 $bookingFields = self::getBookingFields($calendarEvent);
249 272
250 273 $matched = 0;
251 274 foreach ($bookingFields as $field) {
@@ -259,10 +282,23 @@
259 282 }
260 283 return $fieldName;
261 284 }
262 285
263 - public static function getFormattedCustomBookingData(Booking $booking)
286 + public static function maybeGenerateFieldName($calendarEvent, $fieldValue)
264 287 {
288 + $bookingFields = self::getBookingFields($calendarEvent);
289 +
290 + foreach ($bookingFields as $field) {
291 + if ($field['name'] == $fieldValue['name'] && $field['index'] != $fieldValue['index']) {
292 + return self::generateFieldName($calendarEvent, $fieldValue['label']);
293 + }
294 + }
295 +
296 + return sanitize_text_field($fieldValue['name']);
297 + }
298 +
299 + public static function getFormattedCustomBookingData(Booking $booking, $htmlSupport = true, $isPublic = false)
300 + {
265 301 $customFormData = $booking->getMeta('custom_fields_data', []);
266 302 if (!$customFormData) {
267 303 return [];
268 304 }
@@ -271,16 +307,29 @@
271 307
272 308 $formattedData = [];
273 309
274 310 foreach ($customFormData as $dataKey => $value) {
275 - if (isset($labels[$dataKey])) {
276 - $label = $labels[$dataKey];
277 - } else {
278 - $label = $dataKey;
311 + $label = $labels[$dataKey] ?? $dataKey;
312 +
313 + $formattedValue = is_array($value) ? implode(', ', $value) : $value;
314 +
315 + $field = self::getBookingFieldByName($booking->calendar_event, $dataKey);
316 +
317 + $fieldType = Arr::get($field, 'type');
318 +
319 + if ($fieldType == 'file' && is_array($value)) {
320 + $formattedValue = self::getUploadedFiles($value, $htmlSupport);
279 321 }
322 +
323 + if ($fieldType == 'hidden') {
324 + if ($isPublic) continue;
325 + $formattedValue = EditorShortcodeParser::parse($formattedValue, $booking);
326 + }
327 +
280 328 $formattedData[$dataKey] = [
281 329 'label' => $label,
282 - 'value' => is_array($value) ? implode(', ', $value) : $value
330 + 'type' => $fieldType,
331 + 'value' => $formattedValue
283 332 ];
284 333 }
285 334
286 335 return $formattedData;
@@ -311,9 +360,9 @@
311 360 }
312 361
313 362 public static function getBookingFieldByName($calendarEvent, $name)
314 363 {
315 - $fields = self::getBookingFields($calendarEvent);
364 + $fields = self::getBookingFields($calendarEvent, true);
316 365
317 366 foreach ($fields as $field) {
318 367 if (Arr::get($field, 'name') == $name) {
319 368 return $field;
@@ -342,6 +391,116 @@
342 391 }
343 392 }
344 393 }
345 394 return false;
395 + }
396 +
397 + public static function getUploadedFiles($fieldValue, $htmlSupport = true)
398 + {
399 + if (empty($fieldValue)) {
400 + return '';
401 + }
402 +
403 + $files = array_map(function($file) use ($htmlSupport) {
404 + if ($htmlSupport) {
405 + return '<a href="' . esc_url($file) . '" target="_blank" download="' . esc_attr(basename($file)) . '">' . esc_html(basename($file)) . '</a>';
406 + }
407 + return $file;
408 + }, $fieldValue);
409 +
410 + $separator = $htmlSupport ? '<br>' : PHP_EOL;
411 +
412 + return implode($separator, $files);
413 + }
414 +
415 + public static function validateDateFields($customFieldsData, $calendarEvent)
416 + {
417 + foreach ($customFieldsData as $fieldKey => $fieldValue) {
418 + $field = self::getBookingFieldByName($calendarEvent, $fieldKey);
419 + if ($fieldValue && Arr::get($field, 'type') == 'date') {
420 + $minDate = Arr::get($field, 'min_date');
421 + $maxDate = Arr::get($field, 'max_date');
422 +
423 + $fieldValue = DateTimeHelper::getFormattedDate($fieldValue, Arr::get($field, 'date_format'));
424 + $minDate = gmdate('Y-m-d', strtotime($minDate ?: '1900-01-01'));
425 + $maxDate = gmdate('Y-m-d', strtotime($maxDate ?: gmdate('Y-12-31')));
426 +
427 + if ($minDate && $fieldValue < $minDate) {
428 + /* translators: %1$s: Field label, %2$s: Minimum date */
429 + return new \WP_Error('invalid_date', sprintf(__('The date for %1$s cannot be earlier than %2$s.', 'fluent-booking'), $field['label'], $minDate));
430 + }
431 + if ($maxDate && $fieldValue > $maxDate) {
432 + /* translators: %1$s: Field label, %2$s: Maximum date */
433 + return new \WP_Error('invalid_date', sprintf(__('The date for %1$s cannot be later than %2$s.', 'fluent-booking'), $field['label'], $maxDate));
434 + }
435 + }
436 + }
437 + return true;
438 + }
439 +
440 + /**
441 + * Format booking fields (text sanitized, terms-and-conditions kses'd). Shared
442 + * by the admin save and calendar import so neither path can store raw HTML.
443 + * $calendarEvent is null-safe (import skips name generation).
444 + */
445 + public static function sanitizeBookingFields($bookingFields, $calendarEvent = null)
446 + {
447 + if (!is_array($bookingFields)) {
448 + return [];
449 + }
450 +
451 + $optionRequiredFields = ['dropdown', 'radio', 'checkbox-group', 'multi-select'];
452 + $textFields = ['type', 'name', 'label', 'placeholder', 'limit', 'help_text', 'date_format', 'min_date', 'max_date'];
453 + $booleanFields = ['enabled', 'required', 'system_defined', 'disable_alter', 'is_sms_number'];
454 +
455 + $formattedFields = [];
456 +
457 + foreach ($bookingFields as $value) {
458 + if (!is_array($value)) {
459 + continue;
460 + }
461 +
462 + if ($calendarEvent) {
463 + if (empty($value['name'])) {
464 + $value['name'] = self::generateFieldName($calendarEvent, Arr::get($value, 'label', ''));
465 + } else {
466 + $value['name'] = self::maybeGenerateFieldName($calendarEvent, $value);
467 + }
468 + } else {
469 + $value['name'] = sanitize_text_field(Arr::get($value, 'name', ''));
470 + }
471 +
472 + $textValues = array_map('sanitize_text_field', Arr::only($value, $textFields));
473 +
474 + $booleanValues = array_map(function ($valueItem) {
475 + return $valueItem === true || $valueItem === 'true' || $valueItem == 1;
476 + }, Arr::only($value, $booleanFields));
477 +
478 + $formattedField = array_merge($textValues, $booleanValues);
479 +
480 + $fieldType = Arr::get($value, 'type');
481 +
482 + $formattedField['index'] = (int) Arr::get($value, 'index');
483 + if (in_array($fieldType, $optionRequiredFields)) {
484 + $formattedField['options'] = array_map('sanitize_text_field', (array) Arr::get($value, 'options', []));
485 + }
486 + if ($fieldType == 'file') {
487 + $formattedField['max_file_allow'] = intval(Arr::get($value, 'max_file_allow'));
488 + $formattedField['allow_file_types'] = array_map('sanitize_text_field', (array) Arr::get($value, 'allow_file_types', []));
489 + $formattedField['file_size_value'] = intval(Arr::get($value, 'file_size_value'));
490 + $formattedField['file_size_unit'] = SanitizeService::checkCollection(Arr::get($value, 'file_size_unit'), ['kb', 'mb']);
491 + }
492 + if ($fieldType == 'hidden') {
493 + $formattedField['default_value'] = sanitize_text_field(Arr::get($value, 'default_value'));
494 + }
495 + if ($fieldType == 'terms-and-conditions') {
496 + $formattedField['terms_and_conditions'] = wp_kses_post(Arr::get($value, 'terms_and_conditions'));
497 + }
498 +
499 + $formattedField = apply_filters('fluent_booking/save_event_booking_field_' . $fieldType, $formattedField, $value, $calendarEvent);
500 +
501 + $formattedFields[] = $formattedField;
502 + }
503 +
504 + return $formattedFields;
346 505 }
347 506 }