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);
}
}