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 +321 -123 1.6.0 → 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;
@@ -369,39 +471,25 @@
369 471 {
370 472 $details = $this->location_details;
371 473 $locationType = Arr::get($details, 'type');
372 474
373 - if (!$locationType) {
374 - return '--';
375 - }
376 -
377 - if ($locationType == 'in_person_guest') {
378 - return '<b>' . __('Invitee Address:', 'fluent-booking') . ' </b>' . Arr::get($details, 'description');
379 - }
380 -
381 - if ($locationType == 'in_person_organizer') {
382 - $html = '<b>' . Arr::get($details, 'title') . ' </b>';
383 - if ($description = Arr::get($details, 'description')) {
384 - $html .= wpautop($description);
475 + $html = '';
476 + if ($locationType === 'in_person_guest') {
477 + $html = '<b>' . esc_html(__('Invitee Address:', 'fluent-booking')) . ' </b>' . esc_html(Arr::get($details, 'description'));
478 + } else if ($locationType === 'in_person_organizer') {
479 + $html = '<b>' . esc_html(Arr::get($details, 'title')) . ' </b>';
480 + $description = Arr::get($details, 'description');
481 + if ($description) {
482 + $html .= wpautop(wp_kses_post($description));
385 483 }
386 - return $html;
387 - }
388 -
389 - if ($locationType == 'phone_guest') {
390 - return '<b>' . __('Phone Call:', 'fluent-booking') . ' </b>' . $this->phone;
391 - }
392 -
393 - if ($locationType == 'phone_organizer') {
394 - return '<b>' . __('Phone Call:', 'fluent-booking') . ' </b>' . Arr::get($details, 'description') . __(' (Host phone number)', 'fluent-booking');
395 - }
396 -
397 - if ($locationType == 'custom') {
398 - $html = '<b>' . Arr::get($details, 'title') . '</b>';
399 - $html .= wpautop(Arr::get($details, 'description'));
400 - return $html;
401 - }
402 -
403 - if (in_array($locationType, ['google_meet', 'online_meeting', 'zoom_meeting', 'ms_teams'])) {
484 + } else if ($locationType === 'phone_guest') {
485 + $html = '<b>' . esc_html(__('Phone Call:', 'fluent-booking')) . ' </b>' . esc_html($this->phone);
486 + } else if ($locationType === 'phone_organizer') {
487 + $html = '<b>' . __('Phone Call:', 'fluent-booking') . ' </b>' . esc_html(Arr::get($details, 'description')) . esc_html(__(' (Host phone number)', 'fluent-booking'));
488 + } else if ($locationType === 'custom') {
489 + $html = '<b>' . esc_html(Arr::get($details, 'title')) . '</b>';
490 + $html .= wpautop(wp_kses_post(Arr::get($details, 'description')));
491 + } else if (in_array($locationType, ['google_meet', 'online_meeting', 'zoom_meeting', 'ms_teams'])) {
404 492 $platformLabels = [
405 493 'google_meet' => __('Google Meet', 'fluent-booking'),
406 494 'online_meeting' => __('Online Meeting', 'fluent-booking'),
407 495 'zoom_meeting' => __('Zoom Video', 'fluent-booking'),
@@ -407,18 +495,18 @@
407 495 'zoom_meeting' => __('Zoom Video', 'fluent-booking'),
408 496 'ms_teams' => __('MS Teams', 'fluent-booking'),
409 497 ];
410 498
411 - $html = '<b>' . $platformLabels[$locationType] . '</b> ';
412 -
413 - if ($meetingLink = Arr::get($details, 'online_platform_link')) {
414 - $html .= '<a target="_blank" href="' . esc_url($meetingLink) . '">' . __('Join Meeting', 'fluent-booking') . '</a>';
499 + $html = '<b>' . esc_html($platformLabels[$locationType]) . '</b> ';
500 + $meetingLink = Arr::get($details, 'online_platform_link');
501 + if ($meetingLink) {
502 + $html .= '<a target="_blank" href="' . esc_url($meetingLink) . '">' . esc_html(__('Join Meeting', 'fluent-booking')) . '</a>';
415 503 }
416 -
417 - return $html;
504 + } else {
505 + return '--';
418 506 }
419 507
420 - return '--';
508 + return apply_filters('fluent_booking/location_details_html', $html, $details);
421 509 }
422 510
423 511 public function getLocationAsText()
424 512 {
@@ -440,9 +528,11 @@
440 528 if ($locationType == 'phone_guest') {
441 529 return $this->phone;
442 530 }
443 531
444 - return wp_strip_all_tags($this->getLocationDetailsHtml());
532 + $text = wp_strip_all_tags($this->getLocationDetailsHtml());
533 +
534 + return apply_filters('fluent_booking/location_details_text', $text, $details);
445 535 }
446 536
447 537 public function getMessage()
448 538 {
@@ -458,11 +548,34 @@
458 548 }
459 549
460 550 public function getLocationDetailsAttribute($locationDetails)
461 551 {
462 - return \maybe_unserialize($locationDetails);
552 + $value = \maybe_unserialize($locationDetails);
553 +
554 + return is_array($value) ? $value : [];
463 555 }
464 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 +
465 578 public function getOngoingStatus()
466 579 {
467 580 if ($this->status != 'scheduled') {
468 581 return [];
@@ -528,11 +641,15 @@
528 641 }
529 642
530 643 public function getCancelReason($isText = false, $isHtml = false)
531 644 {
532 - $row = BookingActivity::where('booking_id', $this->id)
533 - ->where('type', 'cancel_reason')
534 - ->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 + }
535 652
536 653 if ($row) {
537 654 if ($isText) {
538 655 return $row->description;
@@ -537,9 +654,9 @@
537 654 if ($isText) {
538 655 return $row->description;
539 656 }
540 657 if ($isHtml) {
541 - return wp_unslash($row->description);
658 + return esc_html(wp_unslash($row->description));
542 659 }
543 660 }
544 661
545 662 return $row;
@@ -555,9 +672,9 @@
555 672 if ($isText) {
556 673 return $row->description;
557 674 }
558 675 if ($isHtml) {
559 - return wp_unslash($row->description);
676 + return esc_html(wp_unslash($row->description));
560 677 }
561 678 }
562 679
563 680 return $row;
@@ -663,11 +780,17 @@
663 780
664 781 do_action('fluent_booking/booking_schedule_rejected', $this, $this->calendar_event);
665 782 }
666 783
667 - public function getRescheduleReason()
784 + public function getRescheduleReason($html = false)
668 785 {
669 - 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;
670 793 }
671 794
672 795 private function generateBookingTitle($eventTitle, $authorName, $guestName)
673 796 {
@@ -692,10 +815,15 @@
692 815 $bookingTitle = EditorShortCodeParser::parse($bookingTitle, $this);
693 816
694 817 $bookingTitle = $bookingTitle ?: $this->generateBookingTitle($eventTitle, $authorName, $guestName);
695 818
696 - if ($html && strpos($bookingTitle, $eventTitle) !== false) {
697 - $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 + }
698 826 }
699 827
700 828 return apply_filters('fluent_booking/booking_meeting_title', $bookingTitle, $authorName, $guestName, $calendarEvent, $this);
701 829 }
@@ -734,11 +862,15 @@
734 862 }
735 863
736 864 public function getMeta($key, $default = '')
737 865 {
738 - $exist = BookingMeta::where('booking_id', $this->id)
739 - ->where('meta_key', $key)
740 - ->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 + }
741 873
742 874 if ($exist) {
743 875 return $exist->value;
744 876 }
@@ -753,17 +885,23 @@
753 885 */
754 886 public function scopeSearchBy($query, $search)
755 887 {
756 888 if ($search) {
889 + $escape = function ($value) {
890 + return addcslashes((string) $value, '%_\\');
891 + };
892 +
757 893 $fields = $this->searchable;
758 - $query->where(function ($query) use ($fields, $search) {
759 - $query->where(array_shift($fields), 'LIKE', "%$search%");
894 + $searchEsc = $escape($search);
760 895
896 + $query->where(function ($query) use ($fields, $search, $searchEsc, $escape) {
897 + $query->where(array_shift($fields), 'LIKE', "%$searchEsc%");
898 +
761 899 $nameArray = explode(' ', $search);
762 900 if (count($nameArray) >= 2) {
763 - $query->orWhere(function ($q) use ($nameArray) {
764 - $fname = array_shift($nameArray);
765 - $lastName = implode(' ', $nameArray);
901 + $query->orWhere(function ($q) use ($nameArray, $escape) {
902 + $fname = $escape(array_shift($nameArray));
903 + $lastName = $escape(implode(' ', $nameArray));
766 904 $q->where('first_name', 'LIKE', "%$fname%")
767 905 ->orWhere('last_name', 'LIKE', "%$lastName%");
768 906 });
769 907 }
@@ -768,9 +906,9 @@
768 906 });
769 907 }
770 908
771 909 foreach ($fields as $field) {
772 - $query->orWhere($field, 'LIKE', "%$search%");
910 + $query->orWhere($field, 'LIKE', "%$searchEsc%");
773 911 }
774 912 });
775 913 }
776 914
@@ -864,13 +1002,25 @@
864 1002 'type' => 'cancel',
865 1003 ], Helper::getBookingReceiptLandingBaseUrl());
866 1004 }
867 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 + */
868 1010 public function hasBookingAccess()
869 1011 {
870 - $hostIds = $this->getHostIds();
871 - $hasAccess = PermissionManager::userCan('manage_all_bookings');
872 - 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());
873 1023 }
874 1024
875 1025 private function canPerformAction($settings)
876 1026 {
@@ -891,8 +1041,10 @@
891 1041
892 1042 $conditionTime = $conditionValue * 60;
893 1043 if ($conditionUnit == 'hours') {
894 1044 $conditionTime = $conditionTime * 60;
1045 + } elseif ($conditionUnit == 'days') {
1046 + $conditionTime = $conditionTime * 60 * 24;
895 1047 }
896 1048
897 1049 return $bookingStartTime - $currentTime > $conditionTime;
898 1050 }
@@ -918,21 +1070,39 @@
918 1070 {
919 1071 return $this->event_type == 'group' || $this->event_type == 'group_event';
920 1072 }
921 1073
1074 + public function isRoundRobinBooking()
1075 + {
1076 + return $this->event_type == 'round_robin';
1077 + }
1078 +
922 1079 public function isMultiHostBooking()
923 1080 {
924 1081 return in_array($this->event_type, ['single_event', 'group_event', 'collective']);
925 1082 }
926 1083
1084 + public function isRecurringBooking()
1085 + {
1086 + return Arr::get($this->other_info, 'recurring_count', 0) > 1;
1087 + }
1088 +
927 1089 public function getHostProfiles($public = true)
928 1090 {
929 1091 $hostIds = $this->getHostIds();
930 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 +
931 1102 $hosts = [];
932 1103 foreach ($hostIds as $hostId) {
933 - $calendar = Calendar::where('user_id', $hostId)->where('type', 'simple')->first();
934 - if ($calendar) {
1104 + if ($calendar = $calendars->get($hostId)) {
935 1105 $hosts[] = $calendar->getAuthorProfile($public);
936 1106 }
937 1107 }
938 1108
@@ -964,9 +1134,9 @@
964 1134 if ($message) {
965 1135 return $message;
966 1136 }
967 1137
968 - return __('Sorry! you can not cancel this', 'fluent-booking');
1138 + return __('Sorry! you cannot cancel this', 'fluent-booking');
969 1139 }
970 1140
971 1141 public function getRescheduleMessage()
972 1142 {
@@ -977,9 +1147,9 @@
977 1147 if ($message) {
978 1148 return $message;
979 1149 }
980 1150
981 - return __('Sorry! you can not reschedule this', 'fluent-booking');
1151 + return __('Sorry! you cannot reschedule this', 'fluent-booking');
982 1152 }
983 1153
984 1154 public function getHostDetails($isPublic = true, $hostId = null)
985 1155 {
@@ -1030,15 +1200,30 @@
1030 1200 ->where('type', 'simple')
1031 1201 ->first();
1032 1202
1033 1203 if (!$calendar) {
1034 - return '';
1204 + return 'UTC';
1035 1205 }
1036 1206 return $calendar->author_timezone;
1037 1207 }
1038 - return '';
1208 + return 'UTC';
1039 1209 }
1040 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 +
1041 1226 public function getIcsBookingDescription()
1042 1227 {
1043 1228 $description = str_replace(PHP_EOL, '\\n', $this->getConfirmationData());
1044 1229
@@ -1082,9 +1267,9 @@
1082 1267 if (empty($data['value'])) {
1083 1268 continue;
1084 1269 }
1085 1270 $html .= '<tr>';
1086 - $html .= '<td><b>' . $data['label'] . '</b></td>';
1271 + $html .= '<td><b>' . esc_html($data['label']) . '</b></td>';
1087 1272 $html .= '<td>' . $data['value'] . '</td>';
1088 1273 $html .= '</tr>';
1089 1274 }
1090 1275 $html .= '</table>';
@@ -1091,9 +1276,9 @@
1091 1276
1092 1277 return $html;
1093 1278 }
1094 1279
1095 - public function getConfirmationData()
1280 + public function getConfirmationData($html = false)
1096 1281 {
1097 1282 $author = $this->getHostDetails(false);
1098 1283
1099 1284 $guestName = trim($this->first_name . ' ' . $this->last_name);
@@ -1098,9 +1283,11 @@
1098 1283
1099 1284 $guestName = trim($this->first_name . ' ' . $this->last_name);
1100 1285
1101 1286 $bookingTitle = $this->getBookingTitle();
1102 -
1287 +
1288 + $separator = $html ? '<br>' : PHP_EOL;
1289 +
1103 1290 $sections = [
1104 1291 'what' => [
1105 1292 'title' => __('What', 'fluent-booking'),
1106 1293 'content' => $bookingTitle,
@@ -1106,13 +1293,13 @@
1106 1293 'content' => $bookingTitle,
1107 1294 ],
1108 1295 'when' => [
1109 1296 'title' => __('When', 'fluent-booking'),
1110 - '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 . ')',
1111 1298 ],
1112 1299 'who' => [
1113 1300 'title' => __('Who', 'fluent-booking'),
1114 - '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
1115 1302 ],
1116 1303 'where' => [
1117 1304 'title' => __('Where', 'fluent-booking'),
1118 1305 'content' => $this->getLocationAsText()
@@ -1117,14 +1304,18 @@
1117 1304 'title' => __('Where', 'fluent-booking'),
1118 1305 'content' => $this->getLocationAsText()
1119 1306 ],
1120 1307 ];
1121 -
1122 - $lines = array_map(function ($section) {
1123 - 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']);
1124 1315 }, $sections);
1125 -
1126 - return implode(PHP_EOL . PHP_EOL, $lines) . PHP_EOL . PHP_EOL;
1316 +
1317 + return implode($separator . $separator, $lines) . $separator . $separator;
1127 1318 }
1128 1319
1129 1320 public function getMeetingBookmarks($assetsUrl = '')
1130 1321 {
@@ -1129,45 +1320,52 @@
1129 1320 public function getMeetingBookmarks($assetsUrl = '')
1130 1321 {
1131 1322 $bookingTitle = $this->getBookingTitle();
1132 1323
1133 - $eventTitle = $this->calendar_event->title;
1324 + $eventDescription = $this->getCalendarLinkDescription();
1134 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 +
1135 1354 return apply_filters('fluent_booking/meeting_bookmarks', [
1136 1355 'google' => [
1137 1356 'title' => __('Google Calendar', 'fluent-booking'),
1138 - 'url' => add_query_arg([
1139 - 'dates' => gmdate('Ymd\THis\Z', strtotime($this->start_time)) . '/' . gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1140 - 'text' => $bookingTitle,
1141 - 'details' => $eventTitle,
1142 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1143 - ], 'https://calendar.google.com/calendar/r/eventedit'),
1357 + 'url' => 'https://calendar.google.com/calendar/render?action=TEMPLATE&' . $googleParams,
1144 1358 'icon' => $assetsUrl . 'images/g-icon.svg'
1145 1359 ],
1146 1360 'outlook' => [
1147 1361 'title' => __('Outlook', 'fluent-booking'),
1148 - 'url' => add_query_arg([
1149 - 'startdt' => gmdate('Ymd\THis\Z', strtotime($this->start_time)),
1150 - 'enddt' => gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1151 - 'subject' => $bookingTitle,
1152 - 'path' => '/calendar/action/compose',
1153 - 'body' => $eventTitle,
1154 - 'rru' => 'addevent',
1155 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1156 - ], 'https://outlook.live.com/calendar/0/deeplink/compose'),
1362 + 'url' => 'https://outlook.live.com/calendar/0/deeplink/compose?' . $outlookParams,
1157 1363 'icon' => $assetsUrl . 'images/ol-icon.svg'
1158 1364 ],
1159 1365 'msoffice' => [
1160 1366 'title' => __('Microsoft Office', 'fluent-booking'),
1161 - 'url' => add_query_arg([
1162 - 'startdt' => gmdate('Ymd\THis\Z', strtotime($this->start_time)),
1163 - 'enddt' => gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1164 - 'subject' => $bookingTitle,
1165 - 'path' => '/calendar/action/compose',
1166 - 'body' => $eventTitle,
1167 - 'rru' => 'addevent',
1168 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1169 - ], 'https://outlook.office.com/calendar/0/deeplink/compose'),
1367 + 'url' => 'https://outlook.office.com/calendar/0/deeplink/compose?' . $outlookParams,
1170 1368 'icon' => $assetsUrl . 'images/msoffice.svg'
1171 1369 ],
1172 1370 'other' => [
1173 1371 'title' => __('Other Calendar', 'fluent-booking'),
@@ -1176,5 +1374,5 @@
1176 1374 ]
1177 1375 ], $this);
1178 1376 }
1179 1377
1180 -}
1378 +}