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/Models/Booking.php +299 -94 1.7.1 → 2.5.0 View file →
@@ -1,8 +1,9 @@
1 1 <?php
2 2
3 3 namespace FluentBooking\App\Models;
4 4
5 +use FluentBooking\App\App;
5 6 use FluentBooking\App\Models\Model;
6 7 use FluentBooking\App\Services\BookingFieldService;
7 8 use FluentBooking\App\Services\LocationService;
8 9 use FluentBooking\App\Services\DateTimeHelper;
@@ -54,12 +55,12 @@
54 55 'source_url',
55 56 'utm_source',
56 57 'utm_medium',
57 58 'utm_campaign',
58 - 'utm_term'
59 + 'utm_term',
60 + 'utm_content'
59 61 ];
60 62
61 -
62 63 /**
63 64 * $searchable Columns in table to search
64 65 * @var array
65 66 */
@@ -137,9 +138,11 @@
137 138 }
138 139
139 140 public static function assignNextGroupId()
140 141 {
141 - $lastEvent = static::orderBy('group_id', 'desc')->first(['group_id']);
142 + // Queue on one row the insert never touches; locking the max row alone deadlocks.
143 + App::getInstance('db')->table('options')->where('option_name', 'fcal_booking_group_lock')->lockForUpdate()->first();
144 + $lastEvent = static::orderBy('group_id', 'desc')->lockForUpdate()->first(['group_id']);
142 145
143 146 return $lastEvent ? $lastEvent->group_id + 1 : 1;
144 147 }
145 148
@@ -168,14 +171,24 @@
168 171 return [];
169 172 }
170 173
171 174 if ($isHtml) {
172 - return wpautop(implode('<br>', $additionalGuests));
175 + return wpautop(implode('<br>', array_map('esc_html', $additionalGuests)));
173 176 }
174 177
175 178 return $additionalGuests;
176 179 }
177 180
181 + public function getTotalGuestCount()
182 + {
183 + $additionalGuests = $this->getAdditionalGuests();
184 + $mainGuests = 1;
185 + if ($this->isMultiGuestBooking()) {
186 + $mainGuests = self::where('group_id', $this->group_id)->where('status', 'scheduled')->count();
187 + }
188 + return count($additionalGuests) + $mainGuests;
189 + }
190 +
178 191 public function getHostEmails($excludeHostId = null)
179 192 {
180 193 $hostIds = $this->getHostIds();
181 194
@@ -209,8 +222,29 @@
209 222 {
210 223 return $this->hosts()->pluck('user_id')->toArray();
211 224 }
212 225
226 + public function bookingHosts()
227 + {
228 + return $this->hasMany(BookingHost::class, 'booking_id');
229 + }
230 +
231 + /**
232 + * Limit to bookings the given user may access as a host: either they own
233 + * the booking's calendar, or they are a host on the booking (team events
234 + * such as round-robin/collective put non-owner hosts on fcal_booking_hosts).
235 + */
236 + public function scopeWhereHostAccess($query, $userId)
237 + {
238 + return $query->where(function ($q) use ($userId) {
239 + $q->whereHas('calendar', function ($c) use ($userId) {
240 + $c->where('user_id', $userId);
241 + })->orWhereHas('bookingHosts', function ($h) use ($userId) {
242 + $h->where('user_id', $userId);
243 + });
244 + });
245 + }
246 +
213 247 public function scopeUpcoming($query)
214 248 {
215 249 return $query->where('end_time', '>=', gmdate('Y-m-d H:i:s')); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
216 250 }
@@ -219,8 +253,24 @@
219 253 {
220 254 return $query->where('end_time', '<', gmdate('Y-m-d H:i:s')); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
221 255 }
222 256
257 + public function scopeApplyDateRangeFilter($query, $range)
258 + {
259 + if (empty($range['start_date']) || empty($range['end_date'])) {
260 + return $query;
261 + }
262 +
263 + if (!empty($range['time_zone']) && $range['time_zone'] != 'UTC') {
264 + if (in_array($range['time_zone'], timezone_identifiers_list(), true)) {
265 + $range['start_date'] = gmdate('Y-m-d H:i:s', strtotime($range['start_date'] . ' -1 day'));
266 + $range['end_date'] = gmdate('Y-m-d H:i:s', strtotime($range['end_date'] . ' +1 day'));
267 + }
268 + }
269 +
270 + return $query->whereBetween('start_time', [$range['start_date'], $range['end_date']]);
271 + }
272 +
223 273 public function scopeApplyComputedStatus($query, $status)
224 274 {
225 275 $validStatuses = [
226 276 'upcoming',
@@ -240,17 +290,19 @@
240 290 ->where('status', 'scheduled');
241 291 }
242 292
243 293 if ($status == 'completed') {
244 - return $query->where('end_time', '<', gmdate('Y-m-d H:i:s')) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
245 - ->where('status', '!=', 'cancelled')
246 - ->where('status', '!=', 'rejected')
247 - ->orWhere('status', 'completed'); // maybe cron did not mark few as completed yet
294 + return $query->where(function ($query) {
295 + $query->where(function ($query) {
296 + $query->where('end_time', '<', gmdate('Y-m-d H:i:s')) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
297 + ->where('status', '!=', 'cancelled')
298 + ->where('status', '!=', 'rejected');
299 + })->orWhere('status', 'completed'); // maybe cron did not mark few as completed yet
300 + });
248 301 }
249 302
250 303 if ($status == 'cancelled') {
251 - return $query->where('status', 'cancelled')
252 - ->orWhere('status', 'rejected');
304 + return $query->whereIn('status', ['cancelled', 'rejected']);
253 305 }
254 306
255 307 if ($status == 'pending') {
256 308 return $query->whereIn('status', ['pending', 'reserved']);
@@ -294,14 +346,27 @@
294 346
295 347 return $html;
296 348 }
297 349
298 - public function getShortBookingDateTime($timeZone = 'UTC')
350 + public function getPreviousMeetingDateTimeText($timeZone = 'UTC')
299 351 {
300 - // date format for Fri Feb 10, 2023
301 - $startDate = DateTimeHelper::convertFromUtc($this->start_time, $timeZone, 'D M d, Y');
302 - $startTime = DateTimeHelper::convertFromUtc($this->start_time, $timeZone, 'h:ia');
352 + $previousStartTime = $this->getMeta('previous_meeting_time');
353 + $previousEndTime = gmdate('Y-m-d H:i:s', strtotime($previousStartTime) + ($this->slot_minutes * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
303 354
355 + $startDateTime = DateTimeHelper::convertFromUtc($previousStartTime, $timeZone, 'Y-m-d H:i:s');
356 + $endDateTime = DateTimeHelper::convertFromUtc($previousEndTime, $timeZone, 'Y-m-d H:i:s');
357 +
358 + $text = DateTimeHelper::formatToLocale($startDateTime, 'time') . ' - ' . DateTimeHelper::formatToLocale($endDateTime, 'time') . ', ';
359 + $text .= DateTimeHelper::formatToLocale($startDateTime, 'date');
360 +
361 + return $text;
362 + }
363 +
364 + protected function formatBookingDateTime($dateTime, $timeZone = 'UTC')
365 + {
366 + $startDate = DateTimeHelper::convertFromUtc($dateTime, $timeZone, 'D M d, Y');
367 + $startTime = DateTimeHelper::convertFromUtc($dateTime, $timeZone, 'h:ia');
368 +
304 369 $localDate = date_i18n('D M d, Y', strtotime($startDate));
305 370 $localTime = date_i18n('h:ia', strtotime($startTime));
306 371
307 372 return $localDate . ' ' . $localTime;
@@ -306,15 +371,17 @@
306 371
307 372 return $localDate . ' ' . $localTime;
308 373 }
309 374
375 + public function getShortBookingDateTime($timeZone = 'UTC')
376 + {
377 + return $this->formatBookingDateTime($this->start_time, $timeZone);
378 + }
379 +
310 380 public function getPreviousMeetingTime($timeZone = 'UTC')
311 381 {
312 382 $previousMeetingTime = $this->getMeta('previous_meeting_time');
313 - $html = DateTimeHelper::convertFromUtc($previousMeetingTime, $timeZone, 'D M d, Y');
314 - $html .= ' ' . DateTimeHelper::convertFromUtc($previousMeetingTime, $timeZone, 'h:ia');
315 -
316 - return $html;
383 + return $this->formatBookingDateTime($previousMeetingTime, $timeZone);
317 384 }
318 385
319 386 public function getAttendeeStartTime($format = 'Y-m-d H:i:s')
320 387 {
@@ -334,8 +401,43 @@
334 401 return $otherBooking->getFullBookingDateTimeText($this->person_time_zone, true) . ' (' . $this->person_time_zone . ')';
335 402 })->toArray();
336 403 }
337 404
405 + public function getAllBookingShortTimes($timeZone = 'UTC', $withTimeZone = false)
406 + {
407 + $otherBookings = $this->getOwnChildBookings();
408 +
409 + $otherTimes = $otherBookings->map(function ($otherBooking) use ($timeZone, $withTimeZone) {
410 + return $otherBooking->formatBookingDateTime($otherBooking->start_time, $timeZone) . ($withTimeZone ? ' (' . $timeZone . ')' : '');
411 + })->toArray();
412 +
413 + return array_merge($otherTimes, [
414 + $this->formatBookingDateTime($this->start_time, $timeZone) . ($withTimeZone ? ' (' . $timeZone . ')' : '')
415 + ]);
416 + }
417 +
418 + public function getAllBookingFullTimes($timeZone = 'UTC', $withTimeZone = false)
419 + {
420 + $otherBookings = $this->getOwnChildBookings();
421 +
422 + $otherTimes = $otherBookings->map(function ($otherBooking) use ($timeZone, $withTimeZone) {
423 + return $otherBooking->getFullBookingDateTimeText($timeZone) . ($withTimeZone ? ' (' . $timeZone . ')' : '');
424 + })->toArray();
425 +
426 + return array_merge($otherTimes, [
427 + $this->getFullBookingDateTimeText($timeZone) . ($withTimeZone ? ' (' . $timeZone . ')' : '')
428 + ]);
429 + }
430 +
431 + /**
432 + * Additional guests on a group booking share the parent link too, each
433 + * with their own email, so only this guest's other times are theirs.
434 + */
435 + private function getOwnChildBookings()
436 + {
437 + return self::where('parent_id', $this->id)->where('email', $this->email)->get();
438 + }
439 +
338 440 public function getHostAndGuestDetailsHtml()
339 441 {
340 442 $authors = $this->getHostsDetails();
341 443
@@ -340,9 +442,9 @@
340 442 $authors = $this->getHostsDetails();
341 443
342 444 $guestNames = (array) trim($this->first_name . ' ' . $this->last_name);
343 445
344 - if ($this->isMultiGuestBooking()) {
446 + if ($this->isMultiGuestBooking() && !$this->isRecurringBooking()) {
345 447 $otherGuests = self::where('parent_id', $this->id)->get()->map(function ($guest) {
346 448 return trim($guest->first_name . ' ' . $guest->last_name);
347 449 })->toArray();
348 450 $guestNames = array_merge($guestNames, $otherGuests);
@@ -353,13 +455,13 @@
353 455 $authorListHtml = '<ul class="fcal_listed">';
354 456
355 457 foreach ($authors as $author) {
356 458 $authorBadge = ($author['id'] == $hostUserId) ? '<span class="fcal_host_badge">' . __('Host', 'fluent-booking') . '</span>' : '';
357 - $authorListHtml .= '<li class="fcal_host_name">' . $author['name'] . $authorBadge . '</li>';
459 + $authorListHtml .= '<li class="fcal_host_name">' . esc_html($author['name']) . $authorBadge . '</li>';
358 460 }
359 461
360 462 foreach ($guestNames as $guestName) {
361 - $authorListHtml .= '<li class="fcal_guest_name">' . $guestName . '</li>';
463 + $authorListHtml .= '<li class="fcal_guest_name">' . esc_html($guestName) . '</li>';
362 464 }
363 465 $authorListHtml .= '</ul>';
364 466
365 467 return $authorListHtml;
@@ -371,22 +473,22 @@
371 473 $locationType = Arr::get($details, 'type');
372 474
373 475 $html = '';
374 476 if ($locationType === 'in_person_guest') {
375 - $html = '<b>' . __('Invitee Address:', 'fluent-booking') . ' </b>' . Arr::get($details, 'description');
477 + $html = '<b>' . esc_html(__('Invitee Address:', 'fluent-booking')) . ' </b>' . esc_html(Arr::get($details, 'description'));
376 478 } else if ($locationType === 'in_person_organizer') {
377 - $html = '<b>' . Arr::get($details, 'title') . ' </b>';
479 + $html = '<b>' . esc_html(Arr::get($details, 'title')) . ' </b>';
378 480 $description = Arr::get($details, 'description');
379 481 if ($description) {
380 - $html .= wpautop($description);
482 + $html .= wpautop(wp_kses_post($description));
381 483 }
382 484 } else if ($locationType === 'phone_guest') {
383 - $html = '<b>' . __('Phone Call:', 'fluent-booking') . ' </b>' . $this->phone;
485 + $html = '<b>' . esc_html(__('Phone Call:', 'fluent-booking')) . ' </b>' . esc_html($this->phone);
384 486 } else if ($locationType === 'phone_organizer') {
385 - $html = '<b>' . __('Phone Call:', 'fluent-booking') . ' </b>' . Arr::get($details, 'description') . __(' (Host phone number)', 'fluent-booking');
487 + $html = '<b>' . __('Phone Call:', 'fluent-booking') . ' </b>' . esc_html(Arr::get($details, 'description')) . esc_html(__(' (Host phone number)', 'fluent-booking'));
386 488 } else if ($locationType === 'custom') {
387 - $html = '<b>' . Arr::get($details, 'title') . '</b>';
388 - $html .= wpautop(Arr::get($details, 'description'));
489 + $html = '<b>' . esc_html(Arr::get($details, 'title')) . '</b>';
490 + $html .= wpautop(wp_kses_post(Arr::get($details, 'description')));
389 491 } else if (in_array($locationType, ['google_meet', 'online_meeting', 'zoom_meeting', 'ms_teams'])) {
390 492 $platformLabels = [
391 493 'google_meet' => __('Google Meet', 'fluent-booking'),
392 494 'online_meeting' => __('Online Meeting', 'fluent-booking'),
@@ -393,12 +495,12 @@
393 495 'zoom_meeting' => __('Zoom Video', 'fluent-booking'),
394 496 'ms_teams' => __('MS Teams', 'fluent-booking'),
395 497 ];
396 498
397 - $html = '<b>' . $platformLabels[$locationType] . '</b> ';
499 + $html = '<b>' . esc_html($platformLabels[$locationType]) . '</b> ';
398 500 $meetingLink = Arr::get($details, 'online_platform_link');
399 501 if ($meetingLink) {
400 - $html .= '<a target="_blank" href="' . esc_url($meetingLink) . '">' . __('Join Meeting', 'fluent-booking') . '</a>';
502 + $html .= '<a target="_blank" href="' . esc_url($meetingLink) . '">' . esc_html(__('Join Meeting', 'fluent-booking')) . '</a>';
401 503 }
402 504 } else {
403 505 return '--';
404 506 }
@@ -446,11 +548,34 @@
446 548 }
447 549
448 550 public function getLocationDetailsAttribute($locationDetails)
449 551 {
450 - return \maybe_unserialize($locationDetails);
552 + $value = \maybe_unserialize($locationDetails);
553 +
554 + return is_array($value) ? $value : [];
451 555 }
452 556
557 + public function setOtherInfoAttribute($otherInfo)
558 + {
559 + $originalOtherInfo = $this->getOriginal('other_info');
560 +
561 + $originalOtherInfo = \maybe_unserialize($originalOtherInfo);
562 + $originalOtherInfo = is_array($originalOtherInfo) ? $originalOtherInfo : [];
563 +
564 + $otherInfo = is_array($otherInfo) ? $otherInfo : (array) \maybe_unserialize($otherInfo);
565 +
566 + foreach ($otherInfo as $key => $value) {
567 + $originalOtherInfo[$key] = $value;
568 + }
569 +
570 + $this->attributes['other_info'] = \maybe_serialize($originalOtherInfo);
571 + }
572 +
573 + public function getOtherInfoAttribute($otherInfo)
574 + {
575 + return \maybe_unserialize($otherInfo);
576 + }
577 +
453 578 public function getOngoingStatus()
454 579 {
455 580 if ($this->status != 'scheduled') {
456 581 return [];
@@ -516,11 +641,15 @@
516 641 }
517 642
518 643 public function getCancelReason($isText = false, $isHtml = false)
519 644 {
520 - $row = BookingActivity::where('booking_id', $this->id)
521 - ->where('type', 'cancel_reason')
522 - ->first();
645 + if ($this->relationLoaded('booking_activities')) {
646 + $row = $this->booking_activities->firstWhere('type', 'cancel_reason');
647 + } else {
648 + $row = BookingActivity::where('booking_id', $this->id)
649 + ->where('type', 'cancel_reason')
650 + ->first();
651 + }
523 652
524 653 if ($row) {
525 654 if ($isText) {
526 655 return $row->description;
@@ -525,9 +654,9 @@
525 654 if ($isText) {
526 655 return $row->description;
527 656 }
528 657 if ($isHtml) {
529 - return wp_unslash($row->description);
658 + return esc_html(wp_unslash($row->description));
530 659 }
531 660 }
532 661
533 662 return $row;
@@ -543,9 +672,9 @@
543 672 if ($isText) {
544 673 return $row->description;
545 674 }
546 675 if ($isHtml) {
547 - return wp_unslash($row->description);
676 + return esc_html(wp_unslash($row->description));
548 677 }
549 678 }
550 679
551 680 return $row;
@@ -651,11 +780,17 @@
651 780
652 781 do_action('fluent_booking/booking_schedule_rejected', $this, $this->calendar_event);
653 782 }
654 783
655 - public function getRescheduleReason()
784 + public function getRescheduleReason($html = false)
656 785 {
657 - return $this->getMeta('reschedule_reason', '');
786 + $rescheduleReason = $this->getMeta('reschedule_reason', '');
787 +
788 + if ($rescheduleReason && $html) {
789 + return wp_unslash($rescheduleReason);
790 + }
791 +
792 + return $rescheduleReason;
658 793 }
659 794
660 795 private function generateBookingTitle($eventTitle, $authorName, $guestName)
661 796 {
@@ -680,10 +815,15 @@
680 815 $bookingTitle = EditorShortCodeParser::parse($bookingTitle, $this);
681 816
682 817 $bookingTitle = $bookingTitle ?: $this->generateBookingTitle($eventTitle, $authorName, $guestName);
683 818
684 - if ($html && strpos($bookingTitle, $eventTitle) !== false) {
685 - $bookingTitle = str_replace($eventTitle, "<strong>{$eventTitle}</strong>", $bookingTitle);
819 + if ($html) {
820 + $bookingTitle = esc_html($bookingTitle);
821 + $eventTitle = esc_html($eventTitle);
822 +
823 + if (strpos($bookingTitle, $eventTitle) !== false) {
824 + $bookingTitle = str_replace($eventTitle, "<strong>{$eventTitle}</strong>", $bookingTitle);
825 + }
686 826 }
687 827
688 828 return apply_filters('fluent_booking/booking_meeting_title', $bookingTitle, $authorName, $guestName, $calendarEvent, $this);
689 829 }
@@ -722,11 +862,15 @@
722 862 }
723 863
724 864 public function getMeta($key, $default = '')
725 865 {
726 - $exist = BookingMeta::where('booking_id', $this->id)
727 - ->where('meta_key', $key)
728 - ->first();
866 + if ($this->relationLoaded('booking_meta')) {
867 + $exist = $this->booking_meta->firstWhere('meta_key', $key);
868 + } else {
869 + $exist = BookingMeta::where('booking_id', $this->id)
870 + ->where('meta_key', $key)
871 + ->first();
872 + }
729 873
730 874 if ($exist) {
731 875 return $exist->value;
732 876 }
@@ -741,17 +885,23 @@
741 885 */
742 886 public function scopeSearchBy($query, $search)
743 887 {
744 888 if ($search) {
889 + $escape = function ($value) {
890 + return addcslashes((string) $value, '%_\\');
891 + };
892 +
745 893 $fields = $this->searchable;
746 - $query->where(function ($query) use ($fields, $search) {
747 - $query->where(array_shift($fields), 'LIKE', "%$search%");
894 + $searchEsc = $escape($search);
748 895
896 + $query->where(function ($query) use ($fields, $search, $searchEsc, $escape) {
897 + $query->where(array_shift($fields), 'LIKE', "%$searchEsc%");
898 +
749 899 $nameArray = explode(' ', $search);
750 900 if (count($nameArray) >= 2) {
751 - $query->orWhere(function ($q) use ($nameArray) {
752 - $fname = array_shift($nameArray);
753 - $lastName = implode(' ', $nameArray);
901 + $query->orWhere(function ($q) use ($nameArray, $escape) {
902 + $fname = $escape(array_shift($nameArray));
903 + $lastName = $escape(implode(' ', $nameArray));
754 904 $q->where('first_name', 'LIKE', "%$fname%")
755 905 ->orWhere('last_name', 'LIKE', "%$lastName%");
756 906 });
757 907 }
@@ -756,9 +906,9 @@
756 906 });
757 907 }
758 908
759 909 foreach ($fields as $field) {
760 - $query->orWhere($field, 'LIKE', "%$search%");
910 + $query->orWhere($field, 'LIKE', "%$searchEsc%");
761 911 }
762 912 });
763 913 }
764 914
@@ -852,13 +1002,25 @@
852 1002 'type' => 'cancel',
853 1003 ], Helper::getBookingReceiptLandingBaseUrl());
854 1004 }
855 1005
1006 + /**
1007 + * Whether the current user may access this booking: a host of the booking,
1008 + * or a manage_own_calendar user who hosts the booking's event.
1009 + */
856 1010 public function hasBookingAccess()
857 1011 {
858 - $hostIds = $this->getHostIds();
859 - $hasAccess = PermissionManager::userCan('manage_all_bookings');
860 - return in_array(get_current_user_id(), $hostIds) || $hasAccess;
1012 + $userId = get_current_user_id();
1013 +
1014 + if (in_array($userId, $this->getHostIds())) {
1015 + return true;
1016 + }
1017 +
1018 + if (!PermissionManager::userCan('manage_own_calendar')) {
1019 + return false;
1020 + }
1021 +
1022 + return $this->calendar_event && in_array($userId, $this->calendar_event->getHostIds());
861 1023 }
862 1024
863 1025 private function canPerformAction($settings)
864 1026 {
@@ -879,8 +1041,10 @@
879 1041
880 1042 $conditionTime = $conditionValue * 60;
881 1043 if ($conditionUnit == 'hours') {
882 1044 $conditionTime = $conditionTime * 60;
1045 + } elseif ($conditionUnit == 'days') {
1046 + $conditionTime = $conditionTime * 60 * 24;
883 1047 }
884 1048
885 1049 return $bookingStartTime - $currentTime > $conditionTime;
886 1050 }
@@ -916,16 +1080,29 @@
916 1080 {
917 1081 return in_array($this->event_type, ['single_event', 'group_event', 'collective']);
918 1082 }
919 1083
1084 + public function isRecurringBooking()
1085 + {
1086 + return Arr::get($this->other_info, 'recurring_count', 0) > 1;
1087 + }
1088 +
920 1089 public function getHostProfiles($public = true)
921 1090 {
922 1091 $hostIds = $this->getHostIds();
923 1092
1093 + cache_users($hostIds);
1094 +
1095 + $calendars = Calendar::whereIn('user_id', $hostIds)
1096 + ->where('type', 'simple')
1097 + ->orderBy('id', 'desc')
1098 + ->with(['metas', 'user', 'user.metas'])
1099 + ->get()
1100 + ->keyBy('user_id');
1101 +
924 1102 $hosts = [];
925 1103 foreach ($hostIds as $hostId) {
926 - $calendar = Calendar::where('user_id', $hostId)->where('type', 'simple')->first();
927 - if ($calendar) {
1104 + if ($calendar = $calendars->get($hostId)) {
928 1105 $hosts[] = $calendar->getAuthorProfile($public);
929 1106 }
930 1107 }
931 1108
@@ -957,9 +1134,9 @@
957 1134 if ($message) {
958 1135 return $message;
959 1136 }
960 1137
961 - return __('Sorry! you can not cancel this', 'fluent-booking');
1138 + return __('Sorry! you cannot cancel this', 'fluent-booking');
962 1139 }
963 1140
964 1141 public function getRescheduleMessage()
965 1142 {
@@ -970,9 +1147,9 @@
970 1147 if ($message) {
971 1148 return $message;
972 1149 }
973 1150
974 - return __('Sorry! you can not reschedule this', 'fluent-booking');
1151 + return __('Sorry! you cannot reschedule this', 'fluent-booking');
975 1152 }
976 1153
977 1154 public function getHostDetails($isPublic = true, $hostId = null)
978 1155 {
@@ -1023,15 +1200,30 @@
1023 1200 ->where('type', 'simple')
1024 1201 ->first();
1025 1202
1026 1203 if (!$calendar) {
1027 - return '';
1204 + return 'UTC';
1028 1205 }
1029 1206 return $calendar->author_timezone;
1030 1207 }
1031 - return '';
1208 + return 'UTC';
1032 1209 }
1033 1210
1211 + public function getCalendarLinkDescription()
1212 + {
1213 + $description = str_replace(PHP_EOL, '<br>', $this->getConfirmationData());
1214 +
1215 + if ($this->message) {
1216 + $description .= __('Note: ', 'fluent-booking') . '<br>' . esc_html($this->message) . '<br><br>';
1217 + }
1218 +
1219 + if ($additionalData = $this->getAdditionalData(false)) {
1220 + $description .= '<br>' . str_replace(PHP_EOL, '<br>', $additionalData);
1221 + }
1222 +
1223 + return $description;
1224 + }
1225 +
1034 1226 public function getIcsBookingDescription()
1035 1227 {
1036 1228 $description = str_replace(PHP_EOL, '\\n', $this->getConfirmationData());
1037 1229
@@ -1075,9 +1267,9 @@
1075 1267 if (empty($data['value'])) {
1076 1268 continue;
1077 1269 }
1078 1270 $html .= '<tr>';
1079 - $html .= '<td><b>' . $data['label'] . '</b></td>';
1271 + $html .= '<td><b>' . esc_html($data['label']) . '</b></td>';
1080 1272 $html .= '<td>' . $data['value'] . '</td>';
1081 1273 $html .= '</tr>';
1082 1274 }
1083 1275 $html .= '</table>';
@@ -1084,9 +1276,9 @@
1084 1276
1085 1277 return $html;
1086 1278 }
1087 1279
1088 - public function getConfirmationData()
1280 + public function getConfirmationData($html = false)
1089 1281 {
1090 1282 $author = $this->getHostDetails(false);
1091 1283
1092 1284 $guestName = trim($this->first_name . ' ' . $this->last_name);
@@ -1091,9 +1283,11 @@
1091 1283
1092 1284 $guestName = trim($this->first_name . ' ' . $this->last_name);
1093 1285
1094 1286 $bookingTitle = $this->getBookingTitle();
1095 -
1287 +
1288 + $separator = $html ? '<br>' : PHP_EOL;
1289 +
1096 1290 $sections = [
1097 1291 'what' => [
1098 1292 'title' => __('What', 'fluent-booking'),
1099 1293 'content' => $bookingTitle,
@@ -1099,13 +1293,13 @@
1099 1293 'content' => $bookingTitle,
1100 1294 ],
1101 1295 'when' => [
1102 1296 'title' => __('When', 'fluent-booking'),
1103 - 'content' => $this->getFullBookingDateTimeText($this->person_time_zone, true) . ' (' . $this->person_time_zone . ')',
1297 + 'content' => $this->getFullBookingDateTimeText($this->person_time_zone, !$html) . ' (' . $this->person_time_zone . ')',
1104 1298 ],
1105 1299 'who' => [
1106 1300 'title' => __('Who', 'fluent-booking'),
1107 - 'content' => $author['name'] . ' - ' . __('Organizer', 'fluent-booking') . PHP_EOL . $author['email'] . PHP_EOL . PHP_EOL . $guestName . PHP_EOL . $this->email
1301 + 'content' => $author['name'] . ' - ' . __('Organizer', 'fluent-booking') . $separator . $author['email'] . $separator . $separator . $guestName . $separator . $this->email
1108 1302 ],
1109 1303 'where' => [
1110 1304 'title' => __('Where', 'fluent-booking'),
1111 1305 'content' => $this->getLocationAsText()
@@ -1110,14 +1304,18 @@
1110 1304 'title' => __('Where', 'fluent-booking'),
1111 1305 'content' => $this->getLocationAsText()
1112 1306 ],
1113 1307 ];
1114 -
1115 - $lines = array_map(function ($section) {
1116 - return $section['title'] . ': ' . PHP_EOL . esc_html($section['content']);
1308 +
1309 + if ($html) {
1310 + unset($sections['who']);
1311 + }
1312 +
1313 + $lines = array_map(function ($section) use ($separator) {
1314 + return $section['title'] . ': ' . $separator . esc_html($section['content']);
1117 1315 }, $sections);
1118 -
1119 - return implode(PHP_EOL . PHP_EOL, $lines) . PHP_EOL . PHP_EOL;
1316 +
1317 + return implode($separator . $separator, $lines) . $separator . $separator;
1120 1318 }
1121 1319
1122 1320 public function getMeetingBookmarks($assetsUrl = '')
1123 1321 {
@@ -1122,45 +1320,52 @@
1122 1320 public function getMeetingBookmarks($assetsUrl = '')
1123 1321 {
1124 1322 $bookingTitle = $this->getBookingTitle();
1125 1323
1126 - $eventTitle = $this->calendar_event->title;
1324 + $eventDescription = $this->getCalendarLinkDescription();
1127 1325
1326 + $eventLocation = LocationService::getBookingLocationUrl($this);
1327 +
1328 + $startTimestamp = strtotime($this->start_time);
1329 + $endTimestamp = strtotime($this->end_time);
1330 +
1331 + $compactStart = gmdate('Ymd\THis\Z', $startTimestamp);
1332 + $compactEnd = gmdate('Ymd\THis\Z', $endTimestamp);
1333 +
1334 + $isoStart = gmdate('Y-m-d\TH:i:s\Z', $startTimestamp);
1335 + $isoEnd = gmdate('Y-m-d\TH:i:s\Z', $endTimestamp);
1336 +
1337 + $googleParams = http_build_query([
1338 + 'dates' => $compactStart . '/' . $compactEnd,
1339 + 'text' => $bookingTitle,
1340 + 'details' => $eventDescription,
1341 + 'location' => $eventLocation,
1342 + ], '', '&', PHP_QUERY_RFC3986);
1343 +
1344 + $outlookParams = http_build_query([
1345 + 'path' => '/calendar/action/compose',
1346 + 'rru' => 'addevent',
1347 + 'startdt' => $isoStart,
1348 + 'enddt' => $isoEnd,
1349 + 'subject' => $bookingTitle,
1350 + 'body' => $eventDescription,
1351 + 'location' => $eventLocation,
1352 + ], '', '&', PHP_QUERY_RFC3986);
1353 +
1128 1354 return apply_filters('fluent_booking/meeting_bookmarks', [
1129 1355 'google' => [
1130 1356 'title' => __('Google Calendar', 'fluent-booking'),
1131 - 'url' => add_query_arg([
1132 - 'dates' => gmdate('Ymd\THis\Z', strtotime($this->start_time)) . '/' . gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1133 - 'text' => $bookingTitle,
1134 - 'details' => $eventTitle,
1135 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1136 - ], 'https://calendar.google.com/calendar/r/eventedit'),
1357 + 'url' => 'https://calendar.google.com/calendar/render?action=TEMPLATE&' . $googleParams,
1137 1358 'icon' => $assetsUrl . 'images/g-icon.svg'
1138 1359 ],
1139 1360 'outlook' => [
1140 1361 'title' => __('Outlook', 'fluent-booking'),
1141 - 'url' => add_query_arg([
1142 - 'startdt' => gmdate('Ymd\THis\Z', strtotime($this->start_time)),
1143 - 'enddt' => gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1144 - 'subject' => $bookingTitle,
1145 - 'path' => '/calendar/action/compose',
1146 - 'body' => $eventTitle,
1147 - 'rru' => 'addevent',
1148 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1149 - ], 'https://outlook.live.com/calendar/0/deeplink/compose'),
1362 + 'url' => 'https://outlook.live.com/calendar/0/deeplink/compose?' . $outlookParams,
1150 1363 'icon' => $assetsUrl . 'images/ol-icon.svg'
1151 1364 ],
1152 1365 'msoffice' => [
1153 1366 'title' => __('Microsoft Office', 'fluent-booking'),
1154 - 'url' => add_query_arg([
1155 - 'startdt' => gmdate('Ymd\THis\Z', strtotime($this->start_time)),
1156 - 'enddt' => gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1157 - 'subject' => $bookingTitle,
1158 - 'path' => '/calendar/action/compose',
1159 - 'body' => $eventTitle,
1160 - 'rru' => 'addevent',
1161 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1162 - ], 'https://outlook.office.com/calendar/0/deeplink/compose'),
1367 + 'url' => 'https://outlook.office.com/calendar/0/deeplink/compose?' . $outlookParams,
1163 1368 'icon' => $assetsUrl . 'images/msoffice.svg'
1164 1369 ],
1165 1370 'other' => [
1166 1371 'title' => __('Other Calendar', 'fluent-booking'),
@@ -1169,5 +1374,5 @@
1169 1374 ]
1170 1375 ], $this);
1171 1376 }
1172 1377
1173 -}
1378 +}