| @@ -3,14 +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; | |
| 7 | 8 | use FluentBooking\App\Services\SanitizeService; |
| 8 | 9 | use FluentBooking\Framework\Support\Arr; |
| 9 | 10 | |
| 10 | 11 | class BookingFieldService |
| 11 | 12 | { |
| 12 | - 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) | |
| 13 | 17 | { |
| 14 | 18 | $customFields = self::getCustomFields($slot, true); |
| 15 | 19 | |
| 16 | 20 | $errors = []; |
| @@ -17,10 +21,25 @@ | ||
| 17 | 21 | |
| 18 | 22 | $formattedValues = []; |
| 19 | 23 | |
| 20 | 24 | foreach ($customFields as $fieldKey => $customField) { |
| 25 | + $isMapped = $mappedKeys !== null; | |
| 26 | + | |
| 27 | + if ($isMapped && !in_array($fieldKey, $mappedKeys, true)) { | |
| 28 | + continue; | |
| 29 | + } | |
| 30 | + | |
| 21 | 31 | $value = wp_unslash(Arr::get($postedData, $fieldKey)); |
| 22 | - 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')) { | |
| 23 | 42 | $isTerms = $customField['type'] === 'terms-and-conditions'; |
| 24 | 43 | $isCheckbox = $customField['type'] === 'checkbox'; |
| 25 | 44 | if (!$value || ($isCheckbox && $value !== 'Yes') || ($isTerms && $value !== 'Accepted')) { |
| 26 | 45 | /* translators: %s: Field label */ |
| @@ -34,12 +53,8 @@ | ||
| 34 | 53 | $value = array_map(function ($item) { |
| 35 | 54 | $val = is_array($item) ? Arr::get($item, 'value') : $item; |
| 36 | 55 | return sanitize_text_field($val); |
| 37 | 56 | },$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); | |
| 42 | 57 | } else { |
| 43 | 58 | $value = array_map('sanitize_text_field', $value); |
| 44 | 59 | } |
| 45 | 60 | } else if ($customField['type'] == 'textarea') { |
| @@ -47,9 +62,9 @@ | ||
| 47 | 62 | } else { |
| 48 | 63 | $value = sanitize_text_field($value); |
| 49 | 64 | } |
| 50 | 65 | |
| 51 | - if ($customField['type'] === 'phone' && $value && !Helper::isValidPhoneNumber($value)) { | |
| 66 | + if (!$isMapped && $customField['type'] === 'phone' && $value && !Helper::isValidPhoneNumber($value)) { | |
| 52 | 67 | /* translators: %s: Field label */ |
| 53 | 68 | $errors[$fieldKey . '.valid_phone_number'] = sprintf(__('%s is not a valid phone number', 'fluent-booking'), $customField['label']); |
| 54 | 69 | continue; |
| 55 | 70 | } |
| @@ -65,12 +80,12 @@ | ||
| 65 | 80 | } |
| 66 | 81 | |
| 67 | 82 | public static function getBookingFields(CalendarSlot $calendarSlot, $cached = false) |
| 68 | 83 | { |
| 69 | - static $bookingFields = null; | |
| 84 | + static $bookingFields = []; | |
| 70 | 85 | |
| 71 | - if ($cached && $bookingFields) { | |
| 72 | - return $bookingFields; | |
| 86 | + if ($cached && isset($bookingFields[$calendarSlot->id])) { | |
| 87 | + return $bookingFields[$calendarSlot->id]; | |
| 73 | 88 | } |
| 74 | 89 | |
| 75 | 90 | $requiredIndexes = ['name', 'email', 'message', 'cancellation_reason', 'rescheduling_reason']; |
| 76 | 91 | |
| @@ -242,11 +257,9 @@ | ||
| 242 | 257 | } |
| 243 | 258 | |
| 244 | 259 | $existingFields['email']['disabled'] = false; |
| 245 | 260 | |
| 246 | - $bookingFields = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot); | |
| 247 | - | |
| 248 | - return $bookingFields; | |
| 261 | + return $bookingFields[$calendarSlot->id] = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot); | |
| 249 | 262 | } |
| 250 | 263 | |
| 251 | 264 | public static function getBookingFieldLabels(CalendarSlot $calendarSlot, $enabledOnly = false) |
| 252 | 265 | { |
| @@ -322,8 +335,13 @@ | ||
| 322 | 335 | |
| 323 | 336 | if ($fieldType == 'hidden') { |
| 324 | 337 | if ($isPublic) continue; |
| 325 | 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 | + } | |
| 326 | 344 | } |
| 327 | 345 | |
| 328 | 346 | $formattedData[$dataKey] = [ |
| 329 | 347 | 'label' => $label, |