PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.3.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.3.0
2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 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.3.0, at app/Hooks/Handlers/FrontEndHandler.php

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