PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 1.7.1
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v1.7.1
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 1.7.1, at app/Hooks/Handlers/FrontEndHandler.php

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