PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.5
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.5
2.4.5 2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 All 69 releases
fluent-support / app / Services / Integrations / FluentBooking / BookingLinkManager.php

BookingLinkManager.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.4.5, at app/Services/Integrations/FluentBooking/BookingLinkManager.php

432 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App\Services\Integrations\FluentBooking;
4
5 use FluentSupport\App\Models\Conversation;
6 use FluentSupport\App\Models\Meta;
7 use FluentSupport\App\Models\Ticket;
8 use FluentSupport\App\Services\Helper;
9 use FluentSupport\Framework\Support\Arr;
10
11 class BookingLinkManager
12 {
13 /**
14 * Store a booking link record as ticket meta.
15 *
16 * @param Ticket $ticket
17 * @param array $metadata
18 * @return void
19 */
20 public function storeLink(Ticket $ticket, array $metadata)
21 {
22 Meta::create([
23 'object_type' => 'ticket',
24 'object_id' => (int) $ticket->id,
25 'key' => 'fluent_booking_link',
26 'value' => maybe_serialize($metadata)
27 ]);
28 }
29
30 /**
31 * Store the link token and event from a booking URL, skipping tokens already stored.
32 *
33 * @param Ticket $ticket
34 * @param object $person
35 * @param string $bookingUrl
36 * @return void
37 */
38 public function storeLinkFromUrl(Ticket $ticket, $person, $bookingUrl)
39 {
40 $bookingUrl = esc_url_raw($bookingUrl);
41 $linkToken = $this->getLinkTokenFromUrl($bookingUrl);
42
43 if (!$bookingUrl || !$linkToken || $this->hasStoredLink($ticket, $linkToken)) {
44 return;
45 }
46
47 $event = $this->getEventFromUrl($bookingUrl);
48
49 if (!$event || !$event->calendar || !FluentBookingService::canUseEvent($event)) {
50 return;
51 }
52
53 $this->storeLink($ticket, [
54 'event_type_id' => (int) $event->id,
55 'event_type_title' => sanitize_text_field($event->title),
56 'calendar_id' => (int) $event->calendar_id,
57 'calendar_title' => sanitize_text_field($event->calendar->title),
58 'booking_url' => $bookingUrl,
59 'link_token' => sanitize_key($linkToken),
60 'ticket_id' => (int) $ticket->id,
61 'customer_id' => (int) $ticket->customer_id,
62 'agent_id' => $person ? (int) $person->id : 0,
63 'created_at' => current_time('mysql')
64 ]);
65 }
66
67 /**
68 * @param Ticket $ticket
69 * @param string $linkToken
70 * @return bool
71 */
72 public function hasStoredLink(Ticket $ticket, $linkToken)
73 {
74 return (bool) $this->metaQuery()
75 ->where('object_id', (int) $ticket->id)
76 ->where('value', 'LIKE', '%' . sanitize_key($linkToken) . '%')
77 ->exists();
78 }
79
80 /**
81 * The ticket's 5 most recent booking link records.
82 *
83 * @param Ticket $ticket
84 * @return array
85 */
86 public function getLinksForTicket(Ticket $ticket)
87 {
88 return $this->metaQuery()
89 ->where('object_id', (int) $ticket->id)
90 ->orderBy('id', 'DESC')
91 ->limit(5)
92 ->get()
93 ->map(function ($meta) {
94 $link = Helper::safeUnserialize($meta->value);
95 return is_array($link) ? $link : [];
96 })
97 ->filter()
98 ->toArray();
99 }
100
101 /**
102 * One day before the oldest link's created_at, used to bound the booking query.
103 *
104 * @param array $generatedLinks
105 * @return string
106 */
107 public function getEarliestLinkDate($generatedLinks)
108 {
109 if (!$generatedLinks || !is_array($generatedLinks)) {
110 return '';
111 }
112
113 $dates = array_filter(array_map(function ($link) {
114 $createdAt = Arr::get($link, 'created_at');
115 return $createdAt && strtotime($createdAt) ? $createdAt : '';
116 }, $generatedLinks));
117
118 if (!$dates) {
119 return '';
120 }
121
122 return gmdate('Y-m-d H:i:s', strtotime(min($dates)) - DAY_IN_SECONDS);
123 }
124
125 /**
126 * Booking URLs carrying fs_booking_link_token found in the content.
127 *
128 * @param string $content
129 * @return array
130 */
131 public function extractLinksFromContent($content)
132 {
133 $content = html_entity_decode((string) $content, ENT_QUOTES, 'UTF-8');
134
135 if (!preg_match_all('/https?:\/\/[^\s"\']*fs_booking_link_token=[a-f0-9]{32}[^\s"\']*/i', $content, $matches)) {
136 return [];
137 }
138
139 return array_values(array_unique(array_map('esc_url_raw', $matches[0])));
140 }
141
142 /**
143 * @param string $url
144 * @return string
145 */
146 public function getLinkTokenFromUrl($url)
147 {
148 return sanitize_key(Arr::get($this->parseUrlQueryArgs($url), 'fs_booking_link_token'));
149 }
150
151 /**
152 * Resolve the event from the host/event query args, or from the last two path segments.
153 *
154 * @param string $bookingUrl
155 * @return \FluentBooking\App\Models\CalendarSlot|null
156 */
157 public function getEventFromUrl($bookingUrl)
158 {
159 $queryArgs = $this->parseUrlQueryArgs($bookingUrl);
160 $calendarSlug = sanitize_text_field(Arr::get($queryArgs, 'host'));
161 $eventSlug = sanitize_text_field(Arr::get($queryArgs, 'event'));
162
163 if (!$calendarSlug || !$eventSlug) {
164 $path = trim((string) wp_parse_url($bookingUrl, PHP_URL_PATH), '/');
165 $parts = array_values(array_filter(explode('/', $path)));
166
167 if (count($parts) >= 2) {
168 $eventSlug = sanitize_text_field(array_pop($parts));
169 $calendarSlug = sanitize_text_field(array_pop($parts));
170 }
171 }
172
173 if (!$calendarSlug || !$eventSlug) {
174 return null;
175 }
176
177 return \FluentBooking\App\Models\CalendarSlot::query()
178 ->with('calendar')
179 ->where('slug', $eventSlug)
180 ->whereHas('calendar', function ($query) use ($calendarSlug) {
181 $query->where('slug', $calendarSlug);
182 })
183 ->first();
184 }
185
186 /**
187 * Find the ticket a booking belongs to, by fs_ticket_id first, then by link token.
188 *
189 * @param object $booking
190 * @param array $bookingData
191 * @return Ticket|null
192 */
193 public function getTicketFromBooking($booking, $bookingData = [])
194 {
195 $queryArgs = $this->getBookingSourceQueryArgs($booking, $bookingData);
196 $ticketId = absint(Arr::get($queryArgs, 'fs_ticket_id'));
197
198 if ($ticketId) {
199 $ticket = Ticket::with('customer')->find($ticketId);
200
201 if ($ticket && $this->bookingMatchesTicket($booking, $ticket)) {
202 return $ticket;
203 }
204 }
205
206 $linkToken = sanitize_key(Arr::get($queryArgs, 'fs_booking_link_token'));
207
208 if ($linkToken) {
209 $ticket = $this->findTicketByToken($booking, $linkToken);
210
211 if ($ticket) {
212 return $ticket;
213 }
214 }
215
216 return null;
217 }
218
219 /**
220 * Whether the booking already has a timeline note on the ticket.
221 *
222 * @param object $booking
223 * @return bool
224 */
225 public function hasTimelineNote($booking)
226 {
227 return (bool) Conversation::where('message_id', 'fluent_booking_scheduled_' . (int) $booking->id)
228 ->where('source', 'fluent_booking')
229 ->exists();
230 }
231
232 /**
233 * Sanitised booking data for the frontend.
234 *
235 * @param object $booking
236 * @return array
237 */
238 public function formatBooking($booking)
239 {
240 $joinUrl = '';
241 $timezone = $this->getDisplayTimezone($booking);
242
243 if (method_exists($booking, 'getLocationAsText')) {
244 $locationText = $booking->getLocationAsText();
245 if (filter_var($locationText, FILTER_VALIDATE_URL)) {
246 $joinUrl = esc_url_raw($locationText);
247 }
248 }
249
250 return [
251 'id' => (int) $booking->id,
252 'event_title' => $booking->calendar_event ? sanitize_text_field($booking->calendar_event->title) : '',
253 'calendar_title' => $booking->calendar ? sanitize_text_field($booking->calendar->title) : '',
254 'status' => sanitize_key($booking->status),
255 'status_label' => sanitize_text_field(method_exists($booking, 'getStatusLabel') ? $booking->getStatusLabel() : ucwords(str_replace('_', ' ', $booking->status))),
256 'start_time' => sanitize_text_field($booking->start_time),
257 'local_start_time' => sanitize_text_field($this->convertTime($booking->start_time, $timezone)),
258 'timezone' => sanitize_text_field($timezone),
259 'date_text' => sanitize_text_field(method_exists($booking, 'getFullBookingDateTimeText') ? $booking->getFullBookingDateTimeText($timezone) : $booking->start_time),
260 'admin_url' => method_exists($booking, 'getAdminViewUrl') ? esc_url_raw($booking->getAdminViewUrl()) : '',
261 'join_url' => esc_url_raw($joinUrl),
262 'internal_note' => !empty($booking->internal_note) ? sanitize_textarea_field(wp_strip_all_tags($booking->internal_note)) : '',
263 ];
264 }
265
266 /**
267 * @return \FluentSupport\Framework\Database\Orm\Builder
268 */
269 private function metaQuery()
270 {
271 return Meta::where('object_type', 'ticket')
272 ->where('key', 'fluent_booking_link');
273 }
274
275 /**
276 * @param string $url
277 * @return array
278 */
279 private function parseUrlQueryArgs($url)
280 {
281 $query = wp_parse_url($url, PHP_URL_QUERY);
282
283 if (!$query) {
284 return [];
285 }
286
287 parse_str($query, $queryArgs);
288
289 return is_array($queryArgs) ? $queryArgs : [];
290 }
291
292 /**
293 * Query args of the booking's source URL, preferring $bookingData['source_url'].
294 *
295 * @param object $booking
296 * @param array $bookingData
297 * @return array
298 */
299 private function getBookingSourceQueryArgs($booking, $bookingData = [])
300 {
301 $sourceUrl = Arr::get($bookingData, 'source_url') ?: (!empty($booking->source_url) ? $booking->source_url : '');
302
303 return $sourceUrl ? $this->parseUrlQueryArgs($sourceUrl) : [];
304 }
305
306 /**
307 * @param object $booking
308 * @param Ticket $ticket
309 * @return bool
310 */
311 private function bookingEmailMatchesTicket($booking, Ticket $ticket)
312 {
313 return $ticket->customer
314 && !empty($booking->email)
315 && strtolower(sanitize_email($ticket->customer->email)) === strtolower(sanitize_email($booking->email));
316 }
317
318 /**
319 * Email matches and the ticket has a stored link for this event.
320 *
321 * @param object $booking
322 * @param Ticket $ticket
323 * @return bool
324 */
325 private function bookingMatchesTicket($booking, Ticket $ticket)
326 {
327 return $this->bookingEmailMatchesTicket($booking, $ticket)
328 && $this->ticketHasGeneratedEventLink($ticket, (int) $booking->event_id);
329 }
330
331 /**
332 * @param Ticket $ticket
333 * @param int $eventId
334 * @return bool
335 */
336 private function ticketHasGeneratedEventLink(Ticket $ticket, $eventId)
337 {
338 foreach ($this->getLinksForTicket($ticket) as $link) {
339 if ((int) Arr::get($link, 'event_type_id') === (int) $eventId) {
340 return true;
341 }
342 }
343
344 return false;
345 }
346
347 /**
348 * First ticket holding this link token whose customer email and event match the booking.
349 *
350 * @param object $booking
351 * @param string $linkToken
352 * @return Ticket|null
353 */
354 private function findTicketByToken($booking, $linkToken)
355 {
356 $linkToken = sanitize_key($linkToken);
357
358 if (!$linkToken) {
359 return null;
360 }
361
362 $metaRows = $this->metaQuery()
363 ->where('value', 'LIKE', '%' . $linkToken . '%')
364 ->orderBy('id', 'DESC')
365 ->limit(5)
366 ->get();
367
368 foreach ($metaRows as $meta) {
369 $link = Helper::safeUnserialize($meta->value);
370
371 if (!is_array($link) || sanitize_key(Arr::get($link, 'link_token')) !== $linkToken) {
372 continue;
373 }
374
375 $ticket = Ticket::with('customer')->find((int) $meta->object_id);
376
377 if ($ticket && $this->bookingMatchesGeneratedLink($booking, $ticket, $link)) {
378 return $ticket;
379 }
380 }
381
382 return null;
383 }
384
385 /**
386 * @param object $booking
387 * @param Ticket $ticket
388 * @param array $link
389 * @return bool
390 */
391 private function bookingMatchesGeneratedLink($booking, Ticket $ticket, array $link)
392 {
393 return $this->bookingEmailMatchesTicket($booking, $ticket)
394 && (int) Arr::get($link, 'event_type_id') === (int) $booking->event_id;
395 }
396
397 /**
398 * Calendar author's timezone, then the host's, then the site's.
399 *
400 * @param object $booking
401 * @return string
402 */
403 private function getDisplayTimezone($booking)
404 {
405 if ($booking->calendar && !empty($booking->calendar->author_timezone)) {
406 return $booking->calendar->author_timezone;
407 }
408
409 if (method_exists($booking, 'getHostTimezone') && $timezone = $booking->getHostTimezone()) {
410 return $timezone;
411 }
412
413 return wp_timezone_string();
414 }
415
416 /**
417 * Convert a UTC time to $timezone.
418 *
419 * @param string $time
420 * @param string $timezone
421 * @return string
422 */
423 private function convertTime($time, $timezone)
424 {
425 if (!$time) {
426 return $time;
427 }
428
429 return \FluentBooking\App\Services\DateTimeHelper::convertFromUtc($time, $timezone, 'Y-m-d H:i:s');
430 }
431 }
432