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

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