| @@ -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 | } |