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 +124 -24 1.6.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,12 +21,29 @@
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')) {
22 - if (!$value || ($customField['type'] == 'checkbox' && $value != 'Yes')) {
23 - // translators: %s is the label of the required field
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')) {
42 + $isTerms = $customField['type'] === 'terms-and-conditions';
43 + $isCheckbox = $customField['type'] === 'checkbox';
44 + if (!$value || ($isCheckbox && $value !== 'Yes') || ($isTerms && $value !== 'Accepted')) {
45 + /* translators: %s: Field label */
24 46 $errors[$fieldKey . '.required'] = sprintf(__('%s is required', 'fluent-booking'), $customField['label']);
25 47 continue;
26 48 }
27 49 }
@@ -28,14 +50,11 @@
28 50
29 51 if (is_array($value)) {
30 52 if ($customField['type'] === 'multi-select') {
31 53 $value = array_map(function ($item) {
32 - return sanitize_text_field(Arr::get($item, 'value'));
54 + $val = is_array($item) ? Arr::get($item, 'value') : $item;
55 + return sanitize_text_field($val);
33 56 },$value);
34 - } else if ($customField['type'] === 'file') {
35 - $maxField = Arr::get($customField, 'max_file_allow', 1);
36 - $value = array_slice($value, 0, $maxField);
37 - $value = array_map('sanitize_text_field', $value);
38 57 } else {
39 58 $value = array_map('sanitize_text_field', $value);
40 59 }
41 60 } else if ($customField['type'] == 'textarea') {
@@ -43,8 +62,14 @@
43 62 } else {
44 63 $value = sanitize_text_field($value);
45 64 }
46 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 +
47 72 $formattedValues[$fieldKey] = $value;
48 73 }
49 74
50 75 if ($errors) {
@@ -55,12 +80,12 @@
55 80 }
56 81
57 82 public static function getBookingFields(CalendarSlot $calendarSlot, $cached = false)
58 83 {
59 - static $bookingFields = null;
84 + static $bookingFields = [];
60 85
61 - if ($cached && $bookingFields) {
62 - return $bookingFields;
86 + if ($cached && isset($bookingFields[$calendarSlot->id])) {
87 + return $bookingFields[$calendarSlot->id];
63 88 }
64 89
65 90 $requiredIndexes = ['name', 'email', 'message', 'cancellation_reason', 'rescheduling_reason'];
66 91
@@ -141,8 +166,9 @@
141 166 'system_defined' => true,
142 167 'disable_alter' => false
143 168 ];
144 169 }
170 +
145 171 if ($calendarSlot->isLocationFieldRequired()) {
146 172 $requiredIndexes[] = 'location';
147 173 $defaultFields['location'] = [
148 174 'index' => 7,
@@ -231,11 +257,9 @@
231 257 }
232 258
233 259 $existingFields['email']['disabled'] = false;
234 260
235 - $bookingFields = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
236 -
237 - return $bookingFields;
261 + return $bookingFields[$calendarSlot->id] = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
238 262 }
239 263
240 264 public static function getBookingFieldLabels(CalendarSlot $calendarSlot, $enabledOnly = false)
241 265 {
@@ -254,9 +278,10 @@
254 278 }
255 279
256 280 public static function generateFieldName($calendarEvent, $fieldLabel)
257 281 {
258 - $fieldName = 'custom_' . sanitize_title($fieldLabel);
282 + $fieldLabel = preg_replace('/[^A-Za-z0-9]/', '_', $fieldLabel);
283 + $fieldName = 'custom_' . strtolower($fieldLabel);
259 284 $bookingFields = self::getBookingFields($calendarEvent);
260 285
261 286 $matched = 0;
262 287 foreach ($bookingFields as $field) {
@@ -296,11 +321,11 @@
296 321 $formattedData = [];
297 322
298 323 foreach ($customFormData as $dataKey => $value) {
299 324 $label = $labels[$dataKey] ?? $dataKey;
300 -
325 +
301 326 $formattedValue = is_array($value) ? implode(', ', $value) : $value;
302 -
327 +
303 328 $field = self::getBookingFieldByName($booking->calendar_event, $dataKey);
304 329
305 330 $fieldType = Arr::get($field, 'type');
306 331
@@ -306,16 +331,22 @@
306 331
307 332 if ($fieldType == 'file' && is_array($value)) {
308 333 $formattedValue = self::getUploadedFiles($value, $htmlSupport);
309 334 }
310 -
335 +
311 336 if ($fieldType == 'hidden') {
312 337 if ($isPublic) continue;
313 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 + }
314 344 }
315 345
316 346 $formattedData[$dataKey] = [
317 347 'label' => $label,
348 + 'type' => $fieldType,
318 349 'value' => $formattedValue
319 350 ];
320 351 }
321 352
@@ -406,19 +437,88 @@
406 437 if ($fieldValue && Arr::get($field, 'type') == 'date') {
407 438 $minDate = Arr::get($field, 'min_date');
408 439 $maxDate = Arr::get($field, 'max_date');
409 440
410 - $fieldValue = date('Y-m-d', strtotime($fieldValue));
411 - $minDate = date('Y-m-d', strtotime($minDate ?: '1900-01-01'));
412 - $maxDate = date('Y-m-d', strtotime($maxDate ?: date('Y-12-31')));
441 + $fieldValue = DateTimeHelper::getFormattedDate($fieldValue, Arr::get($field, 'date_format'));
442 + $minDate = gmdate('Y-m-d', strtotime($minDate ?: '1900-01-01'));
443 + $maxDate = gmdate('Y-m-d', strtotime($maxDate ?: gmdate('Y-12-31')));
413 444
414 445 if ($minDate && $fieldValue < $minDate) {
415 - return new \WP_Error('invalid_date', sprintf(__('The date for %s cannot be earlier than %s.', 'fluent-booking'), $field['label'], $minDate));
446 + /* translators: %1$s: Field label, %2$s: Minimum date */
447 + return new \WP_Error('invalid_date', sprintf(__('The date for %1$s cannot be earlier than %2$s.', 'fluent-booking'), $field['label'], $minDate));
416 448 }
417 449 if ($maxDate && $fieldValue > $maxDate) {
418 - return new \WP_Error('invalid_date', sprintf(__('The date for %s cannot be later than %s.', 'fluent-booking'), $field['label'], $maxDate));
450 + /* translators: %1$s: Field label, %2$s: Maximum date */
451 + return new \WP_Error('invalid_date', sprintf(__('The date for %1$s cannot be later than %2$s.', 'fluent-booking'), $field['label'], $maxDate));
419 452 }
420 453 }
421 454 }
422 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;
423 523 }
424 524 }