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

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

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