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 +110 -16 1.10.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 */
@@ -30,14 +50,11 @@
30 50
31 51 if (is_array($value)) {
32 52 if ($customField['type'] === 'multi-select') {
33 53 $value = array_map(function ($item) {
34 - return sanitize_text_field(Arr::get($item, 'value'));
54 + $val = is_array($item) ? Arr::get($item, 'value') : $item;
55 + return sanitize_text_field($val);
35 56 },$value);
36 - } else if ($customField['type'] === 'file') {
37 - $maxField = Arr::get($customField, 'max_file_allow', 1);
38 - $value = array_slice($value, 0, $maxField);
39 - $value = array_map('sanitize_text_field', $value);
40 57 } else {
41 58 $value = array_map('sanitize_text_field', $value);
42 59 }
43 60 } else if ($customField['type'] == 'textarea') {
@@ -45,8 +62,14 @@
45 62 } else {
46 63 $value = sanitize_text_field($value);
47 64 }
48 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 +
49 72 $formattedValues[$fieldKey] = $value;
50 73 }
51 74
52 75 if ($errors) {
@@ -57,12 +80,12 @@
57 80 }
58 81
59 82 public static function getBookingFields(CalendarSlot $calendarSlot, $cached = false)
60 83 {
61 - static $bookingFields = null;
84 + static $bookingFields = [];
62 85
63 - if ($cached && $bookingFields) {
64 - return $bookingFields;
86 + if ($cached && isset($bookingFields[$calendarSlot->id])) {
87 + return $bookingFields[$calendarSlot->id];
65 88 }
66 89
67 90 $requiredIndexes = ['name', 'email', 'message', 'cancellation_reason', 'rescheduling_reason'];
68 91
@@ -143,8 +166,9 @@
143 166 'system_defined' => true,
144 167 'disable_alter' => false
145 168 ];
146 169 }
170 +
147 171 if ($calendarSlot->isLocationFieldRequired()) {
148 172 $requiredIndexes[] = 'location';
149 173 $defaultFields['location'] = [
150 174 'index' => 7,
@@ -233,11 +257,9 @@
233 257 }
234 258
235 259 $existingFields['email']['disabled'] = false;
236 260
237 - $bookingFields = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
238 -
239 - return $bookingFields;
261 + return $bookingFields[$calendarSlot->id] = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
240 262 }
241 263
242 264 public static function getBookingFieldLabels(CalendarSlot $calendarSlot, $enabledOnly = false)
243 265 {
@@ -299,11 +321,11 @@
299 321 $formattedData = [];
300 322
301 323 foreach ($customFormData as $dataKey => $value) {
302 324 $label = $labels[$dataKey] ?? $dataKey;
303 -
325 +
304 326 $formattedValue = is_array($value) ? implode(', ', $value) : $value;
305 -
327 +
306 328 $field = self::getBookingFieldByName($booking->calendar_event, $dataKey);
307 329
308 330 $fieldType = Arr::get($field, 'type');
309 331
@@ -309,12 +331,17 @@
309 331
310 332 if ($fieldType == 'file' && is_array($value)) {
311 333 $formattedValue = self::getUploadedFiles($value, $htmlSupport);
312 334 }
313 -
335 +
314 336 if ($fieldType == 'hidden') {
315 337 if ($isPublic) continue;
316 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 + }
317 344 }
318 345
319 346 $formattedData[$dataKey] = [
320 347 'label' => $label,
@@ -425,6 +452,73 @@
425 452 }
426 453 }
427 454 }
428 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;
429 523 }
430 524 }