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 +347 -129 1.5.21 → 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,17 +138,19 @@
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
146 - public function getCustomFormData($isFormatted = true)
149 + public function getCustomFormData($isFormatted = true, $isPublic = false)
147 150 {
148 151 if ($isFormatted) {
149 - return BookingFieldService::getFormattedCustomBookingData($this);
152 + return BookingFieldService::getFormattedCustomBookingData($this, true, $isPublic);
150 153 }
151 154
152 155 return $this->getMeta('custom_fields_data', []);
153 156 }
@@ -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']);
@@ -262,8 +314,25 @@
262 314
263 315 return $query->where('status', $status);
264 316 }
265 317
318 + public function scopeApplyBookingOrderByStatus($query, $status)
319 + {
320 + if ($status == 'upcoming') {
321 + return $query->orderBy('start_time', 'ASC');
322 + }
323 +
324 + if ($status == 'latest_bookings') {
325 + return $query->orderBy('created_at', 'DESC');
326 + }
327 +
328 + if (in_array($status, ['completed', 'cancelled'])) {
329 + return $query->orderBy('updated_at', 'DESC');
330 + }
331 +
332 + return $query->orderBy('start_time', 'DESC');
333 + }
334 +
266 335 public function getFullBookingDateTimeText($timeZone = 'UTC', $isHtml = false)
267 336 {
268 337 $startDateTime = DateTimeHelper::convertFromUtc($this->start_time, $timeZone, 'Y-m-d H:i:s');
269 338 $endDateTime = DateTimeHelper::convertFromUtc($this->end_time, $timeZone, 'Y-m-d H:i:s');
@@ -277,24 +346,42 @@
277 346
278 347 return $html;
279 348 }
280 349
350 + public function getPreviousMeetingDateTimeText($timeZone = 'UTC')
351 + {
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
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 +
369 + $localDate = date_i18n('D M d, Y', strtotime($startDate));
370 + $localTime = date_i18n('h:ia', strtotime($startTime));
371 +
372 + return $localDate . ' ' . $localTime;
373 + }
374 +
281 375 public function getShortBookingDateTime($timeZone = 'UTC')
282 376 {
283 - // date format for Fri Feb 10, 2023
284 - $html = DateTimeHelper::convertFromUtc($this->start_time, $timeZone, 'D M d, Y');
285 - $html .= ' ' . DateTimeHelper::convertFromUtc($this->start_time, $timeZone, 'h:ia');
286 -
287 - return $html;
377 + return $this->formatBookingDateTime($this->start_time, $timeZone);
288 378 }
289 379
290 380 public function getPreviousMeetingTime($timeZone = 'UTC')
291 381 {
292 382 $previousMeetingTime = $this->getMeta('previous_meeting_time');
293 - $html = DateTimeHelper::convertFromUtc($previousMeetingTime, $timeZone, 'D M d, Y');
294 - $html .= ' ' . DateTimeHelper::convertFromUtc($previousMeetingTime, $timeZone, 'h:ia');
295 -
296 - return $html;
383 + return $this->formatBookingDateTime($previousMeetingTime, $timeZone);
297 384 }
298 385
299 386 public function getAttendeeStartTime($format = 'Y-m-d H:i:s')
300 387 {
@@ -314,8 +401,43 @@
314 401 return $otherBooking->getFullBookingDateTimeText($this->person_time_zone, true) . ' (' . $this->person_time_zone . ')';
315 402 })->toArray();
316 403 }
317 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 +
318 440 public function getHostAndGuestDetailsHtml()
319 441 {
320 442 $authors = $this->getHostsDetails();
321 443
@@ -320,9 +442,9 @@
320 442 $authors = $this->getHostsDetails();
321 443
322 444 $guestNames = (array) trim($this->first_name . ' ' . $this->last_name);
323 445
324 - if ($this->isMultiGuestBooking()) {
446 + if ($this->isMultiGuestBooking() && !$this->isRecurringBooking()) {
325 447 $otherGuests = self::where('parent_id', $this->id)->get()->map(function ($guest) {
326 448 return trim($guest->first_name . ' ' . $guest->last_name);
327 449 })->toArray();
328 450 $guestNames = array_merge($guestNames, $otherGuests);
@@ -333,13 +455,13 @@
333 455 $authorListHtml = '<ul class="fcal_listed">';
334 456
335 457 foreach ($authors as $author) {
336 458 $authorBadge = ($author['id'] == $hostUserId) ? '<span class="fcal_host_badge">' . __('Host', 'fluent-booking') . '</span>' : '';
337 - $authorListHtml .= '<li class="fcal_host_name">' . $author['name'] . $authorBadge . '</li>';
459 + $authorListHtml .= '<li class="fcal_host_name">' . esc_html($author['name']) . $authorBadge . '</li>';
338 460 }
339 461
340 462 foreach ($guestNames as $guestName) {
341 - $authorListHtml .= '<li class="fcal_guest_name">' . $guestName . '</li>';
463 + $authorListHtml .= '<li class="fcal_guest_name">' . esc_html($guestName) . '</li>';
342 464 }
343 465 $authorListHtml .= '</ul>';
344 466
345 467 return $authorListHtml;
@@ -349,39 +471,25 @@
349 471 {
350 472 $details = $this->location_details;
351 473 $locationType = Arr::get($details, 'type');
352 474
353 - if (!$locationType) {
354 - return '--';
355 - }
356 -
357 - if ($locationType == 'in_person_guest') {
358 - return '<b>' . __('Invitee Address:', 'fluent-booking') . ' </b>' . Arr::get($details, 'description');
359 - }
360 -
361 - if ($locationType == 'in_person_organizer') {
362 - $html = '<b>' . Arr::get($details, 'title') . ' </b>';
363 - if ($description = Arr::get($details, 'description')) {
364 - $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));
365 483 }
366 - return $html;
367 - }
368 -
369 - if ($locationType == 'phone_guest') {
370 - return '<b>' . __('Phone Call:', 'fluent-booking') . ' </b>' . $this->phone;
371 - }
372 -
373 - if ($locationType == 'phone_organizer') {
374 - return '<b>' . __('Phone Call:', 'fluent-booking') . ' </b>' . Arr::get($details, 'description') . __(' (Host phone number)', 'fluent-booking');
375 - }
376 -
377 - if ($locationType == 'custom') {
378 - $html = '<b>' . Arr::get($details, 'title') . '</b>';
379 - $html .= wpautop(Arr::get($details, 'description'));
380 - return $html;
381 - }
382 -
383 - 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'])) {
384 492 $platformLabels = [
385 493 'google_meet' => __('Google Meet', 'fluent-booking'),
386 494 'online_meeting' => __('Online Meeting', 'fluent-booking'),
387 495 'zoom_meeting' => __('Zoom Video', 'fluent-booking'),
@@ -387,18 +495,18 @@
387 495 'zoom_meeting' => __('Zoom Video', 'fluent-booking'),
388 496 'ms_teams' => __('MS Teams', 'fluent-booking'),
389 497 ];
390 498
391 - $html = '<b>' . $platformLabels[$locationType] . '</b> ';
392 -
393 - if ($meetingLink = Arr::get($details, 'online_platform_link')) {
394 - $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>';
395 503 }
396 -
397 - return $html;
504 + } else {
505 + return '--';
398 506 }
399 507
400 - return '--';
508 + return apply_filters('fluent_booking/location_details_html', $html, $details);
401 509 }
402 510
403 511 public function getLocationAsText()
404 512 {
@@ -420,9 +528,11 @@
420 528 if ($locationType == 'phone_guest') {
421 529 return $this->phone;
422 530 }
423 531
424 - 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);
425 535 }
426 536
427 537 public function getMessage()
428 538 {
@@ -438,11 +548,34 @@
438 548 }
439 549
440 550 public function getLocationDetailsAttribute($locationDetails)
441 551 {
442 - return \maybe_unserialize($locationDetails);
552 + $value = \maybe_unserialize($locationDetails);
553 +
554 + return is_array($value) ? $value : [];
443 555 }
444 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 +
445 578 public function getOngoingStatus()
446 579 {
447 580 if ($this->status != 'scheduled') {
448 581 return [];
@@ -508,11 +641,15 @@
508 641 }
509 642
510 643 public function getCancelReason($isText = false, $isHtml = false)
511 644 {
512 - $row = BookingActivity::where('booking_id', $this->id)
513 - ->where('type', 'cancel_reason')
514 - ->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 + }
515 652
516 653 if ($row) {
517 654 if ($isText) {
518 655 return $row->description;
@@ -517,9 +654,9 @@
517 654 if ($isText) {
518 655 return $row->description;
519 656 }
520 657 if ($isHtml) {
521 - return wp_unslash($row->description);
658 + return esc_html(wp_unslash($row->description));
522 659 }
523 660 }
524 661
525 662 return $row;
@@ -535,9 +672,9 @@
535 672 if ($isText) {
536 673 return $row->description;
537 674 }
538 675 if ($isHtml) {
539 - return wp_unslash($row->description);
676 + return esc_html(wp_unslash($row->description));
540 677 }
541 678 }
542 679
543 680 return $row;
@@ -643,11 +780,17 @@
643 780
644 781 do_action('fluent_booking/booking_schedule_rejected', $this, $this->calendar_event);
645 782 }
646 783
647 - public function getRescheduleReason()
784 + public function getRescheduleReason($html = false)
648 785 {
649 - 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;
650 793 }
651 794
652 795 private function generateBookingTitle($eventTitle, $authorName, $guestName)
653 796 {
@@ -672,10 +815,15 @@
672 815 $bookingTitle = EditorShortCodeParser::parse($bookingTitle, $this);
673 816
674 817 $bookingTitle = $bookingTitle ?: $this->generateBookingTitle($eventTitle, $authorName, $guestName);
675 818
676 - if ($html && strpos($bookingTitle, $eventTitle) !== false) {
677 - $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 + }
678 826 }
679 827
680 828 return apply_filters('fluent_booking/booking_meeting_title', $bookingTitle, $authorName, $guestName, $calendarEvent, $this);
681 829 }
@@ -714,11 +862,15 @@
714 862 }
715 863
716 864 public function getMeta($key, $default = '')
717 865 {
718 - $exist = BookingMeta::where('booking_id', $this->id)
719 - ->where('meta_key', $key)
720 - ->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 + }
721 873
722 874 if ($exist) {
723 875 return $exist->value;
724 876 }
@@ -733,17 +885,23 @@
733 885 */
734 886 public function scopeSearchBy($query, $search)
735 887 {
736 888 if ($search) {
889 + $escape = function ($value) {
890 + return addcslashes((string) $value, '%_\\');
891 + };
892 +
737 893 $fields = $this->searchable;
738 - $query->where(function ($query) use ($fields, $search) {
739 - $query->where(array_shift($fields), 'LIKE', "%$search%");
894 + $searchEsc = $escape($search);
740 895
896 + $query->where(function ($query) use ($fields, $search, $searchEsc, $escape) {
897 + $query->where(array_shift($fields), 'LIKE', "%$searchEsc%");
898 +
741 899 $nameArray = explode(' ', $search);
742 900 if (count($nameArray) >= 2) {
743 - $query->orWhere(function ($q) use ($nameArray) {
744 - $fname = array_shift($nameArray);
745 - $lastName = implode(' ', $nameArray);
901 + $query->orWhere(function ($q) use ($nameArray, $escape) {
902 + $fname = $escape(array_shift($nameArray));
903 + $lastName = $escape(implode(' ', $nameArray));
746 904 $q->where('first_name', 'LIKE', "%$fname%")
747 905 ->orWhere('last_name', 'LIKE', "%$lastName%");
748 906 });
749 907 }
@@ -748,9 +906,9 @@
748 906 });
749 907 }
750 908
751 909 foreach ($fields as $field) {
752 - $query->orWhere($field, 'LIKE', "%$search%");
910 + $query->orWhere($field, 'LIKE', "%$searchEsc%");
753 911 }
754 912 });
755 913 }
756 914
@@ -844,13 +1002,25 @@
844 1002 'type' => 'cancel',
845 1003 ], Helper::getBookingReceiptLandingBaseUrl());
846 1004 }
847 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 + */
848 1010 public function hasBookingAccess()
849 1011 {
850 - $hostIds = $this->getHostIds();
851 - $hasAccess = PermissionManager::userCan('manage_all_bookings');
852 - 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());
853 1023 }
854 1024
855 1025 private function canPerformAction($settings)
856 1026 {
@@ -871,8 +1041,10 @@
871 1041
872 1042 $conditionTime = $conditionValue * 60;
873 1043 if ($conditionUnit == 'hours') {
874 1044 $conditionTime = $conditionTime * 60;
1045 + } elseif ($conditionUnit == 'days') {
1046 + $conditionTime = $conditionTime * 60 * 24;
875 1047 }
876 1048
877 1049 return $bookingStartTime - $currentTime > $conditionTime;
878 1050 }
@@ -898,21 +1070,39 @@
898 1070 {
899 1071 return $this->event_type == 'group' || $this->event_type == 'group_event';
900 1072 }
901 1073
1074 + public function isRoundRobinBooking()
1075 + {
1076 + return $this->event_type == 'round_robin';
1077 + }
1078 +
902 1079 public function isMultiHostBooking()
903 1080 {
904 - return $this->event_type == 'single_event' || $this->event_type == 'group_event';
1081 + return in_array($this->event_type, ['single_event', 'group_event', 'collective']);
905 1082 }
906 1083
1084 + public function isRecurringBooking()
1085 + {
1086 + return Arr::get($this->other_info, 'recurring_count', 0) > 1;
1087 + }
1088 +
907 1089 public function getHostProfiles($public = true)
908 1090 {
909 1091 $hostIds = $this->getHostIds();
910 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 +
911 1102 $hosts = [];
912 1103 foreach ($hostIds as $hostId) {
913 - $calendar = Calendar::where('user_id', $hostId)->where('type', 'simple')->first();
914 - if ($calendar) {
1104 + if ($calendar = $calendars->get($hostId)) {
915 1105 $hosts[] = $calendar->getAuthorProfile($public);
916 1106 }
917 1107 }
918 1108
@@ -922,9 +1112,9 @@
922 1112 public function getInviteePhoneNumber($calendarEvent)
923 1113 {
924 1114 $customFormData = $this->getCustomFormData(false);
925 1115
926 - $customFields = BookingFieldService::getBookingFields($calendarEvent);
1116 + $customFields = BookingFieldService::getBookingFields($calendarEvent, true);
927 1117
928 1118 foreach ($customFields as $field) {
929 1119 $fieldValue = Arr::get($customFormData, $field['name']);
930 1120 if ($fieldValue && $field['type'] == 'phone' && Arr::isTrue($field, 'is_sms_number')) {
@@ -944,9 +1134,9 @@
944 1134 if ($message) {
945 1135 return $message;
946 1136 }
947 1137
948 - return __('Sorry! you can not cancel this', 'fluent-booking');
1138 + return __('Sorry! you cannot cancel this', 'fluent-booking');
949 1139 }
950 1140
951 1141 public function getRescheduleMessage()
952 1142 {
@@ -957,9 +1147,9 @@
957 1147 if ($message) {
958 1148 return $message;
959 1149 }
960 1150
961 - return __('Sorry! you can not reschedule this', 'fluent-booking');
1151 + return __('Sorry! you cannot reschedule this', 'fluent-booking');
962 1152 }
963 1153
964 1154 public function getHostDetails($isPublic = true, $hostId = null)
965 1155 {
@@ -1010,15 +1200,30 @@
1010 1200 ->where('type', 'simple')
1011 1201 ->first();
1012 1202
1013 1203 if (!$calendar) {
1014 - return '';
1204 + return 'UTC';
1015 1205 }
1016 1206 return $calendar->author_timezone;
1017 1207 }
1018 - return '';
1208 + return 'UTC';
1019 1209 }
1020 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 +
1021 1226 public function getIcsBookingDescription()
1022 1227 {
1023 1228 $description = str_replace(PHP_EOL, '\\n', $this->getConfirmationData());
1024 1229
@@ -1042,9 +1247,9 @@
1042 1247 }
1043 1248
1044 1249 public function getAdditionalData($isHtml = false)
1045 1250 {
1046 - $customData = BookingFieldService::getFormattedCustomBookingData($this);
1251 + $customData = BookingFieldService::getFormattedCustomBookingData($this, $isHtml, true);
1047 1252
1048 1253 if (!$customData) {
1049 1254 return '';
1050 1255 }
@@ -1062,9 +1267,9 @@
1062 1267 if (empty($data['value'])) {
1063 1268 continue;
1064 1269 }
1065 1270 $html .= '<tr>';
1066 - $html .= '<td><b>' . $data['label'] . '</b></td>';
1271 + $html .= '<td><b>' . esc_html($data['label']) . '</b></td>';
1067 1272 $html .= '<td>' . $data['value'] . '</td>';
1068 1273 $html .= '</tr>';
1069 1274 }
1070 1275 $html .= '</table>';
@@ -1071,9 +1276,9 @@
1071 1276
1072 1277 return $html;
1073 1278 }
1074 1279
1075 - public function getConfirmationData()
1280 + public function getConfirmationData($html = false)
1076 1281 {
1077 1282 $author = $this->getHostDetails(false);
1078 1283
1079 1284 $guestName = trim($this->first_name . ' ' . $this->last_name);
@@ -1078,9 +1283,11 @@
1078 1283
1079 1284 $guestName = trim($this->first_name . ' ' . $this->last_name);
1080 1285
1081 1286 $bookingTitle = $this->getBookingTitle();
1082 -
1287 +
1288 + $separator = $html ? '<br>' : PHP_EOL;
1289 +
1083 1290 $sections = [
1084 1291 'what' => [
1085 1292 'title' => __('What', 'fluent-booking'),
1086 1293 'content' => $bookingTitle,
@@ -1086,13 +1293,13 @@
1086 1293 'content' => $bookingTitle,
1087 1294 ],
1088 1295 'when' => [
1089 1296 'title' => __('When', 'fluent-booking'),
1090 - '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 . ')',
1091 1298 ],
1092 1299 'who' => [
1093 1300 'title' => __('Who', 'fluent-booking'),
1094 - '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
1095 1302 ],
1096 1303 'where' => [
1097 1304 'title' => __('Where', 'fluent-booking'),
1098 1305 'content' => $this->getLocationAsText()
@@ -1097,14 +1304,18 @@
1097 1304 'title' => __('Where', 'fluent-booking'),
1098 1305 'content' => $this->getLocationAsText()
1099 1306 ],
1100 1307 ];
1101 -
1102 - $lines = array_map(function ($section) {
1103 - 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']);
1104 1315 }, $sections);
1105 -
1106 - return implode(PHP_EOL . PHP_EOL, $lines) . PHP_EOL . PHP_EOL;
1316 +
1317 + return implode($separator . $separator, $lines) . $separator . $separator;
1107 1318 }
1108 1319
1109 1320 public function getMeetingBookmarks($assetsUrl = '')
1110 1321 {
@@ -1109,45 +1320,52 @@
1109 1320 public function getMeetingBookmarks($assetsUrl = '')
1110 1321 {
1111 1322 $bookingTitle = $this->getBookingTitle();
1112 1323
1113 - $eventTitle = $this->calendar_event->title;
1324 + $eventDescription = $this->getCalendarLinkDescription();
1114 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 +
1115 1354 return apply_filters('fluent_booking/meeting_bookmarks', [
1116 1355 'google' => [
1117 1356 'title' => __('Google Calendar', 'fluent-booking'),
1118 - 'url' => add_query_arg([
1119 - 'dates' => gmdate('Ymd\THis\Z', strtotime($this->start_time)) . '/' . gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1120 - 'text' => $bookingTitle,
1121 - 'details' => $eventTitle,
1122 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1123 - ], 'https://calendar.google.com/calendar/r/eventedit'),
1357 + 'url' => 'https://calendar.google.com/calendar/render?action=TEMPLATE&' . $googleParams,
1124 1358 'icon' => $assetsUrl . 'images/g-icon.svg'
1125 1359 ],
1126 1360 'outlook' => [
1127 1361 'title' => __('Outlook', 'fluent-booking'),
1128 - 'url' => add_query_arg([
1129 - 'startdt' => gmdate('Ymd\THis\Z', strtotime($this->start_time)),
1130 - 'enddt' => gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1131 - 'subject' => $bookingTitle,
1132 - 'path' => '/calendar/action/compose',
1133 - 'body' => $eventTitle,
1134 - 'rru' => 'addevent',
1135 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1136 - ], 'https://outlook.live.com/calendar/0/deeplink/compose'),
1362 + 'url' => 'https://outlook.live.com/calendar/0/deeplink/compose?' . $outlookParams,
1137 1363 'icon' => $assetsUrl . 'images/ol-icon.svg'
1138 1364 ],
1139 1365 'msoffice' => [
1140 1366 'title' => __('Microsoft Office', '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.office.com/calendar/0/deeplink/compose'),
1367 + 'url' => 'https://outlook.office.com/calendar/0/deeplink/compose?' . $outlookParams,
1150 1368 'icon' => $assetsUrl . 'images/msoffice.svg'
1151 1369 ],
1152 1370 'other' => [
1153 1371 'title' => __('Other Calendar', 'fluent-booking'),
@@ -1156,5 +1374,5 @@
1156 1374 ]
1157 1375 ], $this);
1158 1376 }
1159 1377
1160 -}
1378 +}