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

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