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 +152 -19 1.5.22 → 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,24 +21,40 @@
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 (!$value && Arr::isTrue($customField, 'required')) {
22 - // translators: %s is the label of the required field
23 - $errors[$fieldKey . '.required'] = sprintf(__('%s is required', 'fluent-booking'), $customField['label']);
24 - continue;
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']));
25 39 }
26 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 */
46 + $errors[$fieldKey . '.required'] = sprintf(__('%s is required', 'fluent-booking'), $customField['label']);
47 + continue;
48 + }
49 + }
50 +
27 51 if (is_array($value)) {
28 52 if ($customField['type'] === 'multi-select') {
29 53 $value = array_map(function ($item) {
30 - return sanitize_text_field(Arr::get($item, 'value'));
54 + $val = is_array($item) ? Arr::get($item, 'value') : $item;
55 + return sanitize_text_field($val);
31 56 },$value);
32 - } else if ($customField['type'] === 'file') {
33 - $maxField = Arr::get($customField, 'max_file_allow', 1);
34 - $value = array_slice($value, 0, $maxField);
35 - $value = array_map('sanitize_text_field', $value);
36 57 } else {
37 58 $value = array_map('sanitize_text_field', $value);
38 59 }
39 60 } else if ($customField['type'] == 'textarea') {
@@ -41,8 +62,14 @@
41 62 } else {
42 63 $value = sanitize_text_field($value);
43 64 }
44 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 +
45 72 $formattedValues[$fieldKey] = $value;
46 73 }
47 74
48 75 if ($errors) {
@@ -51,10 +78,16 @@
51 78
52 79 return $formattedValues;
53 80 }
54 81
55 - public static function getBookingFields(CalendarSlot $calendarSlot)
82 + public static function getBookingFields(CalendarSlot $calendarSlot, $cached = false)
56 83 {
84 + static $bookingFields = [];
85 +
86 + if ($cached && isset($bookingFields[$calendarSlot->id])) {
87 + return $bookingFields[$calendarSlot->id];
88 + }
89 +
57 90 $requiredIndexes = ['name', 'email', 'message', 'cancellation_reason', 'rescheduling_reason'];
58 91
59 92 $defaultFields = [
60 93 'name' => [
@@ -133,8 +166,9 @@
133 166 'system_defined' => true,
134 167 'disable_alter' => false
135 168 ];
136 169 }
170 +
137 171 if ($calendarSlot->isLocationFieldRequired()) {
138 172 $requiredIndexes[] = 'location';
139 173 $defaultFields['location'] = [
140 174 'index' => 7,
@@ -223,16 +257,16 @@
223 257 }
224 258
225 259 $existingFields['email']['disabled'] = false;
226 260
227 - return array_values($existingFields);
261 + return $bookingFields[$calendarSlot->id] = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
228 262 }
229 263
230 264 public static function getBookingFieldLabels(CalendarSlot $calendarSlot, $enabledOnly = false)
231 265 {
232 - $fields = self::getBookingFields($calendarSlot);
266 + $fields = self::getBookingFields($calendarSlot, true);
267 +
233 268 $labels = [];
234 -
235 269 foreach ($fields as $field) {
236 270 if ($enabledOnly && !Arr::isTrue($field, 'enabled')) {
237 271 continue;
238 272 }
@@ -244,9 +278,10 @@
244 278 }
245 279
246 280 public static function generateFieldName($calendarEvent, $fieldLabel)
247 281 {
248 - $fieldName = 'custom_' . sanitize_title($fieldLabel);
282 + $fieldLabel = preg_replace('/[^A-Za-z0-9]/', '_', $fieldLabel);
283 + $fieldName = 'custom_' . strtolower($fieldLabel);
249 284 $bookingFields = self::getBookingFields($calendarEvent);
250 285
251 286 $matched = 0;
252 287 foreach ($bookingFields as $field) {
@@ -286,11 +321,11 @@
286 321 $formattedData = [];
287 322
288 323 foreach ($customFormData as $dataKey => $value) {
289 324 $label = $labels[$dataKey] ?? $dataKey;
290 -
325 +
291 326 $formattedValue = is_array($value) ? implode(', ', $value) : $value;
292 -
327 +
293 328 $field = self::getBookingFieldByName($booking->calendar_event, $dataKey);
294 329
295 330 $fieldType = Arr::get($field, 'type');
296 331
@@ -296,16 +331,22 @@
296 331
297 332 if ($fieldType == 'file' && is_array($value)) {
298 333 $formattedValue = self::getUploadedFiles($value, $htmlSupport);
299 334 }
300 -
335 +
301 336 if ($fieldType == 'hidden') {
302 337 if ($isPublic) continue;
303 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 + }
304 344 }
305 345
306 346 $formattedData[$dataKey] = [
307 347 'label' => $label,
348 + 'type' => $fieldType,
308 349 'value' => $formattedValue
309 350 ];
310 351 }
311 352
@@ -337,9 +378,9 @@
337 378 }
338 379
339 380 public static function getBookingFieldByName($calendarEvent, $name)
340 381 {
341 - $fields = self::getBookingFields($calendarEvent);
382 + $fields = self::getBookingFields($calendarEvent, true);
342 383
343 384 foreach ($fields as $field) {
344 385 if (Arr::get($field, 'name') == $name) {
345 386 return $field;
@@ -386,6 +427,98 @@
386 427
387 428 $separator = $htmlSupport ? '<br>' : PHP_EOL;
388 429
389 430 return implode($separator, $files);
431 + }
432 +
433 + public static function validateDateFields($customFieldsData, $calendarEvent)
434 + {
435 + foreach ($customFieldsData as $fieldKey => $fieldValue) {
436 + $field = self::getBookingFieldByName($calendarEvent, $fieldKey);
437 + if ($fieldValue && Arr::get($field, 'type') == 'date') {
438 + $minDate = Arr::get($field, 'min_date');
439 + $maxDate = Arr::get($field, 'max_date');
440 +
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')));
444 +
445 + if ($minDate && $fieldValue < $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));
448 + }
449 + if ($maxDate && $fieldValue > $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));
452 + }
453 + }
454 + }
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;
390 523 }
391 524 }