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 +94 -27 2.1.1 → 2.5.0 View file →
@@ -1,8 +1,9 @@
1 1 <?php
2 2
3 3 namespace FluentBooking\App\Models;
4 4
5 +use FluentBooking\App\App;
5 6 use FluentBooking\App\Models\Model;
6 7 use FluentBooking\App\Services\BookingFieldService;
7 8 use FluentBooking\App\Services\LocationService;
8 9 use FluentBooking\App\Services\DateTimeHelper;
@@ -137,9 +138,11 @@
137 138 }
138 139
139 140 public static function assignNextGroupId()
140 141 {
141 - $lastEvent = static::orderBy('group_id', 'desc')->first(['group_id']);
142 + // Queue on one row the insert never touches; locking the max row alone deadlocks.
143 + App::getInstance('db')->table('options')->where('option_name', 'fcal_booking_group_lock')->lockForUpdate()->first();
144 + $lastEvent = static::orderBy('group_id', 'desc')->lockForUpdate()->first(['group_id']);
142 145
143 146 return $lastEvent ? $lastEvent->group_id + 1 : 1;
144 147 }
145 148
@@ -168,9 +171,9 @@
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 }
@@ -219,8 +222,29 @@
219 222 {
220 223 return $this->hosts()->pluck('user_id')->toArray();
221 224 }
222 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 +
223 247 public function scopeUpcoming($query)
224 248 {
225 249 return $query->where('end_time', '>=', gmdate('Y-m-d H:i:s')); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
226 250 }
@@ -266,17 +290,19 @@
266 290 ->where('status', 'scheduled');
267 291 }
268 292
269 293 if ($status == 'completed') {
270 - return $query->where('end_time', '<', gmdate('Y-m-d H:i:s')) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
271 - ->where('status', '!=', 'cancelled')
272 - ->where('status', '!=', 'rejected')
273 - ->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 + });
274 301 }
275 302
276 303 if ($status == 'cancelled') {
277 - return $query->where('status', 'cancelled')
278 - ->orWhere('status', 'rejected');
304 + return $query->whereIn('status', ['cancelled', 'rejected']);
279 305 }
280 306
281 307 if ($status == 'pending') {
282 308 return $query->whereIn('status', ['pending', 'reserved']);
@@ -377,9 +403,9 @@
377 403 }
378 404
379 405 public function getAllBookingShortTimes($timeZone = 'UTC', $withTimeZone = false)
380 406 {
381 - $otherBookings = self::where('parent_id', $this->id)->get();
407 + $otherBookings = $this->getOwnChildBookings();
382 408
383 409 $otherTimes = $otherBookings->map(function ($otherBooking) use ($timeZone, $withTimeZone) {
384 410 return $otherBooking->formatBookingDateTime($otherBooking->start_time, $timeZone) . ($withTimeZone ? ' (' . $timeZone . ')' : '');
385 411 })->toArray();
@@ -390,9 +416,9 @@
390 416 }
391 417
392 418 public function getAllBookingFullTimes($timeZone = 'UTC', $withTimeZone = false)
393 419 {
394 - $otherBookings = self::where('parent_id', $this->id)->get();
420 + $otherBookings = $this->getOwnChildBookings();
395 421
396 422 $otherTimes = $otherBookings->map(function ($otherBooking) use ($timeZone, $withTimeZone) {
397 423 return $otherBooking->getFullBookingDateTimeText($timeZone) . ($withTimeZone ? ' (' . $timeZone . ')' : '');
398 424 })->toArray();
@@ -401,8 +427,17 @@
401 427 $this->getFullBookingDateTimeText($timeZone) . ($withTimeZone ? ' (' . $timeZone . ')' : '')
402 428 ]);
403 429 }
404 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 +
405 440 public function getHostAndGuestDetailsHtml()
406 441 {
407 442 $authors = $this->getHostsDetails();
408 443
@@ -420,13 +455,13 @@
420 455 $authorListHtml = '<ul class="fcal_listed">';
421 456
422 457 foreach ($authors as $author) {
423 458 $authorBadge = ($author['id'] == $hostUserId) ? '<span class="fcal_host_badge">' . __('Host', 'fluent-booking') . '</span>' : '';
424 - $authorListHtml .= '<li class="fcal_host_name">' . $author['name'] . $authorBadge . '</li>';
459 + $authorListHtml .= '<li class="fcal_host_name">' . esc_html($author['name']) . $authorBadge . '</li>';
425 460 }
426 461
427 462 foreach ($guestNames as $guestName) {
428 - $authorListHtml .= '<li class="fcal_guest_name">' . $guestName . '</li>';
463 + $authorListHtml .= '<li class="fcal_guest_name">' . esc_html($guestName) . '</li>';
429 464 }
430 465 $authorListHtml .= '</ul>';
431 466
432 467 return $authorListHtml;
@@ -513,9 +548,11 @@
513 548 }
514 549
515 550 public function getLocationDetailsAttribute($locationDetails)
516 551 {
517 - return \maybe_unserialize($locationDetails);
552 + $value = \maybe_unserialize($locationDetails);
553 +
554 + return is_array($value) ? $value : [];
518 555 }
519 556
520 557 public function setOtherInfoAttribute($otherInfo)
521 558 {
@@ -521,9 +558,12 @@
521 558 {
522 559 $originalOtherInfo = $this->getOriginal('other_info');
523 560
524 561 $originalOtherInfo = \maybe_unserialize($originalOtherInfo);
562 + $originalOtherInfo = is_array($originalOtherInfo) ? $originalOtherInfo : [];
525 563
564 + $otherInfo = is_array($otherInfo) ? $otherInfo : (array) \maybe_unserialize($otherInfo);
565 +
526 566 foreach ($otherInfo as $key => $value) {
527 567 $originalOtherInfo[$key] = $value;
528 568 }
529 569
@@ -614,9 +654,9 @@
614 654 if ($isText) {
615 655 return $row->description;
616 656 }
617 657 if ($isHtml) {
618 - return wp_unslash($row->description);
658 + return esc_html(wp_unslash($row->description));
619 659 }
620 660 }
621 661
622 662 return $row;
@@ -632,9 +672,9 @@
632 672 if ($isText) {
633 673 return $row->description;
634 674 }
635 675 if ($isHtml) {
636 - return wp_unslash($row->description);
676 + return esc_html(wp_unslash($row->description));
637 677 }
638 678 }
639 679
640 680 return $row;
@@ -775,10 +815,15 @@
775 815 $bookingTitle = EditorShortCodeParser::parse($bookingTitle, $this);
776 816
777 817 $bookingTitle = $bookingTitle ?: $this->generateBookingTitle($eventTitle, $authorName, $guestName);
778 818
779 - if ($html && strpos($bookingTitle, $eventTitle) !== false) {
780 - $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 + }
781 826 }
782 827
783 828 return apply_filters('fluent_booking/booking_meeting_title', $bookingTitle, $authorName, $guestName, $calendarEvent, $this);
784 829 }
@@ -957,13 +1002,25 @@
957 1002 'type' => 'cancel',
958 1003 ], Helper::getBookingReceiptLandingBaseUrl());
959 1004 }
960 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 + */
961 1010 public function hasBookingAccess()
962 1011 {
963 - $hostIds = $this->getHostIds();
964 - $hasAccess = PermissionManager::userCan(['manage_all_data', 'manage_all_bookings']);
965 - 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());
966 1023 }
967 1024
968 1025 private function canPerformAction($settings)
969 1026 {
@@ -984,8 +1041,10 @@
984 1041
985 1042 $conditionTime = $conditionValue * 60;
986 1043 if ($conditionUnit == 'hours') {
987 1044 $conditionTime = $conditionTime * 60;
1045 + } elseif ($conditionUnit == 'days') {
1046 + $conditionTime = $conditionTime * 60 * 24;
988 1047 }
989 1048
990 1049 return $bookingStartTime - $currentTime > $conditionTime;
991 1050 }
@@ -1030,12 +1089,20 @@
1030 1089 public function getHostProfiles($public = true)
1031 1090 {
1032 1091 $hostIds = $this->getHostIds();
1033 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 +
1034 1102 $hosts = [];
1035 1103 foreach ($hostIds as $hostId) {
1036 - $calendar = Calendar::where('user_id', $hostId)->where('type', 'simple')->first();
1037 - if ($calendar) {
1104 + if ($calendar = $calendars->get($hostId)) {
1038 1105 $hosts[] = $calendar->getAuthorProfile($public);
1039 1106 }
1040 1107 }
1041 1108
@@ -1067,9 +1134,9 @@
1067 1134 if ($message) {
1068 1135 return $message;
1069 1136 }
1070 1137
1071 - return __('Sorry! you can not cancel this', 'fluent-booking');
1138 + return __('Sorry! you cannot cancel this', 'fluent-booking');
1072 1139 }
1073 1140
1074 1141 public function getRescheduleMessage()
1075 1142 {
@@ -1080,9 +1147,9 @@
1080 1147 if ($message) {
1081 1148 return $message;
1082 1149 }
1083 1150
1084 - return __('Sorry! you can not reschedule this', 'fluent-booking');
1151 + return __('Sorry! you cannot reschedule this', 'fluent-booking');
1085 1152 }
1086 1153
1087 1154 public function getHostDetails($isPublic = true, $hostId = null)
1088 1155 {
@@ -1200,9 +1267,9 @@
1200 1267 if (empty($data['value'])) {
1201 1268 continue;
1202 1269 }
1203 1270 $html .= '<tr>';
1204 - $html .= '<td><b>' . $data['label'] . '</b></td>';
1271 + $html .= '<td><b>' . esc_html($data['label']) . '</b></td>';
1205 1272 $html .= '<td>' . $data['value'] . '</td>';
1206 1273 $html .= '</tr>';
1207 1274 }
1208 1275 $html .= '</table>';
@@ -1286,9 +1353,9 @@
1286 1353
1287 1354 return apply_filters('fluent_booking/meeting_bookmarks', [
1288 1355 'google' => [
1289 1356 'title' => __('Google Calendar', 'fluent-booking'),
1290 - 'url' => 'https://calendar.google.com/calendar/r/eventedit?' . $googleParams,
1357 + 'url' => 'https://calendar.google.com/calendar/render?action=TEMPLATE&' . $googleParams,
1291 1358 'icon' => $assetsUrl . 'images/g-icon.svg'
1292 1359 ],
1293 1360 'outlook' => [
1294 1361 'title' => __('Outlook', 'fluent-booking'),
@@ -1307,5 +1374,5 @@
1307 1374 ]
1308 1375 ], $this);
1309 1376 }
1310 1377
1311 -}
1378 +}