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 +187 -186 1.7.0 → 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;
15 17 use FluentBooking\App\Services\CalendarEventService;
16 18 use FluentBooking\App\Services\LocationService;
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()
@@ -68,19 +72,17 @@
68 72 if (!$calendar) {
69 73 return __('Calendar not found', 'fluent-booking');
70 74 }
71 75
72 - $assetUrl = App::getInstance('url.assets');
73 -
74 76 $localizeData = $this->getCalendarEventVars($calendar, $calendarEvent);
75 77 $localizeData['disable_author'] = $atts['disable_author'] == 'yes';
76 78 $localizeData['theme'] = $atts['theme'];
77 79
78 80 if (BookingFieldService::hasPhoneNumberField($localizeData['form_fields'])) {
79 - wp_enqueue_script('fluent-booking-phone-field', $assetUrl . 'public/js/phone-field.js', [], FLUENT_BOOKING_ASSETS_VERSION, true);
81 + Vite::enqueueScript('fluent-booking-phone-field', 'phone_field', [], FLUENT_BOOKING_ASSETS_VERSION);
80 82 }
81 83
82 - 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);
83 85
84 86 $this->loadGlobalVars();
85 87 wp_localize_script(
86 88 'fluent-booking-public',
@@ -128,9 +130,9 @@
128 130 $event = CalendarEventService::processEvent($event);
129 131 $calendarEvents[$event->calendar_id][] = $event;
130 132 }
131 133
132 - $calendars = Calendar::query()->whereIn('id', $calendarIds)->get();
134 + $calendars = Calendar::query()->with('metas')->whereIn('id', $calendarIds)->get();
133 135
134 136 foreach ($calendars as $calendar) {
135 137 $calendar->activeEvents = $calendarEvents[$calendar->id] ?? [];
136 138 $eventOrder = $calendar->getMeta('event_order');
@@ -155,9 +157,9 @@
155 157
156 158 public function renderTeamHosts($calendars, $headerConfig = [])
157 159 {
158 160 $wrapperId = 'fcal_team_' . Helper::getNextIndex();
159 - 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);
160 162
161 163 $vars = [];
162 164 foreach ($calendars as $calendar) {
163 165 $hostHtml = (string)(string)\FluentBooking\App\App::getInstance('view')->make('landing.author_html', [
@@ -187,10 +189,9 @@
187 189 }
188 190
189 191 wp_localize_script('fluent-booking-team', $wrapperId, $vars);
190 192
191 - $assetUrl = App::getInstance('url.assets');
192 - 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);
193 194 $this->loadGlobalVars();
194 195
195 196 return App::make('view')->make('public.team_page', [
196 197 'hosts' => $calendars,
@@ -228,15 +229,29 @@
228 229 if (!$calendar) {
229 230 return '';
230 231 }
231 232
233 + $settings = LandingPageHelper::getSettings($calendar, 'public');
234 +
232 235 $calendarEventQuery = CalendarSlot::where('calendar_id', $calendar->id)
233 236 ->where('status', 'active');
234 -
237 +
238 + $enabledEvents = [];
239 + $isEnabledOnly = false;
240 + if ($settings['show_type'] != 'all') {
241 + $isEnabledOnly = true;
242 + $enabledEvents = $settings['enabled_slots'];
243 + }
244 +
235 245 if ($eventIds && $eventIds != 'all') {
236 - $calendarEventQuery->whereIn('id', $eventIds);
246 + $isEnabledOnly = true;
247 + $enabledEvents = !empty($enabledEvents) ? array_intersect($enabledEvents, $eventIds) : $eventIds;
237 248 }
238 249
250 + if (!empty($enabledEvents) || $isEnabledOnly) {
251 + $calendarEventQuery->whereIn('id', $enabledEvents);
252 + }
253 +
239 254 $calendarEvents = $calendarEventQuery->get();
240 255
241 256 if ($calendarEvents->isEmpty()) {
242 257 return '';
@@ -256,10 +271,10 @@
256 271 }
257 272
258 273 public function renderCalendarBlock($calendar, $headerConfig = [])
259 274 {
260 - $wrapperId = 'fcal_team_' . Helper::getNextIndex();
261 - 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);
262 277
263 278 $calendarHtml = (string)(string)\FluentBooking\App\App::getInstance('view')->make('landing.author_html', [
264 279 'author' => $calendar->getAuthorProfile(),
265 280 'calendar' => $calendar,
@@ -286,10 +301,9 @@
286 301 }
287 302
288 303 wp_localize_script('fluent-booking-calendar', $wrapperId, $vars);
289 304
290 - $assetUrl = App::getInstance('url.assets');
291 - 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);
292 306 $this->loadGlobalVars();
293 307
294 308 return App::make('view')->make('public.calendar_page', [
295 309 'calendar' => $calendar,
@@ -312,9 +326,9 @@
312 326 'calendar_ids' => 'all',
313 327 'no_bookings' => __('No bookings found', 'fluent-booking'),
314 328 'per_page' => 10
315 329 ], $atts);
316 -
330 +
317 331 $atts['title'] = sanitize_text_field($atts['title']);
318 332 $atts['filter'] = sanitize_text_field($atts['filter']);
319 333 $atts['pagination'] = sanitize_text_field($atts['pagination']);
320 334 $atts['no_bookings'] = sanitize_text_field($atts['no_bookings']);
@@ -319,15 +333,15 @@
319 333 $atts['pagination'] = sanitize_text_field($atts['pagination']);
320 334 $atts['no_bookings'] = sanitize_text_field($atts['no_bookings']);
321 335
322 336 $userData = get_userdata(get_current_user_id());
323 -
337 +
324 338 $userEmail = $userData ? $userData->user_email : null;
325 -
339 +
326 340 if (!$userEmail) {
327 341 return __('Please login to view your bookings', 'fluent-booking');
328 342 }
329 -
343 +
330 344 $data = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
331 345
332 346 $perPage = intval(Arr::get($data, 'booking_per_page', $atts['per_page']));
333 347 $currentPage = intval(Arr::get($data, 'booking_page', 1));
@@ -373,9 +387,9 @@
373 387 $periodOptions = Helper::getBookingPeriodOptions();
374 388
375 389 $pageOptions = apply_filters('fluent_booking/booking_per_page_options', [5, 10, 15, 20, 50, 100]);
376 390
377 - 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);
378 392
379 393 return App::make('view')->make('public.bookings', [
380 394 'bookings' => $bookings,
381 395 'attributes' => $atts,
@@ -393,9 +407,9 @@
393 407 if (!isset($_REQUEST['hash'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
394 408 return __('Booking hash is missing!', 'fluent-booking');
395 409 }
396 410
397 - $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
398 412
399 413 return apply_filters('fluent_booking/payment_receipt_html', '', $hash);
400 414 }
401 415
@@ -404,9 +418,9 @@
404 418 if (empty($data['rescheduling_hash'])) {
405 419 return;
406 420 }
407 421
408 - add_filter('fluent_booking/schedule_custom_field_data', function ($array) {
422 + add_filter('fluent_booking/schedule_custom_field_data', function ($data) {
409 423 return [];
410 424 });
411 425
412 426 add_filter('fluent_booking/schedule_validation_rules_data', function ($data, $postedData, $calendarEvent)
@@ -434,77 +448,28 @@
434 448 'message' => __('Invalid rescheduling request', 'fluent-booking')
435 449 ], 422);
436 450 }
437 451
438 - $rescheduleBy = 'guest';
439 - $hostIds = $existingBooking->getHostIds();
440 - if (in_array(get_current_user_id(), $hostIds) || PermissionManager::userCan('manage_all_bookings')) {
441 - $rescheduleBy = 'host';
442 - }
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 + );
443 463
444 - $existingBooking->updateMeta('rescheduled_by_type', $rescheduleBy);
445 -
446 - if ($rescheduleBy == 'guest' && !$existingBooking->canReschedule()) {
464 + if (is_wp_error($result)) {
447 465 wp_send_json([
448 - 'message' => $existingBooking->getRescheduleMessage()
466 + 'message' => $result->get_error_message()
449 467 ], 422);
450 468 }
451 469
452 - if ($bookingData['start_time'] == $existingBooking->start_time) {
453 - wp_send_json([
454 - 'message' => __('Sorry! you can not reschedule to the same time.', 'fluent-booking')
455 - ], 422);
456 - }
470 + $existingBooking = $result;
457 471
458 - $endDateTime = gmdate('Y-m-d H:i:s', strtotime($bookingData['start_time']) + ($existingBooking->slot_minutes * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
459 -
460 - $previousBooking = clone $existingBooking;
461 -
462 - if ($existingBooking->isMultiGuestBooking()) {
463 - // Need to handle group booking type here
464 - // check for existing group
465 - $parent = Booking::where('status', 'scheduled')
466 - ->where('event_id', $existingBooking->event_id)
467 - ->where('start_time', $bookingData['start_time'])
468 - ->orderBy('id', 'ASC')
469 - ->first();
470 -
471 - if ($parent) {
472 - $existingBooking->group_id = $parent->group_id;
473 - } else {
474 - $existingBooking->group_id = Helper::getNextBookingGroup();
475 - }
476 - }
477 -
478 - if ($existingBooking->isRoundRobinBooking()) {
479 - $hostId = $bookingData['host_user_id'];
480 - $existingBooking->host_user_id = $hostId;
481 - $existingBooking->hosts()->sync([$hostId]);
482 - }
483 -
484 - $existingBooking->start_time = $bookingData['start_time'];
485 - $existingBooking->person_time_zone = $bookingData['person_time_zone'];
486 - $existingBooking->end_time = $endDateTime;
487 - $existingBooking->save();
488 -
489 - $existingBooking->updateMeta('previous_meeting_time', $previousBooking->start_time);
490 -
491 - $reschedulingMessage = sanitize_textarea_field(Arr::get($postedData, 'rescheduling_reason'));
492 - if ($reschedulingMessage) {
493 - $existingBooking->updateMeta('reschedule_reason', $reschedulingMessage);
494 - }
495 -
496 - do_action('fluent_booking/log_booking_activity', [
497 - 'booking_id' => $existingBooking->id,
498 - 'type' => 'info',
499 - 'status' => 'closed',
500 - 'title' => __('Meeting Rescheduled', 'fluent-booking'),
501 - /* translators: %1$s is the user who rescheduled the meeting, %2$s is the previous date and time in UTC. */
502 - 'description' => sprintf(__('Meeting has been rescheduled by %1$s from Web UI. Previous date time: %2$s (UTC)', 'fluent-booking'), $rescheduleBy, $previousBooking->start_time)
503 - ]);
504 -
505 - do_action('fluent_booking/after_booking_rescheduled', $existingBooking, $previousBooking, $calendarEvent);
506 -
507 472 add_filter('fluent_booking/schedule_receipt_data', function ($data) {
508 473 $data['title'] = __('Your meeting has been rescheduled', 'fluent-booking');
509 474 return $data;
510 475 });
@@ -586,74 +551,22 @@
586 551 $globalSettings = Helper::getGlobalSettings();
587 552 $startDay = Arr::get($globalSettings, 'administration.start_day', 'mon');
588 553
589 554 $data = [
590 - 'ajaxurl' => admin_url('admin-ajax.php'),
591 - 'timezones' => DateTimeHelper::getFlatGroupedTimeZones(),
592 - 'current_person' => $currentPerson,
593 - 'start_day' => $startDay,
594 - 'i18' => [
595 - 'Timezone' => __('Timezone', 'fluent-booking'),
596 - 'Day' => __('Day', 'fluent-booking'),
597 - 'Days' => __('Days', 'fluent-booking'),
598 - 'Hour' => __('Hour', 'fluent-booking'),
599 - 'Hours' => __('Hours', 'fluent-booking'),
600 - 'Minute' => __('Minute', 'fluent-booking'),
601 - 'Minutes' => __('Minutes', 'fluent-booking'),
602 - 'Enter Details' => __('Enter Details', 'fluent-booking'),
603 - 'Summary' => __('Summary', 'fluent-booking'),
604 - 'Payment Details' => __('Payment Details', 'fluent-booking'),
605 - 'Total Payment' => __('Total Payment', 'fluent-booking'),
606 - 'Payment Method' => __('Payment Method', 'fluent-booking'),
607 - 'Pay Now' => __('Pay Now', 'fluent-booking'),
608 - 'processing' => __('Processing', 'fluent-booking'),
609 - 'date_time_config' => DateTimeHelper::getI18nDateTimeConfig(),
610 - 'Country' => __('Country', 'fluent-booking'),
611 - '12h' => _x('12h', 'date time format switch', 'fluent-booking'),
612 - '24h' => _x('24h', 'date time format switch', 'fluent-booking'),
613 - 'spots left' => _x('spots left', 'for how many spots left for available booking', 'fluent-booking'),
614 - 'spots remaining' => _x('spots remaining', 'for how many spots remaining for available booking', 'fluent-booking'),
615 - 'Next' => _x('Next', 'Booking form spot selection', 'fluent-booking'),
616 - 'Select on the Next Step' => __('Select on the Next Step', 'fluent-booking'),
617 - 'location options' => __('location options', 'fluent-booking'),
618 - 'Your address' => __('Your address', 'fluent-booking'),
619 - 'Organizer Phone Number' => __('Organizer Phone Number', 'fluent-booking'),
620 - 'In Person (Attendee Address)' => __('In Person (Attendee Address)', 'fluent-booking'),
621 - 'In Person (Organizer Address)' => __('In Person (Organizer Address)', 'fluent-booking'),
622 - 'Attendee Phone Number' => __('Attendee Phone Number', 'fluent-booking'),
623 - 'Google Meet' => __('Google Meet', 'fluent-booking'),
624 - 'Zoom Meeting' => __('Zoom Meeting', 'fluent-booking'),
625 - 'Online Meeting' => __('Online Meeting', 'fluent-booking'),
626 - 'Phone Call' => __('Phone Call', 'fluent-booking'),
627 - 'Processing...' => __('Processing...', 'fluent-booking'),
628 - 'Loading Payment Processor...' => __('Loading Payment Processor...', 'fluent-booking'),
629 - 'PM' => __('PM', 'fluent-booking'),
630 - 'AM' => __('AM', 'fluent-booking'),
631 - 'Name' => __('Name', 'fluent-booking'),
632 - 'Email' => __('Email', 'fluent-booking'),
633 - 'Date' => __('Date', 'fluent-booking'),
634 - 'Time' => __('Time', 'fluent-booking'),
635 - 'per guest' => __('per guest', 'fluent-booking'),
636 - 'Add guest' => __('Add guest', 'fluent-booking'),
637 - 'Add guests' => __('Add guests', 'fluent-booking'),
638 - 'Add another' => __('Add another', 'fluent-booking'),
639 - 'Choose File' => __('Choose File', 'fluent-booking'),
640 - 'This field is required.' => __('This field is required.', 'fluent-booking'),
641 - 'No availability in' => __('No availability in', 'fluent-booking'),
642 - 'View next month' => __('View next month', 'fluent-booking'),
643 - 'View previous month' => __('View previous month', 'fluent-booking'),
644 - 'No_payment_method_description' => __('No activated payment method found. If you are an admin please check the event payment settings', 'fluent-booking'),
645 - 'Please fill up the required data' => __('Please fill up the required data', 'fluent-booking'),
646 - 'Please select a valid payment method' => __('Please select a valid payment method', 'fluent-booking'),
647 - 'Please Select' => __('Please Select', 'fluent-booking'),
648 - 'Something is wrong!' => __('Something is wrong!', 'fluent-booking'),
649 - 'Requires Confirmation' => __('Requires Confirmation', 'fluent-booking'),
650 - ],
651 - '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()
652 565 ];
653 566
654 567 if (isset($_SERVER['HTTP_CF_IPCOUNTRY'])) {
655 - $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
656 569 } else {
657 570 $data['user_country'] = Arr::get($globalSettings, 'administration.default_country', '');
658 571 }
659 572
@@ -661,8 +574,12 @@
661 574 }
662 575
663 576 public function ajaxScheduleMeeting()
664 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 +
665 582 $app = App::getInstance();
666 583
667 584 $postedData = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
668 585
@@ -680,12 +597,17 @@
680 597
681 598 do_action('fluent_booking/starting_scheduling_ajax', $postedData);
682 599
683 600 $rules = [
684 - 'name' => 'required',
685 - 'email' => 'required|email',
686 - 'timezone' => 'required',
687 - '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',
688 610 ];
689 611
690 612 $messages = [
691 613 'name.required' => __('Please enter your name', 'fluent-booking'),
@@ -695,9 +617,9 @@
695 617 'start_date.required' => __('Please select a date and time', 'fluent-booking')
696 618 ];
697 619
698 620 if ($calendarEvent->isPhoneRequired()) {
699 - $rules['phone_number'] = 'required';
621 + $rules['phone_number'] = ['required', $this->validPhoneNumberRule()];
700 622 $messages['phone_number.required'] = __('Please provide your phone number', 'fluent-booking');
701 623 } else if ($calendarEvent->isAddressRequired()) {
702 624 $rules['address'] = 'required';
703 625 $messages['address.required'] = __('Please provide your Address', 'fluent-booking');
@@ -708,12 +630,13 @@
708 630 $selectedLocation = LocationService::getLocationDetails($calendarEvent, Arr::get($postedData, 'location_config', []), $postedData);
709 631 $selectedLocationDriver = Arr::get($selectedLocation, 'type');
710 632 // is user input required
711 633 if (in_array($selectedLocationDriver, ['in_person_guest', 'phone_guest'])) {
712 - $rules['location_config.user_location_input'] = 'required';
713 634 if ($selectedLocationDriver == 'in_person_guest') {
635 + $rules['location_config.user_location_input'] = 'required';
714 636 $messages['location_config.user_location_input.required'] = __('Please provide your address', 'fluent-booking');
715 637 } else {
638 + $rules['location_config.user_location_input'] = ['required', $this->validPhoneNumberRule()];
716 639 $messages['location_config.user_location_input.required'] = __('Please provide your phone number', 'fluent-booking');
717 640 }
718 641 }
719 642 }
@@ -719,13 +642,8 @@
719 642 }
720 643
721 644 $duration = (int)$calendarEvent->getDuration(Arr::get($postedData, 'duration', null));
722 645
723 - if ($calendarEvent->isPaymentEnabled($duration)) {
724 - $rules['payment_method'] = 'required';
725 - $messages['payment_method.required'] = __('Please select a valid payment method', 'fluent-booking');
726 - }
727 -
728 646 if ($additionalGuests = Arr::get($postedData, 'guests', [])) {
729 647 if ($calendarEvent->isMultiGuestEvent()) {
730 648 $additionalGuests = $this->sanitize_mapped_data($additionalGuests);
731 649 $additionalGuests = array_values(array_filter($additionalGuests, function ($guest) {
@@ -755,17 +673,17 @@
755 673 ], $postedData, $calendarEvent);
756 674
757 675 $validator = $app->validator->make($postedData, $validationConfig['rules'], $validationConfig['messages']);
758 676 if ($validator->validate()->fails()) {
677 + $errorMessage = $validator->firstError() ?: __('Please fill up the required data', 'fluent-booking');
759 678 wp_send_json([
760 - 'message' => __('Please fill up the required data', 'fluent-booking'),
679 + 'message' => $errorMessage,
761 680 'errors' => $validator->errors()
762 681 ], 422);
763 - return;
764 682 }
765 683
766 684 $customFieldsData = BookingFieldService::getCustomFieldsData($postedData, $calendarEvent);
767 - $customFieldsData = apply_filters('fluent_booking/schedule_custom_field_data', $customFieldsData, $customFieldsData, $calendarEvent);
685 + $customFieldsData = apply_filters('fluent_booking/schedule_custom_field_data', $customFieldsData, $calendarEvent);
768 686
769 687 if (is_wp_error($customFieldsData)) {
770 688 wp_send_json([
771 689 'message' => $customFieldsData->get_error_message(),
@@ -770,9 +688,8 @@
770 688 wp_send_json([
771 689 'message' => $customFieldsData->get_error_message(),
772 690 'errors' => $customFieldsData->get_error_data()
773 691 ], 422);
774 - return;
775 692 }
776 693
777 694 $validateDateFields = BookingFieldService::validateDateFields($customFieldsData, $calendarEvent);
778 695
@@ -779,9 +696,8 @@
779 696 if (is_wp_error($validateDateFields)) {
780 697 wp_send_json([
781 698 'message' => $validateDateFields->get_error_message(),
782 699 ], 422);
783 - return;
784 700 }
785 701
786 702 $startDate = Arr::get($postedData, 'start_date');
787 703 $timezone = sanitize_text_field(Arr::get($postedData, 'timezone', 'UTC'));
@@ -802,9 +718,9 @@
802 718 $startDateTime = DateTimeHelper::convertToUtc($startDate, $timezone);
803 719 $endDateTime = gmdate('Y-m-d H:i:s', strtotime($startDateTime) + ($duration * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
804 720 }
805 721
806 - $bookingData = [
722 + $bookingData = apply_filters('fluent_booking/initialize_booking_data', [
807 723 'person_time_zone' => sanitize_text_field($timezone),
808 724 'start_time' => $startDateTime,
809 725 'end_time' => $endDateTime,
810 726 'name' => sanitize_text_field($postedData['name']),
@@ -809,18 +725,23 @@
809 725 'end_time' => $endDateTime,
810 726 'name' => sanitize_text_field($postedData['name']),
811 727 'email' => sanitize_email($postedData['email']),
812 728 'message' => sanitize_textarea_field(wp_unslash(Arr::get($postedData, 'message', ''))),
813 - 'phone' => sanitize_textarea_field(Arr::get($postedData, 'phone_number', '')),
729 + 'phone' => sanitize_text_field(Arr::get($postedData, 'phone_number', '')),
814 730 'address' => sanitize_textarea_field(Arr::get($postedData, 'address', '')),
815 731 'ip_address' => Helper::getIp(),
816 732 'status' => 'scheduled',
817 733 'source' => 'web',
818 734 'event_type' => $calendarEvent->event_type,
819 - 'slot_minutes' => $duration
820 - ];
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);
821 742
822 - if ($calendarEvent->isConfirmationRequired($startDateTime)) {
743 + if ($calendarEvent->isConfirmationRequired($bookingData['start_time'])) {
823 744 $bookingData['status'] = 'pending';
824 745 }
825 746
826 747 $locationConfig = Arr::get($postedData, 'location_config', []);
@@ -834,12 +755,20 @@
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
767 + if (!empty($postedData['recurring_count'])) {
768 + $bookingData['recurring_count'] = (int) Arr::get($postedData, 'recurring_count', 0);
769 + }
770 +
842 771 $timeSlotService = TimeSlotServiceHandler::initService($calendarEvent->calendar, $calendarEvent);
843 772
844 773 if (is_wp_error($timeSlotService)) {
845 774 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timezone);
@@ -844,10 +773,12 @@
844 773 if (is_wp_error($timeSlotService)) {
845 774 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timezone);
846 775 }
847 776
848 - $availableSpot = $timeSlotService->isSpotAvailable($startDateTime, $endDateTime, $duration);
777 + $isSlotLocked = Helper::lockRoundRobinSlot($calendarEvent, $bookingData['start_time'], $bookingData['end_time']);
849 778
779 + $availableSpot = $isSlotLocked ? $timeSlotService->isSpotAvailable($bookingData['start_time'], $bookingData['end_time'], $duration) : false;
780 +
850 781 if (!$availableSpot) {
851 782 wp_send_json([
852 783 'message' => __('This selected time slot is not available. Maybe someone booked the spot just a few seconds ago.', 'fluent-booking')
853 784 ], 422);
@@ -894,26 +825,52 @@
894 825 'booking_hash' => $booking->hash
895 826 ], $booking), 200);
896 827 }
897 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 +
898 841 public function ajaxGetAvailableDates()
899 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 +
900 847 $startBenchmark = microtime(true);
901 848
902 849 $request = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
903 850
904 - $eventId = (int)$request['event_id'];
851 + $eventId = (int)Arr::get($request, 'event_id');
905 852
906 - $rescheduling = Arr::get($request, 'rescheduling', 'no');
853 + $reschedulingHash = sanitize_text_field(Arr::get($request, 'rescheduling_hash', '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
907 854
908 - $calendarEvent = CalendarSlot::findOrfail($eventId);
855 + $calendarEvent = $eventId ? CalendarSlot::find($eventId) : null;
909 856
910 - if (!$calendarEvent || ($calendarEvent->status != 'active' && $rescheduling == 'no')) {
857 + if (!$calendarEvent) {
911 858 wp_send_json([
912 859 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
913 860 ], 422);
914 861 }
915 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 +
916 873 $calendar = $calendarEvent->calendar;
917 874 $startDate = sanitize_text_field(Arr::get($request, 'start_date')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
918 875
919 876 if (!$startDate) {
@@ -932,9 +889,9 @@
932 889
933 890 $duration = (int)$calendarEvent->getDuration(Arr::get($request, 'duration', null));
934 891
935 892 $timeSlotService = TimeSlotServiceHandler::initService($calendar, $calendarEvent);
936 -
893 +
937 894 if (is_wp_error($timeSlotService)) {
938 895 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timeZone);
939 896 }
940 897
@@ -943,9 +900,9 @@
943 900 if (is_wp_error($availableSpots)) {
944 901 return TimeSlotServiceHandler::sendError($availableSpots, $calendarEvent, $timeZone);
945 902 }
946 903
947 - $availableSpots = array_filter((array)$availableSpots);
904 + $availableSpots = array_filter((array) $availableSpots);
948 905 $availableSpots = apply_filters('fluent_booking/available_slots_for_view', $availableSpots, $calendarEvent, $calendar, $timeZone, $duration);
949 906
950 907 wp_send_json([
951 908 'available_slots' => $availableSpots,
@@ -956,8 +913,20 @@
956 913 }
957 914
958 915 public function getCalendarEventVars(Calendar $calendar, CalendarSlot $calendarEvent)
959 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 +
960 929 $calendarEvent->description = wpautop($calendarEvent->description);
961 930 $calendarEvent->location_icon_html = $calendarEvent->defaultLocationHtml();
962 931 $formFields = BookingFieldService::getBookingFields($calendarEvent);
963 932
@@ -968,16 +937,16 @@
968 937 'min_bookable_date' => $calendarEvent->getMinBookableDateTime(),
969 938 'is_display_spots' => $calendarEvent->isDisplaySpots(),
970 939 'duration' => $calendarEvent->getDefaultDuration(),
971 940 'title' => $calendarEvent->title,
972 - 'location_settings' => $calendarEvent->location_settings,
941 + 'location_settings' => LocationService::sanitizePublicLocationSettings($calendarEvent->location_settings),
973 942 'location_icon_html' => $calendarEvent->location_icon_html,
974 943 'description' => $calendarEvent->description,
975 944 'pre_selects' => null,
976 - 'settings' => $calendarEvent->settings,
945 + 'settings' => $this->sanitizePublicEventSettings($calendarEvent->settings),
977 946 'type' => $calendarEvent->type,
978 947 'event_type' => $calendarEvent->event_type,
979 - 'time_format' => Arr::get(get_option('_fluent_booking_settings'), 'time_format', '12'),
948 + 'time_format' => $globalConfig['time_format'],
980 949 ];
981 950
982 951 $author = $calendar->getAuthorProfile(true);
983 952 $author['name'] = $calendar->title;
@@ -990,12 +959,13 @@
990 959 'Schedule_Meeting' => __('Schedule Meeting', 'fluent-booking'),
991 960 'Continue_to_Payments' => __('Continue to Payments', 'fluent-booking'),
992 961 'Confirm_Payment' => __('Confirm Payment', 'fluent-booking'),
993 962 ],
994 - 'date_formatter' => DateTimeHelper::getDateFormatter(true),
995 - 'isRtl' => Helper::fluentbooking_is_rtl(),
996 - 'duration_lookup' => Helper::getDurationLookup(),
997 - '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']
998 968 ];
999 969
1000 970 $eventVars['form_fields'] = array_values($eventVars['form_fields']);
1001 971
@@ -1005,10 +975,41 @@
1005 975
1006 976 return apply_filters('fluent_booking/public_event_vars', $eventVars, $calendarEvent);
1007 977 }
1008 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 +
1009 1006 public function ajaxHandleCancelMeeting()
1010 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 +
1011 1012 $data = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1012 1013
1013 1014 $meetingHash = Arr::get($data, 'meeting_hash');
1014 1015
@@ -1039,9 +1040,9 @@
1039 1040 $result = $meeting->cancelMeeting($message, 'guest', get_current_user_id());
1040 1041
1041 1042 if (is_wp_error($result)) {
1042 1043 if (!wp_doing_ajax()) {
1043 - wp_redirect($meeting->getConfirmationUrl());
1044 + wp_safe_redirect($meeting->getConfirmationUrl());
1044 1045 exit();
1045 1046 }
1046 1047
1047 1048 wp_send_json([
@@ -1054,9 +1055,9 @@
1054 1055 'message' => __('Meeting has been cancelled', 'fluent-booking')
1055 1056 ], 200);
1056 1057 }
1057 1058
1058 - wp_redirect($meeting->getConfirmationUrl());
1059 + wp_safe_redirect($meeting->getConfirmationUrl());
1059 1060 exit;
1060 1061 }
1061 1062
1062 1063 private static function sanitize_mapped_data($settings)