PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / trunk
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution vtrunk
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 1.7.2 All 33 releases
← All changes | app/Services/BookingFieldService.php +129 -14 1.5.24trunk View file →
@@ -3,8 +3,9 @@
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\SanitizeService;
7 8 use FluentBooking\Framework\Support\Arr;
8 9
9 10 class BookingFieldService
10 11 {
@@ -17,18 +18,23 @@
17 18 $formattedValues = [];
18 19
19 20 foreach ($customFields as $fieldKey => $customField) {
20 21 $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;
22 + if (Arr::isTrue($customField, 'required')) {
23 + $isTerms = $customField['type'] === 'terms-and-conditions';
24 + $isCheckbox = $customField['type'] === 'checkbox';
25 + if (!$value || ($isCheckbox && $value !== 'Yes') || ($isTerms && $value !== 'Accepted')) {
26 + /* translators: %s: Field label */
27 + $errors[$fieldKey . '.required'] = sprintf(__('%s is required', 'fluent-booking'), $customField['label']);
28 + continue;
29 + }
25 30 }
26 31
27 32 if (is_array($value)) {
28 33 if ($customField['type'] === 'multi-select') {
29 34 $value = array_map(function ($item) {
30 - return sanitize_text_field(Arr::get($item, 'value'));
35 + $val = is_array($item) ? Arr::get($item, 'value') : $item;
36 + return sanitize_text_field($val);
31 37 },$value);
32 38 } else if ($customField['type'] === 'file') {
33 39 $maxField = Arr::get($customField, 'max_file_allow', 1);
34 40 $value = array_slice($value, 0, $maxField);
@@ -41,8 +47,14 @@
41 47 } else {
42 48 $value = sanitize_text_field($value);
43 49 }
44 50
51 + if ($customField['type'] === 'phone' && $value && !Helper::isValidPhoneNumber($value)) {
52 + /* translators: %s: Field label */
53 + $errors[$fieldKey . '.valid_phone_number'] = sprintf(__('%s is not a valid phone number', 'fluent-booking'), $customField['label']);
54 + continue;
55 + }
56 +
45 57 $formattedValues[$fieldKey] = $value;
46 58 }
47 59
48 60 if ($errors) {
@@ -51,10 +63,16 @@
51 63
52 64 return $formattedValues;
53 65 }
54 66
55 - public static function getBookingFields(CalendarSlot $calendarSlot)
67 + public static function getBookingFields(CalendarSlot $calendarSlot, $cached = false)
56 68 {
69 + static $bookingFields = null;
70 +
71 + if ($cached && $bookingFields) {
72 + return $bookingFields;
73 + }
74 +
57 75 $requiredIndexes = ['name', 'email', 'message', 'cancellation_reason', 'rescheduling_reason'];
58 76
59 77 $defaultFields = [
60 78 'name' => [
@@ -133,8 +151,9 @@
133 151 'system_defined' => true,
134 152 'disable_alter' => false
135 153 ];
136 154 }
155 +
137 156 if ($calendarSlot->isLocationFieldRequired()) {
138 157 $requiredIndexes[] = 'location';
139 158 $defaultFields['location'] = [
140 159 'index' => 7,
@@ -223,16 +242,18 @@
223 242 }
224 243
225 244 $existingFields['email']['disabled'] = false;
226 245
227 - return array_values($existingFields);
246 + $bookingFields = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
247 +
248 + return $bookingFields;
228 249 }
229 250
230 251 public static function getBookingFieldLabels(CalendarSlot $calendarSlot, $enabledOnly = false)
231 252 {
232 - $fields = self::getBookingFields($calendarSlot);
253 + $fields = self::getBookingFields($calendarSlot, true);
254 +
233 255 $labels = [];
234 -
235 256 foreach ($fields as $field) {
236 257 if ($enabledOnly && !Arr::isTrue($field, 'enabled')) {
237 258 continue;
238 259 }
@@ -244,9 +265,10 @@
244 265 }
245 266
246 267 public static function generateFieldName($calendarEvent, $fieldLabel)
247 268 {
248 - $fieldName = 'custom_' . sanitize_title($fieldLabel);
269 + $fieldLabel = preg_replace('/[^A-Za-z0-9]/', '_', $fieldLabel);
270 + $fieldName = 'custom_' . strtolower($fieldLabel);
249 271 $bookingFields = self::getBookingFields($calendarEvent);
250 272
251 273 $matched = 0;
252 274 foreach ($bookingFields as $field) {
@@ -286,11 +308,11 @@
286 308 $formattedData = [];
287 309
288 310 foreach ($customFormData as $dataKey => $value) {
289 311 $label = $labels[$dataKey] ?? $dataKey;
290 -
312 +
291 313 $formattedValue = is_array($value) ? implode(', ', $value) : $value;
292 -
314 +
293 315 $field = self::getBookingFieldByName($booking->calendar_event, $dataKey);
294 316
295 317 $fieldType = Arr::get($field, 'type');
296 318
@@ -296,9 +318,9 @@
296 318
297 319 if ($fieldType == 'file' && is_array($value)) {
298 320 $formattedValue = self::getUploadedFiles($value, $htmlSupport);
299 321 }
300 -
322 +
301 323 if ($fieldType == 'hidden') {
302 324 if ($isPublic) continue;
303 325 $formattedValue = EditorShortcodeParser::parse($formattedValue, $booking);
304 326 }
@@ -304,8 +326,9 @@
304 326 }
305 327
306 328 $formattedData[$dataKey] = [
307 329 'label' => $label,
330 + 'type' => $fieldType,
308 331 'value' => $formattedValue
309 332 ];
310 333 }
311 334
@@ -337,9 +360,9 @@
337 360 }
338 361
339 362 public static function getBookingFieldByName($calendarEvent, $name)
340 363 {
341 - $fields = self::getBookingFields($calendarEvent);
364 + $fields = self::getBookingFields($calendarEvent, true);
342 365
343 366 foreach ($fields as $field) {
344 367 if (Arr::get($field, 'name') == $name) {
345 368 return $field;
@@ -386,6 +409,98 @@
386 409
387 410 $separator = $htmlSupport ? '<br>' : PHP_EOL;
388 411
389 412 return implode($separator, $files);
413 + }
414 +
415 + public static function validateDateFields($customFieldsData, $calendarEvent)
416 + {
417 + foreach ($customFieldsData as $fieldKey => $fieldValue) {
418 + $field = self::getBookingFieldByName($calendarEvent, $fieldKey);
419 + if ($fieldValue && Arr::get($field, 'type') == 'date') {
420 + $minDate = Arr::get($field, 'min_date');
421 + $maxDate = Arr::get($field, 'max_date');
422 +
423 + $fieldValue = DateTimeHelper::getFormattedDate($fieldValue, Arr::get($field, 'date_format'));
424 + $minDate = gmdate('Y-m-d', strtotime($minDate ?: '1900-01-01'));
425 + $maxDate = gmdate('Y-m-d', strtotime($maxDate ?: gmdate('Y-12-31')));
426 +
427 + if ($minDate && $fieldValue < $minDate) {
428 + /* translators: %1$s: Field label, %2$s: Minimum date */
429 + return new \WP_Error('invalid_date', sprintf(__('The date for %1$s cannot be earlier than %2$s.', 'fluent-booking'), $field['label'], $minDate));
430 + }
431 + if ($maxDate && $fieldValue > $maxDate) {
432 + /* translators: %1$s: Field label, %2$s: Maximum date */
433 + return new \WP_Error('invalid_date', sprintf(__('The date for %1$s cannot be later than %2$s.', 'fluent-booking'), $field['label'], $maxDate));
434 + }
435 + }
436 + }
437 + return true;
438 + }
439 +
440 + /**
441 + * Format booking fields (text sanitized, terms-and-conditions kses'd). Shared
442 + * by the admin save and calendar import so neither path can store raw HTML.
443 + * $calendarEvent is null-safe (import skips name generation).
444 + */
445 + public static function sanitizeBookingFields($bookingFields, $calendarEvent = null)
446 + {
447 + if (!is_array($bookingFields)) {
448 + return [];
449 + }
450 +
451 + $optionRequiredFields = ['dropdown', 'radio', 'checkbox-group', 'multi-select'];
452 + $textFields = ['type', 'name', 'label', 'placeholder', 'limit', 'help_text', 'date_format', 'min_date', 'max_date'];
453 + $booleanFields = ['enabled', 'required', 'system_defined', 'disable_alter', 'is_sms_number'];
454 +
455 + $formattedFields = [];
456 +
457 + foreach ($bookingFields as $value) {
458 + if (!is_array($value)) {
459 + continue;
460 + }
461 +
462 + if ($calendarEvent) {
463 + if (empty($value['name'])) {
464 + $value['name'] = self::generateFieldName($calendarEvent, Arr::get($value, 'label', ''));
465 + } else {
466 + $value['name'] = self::maybeGenerateFieldName($calendarEvent, $value);
467 + }
468 + } else {
469 + $value['name'] = sanitize_text_field(Arr::get($value, 'name', ''));
470 + }
471 +
472 + $textValues = array_map('sanitize_text_field', Arr::only($value, $textFields));
473 +
474 + $booleanValues = array_map(function ($valueItem) {
475 + return $valueItem === true || $valueItem === 'true' || $valueItem == 1;
476 + }, Arr::only($value, $booleanFields));
477 +
478 + $formattedField = array_merge($textValues, $booleanValues);
479 +
480 + $fieldType = Arr::get($value, 'type');
481 +
482 + $formattedField['index'] = (int) Arr::get($value, 'index');
483 + if (in_array($fieldType, $optionRequiredFields)) {
484 + $formattedField['options'] = array_map('sanitize_text_field', (array) Arr::get($value, 'options', []));
485 + }
486 + if ($fieldType == 'file') {
487 + $formattedField['max_file_allow'] = intval(Arr::get($value, 'max_file_allow'));
488 + $formattedField['allow_file_types'] = array_map('sanitize_text_field', (array) Arr::get($value, 'allow_file_types', []));
489 + $formattedField['file_size_value'] = intval(Arr::get($value, 'file_size_value'));
490 + $formattedField['file_size_unit'] = SanitizeService::checkCollection(Arr::get($value, 'file_size_unit'), ['kb', 'mb']);
491 + }
492 + if ($fieldType == 'hidden') {
493 + $formattedField['default_value'] = sanitize_text_field(Arr::get($value, 'default_value'));
494 + }
495 + if ($fieldType == 'terms-and-conditions') {
496 + $formattedField['terms_and_conditions'] = wp_kses_post(Arr::get($value, 'terms_and_conditions'));
497 + }
498 +
499 + $formattedField = apply_filters('fluent_booking/save_event_booking_field_' . $fieldType, $formattedField, $value, $calendarEvent);
500 +
501 + $formattedFields[] = $formattedField;
502 + }
503 +
504 + return $formattedFields;
390 505 }
391 506 }