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/Http/Controllers/CalendarController.php +232 -68 1.10.0 → 2.5.0 View file →
@@ -1,8 +1,9 @@
1 1 <?php
2 2
3 3 namespace FluentBooking\App\Http\Controllers;
4 4
5 +use FluentBooking\App\Models\Availability;
5 6 use FluentBooking\App\Models\Calendar;
6 7 use FluentBooking\App\Models\CalendarSlot;
7 8 use FluentBooking\App\Services\Helper;
8 9 use FluentBooking\App\Services\LandingPage\LandingPageHelper;
@@ -32,9 +33,9 @@
32 33 $query->where('title', 'LIKE', '%' . $search . '%');
33 34 }
34 35 };
35 36
36 - $calendarsQuery = Calendar::with(['slots' => function($query) use ($applySearchFilter) {
37 + $calendarsQuery = Calendar::with(['metas', 'slots' => function($query) use ($applySearchFilter) {
37 38 $query->where($applySearchFilter);
38 39 }])
39 40 ->where('status', '!=', 'expired');
40 41
@@ -60,23 +61,29 @@
60 61 foreach ($calendars as $calendar) {
61 62 $calendar->author_profile = $calendar->getAuthorProfile();
62 63 $calendar->public_url = $calendar->getLandingPageUrl();
63 64 $calendar->event_order = $calendar->getMeta('event_order');
64 - foreach ($calendar->slots as $key => $slot) {
65 - if (!$hasPermission && !CalendarEventService::isSharedCalendarEvent($slot)) {
66 - unset($calendar->slots[$key]);
67 - }
65 +
66 + if (!$hasPermission) {
67 + $calendar->setRelation('slots', $calendar->slots->filter(function ($slot) {
68 + return CalendarEventService::isSharedCalendarEvent($slot);
69 + })->values());
70 + }
71 +
72 + foreach ($calendar->slots as $slot) {
73 + $slot->setRelation('calendar', $calendar);
68 74 $slot->shortcode = '[fluent_booking id="' . $slot->id . '"]';
69 75 $slot->public_url = $slot->getPublicUrl();
70 76 $slot->duration = $slot->getDefaultDuration();
71 - $slot->price_total = $slot->getPricingTotal();
77 + $slot->price_total = $slot->getEventPrice();
72 78 $slot->location_fields = $slot->getLocationFields();
73 79 $slot->author_profiles = $slot->isMultiHostEvent() ? $slot->getAuthorProfiles() : [];
74 80 do_action_ref_array('fluent_booking/calendar_slot', [&$slot]);
81 + $slot->unsetRelation('calendar');
75 82 }
76 83
77 84 if(empty($calendar->author_profile['ID'])) {
78 - $calendar->generic_error = '<p style="color: red; margin:0;">Connected Host user is missing</p>';
85 + $calendar->generic_error = '<p style="color: var(--fcal-danger-fg); margin:0;">Connected Host user is missing</p>';
79 86 }
80 87
81 88 do_action_ref_array('fluent_booking/calendar', [&$calendar, 'lists']);
82 89 }
@@ -106,12 +113,59 @@
106 113 ], 422);
107 114 }
108 115
109 116 return [
110 - 'status' => true
117 + 'status' => true,
118 + 'message' => __('The provided slug is available', 'fluent-booking')
111 119 ];
112 120 }
113 121
122 + public function getNewEventLocationFields(Request $request)
123 + {
124 + $eventType = SanitizeService::checkCollection(
125 + sanitize_text_field($request->get('event_type', 'single')),
126 + CalendarSlot::getEventTypes(),
127 + 'single'
128 + );
129 +
130 + // Resolve the organizer the same way createCalendar() does, so the
131 + // connection checks run against the host the event will be saved under.
132 + $canAssignOthers = PermissionManager::canManageOtherHosts();
133 +
134 + $userId = get_current_user_id();
135 + $requestedUserId = (int) $request->get('user_id');
136 + if ($requestedUserId && $canAssignOthers) {
137 + $userId = $requestedUserId;
138 + }
139 +
140 + $calendarEvent = new CalendarSlot();
141 + $calendarEvent->event_type = $eventType;
142 +
143 + if ($calendarEvent->isMultiHostEvent()) {
144 + $teamMembers = array_values(array_unique(array_filter(
145 + array_map('intval', (array) $request->get('team_members', []))
146 + )));
147 +
148 + if (!PermissionManager::canAssignHosts($teamMembers, [$userId])) {
149 + return $this->sendError([
150 + 'message' => __('You are not allowed to create a calendar for another user', 'fluent-booking')
151 + ], 403);
152 + }
153 +
154 + if ($teamMembers && !in_array($userId, $teamMembers, true)) {
155 + $userId = reset($teamMembers);
156 + }
157 +
158 + $calendarEvent->settings = ['team_members' => $teamMembers];
159 + }
160 +
161 + $calendarEvent->user_id = $userId;
162 +
163 + return [
164 + 'location_fields' => $calendarEvent->getLocationFields()
165 + ];
166 + }
167 +
114 168 public function createCalendar(Request $request)
115 169 {
116 170 $data = $request->get('calendar');
117 171
@@ -147,9 +201,9 @@
147 201 $this->validate($data, $validationConfig['rules'], $validationConfig['messages']);
148 202
149 203 do_action('fluent_booking/before_create_calendar', $data, $this);
150 204
151 - if (!empty($data['user_id']) && PermissionManager::userCan('invite_team_members')) {
205 + if (!empty($data['user_id']) && PermissionManager::canManageOtherHosts()) {
152 206 $user = get_user_by('ID', $data['user_id']);
153 207 } else {
154 208 $user = get_user_by('ID', get_current_user_id());
155 209 }
@@ -165,15 +219,19 @@
165 219 $installableAddons = SanitizeService::sanitizeAddons($onboardinFeatures);
166 220 OnboardingService::installAddons($installableAddons);
167 221 }
168 222
169 - $type = sanitize_text_field(Arr::get($data, 'type', 'simple'));
223 + $type = SanitizeService::checkCollection(
224 + sanitize_text_field(Arr::get($data, 'type', 'simple')),
225 + ['simple', 'team', 'event'],
226 + 'simple'
227 + );
170 228
171 229 $isHostCalendar = $type == 'simple' ? true : false;
172 230
173 231 if ($isHostCalendar && Calendar::where('user_id', $user->ID)->where('type', 'simple')->first()) {
174 232 return $this->sendError([
175 - 'message' => __('The user already have a calendar. Please delete it first to create a new one', 'fluent-booking')
233 + 'message' => __('The user already has a calendar. Please delete it first to create a new one', 'fluent-booking')
176 234 ], 422);
177 235 }
178 236
179 237 if ($isHostCalendar) {
@@ -189,10 +247,30 @@
189 247
190 248 if (!$isHostCalendar) {
191 249 $title = sanitize_text_field(Arr::get($data, 'title', ''));
192 250 $data['slug'] = sanitize_title($title, '', 'display');
193 - $teamMembers = array_map('intval', Arr::get($slot, 'settings.team_members', []));
194 - if (!in_array($user->ID, $teamMembers)) {
251 + $teamMembers = array_values(array_filter(
252 + array_map('intval', (array) Arr::get($slot, 'settings.team_members', []))
253 + ));
254 +
255 + cache_users($teamMembers);
256 +
257 + foreach ($teamMembers as $memberId) {
258 + if (!get_user_by('ID', $memberId)) {
259 + return $this->sendError([
260 + 'message' => __('Invalid Team Member', 'fluent-booking')
261 + ], 422);
262 + }
263 + }
264 +
265 + // Gated even when the creator is listed too, not only when they are absent.
266 + if (!PermissionManager::canAssignHosts($teamMembers, [$user->ID])) {
267 + return $this->sendError([
268 + 'message' => __('You are not allowed to create a calendar for another user', 'fluent-booking')
269 + ], 403);
270 + }
271 +
272 + if (!in_array($user->ID, $teamMembers, true)) {
195 273 $user = get_user_by('ID', reset($teamMembers));
196 274 if (!$user) {
197 275 return $this->sendError([
198 276 'message' => __('Invalid Team Member', 'fluent-booking')
@@ -287,9 +365,28 @@
287 365 $query->where('status', '!=', 'expired');
288 366 }])->findOrFail($calendarId);
289 367
290 368 $calendar->author_profile = $calendar->getAuthorProfile();
369 + $calendar->event_order = $calendar->getMeta('event_order');
291 370
371 + if (!PermissionManager::hasAllCalendarAccess(true)) {
372 + $calendar->setRelation('slots', $calendar->slots->filter(function ($slot) {
373 + return CalendarEventService::isSharedCalendarEvent($slot);
374 + })->values());
375 + }
376 +
377 + foreach ($calendar->slots as $slot) {
378 + $slot->setRelation('calendar', $calendar);
379 + $slot->shortcode = '[fluent_booking id="' . $slot->id . '"]';
380 + $slot->public_url = $slot->getPublicUrl();
381 + $slot->duration = $slot->getDefaultDuration();
382 + $slot->price_total = $slot->getEventPrice();
383 + $slot->location_fields = $slot->getLocationFields();
384 + $slot->author_profiles = $slot->isMultiHostEvent() ? $slot->getAuthorProfiles() : [];
385 + do_action_ref_array('fluent_booking/calendar_slot', [&$slot]);
386 + $slot->unsetRelation('calendar');
387 + }
388 +
292 389 $data = [
293 390 'calendar' => $calendar
294 391 ];
295 392
@@ -296,8 +393,12 @@
296 393 if (in_array('settings_menu', $request->get('with', []))) {
297 394 $data['settings_menu'] = AdminMenuHandler::getCalendarSettingsMenuItems($calendar);
298 395 }
299 396
397 + if (in_array('public_url', $request->get('with', []))) {
398 + $data['public_url'] = $calendar->getLandingPageUrl();
399 + }
400 +
300 401 return $data;
301 402 }
302 403
303 404 public function getSharingSettings(Request $request, $calendarId)
@@ -304,10 +405,11 @@
304 405 {
305 406 $calendar = Calendar::findOrFail($calendarId);
306 407
307 408 return [
308 - 'settings' => LandingPageHelper::getSettings($calendar),
309 - 'share_url' => $calendar->getLandingPageUrl(true)
409 + 'settings' => LandingPageHelper::getSettings($calendar),
410 + 'share_url' => $calendar->getLandingPageUrl(true),
411 + 'public_url' => $this->getSharePublicUrl($calendar, intval($request->get('event_id')))
310 412 ];
311 413 }
312 414
313 415 public function saveSharingSettings(Request $request, $calendarId)
@@ -343,12 +445,27 @@
343 445 $sharingSettings = $request->get('landing_page_settings', []);
344 446 LandingPageHelper::updateSettings($calendar, $sharingSettings);
345 447
346 448 return [
347 - 'message' => __('Landing Page settings has been updated', 'fluent-booking')
449 + 'message' => __('Landing Page settings has been updated', 'fluent-booking'),
450 + 'public_url' => $this->getSharePublicUrl($calendar, intval($request->get('event_id')))
348 451 ];
349 452 }
350 453
454 + private function getSharePublicUrl($calendar, $eventId)
455 + {
456 + if ($eventId) {
457 + $event = CalendarSlot::where('calendar_id', $calendar->id)
458 + ->where('id', $eventId)
459 + ->first();
460 + if ($event) {
461 + return $event->getPublicUrl();
462 + }
463 + }
464 +
465 + return $calendar->getLandingPageUrl();
466 + }
467 +
351 468 public function updateCalendar(Request $request, $calendarId)
352 469 {
353 470 $data = $request->all();
354 471
@@ -477,12 +594,32 @@
477 594 ], $slot);
478 595
479 596 $this->validate($slot, $validationConfig['rules'], $validationConfig['messages']);
480 597
598 + $teamMembers = array_values(array_unique(array_filter(
599 + array_map('intval', (array) Arr::get($slot, 'settings.team_members', []))
600 + )));
601 +
602 + cache_users($teamMembers);
603 +
604 + foreach ($teamMembers as $memberId) {
605 + if (!get_user_by('ID', $memberId)) {
606 + return $this->sendError([
607 + 'message' => __('Invalid Team Member', 'fluent-booking')
608 + ], 422);
609 + }
610 + }
611 +
612 + if ($teamMembers && !PermissionManager::canAssignHosts($teamMembers, $calendar->getMemberIds())) {
613 + return $this->sendError([
614 + 'message' => __('You are not allowed to create a calendar for another user', 'fluent-booking')
615 + ], 403);
616 + }
617 +
481 618 $availability = AvailabilityService::getDefaultSchedule($calendar->user_id);
482 619
483 620 $slotData = [
484 - 'title' => $slot['title'],
621 + 'title' => sanitize_text_field($slot['title']),
485 622 'slug' => Helper::generateSlotSlug($slot['duration'] . 'min', $calendar),
486 623 'calendar_id' => $calendar->id,
487 624 'user_id' => $calendar->user_id,
488 625 'duration' => (int)$slot['duration'],
@@ -497,13 +634,13 @@
497 634 'schedule_conditions' => SanitizeService::scheduleConditions(Arr::get($slot['settings'], 'schedule_conditions', [])),
498 635 'buffer_time_before' => sanitize_text_field(Arr::get($slot['settings'], 'buffer_time_before', '0')),
499 636 'buffer_time_after' => sanitize_text_field(Arr::get($slot['settings'], 'buffer_time_after', '0')),
500 637 'slot_interval' => sanitize_text_field(Arr::get($slot['settings'], 'slot_interval', '')),
501 - 'team_members' => array_map('intval', Arr::get($slot['settings'], 'team_members', []))
638 + 'team_members' => $teamMembers
502 639 ],
503 640 'status' => SanitizeService::checkCollection($slot['status'], ['active', 'draft'], 'active'),
504 641 'color_schema' => sanitize_text_field(Arr::get($slot, 'color_schema', '#0099ff')),
505 - 'event_type' => sanitize_text_field(Arr::get($slot, 'event_type')),
642 + 'event_type' => SanitizeService::checkCollection(sanitize_text_field(Arr::get($slot, 'event_type')), CalendarSlot::getEventTypes(), 'single'),
506 643 'availability_type' => 'existing_schedule',
507 644 'availability_id' => $availability ? $availability->id : null,
508 645 'location_type' => sanitize_text_field(Arr::get($slot, 'location_type')),
509 646 'location_settings' => SanitizeService::locationSettings(Arr::get($slot, 'location_settings', [])),
@@ -618,19 +755,69 @@
618 755 'range_date_between' => SanitizeService::rangeDateBetween(Arr::get($data, 'range_date_between', ['', ''])),
619 756 'common_schedule' => Arr::isTrue($data, 'common_schedule', false)
620 757 ];
621 758
759 + $hostsSchedules = [];
760 +
622 761 if ($event->isTeamEvent()) {
623 - $eventSettings['hosts_schedules'] = array_map('intval', array_combine(
762 + $hostsSchedules = array_map('intval', array_combine(
624 763 array_map('intval', array_keys(Arr::get($data, 'hosts_schedules', []))),
625 764 array_map('intval', Arr::get($data, 'hosts_schedules', []))
626 765 ));
627 766 }
628 767
768 + $availabilityId = (int)Arr::get($data, 'availability_id');
769 + $availabilityType = SanitizeService::checkCollection(Arr::get($data, 'availability_type'), ['existing_schedule', 'custom']);
770 +
771 + $submittedIds = array_values(array_filter(array_unique(array_merge(
772 + [$availabilityType === 'existing_schedule' ? $availabilityId : 0],
773 + array_values($hostsSchedules)
774 + ))));
775 +
776 + $usableIds = [];
777 + $scheduleOwners = [];
778 +
779 + if ($submittedIds) {
780 + $usableIds = array_map('intval', AvailabilityService::usableAvailabilityQuery()
781 + ->whereIn('id', $submittedIds)->pluck('id')->toArray());
782 +
783 + $scheduleOwners = array_map('intval', Availability::whereIn('id', $submittedIds)
784 + ->pluck('object_id', 'id')->toArray());
785 + }
786 +
787 + foreach ($hostsSchedules as $hostId => $scheduleId) {
788 + if (($scheduleOwners[$scheduleId] ?? 0) === (int)$hostId) {
789 + continue;
790 + }
791 +
792 + if (!in_array($scheduleId, $usableIds, true)) {
793 + return $this->sendError([
794 + 'message' => __('You are not allowed to use the selected schedule', 'fluent-booking')
795 + ], 403);
796 + }
797 + }
798 +
799 + if ($hostsSchedules) {
800 + $eventSettings['hosts_schedules'] = $hostsSchedules;
801 + }
802 +
803 + $eventHostIds = array_map('intval', array_merge(
804 + $event->getHostIds(),
805 + [$event->user_id, $event->calendar->user_id]
806 + ));
807 +
808 + if ($availabilityType === 'existing_schedule' && $availabilityId
809 + && !in_array($availabilityId, $usableIds, true)
810 + && !in_array($scheduleOwners[$availabilityId] ?? 0, $eventHostIds, true)) {
811 + return $this->sendError([
812 + 'message' => __('You are not allowed to use the selected schedule', 'fluent-booking')
813 + ], 403);
814 + }
815 +
629 816 $event->settings = $eventSettings;
630 817
631 - $event->availability_id = (int)Arr::get($data, 'availability_id');
632 - $event->availability_type = SanitizeService::checkCollection(Arr::get($data, 'availability_type'), ['existing_schedule', 'custom']);
818 + $event->availability_id = $availabilityId;
819 + $event->availability_type = $availabilityType;
633 820
634 821 $event->save();
635 822
636 823 return [
@@ -692,12 +879,28 @@
692 879 public function cloneCalendarEvent(Request $request, $calendarId, $eventId)
693 880 {
694 881 $newCalendarId = intval($request->get('new_calendar_id')) ?: $calendarId;
695 882
883 + if (!PermissionManager::canWriteCalendar($newCalendarId)) {
884 + return $this->sendError([
885 + 'message' => __('You do not have permission to write to the destination calendar.', 'fluent-booking')
886 + ], 403);
887 + }
888 +
696 889 $calendar = Calendar::findOrFail($newCalendarId);
697 890
698 891 $originalEvent = CalendarSlot::with('event_metas')->where('calendar_id', $calendarId)->findOrFail($eventId);
699 892
893 + $teamMembers = Arr::get($originalEvent->settings, 'team_members', []);
894 +
895 + // Cloning into another calendar carries the source hosts along with it.
896 + if ($teamMembers && $calendar->id != $calendarId
897 + && !PermissionManager::canAssignHosts($teamMembers, $calendar->getMemberIds())) {
898 + return $this->sendError([
899 + 'message' => __('You are not allowed to create a calendar for another user', 'fluent-booking')
900 + ], 403);
901 + }
902 +
700 903 $clonedEvent = $originalEvent->replicate();
701 904
702 905 $clonedEvent->hash = null;
703 906
@@ -746,8 +949,14 @@
746 949 $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
747 950
748 951 $fromEventId = intval($request->get('from_event_id'));
749 952
953 + if (!$fromEventId || !PermissionManager::canUpdateCalendarEvent($fromEventId)) {
954 + return $this->sendError([
955 + 'message' => __('You do not have permission to clone from the selected event.', 'fluent-booking')
956 + ], 403);
957 + }
958 +
750 959 $fromCalendarEvent = CalendarSlot::findOrFail($fromEventId);
751 960
752 961 $notification = $fromCalendarEvent->getNotifications(true);
753 962
@@ -833,54 +1042,9 @@
833 1042 $calendarEvent = CalendarSlot::where('calendar_id', $calendarId)->findOrFail($eventId);
834 1043
835 1044 $bookingFields = $request->get('booking_fields');
836 1045
837 - $optionRequiredFields = ['dropdown', 'radio', 'checkbox-group', 'multi-select'];
838 -
839 - $formattedFields = [];
840 -
841 - $textFields = ['type', 'name', 'label', 'placeholder', 'limit', 'help_text', 'date_format', 'min_date', 'max_date'];
842 - $booleanFields = ['enabled', 'required', 'system_defined', 'disable_alter', 'is_sms_number'];
843 -
844 - foreach ($bookingFields as $value) {
845 - if (empty($value['name'])) {
846 - $value['name'] = BookingFieldService::generateFieldName($calendarEvent, $value['label']);
847 - } else {
848 - $value['name'] = BookingFieldService::maybeGenerateFieldName($calendarEvent, $value);
849 - }
850 -
851 - $textValues = array_map('sanitize_text_field', Arr::only($value, $textFields));
852 -
853 - $booleanValues = array_map(function ($valueItem) {
854 - return $valueItem === true || $valueItem === 'true' || $valueItem == 1;
855 - }, Arr::only($value, $booleanFields));
856 -
857 - $formattedField = array_merge($textValues, $booleanValues);
858 -
859 - $fieldType = Arr::get($value, 'type');
860 -
861 - $formattedField['index'] = (int)Arr::get($value, 'index');
862 - if (in_array($fieldType, $optionRequiredFields)) {
863 - $sanitizedOptions = array_map('sanitize_text_field', Arr::get($value, 'options'));
864 - $formattedField['options'] = $sanitizedOptions;
865 - }
866 - if ($fieldType == 'file') {
867 - $formattedField['max_file_allow'] = intval(Arr::get($value, 'max_file_allow'));
868 - $formattedField['allow_file_types'] = array_map('sanitize_text_field', Arr::get($value, 'allow_file_types'));
869 - $formattedField['file_size_value'] = intval(Arr::get($value, 'file_size_value'));
870 - $formattedField['file_size_unit'] = SanitizeService::checkCollection(Arr::get($value, 'file_size_unit'), ['kb','mb']);
871 - }
872 - if ($fieldType == 'hidden') {
873 - $formattedField['default_value'] = sanitize_text_field(Arr::get($value, 'default_value'));
874 - }
875 - if ($fieldType == 'terms-and-conditions') {
876 - $formattedField['terms_and_conditions'] = wp_kses_post(Arr::get($value, 'terms_and_conditions'));
877 - }
878 -
879 - $formattedField = apply_filters('fluent_booking/save_event_booking_field_' . $fieldType, $formattedField, $value, $calendarEvent);
880 -
881 - $formattedFields[] = $formattedField;
882 - }
1046 + $formattedFields = BookingFieldService::sanitizeBookingFields($bookingFields, $calendarEvent);
883 1047
884 1048 $calendarEvent->setBookingFields($formattedFields);
885 1049
886 1050 return [