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 +99 -13 2.3.0 → 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,9 +62,9 @@
46 62 } else {
47 63 $value = sanitize_text_field($value);
48 64 }
49 65
50 - if ($customField['type'] === 'phone' && $value && !Helper::isValidPhoneNumber($value)) {
66 + if (!$isMapped && $customField['type'] === 'phone' && $value && !Helper::isValidPhoneNumber($value)) {
51 67 /* translators: %s: Field label */
52 68 $errors[$fieldKey . '.valid_phone_number'] = sprintf(__('%s is not a valid phone number', 'fluent-booking'), $customField['label']);
53 69 continue;
54 70 }
@@ -64,12 +80,12 @@
64 80 }
65 81
66 82 public static function getBookingFields(CalendarSlot $calendarSlot, $cached = false)
67 83 {
68 - static $bookingFields = null;
84 + static $bookingFields = [];
69 85
70 - if ($cached && $bookingFields) {
71 - return $bookingFields;
86 + if ($cached && isset($bookingFields[$calendarSlot->id])) {
87 + return $bookingFields[$calendarSlot->id];
72 88 }
73 89
74 90 $requiredIndexes = ['name', 'email', 'message', 'cancellation_reason', 'rescheduling_reason'];
75 91
@@ -241,11 +257,9 @@
241 257 }
242 258
243 259 $existingFields['email']['disabled'] = false;
244 260
245 - $bookingFields = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
246 -
247 - return $bookingFields;
261 + return $bookingFields[$calendarSlot->id] = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
248 262 }
249 263
250 264 public static function getBookingFieldLabels(CalendarSlot $calendarSlot, $enabledOnly = false)
251 265 {
@@ -321,8 +335,13 @@
321 335
322 336 if ($fieldType == 'hidden') {
323 337 if ($isPublic) continue;
324 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 + }
325 344 }
326 345
327 346 $formattedData[$dataKey] = [
328 347 'label' => $label,
@@ -433,6 +452,73 @@
433 452 }
434 453 }
435 454 }
436 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;
437 523 }
438 524 }