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

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

507 lines 19.1 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\SanitizeService;
8 use FluentBooking\Framework\Support\Arr;
9
10 class BookingFieldService
11 {
12 public static function getCustomFieldsData($postedData, CalendarSlot $slot)
13 {
14 $customFields = self::getCustomFields($slot, true);
15
16 $errors = [];
17
18 $formattedValues = [];
19
20 foreach ($customFields as $fieldKey => $customField) {
21 $value = wp_unslash(Arr::get($postedData, $fieldKey));
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 }
30 }
31
32 if (is_array($value)) {
33 if ($customField['type'] === 'multi-select') {
34 $value = array_map(function ($item) {
35 $val = is_array($item) ? Arr::get($item, 'value') : $item;
36 return sanitize_text_field($val);
37 },$value);
38 } else if ($customField['type'] === 'file') {
39 $maxField = Arr::get($customField, 'max_file_allow', 1);
40 $value = array_slice($value, 0, $maxField);
41 $value = array_map('sanitize_text_field', $value);
42 } else {
43 $value = array_map('sanitize_text_field', $value);
44 }
45 } else if ($customField['type'] == 'textarea') {
46 $value = sanitize_textarea_field($value);
47 } else {
48 $value = sanitize_text_field($value);
49 }
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
57 $formattedValues[$fieldKey] = $value;
58 }
59
60 if ($errors) {
61 return new \WP_Error('required_field', __('Please fill up the required data', 'fluent-booking'), $errors);
62 }
63
64 return $formattedValues;
65 }
66
67 public static function getBookingFields(CalendarSlot $calendarSlot, $cached = false)
68 {
69 static $bookingFields = null;
70
71 if ($cached && $bookingFields) {
72 return $bookingFields;
73 }
74
75 $requiredIndexes = ['name', 'email', 'message', 'cancellation_reason', 'rescheduling_reason'];
76
77 $defaultFields = [
78 'name' => [
79 'index' => 1,
80 'type' => 'text',
81 'name' => 'name',
82 'label' => __('Your Name', 'fluent-booking'),
83 'required' => true,
84 'enabled' => true,
85 'system_defined' => true,
86 'disable_alter' => true,
87 'is_visible' => true,
88 'placeholder' => __('Your Name', 'fluent-booking'),
89 'help_text' => ''
90 ],
91 'email' => [
92 'index' => 2,
93 'type' => 'email',
94 'name' => 'email',
95 'label' => __('Your Email', 'fluent-booking'),
96 'required' => true,
97 'enabled' => true,
98 'system_defined' => true,
99 'disable_alter' => true,
100 'is_visible' => true,
101 'placeholder' => __('Your Email', 'fluent-booking'),
102 'help_text' => ''
103 ],
104 'message' => [
105 'index' => 3,
106 'type' => 'textarea',
107 'name' => 'message',
108 'label' => __('What is this meeting about?', 'fluent-booking'),
109 'required' => false,
110 'enabled' => true,
111 'system_defined' => true,
112 'disable_alter' => false,
113 'help_text' => ''
114 ],
115 'cancellation_reason' => [
116 'index' => 4,
117 'type' => 'textarea',
118 'name' => 'cancellation_reason',
119 'label' => __('Reason for cancellation', 'fluent-booking'),
120 'placeholder' => __('Why are you cancelling?', 'fluent-booking'),
121 'required' => true,
122 'enabled' => true,
123 'system_defined' => true,
124 'disable_alter' => false,
125 'help_text' => ''
126 ],
127 'rescheduling_reason' => [
128 'index' => 5,
129 'type' => 'textarea',
130 'name' => 'rescheduling_reason',
131 'label' => __('Reason for reschedule', 'fluent-booking'),
132 'placeholder' => __('Let others know why you need to reschedule', 'fluent-booking'),
133 'required' => true,
134 'enabled' => true,
135 'system_defined' => true,
136 'disable_alter' => false,
137 'help_text' => ''
138 ],
139 ];
140
141 if ($calendarSlot->isGuestFieldRequired()) {
142 $requiredIndexes[] = 'guests';
143 $defaultFields['guests'] = [
144 'index' => 6,
145 'type' => 'multi-guests',
146 'name' => 'guests',
147 'label' => __('Additional Guests', 'fluent-booking'),
148 'limit' => 10,
149 'required' => false,
150 'enabled' => false,
151 'system_defined' => true,
152 'disable_alter' => false
153 ];
154 }
155
156 if ($calendarSlot->isLocationFieldRequired()) {
157 $requiredIndexes[] = 'location';
158 $defaultFields['location'] = [
159 'index' => 7,
160 'type' => 'radio',
161 'name' => 'location',
162 'label' => __('Location', 'fluent-booking'),
163 'options' => LocationService::getLocationOptions($calendarSlot),
164 'required' => true,
165 'enabled' => true,
166 'system_defined' => true,
167 'disable_alter' => true,
168 'placeholder' => esc_attr__('Location', 'fluent-booking')
169 ];
170 } else if ($calendarSlot->isPhoneRequired()) {
171 $requiredIndexes[] = 'phone_number';
172 $defaultFields['phone_number'] = [
173 'index' => 8,
174 'type' => 'phone',
175 'name' => 'phone_number',
176 'label' => __('Your Phone Number', 'fluent-booking'),
177 'required' => true,
178 'enabled' => true,
179 'system_defined' => true,
180 'disable_alter' => true,
181 'is_sms_number' => true,
182 'help_text' => ''
183 ];
184 } else if ($calendarSlot->isAddressRequired()) {
185 $requiredIndexes[] = 'address';
186 $defaultFields['address'] = [
187 'index' => 9,
188 'type' => 'text',
189 'name' => 'address',
190 'label' => __('Your Address', 'fluent-booking'),
191 'required' => true,
192 'enabled' => true,
193 'system_defined' => true,
194 'disable_alter' => true,
195 'placeholder' => esc_attr__('Address', 'fluent-booking'),
196 'help_text' => ''
197 ];
198 }
199
200 $dbFields = $calendarSlot->getMeta('booking_fields', []);
201
202 if (!$dbFields) {
203 $dbFields = array_values($defaultFields);
204 }
205
206 $existingFields = [];
207 foreach ($dbFields as $dbField) {
208 $name = $dbField['name'];
209 $existingFields[$name] = $dbField;
210 }
211
212 if (empty($defaultFields['location'])) {
213 unset($existingFields['location']);
214 } else {
215 if (empty($existingFields['location'])) {
216 $existingFields['location'] = $defaultFields['location'];
217 } else {
218 $existingFields['location']['options'] = $defaultFields['location']['options'];
219 }
220 }
221
222 if (empty($defaultFields['phone_number'])) {
223 unset($existingFields['phone_number']);
224 }
225
226 if (empty($defaultFields['address'])) {
227 unset($existingFields['address']);
228 }
229
230 foreach ($requiredIndexes as $index) {
231 if (empty($existingFields[$index]) && !empty($defaultFields[$index])) {
232 $existingFields[$index] = $defaultFields[$index];
233 }
234 }
235
236 $paymentField = apply_filters('fluent_booking/payment_booking_field', [], $calendarSlot, $existingFields);
237
238 if (!$paymentField) {
239 unset($existingFields['payment_method']);
240 } else {
241 $existingFields['payment_method'] = $paymentField;
242 }
243
244 $existingFields['email']['disabled'] = false;
245
246 $bookingFields = apply_filters('fluent_booking/booking_fields', array_values($existingFields), $calendarSlot);
247
248 return $bookingFields;
249 }
250
251 public static function getBookingFieldLabels(CalendarSlot $calendarSlot, $enabledOnly = false)
252 {
253 $fields = self::getBookingFields($calendarSlot, true);
254
255 $labels = [];
256 foreach ($fields as $field) {
257 if ($enabledOnly && !Arr::isTrue($field, 'enabled')) {
258 continue;
259 }
260
261 $labels[$field['name']] = $field['label'];
262 }
263
264 return $labels;
265 }
266
267 public static function generateFieldName($calendarEvent, $fieldLabel)
268 {
269 $fieldLabel = preg_replace('/[^A-Za-z0-9]/', '_', $fieldLabel);
270 $fieldName = 'custom_' . strtolower($fieldLabel);
271 $bookingFields = self::getBookingFields($calendarEvent);
272
273 $matched = 0;
274 foreach ($bookingFields as $field) {
275 if (strpos($field['name'], $fieldName) !== false) {
276 $matched++;
277 }
278 }
279
280 if ($matched) {
281 $fieldName .= '_' . $matched;
282 }
283 return $fieldName;
284 }
285
286 public static function maybeGenerateFieldName($calendarEvent, $fieldValue)
287 {
288 $bookingFields = self::getBookingFields($calendarEvent);
289
290 foreach ($bookingFields as $field) {
291 if ($field['name'] == $fieldValue['name'] && $field['index'] != $fieldValue['index']) {
292 return self::generateFieldName($calendarEvent, $fieldValue['label']);
293 }
294 }
295
296 return sanitize_text_field($fieldValue['name']);
297 }
298
299 public static function getFormattedCustomBookingData(Booking $booking, $htmlSupport = true, $isPublic = false)
300 {
301 $customFormData = $booking->getMeta('custom_fields_data', []);
302 if (!$customFormData) {
303 return [];
304 }
305
306 $labels = self::getBookingFieldLabels($booking->calendar_event);
307
308 $formattedData = [];
309
310 foreach ($customFormData as $dataKey => $value) {
311 $label = $labels[$dataKey] ?? $dataKey;
312
313 $formattedValue = is_array($value) ? implode(', ', $value) : $value;
314
315 $field = self::getBookingFieldByName($booking->calendar_event, $dataKey);
316
317 $fieldType = Arr::get($field, 'type');
318
319 if ($fieldType == 'file' && is_array($value)) {
320 $formattedValue = self::getUploadedFiles($value, $htmlSupport);
321 }
322
323 if ($fieldType == 'hidden') {
324 if ($isPublic) continue;
325 $formattedValue = EditorShortcodeParser::parse($formattedValue, $booking);
326 }
327
328 $formattedData[$dataKey] = [
329 'label' => $label,
330 'type' => $fieldType,
331 'value' => $formattedValue
332 ];
333 }
334
335 return $formattedData;
336 }
337
338 public static function getCustomFields($calendarEvent, $withConfig = false)
339 {
340 $existingFields = $calendarEvent->getMeta('booking_fields', []);
341
342 if (!$existingFields) {
343 return [];
344 }
345
346 $customFields = [];
347
348 foreach ($existingFields as $existingField) {
349 if (Arr::get($existingField, 'system_defined') || !Arr::isTrue($existingField, 'enabled')) {
350 continue;
351 }
352 if ($withConfig) {
353 $customFields[$existingField['name']] = $existingField;
354 } else {
355 $customFields[$existingField['name']] = $existingField['label'];
356 }
357 }
358
359 return $customFields;
360 }
361
362 public static function getBookingFieldByName($calendarEvent, $name)
363 {
364 $fields = self::getBookingFields($calendarEvent, true);
365
366 foreach ($fields as $field) {
367 if (Arr::get($field, 'name') == $name) {
368 return $field;
369 }
370 }
371
372 return [];
373 }
374
375 public static function hasPhoneNumberField($fields)
376 {
377 if(!$fields) {
378 return false;
379 }
380
381 foreach ($fields as $field) {
382 if ($field['type'] == 'phone') {
383 return true;
384 } else if ($field['name'] == 'location') {
385 if (!empty($field['options'])) {
386 foreach ($field['options'] as $option) {
387 if (Arr::get($option, 'type') == 'phone_guest') {
388 return true;
389 }
390 }
391 }
392 }
393 }
394 return false;
395 }
396
397 public static function getUploadedFiles($fieldValue, $htmlSupport = true)
398 {
399 if (empty($fieldValue)) {
400 return '';
401 }
402
403 $files = array_map(function($file) use ($htmlSupport) {
404 if ($htmlSupport) {
405 return '<a href="' . esc_url($file) . '" target="_blank" download="' . esc_attr(basename($file)) . '">' . esc_html(basename($file)) . '</a>';
406 }
407 return $file;
408 }, $fieldValue);
409
410 $separator = $htmlSupport ? '<br>' : PHP_EOL;
411
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;
505 }
506 }
507