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

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

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