PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 1.5.21
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v1.5.21
2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 1.7.2 All 33 releases
fluent-booking / app / Hooks / Handlers / FrontEndHandler.php

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

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