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

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