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

1,198 lines 52.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
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 $rescheduleBy = 'guest';
455 $hostIds = $existingBooking->getHostIds();
456 if (in_array(get_current_user_id(), $hostIds) || PermissionManager::userCan(['manage_all_data', 'manage_all_bookings'])) {
457 $rescheduleBy = 'host';
458 }
459
460 $existingBooking->updateMeta('rescheduled_by_type', $rescheduleBy);
461
462 if ($rescheduleBy == 'guest' && !$existingBooking->canReschedule()) {
463 wp_send_json([
464 'message' => $existingBooking->getRescheduleMessage()
465 ], 422);
466 }
467
468 if ($bookingData['start_time'] == $existingBooking->start_time) {
469 wp_send_json([
470 'message' => __('Sorry! you can not reschedule to the same time.', 'fluent-booking')
471 ], 422);
472 }
473
474 $endDateTime = gmdate('Y-m-d H:i:s', strtotime($bookingData['start_time']) + ($existingBooking->slot_minutes * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
475
476 $previousBooking = clone $existingBooking;
477
478 if ($existingBooking->isMultiGuestBooking()) {
479 // Need to handle group booking type here
480 // check for existing group
481 $parent = Booking::where('status', 'scheduled')
482 ->where('event_id', $existingBooking->event_id)
483 ->where('start_time', $bookingData['start_time'])
484 ->orderBy('id', 'ASC')
485 ->first();
486
487 if ($parent) {
488 $existingBooking->group_id = $parent->group_id;
489 } else {
490 $existingBooking->group_id = Helper::getNextBookingGroup();
491 }
492 }
493
494 if ($existingBooking->isRoundRobinBooking()) {
495 $hostId = $bookingData['host_user_id'];
496 $existingBooking->host_user_id = $hostId;
497 $existingBooking->hosts()->sync([$hostId]);
498 }
499
500 $existingBooking->start_time = $bookingData['start_time'];
501 $existingBooking->person_time_zone = $bookingData['person_time_zone'];
502 $existingBooking->end_time = $endDateTime;
503 $existingBooking->save();
504
505 $existingBooking->updateMeta('previous_meeting_time', $previousBooking->start_time);
506
507 $reschedulingMessage = sanitize_textarea_field(Arr::get($postedData, 'rescheduling_reason'));
508 if ($reschedulingMessage) {
509 $existingBooking->updateMeta('reschedule_reason', $reschedulingMessage);
510 }
511
512 do_action('fluent_booking/log_booking_activity', [
513 'booking_id' => $existingBooking->id,
514 'type' => 'info',
515 'status' => 'closed',
516 'title' => __('Meeting Rescheduled', 'fluent-booking'),
517 /* translators: %1$s is the user who rescheduled the meeting, %2$s is the previous date and time in UTC. */
518 'description' => sprintf(__('Meeting has been rescheduled by %1$s from Web UI. Previous date time: %2$s (UTC)', 'fluent-booking'), $rescheduleBy, $previousBooking->start_time)
519 ]);
520
521 do_action('fluent_booking/after_booking_rescheduled', $existingBooking, $previousBooking, $calendarEvent);
522
523 add_filter('fluent_booking/schedule_receipt_data', function ($data) {
524 $data['title'] = __('Your meeting has been rescheduled', 'fluent-booking');
525 return $data;
526 });
527
528 $redirectUrl = $existingBooking->getRedirectUrlWithQuery();
529
530 $html = BookingService::getBookingConfirmationHtml($existingBooking);
531
532 wp_send_json(apply_filters('fluent_booking/booking_rescheduled_response', [
533 'message' => __('Booking has been rescheduled', 'fluent-booking'),
534 'redirect_url' => $redirectUrl,
535 'response_html' => $html,
536 'booking_hash' => $existingBooking->hash
537 ], $existingBooking), 200);
538
539 }, 10, 3);
540 }
541
542 private function loadGlobalVars()
543 {
544 static $loaded;
545
546 if ($loaded) {
547 return;
548 }
549
550 $loaded = true;
551
552 wp_localize_script('fluent-booking-public', 'fluentCalendarPublicVars', $this->getGlobalVars());
553 }
554
555 public function getGlobalVars()
556 {
557 $currentPerson = [
558 'name' => '',
559 'email' => ''
560 ];
561
562 if (is_user_logged_in()) {
563 $currentUser = wp_get_current_user();
564 $name = trim($currentUser->first_name . ' ' . $currentUser->last_name);
565
566 if (!$name) {
567 $name = $currentUser->display_name;
568 }
569
570 $currentPerson = [
571 'name' => $name,
572 'email' => $currentUser->user_email,
573 'user_id' => $currentUser->ID
574 ];
575 } else {
576 $request = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
577
578 // Check for url params
579 if ($name = sanitize_text_field(Arr::get($request, 'invitee_name'))) {
580 $currentPerson['name'] = $name;
581 }
582
583 if ($email = sanitize_email(Arr::get($request, 'invitee_email'))) {
584 if (is_email($email)) {
585 $currentPerson['email'] = $email;
586 }
587 }
588 }
589
590 if (empty($currentPerson['email'])) {
591 // Let's try to get from FluentCRM is exists
592 if (defined('FLUENTCRM')) {
593 $contactApi = FluentCrmApi('contacts');
594 $contact = $contactApi->getCurrentContact();
595 if ($contact) {
596 $currentPerson['email'] = $contact->email;
597 $currentPerson['name'] = $contact->full_name;
598 }
599 }
600 }
601
602 $globalSettings = Helper::getGlobalSettings();
603 $startDay = Arr::get($globalSettings, 'administration.start_day', 'mon');
604
605 $data = [
606 'ajaxurl' => admin_url('admin-ajax.php'),
607 'timezones' => DateTimeHelper::getFlatGroupedTimeZones(),
608 'current_person' => $currentPerson,
609 'start_day' => $startDay,
610 'i18' => [
611 'Timezone' => __('Timezone', 'fluent-booking'),
612 'Day' => __('Day', 'fluent-booking'),
613 'Days' => __('Days', 'fluent-booking'),
614 'Hour' => __('Hour', 'fluent-booking'),
615 'Hours' => __('Hours', 'fluent-booking'),
616 'Minute' => __('Minute', 'fluent-booking'),
617 'Minutes' => __('Minutes', 'fluent-booking'),
618 'week' => __('week', 'fluent-booking'),
619 'month' => __('month', 'fluent-booking'),
620 'year' => __('year', 'fluent-booking'),
621 'weeks' => __('weeks', 'fluent-booking'),
622 'months' => __('months', 'fluent-booking'),
623 'years' => __('years', 'fluent-booking'),
624 'Every' => __('Every', 'fluent-booking'),
625 'for' => __('for', 'fluent-booking'),
626 'Number of Occurrences' => __('Number of Occurrences', 'fluent-booking'),
627 'occurrence' => __('occurrence', 'fluent-booking'),
628 'occurrences' => __('occurrences', 'fluent-booking'),
629 'You can only book up to' => __('You can only book up to', 'fluent-booking'),
630 'at a time' => __('at a time', 'fluent-booking'),
631 'Enter Details' => __('Enter Details', 'fluent-booking'),
632 'Summary' => __('Summary', 'fluent-booking'),
633 'Payment Details' => __('Payment Details', 'fluent-booking'),
634 'Item' => __('Item', 'fluent-booking'),
635 'Price' => __('Price', 'fluent-booking'),
636 'Quantity' => __('Quantity', 'fluent-booking'),
637 'Subtotal:' => __('Subtotal:', 'fluent-booking'),
638 'Total:' => __('Total:', 'fluent-booking'),
639 'Total Payment' => __('Total Payment', 'fluent-booking'),
640 'Payment Method' => __('Payment Method', 'fluent-booking'),
641 'Pay Now' => __('Pay Now', 'fluent-booking'),
642 'processing' => __('Processing', 'fluent-booking'),
643 'date_time_config' => DateTimeHelper::getI18nDateTimeConfig(),
644 'Country' => __('Country', 'fluent-booking'),
645 '12h' => _x('12h', 'date time format switch', 'fluent-booking'),
646 '24h' => _x('24h', 'date time format switch', 'fluent-booking'),
647 'spots left' => _x('spots left', 'for how many spots left for available booking', 'fluent-booking'),
648 'spots remaining' => _x('spots remaining', 'for how many spots remaining for available booking', 'fluent-booking'),
649 'Next' => _x('Next', 'Booking form spot selection', 'fluent-booking'),
650 'Select on the Next Step' => __('Select on the Next Step', 'fluent-booking'),
651 'location options' => __('location options', 'fluent-booking'),
652 'Your address' => __('Your address', 'fluent-booking'),
653 'Organizer Phone Number' => __('Organizer Phone Number', 'fluent-booking'),
654 'In Person (Attendee Address)' => __('In Person (Attendee Address)', 'fluent-booking'),
655 'In Person (Organizer Address)' => __('In Person (Organizer Address)', 'fluent-booking'),
656 'Attendee Phone Number' => __('Attendee Phone Number', 'fluent-booking'),
657 'Google Meet' => __('Google Meet', 'fluent-booking'),
658 'Zoom Meeting' => __('Zoom Meeting', 'fluent-booking'),
659 'Online Meeting' => __('Online Meeting', 'fluent-booking'),
660 'Phone Call' => __('Phone Call', 'fluent-booking'),
661 'Processing...' => __('Processing...', 'fluent-booking'),
662 'Loading Payment Processor...' => __('Loading Payment Processor...', 'fluent-booking'),
663 'PM' => __('PM', 'fluent-booking'),
664 'AM' => __('AM', 'fluent-booking'),
665 'Name' => __('Name', 'fluent-booking'),
666 'Email' => __('Email', 'fluent-booking'),
667 'Date' => __('Date', 'fluent-booking'),
668 'Time' => __('Time', 'fluent-booking'),
669 'per occurrence' => __('per occurrence', 'fluent-booking'),
670 'per guest' => __('per guest', 'fluent-booking'),
671 'Add guest' => __('Add guest', 'fluent-booking'),
672 'Add guests' => __('Add guests', 'fluent-booking'),
673 'Add another' => __('Add another', 'fluent-booking'),
674 'Choose File' => __('Choose File', 'fluent-booking'),
675 'This field is required.' => __('This field is required.', 'fluent-booking'),
676 'No availability in' => __('No availability in', 'fluent-booking'),
677 'View next month' => __('View next month', 'fluent-booking'),
678 'View previous month' => __('View previous month', 'fluent-booking'),
679 'No_payment_method_description' => __('No activated payment method found. If you are an admin please check the event payment settings', 'fluent-booking'),
680 'Please fill up the required data' => __('Please fill up the required data', 'fluent-booking'),
681 'Please select a valid payment method' => __('Please select a valid payment method', 'fluent-booking'),
682 'Please Select' => __('Please Select', 'fluent-booking'),
683 'Something is wrong!' => __('Something is wrong!', 'fluent-booking'),
684 'Requires Confirmation' => __('Requires Confirmation', 'fluent-booking'),
685 ],
686 'theme' => Arr::get(get_option('_fluent_booking_settings'), 'theme', 'system-default'),
687 'currency_settings' => CurrenciesHelper::getGlobalCurrencySettings()
688 ];
689
690 if (isset($_SERVER['HTTP_CF_IPCOUNTRY'])) {
691 $data['user_country'] = isset($_SERVER['HTTP_CF_IPCOUNTRY']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_IPCOUNTRY'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
692 } else {
693 $data['user_country'] = Arr::get($globalSettings, 'administration.default_country', '');
694 }
695
696 return apply_filters('fluent_calendar/global_booking_vars', $data);
697 }
698
699 public function ajaxScheduleMeeting()
700 {
701 if (!Helper::checkRateLimit('schedule_meeting', 15)) {
702 wp_send_json_error(['message' => __('Too many requests. Please try again in a minute.', 'fluent-booking')], 429);
703 }
704
705 $app = App::getInstance();
706
707 $postedData = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
708
709 $eventId = (int)$postedData['event_id'];
710
711 $isRescheduling = Arr::get($postedData, 'rescheduling_hash', '');
712
713 $calendarEvent = CalendarSlot::find($eventId);
714
715 if (!$calendarEvent || ($calendarEvent->status != 'active' && !$isRescheduling)) {
716 wp_send_json([
717 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
718 ], 422);
719 }
720
721 do_action('fluent_booking/starting_scheduling_ajax', $postedData);
722
723 $rules = [
724 'name' => 'required',
725 'email' => 'required|email',
726 'timezone' => 'required',
727 'start_date' => 'required',
728 'utm_source' => 'max:192',
729 'utm_medium' => 'max:192',
730 'utm_campaign' => 'max:192',
731 'utm_term' => 'max:192',
732 'utm_content' => 'max:192',
733 ];
734
735 $messages = [
736 'name.required' => __('Please enter your name', 'fluent-booking'),
737 'email.required' => __('Please enter your email address', 'fluent-booking'),
738 'email.email' => __('Please enter provide a valid email address', 'fluent-booking'),
739 'timezone.required' => __('Please select timezone first', 'fluent-booking'),
740 'start_date.required' => __('Please select a date and time', 'fluent-booking')
741 ];
742
743 if ($calendarEvent->isPhoneRequired()) {
744 $rules['phone_number'] = 'required';
745 $messages['phone_number.required'] = __('Please provide your phone number', 'fluent-booking');
746 } else if ($calendarEvent->isAddressRequired()) {
747 $rules['address'] = 'required';
748 $messages['address.required'] = __('Please provide your Address', 'fluent-booking');
749 } else if ($calendarEvent->isLocationFieldRequired()) {
750 $rules['location_config.driver'] = 'required';
751 $messages['location_config.driver'] = __('Please select location', 'fluent-booking');
752
753 $selectedLocation = LocationService::getLocationDetails($calendarEvent, Arr::get($postedData, 'location_config', []), $postedData);
754 $selectedLocationDriver = Arr::get($selectedLocation, 'type');
755 // is user input required
756 if (in_array($selectedLocationDriver, ['in_person_guest', 'phone_guest'])) {
757 $rules['location_config.user_location_input'] = 'required';
758 if ($selectedLocationDriver == 'in_person_guest') {
759 $messages['location_config.user_location_input.required'] = __('Please provide your address', 'fluent-booking');
760 } else {
761 $messages['location_config.user_location_input.required'] = __('Please provide your phone number', 'fluent-booking');
762 }
763 }
764 }
765
766 $duration = (int)$calendarEvent->getDuration(Arr::get($postedData, 'duration', null));
767
768 if ($additionalGuests = Arr::get($postedData, 'guests', [])) {
769 if ($calendarEvent->isMultiGuestEvent()) {
770 $additionalGuests = $this->sanitize_mapped_data($additionalGuests);
771 $additionalGuests = array_values(array_filter($additionalGuests, function ($guest) {
772 return Arr::get($guest, 'name') && Arr::get($guest, 'email');
773 }));
774 } else {
775 $additionalGuests = array_filter(array_map('sanitize_email', $additionalGuests));
776 }
777 }
778
779 $postedData['guests'] = $additionalGuests;
780
781 $requiredFields = array_filter($calendarEvent->getMeta('booking_fields', []), function ($field) {
782 return Arr::isTrue($field, 'required') && Arr::isTrue($field, 'enabled') && (Arr::get($field, 'name') == 'message' || Arr::get($field, 'name') == 'guests');
783 });
784
785 foreach ($requiredFields as $field) {
786 if (empty($rules[$field['name']])) {
787 $rules[$field['name']] = 'required';
788 $messages[$field['name'] . '.required'] = __('This field is required', 'fluent-booking');
789 }
790 }
791
792 $validationConfig = apply_filters('fluent_booking/schedule_validation_rules_data', [
793 'rules' => $rules,
794 'messages' => $messages
795 ], $postedData, $calendarEvent);
796
797 $validator = $app->validator->make($postedData, $validationConfig['rules'], $validationConfig['messages']);
798 if ($validator->validate()->fails()) {
799 $errorMessage = $validator->firstError() ?: __('Please fill up the required data', 'fluent-booking');
800 wp_send_json([
801 'message' => $errorMessage,
802 'errors' => $validator->errors()
803 ], 422);
804 }
805
806 $customFieldsData = BookingFieldService::getCustomFieldsData($postedData, $calendarEvent);
807 $customFieldsData = apply_filters('fluent_booking/schedule_custom_field_data', $customFieldsData, $calendarEvent);
808
809 if (is_wp_error($customFieldsData)) {
810 wp_send_json([
811 'message' => $customFieldsData->get_error_message(),
812 'errors' => $customFieldsData->get_error_data()
813 ], 422);
814 }
815
816 $validateDateFields = BookingFieldService::validateDateFields($customFieldsData, $calendarEvent);
817
818 if (is_wp_error($validateDateFields)) {
819 wp_send_json([
820 'message' => $validateDateFields->get_error_message(),
821 ], 422);
822 }
823
824 $startDate = Arr::get($postedData, 'start_date');
825 $timezone = sanitize_text_field(Arr::get($postedData, 'timezone', 'UTC'));
826
827 if (is_array($startDate)) {
828 $startDateTime = array_slice(
829 array_map(function($date) use ($timezone) {
830 return DateTimeHelper::convertToUtc(sanitize_text_field($date), $timezone);
831 }, $startDate), 0, $calendarEvent->multiBookingLimit()
832 );
833 $endDateTime = array_map(function($date) use ($duration) {
834 return gmdate('Y-m-d H:i:s', strtotime($date) + ($duration * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
835 }, $startDateTime);
836 }
837
838 if (is_string($startDate)) {
839 $startDate = sanitize_text_field($startDate);
840 $startDateTime = DateTimeHelper::convertToUtc($startDate, $timezone);
841 $endDateTime = gmdate('Y-m-d H:i:s', strtotime($startDateTime) + ($duration * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
842 }
843
844 $bookingData = apply_filters('fluent_booking/initialize_booking_data', [
845 'person_time_zone' => sanitize_text_field($timezone),
846 'start_time' => $startDateTime,
847 'end_time' => $endDateTime,
848 'name' => sanitize_text_field($postedData['name']),
849 'email' => sanitize_email($postedData['email']),
850 'message' => sanitize_textarea_field(wp_unslash(Arr::get($postedData, 'message', ''))),
851 'phone' => sanitize_text_field(Arr::get($postedData, 'phone_number', '')),
852 'address' => sanitize_textarea_field(Arr::get($postedData, 'address', '')),
853 'ip_address' => Helper::getIp(),
854 'status' => 'scheduled',
855 'source' => 'web',
856 'event_type' => $calendarEvent->event_type,
857 'slot_minutes' => $duration,
858 'utm_source' => sanitize_text_field(Arr::get($postedData, 'utm_source', '')),
859 'utm_medium' => sanitize_text_field(Arr::get($postedData, 'utm_medium', '')),
860 'utm_campaign' => sanitize_text_field(Arr::get($postedData, 'utm_campaign', '')),
861 'utm_term' => sanitize_text_field(Arr::get($postedData, 'utm_term', '')),
862 'utm_content' => sanitize_text_field(Arr::get($postedData, 'utm_content', ''))
863 ], $postedData, $calendarEvent);
864
865 if ($calendarEvent->isConfirmationRequired($bookingData['start_time'])) {
866 $bookingData['status'] = 'pending';
867 }
868
869 $locationConfig = Arr::get($postedData, 'location_config', []);
870 $selectedLocation = LocationService::getLocationDetails($calendarEvent, $locationConfig, $postedData);
871 if ($selectedLocation['type'] == 'phone_guest') {
872 $bookingData['phone'] = $selectedLocation['description'];
873 }
874
875 $bookingData['location_details'] = $selectedLocation;
876
877 if ($sourceUrl = Arr::get($postedData, 'source_url', '')) {
878 $bookingData['source_url'] = sanitize_url($sourceUrl);
879 }
880
881 if (!empty($postedData['coupon_codes'])) {
882 $bookingData['coupon_codes'] = array_map('sanitize_text_field', array_unique($postedData['coupon_codes']));
883 }
884
885 if (!empty($postedData['payment_method'])) {
886 $customFieldsData['payment_method'] = sanitize_text_field($postedData['payment_method']);
887 }
888
889 if (!empty($postedData['recurring_count'])) {
890 $bookingData['recurring_count'] = (int) Arr::get($postedData, 'recurring_count', 0);
891 }
892
893 $timeSlotService = TimeSlotServiceHandler::initService($calendarEvent->calendar, $calendarEvent);
894
895 if (is_wp_error($timeSlotService)) {
896 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timezone);
897 }
898
899 $availableSpot = $timeSlotService->isSpotAvailable($bookingData['start_time'], $bookingData['end_time'], $duration);
900
901 if (!$availableSpot) {
902 wp_send_json([
903 'message' => __('This selected time slot is not available. Maybe someone booked the spot just a few seconds ago.', 'fluent-booking')
904 ], 422);
905 }
906
907 if ($additionalGuests) {
908 $guestField = BookingFieldService::getBookingFieldByName($calendarEvent, 'guests');
909 $guestLimit = Arr::get($guestField, 'limit', 10);
910 if ($calendarEvent->isMultiGuestEvent()) {
911 $remaining = Arr::get($availableSpot, 'remaining', $calendarEvent->getMaxBookingPerSlot());
912 $guestLimit = min($remaining, $guestLimit) - 1;
913 }
914 $bookingData['additional_guests'] = array_slice($additionalGuests, 0, $guestLimit);
915 }
916
917 if ($calendarEvent->isRoundRobin()) {
918 $bookingData['host_user_id'] = $timeSlotService->hostUserId;
919 }
920
921 do_action('fluent_booking/before_creating_schedule', $bookingData, $postedData, $calendarEvent);
922
923 try {
924 $booking = BookingService::createBooking($bookingData, $calendarEvent, $customFieldsData);
925
926 if (is_wp_error($booking)) {
927 throw new \Exception(wp_kses_post($booking->get_error_message()), 422);
928 }
929
930 } catch (\Exception $e) {
931 wp_send_json([
932 'message' => $e->getMessage()
933 ], 422);
934 return;
935 }
936
937 $redirectUrl = $booking->getRedirectUrlWithQuery();
938
939 $html = BookingService::getBookingConfirmationHtml($booking);
940
941 wp_send_json(apply_filters('fluent_booking/booking_confirmation_response', [
942 'message' => __('Booking has been confirmed', 'fluent-booking'),
943 'redirect_url' => $redirectUrl,
944 'response_html' => $html,
945 'booking_hash' => $booking->hash
946 ], $booking), 200);
947 }
948
949 public function ajaxGetAvailableDates()
950 {
951 if (!Helper::checkRateLimit('available_dates', 30)) {
952 wp_send_json_error(['message' => __('Too many requests. Please try again in a minute.', 'fluent-booking')], 429);
953 }
954
955 $startBenchmark = microtime(true);
956
957 $request = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
958
959 $eventId = (int)$request['event_id'];
960
961 $rescheduling = Arr::get($request, 'rescheduling', 'no');
962
963 $calendarEvent = CalendarSlot::findOrfail($eventId);
964
965 if (!$calendarEvent || ($calendarEvent->status != 'active' && $rescheduling == 'no')) {
966 wp_send_json([
967 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
968 ], 422);
969 }
970
971 $calendar = $calendarEvent->calendar;
972 $startDate = sanitize_text_field(Arr::get($request, 'start_date')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
973
974 if (!$startDate) {
975 $startDate = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
976 }
977
978 $timeZone = sanitize_text_field(Arr::get($request, 'timezone')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
979
980 if (!$timeZone) {
981 $timeZone = wp_timezone_string();
982 }
983
984 if (!in_array($timeZone, \DateTimeZone::listIdentifiers())) {
985 $timeZone = $calendar->author_timezone;
986 }
987
988 $duration = (int)$calendarEvent->getDuration(Arr::get($request, 'duration', null));
989
990 $timeSlotService = TimeSlotServiceHandler::initService($calendar, $calendarEvent);
991
992 if (is_wp_error($timeSlotService)) {
993 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timeZone);
994 }
995
996 $availableSpots = $timeSlotService->getAvailableSpots($startDate, $timeZone, $duration);
997
998 if (is_wp_error($availableSpots)) {
999 return TimeSlotServiceHandler::sendError($availableSpots, $calendarEvent, $timeZone);
1000 }
1001
1002 $availableSpots = array_filter((array) $availableSpots);
1003 $availableSpots = apply_filters('fluent_booking/available_slots_for_view', $availableSpots, $calendarEvent, $calendar, $timeZone, $duration);
1004
1005 wp_send_json([
1006 'available_slots' => $availableSpots,
1007 'timezone' => $timeZone,
1008 'max_lookup_date' => $calendarEvent->getMaxLookUpDate(),
1009 'execution_time' => microtime(true) - $startBenchmark
1010 ], 200);
1011 }
1012
1013 public function getCalendarEventVars(Calendar $calendar, CalendarSlot $calendarEvent)
1014 {
1015 $calendarEvent->description = wpautop($calendarEvent->description);
1016 $calendarEvent->location_icon_html = $calendarEvent->defaultLocationHtml();
1017 $formFields = BookingFieldService::getBookingFields($calendarEvent);
1018
1019 $eventData = [
1020 'id' => $calendarEvent->id,
1021 'max_lookup_date' => $calendarEvent->getMaxLookUpDate(),
1022 'min_lookup_date' => $calendarEvent->getMinLookUpDate(),
1023 'min_bookable_date' => $calendarEvent->getMinBookableDateTime(),
1024 'is_display_spots' => $calendarEvent->isDisplaySpots(),
1025 'duration' => $calendarEvent->getDefaultDuration(),
1026 'title' => $calendarEvent->title,
1027 'location_settings' => $this->sanitizePublicLocationSettings($calendarEvent->location_settings),
1028 'location_icon_html' => $calendarEvent->location_icon_html,
1029 'description' => $calendarEvent->description,
1030 'pre_selects' => null,
1031 'settings' => $this->sanitizePublicEventSettings($calendarEvent->settings),
1032 'type' => $calendarEvent->type,
1033 'event_type' => $calendarEvent->event_type,
1034 'time_format' => Arr::get(get_option('_fluent_booking_settings'), 'time_format', '12'),
1035 ];
1036
1037 $author = $calendar->getAuthorProfile(true);
1038 $author['name'] = $calendar->title;
1039
1040 $eventVars = [
1041 'slot' => $eventData,
1042 'author_profile' => $author,
1043 'form_fields' => $formFields,
1044 'i18n' => [
1045 'Schedule_Meeting' => __('Schedule Meeting', 'fluent-booking'),
1046 'Continue_to_Payments' => __('Continue to Payments', 'fluent-booking'),
1047 'Confirm_Payment' => __('Confirm Payment', 'fluent-booking'),
1048 ],
1049 'date_formatter' => DateTimeHelper::getDateFormatter(true),
1050 'isRtl' => Helper::fluentbooking_is_rtl(),
1051 'has_pro' => defined('FLUENT_BOOKING_PRO_DIR_FILE'),
1052 'duration_lookup' => Helper::getDurationLookup(),
1053 'multi_duration_lookup' => Helper::getDurationLookup(true)
1054 ];
1055
1056 $eventVars['form_fields'] = array_values($eventVars['form_fields']);
1057
1058 if (!$calendar->isHostCalendar()) {
1059 $eventVars['team_member_profiles'] = $calendarEvent->getAuthorProfiles(true);
1060 }
1061
1062 return apply_filters('fluent_booking/public_event_vars', $eventVars, $calendarEvent);
1063 }
1064
1065 private function sanitizePublicLocationSettings($locationSettings)
1066 {
1067 if (!is_array($locationSettings)) {
1068 return [];
1069 }
1070
1071 $safe = [];
1072 foreach ($locationSettings as $location) {
1073 if (!is_array($location)) {
1074 continue;
1075 }
1076
1077 $type = Arr::get($location, 'type');
1078 $displayOnBooking = Arr::get($location, 'display_on_booking') === 'yes';
1079
1080 $sanitized = [
1081 'type' => $type,
1082 'title' => Arr::get($location, 'title'),
1083 'display_on_booking' => Arr::get($location, 'display_on_booking', 'no'),
1084 ];
1085
1086 // Only expose host-private fields when the host explicitly opted
1087 // in to display them before booking.
1088 if ($displayOnBooking) {
1089 if ($type === 'online_meeting') {
1090 $sanitized['meeting_link'] = Arr::get($location, 'meeting_link');
1091 } elseif ($type === 'phone_organizer') {
1092 $sanitized['host_phone_number'] = Arr::get($location, 'host_phone_number');
1093 } elseif (in_array($type, ['in_person_organizer', 'custom'], true)) {
1094 $sanitized['description'] = Arr::get($location, 'description');
1095 }
1096 }
1097
1098 $safe[] = $sanitized;
1099 }
1100
1101 return $safe;
1102 }
1103
1104 private function sanitizePublicEventSettings($settings)
1105 {
1106 if (!is_array($settings)) {
1107 return [];
1108 }
1109
1110 $publicKeys = [
1111 'recurring_config',
1112 'multiple_booking',
1113 'multi_duration',
1114 'lock_timezone',
1115 'requires_confirmation',
1116 'submit_button_text',
1117 ];
1118
1119 $publicKeys = apply_filters('fluent_booking/public_event_settings_keys', $publicKeys);
1120
1121 $safe = [];
1122 foreach ($publicKeys as $key) {
1123 if (array_key_exists($key, $settings)) {
1124 $safe[$key] = $settings[$key];
1125 }
1126 }
1127
1128 return $safe;
1129 }
1130
1131 public function ajaxHandleCancelMeeting()
1132 {
1133 if (!Helper::checkRateLimit('cancel_meeting', 15)) {
1134 wp_send_json_error(['message' => __('Too many requests. Please try again in a minute.', 'fluent-booking')], 429);
1135 }
1136
1137 $data = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1138
1139 $meetingHash = Arr::get($data, 'meeting_hash');
1140
1141 $meeting = Booking::where('hash', $meetingHash)->first();
1142
1143 if (!$meeting) {
1144 wp_send_json([
1145 'message' => __('Sorry! meeting could not be found', 'fluent-booking')
1146 ], 422);
1147 }
1148
1149 if (!$meeting->canCancel()) {
1150 wp_send_json([
1151 'message' => $meeting->getCancellationMessage()
1152 ], 422);
1153 }
1154
1155 $message = sanitize_textarea_field(Arr::get($data, 'cancellation_reason', ''));
1156
1157 $cancelField = BookingFieldService::getBookingFieldByName($meeting->calendar_event, 'cancellation_reason');
1158
1159 if (!$message && Arr::isTrue($cancelField, 'required')) {
1160 wp_send_json([
1161 'message' => __('Please provide a reason for cancellation', 'fluent-booking')
1162 ], 422);
1163 }
1164
1165 $result = $meeting->cancelMeeting($message, 'guest', get_current_user_id());
1166
1167 if (is_wp_error($result)) {
1168 if (!wp_doing_ajax()) {
1169 wp_safe_redirect($meeting->getConfirmationUrl());
1170 exit();
1171 }
1172
1173 wp_send_json([
1174 'message' => $result->get_error_message()
1175 ], 422);
1176 }
1177
1178 if (wp_doing_ajax()) {
1179 wp_send_json([
1180 'message' => __('Meeting has been cancelled', 'fluent-booking')
1181 ], 200);
1182 }
1183
1184 wp_safe_redirect($meeting->getConfirmationUrl());
1185 exit;
1186 }
1187
1188 private static function sanitize_mapped_data($settings)
1189 {
1190 $sanitizerMap = [
1191 'name' => 'sanitize_text_field',
1192 'email' => 'sanitize_email',
1193 ];
1194
1195 return Helper::fcal_backend_sanitizer($settings, $sanitizerMap);
1196 }
1197 }
1198