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 / Hooks / Handlers / FrontEndHandler.php

FrontEndHandler.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.5.0, at app/Hooks/Handlers/FrontEndHandler.php

1,073 lines 43.5 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\Hooks\Handlers;
4
5 use FluentBooking\App\App;
6 use FluentBooking\App\Models\Booking;
7 use FluentBooking\App\Models\Calendar;
8 use FluentBooking\App\Models\CalendarSlot;
9 use FluentBooking\App\Services\BookingFieldService;
10 use FluentBooking\App\Services\BookingService;
11 use FluentBooking\App\Services\DateTimeHelper;
12 use FluentBooking\App\Services\PublicTransStrings;
13 use FluentBooking\App\Services\Helper;
14 use FluentBooking\App\Services\LandingPage\LandingPageHandler;
15 use FluentBooking\App\Services\LandingPage\LandingPageHelper;
16 use FluentBooking\App\Hooks\Handlers\TimeSlotServiceHandler;
17 use FluentBooking\App\Services\CalendarEventService;
18 use FluentBooking\App\Services\LocationService;
19 use FluentBooking\App\Services\RescheduleService;
20 use FluentBooking\App\Services\CurrenciesHelper;
21 use FluentBooking\Framework\Support\Arr;
22 use FluentBooking\App\Vite;
23
24 class FrontEndHandler
25 {
26 public function register()
27 {
28 add_shortcode('fluent_booking', [$this, 'handleBookingShortcode']);
29
30 add_shortcode('fluent_booking_team', [$this, 'handleTeamShortcode']);
31
32 add_shortcode('fluent_booking_calendar', [$this, 'handleCalendarShortcode']);
33
34 add_shortcode('fluent_booking_lists', [$this, 'handleBookingListsShortcode']);
35
36 add_shortcode('fluent_booking_receipt', [$this, 'handleReceiptShortcode']);
37
38 add_action('wp_ajax_fluent_cal_schedule_meeting', [$this, 'ajaxScheduleMeeting']);
39 add_action('wp_ajax_nopriv_fluent_cal_schedule_meeting', [$this, 'ajaxScheduleMeeting']);
40
41 add_action('wp_ajax_fcal_cancel_meeting', [$this, 'ajaxHandleCancelMeeting']);
42 add_action('wp_ajax_nopriv_fcal_cancel_meeting', [$this, 'ajaxHandleCancelMeeting']);
43
44 add_action('wp_ajax_fluent_cal_get_available_dates', [$this, 'ajaxGetAvailableDates']);
45 add_action('wp_ajax_nopriv_fluent_cal_get_available_dates', [$this, 'ajaxGetAvailableDates']);
46
47 add_action('fluent_booking/starting_scheduling_ajax', [$this, 'handleRescheduling']);
48 }
49
50 public function handleBookingShortcode($atts, $content)
51 {
52 $atts = shortcode_atts([
53 'id' => 0,
54 'theme' => 'light',
55 'disable_author' => 'no',
56 'hash' => ''
57 ], $atts);
58
59 if (!$atts['id'] && !$atts['hash']) {
60 return '';
61 }
62
63 $calendarEvent = CalendarSlot::find($atts['id']);
64 if (!$calendarEvent) {
65 $calendarEvent = CalendarSlot::where('hash', $atts['hash'])->first();
66 if (!$calendarEvent) {
67 return __('Calendar event not found', 'fluent-booking');
68 }
69 }
70
71 $calendar = $calendarEvent->calendar;
72 if (!$calendar) {
73 return __('Calendar not found', 'fluent-booking');
74 }
75
76 $localizeData = $this->getCalendarEventVars($calendar, $calendarEvent);
77 $localizeData['disable_author'] = $atts['disable_author'] == 'yes';
78 $localizeData['theme'] = $atts['theme'];
79
80 if (BookingFieldService::hasPhoneNumberField($localizeData['form_fields'])) {
81 Vite::enqueueScript('fluent-booking-phone-field', 'phone_field', [], FLUENT_BOOKING_ASSETS_VERSION);
82 }
83
84 Vite::enqueueScript('fluent-booking-public', 'public_app', [], FLUENT_BOOKING_ASSETS_VERSION);
85
86 $this->loadGlobalVars();
87 wp_localize_script(
88 'fluent-booking-public',
89 'fcal_public_vars_' . $calendar->id . '_' . $calendarEvent->id,
90 $localizeData,
91 );
92
93 return App::make('view')->make('public.calendar', [
94 'calenderEvent' => $calendarEvent,
95 'theme' => $atts['theme']
96 ]);
97 }
98
99 public function handleTeamShortcode($atts, $content)
100 {
101 $atts = shortcode_atts([
102 'event_ids' => '',
103 'title' => '',
104 'description' => '',
105 'logo_url' => ''
106 ], $atts);
107
108 if (!$atts['event_ids']) {
109 return '';
110 }
111
112 $eventIds = array_filter(array_map('intval', explode(',', $atts['event_ids'])));
113
114 if (empty($eventIds)) {
115 return '';
116 }
117
118 $events = CalendarSlot::query()->whereIn('id', $eventIds)
119 ->where('status', 'active')
120 ->get();
121
122 $calendarIds = [];
123 $calendarEvents = [];
124
125 foreach ($events as $event) {
126 $calendarIds[] = $event->calendar_id;
127 if (!isset($calendarEvents[$event->calendar_id])) {
128 $calendarEvents[$event->calendar_id] = [];
129 }
130 $event = CalendarEventService::processEvent($event);
131 $calendarEvents[$event->calendar_id][] = $event;
132 }
133
134 $calendars = Calendar::query()->with('metas')->whereIn('id', $calendarIds)->get();
135
136 foreach ($calendars as $calendar) {
137 $calendar->activeEvents = $calendarEvents[$calendar->id] ?? [];
138 $eventOrder = $calendar->getMeta('event_order');
139 if (!empty($eventOrder)) {
140 $eventsArray = $calendar->activeEvents;
141 usort($eventsArray, function($a, $b) use ($eventOrder) {
142 $posA = array_search($a->id, $eventOrder);
143 $posB = array_search($b->id, $eventOrder);
144 return $posA - $posB;
145 });
146 $calendar->activeEvents = $eventsArray;
147 }
148 }
149
150 return $this->renderTeamHosts($calendars, [
151 'title' => $atts['title'],
152 'description' => $atts['description'],
153 'logo' => $atts['logo_url'],
154 'wrapper_class' => ''
155 ]);
156 }
157
158 public function renderTeamHosts($calendars, $headerConfig = [])
159 {
160 $wrapperId = 'fcal_team_' . Helper::getNextIndex();
161 Vite::enqueueScript('fluent-booking-team', 'team_app', [], FLUENT_BOOKING_ASSETS_VERSION);
162
163 $vars = [];
164 foreach ($calendars as $calendar) {
165 $hostHtml = (string)(string)\FluentBooking\App\App::getInstance('view')->make('landing.author_html', [
166 'author' => $calendar->getAuthorProfile(),
167 'calendar' => $calendar,
168 'events' => $calendar->activeEvents
169 ]);
170
171 $hostHtml .= '<div onclick="fcalBackToTeam(this)" class="fcal_back_btn_team"><svg height="20px" version="1.1" viewBox="0 0 512 512" width="512px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><polygon points="352,128.4 319.7,96 160,256 160,256 160,256 319.7,416 352,383.6 224.7,256 "></polygon></svg> <span>' . __('Back to team', 'fluent-booking') . '</span></div>';
172
173 $eventCount = count($calendar->activeEvents);
174
175 $vars['fcal_host_' . $calendar->id] = [
176 'host_html' => $hostHtml,
177 'event_count' => $eventCount,
178 'target_event_id' => ($eventCount == 1) ? $calendar->activeEvents[0]->id : 0
179 ];
180
181 foreach ($calendar->activeEvents as $event) {
182 $itemVars = $this->getCalendarEventVars($event->calendar, $event);
183 $extraJs = (new LandingPageHandler())->getEventLandingExtraJsFiles($itemVars['form_fields'], $event);
184 if ($extraJs) {
185 $itemVars['lazy_js_files'] = $extraJs;
186 }
187 wp_localize_script('fluent-booking-team', 'fcal_public_vars_' . $event->calendar_id . '_' . $event->id, $itemVars);
188 }
189 }
190
191 wp_localize_script('fluent-booking-team', $wrapperId, $vars);
192
193 Vite::enqueueScript('fluent-booking-public', 'public_app', [], FLUENT_BOOKING_ASSETS_VERSION);
194 $this->loadGlobalVars();
195
196 return App::make('view')->make('public.team_page', [
197 'hosts' => $calendars,
198 'wrapper_id' => $wrapperId,
199 'logo' => Arr::get($headerConfig, 'logo', ''),
200 'title' => Arr::get($headerConfig, 'title', ''),
201 'description' => Arr::get($headerConfig, 'description', ''),
202 'wrapper_class' => Arr::get($headerConfig, 'wrapper_class', '')
203 ]);
204 }
205
206 public function handleCalendarShortcode($atts, $content)
207 {
208 $atts = shortcode_atts([
209 'calendar_id' => '',
210 'event_ids' => '',
211 'title' => '',
212 'description' => '',
213 'logo' => '',
214 'hide_info' => false
215 ], $atts);
216
217 $calendarId = intval($atts['calendar_id']);
218 $title = sanitize_text_field($atts['title']);
219 $description = sanitize_text_field($atts['description']);
220 $logo = sanitize_text_field($atts['logo']);
221 $hideInfo = $atts['hide_info'] ? true : false;
222 $eventIds = array_filter(array_map('intval', explode(',', $atts['event_ids'])));
223
224 if (!$calendarId) {
225 return '';
226 }
227
228 $calendar = Calendar::find($calendarId);
229 if (!$calendar) {
230 return '';
231 }
232
233 $settings = LandingPageHelper::getSettings($calendar, 'public');
234
235 $calendarEventQuery = CalendarSlot::where('calendar_id', $calendar->id)
236 ->where('status', 'active');
237
238 $enabledEvents = [];
239 $isEnabledOnly = false;
240 if ($settings['show_type'] != 'all') {
241 $isEnabledOnly = true;
242 $enabledEvents = $settings['enabled_slots'];
243 }
244
245 if ($eventIds && $eventIds != 'all') {
246 $isEnabledOnly = true;
247 $enabledEvents = !empty($enabledEvents) ? array_intersect($enabledEvents, $eventIds) : $eventIds;
248 }
249
250 if (!empty($enabledEvents) || $isEnabledOnly) {
251 $calendarEventQuery->whereIn('id', $enabledEvents);
252 }
253
254 $calendarEvents = $calendarEventQuery->get();
255
256 if ($calendarEvents->isEmpty()) {
257 return '';
258 }
259
260 $calendarEvents = CalendarEventService::processEvents($calendar, $calendarEvents);
261
262 $calendar->activeEvents = $calendarEvents;
263
264 return $this->renderCalendarBlock($calendar, [
265 'title' => $title,
266 'description' => $description,
267 'logo' => $logo,
268 'hide_info' => $hideInfo,
269 'wrapper_class' => '',
270 ]);
271 }
272
273 public function renderCalendarBlock($calendar, $headerConfig = [])
274 {
275 $wrapperId = 'fcal_calendar_' . Helper::getNextIndex();
276 Vite::enqueueScript('fluent-booking-calendar', 'calendar_app', [], FLUENT_BOOKING_ASSETS_VERSION);
277
278 $calendarHtml = (string)(string)\FluentBooking\App\App::getInstance('view')->make('landing.author_html', [
279 'author' => $calendar->getAuthorProfile(),
280 'calendar' => $calendar,
281 'events' => $calendar->activeEvents,
282 'hideInfo' => Arr::isTrue($headerConfig, 'hide_info'),
283 'block' => true
284 ]);
285
286 $eventCount = count($calendar->activeEvents);
287
288 $vars['fcal_host_calendar' ] = [
289 'calendar_html' => $calendarHtml,
290 'event_count' => $eventCount,
291 'target_event_id' => ($eventCount == 1) ? $calendar->activeEvents[0]->id : 0
292 ];
293
294 foreach ($calendar->activeEvents as $event) {
295 $itemVars = $this->getCalendarEventVars($event->calendar, $event);
296 $extraJs = (new LandingPageHandler())->getEventLandingExtraJsFiles($itemVars['form_fields'], $event);
297 if ($extraJs) {
298 $itemVars['lazy_js_files'] = $extraJs;
299 }
300 wp_localize_script('fluent-booking-calendar', 'fcal_public_vars_' . $event->calendar_id . '_' . $event->id, $itemVars);
301 }
302
303 wp_localize_script('fluent-booking-calendar', $wrapperId, $vars);
304
305 Vite::enqueueScript('fluent-booking-public', 'public_app', [], FLUENT_BOOKING_ASSETS_VERSION);
306 $this->loadGlobalVars();
307
308 return App::make('view')->make('public.calendar_page', [
309 'calendar' => $calendar,
310 'wrapper_id' => $wrapperId,
311 'logo' => Arr::get($headerConfig, 'logo', ''),
312 'title' => Arr::get($headerConfig, 'title', ''),
313 'description' => Arr::get($headerConfig, 'description', ''),
314 'wrapper_class' => Arr::get($headerConfig, 'wrapper_class', ''),
315 'hide_info' => Arr::isTrue($headerConfig, 'hide_info')
316 ]);
317 }
318
319 public function handleBookingListsShortcode($atts, $content)
320 {
321 $atts = shortcode_atts([
322 'title' => __('My Bookings', 'fluent-booking'),
323 'filter' => 'show',
324 'pagination' => 'show',
325 'period' => 'all',
326 'calendar_ids' => 'all',
327 'no_bookings' => __('No bookings found', 'fluent-booking'),
328 'per_page' => 10
329 ], $atts);
330
331 $atts['title'] = sanitize_text_field($atts['title']);
332 $atts['filter'] = sanitize_text_field($atts['filter']);
333 $atts['pagination'] = sanitize_text_field($atts['pagination']);
334 $atts['no_bookings'] = sanitize_text_field($atts['no_bookings']);
335
336 $userData = get_userdata(get_current_user_id());
337
338 $userEmail = $userData ? $userData->user_email : null;
339
340 if (!$userEmail) {
341 return __('Please login to view your bookings', 'fluent-booking');
342 }
343
344 $data = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
345
346 $perPage = intval(Arr::get($data, 'booking_per_page', $atts['per_page']));
347 $currentPage = intval(Arr::get($data, 'booking_page', 1));
348 $bookingPeriod = sanitize_text_field(Arr::get($data, 'booking_period', $atts['period']));
349
350 $bookingQuery = Booking::query()->with('calendar_event')
351 ->where('email', $userEmail)
352 ->applyComputedStatus($bookingPeriod)
353 ->applyBookingOrderByStatus($bookingPeriod);
354
355 if ($atts['calendar_ids'] != 'all') {
356 $atts['calendar_ids'] = array_map('intval', explode(',', $atts['calendar_ids']));
357 $bookingQuery->whereIn('calendar_id', $atts['calendar_ids']);
358 }
359
360 $bookings = $bookingQuery->paginate($perPage, ['*'], 'booking_page', $currentPage)
361 ->appends(['booking_page' => $currentPage])
362 ->withQueryString();
363
364 foreach ($bookings as &$booking) {
365 $booking->author_name = $booking->getHostDetails(false)['name'];
366 $booking->happening_status = $booking->getOngoingStatus();
367 $booking->booking_status_text = $booking->getBookingStatus();
368 $booking->payment_status_text = $booking->getPaymentStatus();
369
370 $booking->booking_date = DateTimeHelper::formatToLocale($booking->getAttendeeStartTime(), 'date');
371 $booking->booking_time = DateTimeHelper::formatToLocale($booking->getAttendeeStartTime(), 'time') . ' - ' . DateTimeHelper::formatToLocale($booking->getAttendeeEndTime(), 'time');
372 }
373
374 $currentPage = $bookings->currentPage();
375 $lastPage = $bookings->lastPage();
376 $startPage = max(1, $currentPage - 2);
377 $endPage = min($lastPage, $currentPage + 2);
378
379 // Adjust if near the beginning or the end
380 if ($currentPage < 3) {
381 $endPage = min($lastPage, 5);
382 }
383 if ($currentPage > $lastPage - 2) {
384 $startPage = max(1, $lastPage - 4);
385 }
386
387 $periodOptions = Helper::getBookingPeriodOptions();
388
389 $pageOptions = apply_filters('fluent_booking/booking_per_page_options', [5, 10, 15, 20, 50, 100]);
390
391 Vite::enqueueScript('fluent-booking-list', 'bookings', [], FLUENT_BOOKING_ASSETS_VERSION);
392
393 return App::make('view')->make('public.bookings', [
394 'bookings' => $bookings,
395 'attributes' => $atts,
396 'per_page' => $perPage,
397 'start_page' => $startPage,
398 'end_page' => $endPage,
399 'booking_period' => $bookingPeriod,
400 'page_options' => $pageOptions,
401 'period_options' => $periodOptions
402 ]);
403 }
404
405 public function handleReceiptShortcode($atts, $content)
406 {
407 if (!isset($_REQUEST['hash'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
408 return __('Booking hash is missing!', 'fluent-booking');
409 }
410
411 $hash = isset($_REQUEST['hash']) ? sanitize_text_field(wp_unslash($_REQUEST['hash'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
412
413 return apply_filters('fluent_booking/payment_receipt_html', '', $hash);
414 }
415
416 public function handleRescheduling($data)
417 {
418 if (empty($data['rescheduling_hash'])) {
419 return;
420 }
421
422 add_filter('fluent_booking/schedule_custom_field_data', function ($data) {
423 return [];
424 });
425
426 add_filter('fluent_booking/schedule_validation_rules_data', function ($data, $postedData, $calendarEvent)
427 {
428 $rules = $messages = [];
429 $rescheduleField = BookingFieldService::getBookingFieldByName($calendarEvent, 'rescheduling_reason');
430
431 if (Arr::isTrue($rescheduleField, 'required')) {
432 $rules['rescheduling_reason'] = 'required';
433 $messages['rescheduling_reason.required'] = __('Please provide a rescheduling reason', 'fluent-booking');
434 }
435
436 return [
437 'rules' => $rules,
438 'messages' => $messages
439 ];
440 }, 10, 3);
441
442 add_action('fluent_booking/before_creating_schedule', function ($bookingData, $postedData, $calendarEvent) {
443 $existingHash = Arr::get($postedData, 'rescheduling_hash');
444 $existingBooking = Booking::where('hash', $existingHash)->first();
445
446 if (!$existingBooking) {
447 wp_send_json([
448 'message' => __('Invalid rescheduling request', 'fluent-booking')
449 ], 422);
450 }
451
452 $result = RescheduleService::reschedule(
453 $existingBooking,
454 $calendarEvent,
455 $bookingData['start_time'],
456 $bookingData['person_time_zone'],
457 [
458 'reason' => Arr::get($postedData, 'rescheduling_reason', ''),
459 'host_user_id' => Arr::get($bookingData, 'host_user_id'),
460 'source' => __('Web UI', 'fluent-booking')
461 ]
462 );
463
464 if (is_wp_error($result)) {
465 wp_send_json([
466 'message' => $result->get_error_message()
467 ], 422);
468 }
469
470 $existingBooking = $result;
471
472 add_filter('fluent_booking/schedule_receipt_data', function ($data) {
473 $data['title'] = __('Your meeting has been rescheduled', 'fluent-booking');
474 return $data;
475 });
476
477 $redirectUrl = $existingBooking->getRedirectUrlWithQuery();
478
479 $html = BookingService::getBookingConfirmationHtml($existingBooking);
480
481 wp_send_json(apply_filters('fluent_booking/booking_rescheduled_response', [
482 'message' => __('Booking has been rescheduled', 'fluent-booking'),
483 'redirect_url' => $redirectUrl,
484 'response_html' => $html,
485 'booking_hash' => $existingBooking->hash
486 ], $existingBooking), 200);
487
488 }, 10, 3);
489 }
490
491 private function loadGlobalVars()
492 {
493 static $loaded;
494
495 if ($loaded) {
496 return;
497 }
498
499 $loaded = true;
500
501 wp_localize_script('fluent-booking-public', 'fluentCalendarPublicVars', $this->getGlobalVars());
502 }
503
504 public function getGlobalVars()
505 {
506 $currentPerson = [
507 'name' => '',
508 'email' => ''
509 ];
510
511 if (is_user_logged_in()) {
512 $currentUser = wp_get_current_user();
513 $name = trim($currentUser->first_name . ' ' . $currentUser->last_name);
514
515 if (!$name) {
516 $name = $currentUser->display_name;
517 }
518
519 $currentPerson = [
520 'name' => $name,
521 'email' => $currentUser->user_email,
522 'user_id' => $currentUser->ID
523 ];
524 } else {
525 $request = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
526
527 // Check for url params
528 if ($name = sanitize_text_field(Arr::get($request, 'invitee_name'))) {
529 $currentPerson['name'] = $name;
530 }
531
532 if ($email = sanitize_email(Arr::get($request, 'invitee_email'))) {
533 if (is_email($email)) {
534 $currentPerson['email'] = $email;
535 }
536 }
537 }
538
539 if (empty($currentPerson['email'])) {
540 // Let's try to get from FluentCRM is exists
541 if (defined('FLUENTCRM')) {
542 $contactApi = FluentCrmApi('contacts');
543 $contact = $contactApi->getCurrentContact();
544 if ($contact) {
545 $currentPerson['email'] = $contact->email;
546 $currentPerson['name'] = $contact->full_name;
547 }
548 }
549 }
550
551 $globalSettings = Helper::getGlobalSettings();
552 $startDay = Arr::get($globalSettings, 'administration.start_day', 'mon');
553
554 $data = [
555 'ajaxurl' => admin_url('admin-ajax.php'),
556 'timezones' => DateTimeHelper::getFlatGroupedTimeZones(),
557 'current_person' => $currentPerson,
558 'start_day' => $startDay,
559 // Generated from the i18() calls in resources/public; see scripts/i18n.js.
560 'i18' => array_merge(PublicTransStrings::getStrings(), [
561 'date_time_config' => DateTimeHelper::getI18nDateTimeConfig(),
562 ]),
563 'theme' => Arr::get(get_option('_fluent_booking_settings'), 'theme', 'system-default'),
564 'currency_settings' => CurrenciesHelper::getGlobalCurrencySettings()
565 ];
566
567 if (isset($_SERVER['HTTP_CF_IPCOUNTRY'])) {
568 $data['user_country'] = isset($_SERVER['HTTP_CF_IPCOUNTRY']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_IPCOUNTRY'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
569 } else {
570 $data['user_country'] = Arr::get($globalSettings, 'administration.default_country', '');
571 }
572
573 return apply_filters('fluent_calendar/global_booking_vars', $data);
574 }
575
576 public function ajaxScheduleMeeting()
577 {
578 if (!Helper::checkRateLimit('schedule_meeting', 15)) {
579 wp_send_json_error(['message' => __('Too many requests. Please try again in a minute.', 'fluent-booking')], 429);
580 }
581
582 $app = App::getInstance();
583
584 $postedData = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
585
586 $eventId = (int)$postedData['event_id'];
587
588 $isRescheduling = Arr::get($postedData, 'rescheduling_hash', '');
589
590 $calendarEvent = CalendarSlot::find($eventId);
591
592 if (!$calendarEvent || ($calendarEvent->status != 'active' && !$isRescheduling)) {
593 wp_send_json([
594 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
595 ], 422);
596 }
597
598 do_action('fluent_booking/starting_scheduling_ajax', $postedData);
599
600 $rules = [
601 'name' => 'required',
602 'email' => 'required|email',
603 'timezone' => 'required',
604 'start_date' => 'required',
605 'utm_source' => 'max:192',
606 'utm_medium' => 'max:192',
607 'utm_campaign' => 'max:192',
608 'utm_term' => 'max:192',
609 'utm_content' => 'max:192',
610 ];
611
612 $messages = [
613 'name.required' => __('Please enter your name', 'fluent-booking'),
614 'email.required' => __('Please enter your email address', 'fluent-booking'),
615 'email.email' => __('Please enter provide a valid email address', 'fluent-booking'),
616 'timezone.required' => __('Please select timezone first', 'fluent-booking'),
617 'start_date.required' => __('Please select a date and time', 'fluent-booking')
618 ];
619
620 if ($calendarEvent->isPhoneRequired()) {
621 $rules['phone_number'] = ['required', $this->validPhoneNumberRule()];
622 $messages['phone_number.required'] = __('Please provide your phone number', 'fluent-booking');
623 } else if ($calendarEvent->isAddressRequired()) {
624 $rules['address'] = 'required';
625 $messages['address.required'] = __('Please provide your Address', 'fluent-booking');
626 } else if ($calendarEvent->isLocationFieldRequired()) {
627 $rules['location_config.driver'] = 'required';
628 $messages['location_config.driver'] = __('Please select location', 'fluent-booking');
629
630 $selectedLocation = LocationService::getLocationDetails($calendarEvent, Arr::get($postedData, 'location_config', []), $postedData);
631 $selectedLocationDriver = Arr::get($selectedLocation, 'type');
632 // is user input required
633 if (in_array($selectedLocationDriver, ['in_person_guest', 'phone_guest'])) {
634 if ($selectedLocationDriver == 'in_person_guest') {
635 $rules['location_config.user_location_input'] = 'required';
636 $messages['location_config.user_location_input.required'] = __('Please provide your address', 'fluent-booking');
637 } else {
638 $rules['location_config.user_location_input'] = ['required', $this->validPhoneNumberRule()];
639 $messages['location_config.user_location_input.required'] = __('Please provide your phone number', 'fluent-booking');
640 }
641 }
642 }
643
644 $duration = (int)$calendarEvent->getDuration(Arr::get($postedData, 'duration', null));
645
646 if ($additionalGuests = Arr::get($postedData, 'guests', [])) {
647 if ($calendarEvent->isMultiGuestEvent()) {
648 $additionalGuests = $this->sanitize_mapped_data($additionalGuests);
649 $additionalGuests = array_values(array_filter($additionalGuests, function ($guest) {
650 return Arr::get($guest, 'name') && Arr::get($guest, 'email');
651 }));
652 } else {
653 $additionalGuests = array_filter(array_map('sanitize_email', $additionalGuests));
654 }
655 }
656
657 $postedData['guests'] = $additionalGuests;
658
659 $requiredFields = array_filter($calendarEvent->getMeta('booking_fields', []), function ($field) {
660 return Arr::isTrue($field, 'required') && Arr::isTrue($field, 'enabled') && (Arr::get($field, 'name') == 'message' || Arr::get($field, 'name') == 'guests');
661 });
662
663 foreach ($requiredFields as $field) {
664 if (empty($rules[$field['name']])) {
665 $rules[$field['name']] = 'required';
666 $messages[$field['name'] . '.required'] = __('This field is required', 'fluent-booking');
667 }
668 }
669
670 $validationConfig = apply_filters('fluent_booking/schedule_validation_rules_data', [
671 'rules' => $rules,
672 'messages' => $messages
673 ], $postedData, $calendarEvent);
674
675 $validator = $app->validator->make($postedData, $validationConfig['rules'], $validationConfig['messages']);
676 if ($validator->validate()->fails()) {
677 $errorMessage = $validator->firstError() ?: __('Please fill up the required data', 'fluent-booking');
678 wp_send_json([
679 'message' => $errorMessage,
680 'errors' => $validator->errors()
681 ], 422);
682 }
683
684 $customFieldsData = BookingFieldService::getCustomFieldsData($postedData, $calendarEvent);
685 $customFieldsData = apply_filters('fluent_booking/schedule_custom_field_data', $customFieldsData, $calendarEvent);
686
687 if (is_wp_error($customFieldsData)) {
688 wp_send_json([
689 'message' => $customFieldsData->get_error_message(),
690 'errors' => $customFieldsData->get_error_data()
691 ], 422);
692 }
693
694 $validateDateFields = BookingFieldService::validateDateFields($customFieldsData, $calendarEvent);
695
696 if (is_wp_error($validateDateFields)) {
697 wp_send_json([
698 'message' => $validateDateFields->get_error_message(),
699 ], 422);
700 }
701
702 $startDate = Arr::get($postedData, 'start_date');
703 $timezone = sanitize_text_field(Arr::get($postedData, 'timezone', 'UTC'));
704
705 if (is_array($startDate)) {
706 $startDateTime = array_slice(
707 array_map(function($date) use ($timezone) {
708 return DateTimeHelper::convertToUtc(sanitize_text_field($date), $timezone);
709 }, $startDate), 0, $calendarEvent->multiBookingLimit()
710 );
711 $endDateTime = array_map(function($date) use ($duration) {
712 return gmdate('Y-m-d H:i:s', strtotime($date) + ($duration * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
713 }, $startDateTime);
714 }
715
716 if (is_string($startDate)) {
717 $startDate = sanitize_text_field($startDate);
718 $startDateTime = DateTimeHelper::convertToUtc($startDate, $timezone);
719 $endDateTime = gmdate('Y-m-d H:i:s', strtotime($startDateTime) + ($duration * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
720 }
721
722 $bookingData = apply_filters('fluent_booking/initialize_booking_data', [
723 'person_time_zone' => sanitize_text_field($timezone),
724 'start_time' => $startDateTime,
725 'end_time' => $endDateTime,
726 'name' => sanitize_text_field($postedData['name']),
727 'email' => sanitize_email($postedData['email']),
728 'message' => sanitize_textarea_field(wp_unslash(Arr::get($postedData, 'message', ''))),
729 'phone' => sanitize_text_field(Arr::get($postedData, 'phone_number', '')),
730 'address' => sanitize_textarea_field(Arr::get($postedData, 'address', '')),
731 'ip_address' => Helper::getIp(),
732 'status' => 'scheduled',
733 'source' => 'web',
734 'event_type' => $calendarEvent->event_type,
735 'slot_minutes' => $duration,
736 'utm_source' => sanitize_text_field(Arr::get($postedData, 'utm_source', '')),
737 'utm_medium' => sanitize_text_field(Arr::get($postedData, 'utm_medium', '')),
738 'utm_campaign' => sanitize_text_field(Arr::get($postedData, 'utm_campaign', '')),
739 'utm_term' => sanitize_text_field(Arr::get($postedData, 'utm_term', '')),
740 'utm_content' => sanitize_text_field(Arr::get($postedData, 'utm_content', ''))
741 ], $postedData, $calendarEvent);
742
743 if ($calendarEvent->isConfirmationRequired($bookingData['start_time'])) {
744 $bookingData['status'] = 'pending';
745 }
746
747 $locationConfig = Arr::get($postedData, 'location_config', []);
748 $selectedLocation = LocationService::getLocationDetails($calendarEvent, $locationConfig, $postedData);
749 if ($selectedLocation['type'] == 'phone_guest') {
750 $bookingData['phone'] = $selectedLocation['description'];
751 }
752
753 $bookingData['location_details'] = $selectedLocation;
754
755 if ($sourceUrl = Arr::get($postedData, 'source_url', '')) {
756 $bookingData['source_url'] = sanitize_url($sourceUrl);
757 }
758
759 if (!empty($postedData['coupon_codes'])) {
760 $bookingData['coupon_codes'] = array_map('sanitize_text_field', array_unique($postedData['coupon_codes']));
761 }
762
763 if (!empty($postedData['payment_method'])) {
764 $customFieldsData['payment_method'] = sanitize_text_field($postedData['payment_method']);
765 }
766
767 if (!empty($postedData['recurring_count'])) {
768 $bookingData['recurring_count'] = (int) Arr::get($postedData, 'recurring_count', 0);
769 }
770
771 $timeSlotService = TimeSlotServiceHandler::initService($calendarEvent->calendar, $calendarEvent);
772
773 if (is_wp_error($timeSlotService)) {
774 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timezone);
775 }
776
777 $isSlotLocked = Helper::lockRoundRobinSlot($calendarEvent, $bookingData['start_time'], $bookingData['end_time']);
778
779 $availableSpot = $isSlotLocked ? $timeSlotService->isSpotAvailable($bookingData['start_time'], $bookingData['end_time'], $duration) : false;
780
781 if (!$availableSpot) {
782 wp_send_json([
783 'message' => __('This selected time slot is not available. Maybe someone booked the spot just a few seconds ago.', 'fluent-booking')
784 ], 422);
785 }
786
787 if ($additionalGuests) {
788 $guestField = BookingFieldService::getBookingFieldByName($calendarEvent, 'guests');
789 $guestLimit = Arr::get($guestField, 'limit', 10);
790 if ($calendarEvent->isMultiGuestEvent()) {
791 $remaining = Arr::get($availableSpot, 'remaining', $calendarEvent->getMaxBookingPerSlot());
792 $guestLimit = min($remaining, $guestLimit) - 1;
793 }
794 $bookingData['additional_guests'] = array_slice($additionalGuests, 0, $guestLimit);
795 }
796
797 if ($calendarEvent->isRoundRobin()) {
798 $bookingData['host_user_id'] = $timeSlotService->hostUserId;
799 }
800
801 do_action('fluent_booking/before_creating_schedule', $bookingData, $postedData, $calendarEvent);
802
803 try {
804 $booking = BookingService::createBooking($bookingData, $calendarEvent, $customFieldsData);
805
806 if (is_wp_error($booking)) {
807 throw new \Exception(wp_kses_post($booking->get_error_message()), 422);
808 }
809
810 } catch (\Exception $e) {
811 wp_send_json([
812 'message' => $e->getMessage()
813 ], 422);
814 return;
815 }
816
817 $redirectUrl = $booking->getRedirectUrlWithQuery();
818
819 $html = BookingService::getBookingConfirmationHtml($booking);
820
821 wp_send_json(apply_filters('fluent_booking/booking_confirmation_response', [
822 'message' => __('Booking has been confirmed', 'fluent-booking'),
823 'redirect_url' => $redirectUrl,
824 'response_html' => $html,
825 'booking_hash' => $booking->hash
826 ], $booking), 200);
827 }
828
829 /**
830 * @return \Closure
831 */
832 private function validPhoneNumberRule()
833 {
834 return function ($attribute, $value) {
835 if (!empty($value) && !Helper::isValidPhoneNumber($value)) {
836 return __('Please provide a valid phone number', 'fluent-booking');
837 }
838 };
839 }
840
841 public function ajaxGetAvailableDates()
842 {
843 if (!Helper::checkRateLimit('available_dates', 30)) {
844 wp_send_json_error(['message' => __('Too many requests. Please try again in a minute.', 'fluent-booking')], 429);
845 }
846
847 $startBenchmark = microtime(true);
848
849 $request = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
850
851 $eventId = (int)Arr::get($request, 'event_id');
852
853 $reschedulingHash = sanitize_text_field(Arr::get($request, 'rescheduling_hash', '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
854
855 $calendarEvent = $eventId ? CalendarSlot::find($eventId) : null;
856
857 if (!$calendarEvent) {
858 wp_send_json([
859 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
860 ], 422);
861 }
862
863 if ($calendarEvent->status != 'active') {
864 $existingBooking = $reschedulingHash ? Booking::where('hash', $reschedulingHash)->first() : null;
865
866 if (!$existingBooking || (int)$existingBooking->event_id !== (int)$calendarEvent->id) {
867 wp_send_json([
868 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
869 ], 422);
870 }
871 }
872
873 $calendar = $calendarEvent->calendar;
874 $startDate = sanitize_text_field(Arr::get($request, 'start_date')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
875
876 if (!$startDate) {
877 $startDate = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
878 }
879
880 $timeZone = sanitize_text_field(Arr::get($request, 'timezone')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
881
882 if (!$timeZone) {
883 $timeZone = wp_timezone_string();
884 }
885
886 if (!in_array($timeZone, \DateTimeZone::listIdentifiers())) {
887 $timeZone = $calendar->author_timezone;
888 }
889
890 $duration = (int)$calendarEvent->getDuration(Arr::get($request, 'duration', null));
891
892 $timeSlotService = TimeSlotServiceHandler::initService($calendar, $calendarEvent);
893
894 if (is_wp_error($timeSlotService)) {
895 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timeZone);
896 }
897
898 $availableSpots = $timeSlotService->getAvailableSpots($startDate, $timeZone, $duration);
899
900 if (is_wp_error($availableSpots)) {
901 return TimeSlotServiceHandler::sendError($availableSpots, $calendarEvent, $timeZone);
902 }
903
904 $availableSpots = array_filter((array) $availableSpots);
905 $availableSpots = apply_filters('fluent_booking/available_slots_for_view', $availableSpots, $calendarEvent, $calendar, $timeZone, $duration);
906
907 wp_send_json([
908 'available_slots' => $availableSpots,
909 'timezone' => $timeZone,
910 'max_lookup_date' => $calendarEvent->getMaxLookUpDate(),
911 'execution_time' => microtime(true) - $startBenchmark
912 ], 200);
913 }
914
915 public function getCalendarEventVars(Calendar $calendar, CalendarSlot $calendarEvent)
916 {
917 static $globalConfig = null;
918 if ($globalConfig === null) {
919 $globalConfig = [
920 'time_format' => Arr::get(get_option('_fluent_booking_settings'), 'time_format', '12'),
921 'date_formatter' => DateTimeHelper::getDateFormatter(true),
922 'isRtl' => Helper::fluentbooking_is_rtl(),
923 'has_pro' => defined('FLUENT_BOOKING_PRO_DIR_FILE'),
924 'duration_lookup' => Helper::getDurationLookup(),
925 'multi_duration_lookup' => Helper::getDurationLookup(true),
926 ];
927 }
928
929 $calendarEvent->description = wpautop($calendarEvent->description);
930 $calendarEvent->location_icon_html = $calendarEvent->defaultLocationHtml();
931 $formFields = BookingFieldService::getBookingFields($calendarEvent);
932
933 $eventData = [
934 'id' => $calendarEvent->id,
935 'max_lookup_date' => $calendarEvent->getMaxLookUpDate(),
936 'min_lookup_date' => $calendarEvent->getMinLookUpDate(),
937 'min_bookable_date' => $calendarEvent->getMinBookableDateTime(),
938 'is_display_spots' => $calendarEvent->isDisplaySpots(),
939 'duration' => $calendarEvent->getDefaultDuration(),
940 'title' => $calendarEvent->title,
941 'location_settings' => LocationService::sanitizePublicLocationSettings($calendarEvent->location_settings),
942 'location_icon_html' => $calendarEvent->location_icon_html,
943 'description' => $calendarEvent->description,
944 'pre_selects' => null,
945 'settings' => $this->sanitizePublicEventSettings($calendarEvent->settings),
946 'type' => $calendarEvent->type,
947 'event_type' => $calendarEvent->event_type,
948 'time_format' => $globalConfig['time_format'],
949 ];
950
951 $author = $calendar->getAuthorProfile(true);
952 $author['name'] = $calendar->title;
953
954 $eventVars = [
955 'slot' => $eventData,
956 'author_profile' => $author,
957 'form_fields' => $formFields,
958 'i18n' => [
959 'Schedule_Meeting' => __('Schedule Meeting', 'fluent-booking'),
960 'Continue_to_Payments' => __('Continue to Payments', 'fluent-booking'),
961 'Confirm_Payment' => __('Confirm Payment', 'fluent-booking'),
962 ],
963 'date_formatter' => $globalConfig['date_formatter'],
964 'isRtl' => $globalConfig['isRtl'],
965 'has_pro' => $globalConfig['has_pro'],
966 'duration_lookup' => $globalConfig['duration_lookup'],
967 'multi_duration_lookup' => $globalConfig['multi_duration_lookup']
968 ];
969
970 $eventVars['form_fields'] = array_values($eventVars['form_fields']);
971
972 if (!$calendar->isHostCalendar()) {
973 $eventVars['team_member_profiles'] = $calendarEvent->getAuthorProfiles(true);
974 }
975
976 return apply_filters('fluent_booking/public_event_vars', $eventVars, $calendarEvent);
977 }
978
979 private function sanitizePublicEventSettings($settings)
980 {
981 if (!is_array($settings)) {
982 return [];
983 }
984
985 $publicKeys = [
986 'recurring_config',
987 'multiple_booking',
988 'multi_duration',
989 'lock_timezone',
990 'requires_confirmation',
991 'submit_button_text',
992 ];
993
994 $publicKeys = apply_filters('fluent_booking/public_event_settings_keys', $publicKeys);
995
996 $safe = [];
997 foreach ($publicKeys as $key) {
998 if (array_key_exists($key, $settings)) {
999 $safe[$key] = $settings[$key];
1000 }
1001 }
1002
1003 return $safe;
1004 }
1005
1006 public function ajaxHandleCancelMeeting()
1007 {
1008 if (!Helper::checkRateLimit('cancel_meeting', 15)) {
1009 wp_send_json_error(['message' => __('Too many requests. Please try again in a minute.', 'fluent-booking')], 429);
1010 }
1011
1012 $data = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1013
1014 $meetingHash = Arr::get($data, 'meeting_hash');
1015
1016 $meeting = Booking::where('hash', $meetingHash)->first();
1017
1018 if (!$meeting) {
1019 wp_send_json([
1020 'message' => __('Sorry! meeting could not be found', 'fluent-booking')
1021 ], 422);
1022 }
1023
1024 if (!$meeting->canCancel()) {
1025 wp_send_json([
1026 'message' => $meeting->getCancellationMessage()
1027 ], 422);
1028 }
1029
1030 $message = sanitize_textarea_field(Arr::get($data, 'cancellation_reason', ''));
1031
1032 $cancelField = BookingFieldService::getBookingFieldByName($meeting->calendar_event, 'cancellation_reason');
1033
1034 if (!$message && Arr::isTrue($cancelField, 'required')) {
1035 wp_send_json([
1036 'message' => __('Please provide a reason for cancellation', 'fluent-booking')
1037 ], 422);
1038 }
1039
1040 $result = $meeting->cancelMeeting($message, 'guest', get_current_user_id());
1041
1042 if (is_wp_error($result)) {
1043 if (!wp_doing_ajax()) {
1044 wp_safe_redirect($meeting->getConfirmationUrl());
1045 exit();
1046 }
1047
1048 wp_send_json([
1049 'message' => $result->get_error_message()
1050 ], 422);
1051 }
1052
1053 if (wp_doing_ajax()) {
1054 wp_send_json([
1055 'message' => __('Meeting has been cancelled', 'fluent-booking')
1056 ], 200);
1057 }
1058
1059 wp_safe_redirect($meeting->getConfirmationUrl());
1060 exit;
1061 }
1062
1063 private static function sanitize_mapped_data($settings)
1064 {
1065 $sanitizerMap = [
1066 'name' => 'sanitize_text_field',
1067 'email' => 'sanitize_email',
1068 ];
1069
1070 return Helper::fcal_backend_sanitizer($settings, $sanitizerMap);
1071 }
1072 }
1073