PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 All 34 releases
← All changes | app/Hooks/Handlers/FrontEndHandler.php +338 -322 1.5.02 → 2.5.0 View file →
@@ -8,15 +8,19 @@
8 8 use FluentBooking\App\Models\CalendarSlot;
9 9 use FluentBooking\App\Services\BookingFieldService;
10 10 use FluentBooking\App\Services\BookingService;
11 11 use FluentBooking\App\Services\DateTimeHelper;
12 +use FluentBooking\App\Services\PublicTransStrings;
12 13 use FluentBooking\App\Services\Helper;
13 14 use FluentBooking\App\Services\LandingPage\LandingPageHandler;
15 +use FluentBooking\App\Services\LandingPage\LandingPageHelper;
14 16 use FluentBooking\App\Hooks\Handlers\TimeSlotServiceHandler;
17 +use FluentBooking\App\Services\CalendarEventService;
15 18 use FluentBooking\App\Services\LocationService;
16 -use FluentBooking\App\Services\TimeSlotService;
17 -use FluentBooking\App\Services\PermissionManager;
19 +use FluentBooking\App\Services\RescheduleService;
20 +use FluentBooking\App\Services\CurrenciesHelper;
18 21 use FluentBooking\Framework\Support\Arr;
22 +use FluentBooking\App\Vite;
19 23
20 24 class FrontEndHandler
21 25 {
22 26 public function register()
@@ -39,127 +43,9 @@
39 43
40 44 add_action('wp_ajax_fluent_cal_get_available_dates', [$this, 'ajaxGetAvailableDates']);
41 45 add_action('wp_ajax_nopriv_fluent_cal_get_available_dates', [$this, 'ajaxGetAvailableDates']);
42 46
43 - /*
44 - * Rescheduing Handlers
45 - */
46 - add_action('fluent_booking/starting_scheduling_ajax', function ($data) {
47 - if (empty($data['rescheduling_hash'])) {
48 - return;
49 - }
50 -
51 - add_filter('fluent_booking/schedule_custom_field_data', function ($array) {
52 - return [];
53 - });
54 -
55 - add_filter('fluent_booking/schedule_validation_rules_data', function ($data, $postedData, $calendarEvent)
56 - {
57 - $rules = $messages = [];
58 - $rescheduleField = BookingFieldService::getBookingFieldByName($calendarEvent, 'rescheduling_reason');
59 -
60 - if (Arr::isTrue($rescheduleField, 'required')) {
61 - $rules['rescheduling_reason'] = 'required';
62 - $messages['rescheduling_reason.required'] = __('Please provide a rescheduling reason', 'fluent-booking');
63 - }
64 -
65 - return [
66 - 'rules' => $rules,
67 - 'messages' => $messages
68 - ];
69 - }, 10, 3);
70 -
71 - add_action('fluent_booking/before_creating_schedule', function ($bookingData, $postedData, $calendarEvent) {
72 - $existingHash = Arr::get($postedData, 'rescheduling_hash');
73 - $existingBooking = Booking::where('hash', $existingHash)->first();
74 -
75 - if (!$existingBooking) {
76 - wp_send_json([
77 - 'message' => __('Invalid rescheduling request', 'fluent-booking')
78 - ], 422);
79 - }
80 -
81 - $rescheduleBy = 'guest';
82 - $hostIds = $existingBooking->getHostIds();
83 - if (in_array(get_current_user_id(), $hostIds) || PermissionManager::userCan('manage_all_bookings')) {
84 - $rescheduleBy = 'host';
85 - }
86 -
87 - $existingBooking->updateMeta('rescheduled_by_type', $rescheduleBy);
88 -
89 - if ($rescheduleBy == 'guest' && !$existingBooking->canReschedule()) {
90 - wp_send_json([
91 - 'message' => $existingBooking->getRescheduleMessage()
92 - ], 422);
93 - }
94 -
95 - if ($bookingData['start_time'] == $existingBooking->start_time) {
96 - wp_send_json([
97 - 'message' => __('Sorry! you can not reschedule to the same time.', 'fluent-booking')
98 - ], 422);
99 - }
100 -
101 - $endDateTime = gmdate('Y-m-d H:i:s', strtotime($bookingData['start_time']) + ($existingBooking->slot_minutes * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
102 -
103 - $previousBooking = clone $existingBooking;
104 -
105 - if ($existingBooking->isMultiGuestBooking()) {
106 - // Need to handle group booking type here
107 - // check for existing group
108 - $parent = Booking::where('status', 'scheduled')
109 - ->where('event_id', $existingBooking->event_id)
110 - ->where('start_time', $bookingData['start_time'])
111 - ->orderBy('id', 'ASC')
112 - ->first();
113 -
114 - if ($parent) {
115 - $existingBooking->group_id = $parent->group_id;
116 - } else {
117 - $existingBooking->group_id = Helper::getNextBookingGroup();
118 - }
119 - }
120 -
121 - $existingBooking->start_time = $bookingData['start_time'];
122 - $existingBooking->person_time_zone = $bookingData['person_time_zone'];
123 - $existingBooking->end_time = $endDateTime;
124 - $existingBooking->save();
125 -
126 - $existingBooking->updateMeta('previous_meeting_time', $previousBooking->start_time);
127 -
128 - $reschedulingMessage = sanitize_textarea_field(Arr::get($postedData, 'rescheduling_reason'));
129 - if ($reschedulingMessage) {
130 - $existingBooking->updateMeta('reschedule_reason', $reschedulingMessage);
131 - }
132 -
133 - do_action('fluent_booking/log_booking_activity', [
134 - 'booking_id' => $existingBooking->id,
135 - 'type' => 'info',
136 - 'status' => 'closed',
137 - 'title' => __('Meeting Rescheduled', 'fluent-booking'),
138 - /* translators: %1$s is the user who rescheduled the meeting, %2$s is the previous date and time in UTC. */
139 - 'description' => sprintf(__('Meeting has been rescheduled by %1$s from Web UI. Previous date time: %2$s (UTC)', 'fluent-booking'), $rescheduleBy, $previousBooking->start_time)
140 - ]);
141 -
142 - do_action('fluent_booking/after_booking_rescheduled', $existingBooking, $previousBooking, $calendarEvent);
143 -
144 - add_filter('fluent_booking/schedule_receipt_data', function ($data) {
145 - $data['title'] = __('Your meeting has been rescheduled', 'fluent-booking');
146 - return $data;
147 - });
148 -
149 - $redirectUrl = $existingBooking->getRedirectUrlWithQuery();
150 -
151 - $html = BookingService::getBookingConfirmationHtml($existingBooking);
152 -
153 - wp_send_json([
154 - 'message' => __('Booking has been rescheduled', 'fluent-booking'),
155 - 'redirect_url' => $redirectUrl,
156 - 'response_html' => $html,
157 - 'booking_hash' => $existingBooking->hash
158 - ], 200);
159 -
160 - }, 10, 3);
161 - });
47 + add_action('fluent_booking/starting_scheduling_ajax', [$this, 'handleRescheduling']);
162 48 }
163 49
164 50 public function handleBookingShortcode($atts, $content)
165 51 {
@@ -164,19 +50,23 @@
164 50 public function handleBookingShortcode($atts, $content)
165 51 {
166 52 $atts = shortcode_atts([
167 53 'id' => 0,
54 + 'theme' => 'light',
168 55 'disable_author' => 'no',
169 - 'theme' => 'light'
56 + 'hash' => ''
170 57 ], $atts);
171 58
172 - if (!$atts['id']) {
59 + if (!$atts['id'] && !$atts['hash']) {
173 60 return '';
174 61 }
175 62
176 - $calendarEvent = CalendarSlot::query()->find($atts['id']);
63 + $calendarEvent = CalendarSlot::find($atts['id']);
177 64 if (!$calendarEvent) {
178 - return '';
65 + $calendarEvent = CalendarSlot::where('hash', $atts['hash'])->first();
66 + if (!$calendarEvent) {
67 + return __('Calendar event not found', 'fluent-booking');
68 + }
179 69 }
180 70
181 71 $calendar = $calendarEvent->calendar;
182 72 if (!$calendar) {
@@ -182,29 +72,17 @@
182 72 if (!$calendar) {
183 73 return __('Calendar not found', 'fluent-booking');
184 74 }
185 75
186 - $assetUrl = App::getInstance('url.assets');
187 -
188 76 $localizeData = $this->getCalendarEventVars($calendar, $calendarEvent);
189 77 $localizeData['disable_author'] = $atts['disable_author'] == 'yes';
190 78 $localizeData['theme'] = $atts['theme'];
191 79
192 80 if (BookingFieldService::hasPhoneNumberField($localizeData['form_fields'])) {
193 - wp_enqueue_script('fluent-booking-phone-field', App::getInstance('url.assets') . 'public/js/phone-field.js', [], FLUENT_BOOKING_ASSETS_VERSION, true);
194 - add_action('fluent_booking/short_code_render', function () use ($assetUrl) {
195 - ?>
196 - <style>
197 - .fcal_phone_wrapper .flag {
198 - background: url(<?php echo esc_url($assetUrl.'images/flags_responsive.png'); ?>) no-repeat;
199 - background-size: 100%;
200 - }
201 - </style>
202 - <?php
203 - });
81 + Vite::enqueueScript('fluent-booking-phone-field', 'phone_field', [], FLUENT_BOOKING_ASSETS_VERSION);
204 82 }
205 83
206 - wp_enqueue_script('fluent-booking-public', $assetUrl . 'public/js/app.js', [], FLUENT_BOOKING_ASSETS_VERSION, true);
84 + Vite::enqueueScript('fluent-booking-public', 'public_app', [], FLUENT_BOOKING_ASSETS_VERSION);
207 85
208 86 $this->loadGlobalVars();
209 87 wp_localize_script(
210 88 'fluent-booking-public',
@@ -248,18 +126,26 @@
248 126 $calendarIds[] = $event->calendar_id;
249 127 if (!isset($calendarEvents[$event->calendar_id])) {
250 128 $calendarEvents[$event->calendar_id] = [];
251 129 }
252 - $event->durations = $event->getAvailableDurations();
253 - $event->description = $event->getDescription();
254 - $event->short_description = Helper::excerpt($event->description);
130 + $event = CalendarEventService::processEvent($event);
255 131 $calendarEvents[$event->calendar_id][] = $event;
256 132 }
257 133
258 - $calendars = Calendar::query()->whereIn('id', $calendarIds)->get();
134 + $calendars = Calendar::query()->with('metas')->whereIn('id', $calendarIds)->get();
259 135
260 136 foreach ($calendars as $calendar) {
261 - $calendar->activeEvents = $calendarEvents[$calendar->id];
137 + $calendar->activeEvents = $calendarEvents[$calendar->id] ?? [];
138 + $eventOrder = $calendar->getMeta('event_order');
139 + if (!empty($eventOrder)) {
140 + $eventsArray = $calendar->activeEvents;
141 + usort($eventsArray, function($a, $b) use ($eventOrder) {
142 + $posA = array_search($a->id, $eventOrder);
143 + $posB = array_search($b->id, $eventOrder);
144 + return $posA - $posB;
145 + });
146 + $calendar->activeEvents = $eventsArray;
147 + }
262 148 }
263 149
264 150 return $this->renderTeamHosts($calendars, [
265 151 'title' => $atts['title'],
@@ -271,9 +157,9 @@
271 157
272 158 public function renderTeamHosts($calendars, $headerConfig = [])
273 159 {
274 160 $wrapperId = 'fcal_team_' . Helper::getNextIndex();
275 - wp_enqueue_script('fluent-booking-team', App::getInstance('url.assets') . 'public/js/team_app.js', [], FLUENT_BOOKING_ASSETS_VERSION, true);
161 + Vite::enqueueScript('fluent-booking-team', 'team_app', [], FLUENT_BOOKING_ASSETS_VERSION);
276 162
277 163 $vars = [];
278 164 foreach ($calendars as $calendar) {
279 165 $hostHtml = (string)(string)\FluentBooking\App\App::getInstance('view')->make('landing.author_html', [
@@ -303,10 +189,9 @@
303 189 }
304 190
305 191 wp_localize_script('fluent-booking-team', $wrapperId, $vars);
306 192
307 - $assetUrl = App::getInstance('url.assets');
308 - wp_enqueue_script('fluent-booking-public', $assetUrl . 'public/js/app.js', [], FLUENT_BOOKING_ASSETS_VERSION, true);
193 + Vite::enqueueScript('fluent-booking-public', 'public_app', [], FLUENT_BOOKING_ASSETS_VERSION);
309 194 $this->loadGlobalVars();
310 195
311 196 return App::make('view')->make('public.team_page', [
312 197 'hosts' => $calendars,
@@ -344,15 +229,29 @@
344 229 if (!$calendar) {
345 230 return '';
346 231 }
347 232
233 + $settings = LandingPageHelper::getSettings($calendar, 'public');
234 +
348 235 $calendarEventQuery = CalendarSlot::where('calendar_id', $calendar->id)
349 236 ->where('status', 'active');
350 -
237 +
238 + $enabledEvents = [];
239 + $isEnabledOnly = false;
240 + if ($settings['show_type'] != 'all') {
241 + $isEnabledOnly = true;
242 + $enabledEvents = $settings['enabled_slots'];
243 + }
244 +
351 245 if ($eventIds && $eventIds != 'all') {
352 - $calendarEventQuery->whereIn('id', $eventIds);
246 + $isEnabledOnly = true;
247 + $enabledEvents = !empty($enabledEvents) ? array_intersect($enabledEvents, $eventIds) : $eventIds;
353 248 }
354 249
250 + if (!empty($enabledEvents) || $isEnabledOnly) {
251 + $calendarEventQuery->whereIn('id', $enabledEvents);
252 + }
253 +
355 254 $calendarEvents = $calendarEventQuery->get();
356 255
357 256 if ($calendarEvents->isEmpty()) {
358 257 return '';
@@ -357,15 +256,10 @@
357 256 if ($calendarEvents->isEmpty()) {
358 257 return '';
359 258 }
360 259
361 - foreach ($calendarEvents as $event) {
362 - $event->public_url = $event->getPublicUrl();
363 - $event->durations = $event->getAvailableDurations();
364 - $event->description = $event->getDescription();
365 - $event->short_description = Helper::excerpt($event->description);
366 - }
367 -
260 + $calendarEvents = CalendarEventService::processEvents($calendar, $calendarEvents);
261 +
368 262 $calendar->activeEvents = $calendarEvents;
369 263
370 264 return $this->renderCalendarBlock($calendar, [
371 265 'title' => $title,
@@ -377,10 +271,10 @@
377 271 }
378 272
379 273 public function renderCalendarBlock($calendar, $headerConfig = [])
380 274 {
381 - $wrapperId = 'fcal_team_' . Helper::getNextIndex();
382 - wp_enqueue_script('fluent-booking-calendar', App::getInstance('url.assets') . 'public/js/calendar_app.js', [], FLUENT_BOOKING_ASSETS_VERSION, true);
275 + $wrapperId = 'fcal_calendar_' . Helper::getNextIndex();
276 + Vite::enqueueScript('fluent-booking-calendar', 'calendar_app', [], FLUENT_BOOKING_ASSETS_VERSION);
383 277
384 278 $calendarHtml = (string)(string)\FluentBooking\App\App::getInstance('view')->make('landing.author_html', [
385 279 'author' => $calendar->getAuthorProfile(),
386 280 'calendar' => $calendar,
@@ -407,10 +301,9 @@
407 301 }
408 302
409 303 wp_localize_script('fluent-booking-calendar', $wrapperId, $vars);
410 304
411 - $assetUrl = App::getInstance('url.assets');
412 - wp_enqueue_script('fluent-booking-public', $assetUrl . 'public/js/app.js', [], FLUENT_BOOKING_ASSETS_VERSION, true);
305 + Vite::enqueueScript('fluent-booking-public', 'public_app', [], FLUENT_BOOKING_ASSETS_VERSION);
413 306 $this->loadGlobalVars();
414 307
415 308 return App::make('view')->make('public.calendar_page', [
416 309 'calendar' => $calendar,
@@ -417,9 +310,10 @@
417 310 'wrapper_id' => $wrapperId,
418 311 'logo' => Arr::get($headerConfig, 'logo', ''),
419 312 'title' => Arr::get($headerConfig, 'title', ''),
420 313 'description' => Arr::get($headerConfig, 'description', ''),
421 - 'wrapper_class' => Arr::get($headerConfig, 'wrapper_class', '')
314 + 'wrapper_class' => Arr::get($headerConfig, 'wrapper_class', ''),
315 + 'hide_info' => Arr::isTrue($headerConfig, 'hide_info')
422 316 ]);
423 317 }
424 318
425 319 public function handleBookingListsShortcode($atts, $content)
@@ -432,9 +326,9 @@
432 326 'calendar_ids' => 'all',
433 327 'no_bookings' => __('No bookings found', 'fluent-booking'),
434 328 'per_page' => 10
435 329 ], $atts);
436 -
330 +
437 331 $atts['title'] = sanitize_text_field($atts['title']);
438 332 $atts['filter'] = sanitize_text_field($atts['filter']);
439 333 $atts['pagination'] = sanitize_text_field($atts['pagination']);
440 334 $atts['no_bookings'] = sanitize_text_field($atts['no_bookings']);
@@ -439,15 +333,15 @@
439 333 $atts['pagination'] = sanitize_text_field($atts['pagination']);
440 334 $atts['no_bookings'] = sanitize_text_field($atts['no_bookings']);
441 335
442 336 $userData = get_userdata(get_current_user_id());
443 -
337 +
444 338 $userEmail = $userData ? $userData->user_email : null;
445 -
339 +
446 340 if (!$userEmail) {
447 341 return __('Please login to view your bookings', 'fluent-booking');
448 342 }
449 -
343 +
450 344 $data = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
451 345
452 346 $perPage = intval(Arr::get($data, 'booking_per_page', $atts['per_page']));
453 347 $currentPage = intval(Arr::get($data, 'booking_page', 1));
@@ -454,10 +348,10 @@
454 348 $bookingPeriod = sanitize_text_field(Arr::get($data, 'booking_period', $atts['period']));
455 349
456 350 $bookingQuery = Booking::query()->with('calendar_event')
457 351 ->where('email', $userEmail)
458 - ->orderBy('start_time', 'DESC')
459 - ->applyComputedStatus($bookingPeriod);
352 + ->applyComputedStatus($bookingPeriod)
353 + ->applyBookingOrderByStatus($bookingPeriod);
460 354
461 355 if ($atts['calendar_ids'] != 'all') {
462 356 $atts['calendar_ids'] = array_map('intval', explode(',', $atts['calendar_ids']));
463 357 $bookingQuery->whereIn('calendar_id', $atts['calendar_ids']);
@@ -473,9 +367,9 @@
473 367 $booking->booking_status_text = $booking->getBookingStatus();
474 368 $booking->payment_status_text = $booking->getPaymentStatus();
475 369
476 370 $booking->booking_date = DateTimeHelper::formatToLocale($booking->getAttendeeStartTime(), 'date');
477 - $booking->booking_time = DateTimeHelper::formatToLocale($booking->getAttendeeEndTime(), 'time') . ' - ' . DateTimeHelper::formatToLocale($booking->getAttendeeEndTime(), 'time');
371 + $booking->booking_time = DateTimeHelper::formatToLocale($booking->getAttendeeStartTime(), 'time') . ' - ' . DateTimeHelper::formatToLocale($booking->getAttendeeEndTime(), 'time');
478 372 }
479 373
480 374 $currentPage = $bookings->currentPage();
481 375 $lastPage = $bookings->lastPage();
@@ -493,9 +387,9 @@
493 387 $periodOptions = Helper::getBookingPeriodOptions();
494 388
495 389 $pageOptions = apply_filters('fluent_booking/booking_per_page_options', [5, 10, 15, 20, 50, 100]);
496 390
497 - wp_enqueue_script('fluent-booking-list', App::getInstance('url.assets') . 'public/js/bookings.js', [], FLUENT_BOOKING_ASSETS_VERSION, true);
391 + Vite::enqueueScript('fluent-booking-list', 'bookings', [], FLUENT_BOOKING_ASSETS_VERSION);
498 392
499 393 return App::make('view')->make('public.bookings', [
500 394 'bookings' => $bookings,
501 395 'attributes' => $atts,
@@ -513,13 +407,88 @@
513 407 if (!isset($_REQUEST['hash'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
514 408 return __('Booking hash is missing!', 'fluent-booking');
515 409 }
516 410
517 - $hash = sanitize_text_field($_REQUEST['hash']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
411 + $hash = isset($_REQUEST['hash']) ? sanitize_text_field(wp_unslash($_REQUEST['hash'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
518 412
519 413 return apply_filters('fluent_booking/payment_receipt_html', '', $hash);
520 414 }
521 415
416 + public function handleRescheduling($data)
417 + {
418 + if (empty($data['rescheduling_hash'])) {
419 + return;
420 + }
421 +
422 + add_filter('fluent_booking/schedule_custom_field_data', function ($data) {
423 + return [];
424 + });
425 +
426 + add_filter('fluent_booking/schedule_validation_rules_data', function ($data, $postedData, $calendarEvent)
427 + {
428 + $rules = $messages = [];
429 + $rescheduleField = BookingFieldService::getBookingFieldByName($calendarEvent, 'rescheduling_reason');
430 +
431 + if (Arr::isTrue($rescheduleField, 'required')) {
432 + $rules['rescheduling_reason'] = 'required';
433 + $messages['rescheduling_reason.required'] = __('Please provide a rescheduling reason', 'fluent-booking');
434 + }
435 +
436 + return [
437 + 'rules' => $rules,
438 + 'messages' => $messages
439 + ];
440 + }, 10, 3);
441 +
442 + add_action('fluent_booking/before_creating_schedule', function ($bookingData, $postedData, $calendarEvent) {
443 + $existingHash = Arr::get($postedData, 'rescheduling_hash');
444 + $existingBooking = Booking::where('hash', $existingHash)->first();
445 +
446 + if (!$existingBooking) {
447 + wp_send_json([
448 + 'message' => __('Invalid rescheduling request', 'fluent-booking')
449 + ], 422);
450 + }
451 +
452 + $result = RescheduleService::reschedule(
453 + $existingBooking,
454 + $calendarEvent,
455 + $bookingData['start_time'],
456 + $bookingData['person_time_zone'],
457 + [
458 + 'reason' => Arr::get($postedData, 'rescheduling_reason', ''),
459 + 'host_user_id' => Arr::get($bookingData, 'host_user_id'),
460 + 'source' => __('Web UI', 'fluent-booking')
461 + ]
462 + );
463 +
464 + if (is_wp_error($result)) {
465 + wp_send_json([
466 + 'message' => $result->get_error_message()
467 + ], 422);
468 + }
469 +
470 + $existingBooking = $result;
471 +
472 + add_filter('fluent_booking/schedule_receipt_data', function ($data) {
473 + $data['title'] = __('Your meeting has been rescheduled', 'fluent-booking');
474 + return $data;
475 + });
476 +
477 + $redirectUrl = $existingBooking->getRedirectUrlWithQuery();
478 +
479 + $html = BookingService::getBookingConfirmationHtml($existingBooking);
480 +
481 + wp_send_json(apply_filters('fluent_booking/booking_rescheduled_response', [
482 + 'message' => __('Booking has been rescheduled', 'fluent-booking'),
483 + 'redirect_url' => $redirectUrl,
484 + 'response_html' => $html,
485 + 'booking_hash' => $existingBooking->hash
486 + ], $existingBooking), 200);
487 +
488 + }, 10, 3);
489 + }
490 +
522 491 private function loadGlobalVars()
523 492 {
524 493 static $loaded;
525 494
@@ -582,113 +551,22 @@
582 551 $globalSettings = Helper::getGlobalSettings();
583 552 $startDay = Arr::get($globalSettings, 'administration.start_day', 'mon');
584 553
585 554 $data = [
586 - 'ajaxurl' => admin_url('admin-ajax.php'),
587 - 'timezones' => DateTimeHelper::getFlatGroupedTimeZones(),
588 - 'current_person' => $currentPerson,
589 - 'start_day' => $startDay,
590 - 'i18' => [
591 - 'Timezone' => __('Timezone', 'fluent-booking'),
592 - 'Minutes' => __('Minutes', 'fluent-booking'),
593 - 'Enter Details' => __('Enter Details', 'fluent-booking'),
594 - 'Summary' => __('Summary', 'fluent-booking'),
595 - 'Payment Details' => __('Payment Details', 'fluent-booking'),
596 - 'Total Payment' => __('Total Payment', 'fluent-booking'),
597 - 'Payment Method' => __('Payment Method', 'fluent-booking'),
598 - 'Pay Now' => __('Pay Now', 'fluent-booking'),
599 - 'processing' => __('Processing', 'fluent-booking'),
600 - 'date_time_config' => [
601 - 'weekdays' => array(
602 - 'sunday' => _x('Sunday', 'calendar day full', 'fluent-booking'),
603 - 'monday' => _x('Monday', 'calendar day full', 'fluent-booking'),
604 - 'tuesday' => _x('Tuesday', 'calendar day full', 'fluent-booking'),
605 - 'wednesday' => _x('Wednesday', 'calendar day full', 'fluent-booking'),
606 - 'thursday' => _x('Thursday', 'calendar day full', 'fluent-booking'),
607 - 'friday' => _x('Friday', 'calendar day full', 'fluent-booking'),
608 - 'saturday' => _x('Saturday', 'calendar day full', 'fluent-booking'),
609 - ),
610 - 'months' => array(
611 - 'January' => _x('January', 'calendar month name full', 'fluent-booking'),
612 - 'February' => _x('February', 'calendar month name full', 'fluent-booking'),
613 - 'March' => _x('March', 'calendar month name full', 'fluent-booking'),
614 - 'April' => _x('April', 'calendar month name full', 'fluent-booking'),
615 - 'May' => _x('May', 'calendar month name full', 'fluent-booking'),
616 - 'June' => _x('June', 'calendar month name full', 'fluent-booking'),
617 - 'July' => _x('July', 'calendar month name full', 'fluent-booking'),
618 - 'August' => _x('August', 'calendar month name full', 'fluent-booking'),
619 - 'September' => _x('September', 'calendar month name full', 'fluent-booking'),
620 - 'October' => _x('October', 'calendar month name full', 'fluent-booking'),
621 - 'November' => _x('November', 'calendar month name full', 'fluent-booking'),
622 - 'December' => _x('December', 'calendar month name full', 'fluent-booking')
623 - ),
624 - 'weekdaysShort' => array(
625 - 'sun' => _x('Sun', 'calendar day short', 'fluent-booking'),
626 - 'mon' => _x('Mon', 'calendar day short', 'fluent-booking'),
627 - 'tue' => _x('Tue', 'calendar day short', 'fluent-booking'),
628 - 'wed' => _x('Wed', 'calendar day short', 'fluent-booking'),
629 - 'thu' => _x('Thu', 'calendar day short', 'fluent-booking'),
630 - 'fri' => _x('Fri', 'calendar day short', 'fluent-booking'),
631 - 'sat' => _x('Sat', 'calendar day short', 'fluent-booking')
632 - ),
633 - 'monthsShort' => array(
634 - 'jan' => _x('Jan', 'calendar month name short', 'fluent-booking'),
635 - 'feb' => _x('Feb', 'calendar month name short', 'fluent-booking'),
636 - 'mar' => _x('Mar', 'calendar month name short', 'fluent-booking'),
637 - 'apr' => _x('Apr', 'calendar month name short', 'fluent-booking'),
638 - 'may' => _x('May', 'calendar month name short', 'fluent-booking'),
639 - 'jun' => _x('Jun', 'calendar month name short', 'fluent-booking'),
640 - 'jul' => _x('Jul', 'calendar month name short', 'fluent-booking'),
641 - 'aug' => _x('Aug', 'calendar month name short', 'fluent-booking'),
642 - 'sep' => _x('Sep', 'calendar month name short', 'fluent-booking'),
643 - 'oct' => _x('Oct', 'calendar month name short', 'fluent-booking'),
644 - 'nov' => _x('Nov', 'calendar month name short', 'fluent-booking'),
645 - 'dec' => _x('Dec', 'calendar month name short', 'fluent-booking')
646 - ),
647 - 'numericSystem' => _x('0_1_2_3_4_5_6_7_8_9', 'calendar numeric system - Sequence must need to maintained', 'fluent-booking'),
648 - ],
649 - 'Country' => __('Country', 'fluent-booking'),
650 - '12h' => _x('12h', 'date time format switch', 'fluent-booking'),
651 - '24h' => _x('24h', 'date time format switch', 'fluent-booking'),
652 - 'spots left' => _x('spots left', 'for how many spots left for available booking', 'fluent-booking'),
653 - 'spots remaining' => _x('spots remaining', 'for how many spots remaining for available booking', 'fluent-booking'),
654 - 'Next' => _x('Next', 'Booking form spot selection', 'fluent-booking'),
655 - 'Select on the Next Step' => __('Select on the Next Step', 'fluent-booking'),
656 - 'location options' => __('location options', 'fluent-booking'),
657 - 'Your address' => __('Your address', 'fluent-booking'),
658 - 'Organizer Phone Number' => __('Organizer Phone Number', 'fluent-booking'),
659 - 'In Person (Attendee Address)' => __('In Person (Attendee Address)', 'fluent-booking'),
660 - 'In Person (Organizer Address)' => __('In Person (Organizer Address)', 'fluent-booking'),
661 - 'Attendee Phone Number' => __('Attendee Phone Number', 'fluent-booking'),
662 - 'Google Meet' => __('Google Meet', 'fluent-booking'),
663 - 'Zoom Meeting' => __('Zoom Meeting', 'fluent-booking'),
664 - 'Online Meeting' => __('Online Meeting', 'fluent-booking'),
665 - 'Phone Call' => __('Phone Call', 'fluent-booking'),
666 - 'Processing...' => __('Processing...', 'fluent-booking'),
667 - 'Loading Payment Processor...' => __('Loading Payment Processor...', 'fluent-booking'),
668 - 'PM' => __('PM', 'fluent-booking'),
669 - 'AM' => __('AM', 'fluent-booking'),
670 - 'Email' => __('Email', 'fluent-booking'),
671 - 'Date' => __('Date', 'fluent-booking'),
672 - 'Time' => __('Time', 'fluent-booking'),
673 - 'Add guests' => __('Add guests', 'fluent-booking'),
674 - 'Add another' => __('Add another', 'fluent-booking'),
675 - 'This field is required.' => __('This field is required.', 'fluent-booking'),
676 - 'No availability in' => __('No availability in', 'fluent-booking'),
677 - 'View next month' => __('View next month', 'fluent-booking'),
678 - 'View previous month' => __('View previous month', 'fluent-booking'),
679 - 'No_payment_method_description' => __('No activated payment method found. If you are an admin please check the event payment settings', 'fluent-booking'),
680 - 'Please fill up the required data' => __('Please fill up the required data', 'fluent-booking'),
681 - 'Please select a valid payment method' => __('Please select a valid payment method', 'fluent-booking'),
682 - 'Please Select' => __('Please Select', 'fluent-booking'),
683 - 'Something is wrong!' => __('Something is wrong!', 'fluent-booking'),
684 - 'Requires Confirmation' => __('Requires Confirmation', 'fluent-booking'),
685 - ],
686 - 'theme' => Arr::get(get_option('_fluent_booking_settings'), 'theme','system-default')
555 + 'ajaxurl' => admin_url('admin-ajax.php'),
556 + 'timezones' => DateTimeHelper::getFlatGroupedTimeZones(),
557 + 'current_person' => $currentPerson,
558 + 'start_day' => $startDay,
559 + // Generated from the i18() calls in resources/public; see scripts/i18n.js.
560 + 'i18' => array_merge(PublicTransStrings::getStrings(), [
561 + 'date_time_config' => DateTimeHelper::getI18nDateTimeConfig(),
562 + ]),
563 + 'theme' => Arr::get(get_option('_fluent_booking_settings'), 'theme', 'system-default'),
564 + 'currency_settings' => CurrenciesHelper::getGlobalCurrencySettings()
687 565 ];
688 566
689 567 if (isset($_SERVER['HTTP_CF_IPCOUNTRY'])) {
690 - $data['user_country'] = sanitize_text_field($_SERVER['HTTP_CF_IPCOUNTRY']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
568 + $data['user_country'] = isset($_SERVER['HTTP_CF_IPCOUNTRY']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_IPCOUNTRY'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
691 569 } else {
692 570 $data['user_country'] = Arr::get($globalSettings, 'administration.default_country', '');
693 571 }
694 572
@@ -696,8 +574,12 @@
696 574 }
697 575
698 576 public function ajaxScheduleMeeting()
699 577 {
578 + if (!Helper::checkRateLimit('schedule_meeting', 15)) {
579 + wp_send_json_error(['message' => __('Too many requests. Please try again in a minute.', 'fluent-booking')], 429);
580 + }
581 +
700 582 $app = App::getInstance();
701 583
702 584 $postedData = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
703 585
@@ -708,8 +590,9 @@
708 590 $calendarEvent = CalendarSlot::find($eventId);
709 591
710 592 if (!$calendarEvent || ($calendarEvent->status != 'active' && !$isRescheduling)) {
711 593 wp_send_json([
594 + 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
712 595 ], 422);
713 596 }
714 597
715 598 do_action('fluent_booking/starting_scheduling_ajax', $postedData);
@@ -714,12 +597,17 @@
714 597
715 598 do_action('fluent_booking/starting_scheduling_ajax', $postedData);
716 599
717 600 $rules = [
718 - 'name' => 'required',
719 - 'email' => 'required|email',
720 - 'timezone' => 'required',
721 - 'start_date' => 'required'
601 + 'name' => 'required',
602 + 'email' => 'required|email',
603 + 'timezone' => 'required',
604 + 'start_date' => 'required',
605 + 'utm_source' => 'max:192',
606 + 'utm_medium' => 'max:192',
607 + 'utm_campaign' => 'max:192',
608 + 'utm_term' => 'max:192',
609 + 'utm_content' => 'max:192',
722 610 ];
723 611
724 612 $messages = [
725 613 'name.required' => __('Please enter your name', 'fluent-booking'),
@@ -729,9 +617,9 @@
729 617 'start_date.required' => __('Please select a date and time', 'fluent-booking')
730 618 ];
731 619
732 620 if ($calendarEvent->isPhoneRequired()) {
733 - $rules['phone_number'] = 'required';
621 + $rules['phone_number'] = ['required', $this->validPhoneNumberRule()];
734 622 $messages['phone_number.required'] = __('Please provide your phone number', 'fluent-booking');
735 623 } else if ($calendarEvent->isAddressRequired()) {
736 624 $rules['address'] = 'required';
737 625 $messages['address.required'] = __('Please provide your Address', 'fluent-booking');
@@ -742,27 +630,32 @@
742 630 $selectedLocation = LocationService::getLocationDetails($calendarEvent, Arr::get($postedData, 'location_config', []), $postedData);
743 631 $selectedLocationDriver = Arr::get($selectedLocation, 'type');
744 632 // is user input required
745 633 if (in_array($selectedLocationDriver, ['in_person_guest', 'phone_guest'])) {
746 - $rules['location_config.user_location_input'] = 'required';
747 634 if ($selectedLocationDriver == 'in_person_guest') {
635 + $rules['location_config.user_location_input'] = 'required';
748 636 $messages['location_config.user_location_input.required'] = __('Please provide your address', 'fluent-booking');
749 637 } else {
638 + $rules['location_config.user_location_input'] = ['required', $this->validPhoneNumberRule()];
750 639 $messages['location_config.user_location_input.required'] = __('Please provide your phone number', 'fluent-booking');
751 640 }
752 641 }
753 642 }
754 643
755 - $duration = (int)$calendarEvent->getDuration(Arr::get($postedData, 'duration', null));
644 + $duration = (int)$calendarEvent->getDuration(Arr::get($postedData, 'duration', null));
756 645
757 - if ($calendarEvent->isPaymentEnabled($duration)) {
758 - $rules['payment_method'] = 'required';
759 - $messages['payment_method.required'] = __('Please select a valid payment method', 'fluent-booking');
646 + if ($additionalGuests = Arr::get($postedData, 'guests', [])) {
647 + if ($calendarEvent->isMultiGuestEvent()) {
648 + $additionalGuests = $this->sanitize_mapped_data($additionalGuests);
649 + $additionalGuests = array_values(array_filter($additionalGuests, function ($guest) {
650 + return Arr::get($guest, 'name') && Arr::get($guest, 'email');
651 + }));
652 + } else {
653 + $additionalGuests = array_filter(array_map('sanitize_email', $additionalGuests));
654 + }
760 655 }
761 656
762 - if ($additionalGuests = Arr::get($postedData, 'guests', [])) {
763 - $postedData['guests'] = array_filter(array_map('sanitize_email', $additionalGuests));
764 - }
657 + $postedData['guests'] = $additionalGuests;
765 658
766 659 $requiredFields = array_filter($calendarEvent->getMeta('booking_fields', []), function ($field) {
767 660 return Arr::isTrue($field, 'required') && Arr::isTrue($field, 'enabled') && (Arr::get($field, 'name') == 'message' || Arr::get($field, 'name') == 'guests');
768 661 });
@@ -780,17 +673,17 @@
780 673 ], $postedData, $calendarEvent);
781 674
782 675 $validator = $app->validator->make($postedData, $validationConfig['rules'], $validationConfig['messages']);
783 676 if ($validator->validate()->fails()) {
677 + $errorMessage = $validator->firstError() ?: __('Please fill up the required data', 'fluent-booking');
784 678 wp_send_json([
785 - 'message' => __('Please fill up the required data', 'fluent-booking'),
679 + 'message' => $errorMessage,
786 680 'errors' => $validator->errors()
787 681 ], 422);
788 - return;
789 682 }
790 683
791 684 $customFieldsData = BookingFieldService::getCustomFieldsData($postedData, $calendarEvent);
792 - $customFieldsData = apply_filters('fluent_booking/schedule_custom_field_data', $customFieldsData, $customFieldsData, $calendarEvent);
685 + $customFieldsData = apply_filters('fluent_booking/schedule_custom_field_data', $customFieldsData, $calendarEvent);
793 686
794 687 if (is_wp_error($customFieldsData)) {
795 688 wp_send_json([
796 689 'message' => $customFieldsData->get_error_message(),
@@ -795,37 +688,65 @@
795 688 wp_send_json([
796 689 'message' => $customFieldsData->get_error_message(),
797 690 'errors' => $customFieldsData->get_error_data()
798 691 ], 422);
799 - return;
800 692 }
801 693
802 - $startDate = sanitize_text_field(Arr::get($postedData, 'start_date'));
803 - $timezone = sanitize_text_field(Arr::get($postedData, 'timezone', 'UTC'));
694 + $validateDateFields = BookingFieldService::validateDateFields($customFieldsData, $calendarEvent);
804 695
805 - $startDateTime = DateTimeHelper::convertToUtc($startDate, $timezone);
806 - $endDateTime = gmdate('Y-m-d H:i:s', strtotime($startDateTime) + ($duration * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
696 + if (is_wp_error($validateDateFields)) {
697 + wp_send_json([
698 + 'message' => $validateDateFields->get_error_message(),
699 + ], 422);
700 + }
807 701
808 - $bookingData = [
702 + $startDate = Arr::get($postedData, 'start_date');
703 + $timezone = sanitize_text_field(Arr::get($postedData, 'timezone', 'UTC'));
704 +
705 + if (is_array($startDate)) {
706 + $startDateTime = array_slice(
707 + array_map(function($date) use ($timezone) {
708 + return DateTimeHelper::convertToUtc(sanitize_text_field($date), $timezone);
709 + }, $startDate), 0, $calendarEvent->multiBookingLimit()
710 + );
711 + $endDateTime = array_map(function($date) use ($duration) {
712 + return gmdate('Y-m-d H:i:s', strtotime($date) + ($duration * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
713 + }, $startDateTime);
714 + }
715 +
716 + if (is_string($startDate)) {
717 + $startDate = sanitize_text_field($startDate);
718 + $startDateTime = DateTimeHelper::convertToUtc($startDate, $timezone);
719 + $endDateTime = gmdate('Y-m-d H:i:s', strtotime($startDateTime) + ($duration * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
720 + }
721 +
722 + $bookingData = apply_filters('fluent_booking/initialize_booking_data', [
809 723 'person_time_zone' => sanitize_text_field($timezone),
810 724 'start_time' => $startDateTime,
725 + 'end_time' => $endDateTime,
811 726 'name' => sanitize_text_field($postedData['name']),
812 727 'email' => sanitize_email($postedData['email']),
813 728 'message' => sanitize_textarea_field(wp_unslash(Arr::get($postedData, 'message', ''))),
814 - 'phone' => sanitize_textarea_field(Arr::get($postedData, 'phone_number', '')),
729 + 'phone' => sanitize_text_field(Arr::get($postedData, 'phone_number', '')),
815 730 'address' => sanitize_textarea_field(Arr::get($postedData, 'address', '')),
816 731 'ip_address' => Helper::getIp(),
817 732 'status' => 'scheduled',
818 733 'source' => 'web',
819 734 'event_type' => $calendarEvent->event_type,
820 - 'slot_minutes' => $duration
821 - ];
735 + 'slot_minutes' => $duration,
736 + 'utm_source' => sanitize_text_field(Arr::get($postedData, 'utm_source', '')),
737 + 'utm_medium' => sanitize_text_field(Arr::get($postedData, 'utm_medium', '')),
738 + 'utm_campaign' => sanitize_text_field(Arr::get($postedData, 'utm_campaign', '')),
739 + 'utm_term' => sanitize_text_field(Arr::get($postedData, 'utm_term', '')),
740 + 'utm_content' => sanitize_text_field(Arr::get($postedData, 'utm_content', ''))
741 + ], $postedData, $calendarEvent);
822 742
823 - if ($calendarEvent->isConfirmationRequired($startDateTime)) {
743 + if ($calendarEvent->isConfirmationRequired($bookingData['start_time'])) {
824 744 $bookingData['status'] = 'pending';
825 745 }
826 746
827 - $selectedLocation = LocationService::getLocationDetails($calendarEvent, Arr::get($postedData, 'location_config', []), $postedData);
747 + $locationConfig = Arr::get($postedData, 'location_config', []);
748 + $selectedLocation = LocationService::getLocationDetails($calendarEvent, $locationConfig, $postedData);
828 749 if ($selectedLocation['type'] == 'phone_guest') {
829 750 $bookingData['phone'] = $selectedLocation['description'];
830 751 }
831 752
@@ -834,16 +755,18 @@
834 755 if ($sourceUrl = Arr::get($postedData, 'source_url', '')) {
835 756 $bookingData['source_url'] = sanitize_url($sourceUrl);
836 757 }
837 758
759 + if (!empty($postedData['coupon_codes'])) {
760 + $bookingData['coupon_codes'] = array_map('sanitize_text_field', array_unique($postedData['coupon_codes']));
761 + }
762 +
838 763 if (!empty($postedData['payment_method'])) {
839 - $customFieldsData['payment_method'] = $postedData['payment_method'];
764 + $customFieldsData['payment_method'] = sanitize_text_field($postedData['payment_method']);
840 765 }
841 766
842 - if ($additionalGuests) {
843 - $guestField = BookingFieldService::getBookingFieldByName($calendarEvent, 'guests');
844 - $guestLimit = Arr::get($guestField, 'limit', 10);
845 - $bookingData['additional_guests'] = array_slice($additionalGuests, 0, $guestLimit);
767 + if (!empty($postedData['recurring_count'])) {
768 + $bookingData['recurring_count'] = (int) Arr::get($postedData, 'recurring_count', 0);
846 769 }
847 770
848 771 $timeSlotService = TimeSlotServiceHandler::initService($calendarEvent->calendar, $calendarEvent);
849 772
@@ -850,17 +773,29 @@
850 773 if (is_wp_error($timeSlotService)) {
851 774 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timezone);
852 775 }
853 776
854 - $isSpotAvailable = $timeSlotService->isSpotAvailable($startDateTime, $endDateTime, $duration);
777 + $isSlotLocked = Helper::lockRoundRobinSlot($calendarEvent, $bookingData['start_time'], $bookingData['end_time']);
855 778
856 - if (!$isSpotAvailable) {
779 + $availableSpot = $isSlotLocked ? $timeSlotService->isSpotAvailable($bookingData['start_time'], $bookingData['end_time'], $duration) : false;
780 +
781 + if (!$availableSpot) {
857 782 wp_send_json([
858 783 'message' => __('This selected time slot is not available. Maybe someone booked the spot just a few seconds ago.', 'fluent-booking')
859 784 ], 422);
860 785 }
861 786
862 - if ($calendarEvent->isTeamEvent()) {
787 + if ($additionalGuests) {
788 + $guestField = BookingFieldService::getBookingFieldByName($calendarEvent, 'guests');
789 + $guestLimit = Arr::get($guestField, 'limit', 10);
790 + if ($calendarEvent->isMultiGuestEvent()) {
791 + $remaining = Arr::get($availableSpot, 'remaining', $calendarEvent->getMaxBookingPerSlot());
792 + $guestLimit = min($remaining, $guestLimit) - 1;
793 + }
794 + $bookingData['additional_guests'] = array_slice($additionalGuests, 0, $guestLimit);
795 + }
796 +
797 + if ($calendarEvent->isRoundRobin()) {
863 798 $bookingData['host_user_id'] = $timeSlotService->hostUserId;
864 799 }
865 800
866 801 do_action('fluent_booking/before_creating_schedule', $bookingData, $postedData, $calendarEvent);
@@ -890,26 +825,52 @@
890 825 'booking_hash' => $booking->hash
891 826 ], $booking), 200);
892 827 }
893 828
829 + /**
830 + * @return \Closure
831 + */
832 + private function validPhoneNumberRule()
833 + {
834 + return function ($attribute, $value) {
835 + if (!empty($value) && !Helper::isValidPhoneNumber($value)) {
836 + return __('Please provide a valid phone number', 'fluent-booking');
837 + }
838 + };
839 + }
840 +
894 841 public function ajaxGetAvailableDates()
895 842 {
843 + if (!Helper::checkRateLimit('available_dates', 30)) {
844 + wp_send_json_error(['message' => __('Too many requests. Please try again in a minute.', 'fluent-booking')], 429);
845 + }
846 +
896 847 $startBenchmark = microtime(true);
897 848
898 849 $request = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
899 850
900 - $eventId = (int)$request['event_id'];
851 + $eventId = (int)Arr::get($request, 'event_id');
901 852
902 - $rescheduling = Arr::get($request, 'rescheduling', 'no');
853 + $reschedulingHash = sanitize_text_field(Arr::get($request, 'rescheduling_hash', '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
903 854
904 - $calendarEvent = CalendarSlot::findOrfail($eventId);
855 + $calendarEvent = $eventId ? CalendarSlot::find($eventId) : null;
905 856
906 - if (!$calendarEvent || ($calendarEvent->status != 'active' && $rescheduling == 'no')) {
857 + if (!$calendarEvent) {
907 858 wp_send_json([
908 859 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
909 860 ], 422);
910 861 }
911 862
863 + if ($calendarEvent->status != 'active') {
864 + $existingBooking = $reschedulingHash ? Booking::where('hash', $reschedulingHash)->first() : null;
865 +
866 + if (!$existingBooking || (int)$existingBooking->event_id !== (int)$calendarEvent->id) {
867 + wp_send_json([
868 + 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
869 + ], 422);
870 + }
871 + }
872 +
912 873 $calendar = $calendarEvent->calendar;
913 874 $startDate = sanitize_text_field(Arr::get($request, 'start_date')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
914 875
915 876 if (!$startDate) {
@@ -928,9 +889,9 @@
928 889
929 890 $duration = (int)$calendarEvent->getDuration(Arr::get($request, 'duration', null));
930 891
931 892 $timeSlotService = TimeSlotServiceHandler::initService($calendar, $calendarEvent);
932 -
893 +
933 894 if (is_wp_error($timeSlotService)) {
934 895 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timeZone);
935 896 }
936 897
@@ -939,9 +900,9 @@
939 900 if (is_wp_error($availableSpots)) {
940 901 return TimeSlotServiceHandler::sendError($availableSpots, $calendarEvent, $timeZone);
941 902 }
942 903
943 - $availableSpots = array_filter($availableSpots);
904 + $availableSpots = array_filter((array) $availableSpots);
944 905 $availableSpots = apply_filters('fluent_booking/available_slots_for_view', $availableSpots, $calendarEvent, $calendar, $timeZone, $duration);
945 906
946 907 wp_send_json([
947 908 'available_slots' => $availableSpots,
@@ -952,8 +913,20 @@
952 913 }
953 914
954 915 public function getCalendarEventVars(Calendar $calendar, CalendarSlot $calendarEvent)
955 916 {
917 + static $globalConfig = null;
918 + if ($globalConfig === null) {
919 + $globalConfig = [
920 + 'time_format' => Arr::get(get_option('_fluent_booking_settings'), 'time_format', '12'),
921 + 'date_formatter' => DateTimeHelper::getDateFormatter(true),
922 + 'isRtl' => Helper::fluentbooking_is_rtl(),
923 + 'has_pro' => defined('FLUENT_BOOKING_PRO_DIR_FILE'),
924 + 'duration_lookup' => Helper::getDurationLookup(),
925 + 'multi_duration_lookup' => Helper::getDurationLookup(true),
926 + ];
927 + }
928 +
956 929 $calendarEvent->description = wpautop($calendarEvent->description);
957 930 $calendarEvent->location_icon_html = $calendarEvent->defaultLocationHtml();
958 931 $formFields = BookingFieldService::getBookingFields($calendarEvent);
959 932
@@ -961,18 +934,19 @@
961 934 'id' => $calendarEvent->id,
962 935 'max_lookup_date' => $calendarEvent->getMaxLookUpDate(),
963 936 'min_lookup_date' => $calendarEvent->getMinLookUpDate(),
964 937 'min_bookable_date' => $calendarEvent->getMinBookableDateTime(),
938 + 'is_display_spots' => $calendarEvent->isDisplaySpots(),
965 939 'duration' => $calendarEvent->getDefaultDuration(),
966 940 'title' => $calendarEvent->title,
967 - 'location_settings' => $calendarEvent->location_settings,
941 + 'location_settings' => LocationService::sanitizePublicLocationSettings($calendarEvent->location_settings),
968 942 'location_icon_html' => $calendarEvent->location_icon_html,
969 943 'description' => $calendarEvent->description,
970 944 'pre_selects' => null,
971 - 'settings' => $calendarEvent->settings,
945 + 'settings' => $this->sanitizePublicEventSettings($calendarEvent->settings),
972 946 'type' => $calendarEvent->type,
973 947 'event_type' => $calendarEvent->event_type,
974 - 'time_format' => Arr::get(get_option('_fluent_booking_settings'), 'time_format', '12'),
948 + 'time_format' => $globalConfig['time_format'],
975 949 ];
976 950
977 951 $author = $calendar->getAuthorProfile(true);
978 952 $author['name'] = $calendar->title;
@@ -985,12 +959,13 @@
985 959 'Schedule_Meeting' => __('Schedule Meeting', 'fluent-booking'),
986 960 'Continue_to_Payments' => __('Continue to Payments', 'fluent-booking'),
987 961 'Confirm_Payment' => __('Confirm Payment', 'fluent-booking'),
988 962 ],
989 - 'date_formatter' => DateTimeHelper::getDateFormatter(true),
990 - 'isRtl' => Helper::fluentbooking_is_rtl(),
991 - 'duration_lookup' => Helper::getDurationLookup(),
992 - 'multi_duration_lookup' => Helper::getDurationLookup(true)
963 + 'date_formatter' => $globalConfig['date_formatter'],
964 + 'isRtl' => $globalConfig['isRtl'],
965 + 'has_pro' => $globalConfig['has_pro'],
966 + 'duration_lookup' => $globalConfig['duration_lookup'],
967 + 'multi_duration_lookup' => $globalConfig['multi_duration_lookup']
993 968 ];
994 969
995 970 $eventVars['form_fields'] = array_values($eventVars['form_fields']);
996 971
@@ -1000,10 +975,41 @@
1000 975
1001 976 return apply_filters('fluent_booking/public_event_vars', $eventVars, $calendarEvent);
1002 977 }
1003 978
979 + private function sanitizePublicEventSettings($settings)
980 + {
981 + if (!is_array($settings)) {
982 + return [];
983 + }
984 +
985 + $publicKeys = [
986 + 'recurring_config',
987 + 'multiple_booking',
988 + 'multi_duration',
989 + 'lock_timezone',
990 + 'requires_confirmation',
991 + 'submit_button_text',
992 + ];
993 +
994 + $publicKeys = apply_filters('fluent_booking/public_event_settings_keys', $publicKeys);
995 +
996 + $safe = [];
997 + foreach ($publicKeys as $key) {
998 + if (array_key_exists($key, $settings)) {
999 + $safe[$key] = $settings[$key];
1000 + }
1001 + }
1002 +
1003 + return $safe;
1004 + }
1005 +
1004 1006 public function ajaxHandleCancelMeeting()
1005 1007 {
1008 + if (!Helper::checkRateLimit('cancel_meeting', 15)) {
1009 + wp_send_json_error(['message' => __('Too many requests. Please try again in a minute.', 'fluent-booking')], 429);
1010 + }
1011 +
1006 1012 $data = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1007 1013
1008 1014 $meetingHash = Arr::get($data, 'meeting_hash');
1009 1015
@@ -1034,9 +1040,9 @@
1034 1040 $result = $meeting->cancelMeeting($message, 'guest', get_current_user_id());
1035 1041
1036 1042 if (is_wp_error($result)) {
1037 1043 if (!wp_doing_ajax()) {
1038 - wp_redirect($meeting->getConfirmationUrl());
1044 + wp_safe_redirect($meeting->getConfirmationUrl());
1039 1045 exit();
1040 1046 }
1041 1047
1042 1048 wp_send_json([
@@ -1049,8 +1055,18 @@
1049 1055 'message' => __('Meeting has been cancelled', 'fluent-booking')
1050 1056 ], 200);
1051 1057 }
1052 1058
1053 - wp_redirect($meeting->getConfirmationUrl());
1059 + wp_safe_redirect($meeting->getConfirmationUrl());
1054 1060 exit;
1061 + }
1062 +
1063 + private static function sanitize_mapped_data($settings)
1064 + {
1065 + $sanitizerMap = [
1066 + 'name' => 'sanitize_text_field',
1067 + 'email' => 'sanitize_email',
1068 + ];
1069 +
1070 + return Helper::fcal_backend_sanitizer($settings, $sanitizerMap);
1055 1071 }
1056 1072 }