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