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
fluent-booking / app / Services / BookingFieldService.php

BookingFieldService.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.5.0, at app/Services/BookingFieldService.php

525 lines 20.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBooking\App\Services;
4
5 use FluentBooking\App\Models\Booking;
6 use FluentBooking\App\Models\CalendarSlot;
7 use FluentBooking\App\Services\Libs\FileSystem;
8 use FluentBooking\App\Services\SanitizeService;
9 use FluentBooking\Framework\Support\Arr;
10
11 class BookingFieldService
12 {
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)
17 {
18 $customFields = self::getCustomFields($slot, true);
19
20 $errors = [];
21
22 $formattedValues = [];
23
24 foreach ($customFields as $fieldKey => $customField) {
25 $isMapped = $mappedKeys !== null;
26
27 if ($isMapped && !in_array($fieldKey, $mappedKeys, true)) {
28 continue;
29 }
30
31 $value = wp_unslash(Arr::get($postedData, $fieldKey));
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 */
46 $errors[$fieldKey . '.required'] = sprintf(__('%s is required', 'fluent-booking'), $customField['label']);
47 continue;
48 }
49 }
50
51 if (is_array($value)) {
52 if ($customField['type'] === 'multi-select') {
53 $value = array_map(function ($item) {
54 $val = is_array($item) ? Arr::get($item, 'value') : $item;
55 return sanitize_text_field($val);
56 },$value);
57 } else {
58 $value = array_map('sanitize_text_field', $value);
59 }
60 } else if ($customField['type'] == 'textarea') {
61 $value = sanitize_textarea_field($value);
62 } else {
63 $value = sanitize_text_field($value);
64 }
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
72 $formattedValues[$fieldKey] = $value;
73 }
74
75 if ($errors) {
76 return new \WP_Error('required_field', __('Please fill up the required data', 'fluent-booking'), $errors);
77 }
78
79 return $formattedValues;
80 }
81
82 public static function getBookingFields(CalendarSlot $calendarSlot, $cached = false)
83 {
84 static $bookingFields = [];
85
86 if ($cached && isset($bookingFields[$calendarSlot->id])) {
87 return $bookingFields[$calendarSlot->id];
88 }
89
90 $requiredIndexes = ['name', 'email', 'message', 'cancellation_reason', 'rescheduling_reason'];
91
92 $defaultFields = [
93 'name' => [
94 'index' => 1,
95 'type' => 'text',
96 'name' => 'name',
97 'label' => __('Your Name', 'fluent-booking'),
98 'required' => true,
99 'enabled' => true,
100 'system_defined' => true,
101 'disable_alter' => true,
102 'is_visible' => true,
103 'placeholder' => __('Your Name', 'fluent-booking'),
104 'help_text' => ''
105 ],
106 'email' => [
107 'index' => 2,
108 'type' => 'email',
109 'name' => 'email',
110 'label' => __('Your Email', 'fluent-booking'),
111 'required' => true,
112 'enabled' => true,
113 'system_defined' => true,
114 'disable_alter' => true,
115 'is_visible' => true,
116 'placeholder' => __('Your Email', 'fluent-booking'),
117 'help_text' => ''
118 ],
119 'message' => [
120 'index' => 3,
121 'type' => 'textarea',
122 'name' => 'message',
123 'label' => __('What is this meeting about?', 'fluent-booking'),
124 'required' => false,
125 'enabled' => true,
126 'system_defined' => true,
127 'disable_alter' => false,
128 'help_text' => ''
129 ],
130 'cancellation_reason' => [
131 'index' => 4,
132 'type' => 'textarea',
133 'name' => 'cancellation_reason',
134 'label' => __('Reason for cancellation', 'fluent-booking'),
135 'placeholder' => __('Why are you cancelling?', 'fluent-booking'),
136 'required' => true,
137 'enabled' => true,
138 'system_defined' => true,
139 'disable_alter' => false,
140 'help_text' => ''
141 ],
142 'rescheduling_reason' => [
143 'index' => 5,
144 'type' => 'textarea',
145 'name' => 'rescheduling_reason',
146 'label' => __('Reason for reschedule', 'fluent-booking'),
147 'placeholder' => __('Let others know why you need to reschedule', 'fluent-booking'),
148 'required' => true,
149 'enabled' => true,
150 'system_defined' => true,
151 'disable_alter' => false,
152 'help_text' => ''
153 ],
154 ];
155
156 if ($calendarSlot->isGuestFieldRequired()) {
157 $requiredIndexes[] = 'guests';
158 $defaultFields['guests'] = [
159 'index' => 6,
160 'type' => 'multi-guests',
161 'name' => 'guests',
162 'label' => __('Additional Guests', 'fluent-booking'),
163 'limit' => 10,
164 'required' => false,
165 'enabled' => false,
166 'system_defined' => true,
167 'disable_alter' => false
168 ];
169 }
170
171 if ($calendarSlot->isLocationFieldRequired()) {
172 $requiredIndexes[] = 'location';
173 $defaultFields['location'] = [
174 'index' => 7,
175 'type' => 'radio',
176 'name' => 'location',
177 'label' => __('Location', 'fluent-booking'),
178 'options' => LocationService::getLocationOptions($calendarSlot),
179 'required' => true,
180 'enabled' => true,
181 'system_defined' => true,
182 'disable_alter' => true,
183 'placeholder' => esc_attr__('Location', 'fluent-booking')
184 ];
185 } else if ($calendarSlot->isPhoneRequired()) {
186 $requiredIndexes[] = 'phone_number';
187 $defaultFields['phone_number'] = [
188 'index' => 8,
189 'type' => 'phone',
190 'name' => 'phone_number',
191 'label' => __('Your Phone Number', 'fluent-booking'),
192 'required' => true,
193 'enabled' => true,
194 'system_defined' => true,
195 'disable_alter' => true,
196 'is_sms_number' => true,
197 'help_text' => ''
198 ];
199 } else if ($calendarSlot->isAddressRequired()) {
200 $requiredIndexes[] = 'address';
201 $defaultFields['address'] = [
202 'index' => 9,
203 'type' => 'text',
204 'name' => 'address',
205 'label' => __('Your Address', 'fluent-booking'),
206 'required' => true,
207 'enabled' => true,
208 'system_defined' => true,
209 'disable_alter' => true,
210 'placeholder' => esc_attr__('Address', 'fluent-booking'),
211 'help_text' => ''
212 ];
213 }
214
215 $dbFields = $calendarSlot->getMeta('booking_fields', []);
216
217 if (!$dbFields) {
218 $dbFields = array_values($defaultFields);
219 }
220
221 $existingFields = [];
222 foreach ($dbFields as $dbField) {
223 $name = $dbField['name'];
224 $existingFields[$name] = $dbField;
225 }
226
227 if (empty($defaultFields['location'])) {
228 unset($existingFields['location']);
229 } else {
230 if (empty($existingFields['location'])) {
231 $existingFields['location'] = $defaultFields['location'];
232 } else {
233 $existingFields['location']['options'] = $defaultFields['location']['options'];
234 }
235 }
236
237 if (empty($defaultFields['phone_number'])) {
238 unset($existingFields['phone_number']);
239 }
240
241 if (empty($defaultFields['address'])) {
242 unset($existingFields['address']);
243 }
244
245 foreach ($requiredIndexes as $index) {
246 if (empty($existingFields[$index]) && !empty($defaultFields[$index])) {
247 $existingFields[$index] = $defaultFields[$index];
248 }
249 }
250
251 $paymentField = apply_filters('fluent_booking/payment_booking_field', [], $calendarSlot, $existingFields);
252
253 if (!$paymentField) {
254 unset($existingFields['payment_method']);
255 } else {
256 $existingFields['payment_method'] = $paymentField;
257 }
258
259 $existingFields['email']['disabled'] = false;
260
261 return $bookingFields[$calendarSlot->id] = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
262 }
263
264 public static function getBookingFieldLabels(CalendarSlot $calendarSlot, $enabledOnly = false)
265 {
266 $fields = self::getBookingFields($calendarSlot, true);
267
268 $labels = [];
269 foreach ($fields as $field) {
270 if ($enabledOnly && !Arr::isTrue($field, 'enabled')) {
271 continue;
272 }
273
274 $labels[$field['name']] = $field['label'];
275 }
276
277 return $labels;
278 }
279
280 public static function generateFieldName($calendarEvent, $fieldLabel)
281 {
282 $fieldLabel = preg_replace('/[^A-Za-z0-9]/', '_', $fieldLabel);
283 $fieldName = 'custom_' . strtolower($fieldLabel);
284 $bookingFields = self::getBookingFields($calendarEvent);
285
286 $matched = 0;
287 foreach ($bookingFields as $field) {
288 if (strpos($field['name'], $fieldName) !== false) {
289 $matched++;
290 }
291 }
292
293 if ($matched) {
294 $fieldName .= '_' . $matched;
295 }
296 return $fieldName;
297 }
298
299 public static function maybeGenerateFieldName($calendarEvent, $fieldValue)
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 {
314 $customFormData = $booking->getMeta('custom_fields_data', []);
315 if (!$customFormData) {
316 return [];
317 }
318
319 $labels = self::getBookingFieldLabels($booking->calendar_event);
320
321 $formattedData = [];
322
323 foreach ($customFormData as $dataKey => $value) {
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);
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
346 $formattedData[$dataKey] = [
347 'label' => $label,
348 'type' => $fieldType,
349 'value' => $formattedValue
350 ];
351 }
352
353 return $formattedData;
354 }
355
356 public static function getCustomFields($calendarEvent, $withConfig = false)
357 {
358 $existingFields = $calendarEvent->getMeta('booking_fields', []);
359
360 if (!$existingFields) {
361 return [];
362 }
363
364 $customFields = [];
365
366 foreach ($existingFields as $existingField) {
367 if (Arr::get($existingField, 'system_defined') || !Arr::isTrue($existingField, 'enabled')) {
368 continue;
369 }
370 if ($withConfig) {
371 $customFields[$existingField['name']] = $existingField;
372 } else {
373 $customFields[$existingField['name']] = $existingField['label'];
374 }
375 }
376
377 return $customFields;
378 }
379
380 public static function getBookingFieldByName($calendarEvent, $name)
381 {
382 $fields = self::getBookingFields($calendarEvent, true);
383
384 foreach ($fields as $field) {
385 if (Arr::get($field, 'name') == $name) {
386 return $field;
387 }
388 }
389
390 return [];
391 }
392
393 public static function hasPhoneNumberField($fields)
394 {
395 if(!$fields) {
396 return false;
397 }
398
399 foreach ($fields as $field) {
400 if ($field['type'] == 'phone') {
401 return true;
402 } else if ($field['name'] == 'location') {
403 if (!empty($field['options'])) {
404 foreach ($field['options'] as $option) {
405 if (Arr::get($option, 'type') == 'phone_guest') {
406 return true;
407 }
408 }
409 }
410 }
411 }
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;
523 }
524 }
525