PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 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 All 34 releases
← All changes | app/Services/BookingFieldService.php +104 -12 2.1.1 → 2.5.0 View file →
@@ -3,13 +3,18 @@
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\Libs\FileSystem;
8 +use FluentBooking\App\Services\SanitizeService;
7 9 use FluentBooking\Framework\Support\Arr;
8 10
9 11 class BookingFieldService
10 12 {
11 - public static function getCustomFieldsData($postedData, CalendarSlot $slot)
13 + /**
14 + * @param array|null $mappedKeys Fluent Forms mapped fields: read and sanitized only, the form owns validation.
15 + */
16 + public static function getCustomFieldsData($postedData, CalendarSlot $slot, $mappedKeys = null)
12 17 {
13 18 $customFields = self::getCustomFields($slot, true);
14 19
15 20 $errors = [];
@@ -16,10 +21,25 @@
16 21
17 22 $formattedValues = [];
18 23
19 24 foreach ($customFields as $fieldKey => $customField) {
25 + $isMapped = $mappedKeys !== null;
26 +
27 + if ($isMapped && !in_array($fieldKey, $mappedKeys, true)) {
28 + continue;
29 + }
30 +
20 31 $value = wp_unslash(Arr::get($postedData, $fieldKey));
21 - if (Arr::isTrue($customField, 'required')) {
32 +
33 + if ($customField['type'] === 'file' && $value) {
34 + // A posted external URL would render as a trusted download link, and
35 + // pro deletes stored basenames from the upload folder with the booking.
36 + // Slice first so a crafted array can't force a check per entry.
37 + $value = array_slice((array) $value, 0, (int) Arr::get($customField, 'max_file_allow', 1));
38 + $value = array_values(array_filter($value, [FileSystem::class, 'isUploadedFileUrl']));
39 + }
40 +
41 + if (!$isMapped && Arr::isTrue($customField, 'required')) {
22 42 $isTerms = $customField['type'] === 'terms-and-conditions';
23 43 $isCheckbox = $customField['type'] === 'checkbox';
24 44 if (!$value || ($isCheckbox && $value !== 'Yes') || ($isTerms && $value !== 'Accepted')) {
25 45 /* translators: %s: Field label */
@@ -33,12 +53,8 @@
33 53 $value = array_map(function ($item) {
34 54 $val = is_array($item) ? Arr::get($item, 'value') : $item;
35 55 return sanitize_text_field($val);
36 56 },$value);
37 - } else if ($customField['type'] === 'file') {
38 - $maxField = Arr::get($customField, 'max_file_allow', 1);
39 - $value = array_slice($value, 0, $maxField);
40 - $value = array_map('sanitize_text_field', $value);
41 57 } else {
42 58 $value = array_map('sanitize_text_field', $value);
43 59 }
44 60 } else if ($customField['type'] == 'textarea') {
@@ -46,8 +62,14 @@
46 62 } else {
47 63 $value = sanitize_text_field($value);
48 64 }
49 65
66 + if (!$isMapped && $customField['type'] === 'phone' && $value && !Helper::isValidPhoneNumber($value)) {
67 + /* translators: %s: Field label */
68 + $errors[$fieldKey . '.valid_phone_number'] = sprintf(__('%s is not a valid phone number', 'fluent-booking'), $customField['label']);
69 + continue;
70 + }
71 +
50 72 $formattedValues[$fieldKey] = $value;
51 73 }
52 74
53 75 if ($errors) {
@@ -58,12 +80,12 @@
58 80 }
59 81
60 82 public static function getBookingFields(CalendarSlot $calendarSlot, $cached = false)
61 83 {
62 - static $bookingFields = null;
84 + static $bookingFields = [];
63 85
64 - if ($cached && $bookingFields) {
65 - return $bookingFields;
86 + if ($cached && isset($bookingFields[$calendarSlot->id])) {
87 + return $bookingFields[$calendarSlot->id];
66 88 }
67 89
68 90 $requiredIndexes = ['name', 'email', 'message', 'cancellation_reason', 'rescheduling_reason'];
69 91
@@ -235,11 +257,9 @@
235 257 }
236 258
237 259 $existingFields['email']['disabled'] = false;
238 260
239 - $bookingFields = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
240 -
241 - return $bookingFields;
261 + return $bookingFields[$calendarSlot->id] = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
242 262 }
243 263
244 264 public static function getBookingFieldLabels(CalendarSlot $calendarSlot, $enabledOnly = false)
245 265 {
@@ -315,8 +335,13 @@
315 335
316 336 if ($fieldType == 'hidden') {
317 337 if ($isPublic) continue;
318 338 $formattedValue = EditorShortcodeParser::parse($formattedValue, $booking);
339 +
340 + // Some shortcodes return HTML, and the admin renders hidden values as HTML.
341 + if ($htmlSupport) {
342 + $formattedValue = wp_kses_post($formattedValue);
343 + }
319 344 }
320 345
321 346 $formattedData[$dataKey] = [
322 347 'label' => $label,
@@ -427,6 +452,73 @@
427 452 }
428 453 }
429 454 }
430 455 return true;
456 + }
457 +
458 + /**
459 + * Format booking fields (text sanitized, terms-and-conditions kses'd). Shared
460 + * by the admin save and calendar import so neither path can store raw HTML.
461 + * $calendarEvent is null-safe (import skips name generation).
462 + */
463 + public static function sanitizeBookingFields($bookingFields, $calendarEvent = null)
464 + {
465 + if (!is_array($bookingFields)) {
466 + return [];
467 + }
468 +
469 + $optionRequiredFields = ['dropdown', 'radio', 'checkbox-group', 'multi-select'];
470 + $textFields = ['type', 'name', 'label', 'placeholder', 'limit', 'help_text', 'date_format', 'min_date', 'max_date'];
471 + $booleanFields = ['enabled', 'required', 'system_defined', 'disable_alter', 'is_sms_number'];
472 +
473 + $formattedFields = [];
474 +
475 + foreach ($bookingFields as $value) {
476 + if (!is_array($value)) {
477 + continue;
478 + }
479 +
480 + if ($calendarEvent) {
481 + if (empty($value['name'])) {
482 + $value['name'] = self::generateFieldName($calendarEvent, Arr::get($value, 'label', ''));
483 + } else {
484 + $value['name'] = self::maybeGenerateFieldName($calendarEvent, $value);
485 + }
486 + } else {
487 + $value['name'] = sanitize_text_field(Arr::get($value, 'name', ''));
488 + }
489 +
490 + $textValues = array_map('sanitize_text_field', Arr::only($value, $textFields));
491 +
492 + $booleanValues = array_map(function ($valueItem) {
493 + return $valueItem === true || $valueItem === 'true' || $valueItem == 1;
494 + }, Arr::only($value, $booleanFields));
495 +
496 + $formattedField = array_merge($textValues, $booleanValues);
497 +
498 + $fieldType = Arr::get($value, 'type');
499 +
500 + $formattedField['index'] = (int) Arr::get($value, 'index');
501 + if (in_array($fieldType, $optionRequiredFields)) {
502 + $formattedField['options'] = array_map('sanitize_text_field', (array) Arr::get($value, 'options', []));
503 + }
504 + if ($fieldType == 'file') {
505 + $formattedField['max_file_allow'] = intval(Arr::get($value, 'max_file_allow'));
506 + $formattedField['allow_file_types'] = array_map('sanitize_text_field', (array) Arr::get($value, 'allow_file_types', []));
507 + $formattedField['file_size_value'] = intval(Arr::get($value, 'file_size_value'));
508 + $formattedField['file_size_unit'] = SanitizeService::checkCollection(Arr::get($value, 'file_size_unit'), ['kb', 'mb']);
509 + }
510 + if ($fieldType == 'hidden') {
511 + $formattedField['default_value'] = sanitize_text_field(Arr::get($value, 'default_value'));
512 + }
513 + if ($fieldType == 'terms-and-conditions') {
514 + $formattedField['terms_and_conditions'] = wp_kses_post(Arr::get($value, 'terms_and_conditions'));
515 + }
516 +
517 + $formattedField = apply_filters('fluent_booking/save_event_booking_field_' . $fieldType, $formattedField, $value, $calendarEvent);
518 +
519 + $formattedFields[] = $formattedField;
520 + }
521 +
522 + return $formattedFields;
431 523 }
432 524 }