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 +169 -31 1.5.24 → 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
@@ -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';
92 + $rules['location_description'] = ['required', $this->validPhoneNumberRule()];
93 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 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)) {
@@ -215,13 +230,23 @@
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,8 +263,20 @@
238 263 'message' => __('Booking has been created', 'fluent-booking'),
239 264 ];
240 265 }
241 266
267 + /**
268 + * @return \Closure
269 + */
270 + private function validPhoneNumberRule()
271 + {
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 + }
278 +
242 279 public function getEvent(Request $request, $eventId)
243 280 {
244 281 $calendarEvent = CalendarSlot::find($eventId);
245 282
@@ -277,13 +314,23 @@
277 314
278 315 $duration = $calendarEvent->getDuration($request->get('duration'));
279 316
280 317 $hostId = $request->get('host_id', null);
281 -
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 +
282 329 $timeSlotService = TimeSlotServiceHandler::initService($calendar, $calendarEvent);
283 330
284 331 if (is_wp_error($timeSlotService)) {
285 - return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timezone);
332 + return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timeZone);
286 333 }
287 334
288 335 $availableSpots = $timeSlotService->getAvailableSpots($startDate, $timeZone, $duration, $hostId);
289 336
@@ -321,10 +368,10 @@
321 368 $bookingPeriod = sanitize_text_field($request->get('period', 'all'));
322 369
323 370 $bookingQuery = Booking::query()->with('calendar_event')
324 371 ->where('email', $userEmail)
325 - ->orderBy('start_time', 'DESC')
326 - ->applyComputedStatus($bookingPeriod);
372 + ->applyComputedStatus($bookingPeriod)
373 + ->applyBookingOrderByStatus($bookingPeriod);
327 374
328 375 $calendarIds = $request->get('calendar_ids', []);
329 376
330 377 if (!in_array('all', $calendarIds)) {
@@ -347,9 +394,9 @@
347 394 'payment_status' => $booking->payment_status,
348 395 'booking_title' => $booking->getBookingTitle(true),
349 396 'author_name' => $booking->getHostDetails(false)['name'],
350 397 'booking_date' => DateTimeHelper::formatToLocale($booking->getAttendeeStartTime(), 'date'),
351 - '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'),
352 399 ];
353 400 }
354 401
355 402 return [
@@ -355,6 +402,97 @@
355 402 return [
356 403 'bookings' => $formattedBookings,
357 404 'total' => $totalBookings
358 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);
359 497 }
360 498 }