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 +372 -129 1.5.01 → 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 {
@@ -305,14 +392,65 @@
305 392 {
306 393 return DateTimeHelper::convertFromUtc($this->end_time, $this->person_time_zone, $format);
307 394 }
308 395
396 + public function getOtherBookingTimes()
397 + {
398 + $otherBookings = self::where('parent_id', $this->id)->get();
399 +
400 + return $otherBookings->map(function ($otherBooking) {
401 + return $otherBooking->getFullBookingDateTimeText($this->person_time_zone, true) . ' (' . $this->person_time_zone . ')';
402 + })->toArray();
403 + }
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 +
309 440 public function getHostAndGuestDetailsHtml()
310 441 {
311 442 $authors = $this->getHostsDetails();
312 443
313 - $guestName = trim($this->first_name . ' ' . $this->last_name);
444 + $guestNames = (array) trim($this->first_name . ' ' . $this->last_name);
314 445
446 + if ($this->isMultiGuestBooking() && !$this->isRecurringBooking()) {
447 + $otherGuests = self::where('parent_id', $this->id)->get()->map(function ($guest) {
448 + return trim($guest->first_name . ' ' . $guest->last_name);
449 + })->toArray();
450 + $guestNames = array_merge($guestNames, $otherGuests);
451 + }
452 +
315 453 $hostUserId = $this->host_user_id;
316 454
317 455 $authorListHtml = '<ul class="fcal_listed">';
318 456
@@ -317,12 +455,14 @@
317 455 $authorListHtml = '<ul class="fcal_listed">';
318 456
319 457 foreach ($authors as $author) {
320 458 $authorBadge = ($author['id'] == $hostUserId) ? '<span class="fcal_host_badge">' . __('Host', 'fluent-booking') . '</span>' : '';
321 - $authorListHtml .= '<li class="fcal_host_name">' . $author['name'] . $authorBadge . '</li>';
459 + $authorListHtml .= '<li class="fcal_host_name">' . esc_html($author['name']) . $authorBadge . '</li>';
322 460 }
323 461
324 - $authorListHtml .= '<li class="fcal_guest_name">' . $guestName . '</li>';
462 + foreach ($guestNames as $guestName) {
463 + $authorListHtml .= '<li class="fcal_guest_name">' . esc_html($guestName) . '</li>';
464 + }
325 465 $authorListHtml .= '</ul>';
326 466
327 467 return $authorListHtml;
328 468 }
@@ -331,39 +471,25 @@
331 471 {
332 472 $details = $this->location_details;
333 473 $locationType = Arr::get($details, 'type');
334 474
335 - if (!$locationType) {
336 - return '--';
337 - }
338 -
339 - if ($locationType == 'in_person_guest') {
340 - return '<b>' . __('Invitee Address:', 'fluent-booking') . ' </b>' . Arr::get($details, 'description');
341 - }
342 -
343 - if ($locationType == 'in_person_organizer') {
344 - $html = '<b>' . Arr::get($details, 'title') . ' </b>';
345 - if ($description = Arr::get($details, 'description')) {
346 - $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));
347 483 }
348 - return $html;
349 - }
350 -
351 - if ($locationType == 'phone_guest') {
352 - return '<b>' . __('Phone Call:', 'fluent-booking') . ' </b>' . $this->phone;
353 - }
354 -
355 - if ($locationType == 'phone_organizer') {
356 - return '<b>' . __('Phone Call:', 'fluent-booking') . ' </b>' . Arr::get($details, 'description') . __(' (Host phone number)', 'fluent-booking');
357 - }
358 -
359 - if ($locationType == 'custom') {
360 - $html = '<b>' . Arr::get($details, 'title') . '</b>';
361 - $html .= wpautop(Arr::get($details, 'description'));
362 - return $html;
363 - }
364 -
365 - 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'])) {
366 492 $platformLabels = [
367 493 'google_meet' => __('Google Meet', 'fluent-booking'),
368 494 'online_meeting' => __('Online Meeting', 'fluent-booking'),
369 495 'zoom_meeting' => __('Zoom Video', 'fluent-booking'),
@@ -369,18 +495,18 @@
369 495 'zoom_meeting' => __('Zoom Video', 'fluent-booking'),
370 496 'ms_teams' => __('MS Teams', 'fluent-booking'),
371 497 ];
372 498
373 - $html = '<b>' . $platformLabels[$locationType] . '</b> ';
374 -
375 - if ($meetingLink = Arr::get($details, 'online_platform_link')) {
376 - $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>';
377 503 }
378 -
379 - return $html;
504 + } else {
505 + return '--';
380 506 }
381 507
382 - return '--';
508 + return apply_filters('fluent_booking/location_details_html', $html, $details);
383 509 }
384 510
385 511 public function getLocationAsText()
386 512 {
@@ -402,9 +528,11 @@
402 528 if ($locationType == 'phone_guest') {
403 529 return $this->phone;
404 530 }
405 531
406 - 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);
407 535 }
408 536
409 537 public function getMessage()
410 538 {
@@ -420,11 +548,34 @@
420 548 }
421 549
422 550 public function getLocationDetailsAttribute($locationDetails)
423 551 {
424 - return \maybe_unserialize($locationDetails);
552 + $value = \maybe_unserialize($locationDetails);
553 +
554 + return is_array($value) ? $value : [];
425 555 }
426 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 +
427 578 public function getOngoingStatus()
428 579 {
429 580 if ($this->status != 'scheduled') {
430 581 return [];
@@ -490,11 +641,15 @@
490 641 }
491 642
492 643 public function getCancelReason($isText = false, $isHtml = false)
493 644 {
494 - $row = BookingActivity::where('booking_id', $this->id)
495 - ->where('type', 'cancel_reason')
496 - ->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 + }
497 652
498 653 if ($row) {
499 654 if ($isText) {
500 655 return $row->description;
@@ -499,9 +654,9 @@
499 654 if ($isText) {
500 655 return $row->description;
501 656 }
502 657 if ($isHtml) {
503 - return wp_unslash($row->description);
658 + return esc_html(wp_unslash($row->description));
504 659 }
505 660 }
506 661
507 662 return $row;
@@ -517,9 +672,9 @@
517 672 if ($isText) {
518 673 return $row->description;
519 674 }
520 675 if ($isHtml) {
521 - return wp_unslash($row->description);
676 + return esc_html(wp_unslash($row->description));
522 677 }
523 678 }
524 679
525 680 return $row;
@@ -625,11 +780,17 @@
625 780
626 781 do_action('fluent_booking/booking_schedule_rejected', $this, $this->calendar_event);
627 782 }
628 783
629 - public function getRescheduleReason()
784 + public function getRescheduleReason($html = false)
630 785 {
631 - 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;
632 793 }
633 794
634 795 private function generateBookingTitle($eventTitle, $authorName, $guestName)
635 796 {
@@ -654,10 +815,15 @@
654 815 $bookingTitle = EditorShortCodeParser::parse($bookingTitle, $this);
655 816
656 817 $bookingTitle = $bookingTitle ?: $this->generateBookingTitle($eventTitle, $authorName, $guestName);
657 818
658 - if ($html && strpos($bookingTitle, $eventTitle) !== false) {
659 - $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 + }
660 826 }
661 827
662 828 return apply_filters('fluent_booking/booking_meeting_title', $bookingTitle, $authorName, $guestName, $calendarEvent, $this);
663 829 }
@@ -687,13 +853,24 @@
687 853 'value' => $value
688 854 ]);
689 855 }
690 856
857 + public function deleteMeta($key)
858 + {
859 + return BookingMeta::where('booking_id', $this->id)
860 + ->where('meta_key', $key)
861 + ->delete();
862 + }
863 +
691 864 public function getMeta($key, $default = '')
692 865 {
693 - $exist = BookingMeta::where('booking_id', $this->id)
694 - ->where('meta_key', $key)
695 - ->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 + }
696 873
697 874 if ($exist) {
698 875 return $exist->value;
699 876 }
@@ -708,17 +885,23 @@
708 885 */
709 886 public function scopeSearchBy($query, $search)
710 887 {
711 888 if ($search) {
889 + $escape = function ($value) {
890 + return addcslashes((string) $value, '%_\\');
891 + };
892 +
712 893 $fields = $this->searchable;
713 - $query->where(function ($query) use ($fields, $search) {
714 - $query->where(array_shift($fields), 'LIKE', "%$search%");
894 + $searchEsc = $escape($search);
715 895
896 + $query->where(function ($query) use ($fields, $search, $searchEsc, $escape) {
897 + $query->where(array_shift($fields), 'LIKE', "%$searchEsc%");
898 +
716 899 $nameArray = explode(' ', $search);
717 900 if (count($nameArray) >= 2) {
718 - $query->orWhere(function ($q) use ($nameArray) {
719 - $fname = array_shift($nameArray);
720 - $lastName = implode(' ', $nameArray);
901 + $query->orWhere(function ($q) use ($nameArray, $escape) {
902 + $fname = $escape(array_shift($nameArray));
903 + $lastName = $escape(implode(' ', $nameArray));
721 904 $q->where('first_name', 'LIKE', "%$fname%")
722 905 ->orWhere('last_name', 'LIKE', "%$lastName%");
723 906 });
724 907 }
@@ -723,9 +906,9 @@
723 906 });
724 907 }
725 908
726 909 foreach ($fields as $field) {
727 - $query->orWhere($field, 'LIKE', "%$search%");
910 + $query->orWhere($field, 'LIKE', "%$searchEsc%");
728 911 }
729 912 });
730 913 }
731 914
@@ -819,13 +1002,25 @@
819 1002 'type' => 'cancel',
820 1003 ], Helper::getBookingReceiptLandingBaseUrl());
821 1004 }
822 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 + */
823 1010 public function hasBookingAccess()
824 1011 {
825 - $hostIds = $this->getHostIds();
826 - $hasAccess = PermissionManager::userCan('manage_all_bookings');
827 - 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());
828 1023 }
829 1024
830 1025 private function canPerformAction($settings)
831 1026 {
@@ -846,8 +1041,10 @@
846 1041
847 1042 $conditionTime = $conditionValue * 60;
848 1043 if ($conditionUnit == 'hours') {
849 1044 $conditionTime = $conditionTime * 60;
1045 + } elseif ($conditionUnit == 'days') {
1046 + $conditionTime = $conditionTime * 60 * 24;
850 1047 }
851 1048
852 1049 return $bookingStartTime - $currentTime > $conditionTime;
853 1050 }
@@ -873,21 +1070,39 @@
873 1070 {
874 1071 return $this->event_type == 'group' || $this->event_type == 'group_event';
875 1072 }
876 1073
1074 + public function isRoundRobinBooking()
1075 + {
1076 + return $this->event_type == 'round_robin';
1077 + }
1078 +
877 1079 public function isMultiHostBooking()
878 1080 {
879 - return $this->event_type == 'single_event' || $this->event_type == 'group_event';
1081 + return in_array($this->event_type, ['single_event', 'group_event', 'collective']);
880 1082 }
881 1083
1084 + public function isRecurringBooking()
1085 + {
1086 + return Arr::get($this->other_info, 'recurring_count', 0) > 1;
1087 + }
1088 +
882 1089 public function getHostProfiles($public = true)
883 1090 {
884 1091 $hostIds = $this->getHostIds();
885 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 +
886 1102 $hosts = [];
887 1103 foreach ($hostIds as $hostId) {
888 - $calendar = Calendar::where('user_id', $hostId)->where('type', 'simple')->first();
889 - if ($calendar) {
1104 + if ($calendar = $calendars->get($hostId)) {
890 1105 $hosts[] = $calendar->getAuthorProfile($public);
891 1106 }
892 1107 }
893 1108
@@ -897,9 +1112,9 @@
897 1112 public function getInviteePhoneNumber($calendarEvent)
898 1113 {
899 1114 $customFormData = $this->getCustomFormData(false);
900 1115
901 - $customFields = BookingFieldService::getBookingFields($calendarEvent);
1116 + $customFields = BookingFieldService::getBookingFields($calendarEvent, true);
902 1117
903 1118 foreach ($customFields as $field) {
904 1119 $fieldValue = Arr::get($customFormData, $field['name']);
905 1120 if ($fieldValue && $field['type'] == 'phone' && Arr::isTrue($field, 'is_sms_number')) {
@@ -919,9 +1134,9 @@
919 1134 if ($message) {
920 1135 return $message;
921 1136 }
922 1137
923 - return __('Sorry! you can not cancel this', 'fluent-booking');
1138 + return __('Sorry! you cannot cancel this', 'fluent-booking');
924 1139 }
925 1140
926 1141 public function getRescheduleMessage()
927 1142 {
@@ -932,9 +1147,9 @@
932 1147 if ($message) {
933 1148 return $message;
934 1149 }
935 1150
936 - return __('Sorry! you can not reschedule this', 'fluent-booking');
1151 + return __('Sorry! you cannot reschedule this', 'fluent-booking');
937 1152 }
938 1153
939 1154 public function getHostDetails($isPublic = true, $hostId = null)
940 1155 {
@@ -985,15 +1200,30 @@
985 1200 ->where('type', 'simple')
986 1201 ->first();
987 1202
988 1203 if (!$calendar) {
989 - return '';
1204 + return 'UTC';
990 1205 }
991 1206 return $calendar->author_timezone;
992 1207 }
993 - return '';
1208 + return 'UTC';
994 1209 }
995 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 +
996 1226 public function getIcsBookingDescription()
997 1227 {
998 1228 $description = str_replace(PHP_EOL, '\\n', $this->getConfirmationData());
999 1229
@@ -1017,9 +1247,9 @@
1017 1247 }
1018 1248
1019 1249 public function getAdditionalData($isHtml = false)
1020 1250 {
1021 - $customData = BookingFieldService::getFormattedCustomBookingData($this);
1251 + $customData = BookingFieldService::getFormattedCustomBookingData($this, $isHtml, true);
1022 1252
1023 1253 if (!$customData) {
1024 1254 return '';
1025 1255 }
@@ -1037,9 +1267,9 @@
1037 1267 if (empty($data['value'])) {
1038 1268 continue;
1039 1269 }
1040 1270 $html .= '<tr>';
1041 - $html .= '<td><b>' . $data['label'] . '</b></td>';
1271 + $html .= '<td><b>' . esc_html($data['label']) . '</b></td>';
1042 1272 $html .= '<td>' . $data['value'] . '</td>';
1043 1273 $html .= '</tr>';
1044 1274 }
1045 1275 $html .= '</table>';
@@ -1046,9 +1276,9 @@
1046 1276
1047 1277 return $html;
1048 1278 }
1049 1279
1050 - public function getConfirmationData()
1280 + public function getConfirmationData($html = false)
1051 1281 {
1052 1282 $author = $this->getHostDetails(false);
1053 1283
1054 1284 $guestName = trim($this->first_name . ' ' . $this->last_name);
@@ -1053,9 +1283,11 @@
1053 1283
1054 1284 $guestName = trim($this->first_name . ' ' . $this->last_name);
1055 1285
1056 1286 $bookingTitle = $this->getBookingTitle();
1057 -
1287 +
1288 + $separator = $html ? '<br>' : PHP_EOL;
1289 +
1058 1290 $sections = [
1059 1291 'what' => [
1060 1292 'title' => __('What', 'fluent-booking'),
1061 1293 'content' => $bookingTitle,
@@ -1061,13 +1293,13 @@
1061 1293 'content' => $bookingTitle,
1062 1294 ],
1063 1295 'when' => [
1064 1296 'title' => __('When', 'fluent-booking'),
1065 - '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 . ')',
1066 1298 ],
1067 1299 'who' => [
1068 1300 'title' => __('Who', 'fluent-booking'),
1069 - '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
1070 1302 ],
1071 1303 'where' => [
1072 1304 'title' => __('Where', 'fluent-booking'),
1073 1305 'content' => $this->getLocationAsText()
@@ -1072,14 +1304,18 @@
1072 1304 'title' => __('Where', 'fluent-booking'),
1073 1305 'content' => $this->getLocationAsText()
1074 1306 ],
1075 1307 ];
1076 -
1077 - $lines = array_map(function ($section) {
1078 - 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']);
1079 1315 }, $sections);
1080 -
1081 - return implode(PHP_EOL . PHP_EOL, $lines) . PHP_EOL . PHP_EOL;
1316 +
1317 + return implode($separator . $separator, $lines) . $separator . $separator;
1082 1318 }
1083 1319
1084 1320 public function getMeetingBookmarks($assetsUrl = '')
1085 1321 {
@@ -1084,45 +1320,52 @@
1084 1320 public function getMeetingBookmarks($assetsUrl = '')
1085 1321 {
1086 1322 $bookingTitle = $this->getBookingTitle();
1087 1323
1088 - $eventTitle = $this->calendar_event->title;
1324 + $eventDescription = $this->getCalendarLinkDescription();
1089 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 +
1090 1354 return apply_filters('fluent_booking/meeting_bookmarks', [
1091 1355 'google' => [
1092 1356 'title' => __('Google Calendar', 'fluent-booking'),
1093 - 'url' => add_query_arg([
1094 - 'dates' => gmdate('Ymd\THis\Z', strtotime($this->start_time)) . '/' . gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1095 - 'text' => $bookingTitle,
1096 - 'details' => $eventTitle,
1097 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1098 - ], 'https://calendar.google.com/calendar/r/eventedit'),
1357 + 'url' => 'https://calendar.google.com/calendar/render?action=TEMPLATE&' . $googleParams,
1099 1358 'icon' => $assetsUrl . 'images/g-icon.svg'
1100 1359 ],
1101 1360 'outlook' => [
1102 1361 'title' => __('Outlook', 'fluent-booking'),
1103 - 'url' => add_query_arg([
1104 - 'startdt' => gmdate('Ymd\THis\Z', strtotime($this->start_time)),
1105 - 'enddt' => gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1106 - 'subject' => $bookingTitle,
1107 - 'path' => '/calendar/action/compose',
1108 - 'body' => $eventTitle,
1109 - 'rru' => 'addevent',
1110 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1111 - ], 'https://outlook.live.com/calendar/0/deeplink/compose'),
1362 + 'url' => 'https://outlook.live.com/calendar/0/deeplink/compose?' . $outlookParams,
1112 1363 'icon' => $assetsUrl . 'images/ol-icon.svg'
1113 1364 ],
1114 1365 'msoffice' => [
1115 1366 'title' => __('Microsoft Office', 'fluent-booking'),
1116 - 'url' => add_query_arg([
1117 - 'startdt' => gmdate('Ymd\THis\Z', strtotime($this->start_time)),
1118 - 'enddt' => gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1119 - 'subject' => $bookingTitle,
1120 - 'path' => '/calendar/action/compose',
1121 - 'body' => $eventTitle,
1122 - 'rru' => 'addevent',
1123 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1124 - ], 'https://outlook.office.com/calendar/0/deeplink/compose'),
1367 + 'url' => 'https://outlook.office.com/calendar/0/deeplink/compose?' . $outlookParams,
1125 1368 'icon' => $assetsUrl . 'images/msoffice.svg'
1126 1369 ],
1127 1370 'other' => [
1128 1371 'title' => __('Other Calendar', 'fluent-booking'),
@@ -1131,5 +1374,5 @@
1131 1374 ]
1132 1375 ], $this);
1133 1376 }
1134 1377
1135 -}
1378 +}