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/Http/Controllers/BookingController.php +177 -41 1.5.0 → 2.5.0 View file →
@@ -5,10 +5,10 @@
5 5 use FluentBooking\App\App;
6 6 use FluentBooking\App\Models\Booking;
7 7 use FluentBooking\App\Models\CalendarSlot;
8 8 use FluentBooking\App\Services\BookingService;
9 +use FluentBooking\App\Services\RescheduleService;
9 10 use FluentBooking\App\Services\DateTimeHelper;
10 -use FluentBooking\App\Services\TimeSlotService;
11 11 use FluentBooking\App\Services\BookingFieldService;
12 12 use FluentBooking\App\Hooks\Handlers\FrontEndHandler;
13 13 use FluentBooking\App\Hooks\Handlers\TimeSlotServiceHandler;
14 14 use FluentBooking\Framework\Http\Request\Request;
@@ -37,9 +37,9 @@
37 37
38 38 $timeSlotService = TimeSlotServiceHandler::initService($calendar, $slot);
39 39
40 40 if (is_wp_error($timeSlotService)) {
41 - return TimeSlotServiceHandler::sendError($timeSlotService, $slot, $timezone);
41 + return TimeSlotServiceHandler::sendError($timeSlotService, $slot, $timeZone);
42 42 }
43 43
44 44 $availableSpots = $timeSlotService->getAvailableSpots($startDate, $timeZone);
45 45
@@ -78,10 +78,10 @@
78 78 'event_time' => 'required'
79 79 ];
80 80
81 81 $messages = [
82 - 'name.required' => __("Please enter attendee's name", 'fluent-booking'),
83 - 'email.required' => __("Please enter attendee's email address", 'fluent-booking'),
82 + 'name.required' => __('Please enter attendee\'s name', 'fluent-booking'),
83 + 'email.required' => __('Please enter attendee\'s email address', 'fluent-booking'),
84 84 'email.email' => __('Please provide a valid email address', 'fluent-booking'),
85 85 'timezone.required' => __('Please select the timezone', 'fluent-booking'),
86 86 'event_time.required' => __('Please select a date and time', 'fluent-booking')
87 87 ];
@@ -88,19 +88,31 @@
88 88
89 89 $locationType = Arr::get($postedData, 'location_type');
90 90
91 91 if ($calendarEvent->isPhoneRequired()) {
92 - $rules['location_description'] = 'required';
93 - $messages['location_description.required'] = __("Please provide attendee's phone number", 'fluent-booking');
92 + $rules['location_description'] = ['required', $this->validPhoneNumberRule()];
93 + $messages['location_description.required'] = __('Please provide attendee\'s phone number', 'fluent-booking');
94 94 } else if ($calendarEvent->isAddressRequired()) {
95 95 $rules['location_description'] = 'required';
96 - $messages['location_description.required'] = __("Please provide attendee's address", 'fluent-booking');
96 + $messages['location_description.required'] = __('Please provide attendee\'s address', 'fluent-booking');
97 + } else if ($locationType === 'phone_guest') {
98 + $rules['location_description'] = [$this->validPhoneNumberRule()];
97 99 }
98 100
99 - if ($additionalGuests = array_filter(Arr::get($postedData, 'guests', []))) {
100 - $postedData['guests'] = array_map('sanitize_email', $additionalGuests);
101 + $additionalGuests = Arr::get($postedData, 'guests', []);
102 + if (!empty($additionalGuests)) {
103 + if ($calendarEvent->isMultiGuestEvent()) {
104 + $additionalGuests = $this->sanitize_mapped_data($additionalGuests);
105 + $additionalGuests = array_values(array_filter($additionalGuests, function ($guest) {
106 + return Arr::get($guest, 'name') && Arr::get($guest, 'email');
107 + }));
108 + } else {
109 + $additionalGuests = array_filter(array_map('sanitize_email', $additionalGuests));
110 + }
101 111 }
102 112
113 + $postedData['guests'] = $additionalGuests;
114 +
103 115 $requiredFields = array_filter($calendarEvent->getMeta('booking_fields', []), function ($field) {
104 116 return Arr::isTrue($field, 'required') && Arr::isTrue($field, 'enabled') && (Arr::get($field, 'name') == 'message' || Arr::get($field, 'name') == 'guests');
105 117 });
106 118
@@ -138,27 +150,27 @@
138 150 return;
139 151 }
140 152
141 153 $duration = $calendarEvent->getDuration(Arr::get($postedData, 'duration', null));
142 - $timezone = Arr::get($postedData, 'timezone', 'UTC');
154 + $timezone = sanitize_text_field($postedData['timezone']);
143 155
144 - $startDateTime = DateTimeHelper::convertToUtc($postedData['event_time'], $timezone);
156 + $startDateTime = DateTimeHelper::convertToUtc(sanitize_text_field($postedData['event_time']), $timezone);
145 157 $endDateTime = gmdate('Y-m-d H:i:s', strtotime($startDateTime) + ($duration * 60));
146 158
147 - $bookingData = [
148 - 'person_time_zone' => sanitize_text_field($timezone),
159 + $bookingData = apply_filters('fluent_booking/initialize_booking_data', [
160 + 'person_time_zone' => $timezone,
149 161 'start_time' => $startDateTime,
150 162 'name' => sanitize_text_field($postedData['name']),
151 163 'email' => sanitize_email($postedData['email']),
152 164 'message' => sanitize_textarea_field(wp_unslash(Arr::get($postedData, 'message', ''))),
153 - 'phone' => sanitize_textarea_field(Arr::get($postedData, 'phone_number', '')),
165 + 'phone' => sanitize_text_field(Arr::get($postedData, 'phone_number', '')),
154 166 'address' => sanitize_textarea_field(Arr::get($postedData, 'address', '')),
155 167 'ip_address' => Helper::getIp(),
156 - 'status' => sanitize_text_field($postedData['status']),
157 - 'source' => 'admin',
168 + 'status' => sanitize_text_field(Arr::get($postedData, 'status', 'scheduled')),
169 + 'source' => Arr::get($postedData, 'source') == 'admin' ? 'admin' : 'web',
158 170 'event_type' => $calendarEvent->event_type,
159 171 'slot_minutes' => $duration
160 - ];
172 + ], $postedData, $calendarEvent);
161 173
162 174 $eventLocations = [];
163 175 $locationSettings = $calendarEvent->location_settings;
164 176 foreach ($locationSettings as $index => $location) {
@@ -168,15 +180,16 @@
168 180 $locationDetails['type'] = $locationType;
169 181 if ($locationType == 'phone_organizer') {
170 182 $locationDetails['description'] = $eventLocations[$locationType]['host_phone_number'];
171 183 } else if ($locationType == 'phone_guest') {
172 - $bookingData['phone'] = Arr::get($postedData, 'location_description', '');
184 + $bookingData['phone'] = sanitize_text_field(Arr::get($postedData, 'location_description', ''));
173 185 } else if ($locationType == 'in_person_guest') {
174 - $locationDetails['description'] = Arr::get($postedData, 'location_description', '');
186 + $locationDetails['description'] = sanitize_textarea_field(Arr::get($postedData, 'location_description', ''));
175 187 } else if (in_array($locationType, ['custom', 'in_person_organizer'])) {
176 188 $locationDetails['description'] = $eventLocations[$locationType]['description'];
177 189 } else if (in_array($locationType, ['google_meet', 'online_meeting', 'zoom_meeting', 'ms_teams'])) {
178 - $locationDetails['description'] = $eventLocations[$locationType]['meeting_link'];
190 + $locationDetails['description'] = Arr::get($eventLocations[$locationType], 'meeting_link', '');
191 + $locationDetails['online_platform_link'] = $locationDetails['description'];
179 192 }
180 193
181 194 $bookingData['location_details'] = $locationDetails;
182 195
@@ -183,25 +196,27 @@
183 196 if ($sourceUrl = Arr::get($postedData, 'source_url', '')) {
184 197 $bookingData['source_url'] = sanitize_url($sourceUrl);
185 198 }
186 199
187 - if ($additionalGuests) {
188 - $guestField = BookingFieldService::getBookingFieldByName($calendarEvent, 'guests');
189 - $guestLimit = Arr::get($guestField, 'limit', 10);
190 - $bookingData['additional_guests'] = array_slice($additionalGuests, 0, $guestLimit);
191 - }
200 + if ($hostUserId = Arr::get($postedData, 'host_user_id', null)) {
201 + $hostUserId = (int) $hostUserId;
192 202
193 - if ($hostUserId = Arr::get($postedData, 'host_user_id', null)) {
194 - $bookingData['host_user_id'] = (int)$hostUserId;
203 + if (!in_array($hostUserId, array_map('intval', $calendarEvent->getHostIds()), true)) {
204 + return $this->sendError([
205 + 'message' => __('The selected host is not a host of this event', 'fluent-booking')
206 + ], 422);
207 + }
208 +
209 + $bookingData['host_user_id'] = $hostUserId;
195 210 }
196 211
197 212 $hostIds = null;
198 - if ($calendarEvent->isTeamEvent() && !$hostUserId) {
213 + if ($calendarEvent->isRoundRobin() && !$hostUserId) {
199 214 $hostIds = $calendarEvent->getHostIdsSortedByBookings($startDateTime);
200 215 $bookingData['host_user_id'] = $hostIds[0];
201 216 }
202 217
203 - // Check if the time is available or not for this slot
218 + $availableSpot = false;
204 219 if (!Arr::isTrue($postedData, 'ignore_availability')) {
205 220 $timeSlotService = TimeSlotServiceHandler::initService($calendarEvent->calendar, $calendarEvent);
206 221
207 222 if (is_wp_error($timeSlotService)) {
@@ -207,21 +222,31 @@
207 222 if (is_wp_error($timeSlotService)) {
208 223 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timezone);
209 224 }
210 225
211 - $isSpotAvailable = $timeSlotService->isSpotAvailable($startDateTime, $endDateTime, $duration, $hostUserId);
226 + $availableSpot = $timeSlotService->isSpotAvailable($startDateTime, $endDateTime, $duration, $hostUserId);
212 227
213 - if (!$isSpotAvailable) {
228 + if (!$availableSpot) {
214 229 wp_send_json([
215 230 'message' => __('This selected time slot is not available. Maybe someone booked the spot just a few seconds ago.', 'fluent-booking')
216 231 ], 422);
217 232 }
218 233
219 - if ($calendarEvent->isTeamEvent() && !$hostUserId) {
234 + if ($calendarEvent->isRoundRobin() && !$hostUserId) {
220 235 $bookingData['host_user_id'] = $timeSlotService->hostUserId;
221 236 }
222 237 }
223 238
239 + if ($additionalGuests) {
240 + $guestField = BookingFieldService::getBookingFieldByName($calendarEvent, 'guests');
241 + $guestLimit = Arr::get($guestField, 'limit', 10);
242 + if ($calendarEvent->isMultiGuestEvent() && $availableSpot) {
243 + $remaining = Arr::get($availableSpot, 'remaining', $calendarEvent->getMaxBookingPerSlot());
244 + $guestLimit = min($remaining, $guestLimit) - 1;
245 + }
246 + $bookingData['additional_guests'] = array_slice($additionalGuests, 0, $guestLimit);
247 + }
248 +
224 249 do_action('fluent_booking/before_creating_schedule', $bookingData, $postedData, $calendarEvent);
225 250
226 251 try {
227 252 $booking = BookingService::createBooking($bookingData, $calendarEvent, $customFieldsData);
@@ -238,14 +263,24 @@
238 263 'message' => __('Booking has been created', 'fluent-booking'),
239 264 ];
240 265 }
241 266
242 - public function getEvent(Request $request)
267 + /**
268 + * @return \Closure
269 + */
270 + private function validPhoneNumberRule()
243 271 {
244 - $eventId = $request->get('event_id');
245 -
246 - $calendarEvent = CalendarSlot::query()->find($eventId);
272 + return function ($attribute, $value) {
273 + if (!empty($value) && !Helper::isValidPhoneNumber($value)) {
274 + return __('Please provide a valid phone number', 'fluent-booking');
275 + }
276 + };
277 + }
247 278
279 + public function getEvent(Request $request, $eventId)
280 + {
281 + $calendarEvent = CalendarSlot::find($eventId);
282 +
248 283 if (!$calendarEvent || $calendarEvent->status != 'active') {
249 284 wp_send_json([
250 285 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
251 286 ], 422);
@@ -279,13 +314,23 @@
279 314
280 315 $duration = $calendarEvent->getDuration($request->get('duration'));
281 316
282 317 $hostId = $request->get('host_id', null);
283 -
318 +
319 + if ($hostId) {
320 + $hostId = (int) $hostId;
321 +
322 + if (!in_array($hostId, array_map('intval', $calendarEvent->getHostIds()), true)) {
323 + wp_send_json([
324 + 'message' => __('The selected host is not a host of this event', 'fluent-booking')
325 + ], 422);
326 + }
327 + }
328 +
284 329 $timeSlotService = TimeSlotServiceHandler::initService($calendar, $calendarEvent);
285 330
286 331 if (is_wp_error($timeSlotService)) {
287 - return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timezone);
332 + return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timeZone);
288 333 }
289 334
290 335 $availableSpots = $timeSlotService->getAvailableSpots($startDate, $timeZone, $duration, $hostId);
291 336
@@ -323,10 +368,10 @@
323 368 $bookingPeriod = sanitize_text_field($request->get('period', 'all'));
324 369
325 370 $bookingQuery = Booking::query()->with('calendar_event')
326 371 ->where('email', $userEmail)
327 - ->orderBy('start_time', 'DESC')
328 - ->applyComputedStatus($bookingPeriod);
372 + ->applyComputedStatus($bookingPeriod)
373 + ->applyBookingOrderByStatus($bookingPeriod);
329 374
330 375 $calendarIds = $request->get('calendar_ids', []);
331 376
332 377 if (!in_array('all', $calendarIds)) {
@@ -349,9 +394,9 @@
349 394 'payment_status' => $booking->payment_status,
350 395 'booking_title' => $booking->getBookingTitle(true),
351 396 'author_name' => $booking->getHostDetails(false)['name'],
352 397 'booking_date' => DateTimeHelper::formatToLocale($booking->getAttendeeStartTime(), 'date'),
353 - 'booking_time' => DateTimeHelper::formatToLocale($booking->getAttendeeEndTime(), 'time') . ' - ' . DateTimeHelper::formatToLocale($booking->getAttendeeEndTime(), 'time'),
398 + 'booking_time' => DateTimeHelper::formatToLocale($booking->getAttendeeStartTime(), 'time') . ' - ' . DateTimeHelper::formatToLocale($booking->getAttendeeEndTime(), 'time'),
354 399 ];
355 400 }
356 401
357 402 return [
@@ -357,6 +402,97 @@
357 402 return [
358 403 'bookings' => $formattedBookings,
359 404 'total' => $totalBookings
360 405 ];
406 + }
407 +
408 + public function getRescheduleSlots(Request $request, $eventId, $bookingId)
409 + {
410 + return $this->getEvent($request, $eventId);
411 + }
412 +
413 + public function rescheduleBooking(Request $request, $eventId, $bookingId)
414 + {
415 + $booking = Booking::with(['calendar_event'])->findOrFail($bookingId);
416 +
417 + $data = $request->all();
418 +
419 + $this->validate($data, [
420 + 'event_time' => 'required',
421 + 'timezone' => 'required'
422 + ], [
423 + 'event_time.required' => __('Please select a date and time', 'fluent-booking'),
424 + 'timezone.required' => __('Please select the timezone', 'fluent-booking')
425 + ]);
426 +
427 + $calendarEvent = $booking->calendar_event;
428 +
429 + if (!$calendarEvent || (int) $calendarEvent->id !== (int) $eventId) {
430 + return $this->sendError(['message' => __('The event of this booking was not found', 'fluent-booking')], 422);
431 + }
432 +
433 + if (!in_array($booking->status, ['scheduled', 'pending', 'approved', 'rescheduled'], true)) {
434 + return $this->sendError(['message' => __('This booking can not be rescheduled', 'fluent-booking')], 422);
435 + }
436 +
437 + $timezone = sanitize_text_field(Arr::get($data, 'timezone'));
438 +
439 + if (!in_array($timezone, \DateTimeZone::listIdentifiers(), true)) {
440 + return $this->sendError(['message' => __('Please select a valid timezone', 'fluent-booking')], 422);
441 + }
442 +
443 + $eventTime = sanitize_text_field(Arr::get($data, 'event_time'));
444 +
445 + if (!\DateTime::createFromFormat('Y-m-d H:i:s', $eventTime)) {
446 + return $this->sendError(['message' => __('Please select a valid date and time', 'fluent-booking')], 422);
447 + }
448 +
449 + $startTime = DateTimeHelper::convertToUtc($eventTime, $timezone);
450 + $endTime = gmdate('Y-m-d H:i:s', strtotime($startTime) + ($booking->slot_minutes * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
451 +
452 + $hostUserId = null;
453 +
454 + if (!Arr::isTrue($data, 'ignore_availability')) {
455 + $timeSlotService = TimeSlotServiceHandler::initService($calendarEvent->calendar, $calendarEvent);
456 +
457 + if (is_wp_error($timeSlotService)) {
458 + return $this->sendError(['message' => $timeSlotService->get_error_message()], 422);
459 + }
460 +
461 + $isSlotLocked = Helper::lockRoundRobinSlot($calendarEvent, $startTime, $endTime);
462 +
463 + if (!$isSlotLocked || !$timeSlotService->isSpotAvailable($startTime, $endTime, $booking->slot_minutes)) {
464 + return $this->sendError([
465 + 'message' => __('This selected time slot is not available. Maybe someone booked the spot just a few seconds ago.', 'fluent-booking')
466 + ], 422);
467 + }
468 +
469 + if ($calendarEvent->isRoundRobin()) {
470 + $hostUserId = $timeSlotService->hostUserId;
471 + }
472 + }
473 +
474 + $result = RescheduleService::reschedule($booking, $calendarEvent, $startTime, $timezone, [
475 + 'reason' => Arr::get($data, 'reason', ''),
476 + 'host_user_id' => $hostUserId,
477 + 'source' => __('Admin Panel', 'fluent-booking')
478 + ]);
479 +
480 + if (is_wp_error($result)) {
481 + return $this->sendError(['message' => $result->get_error_message()], 422);
482 + }
483 +
484 + return [
485 + 'message' => __('Booking has been rescheduled', 'fluent-booking')
486 + ];
487 + }
488 +
489 + private static function sanitize_mapped_data($settings)
490 + {
491 + $sanitizerMap = [
492 + 'name' => 'sanitize_text_field',
493 + 'email' => 'sanitize_email',
494 + ];
495 +
496 + return Helper::fcal_backend_sanitizer($settings, $sanitizerMap);
361 497 }
362 498 }