PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / trunk
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution vtrunk
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 1.7.2 All 33 releases
fluent-booking / app / Services / CalendarService.php

CalendarService.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution trunk, at app/Services/CalendarService.php

545 lines 22.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBooking\App\Services;
4
5 use FluentBooking\App\Models\Calendar;
6 use FluentBooking\App\Models\CalendarSlot;
7 use FluentBooking\App\Models\Availability;
8 use FluentBooking\App\Services\Helper;
9 use FluentBooking\Framework\Support\Arr;
10
11 class CalendarService
12 {
13 public static function createCalendar($data, $useCurrentUser = false, $isFileInput = false)
14 {
15 $calendarData = self::prepareCalendarData($data, $useCurrentUser, $isFileInput);
16
17 if (is_wp_error($calendarData)) {
18 return new \WP_Error($calendarData->get_error_code(), $calendarData->get_error_message());
19 }
20
21 $calendarData['slug'] = sanitize_title($calendarData['title']);
22
23 if (!Helper::isCalendarSlugAvailable($calendarData['slug'], true)) {
24 $calendarData['slug'] .= '-' . time();
25 }
26
27 $calendar = Calendar::create($calendarData);
28
29 $calendarMetas = self::prepareCalendarMetas(Arr::get($data, 'metas', []));
30
31 $calendar->metas()->createMany($calendarMetas);
32
33 $availabilitiesData = Arr::get($data, 'availabilities', []);
34
35 $createdAvailabilities = self::createAvailabilities($calendar, $availabilitiesData);
36
37 $eventsData = Arr::get($data, 'events', []);
38
39 self::createCalendarEvents($calendar, $eventsData, $createdAvailabilities);
40
41 do_action('fluent_booking/after_create_calendar', $calendar);
42
43 return [
44 'calendar' => $calendar
45 ];
46 }
47
48 public static function createAvailabilities($calendar, $availabilitiesData)
49 {
50 $createdAvailabilities = [];
51
52 foreach ($availabilitiesData as $existingId => $availabilityData) {
53 $availability = Arr::only($availabilityData, ['key', 'value']);
54 $availability['value']['timezone'] = $calendar->author_timezone;
55 $availability['object_id'] = $calendar->user_id;
56 $availabilityModel = Availability::create($availability);
57 $createdAvailabilities[$existingId] = $availabilityModel->id;
58 }
59
60 return $createdAvailabilities;
61 }
62
63 public static function createCalendarEvents($calendar, $eventsData, $availabilities = [])
64 {
65 $defaultEventData = (new CalendarSlot())->getEventDefaultData($calendar);
66
67 if (empty($eventsData)) {
68 $eventsData = [$defaultEventData];
69 }
70
71 $createEventsData = [];
72
73 $createEventMetasData = [];
74
75 foreach ($eventsData as $eventData) {
76 $eventMetas = Arr::get($eventData, 'event_metas', []);
77
78 $eventData = self::prepareEventData($eventData, $calendar, $availabilities);
79
80 $createEventData = wp_parse_args($eventData, $defaultEventData);
81
82 $createEventData['slug'] = Helper::generateSlotSlug((int)$createEventData['duration'] . 'min', $calendar);
83
84 $createEventData['settings'] = wp_parse_args($createEventData['settings'], $defaultEventData['settings']);
85
86 $createEventsData[] = Arr::only($createEventData, (new CalendarSlot())->getFillable());
87
88 $createEventMetasData[] = $eventMetas;
89 }
90
91 $createdEvents = $calendar->events()->createMany($createEventsData);
92
93 foreach ($createdEvents as $index => $event) {
94 $eventMetasData = Arr::get($createEventMetasData, $index, []);
95
96 $eventMetasData = self::prepareEventMetas($eventMetasData, $event);
97
98 $event->event_metas()->createMany($eventMetasData);
99 }
100
101 return $createdEvents;
102 }
103
104 protected static function prepareCalendarData($calendarData, $useCurrentUser = false, $isFileInput = false)
105 {
106 if (!$calendarData) {
107 return new \WP_Error('invalid_data', esc_html__('Invalid JSON Data', 'fluent-booking'));
108 }
109
110 $preparedData = [
111 'title' => sanitize_text_field(Arr::get($calendarData, 'title')),
112 'description' => sanitize_textarea_field(Arr::get($calendarData, 'description')),
113 'user_id' => intval(Arr::get($calendarData, 'user_id')),
114 'status' => sanitize_text_field(Arr::get($calendarData, 'status', 'active')),
115 'type' => sanitize_text_field(Arr::get($calendarData, 'type', 'simple')),
116 'event_type' => sanitize_text_field(Arr::get($calendarData, 'event_type')),
117 'account_type' => sanitize_text_field(Arr::get($calendarData, 'account_type')),
118 'visibility' => sanitize_text_field(Arr::get($calendarData, 'visibility')),
119 'author_timezone' => sanitize_text_field(Arr::get($calendarData, 'author_timezone')),
120 ];
121
122 if ($useCurrentUser || !Arr::get($preparedData, 'user_id')) {
123 $preparedData['user_id'] = get_current_user_id();
124 }
125
126 if (!Arr::get($preparedData, 'author_timezone')) {
127 $preparedData['author_timezone'] = wp_timezone_string();
128 }
129
130 if (!in_array(Arr::get($preparedData, 'type'), ['simple', 'team', 'event'])) {
131 $preparedData['type'] = 'simple';
132 }
133
134 $user = get_user_by('ID', $preparedData['user_id']);
135
136 if (!$user) {
137 return new \WP_Error('invalid_user', esc_html__('Invalid User ID', 'fluent-booking'));
138 }
139
140 $isHostCalendar = $preparedData['type'] == 'simple' ? true : false;
141
142 if ($isHostCalendar || !$preparedData['title']) {
143 $preparedData['title'] = is_email($user->user_login) ? explode('@', $user->user_login)[0] : $user->user_login;
144 }
145
146 $firstCalendar = Calendar::where('user_id', $preparedData['user_id'])->where('type', 'simple')->first();
147
148 if ($isHostCalendar && $firstCalendar) {
149 if ($isFileInput) {
150 return new \WP_Error('calendar_exists', esc_html__('The user already has a calendar. Please delete it first to create a new one', 'fluent-booking'));
151 }
152 return $firstCalendar;
153 }
154
155 return $preparedData;
156 }
157
158 protected static function prepareCalendarMetas($calendarMetas)
159 {
160 $preparedCalendarMetas = [];
161
162 foreach ($calendarMetas as $calendarMeta) {
163 if (empty($calendarMeta['key']) || empty($calendarMeta['value'])) {
164 continue;
165 }
166
167 $value = $calendarMeta['value'];
168
169 $preparedCalendarMetas[] = [
170 'key' => sanitize_text_field($calendarMeta['key']),
171 'value' => is_array($value) ? self::sanitize_mapped_data($value) : sanitize_text_field($value),
172 // Bound to the imported calendar; never let the payload pick the type
173 // (e.g. a user_meta / _access_permissions row).
174 'object_type' => 'Calendar'
175 ];
176 }
177
178 return $preparedCalendarMetas;
179 }
180
181 protected static function prepareEventData($eventData, $calendar, $availabilities = [])
182 {
183 $preparedEventData = [
184 'title' => sanitize_text_field(Arr::get($eventData, 'title')),
185 'duration' => (int)Arr::get($eventData, 'duration', 30),
186 'description' => wp_kses_post(Arr::get($eventData, 'description')),
187 'type' => sanitize_text_field(Arr::get($eventData, 'type')),
188 'status' => sanitize_text_field(Arr::get($eventData, 'status', 'active')),
189 'color_schema' => sanitize_text_field(Arr::get($eventData, 'color_schema', '#0099ff')),
190 'event_type' => sanitize_text_field(Arr::get($eventData, 'event_type')),
191 'availability_id' => (int)self::prepareAvailabilityId(Arr::get($eventData, 'availability_id', 0), $availabilities),
192 'availability_type' => sanitize_text_field(Arr::get($eventData, 'availability_type')),
193 'location_type' => sanitize_text_field(Arr::get($eventData, 'location_type')),
194 'location_settings' => SanitizeService::locationSettings(Arr::get($eventData, 'location_settings', [])),
195 'max_book_per_slot' => (int)Arr::get($eventData, 'max_book_per_slot', 1),
196 'is_display_spots' => (bool)Arr::get($eventData, 'is_display_spots', false),
197 ];
198
199 if (!empty($eventData['hash'])) {
200 $hash = sanitize_text_field($eventData['hash']);
201 if (!CalendarSlot::where('hash', $hash)->exists()) {
202 $preparedEventData['hash'] = $hash;
203 }
204 }
205
206 $eventSettings = Arr::get($eventData, 'settings', []);
207
208 if (!$eventSettings) {
209 return $preparedEventData;
210 }
211
212 $preparedEventData['settings'] = [
213 'schedule_type' => sanitize_text_field(Arr::get($eventSettings, 'schedule_type')),
214 'weekly_schedules' => SanitizeService::weeklySchedules(Arr::get($eventSettings, 'weekly_schedules', []), $calendar->author_timezone, 'UTC', true),
215 'date_overrides' => SanitizeService::slotDateOverrides(Arr::get($eventSettings, 'date_overrides', []), $calendar->author_timezone, 'UTC', null, true),
216 'range_type' => sanitize_text_field(Arr::get($eventSettings, 'range_type')),
217 'range_days' => (int)(Arr::get($eventSettings, 'range_days', 60)) ?: 60,
218 'range_date_between' => SanitizeService::rangeDateBetween(Arr::get($eventSettings, 'range_date_between', ['', ''])),
219 'schedule_conditions' => SanitizeService::scheduleConditions(Arr::get($eventSettings, 'schedule_conditions', [])),
220 'common_schedule' => Arr::isTrue($eventSettings, 'common_schedule', false),
221 'buffer_time_before' => sanitize_text_field(Arr::get($eventSettings, 'buffer_time_before', '0')),
222 'buffer_time_after' => sanitize_text_field(Arr::get($eventSettings, 'buffer_time_after', '0')),
223 'slot_interval' => sanitize_text_field(Arr::get($eventSettings, 'slot_interval', '')),
224 'team_members' => array_map('intval', Arr::get($eventSettings, 'team_members', [])),
225 'multi_duration' => [
226 'enabled' => Arr::isTrue($eventSettings, 'multi_duration.enabled'),
227 'default_duration' => Arr::get($eventSettings, 'multi_duration.default_duration', ''),
228 'available_durations' => array_map('sanitize_text_field', Arr::get($eventSettings, 'multi_duration.available_durations', []))
229 ],
230 'booking_frequency' => [
231 'enabled' => Arr::isTrue($eventSettings, 'booking_frequency.enabled'),
232 'limits' => self::sanitize_mapped_data(Arr::get($eventSettings, 'booking_frequency.limits', []))
233 ],
234 'booking_duration' => [
235 'enabled' => Arr::isTrue($eventSettings, 'booking_duration.enabled'),
236 'limits' => self::sanitize_mapped_data(Arr::get($eventSettings, 'booking_duration.limits', []))
237 ],
238 'lock_timezone' => [
239 'enabled' => Arr::isTrue($eventSettings, 'lock_timezone.enabled'),
240 'timezone' => sanitize_text_field(Arr::get($eventSettings, 'lock_timezone.timezone'))
241 ],
242 'booking_title' => sanitize_text_field(Arr::get($eventSettings, 'booking_title')),
243 'submit_button_text' => sanitize_text_field(Arr::get($eventSettings, 'submit_button_text')),
244 'custom_redirect' => [
245 'enabled' => Arr::isTrue($eventSettings, 'custom_redirect.enabled'),
246 'redirect_url' => sanitize_url(Arr::get($eventSettings, 'custom_redirect.redirect_url')),
247 'is_query_string' => Arr::get($eventSettings, 'custom_redirect.is_query_string') == 'yes' ? 'yes' : 'no',
248 'query_string' => sanitize_text_field(Arr::get($eventSettings, 'custom_redirect.query_string')),
249 ],
250 'requires_confirmation' => [
251 'enabled' => Arr::isTrue($eventSettings, 'requires_confirmation.enabled'),
252 'type' => sanitize_text_field(Arr::get($eventSettings, 'requires_confirmation.type')),
253 'condition' => [
254 'unit' => sanitize_text_field(Arr::get($eventSettings, 'requires_confirmation.condition.unit')),
255 'value' => intval(Arr::get($eventSettings, 'requires_confirmation.condition.value'))
256 ]
257 ],
258 'multiple_booking' => [
259 'enabled' => Arr::isTrue($eventSettings, 'multiple_booking.enabled'),
260 'limit' => intval(Arr::get($eventSettings, 'multiple_booking.limit'))
261 ],
262 'can_not_cancel' => [
263 'enabled' => Arr::isTrue($eventSettings, 'can_not_cancel.enabled'),
264 'type' => sanitize_text_field(Arr::get($eventSettings, 'can_not_cancel.type')),
265 'message' => sanitize_text_field(Arr::get($eventSettings, 'can_not_cancel.message')),
266 'condition' => [
267 'unit' => sanitize_text_field(Arr::get($eventSettings, 'can_not_cancel.condition.unit')),
268 'value' => intval(Arr::get($eventSettings, 'can_not_cancel.condition.value'))
269 ]
270 ],
271 'can_not_reschedule' => [
272 'enabled' => Arr::isTrue($eventSettings, 'can_not_reschedule.enabled'),
273 'type' => sanitize_text_field(Arr::get($eventSettings, 'can_not_reschedule.type')),
274 'message' => sanitize_text_field(Arr::get($eventSettings, 'can_not_reschedule.message')),
275 'condition' => [
276 'unit' => sanitize_text_field(Arr::get($eventSettings, 'can_not_reschedule.condition.unit')),
277 'value' => intval(Arr::get($eventSettings, 'can_not_reschedule.condition.value'))
278 ]
279 ]
280 ];
281
282 return $preparedEventData;
283 }
284
285 protected static function prepareEventMetas($eventMetasData, $event = null)
286 {
287 $preparedEventMetas = [];
288
289 foreach ($eventMetasData as $eventMeta) {
290 if (empty($eventMeta['key']) || empty($eventMeta['value'])) {
291 continue;
292 }
293
294 $value = $eventMeta['value'];
295
296 // Only the two types an event actually stores; anything else in the
297 // payload (e.g. user_meta / _access_permissions) is dropped.
298 $objectType = sanitize_text_field(Arr::get($eventMeta, 'object_type'));
299 if (!in_array($objectType, ['calendar_event', 'integration'], true)) {
300 continue;
301 }
302
303 if ($eventMeta['key'] == 'email_notification') {
304 $value = self::updateNotificationImageUrl($value);
305 }
306
307 // Rendered on the public form via {@html}; sanitize as the admin path does.
308 if ($eventMeta['key'] == 'booking_fields' && is_array($value)) {
309 $value = BookingFieldService::sanitizeBookingFields($value, $event);
310 }
311
312 $preparedEventMetas[] = [
313 'key' => sanitize_text_field($eventMeta['key']),
314 'value' => is_array($value) ? self::sanitize_mapped_data($value) : sanitize_text_field($value),
315 'object_type' => $objectType
316 ];
317 }
318
319 return $preparedEventMetas;
320 }
321
322 protected static function prepareAvailabilityId($availabilityId, $availabilities)
323 {
324 if ($availabilityId && isset($availabilities[$availabilityId])) {
325 return $availabilities[$availabilityId];
326 }
327
328 if ($availabilities) {
329 $firstKey = array_key_first($availabilities);
330 return $availabilities[$firstKey];
331 }
332
333 return $availabilityId;
334 }
335
336 protected static function updateNotificationImageUrl($notifications)
337 {
338 $formattedNotifications = [];
339
340 foreach ($notifications as $key => $notification) {
341 $emailBody = Arr::get($notification, 'email.body');
342 if (!$emailBody) {
343 continue;
344 }
345
346 $newImageUrl = FLUENT_BOOKING_URL . 'assets/images';
347 $pattern = '/(https:\/\/[^"]*?' . preg_quote('assets/images', '/') . ')/';
348 $emailBody = preg_replace($pattern, $newImageUrl, $emailBody);
349 $notification['email']['body'] = $emailBody;
350
351 $formattedNotifications[$key] = $notification;
352 }
353
354 return $formattedNotifications;
355 }
356
357 public static function getSlotOptions($calendarId = null, $userId = null)
358 {
359 $calendarSlots = CalendarSlot::select(['id', 'title'])
360 ->when($calendarId, function ($query) use ($calendarId) {
361 return $query->where('calendar_id', $calendarId);
362 })
363 ->when($userId, function ($query) use ($userId) {
364 return $query->where('user_id', $userId);
365 })
366 ->where('status', '!=', 'expired')
367 ->latest()
368 ->get();
369
370 $options = [];
371 foreach ($calendarSlots as $slot) {
372 $options[] = [
373 'id' => $slot->id,
374 'label' => $slot->title,
375 ];
376 }
377 return apply_filters('fluent_booking/calendar_event_options', $options, $calendarId);
378 }
379
380 public static function getCalendarOptionsByHost()
381 {
382 $calendars = Calendar::select(['id', 'title'])
383 ->when(!PermissionManager::hasAllCalendarAccess(true), function ($query) {
384 return $query->where('user_id', get_current_user_id());
385 })
386 ->with(['slots' => function ($query) {
387 $query->where('status', '!=', 'expired');
388 }])
389 ->latest()
390 ->get();
391
392 $formattedCalendars = [];
393 foreach ($calendars as $index => $calendar) {
394 $slots = Arr::get($calendar, 'slots');
395 if (!empty($slots)) {
396 $options = [];
397 foreach ($slots as $slot) {
398 $options[] = [
399 'label' => Arr::get($slot, 'title'),
400 'value' => Arr::get($slot, 'id')
401 ];
402 }
403 if (!empty($options)) {
404 $formattedCalendars[$index] = [
405 'label' => Arr::get($calendar, 'title'),
406 'options' => $options
407 ];
408 }
409 }
410 }
411 return $formattedCalendars;
412 }
413
414 public static function getCalendarOptionsByTitle($condition = '')
415 {
416 $calendarsQuery = Calendar::select(['id', 'title', 'user_id'])
417 ->where('status', '!=', 'expired')
418 ->with(['slots' => function ($query) {
419 $query->where('status', '!=', 'expired');
420 }]);
421
422 switch ($condition) {
423 case 'only_hosts':
424 $calendarsQuery->where('type', 'simple');
425 break;
426 case 'only_teams':
427 $calendarsQuery->where('type', 'team');
428 break;
429 case 'only_events':
430 $calendarsQuery->where('type', 'event');
431 break;
432 }
433
434 if (!PermissionManager::hasAllCalendarAccess(true)) {
435 $attachedCalendarIds = self::getAttachedCalendarIds($calendarsQuery);
436 $calendarsQuery->whereIn('id', $attachedCalendarIds);
437 }
438
439 $calendars = $calendarsQuery->latest()->get();
440
441 $formattedCalendars = [];
442 foreach ($calendars as $index => $calendar) {
443 $slots = Arr::get($calendar, 'slots');
444 if (!empty($slots)) {
445 $options = [];
446 foreach ($slots as $slot) {
447 $options[] = [
448 'id' => Arr::get($slot, 'id'),
449 'title' => Arr::get($slot, 'title')
450 ];
451 }
452 if (!empty($options)) {
453 $formattedCalendars[$index] = [
454 'id' => Arr::get($calendar, 'id'),
455 'title' => Arr::get($calendar, 'title'),
456 'options' => $options
457 ];
458 }
459 }
460 }
461 return apply_filters('fluent_booking/calendar_options_by_title', $formattedCalendars);
462 }
463
464 public static function getAttachedCalendarIds($calendarsQuery)
465 {
466 $userId = get_current_user_id();
467
468 $calendars = $calendarsQuery->get();
469
470 $calendarIds = [];
471 foreach ($calendars as $calendar) {
472 if ($calendar->user_id == $userId) {
473 $calendarIds[] = $calendar->id;
474 continue;
475 }
476
477 $events = Arr::get($calendar, 'slots', []);
478 foreach ($events as $event) {
479 $teamMembers = Arr::get($event, 'settings.team_members', []);
480 if (in_array($userId, $teamMembers)) {
481 $calendarIds[] = $calendar->id;
482 }
483 }
484 }
485
486 return $calendarIds;
487 }
488
489 public static function isSharedCalendar($calendar)
490 {
491 $calendarEvents = $calendar->events;
492
493 $userId = get_current_user_id();
494
495 foreach ($calendarEvents as $event) {
496 if ($event->user_id == $userId) {
497 return true;
498 }
499 $teamMembers = Arr::get($event, 'settings.team_members', []);
500 if (in_array($userId, $teamMembers)) {
501 return true;
502 }
503 }
504
505 return false;
506 }
507
508 public static function updateCalendarEventsSchedule($calendarId, $oldTimezone, $updatedTimezone)
509 {
510 $calendarEvents = CalendarSlot::query()->where('calendar_id', $calendarId)->get();
511
512 foreach ($calendarEvents as $event) {
513 if ($weeklySchedule = Arr::get($event->settings, 'weekly_schedules', [])) {
514 $originalSchedule = SanitizeService::weeklySchedules($weeklySchedule, 'UTC', $oldTimezone);
515 $weeklySchedule = SanitizeService::weeklySchedules($originalSchedule, $updatedTimezone, 'UTC', true);
516 }
517
518 if ($dateOverride = Arr::get($event->settings, 'date_overrides', [])) {
519 $originalOverride = SanitizeService::slotDateOverrides($dateOverride, 'UTC', $oldTimezone);
520 $dateOverride = SanitizeService::slotDateOverrides($originalOverride, $updatedTimezone, 'UTC', null, true);
521 }
522
523 $event->settings = [
524 'weekly_schedules' => $weeklySchedule,
525 'date_overrides' => $dateOverride
526 ];
527
528 $event->save();
529 }
530 }
531
532 private static function sanitize_mapped_data($settings)
533 {
534 $sanitizerMap = [
535 'value' => 'intval',
536 'unit' => 'sanitize_text_field',
537 'subject' => 'sanitize_text_field',
538 'body' => 'fcal_sanitize_html',
539 'additional_recipients' => 'sanitize_text_field'
540 ];
541
542 return Helper::fcal_backend_sanitizer($settings, $sanitizerMap);
543 }
544 }
545