0, 'theme' => 'light', 'disable_author' => 'no', 'hash' => '' ], $atts); if (!$atts['id'] && !$atts['hash']) { return ''; } $calendarEvent = CalendarSlot::find($atts['id']); if (!$calendarEvent) { $calendarEvent = CalendarSlot::where('hash', $atts['hash'])->first(); if (!$calendarEvent) { return __('Calendar event not found', 'fluent-booking'); } } $calendar = $calendarEvent->calendar; if (!$calendar) { return __('Calendar not found', 'fluent-booking'); } $localizeData = $this->getCalendarEventVars($calendar, $calendarEvent); $localizeData['disable_author'] = $atts['disable_author'] == 'yes'; $localizeData['theme'] = $atts['theme']; if (BookingFieldService::hasPhoneNumberField($localizeData['form_fields'])) { Vite::enqueueScript('fluent-booking-phone-field', 'phone_field', [], FLUENT_BOOKING_ASSETS_VERSION); } Vite::enqueueScript('fluent-booking-public', 'public_app', [], FLUENT_BOOKING_ASSETS_VERSION); $this->loadGlobalVars(); wp_localize_script( 'fluent-booking-public', 'fcal_public_vars_' . $calendar->id . '_' . $calendarEvent->id, $localizeData, ); return App::make('view')->make('public.calendar', [ 'calenderEvent' => $calendarEvent, 'theme' => $atts['theme'] ]); } public function handleTeamShortcode($atts, $content) { $atts = shortcode_atts([ 'event_ids' => '', 'title' => '', 'description' => '', 'logo_url' => '' ], $atts); if (!$atts['event_ids']) { return ''; } $eventIds = array_filter(array_map('intval', explode(',', $atts['event_ids']))); if (empty($eventIds)) { return ''; } $events = CalendarSlot::query()->whereIn('id', $eventIds) ->where('status', 'active') ->get(); $calendarIds = []; $calendarEvents = []; foreach ($events as $event) { $calendarIds[] = $event->calendar_id; if (!isset($calendarEvents[$event->calendar_id])) { $calendarEvents[$event->calendar_id] = []; } $event = CalendarEventService::processEvent($event); $calendarEvents[$event->calendar_id][] = $event; } $calendars = Calendar::query()->with('metas')->whereIn('id', $calendarIds)->get(); foreach ($calendars as $calendar) { $calendar->activeEvents = $calendarEvents[$calendar->id] ?? []; $eventOrder = $calendar->getMeta('event_order'); if (!empty($eventOrder)) { $eventsArray = $calendar->activeEvents; usort($eventsArray, function($a, $b) use ($eventOrder) { $posA = array_search($a->id, $eventOrder); $posB = array_search($b->id, $eventOrder); return $posA - $posB; }); $calendar->activeEvents = $eventsArray; } } return $this->renderTeamHosts($calendars, [ 'title' => $atts['title'], 'description' => $atts['description'], 'logo' => $atts['logo_url'], 'wrapper_class' => '' ]); } public function renderTeamHosts($calendars, $headerConfig = []) { $wrapperId = 'fcal_team_' . Helper::getNextIndex(); Vite::enqueueScript('fluent-booking-team', 'team_app', [], FLUENT_BOOKING_ASSETS_VERSION); $vars = []; foreach ($calendars as $calendar) { $hostHtml = (string)(string)\FluentBooking\App\App::getInstance('view')->make('landing.author_html', [ 'author' => $calendar->getAuthorProfile(), 'calendar' => $calendar, 'events' => $calendar->activeEvents ]); $hostHtml .= '
' . __('Back to team', 'fluent-booking') . '
'; $eventCount = count($calendar->activeEvents); $vars['fcal_host_' . $calendar->id] = [ 'host_html' => $hostHtml, 'event_count' => $eventCount, 'target_event_id' => ($eventCount == 1) ? $calendar->activeEvents[0]->id : 0 ]; foreach ($calendar->activeEvents as $event) { $itemVars = $this->getCalendarEventVars($event->calendar, $event); $extraJs = (new LandingPageHandler())->getEventLandingExtraJsFiles($itemVars['form_fields'], $event); if ($extraJs) { $itemVars['lazy_js_files'] = $extraJs; } wp_localize_script('fluent-booking-team', 'fcal_public_vars_' . $event->calendar_id . '_' . $event->id, $itemVars); } } wp_localize_script('fluent-booking-team', $wrapperId, $vars); Vite::enqueueScript('fluent-booking-public', 'public_app', [], FLUENT_BOOKING_ASSETS_VERSION); $this->loadGlobalVars(); return App::make('view')->make('public.team_page', [ 'hosts' => $calendars, 'wrapper_id' => $wrapperId, 'logo' => Arr::get($headerConfig, 'logo', ''), 'title' => Arr::get($headerConfig, 'title', ''), 'description' => Arr::get($headerConfig, 'description', ''), 'wrapper_class' => Arr::get($headerConfig, 'wrapper_class', '') ]); } public function handleCalendarShortcode($atts, $content) { $atts = shortcode_atts([ 'calendar_id' => '', 'event_ids' => '', 'title' => '', 'description' => '', 'logo' => '', 'hide_info' => false ], $atts); $calendarId = intval($atts['calendar_id']); $title = sanitize_text_field($atts['title']); $description = sanitize_text_field($atts['description']); $logo = sanitize_text_field($atts['logo']); $hideInfo = $atts['hide_info'] ? true : false; $eventIds = array_filter(array_map('intval', explode(',', $atts['event_ids']))); if (!$calendarId) { return ''; } $calendar = Calendar::find($calendarId); if (!$calendar) { return ''; } $settings = LandingPageHelper::getSettings($calendar, 'public'); $calendarEventQuery = CalendarSlot::where('calendar_id', $calendar->id) ->where('status', 'active'); $enabledEvents = []; $isEnabledOnly = false; if ($settings['show_type'] != 'all') { $isEnabledOnly = true; $enabledEvents = $settings['enabled_slots']; } if ($eventIds && $eventIds != 'all') { $isEnabledOnly = true; $enabledEvents = !empty($enabledEvents) ? array_intersect($enabledEvents, $eventIds) : $eventIds; } if (!empty($enabledEvents) || $isEnabledOnly) { $calendarEventQuery->whereIn('id', $enabledEvents); } $calendarEvents = $calendarEventQuery->get(); if ($calendarEvents->isEmpty()) { return ''; } $calendarEvents = CalendarEventService::processEvents($calendar, $calendarEvents); $calendar->activeEvents = $calendarEvents; return $this->renderCalendarBlock($calendar, [ 'title' => $title, 'description' => $description, 'logo' => $logo, 'hide_info' => $hideInfo, 'wrapper_class' => '', ]); } public function renderCalendarBlock($calendar, $headerConfig = []) { $wrapperId = 'fcal_calendar_' . Helper::getNextIndex(); Vite::enqueueScript('fluent-booking-calendar', 'calendar_app', [], FLUENT_BOOKING_ASSETS_VERSION); $calendarHtml = (string)(string)\FluentBooking\App\App::getInstance('view')->make('landing.author_html', [ 'author' => $calendar->getAuthorProfile(), 'calendar' => $calendar, 'events' => $calendar->activeEvents, 'hideInfo' => Arr::isTrue($headerConfig, 'hide_info'), 'block' => true ]); $eventCount = count($calendar->activeEvents); $vars['fcal_host_calendar' ] = [ 'calendar_html' => $calendarHtml, 'event_count' => $eventCount, 'target_event_id' => ($eventCount == 1) ? $calendar->activeEvents[0]->id : 0 ]; foreach ($calendar->activeEvents as $event) { $itemVars = $this->getCalendarEventVars($event->calendar, $event); $extraJs = (new LandingPageHandler())->getEventLandingExtraJsFiles($itemVars['form_fields'], $event); if ($extraJs) { $itemVars['lazy_js_files'] = $extraJs; } wp_localize_script('fluent-booking-calendar', 'fcal_public_vars_' . $event->calendar_id . '_' . $event->id, $itemVars); } wp_localize_script('fluent-booking-calendar', $wrapperId, $vars); Vite::enqueueScript('fluent-booking-public', 'public_app', [], FLUENT_BOOKING_ASSETS_VERSION); $this->loadGlobalVars(); return App::make('view')->make('public.calendar_page', [ 'calendar' => $calendar, 'wrapper_id' => $wrapperId, 'logo' => Arr::get($headerConfig, 'logo', ''), 'title' => Arr::get($headerConfig, 'title', ''), 'description' => Arr::get($headerConfig, 'description', ''), 'wrapper_class' => Arr::get($headerConfig, 'wrapper_class', ''), 'hide_info' => Arr::isTrue($headerConfig, 'hide_info') ]); } public function handleBookingListsShortcode($atts, $content) { $atts = shortcode_atts([ 'title' => __('My Bookings', 'fluent-booking'), 'filter' => 'show', 'pagination' => 'show', 'period' => 'all', 'calendar_ids' => 'all', 'no_bookings' => __('No bookings found', 'fluent-booking'), 'per_page' => 10 ], $atts); $atts['title'] = sanitize_text_field($atts['title']); $atts['filter'] = sanitize_text_field($atts['filter']); $atts['pagination'] = sanitize_text_field($atts['pagination']); $atts['no_bookings'] = sanitize_text_field($atts['no_bookings']); $userData = get_userdata(get_current_user_id()); $userEmail = $userData ? $userData->user_email : null; if (!$userEmail) { return __('Please login to view your bookings', 'fluent-booking'); } $data = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $perPage = intval(Arr::get($data, 'booking_per_page', $atts['per_page'])); $currentPage = intval(Arr::get($data, 'booking_page', 1)); $bookingPeriod = sanitize_text_field(Arr::get($data, 'booking_period', $atts['period'])); $bookingQuery = Booking::query()->with('calendar_event') ->where('email', $userEmail) ->applyComputedStatus($bookingPeriod) ->applyBookingOrderByStatus($bookingPeriod); if ($atts['calendar_ids'] != 'all') { $atts['calendar_ids'] = array_map('intval', explode(',', $atts['calendar_ids'])); $bookingQuery->whereIn('calendar_id', $atts['calendar_ids']); } $bookings = $bookingQuery->paginate($perPage, ['*'], 'booking_page', $currentPage) ->appends(['booking_page' => $currentPage]) ->withQueryString(); foreach ($bookings as &$booking) { $booking->author_name = $booking->getHostDetails(false)['name']; $booking->happening_status = $booking->getOngoingStatus(); $booking->booking_status_text = $booking->getBookingStatus(); $booking->payment_status_text = $booking->getPaymentStatus(); $booking->booking_date = DateTimeHelper::formatToLocale($booking->getAttendeeStartTime(), 'date'); $booking->booking_time = DateTimeHelper::formatToLocale($booking->getAttendeeStartTime(), 'time') . ' - ' . DateTimeHelper::formatToLocale($booking->getAttendeeEndTime(), 'time'); } $currentPage = $bookings->currentPage(); $lastPage = $bookings->lastPage(); $startPage = max(1, $currentPage - 2); $endPage = min($lastPage, $currentPage + 2); // Adjust if near the beginning or the end if ($currentPage < 3) { $endPage = min($lastPage, 5); } if ($currentPage > $lastPage - 2) { $startPage = max(1, $lastPage - 4); } $periodOptions = Helper::getBookingPeriodOptions(); $pageOptions = apply_filters('fluent_booking/booking_per_page_options', [5, 10, 15, 20, 50, 100]); Vite::enqueueScript('fluent-booking-list', 'bookings', [], FLUENT_BOOKING_ASSETS_VERSION); return App::make('view')->make('public.bookings', [ 'bookings' => $bookings, 'attributes' => $atts, 'per_page' => $perPage, 'start_page' => $startPage, 'end_page' => $endPage, 'booking_period' => $bookingPeriod, 'page_options' => $pageOptions, 'period_options' => $periodOptions ]); } public function handleReceiptShortcode($atts, $content) { if (!isset($_REQUEST['hash'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return __('Booking hash is missing!', 'fluent-booking'); } $hash = isset($_REQUEST['hash']) ? sanitize_text_field(wp_unslash($_REQUEST['hash'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return apply_filters('fluent_booking/payment_receipt_html', '', $hash); } public function handleRescheduling($data) { if (empty($data['rescheduling_hash'])) { return; } add_filter('fluent_booking/schedule_custom_field_data', function ($data) { return []; }); add_filter('fluent_booking/schedule_validation_rules_data', function ($data, $postedData, $calendarEvent) { $rules = $messages = []; $rescheduleField = BookingFieldService::getBookingFieldByName($calendarEvent, 'rescheduling_reason'); if (Arr::isTrue($rescheduleField, 'required')) { $rules['rescheduling_reason'] = 'required'; $messages['rescheduling_reason.required'] = __('Please provide a rescheduling reason', 'fluent-booking'); } return [ 'rules' => $rules, 'messages' => $messages ]; }, 10, 3); add_action('fluent_booking/before_creating_schedule', function ($bookingData, $postedData, $calendarEvent) { $existingHash = Arr::get($postedData, 'rescheduling_hash'); $existingBooking = Booking::where('hash', $existingHash)->first(); if (!$existingBooking) { wp_send_json([ 'message' => __('Invalid rescheduling request', 'fluent-booking') ], 422); } $result = RescheduleService::reschedule( $existingBooking, $calendarEvent, $bookingData['start_time'], $bookingData['person_time_zone'], [ 'reason' => Arr::get($postedData, 'rescheduling_reason', ''), 'host_user_id' => Arr::get($bookingData, 'host_user_id'), 'source' => __('Web UI', 'fluent-booking') ] ); if (is_wp_error($result)) { wp_send_json([ 'message' => $result->get_error_message() ], 422); } $existingBooking = $result; add_filter('fluent_booking/schedule_receipt_data', function ($data) { $data['title'] = __('Your meeting has been rescheduled', 'fluent-booking'); return $data; }); $redirectUrl = $existingBooking->getRedirectUrlWithQuery(); $html = BookingService::getBookingConfirmationHtml($existingBooking); wp_send_json(apply_filters('fluent_booking/booking_rescheduled_response', [ 'message' => __('Booking has been rescheduled', 'fluent-booking'), 'redirect_url' => $redirectUrl, 'response_html' => $html, 'booking_hash' => $existingBooking->hash ], $existingBooking), 200); }, 10, 3); } private function loadGlobalVars() { static $loaded; if ($loaded) { return; } $loaded = true; wp_localize_script('fluent-booking-public', 'fluentCalendarPublicVars', $this->getGlobalVars()); } public function getGlobalVars() { $currentPerson = [ 'name' => '', 'email' => '' ]; if (is_user_logged_in()) { $currentUser = wp_get_current_user(); $name = trim($currentUser->first_name . ' ' . $currentUser->last_name); if (!$name) { $name = $currentUser->display_name; } $currentPerson = [ 'name' => $name, 'email' => $currentUser->user_email, 'user_id' => $currentUser->ID ]; } else { $request = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended // Check for url params if ($name = sanitize_text_field(Arr::get($request, 'invitee_name'))) { $currentPerson['name'] = $name; } if ($email = sanitize_email(Arr::get($request, 'invitee_email'))) { if (is_email($email)) { $currentPerson['email'] = $email; } } } if (empty($currentPerson['email'])) { // Let's try to get from FluentCRM is exists if (defined('FLUENTCRM')) { $contactApi = FluentCrmApi('contacts'); $contact = $contactApi->getCurrentContact(); if ($contact) { $currentPerson['email'] = $contact->email; $currentPerson['name'] = $contact->full_name; } } } $globalSettings = Helper::getGlobalSettings(); $startDay = Arr::get($globalSettings, 'administration.start_day', 'mon'); $data = [ 'ajaxurl' => admin_url('admin-ajax.php'), 'timezones' => DateTimeHelper::getFlatGroupedTimeZones(), 'current_person' => $currentPerson, 'start_day' => $startDay, 'i18' => [ 'Timezone' => __('Timezone', 'fluent-booking'), 'Day' => __('Day', 'fluent-booking'), 'Days' => __('Days', 'fluent-booking'), 'Hour' => __('Hour', 'fluent-booking'), 'Hours' => __('Hours', 'fluent-booking'), 'Minute' => __('Minute', 'fluent-booking'), 'Minutes' => __('Minutes', 'fluent-booking'), 'week' => __('week', 'fluent-booking'), 'month' => __('month', 'fluent-booking'), 'year' => __('year', 'fluent-booking'), 'weeks' => __('weeks', 'fluent-booking'), 'months' => __('months', 'fluent-booking'), 'years' => __('years', 'fluent-booking'), 'Every' => __('Every', 'fluent-booking'), 'for' => __('for', 'fluent-booking'), 'Number of Occurrences' => __('Number of Occurrences', 'fluent-booking'), 'occurrence' => __('occurrence', 'fluent-booking'), 'occurrences' => __('occurrences', 'fluent-booking'), 'You can only book up to' => __('You can only book up to', 'fluent-booking'), 'at a time' => __('at a time', 'fluent-booking'), 'Enter Details' => __('Enter Details', 'fluent-booking'), 'Summary' => __('Summary', 'fluent-booking'), 'Payment Details' => __('Payment Details', 'fluent-booking'), 'Item' => __('Item', 'fluent-booking'), 'Price' => __('Price', 'fluent-booking'), 'Quantity' => __('Quantity', 'fluent-booking'), 'Subtotal:' => __('Subtotal:', 'fluent-booking'), 'Total:' => __('Total:', 'fluent-booking'), 'Total Payment' => __('Total Payment', 'fluent-booking'), 'Payment Method' => __('Payment Method', 'fluent-booking'), 'Pay Now' => __('Pay Now', 'fluent-booking'), 'processing' => __('Processing', 'fluent-booking'), 'date_time_config' => DateTimeHelper::getI18nDateTimeConfig(), 'Country' => __('Country', 'fluent-booking'), '12h' => _x('12h', 'date time format switch', 'fluent-booking'), '24h' => _x('24h', 'date time format switch', 'fluent-booking'), 'spots left' => _x('spots left', 'for how many spots left for available booking', 'fluent-booking'), 'spots remaining' => _x('spots remaining', 'for how many spots remaining for available booking', 'fluent-booking'), 'Next' => _x('Next', 'Booking form spot selection', 'fluent-booking'), 'Select on the Next Step' => __('Select on the Next Step', 'fluent-booking'), 'location options' => __('location options', 'fluent-booking'), 'Your address' => __('Your address', 'fluent-booking'), 'Organizer Phone Number' => __('Organizer Phone Number', 'fluent-booking'), 'In Person (Attendee Address)' => __('In Person (Attendee Address)', 'fluent-booking'), 'In Person (Organizer Address)' => __('In Person (Organizer Address)', 'fluent-booking'), 'Attendee Phone Number' => __('Attendee Phone Number', 'fluent-booking'), 'Google Meet' => __('Google Meet', 'fluent-booking'), 'Zoom Meeting' => __('Zoom Meeting', 'fluent-booking'), 'Online Meeting' => __('Online Meeting', 'fluent-booking'), 'Phone Call' => __('Phone Call', 'fluent-booking'), 'Processing...' => __('Processing...', 'fluent-booking'), 'Loading Payment Processor...' => __('Loading Payment Processor...', 'fluent-booking'), 'PM' => __('PM', 'fluent-booking'), 'AM' => __('AM', 'fluent-booking'), 'Name' => __('Name', 'fluent-booking'), 'Email' => __('Email', 'fluent-booking'), 'Date' => __('Date', 'fluent-booking'), 'Time' => __('Time', 'fluent-booking'), 'per occurrence' => __('per occurrence', 'fluent-booking'), 'per guest' => __('per guest', 'fluent-booking'), 'Add guest' => __('Add guest', 'fluent-booking'), 'Add guests' => __('Add guests', 'fluent-booking'), 'Add another' => __('Add another', 'fluent-booking'), 'Choose File' => __('Choose File', 'fluent-booking'), 'This field is required.' => __('This field is required.', 'fluent-booking'), 'No availability in' => __('No availability in', 'fluent-booking'), 'View next month' => __('View next month', 'fluent-booking'), 'View previous month' => __('View previous month', 'fluent-booking'), 'Back to Date Selection' => __('Back to Date Selection', 'fluent-booking'), 'Go to previous page' => __('Go to previous page', 'fluent-booking'), 'Remove this time slot' => __('Remove this time slot', 'fluent-booking'), 'Remove coupon' => __('Remove coupon', 'fluent-booking'), 'Select this time' => __('Select this time', 'fluent-booking'), 'Confirm Time' => __('Confirm Time', 'fluent-booking'), '12 hour time format' => __('12 hour time format', 'fluent-booking'), '24 hour time format' => __('24 hour time format', 'fluent-booking'), 'No_payment_method_description' => __('No activated payment method found. If you are an admin please check the event payment settings', 'fluent-booking'), 'Please fill up the required data' => __('Please fill up the required data', 'fluent-booking'), 'Please select a valid payment method' => __('Please select a valid payment method', 'fluent-booking'), 'Please Select' => __('Please Select', 'fluent-booking'), 'Something is wrong!' => __('Something is wrong!', 'fluent-booking'), 'Requires Confirmation' => __('Requires Confirmation', 'fluent-booking'), ], 'theme' => Arr::get(get_option('_fluent_booking_settings'), 'theme', 'system-default'), 'currency_settings' => CurrenciesHelper::getGlobalCurrencySettings() ]; if (isset($_SERVER['HTTP_CF_IPCOUNTRY'])) { $data['user_country'] = isset($_SERVER['HTTP_CF_IPCOUNTRY']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_IPCOUNTRY'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended } else { $data['user_country'] = Arr::get($globalSettings, 'administration.default_country', ''); } return apply_filters('fluent_calendar/global_booking_vars', $data); } public function ajaxScheduleMeeting() { if (!Helper::checkRateLimit('schedule_meeting', 15)) { wp_send_json_error(['message' => __('Too many requests. Please try again in a minute.', 'fluent-booking')], 429); } $app = App::getInstance(); $postedData = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $eventId = (int)$postedData['event_id']; $isRescheduling = Arr::get($postedData, 'rescheduling_hash', ''); $calendarEvent = CalendarSlot::find($eventId); if (!$calendarEvent || ($calendarEvent->status != 'active' && !$isRescheduling)) { wp_send_json([ 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking') ], 422); } do_action('fluent_booking/starting_scheduling_ajax', $postedData); $rules = [ 'name' => 'required', 'email' => 'required|email', 'timezone' => 'required', 'start_date' => 'required', 'utm_source' => 'max:192', 'utm_medium' => 'max:192', 'utm_campaign' => 'max:192', 'utm_term' => 'max:192', 'utm_content' => 'max:192', ]; $messages = [ 'name.required' => __('Please enter your name', 'fluent-booking'), 'email.required' => __('Please enter your email address', 'fluent-booking'), 'email.email' => __('Please enter provide a valid email address', 'fluent-booking'), 'timezone.required' => __('Please select timezone first', 'fluent-booking'), 'start_date.required' => __('Please select a date and time', 'fluent-booking') ]; if ($calendarEvent->isPhoneRequired()) { $rules['phone_number'] = ['required', $this->validPhoneNumberRule()]; $messages['phone_number.required'] = __('Please provide your phone number', 'fluent-booking'); } else if ($calendarEvent->isAddressRequired()) { $rules['address'] = 'required'; $messages['address.required'] = __('Please provide your Address', 'fluent-booking'); } else if ($calendarEvent->isLocationFieldRequired()) { $rules['location_config.driver'] = 'required'; $messages['location_config.driver'] = __('Please select location', 'fluent-booking'); $selectedLocation = LocationService::getLocationDetails($calendarEvent, Arr::get($postedData, 'location_config', []), $postedData); $selectedLocationDriver = Arr::get($selectedLocation, 'type'); // is user input required if (in_array($selectedLocationDriver, ['in_person_guest', 'phone_guest'])) { if ($selectedLocationDriver == 'in_person_guest') { $rules['location_config.user_location_input'] = 'required'; $messages['location_config.user_location_input.required'] = __('Please provide your address', 'fluent-booking'); } else { $rules['location_config.user_location_input'] = ['required', $this->validPhoneNumberRule()]; $messages['location_config.user_location_input.required'] = __('Please provide your phone number', 'fluent-booking'); } } } $duration = (int)$calendarEvent->getDuration(Arr::get($postedData, 'duration', null)); if ($additionalGuests = Arr::get($postedData, 'guests', [])) { if ($calendarEvent->isMultiGuestEvent()) { $additionalGuests = $this->sanitize_mapped_data($additionalGuests); $additionalGuests = array_values(array_filter($additionalGuests, function ($guest) { return Arr::get($guest, 'name') && Arr::get($guest, 'email'); })); } else { $additionalGuests = array_filter(array_map('sanitize_email', $additionalGuests)); } } $postedData['guests'] = $additionalGuests; $requiredFields = array_filter($calendarEvent->getMeta('booking_fields', []), function ($field) { return Arr::isTrue($field, 'required') && Arr::isTrue($field, 'enabled') && (Arr::get($field, 'name') == 'message' || Arr::get($field, 'name') == 'guests'); }); foreach ($requiredFields as $field) { if (empty($rules[$field['name']])) { $rules[$field['name']] = 'required'; $messages[$field['name'] . '.required'] = __('This field is required', 'fluent-booking'); } } $validationConfig = apply_filters('fluent_booking/schedule_validation_rules_data', [ 'rules' => $rules, 'messages' => $messages ], $postedData, $calendarEvent); $validator = $app->validator->make($postedData, $validationConfig['rules'], $validationConfig['messages']); if ($validator->validate()->fails()) { $errorMessage = $validator->firstError() ?: __('Please fill up the required data', 'fluent-booking'); wp_send_json([ 'message' => $errorMessage, 'errors' => $validator->errors() ], 422); } $customFieldsData = BookingFieldService::getCustomFieldsData($postedData, $calendarEvent); $customFieldsData = apply_filters('fluent_booking/schedule_custom_field_data', $customFieldsData, $calendarEvent); if (is_wp_error($customFieldsData)) { wp_send_json([ 'message' => $customFieldsData->get_error_message(), 'errors' => $customFieldsData->get_error_data() ], 422); } $validateDateFields = BookingFieldService::validateDateFields($customFieldsData, $calendarEvent); if (is_wp_error($validateDateFields)) { wp_send_json([ 'message' => $validateDateFields->get_error_message(), ], 422); } $startDate = Arr::get($postedData, 'start_date'); $timezone = sanitize_text_field(Arr::get($postedData, 'timezone', 'UTC')); if (is_array($startDate)) { $startDateTime = array_slice( array_map(function($date) use ($timezone) { return DateTimeHelper::convertToUtc(sanitize_text_field($date), $timezone); }, $startDate), 0, $calendarEvent->multiBookingLimit() ); $endDateTime = array_map(function($date) use ($duration) { return gmdate('Y-m-d H:i:s', strtotime($date) + ($duration * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date }, $startDateTime); } if (is_string($startDate)) { $startDate = sanitize_text_field($startDate); $startDateTime = DateTimeHelper::convertToUtc($startDate, $timezone); $endDateTime = gmdate('Y-m-d H:i:s', strtotime($startDateTime) + ($duration * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } $bookingData = apply_filters('fluent_booking/initialize_booking_data', [ 'person_time_zone' => sanitize_text_field($timezone), 'start_time' => $startDateTime, 'end_time' => $endDateTime, 'name' => sanitize_text_field($postedData['name']), 'email' => sanitize_email($postedData['email']), 'message' => sanitize_textarea_field(wp_unslash(Arr::get($postedData, 'message', ''))), 'phone' => sanitize_text_field(Arr::get($postedData, 'phone_number', '')), 'address' => sanitize_textarea_field(Arr::get($postedData, 'address', '')), 'ip_address' => Helper::getIp(), 'status' => 'scheduled', 'source' => 'web', 'event_type' => $calendarEvent->event_type, 'slot_minutes' => $duration, 'utm_source' => sanitize_text_field(Arr::get($postedData, 'utm_source', '')), 'utm_medium' => sanitize_text_field(Arr::get($postedData, 'utm_medium', '')), 'utm_campaign' => sanitize_text_field(Arr::get($postedData, 'utm_campaign', '')), 'utm_term' => sanitize_text_field(Arr::get($postedData, 'utm_term', '')), 'utm_content' => sanitize_text_field(Arr::get($postedData, 'utm_content', '')) ], $postedData, $calendarEvent); if ($calendarEvent->isConfirmationRequired($bookingData['start_time'])) { $bookingData['status'] = 'pending'; } $locationConfig = Arr::get($postedData, 'location_config', []); $selectedLocation = LocationService::getLocationDetails($calendarEvent, $locationConfig, $postedData); if ($selectedLocation['type'] == 'phone_guest') { $bookingData['phone'] = $selectedLocation['description']; } $bookingData['location_details'] = $selectedLocation; if ($sourceUrl = Arr::get($postedData, 'source_url', '')) { $bookingData['source_url'] = sanitize_url($sourceUrl); } if (!empty($postedData['coupon_codes'])) { $bookingData['coupon_codes'] = array_map('sanitize_text_field', array_unique($postedData['coupon_codes'])); } if (!empty($postedData['payment_method'])) { $customFieldsData['payment_method'] = sanitize_text_field($postedData['payment_method']); } if (!empty($postedData['recurring_count'])) { $bookingData['recurring_count'] = (int) Arr::get($postedData, 'recurring_count', 0); } $timeSlotService = TimeSlotServiceHandler::initService($calendarEvent->calendar, $calendarEvent); if (is_wp_error($timeSlotService)) { return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timezone); } $availableSpot = $timeSlotService->isSpotAvailable($bookingData['start_time'], $bookingData['end_time'], $duration); if (!$availableSpot) { wp_send_json([ 'message' => __('This selected time slot is not available. Maybe someone booked the spot just a few seconds ago.', 'fluent-booking') ], 422); } if ($additionalGuests) { $guestField = BookingFieldService::getBookingFieldByName($calendarEvent, 'guests'); $guestLimit = Arr::get($guestField, 'limit', 10); if ($calendarEvent->isMultiGuestEvent()) { $remaining = Arr::get($availableSpot, 'remaining', $calendarEvent->getMaxBookingPerSlot()); $guestLimit = min($remaining, $guestLimit) - 1; } $bookingData['additional_guests'] = array_slice($additionalGuests, 0, $guestLimit); } if ($calendarEvent->isRoundRobin()) { $bookingData['host_user_id'] = $timeSlotService->hostUserId; } do_action('fluent_booking/before_creating_schedule', $bookingData, $postedData, $calendarEvent); try { $booking = BookingService::createBooking($bookingData, $calendarEvent, $customFieldsData); if (is_wp_error($booking)) { throw new \Exception(wp_kses_post($booking->get_error_message()), 422); } } catch (\Exception $e) { wp_send_json([ 'message' => $e->getMessage() ], 422); return; } $redirectUrl = $booking->getRedirectUrlWithQuery(); $html = BookingService::getBookingConfirmationHtml($booking); wp_send_json(apply_filters('fluent_booking/booking_confirmation_response', [ 'message' => __('Booking has been confirmed', 'fluent-booking'), 'redirect_url' => $redirectUrl, 'response_html' => $html, 'booking_hash' => $booking->hash ], $booking), 200); } /** * @return \Closure */ private function validPhoneNumberRule() { return function ($attribute, $value) { if (!empty($value) && !Helper::isValidPhoneNumber($value)) { return __('Please provide a valid phone number', 'fluent-booking'); } }; } public function ajaxGetAvailableDates() { if (!Helper::checkRateLimit('available_dates', 30)) { wp_send_json_error(['message' => __('Too many requests. Please try again in a minute.', 'fluent-booking')], 429); } $startBenchmark = microtime(true); $request = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $eventId = (int)Arr::get($request, 'event_id'); $reschedulingHash = sanitize_text_field(Arr::get($request, 'rescheduling_hash', '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $calendarEvent = $eventId ? CalendarSlot::find($eventId) : null; if (!$calendarEvent) { wp_send_json([ 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking') ], 422); } if ($calendarEvent->status != 'active') { $existingBooking = $reschedulingHash ? Booking::where('hash', $reschedulingHash)->first() : null; if (!$existingBooking || (int)$existingBooking->event_id !== (int)$calendarEvent->id) { wp_send_json([ 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking') ], 422); } } $calendar = $calendarEvent->calendar; $startDate = sanitize_text_field(Arr::get($request, 'start_date')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if (!$startDate) { $startDate = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } $timeZone = sanitize_text_field(Arr::get($request, 'timezone')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if (!$timeZone) { $timeZone = wp_timezone_string(); } if (!in_array($timeZone, \DateTimeZone::listIdentifiers())) { $timeZone = $calendar->author_timezone; } $duration = (int)$calendarEvent->getDuration(Arr::get($request, 'duration', null)); $timeSlotService = TimeSlotServiceHandler::initService($calendar, $calendarEvent); if (is_wp_error($timeSlotService)) { return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timeZone); } $availableSpots = $timeSlotService->getAvailableSpots($startDate, $timeZone, $duration); if (is_wp_error($availableSpots)) { return TimeSlotServiceHandler::sendError($availableSpots, $calendarEvent, $timeZone); } $availableSpots = array_filter((array) $availableSpots); $availableSpots = apply_filters('fluent_booking/available_slots_for_view', $availableSpots, $calendarEvent, $calendar, $timeZone, $duration); wp_send_json([ 'available_slots' => $availableSpots, 'timezone' => $timeZone, 'max_lookup_date' => $calendarEvent->getMaxLookUpDate(), 'execution_time' => microtime(true) - $startBenchmark ], 200); } public function getCalendarEventVars(Calendar $calendar, CalendarSlot $calendarEvent) { static $globalConfig = null; if ($globalConfig === null) { $globalConfig = [ 'time_format' => Arr::get(get_option('_fluent_booking_settings'), 'time_format', '12'), 'date_formatter' => DateTimeHelper::getDateFormatter(true), 'isRtl' => Helper::fluentbooking_is_rtl(), 'has_pro' => defined('FLUENT_BOOKING_PRO_DIR_FILE'), 'duration_lookup' => Helper::getDurationLookup(), 'multi_duration_lookup' => Helper::getDurationLookup(true), ]; } $calendarEvent->description = wpautop($calendarEvent->description); $calendarEvent->location_icon_html = $calendarEvent->defaultLocationHtml(); $formFields = BookingFieldService::getBookingFields($calendarEvent); $eventData = [ 'id' => $calendarEvent->id, 'max_lookup_date' => $calendarEvent->getMaxLookUpDate(), 'min_lookup_date' => $calendarEvent->getMinLookUpDate(), 'min_bookable_date' => $calendarEvent->getMinBookableDateTime(), 'is_display_spots' => $calendarEvent->isDisplaySpots(), 'duration' => $calendarEvent->getDefaultDuration(), 'title' => $calendarEvent->title, 'location_settings' => $this->sanitizePublicLocationSettings($calendarEvent->location_settings), 'location_icon_html' => $calendarEvent->location_icon_html, 'description' => $calendarEvent->description, 'pre_selects' => null, 'settings' => $this->sanitizePublicEventSettings($calendarEvent->settings), 'type' => $calendarEvent->type, 'event_type' => $calendarEvent->event_type, 'time_format' => $globalConfig['time_format'], ]; $author = $calendar->getAuthorProfile(true); $author['name'] = $calendar->title; $eventVars = [ 'slot' => $eventData, 'author_profile' => $author, 'form_fields' => $formFields, 'i18n' => [ 'Schedule_Meeting' => __('Schedule Meeting', 'fluent-booking'), 'Continue_to_Payments' => __('Continue to Payments', 'fluent-booking'), 'Confirm_Payment' => __('Confirm Payment', 'fluent-booking'), ], 'date_formatter' => $globalConfig['date_formatter'], 'isRtl' => $globalConfig['isRtl'], 'has_pro' => $globalConfig['has_pro'], 'duration_lookup' => $globalConfig['duration_lookup'], 'multi_duration_lookup' => $globalConfig['multi_duration_lookup'] ]; $eventVars['form_fields'] = array_values($eventVars['form_fields']); if (!$calendar->isHostCalendar()) { $eventVars['team_member_profiles'] = $calendarEvent->getAuthorProfiles(true); } return apply_filters('fluent_booking/public_event_vars', $eventVars, $calendarEvent); } private function sanitizePublicLocationSettings($locationSettings) { if (!is_array($locationSettings)) { return []; } $safe = []; foreach ($locationSettings as $location) { if (!is_array($location)) { continue; } $type = Arr::get($location, 'type'); $displayOnBooking = Arr::get($location, 'display_on_booking') === 'yes'; $sanitized = [ 'type' => $type, 'title' => Arr::get($location, 'title'), 'display_on_booking' => Arr::get($location, 'display_on_booking', 'no'), ]; // Only expose host-private fields when the host explicitly opted // in to display them before booking. if ($displayOnBooking) { if ($type === 'online_meeting') { $sanitized['meeting_link'] = Arr::get($location, 'meeting_link'); } elseif ($type === 'phone_organizer') { $sanitized['host_phone_number'] = Arr::get($location, 'host_phone_number'); } elseif (in_array($type, ['in_person_organizer', 'custom'], true)) { $sanitized['description'] = Arr::get($location, 'description'); } } $safe[] = $sanitized; } return $safe; } private function sanitizePublicEventSettings($settings) { if (!is_array($settings)) { return []; } $publicKeys = [ 'recurring_config', 'multiple_booking', 'multi_duration', 'lock_timezone', 'requires_confirmation', 'submit_button_text', ]; $publicKeys = apply_filters('fluent_booking/public_event_settings_keys', $publicKeys); $safe = []; foreach ($publicKeys as $key) { if (array_key_exists($key, $settings)) { $safe[$key] = $settings[$key]; } } return $safe; } public function ajaxHandleCancelMeeting() { if (!Helper::checkRateLimit('cancel_meeting', 15)) { wp_send_json_error(['message' => __('Too many requests. Please try again in a minute.', 'fluent-booking')], 429); } $data = $_REQUEST; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $meetingHash = Arr::get($data, 'meeting_hash'); $meeting = Booking::where('hash', $meetingHash)->first(); if (!$meeting) { wp_send_json([ 'message' => __('Sorry! meeting could not be found', 'fluent-booking') ], 422); } if (!$meeting->canCancel()) { wp_send_json([ 'message' => $meeting->getCancellationMessage() ], 422); } $message = sanitize_textarea_field(Arr::get($data, 'cancellation_reason', '')); $cancelField = BookingFieldService::getBookingFieldByName($meeting->calendar_event, 'cancellation_reason'); if (!$message && Arr::isTrue($cancelField, 'required')) { wp_send_json([ 'message' => __('Please provide a reason for cancellation', 'fluent-booking') ], 422); } $result = $meeting->cancelMeeting($message, 'guest', get_current_user_id()); if (is_wp_error($result)) { if (!wp_doing_ajax()) { wp_safe_redirect($meeting->getConfirmationUrl()); exit(); } wp_send_json([ 'message' => $result->get_error_message() ], 422); } if (wp_doing_ajax()) { wp_send_json([ 'message' => __('Meeting has been cancelled', 'fluent-booking') ], 200); } wp_safe_redirect($meeting->getConfirmationUrl()); exit; } private static function sanitize_mapped_data($settings) { $sanitizerMap = [ 'name' => 'sanitize_text_field', 'email' => 'sanitize_email', ]; return Helper::fcal_backend_sanitizer($settings, $sanitizerMap); } }