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 +200 -23 1.5.01 → 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,23 +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 - $value = array_map(
30 - function ($item) {
31 - return sanitize_text_field(Arr::get($item, 'value'));
32 - },
33 - $value
34 - );
53 + $value = array_map(function ($item) {
54 + $val = is_array($item) ? Arr::get($item, 'value') : $item;
55 + return sanitize_text_field($val);
56 + },$value);
35 57 } else {
36 58 $value = array_map('sanitize_text_field', $value);
37 59 }
38 60 } else if ($customField['type'] == 'textarea') {
@@ -40,8 +62,14 @@
40 62 } else {
41 63 $value = sanitize_text_field($value);
42 64 }
43 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 +
44 72 $formattedValues[$fieldKey] = $value;
45 73 }
46 74
47 75 if ($errors) {
@@ -50,10 +78,16 @@
50 78
51 79 return $formattedValues;
52 80 }
53 81
54 - public static function getBookingFields(CalendarSlot $calendarSlot)
82 + public static function getBookingFields(CalendarSlot $calendarSlot, $cached = false)
55 83 {
84 + static $bookingFields = [];
85 +
86 + if ($cached && isset($bookingFields[$calendarSlot->id])) {
87 + return $bookingFields[$calendarSlot->id];
88 + }
89 +
56 90 $requiredIndexes = ['name', 'email', 'message', 'cancellation_reason', 'rescheduling_reason'];
57 91
58 92 $defaultFields = [
59 93 'name' => [
@@ -132,8 +166,9 @@
132 166 'system_defined' => true,
133 167 'disable_alter' => false
134 168 ];
135 169 }
170 +
136 171 if ($calendarSlot->isLocationFieldRequired()) {
137 172 $requiredIndexes[] = 'location';
138 173 $defaultFields['location'] = [
139 174 'index' => 7,
@@ -222,16 +257,16 @@
222 257 }
223 258
224 259 $existingFields['email']['disabled'] = false;
225 260
226 - return array_values($existingFields);
261 + return $bookingFields[$calendarSlot->id] = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
227 262 }
228 263
229 264 public static function getBookingFieldLabels(CalendarSlot $calendarSlot, $enabledOnly = false)
230 265 {
231 - $fields = self::getBookingFields($calendarSlot);
266 + $fields = self::getBookingFields($calendarSlot, true);
267 +
232 268 $labels = [];
233 -
234 269 foreach ($fields as $field) {
235 270 if ($enabledOnly && !Arr::isTrue($field, 'enabled')) {
236 271 continue;
237 272 }
@@ -243,9 +278,10 @@
243 278 }
244 279
245 280 public static function generateFieldName($calendarEvent, $fieldLabel)
246 281 {
247 - $fieldName = 'custom_' . sanitize_title($fieldLabel);
282 + $fieldLabel = preg_replace('/[^A-Za-z0-9]/', '_', $fieldLabel);
283 + $fieldName = 'custom_' . strtolower($fieldLabel);
248 284 $bookingFields = self::getBookingFields($calendarEvent);
249 285
250 286 $matched = 0;
251 287 foreach ($bookingFields as $field) {
@@ -259,10 +295,23 @@
259 295 }
260 296 return $fieldName;
261 297 }
262 298
263 - public static function getFormattedCustomBookingData(Booking $booking)
299 + public static function maybeGenerateFieldName($calendarEvent, $fieldValue)
264 300 {
301 + $bookingFields = self::getBookingFields($calendarEvent);
302 +
303 + foreach ($bookingFields as $field) {
304 + if ($field['name'] == $fieldValue['name'] && $field['index'] != $fieldValue['index']) {
305 + return self::generateFieldName($calendarEvent, $fieldValue['label']);
306 + }
307 + }
308 +
309 + return sanitize_text_field($fieldValue['name']);
310 + }
311 +
312 + public static function getFormattedCustomBookingData(Booking $booking, $htmlSupport = true, $isPublic = false)
313 + {
265 314 $customFormData = $booking->getMeta('custom_fields_data', []);
266 315 if (!$customFormData) {
267 316 return [];
268 317 }
@@ -271,16 +320,34 @@
271 320
272 321 $formattedData = [];
273 322
274 323 foreach ($customFormData as $dataKey => $value) {
275 - if (isset($labels[$dataKey])) {
276 - $label = $labels[$dataKey];
277 - } else {
278 - $label = $dataKey;
324 + $label = $labels[$dataKey] ?? $dataKey;
325 +
326 + $formattedValue = is_array($value) ? implode(', ', $value) : $value;
327 +
328 + $field = self::getBookingFieldByName($booking->calendar_event, $dataKey);
329 +
330 + $fieldType = Arr::get($field, 'type');
331 +
332 + if ($fieldType == 'file' && is_array($value)) {
333 + $formattedValue = self::getUploadedFiles($value, $htmlSupport);
279 334 }
335 +
336 + if ($fieldType == 'hidden') {
337 + if ($isPublic) continue;
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 + }
344 + }
345 +
280 346 $formattedData[$dataKey] = [
281 347 'label' => $label,
282 - 'value' => is_array($value) ? implode(', ', $value) : $value
348 + 'type' => $fieldType,
349 + 'value' => $formattedValue
283 350 ];
284 351 }
285 352
286 353 return $formattedData;
@@ -311,9 +378,9 @@
311 378 }
312 379
313 380 public static function getBookingFieldByName($calendarEvent, $name)
314 381 {
315 - $fields = self::getBookingFields($calendarEvent);
382 + $fields = self::getBookingFields($calendarEvent, true);
316 383
317 384 foreach ($fields as $field) {
318 385 if (Arr::get($field, 'name') == $name) {
319 386 return $field;
@@ -342,6 +409,116 @@
342 409 }
343 410 }
344 411 }
345 412 return false;
413 + }
414 +
415 + public static function getUploadedFiles($fieldValue, $htmlSupport = true)
416 + {
417 + if (empty($fieldValue)) {
418 + return '';
419 + }
420 +
421 + $files = array_map(function($file) use ($htmlSupport) {
422 + if ($htmlSupport) {
423 + return '<a href="' . esc_url($file) . '" target="_blank" download="' . esc_attr(basename($file)) . '">' . esc_html(basename($file)) . '</a>';
424 + }
425 + return $file;
426 + }, $fieldValue);
427 +
428 + $separator = $htmlSupport ? '<br>' : PHP_EOL;
429 +
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;
346 523 }
347 524 }