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 +354 -129 1.5.20 → 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 }
@@ -705,13 +853,24 @@
705 853 'value' => $value
706 854 ]);
707 855 }
708 856
857 + public function deleteMeta($key)
858 + {
859 + return BookingMeta::where('booking_id', $this->id)
860 + ->where('meta_key', $key)
861 + ->delete();
862 + }
863 +
709 864 public function getMeta($key, $default = '')
710 865 {
711 - $exist = BookingMeta::where('booking_id', $this->id)
712 - ->where('meta_key', $key)
713 - ->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 + }
714 873
715 874 if ($exist) {
716 875 return $exist->value;
717 876 }
@@ -726,17 +885,23 @@
726 885 */
727 886 public function scopeSearchBy($query, $search)
728 887 {
729 888 if ($search) {
889 + $escape = function ($value) {
890 + return addcslashes((string) $value, '%_\\');
891 + };
892 +
730 893 $fields = $this->searchable;
731 - $query->where(function ($query) use ($fields, $search) {
732 - $query->where(array_shift($fields), 'LIKE', "%$search%");
894 + $searchEsc = $escape($search);
733 895
896 + $query->where(function ($query) use ($fields, $search, $searchEsc, $escape) {
897 + $query->where(array_shift($fields), 'LIKE', "%$searchEsc%");
898 +
734 899 $nameArray = explode(' ', $search);
735 900 if (count($nameArray) >= 2) {
736 - $query->orWhere(function ($q) use ($nameArray) {
737 - $fname = array_shift($nameArray);
738 - $lastName = implode(' ', $nameArray);
901 + $query->orWhere(function ($q) use ($nameArray, $escape) {
902 + $fname = $escape(array_shift($nameArray));
903 + $lastName = $escape(implode(' ', $nameArray));
739 904 $q->where('first_name', 'LIKE', "%$fname%")
740 905 ->orWhere('last_name', 'LIKE', "%$lastName%");
741 906 });
742 907 }
@@ -741,9 +906,9 @@
741 906 });
742 907 }
743 908
744 909 foreach ($fields as $field) {
745 - $query->orWhere($field, 'LIKE', "%$search%");
910 + $query->orWhere($field, 'LIKE', "%$searchEsc%");
746 911 }
747 912 });
748 913 }
749 914
@@ -837,13 +1002,25 @@
837 1002 'type' => 'cancel',
838 1003 ], Helper::getBookingReceiptLandingBaseUrl());
839 1004 }
840 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 + */
841 1010 public function hasBookingAccess()
842 1011 {
843 - $hostIds = $this->getHostIds();
844 - $hasAccess = PermissionManager::userCan('manage_all_bookings');
845 - 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());
846 1023 }
847 1024
848 1025 private function canPerformAction($settings)
849 1026 {
@@ -864,8 +1041,10 @@
864 1041
865 1042 $conditionTime = $conditionValue * 60;
866 1043 if ($conditionUnit == 'hours') {
867 1044 $conditionTime = $conditionTime * 60;
1045 + } elseif ($conditionUnit == 'days') {
1046 + $conditionTime = $conditionTime * 60 * 24;
868 1047 }
869 1048
870 1049 return $bookingStartTime - $currentTime > $conditionTime;
871 1050 }
@@ -891,21 +1070,39 @@
891 1070 {
892 1071 return $this->event_type == 'group' || $this->event_type == 'group_event';
893 1072 }
894 1073
1074 + public function isRoundRobinBooking()
1075 + {
1076 + return $this->event_type == 'round_robin';
1077 + }
1078 +
895 1079 public function isMultiHostBooking()
896 1080 {
897 - return $this->event_type == 'single_event' || $this->event_type == 'group_event';
1081 + return in_array($this->event_type, ['single_event', 'group_event', 'collective']);
898 1082 }
899 1083
1084 + public function isRecurringBooking()
1085 + {
1086 + return Arr::get($this->other_info, 'recurring_count', 0) > 1;
1087 + }
1088 +
900 1089 public function getHostProfiles($public = true)
901 1090 {
902 1091 $hostIds = $this->getHostIds();
903 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 +
904 1102 $hosts = [];
905 1103 foreach ($hostIds as $hostId) {
906 - $calendar = Calendar::where('user_id', $hostId)->where('type', 'simple')->first();
907 - if ($calendar) {
1104 + if ($calendar = $calendars->get($hostId)) {
908 1105 $hosts[] = $calendar->getAuthorProfile($public);
909 1106 }
910 1107 }
911 1108
@@ -915,9 +1112,9 @@
915 1112 public function getInviteePhoneNumber($calendarEvent)
916 1113 {
917 1114 $customFormData = $this->getCustomFormData(false);
918 1115
919 - $customFields = BookingFieldService::getBookingFields($calendarEvent);
1116 + $customFields = BookingFieldService::getBookingFields($calendarEvent, true);
920 1117
921 1118 foreach ($customFields as $field) {
922 1119 $fieldValue = Arr::get($customFormData, $field['name']);
923 1120 if ($fieldValue && $field['type'] == 'phone' && Arr::isTrue($field, 'is_sms_number')) {
@@ -937,9 +1134,9 @@
937 1134 if ($message) {
938 1135 return $message;
939 1136 }
940 1137
941 - return __('Sorry! you can not cancel this', 'fluent-booking');
1138 + return __('Sorry! you cannot cancel this', 'fluent-booking');
942 1139 }
943 1140
944 1141 public function getRescheduleMessage()
945 1142 {
@@ -950,9 +1147,9 @@
950 1147 if ($message) {
951 1148 return $message;
952 1149 }
953 1150
954 - return __('Sorry! you can not reschedule this', 'fluent-booking');
1151 + return __('Sorry! you cannot reschedule this', 'fluent-booking');
955 1152 }
956 1153
957 1154 public function getHostDetails($isPublic = true, $hostId = null)
958 1155 {
@@ -1003,15 +1200,30 @@
1003 1200 ->where('type', 'simple')
1004 1201 ->first();
1005 1202
1006 1203 if (!$calendar) {
1007 - return '';
1204 + return 'UTC';
1008 1205 }
1009 1206 return $calendar->author_timezone;
1010 1207 }
1011 - return '';
1208 + return 'UTC';
1012 1209 }
1013 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 +
1014 1226 public function getIcsBookingDescription()
1015 1227 {
1016 1228 $description = str_replace(PHP_EOL, '\\n', $this->getConfirmationData());
1017 1229
@@ -1035,9 +1247,9 @@
1035 1247 }
1036 1248
1037 1249 public function getAdditionalData($isHtml = false)
1038 1250 {
1039 - $customData = BookingFieldService::getFormattedCustomBookingData($this);
1251 + $customData = BookingFieldService::getFormattedCustomBookingData($this, $isHtml, true);
1040 1252
1041 1253 if (!$customData) {
1042 1254 return '';
1043 1255 }
@@ -1055,9 +1267,9 @@
1055 1267 if (empty($data['value'])) {
1056 1268 continue;
1057 1269 }
1058 1270 $html .= '<tr>';
1059 - $html .= '<td><b>' . $data['label'] . '</b></td>';
1271 + $html .= '<td><b>' . esc_html($data['label']) . '</b></td>';
1060 1272 $html .= '<td>' . $data['value'] . '</td>';
1061 1273 $html .= '</tr>';
1062 1274 }
1063 1275 $html .= '</table>';
@@ -1064,9 +1276,9 @@
1064 1276
1065 1277 return $html;
1066 1278 }
1067 1279
1068 - public function getConfirmationData()
1280 + public function getConfirmationData($html = false)
1069 1281 {
1070 1282 $author = $this->getHostDetails(false);
1071 1283
1072 1284 $guestName = trim($this->first_name . ' ' . $this->last_name);
@@ -1071,9 +1283,11 @@
1071 1283
1072 1284 $guestName = trim($this->first_name . ' ' . $this->last_name);
1073 1285
1074 1286 $bookingTitle = $this->getBookingTitle();
1075 -
1287 +
1288 + $separator = $html ? '<br>' : PHP_EOL;
1289 +
1076 1290 $sections = [
1077 1291 'what' => [
1078 1292 'title' => __('What', 'fluent-booking'),
1079 1293 'content' => $bookingTitle,
@@ -1079,13 +1293,13 @@
1079 1293 'content' => $bookingTitle,
1080 1294 ],
1081 1295 'when' => [
1082 1296 'title' => __('When', 'fluent-booking'),
1083 - '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 . ')',
1084 1298 ],
1085 1299 'who' => [
1086 1300 'title' => __('Who', 'fluent-booking'),
1087 - '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
1088 1302 ],
1089 1303 'where' => [
1090 1304 'title' => __('Where', 'fluent-booking'),
1091 1305 'content' => $this->getLocationAsText()
@@ -1090,14 +1304,18 @@
1090 1304 'title' => __('Where', 'fluent-booking'),
1091 1305 'content' => $this->getLocationAsText()
1092 1306 ],
1093 1307 ];
1094 -
1095 - $lines = array_map(function ($section) {
1096 - 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']);
1097 1315 }, $sections);
1098 -
1099 - return implode(PHP_EOL . PHP_EOL, $lines) . PHP_EOL . PHP_EOL;
1316 +
1317 + return implode($separator . $separator, $lines) . $separator . $separator;
1100 1318 }
1101 1319
1102 1320 public function getMeetingBookmarks($assetsUrl = '')
1103 1321 {
@@ -1102,45 +1320,52 @@
1102 1320 public function getMeetingBookmarks($assetsUrl = '')
1103 1321 {
1104 1322 $bookingTitle = $this->getBookingTitle();
1105 1323
1106 - $eventTitle = $this->calendar_event->title;
1324 + $eventDescription = $this->getCalendarLinkDescription();
1107 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 +
1108 1354 return apply_filters('fluent_booking/meeting_bookmarks', [
1109 1355 'google' => [
1110 1356 'title' => __('Google Calendar', 'fluent-booking'),
1111 - 'url' => add_query_arg([
1112 - 'dates' => gmdate('Ymd\THis\Z', strtotime($this->start_time)) . '/' . gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1113 - 'text' => $bookingTitle,
1114 - 'details' => $eventTitle,
1115 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1116 - ], 'https://calendar.google.com/calendar/r/eventedit'),
1357 + 'url' => 'https://calendar.google.com/calendar/render?action=TEMPLATE&' . $googleParams,
1117 1358 'icon' => $assetsUrl . 'images/g-icon.svg'
1118 1359 ],
1119 1360 'outlook' => [
1120 1361 'title' => __('Outlook', 'fluent-booking'),
1121 - 'url' => add_query_arg([
1122 - 'startdt' => gmdate('Ymd\THis\Z', strtotime($this->start_time)),
1123 - 'enddt' => gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1124 - 'subject' => $bookingTitle,
1125 - 'path' => '/calendar/action/compose',
1126 - 'body' => $eventTitle,
1127 - 'rru' => 'addevent',
1128 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1129 - ], 'https://outlook.live.com/calendar/0/deeplink/compose'),
1362 + 'url' => 'https://outlook.live.com/calendar/0/deeplink/compose?' . $outlookParams,
1130 1363 'icon' => $assetsUrl . 'images/ol-icon.svg'
1131 1364 ],
1132 1365 'msoffice' => [
1133 1366 'title' => __('Microsoft Office', 'fluent-booking'),
1134 - 'url' => add_query_arg([
1135 - 'startdt' => gmdate('Ymd\THis\Z', strtotime($this->start_time)),
1136 - 'enddt' => gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1137 - 'subject' => $bookingTitle,
1138 - 'path' => '/calendar/action/compose',
1139 - 'body' => $eventTitle,
1140 - 'rru' => 'addevent',
1141 - 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1142 - ], 'https://outlook.office.com/calendar/0/deeplink/compose'),
1367 + 'url' => 'https://outlook.office.com/calendar/0/deeplink/compose?' . $outlookParams,
1143 1368 'icon' => $assetsUrl . 'images/msoffice.svg'
1144 1369 ],
1145 1370 'other' => [
1146 1371 'title' => __('Other Calendar', 'fluent-booking'),
@@ -1149,5 +1374,5 @@
1149 1374 ]
1150 1375 ], $this);
1151 1376 }
1152 1377
1153 -}
1378 +}