PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.0
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 1.5.5 All 68 releases
fluent-support / app / Services / Helper.php

Helper.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.4.0, at app/Services/Helper.php

1,987 lines 73.9 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;
4
5 use FluentSupport\App\App;
6 use FluentSupport\App\Models\Agent;
7 use FluentSupport\App\Models\Attachment;
8 use FluentSupport\App\Models\Ticket;
9 use FluentSupport\App\Models\Conversation;
10 use FluentSupport\App\Models\Customer;
11 use FluentSupport\App\Models\MailBox;
12 use FluentSupport\App\Models\Meta;
13 use FluentSupport\App\Models\AIActivityLogs;
14 use FluentSupport\App\Models\Person;
15 use FluentSupport\App\Models\Product;
16 use FluentSupport\App\Services\Includes\UploadService;
17 use FluentSupport\App\Services\EmailNotification\Settings;
18 use FluentSupport\Framework\Support\Arr;
19
20 /**
21 * Helper - REST API Helper Class
22 *
23 * App helper for REST API
24 *
25 * @package FluentSupport\App\Services
26 *
27 * @version 1.0.0
28 */
29 class Helper
30 {
31 public static function FluentSupport($module = null)
32 {
33 return App::getInstance($module);
34 }
35
36 /**
37 * Get agent information by user id
38 * The function will get user id as parameter or get id from session and return agent information
39 * @param null $userId
40 * @return false | Agent
41 */
42 public static function getAgentByUserId($userId = null)
43 {
44 if ($userId === null) {
45 $userId = get_current_user_id();
46 }
47 if (!$userId) {
48 return false;
49 }
50 return Agent::where('user_id', $userId)->first();
51 }
52
53 /**
54 * This function will return the list of ticket priorities list for customer
55 *
56 * @return mixed
57 */
58 public static function customerTicketPriorities()
59 {
60 return apply_filters('fluent_support/customer_ticket_priorities', [
61 'normal' => __('Normal', 'fluent-support'),
62 'medium' => __('Medium', 'fluent-support'),
63 'critical' => __('Critical', 'fluent-support')
64 ]);
65 }
66
67 /**
68 * This function will return the list of ticket priorities list for Admin
69 *
70 * @return mixed
71 */
72 public static function adminTicketPriorities()
73 {
74 return apply_filters('fluent_support/admin_ticket_priorities', [
75 'normal' => __('Normal', 'fluent-support'),
76 'medium' => __('Medium', 'fluent-support'),
77 'critical' => __('Critical', 'fluent-support')
78 ]);
79 }
80
81
82 /**
83 * This function will return ticket status group
84 *
85 * @return mixed
86 */
87 public static function ticketStatusGroups()
88 {
89 return apply_filters('fluent_support/ticket_status_groups', [
90 'open' => ['new', 'active'],
91 'active' => ['active'],
92 'closed' => ['closed'],
93 'new' => ['new'],
94 'all' => []
95 ]);
96 }
97
98 /**
99 * This function will return custom ticket status group
100 *
101 * @return mixed
102 */
103 public static function changeableTicketStatuses()
104 {
105 $ticketStatus = static::ticketStatusGroups();
106
107 unset($ticketStatus['all']);
108 unset($ticketStatus['open']);
109
110 return apply_filters('fluent_support/changeable_ticket_statuses', $ticketStatus);
111 }
112
113 /**
114 * This function will return ticket status list
115 *
116 * @return mixed
117 */
118 public static function ticketStatuses()
119 {
120 return apply_filters('fluent_support/ticket_statuses', [
121 'new' => __('New', 'fluent-support'),
122 'active' => __('Active', 'fluent-support'),
123 'closed' => __('Closed', 'fluent-support'),
124 ]);
125 }
126
127 public static function getTkStatusesByGroupName($groupName)
128 {
129 $groups = self::ticketStatusGroups();
130 return Arr::get($groups, $groupName, []);
131 }
132
133 public static function ticketAcceptedFileMiles()
134 {
135 $groups = self::getMimeGroups();
136 $globalSettings = (new Settings())->globalBusinessSettings();
137
138 if (empty($globalSettings['accepted_file_types'])) {
139 return apply_filters('fluent_support/accepted_ticket_mimes', []);
140 }
141
142 $mimes = [];
143 $typesGroups = Arr::only($groups, $globalSettings['accepted_file_types']);
144 foreach ($typesGroups as $mimesGroup) {
145 $mimes = array_merge($mimes, $mimesGroup['mimes']);
146 }
147
148 return apply_filters('fluent_support/accepted_ticket_mimes', $mimes);
149 }
150
151 public static function getAcceptedMimeHeadings()
152 {
153 $groups = self::getMimeGroups();
154 $globalSettings = (new Settings())->globalBusinessSettings();
155
156 if (empty($globalSettings['accepted_file_types'])) {
157 return [];
158 }
159
160 $mimeNames = [];
161 $typesGroups = Arr::only($groups, $globalSettings['accepted_file_types']);
162 foreach ($typesGroups as $mimesGroup) {
163 $mimeNames[] = $mimesGroup['title'];
164 }
165
166 return $mimeNames;
167 }
168
169 public static function getFileUploadMessage()
170 {
171 $mimeHeadings = self::getAcceptedMimeHeadings();
172 $settings = (new Settings())->globalBusinessSettings();
173 $maxFileSize = floatval($settings['max_file_size']);
174
175 // translators: %1$s is a comma-separated list of supported file types, %2$.01f is the maximum file size in megabytes
176 return sprintf(__('Supported Types: %1$s and max file size: %2$.01fMB', 'fluent-support'), implode(', ', $mimeHeadings), $maxFileSize);
177 }
178
179 public static function getMimeGroups()
180 {
181 return apply_filters('fluent_support/mime_groups', [
182 'images' => [
183 'title' => __('Photos', 'fluent-support'),
184 'mimes' => [
185 'image/gif',
186 'image/ief',
187 'image/jpeg',
188 'image/webp',
189 'image/pjpeg',
190 'image/ktx',
191 'image/png'
192 ]
193 ],
194 'csv' => [
195 'title' => __('CSV', 'fluent-support'),
196 'mimes' => [
197 'application/csv',
198 'application/txt',
199 'text/csv',
200 'text/plain',
201 'text/comma-separated-values',
202 'text/anytext',
203 ]
204 ],
205 'documents' => [
206 'title' => __('PDF/Docs', 'fluent-support'),
207 'mimes' => [
208 'application/excel',
209 'application/vnd.ms-excel',
210 'application/vnd.msexcel',
211 'application/octet-stream',
212 'application/pdf',
213 'application/msword',
214 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
215 ]
216 ],
217 'zip' => [
218 'title' => __('Zip', 'fluent-support'),
219 'mimes' => [
220 'application/zip'
221 ]
222 ],
223 'json' => [
224 'title' => __('JSON', 'fluent-support'),
225 'mimes' => [
226 'application/json',
227 'application/jsonml+json'
228 ]
229 ]
230 ]);
231 }
232
233 /**
234 * getOption method will return settings using key
235 * This method will get key as parameter, fetch data from database, beautify the data and return
236 * @param $key
237 * @param string $default
238 * @return mixed|string
239 */
240 public static function getOption($key, $default = '')
241 {
242 //Get settings from meta table using the key
243 $data = Meta::where('object_type', 'option')
244 ->where('key', $key)
245 ->first();
246
247 if ($data) {
248 $value = static::safeUnserialize($data->value);
249 if ($value) {
250 return $value;
251 }
252 }
253
254 return $default;
255 }
256
257 /**
258 * updateOption method will update or insert settings
259 * This method will get key and value as parameter, check exists or not. If exist update value by key, else insert value for the key
260 * @param $key
261 * @param $value
262 * @return mixed
263 */
264 public static function updateOption($key, $value)
265 {
266 //Get settings from meta table using the key
267 $data = Meta::where('object_type', 'option')
268 ->where('key', $key)
269 ->first();
270
271 //If data is available, update existing data and return
272 if ($data) {
273 return Meta::where('id', $data->id)
274 ->update([
275 'value' => maybe_serialize($value)
276 ]);
277 }
278
279 //If newly submit, create new record and return
280 return Meta::insert([
281 'object_type' => 'option',
282 'key' => $key,
283 'value' => maybe_serialize($value)
284 ]);
285 }
286
287 /** @internal Not called from core controllers — available for Pro/hook usage. */
288 public static function deleteOption($key)
289 {
290 return Meta::where('object_type', 'option')
291 ->where('key', $key)
292 ->delete();
293 }
294
295 /**
296 * getIntegrationOption method will return the integration settings by integration key
297 * @param $key
298 * @param string $default
299 * @return mixed|string
300 */
301 public static function getIntegrationOption($key, $default = '')
302 {
303 $data = Meta::where('object_type', 'integration_settings')
304 ->where('key', $key)
305 ->first();
306
307 if ($data) {
308 $value = static::safeUnserialize($data->value);
309 if ($value) {
310 return $value;
311 }
312 }
313
314 return $default;
315 }
316
317 /**
318 * updateIntegrationOption method will update existing settings or create new settings by integration key
319 * @param $key
320 * @param $value
321 * @return mixed
322 */
323 public static function updateIntegrationOption($key, $value)
324 {
325 $data = Meta::where('object_type', 'integration_settings')
326 ->where('key', $key)
327 ->first();
328
329 if ($data) {
330 return Meta::where('id', $data->id)
331 ->update([
332 'value' => maybe_serialize($value)
333 ]);
334 }
335
336 return Meta::insert([
337 'object_type' => 'integration_settings',
338 'key' => $key,
339 'value' => maybe_serialize($value)
340 ]);
341 }
342
343 public static function getTicketViewUrl($ticket)
344 {
345 $baseUrl = self::getPortalBaseUrl();
346 $ticketNumber = $ticket->serial_number ?? $ticket->id;
347 return $baseUrl . '/#/ticket/' . $ticketNumber . '/view';
348 }
349
350 public static function getTicketViewSignedUrl($ticket)
351 {
352 if (!self::isPublicSignedTicketEnabled()) {
353 return self::getTicketViewUrl($ticket);
354 }
355
356 $baseUrl = self::getPortalBaseUrl();
357 $ticketNumber = $ticket->serial_number ?? $ticket->id;
358
359 $baseUrl = add_query_arg([
360 'fs_view' => 'ticket',
361 'support_hash' => $ticket->hash,
362 'ticket_id' => $ticketNumber,
363 ], $baseUrl);
364
365 return $baseUrl . '#/ticket/' . $ticketNumber . '/view';
366 }
367
368 public static function saveOpenAIData($objectType, $key, $data)
369 {
370 $serializedData = maybe_serialize($data);
371
372 $previousValue = Meta::where('object_type', $objectType)->first();
373
374 if ($previousValue) {
375 return Meta::where('object_type', $objectType)->update([
376 'value' => $serializedData
377 ]);
378 } else {
379 return Meta::insert([
380 'object_type' => $objectType,
381 'key' => $key,
382 'value' => $serializedData
383 ]);
384 }
385
386 }
387
388 public static function saveAIProviderSettings(array $data)
389 {
390 if (!empty($data['api_key'])) {
391 $data['api_key'] = static::encryptApiKey($data['api_key']);
392 }
393
394 $serializedData = maybe_serialize($data);
395 $previous = Meta::where('object_type', '_fs_ai_provider_settings')->first();
396
397 if ($previous) {
398 return Meta::where('object_type', '_fs_ai_provider_settings')->update([
399 'value' => $serializedData,
400 ]);
401 }
402
403 return Meta::insert([
404 'object_type' => '_fs_ai_provider_settings',
405 'key' => '_fs_ai_provider_data',
406 'value' => $serializedData,
407 ]);
408 }
409
410 public static function getAIProviderSettings(): array
411 {
412 $record = Meta::where('object_type', '_fs_ai_provider_settings')->first();
413
414 if (!$record) {
415 $record = Meta::where('object_type', '_fs_openai_settings')->first();
416 }
417
418 if (!$record) {
419 return [];
420 }
421
422 $settings = static::safeUnserialize($record->value);
423
424 if (empty($settings) || !is_array($settings)) {
425 return [];
426 }
427
428 if (!isset($settings['provider'])) {
429 $settings['provider'] = 'openai';
430 }
431
432 if (!empty($settings['api_key'])) {
433 $settings['api_key'] = static::decryptApiKey($settings['api_key']);
434 }
435
436 return $settings;
437 }
438
439 public static function encryptApiKey(string $apiKey): string
440 {
441 if (!extension_loaded('openssl')) {
442 return $apiKey;
443 }
444
445 $cipher = 'AES-256-CBC';
446 $key = substr(hash('sha256', AUTH_KEY . SECURE_AUTH_KEY, true), 0, 32);
447 $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher));
448 $encrypted = openssl_encrypt($apiKey, $cipher, $key, 0, $iv);
449
450 if ($encrypted === false) {
451 return $apiKey;
452 }
453
454 return 'fsai:' . base64_encode($iv . $encrypted);
455 }
456
457 public static function decryptApiKey(string $value): string
458 {
459 if (!extension_loaded('openssl') || strncmp($value, 'fsai:', 5) !== 0) {
460 return $value;
461 }
462
463 $cipher = 'AES-256-CBC';
464 $decoded = base64_decode(substr($value, 5));
465 $ivLen = openssl_cipher_iv_length($cipher);
466
467 if (strlen($decoded) <= $ivLen) {
468 return $value;
469 }
470
471 $key = substr(hash('sha256', AUTH_KEY . SECURE_AUTH_KEY, true), 0, 32);
472 $iv = substr($decoded, 0, $ivLen);
473 $decrypted = openssl_decrypt(substr($decoded, $ivLen), $cipher, $key, 0, $iv);
474
475 return $decrypted !== false ? $decrypted : $value;
476 }
477
478 public static function authorizeChatGPTAPIKey($data)
479 {
480 return wp_remote_get('https://api.openai.com/v1/models', [
481 'headers' => [
482 'Authorization' => 'Bearer ' . $data['api_key'],
483 'Content-Type' => 'application/json'
484 ]
485 ]);
486 }
487
488 public static function isPublicSignedTicketEnabled()
489 {
490 $businessSettings = self::getBusinessSettings();
491
492 return (Arr::get($businessSettings, 'disable_public_ticket') != 'yes');
493 }
494
495 public static function getTicketAdminUrl($ticket)
496 {
497 $baseUrl = self::getPortalAdminBaseUrl();
498 return $baseUrl . 'tickets/' . $ticket->id . '/view';
499 }
500
501 /**
502 * getPortalBaseUrl will get the portal page id and return link of the page
503 * @return mixed
504 */
505 public static function getPortalBaseUrl()
506 {
507 $businessSettings = self::getBusinessSettings();
508 $portalType = Arr::get($businessSettings, 'ticket_link_portal', 'default');
509 $baseUrl = null;
510
511 if (self::isPortalActive($portalType)) {
512 if ($portalType === 'woocommerce' && function_exists('wc_get_endpoint_url') && function_exists('wc_get_page_permalink')) {
513 $accountUrl = wc_get_page_permalink('myaccount');
514 if ($accountUrl && $accountUrl !== '#') {
515 $baseUrl = wc_get_endpoint_url('support-tickets', '', $accountUrl);
516 }
517 } elseif ($portalType === 'fluent_cart') {
518 $baseUrl = \FluentCart\App\Services\URL::getCustomerDashboardUrl('fluent-support') ?: null;
519 } elseif ($portalType === 'fluent_community') {
520 $baseUrl = \FluentCommunity\App\Services\Helper::baseUrl('support/') ?: null;
521 }
522 }
523
524 if (!$baseUrl) {
525 $baseUrl = get_permalink(absint(Arr::get($businessSettings, 'portal_page_id')));
526 }
527
528 return apply_filters('fluent_support/portal_base_url', rtrim((string) $baseUrl, '/\\'));
529 }
530
531 public static function isPortalActive($portalType)
532 {
533 if ($portalType === 'woocommerce') {
534 return defined('FLUENTSUPPORTPRO_PLUGIN_VERSION') && defined('WC_PLUGIN_FILE');
535 }
536
537 if ($portalType === 'fluent_cart') {
538 return defined('FLUENTCART_VERSION');
539 }
540
541 if ($portalType === 'fluent_community') {
542 return defined('FLUENT_COMMUNITY_PLUGIN_VERSION');
543 }
544
545 return false;
546 }
547
548 public static function getPortalAdminBaseUrl()
549 {
550 return apply_filters('fluent_support/portal_admin_base_url', admin_url('admin.php?page=fluent-support/#/'));
551 }
552
553 public static function getBusinessSettings($key = null)
554 {
555 static $settings;
556
557 if ($settings && $key) {
558 return Arr::get($settings, $key);
559 }
560
561 if ($settings) {
562 return $settings;
563 }
564
565 $settings = (new Settings())->globalBusinessSettings();
566
567 if ($key) {
568 return Arr::get($settings, $key);
569 }
570 return $settings;
571 }
572
573 public static function isAgentFeedbackEnabled()
574 {
575 return self::getBusinessSettings('agent_feedback_rating', 'no') == 'yes';
576 }
577
578 public static function getTicketMeta($ticketId, $key, $default = '')
579 {
580 $data = Meta::where('object_type', 'ticket_meta')
581 ->where('key', $key)
582 ->where('object_id', $ticketId)
583 ->first();
584
585 if ($data) {
586 $value = static::safeUnserialize($data->value);
587 if ($value) {
588 return $value;
589 }
590 }
591
592 return $default;
593 }
594
595 public static function updateTicketMeta($ticketId, $key, $value)
596 {
597 $data = Meta::where('object_type', 'ticket_meta')
598 ->where('key', $key)
599 ->where('object_id', $ticketId)
600 ->first();
601
602 if ($data) {
603 return Meta::where('id', $data->id)
604 ->update([
605 'value' => maybe_serialize($value)
606 ]);
607 }
608
609 return Meta::insert([
610 'object_type' => 'ticket_meta',
611 'object_id' => $ticketId,
612 'key' => $key,
613 'value' => maybe_serialize($value)
614 ]);
615 }
616
617 public static function getWPPages()
618 {
619 $pages = (self::FluentSupport())->app->db
620 ->table('posts')
621 ->select(['ID', 'post_title'])
622 ->where('post_type', 'page')
623 ->where('post_status', 'publish')
624 ->latest('ID')
625 ->get();
626 $formattedPages = [];
627 foreach ($pages as $page) {
628 $formattedPages[] = [
629 'id' => intval($page->ID),
630 'title' => $page->post_title ?: __('(no title)', 'fluent-support')
631 ];
632 }
633 return $formattedPages;
634 }
635
636 public static function getDefaultMailBox()
637 {
638 $mailbox = MailBox::where('is_default', 'yes')->first();
639
640 if ($mailbox) {
641 return $mailbox;
642 }
643
644 return MailBox::oldest('id')->first();
645 }
646
647 public static function getCurrentAgent()
648 {
649 // If user is logged in then return the agent by user id.
650 // This `get_current_user_id` function is WP function and
651 // it returns user id if user is logged in.
652 if (get_current_user_id()) {
653 return Agent::where('user_id', get_current_user_id())->first();
654 }
655 }
656
657 public static function getCurrentCustomer()
658 {
659 // If user is logged in then return the customer by user id.
660 // This `get_current_user_id` function is WP function and
661 // it returns user id if user is logged in.
662 if (get_current_user_id()) { //if user is logged in
663 // Ordered for the same reason getCurrentPerson() and
664 // Customer::getCustomerFromData() are: the user_id column carries a
665 // plain index, not a unique one, so one account can have more than
666 // one customer record. Without an order the record that wins is
667 // whatever the storage engine returns first, and this helper could
668 // disagree with the other two about which record a request belongs
669 // to.
670 return Customer::where('user_id', get_current_user_id())
671 ->orderBy('id', 'ASC')
672 ->first();
673 }
674 }
675
676 public static function getCurrentPerson()
677 {
678 // If user is logged in then return the person(agent/customer) by user id.
679 // This `get_current_user_id` function is WP function and
680 // it returns user id if user is logged in.
681 if (get_current_user_id()) {
682 return Person::where('user_id', get_current_user_id())
683 ->orderBy('id', 'ASC')
684 ->first();
685 }
686 return null;
687 }
688
689 public static function getCustomerByID($customerid)
690 {
691 return Customer::where('id', $customerid)->first();
692 }
693
694 /** @internal Not called from core controllers — available for Pro/hook usage. */
695 public static function sanitizeOrderValue($orderType = '')
696 {
697 $orderBys = ['ASC', 'DESC'];
698
699 $orderType = trim(strtoupper((string)($orderType ?? '')));
700
701 return in_array($orderType, $orderBys) ? $orderType : 'DESC';
702 }
703
704 public static function getFluentCRMTagConfig()
705 {
706 if (!defined('FLUENTCRM')) {
707 return [
708 'can_add_tags' => false,
709 'tags' => [],
710 'lists' => [],
711 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluentcrm-logo.svg',
712 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluent-crm-icon.png',
713 ];
714 }
715
716 $canAddTags = \FluentCrm\App\Services\PermissionManager::currentUserCan('fcrm_manage_contacts');
717
718 $canAddTags = apply_filters('fluent_support/can_user_add_tags_to_customer', $canAddTags);
719 $crmTags = [];
720 $crmLists = [];
721 if ($canAddTags) {
722 $crmTags = \FluentCrm\App\Models\Tag::select(['id', 'title'])->oldest('title')->get();
723 $crmLists = \FluentCrm\App\Models\Lists::select(['id', 'title'])->oldest('title')->get();
724 }
725
726 $crmConfigs = [
727 'can_add_tags' => $canAddTags,
728 'tags' => $crmTags,
729 'lists' => $crmLists,
730 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluentcrm-logo.svg',
731 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluent-crm-icon.png',
732 ];
733
734 if (defined('FLUENTCRM')) {
735 $crmConfigs['contacts'] = []; //(new \FluentCrm\App\Models\Subscriber)->get();
736 }
737
738 return $crmConfigs;
739 }
740
741 /**
742 * getFluentCrmContactData method will get information from fluent crm using user email
743 * @param $customer
744 * @return array|false
745 */
746 public static function getFluentCrmContactData($customer)
747 {
748 if (!defined('FLUENTCRM')) {
749 return false;
750 }
751 //Get contact info from FluentCRM using customer email
752 $contact = \FluentCrmApi('contacts')->getContactByUserRef($customer->email);
753 if ($contact) {
754 $tags = $contact->tags;
755 $lists = $contact->lists;
756 $urlBase = apply_filters('fluentcrm_menu_url_base', admin_url('admin.php?page=fluentcrm-admin#/'));
757 $crmProfileUrl = $urlBase . 'subscribers/' . $contact->id;
758
759 //Return contact data
760 return [
761 'id' => $contact->id,
762 'first_name' => $contact->first_name,
763 'last_name' => $contact->last_name,
764 'full_name' => $contact->full_name,
765 'name_mismatch' => $contact->full_name != $customer->full_name,
766 'tags' => $tags,
767 'lists' => $lists,
768 'status' => $contact->status,
769 'stats' => $contact->stats(),
770 'view_url' => $crmProfileUrl
771 ];
772 }
773
774 return false;
775 }
776
777 public static function openAIIntegrationStatus()
778 {
779 $settings = static::getAIProviderSettings();
780
781 if (($settings['enabled'] ?? 'yes') === 'no') {
782 return false;
783 }
784
785 return !empty($settings['api_key']);
786 }
787
788 public static function fluentBotIntegrationStatus()
789 {
790 // Include object_id and order by id desc to always read the latest row; the
791 // write path (saveFluentBotSettings) does not prune siblings, so duplicates
792 // may exist and a non-deterministic read can return stale state.
793 $meta = Meta::where([
794 'object_type' => 'fluent_bot_settings',
795 'object_id' => 1,
796 'key' => '_fs_fluent_bot_config'
797 ])->orderByDesc('id')->first();
798
799 $settings = static::safeUnserialize($meta ? $meta->value : null);
800
801 return isset($settings['isEnabled']) && filter_var($settings['isEnabled'], FILTER_VALIDATE_BOOLEAN);
802 }
803
804
805 public static function showTicketSummaryAdminBar()
806 {
807 $data = self::getOption('global_business_settings');
808
809 if ($data && isset($data["enable_admin_bar_summary"]) && $data["enable_admin_bar_summary"] == 'yes') {
810 return true;
811 }
812
813 return false;
814 }
815
816 public static function generateMessageID($email)
817 {
818 if (!$email || !is_string($email)) {
819 return false;
820 }
821
822 $emailParts = explode('@', $email);
823 if (count($emailParts) != 2) {
824 return false;
825 }
826
827 $emailDomain = $emailParts[1];
828 try {
829 return sprintf(
830 "<%s.%s@%s>",
831 base_convert((int)microtime(true), 10, 36),
832 base_convert(bin2hex(openssl_random_pseudo_bytes(8)), 16, 36),
833 $emailDomain
834 );
835 } catch (\Exception $exception) {
836 return false;
837 }
838 }
839
840 public static function getExportOptions()
841 {
842 $data = [
843 'Agent First Name' => __('Agent First Name', 'fluent-support'),
844 'Agent Last Name' => __('Agent Last Name', 'fluent-support'),
845 'Agent Full Name' => __('Agent Full Name', 'fluent-support'),
846 'Responses' => __('Responses', 'fluent-support'),
847 'Interactions' => __('Interactions', 'fluent-support'),
848 'Open Tickets' => __('Open Tickets', 'fluent-support'),
849 'Closed' => __('Closed', 'fluent-support'),
850 'Waiting Tickets' => __('Waiting Tickets', 'fluent-support'),
851 'Average Waiting' => __('Average Waiting', 'fluent-support'),
852 'Max Waiting' => __('Max Waiting', 'fluent-support'),
853 ];
854
855 if (Helper::isAgentFeedbackEnabled()) {
856 $data['Likes'] = __('Likes', 'fluent-support');
857 $data['Dislikes'] = __('Dislikes', 'fluent-support');
858 }
859
860 return $data;
861 }
862
863 public static function getAuthProvider()
864 {
865 if (defined('FLUENT_AUTH_PLUGIN_PATH')) {
866 $settings = \FluentAuth\App\Helpers\Helper::getAuthFormsSettings();
867 if ($settings['enabled'] == 'yes') {
868 return 'fluent_auth';
869 }
870 }
871
872 return 'fluent_support';
873 }
874
875 /** @internal Not called from core controllers — available for Pro/hook usage. */
876 public static function getDriversKey(){
877 return [
878 'dropbox_settings',
879 'google_drive_settings',
880 'cloudflare_r2_settings',
881 'amazon_s3_settings',
882 'local'
883 ];
884 }
885
886
887 public static function getUploadDriverKey()
888 {
889 if (!defined('FLUENTSUPPORTPRO')) {
890 return 'local';
891 }
892
893 $driver = self::getOption('file_upload_driver');
894
895 if ($driver) {
896 return $driver;
897 }
898
899 // Now guess the driver and save it
900
901 // check if dropbox is enabled
902 $dropboxSettings = self::getIntegrationOption('dropbox_settings', null);
903 if ($dropboxSettings) {
904 $dropBoxEnabled = Meta::where('object_type', 'enabled_upload_drivers')
905 ->where('key', 'dropbox_settings')
906 ->where('value', 'yes')
907 ->first();
908
909 if ($dropBoxEnabled) {
910 $driver = 'dropbox';
911 self::updateOption('file_upload_driver', $driver);
912 return $driver;
913 }
914 }
915
916 // check if google drive is enabled
917 $googleDriveSettings = self::getIntegrationOption('google_drive_settings', null);
918
919 if ($googleDriveSettings) {
920 $googleDriveEnabled = Meta::where('object_type', 'enabled_upload_drivers')
921 ->where('key', 'google_drive_settings')
922 ->where('value', 'yes')
923 ->first();
924
925 if ($googleDriveEnabled) {
926 $driver = 'google_drive';
927 self::updateOption('file_upload_driver', $driver);
928 return $driver;
929 }
930 }
931
932 // check if cloudflare r2 is enabled
933 $cloudflareR2Settings = self::getIntegrationOption('cloudflare_r2_settings', null);
934
935 if ($cloudflareR2Settings) {
936 $cloudflareR2Enabled = Meta::where('object_type', 'enabled_upload_drivers')
937 ->where('key', 'cloudflare_r2_settings')
938 ->where('value', 'yes')
939 ->first();
940
941 if ($cloudflareR2Enabled) {
942 $driver = 'cloudflare_r2';
943 self::updateOption('file_upload_driver', $driver);
944 return $driver;
945 }
946 }
947
948 // check if amazon s3 is enabled
949 $amazonS3Settings = self::getIntegrationOption('amazon_s3_settings', null);
950
951 if ($amazonS3Settings) {
952 $amazonS3Enabled = Meta::where('object_type', 'enabled_upload_drivers')
953 ->where('key', 'amazon_s3_settings')
954 ->where('value', 'yes')
955 ->first();
956
957 if ($amazonS3Enabled) {
958 $driver = 'amazon_s3';
959 self::updateOption('file_upload_driver', $driver);
960 return $driver;
961 }
962 }
963
964 self::updateOption('file_upload_driver', 'local');
965 return 'local';
966 }
967
968 public static function getIntegrationStatuses()
969 {
970 $connections = [
971 'woocommerce' => [
972 'title' => __('WooCommerce', 'fluent-support'),
973 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/woocommerce.png',
974 'is_integrated' => defined('WC_PLUGIN_FILE'),
975 'description' => __('The most popular e-commerce platform for WordPress', 'fluent-support'),
976 'doc_url' => 'https://docs.fluentsupport.com/woocommerce-integration',
977 ],
978 'fluent-cart' => [
979 'title' => __('FluentCart', 'fluent-support'),
980 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-cart.webp',
981 'is_integrated' => defined('FLUENTCART_VERSION'),
982 'description' => __('A New Era of eCommerce with WordPress', 'fluent-support'),
983 'doc_url' => 'https://docs.fluentsupport.com/fluentcart-integration',
984 ],
985 'lifter-lms' => [
986 'title' => __('LifterLMS', 'fluent-support'),
987 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/lifter-lms.png',
988 'is_integrated' => defined('LLMS_PLUGIN_FILE'),
989 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
990 'doc_url' => 'https://docs.fluentsupport.com/lifterlms-integration',
991 ],
992 'slack' => [
993 'title' => __('Slack', 'fluent-support'),
994 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/slack.png',
995 'is_integrated' => self::getFSIntegrationStatus('slack_settings'),
996 'description' => __('Business communication platform designed to scale', 'fluent-support'),
997 'doc_url' => 'https://docs.fluentsupport.com/managing-tickets-using-slack',
998 ],
999 'pm-pro' => [
1000 'title' => __('Paid Memberships Pro', 'fluent-support'),
1001 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/pmpro.png',
1002 'is_integrated' => defined('PMPRO_VERSION'),
1003 'description' => __('The ultimate platform for any member-focused business', 'fluent-support'),
1004 'doc_url' => 'https://docs.fluentsupport.com/paid-membership-pro-integration',
1005 ],
1006 'tutor-lms' => [
1007 'title' => __('Tutor LMS', 'fluent-support'),
1008 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/tutor-lms.png',
1009 'is_integrated' => defined('TUTOR_VERSION'),
1010 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
1011 'doc_url' => 'https://docs.fluentsupport.com/tutorlms-integration',
1012 ],
1013 'telegram' => [
1014 'title' => __('Telegram', 'fluent-support'),
1015 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/telegram.jpeg',
1016 'is_integrated' => self::getFSIntegrationStatus('telegram_settings'),
1017 'description' => __('Business communication platform designed for security', 'fluent-support'),
1018 'doc_url' => 'https://docs.fluentsupport.com/managing-tickets-using-telegram',
1019 ],
1020 'fluent-crm' => [
1021 'title' => __('FluentCRM', 'fluent-support'),
1022 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-crm.png',
1023 'is_integrated' => defined('FLUENTCRM'),
1024 'description' => __('Self-hosted email and marketing automation for WordPress', 'fluent-support'),
1025 'doc_url' => 'https://docs.fluentsupport.com/fluentcrm-integration',
1026 ],
1027 'fluent-community' => [
1028 'title' => __('FluentCommunity', 'fluent-support'),
1029 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-community.png',
1030 'is_integrated' => defined('FLUENT_COMMUNITY_PLUGIN_VERSION'),
1031 'description' => __('Build and manage vibrant online communities with integrated LMS features directly within WordPress.', 'fluent-support'),
1032 'doc_url' => 'https://docs.fluentsupport.com/fluent-community-integration',
1033 ],
1034 'fluent-forms' => [
1035 'title' => __('Fluent Forms', 'fluent-support'),
1036 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-forms.png',
1037 'is_integrated' => defined('FLUENTFORM'),
1038 'description' => __('A robust form plugin suitable for any business', 'fluent-support'),
1039 'doc_url' => 'https://docs.fluentsupport.com/fluent-form-integration',
1040 ],
1041 'buddy-boss' => [
1042 'title' => __('BuddyBoss', 'fluent-support'),
1043 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/buddy-boss.png',
1044 'is_integrated' => defined('BP_PLUGIN_DIR'),
1045 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
1046 'doc_url' => 'https://docs.fluentsupport.com/buddyboss-integration'
1047 ],
1048 'discord' => [
1049 'title' => __('Discord', 'fluent-support'),
1050 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/discord.png',
1051 'is_integrated' => self::getFSIntegrationStatus('discord_settings'),
1052 'description' => __('Business communication platform designed for tech', 'fluent-support'),
1053 'doc_url' => 'https://docs.fluentsupport.com/managing-tickets-using-discord',
1054 ],
1055 'wishlist-member' => [
1056 'title' => __('WishList Member', 'fluent-support'),
1057 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/wishlist-member.png',
1058 'is_integrated' => defined('WLM3_PLUGIN_VERSION'),
1059 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
1060 'doc_url' => 'https://docs.fluentsupport.com/wishlist-member-integration',
1061 ],
1062 'easy-digital-downloads' => [
1063 'title' => __('Easy Digital Downloads', 'fluent-support'),
1064 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/easy-digital-downloads.png',
1065 'is_integrated' => class_exists('\Easy_Digital_Downloads'),
1066 'description' => __('The ultimate WordPress platform for digital products', 'fluent-support'),
1067 'doc_url' => 'https://docs.fluentsupport.com/edd-integration',
1068 ],
1069 'restrict-content-pro' => [
1070 'title' => __('Restrict Content pro', 'fluent-support'),
1071 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/restrict-content-pro.png',
1072 'is_integrated' => class_exists('\Restrict_Content_Pro' ),
1073 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
1074 'doc_url' => 'https://docs.fluentsupport.com/restrict-content-pro-integration',
1075 ],
1076 'better-docs' => [
1077 'title' => __('BetterDocs', 'fluent-support'),
1078 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/better-docs.png',
1079 'is_integrated' => false,
1080 'description' => __('The standard plugin for knowledge base and documentation', 'fluent-support'),
1081 'doc_url' => 'https://docs.fluentsupport.com/betterdocs-integration',
1082 ],
1083 'whatsapp' => [
1084 'title' => __('WhatsApp', 'fluent-support'),
1085 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/whatsapp.jpeg',
1086 'is_integrated' => self::getFSIntegrationStatus('twilio_settings'),
1087 'description' => __('Business communication platform designed for privacy', 'fluent-support'),
1088 'doc_url' => 'https://docs.fluentsupport.com/whatsapp-integration-via-twilio',
1089 ],
1090 'paymattic' => [
1091 'title' => __('Paymattic', 'fluent-support'),
1092 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/paymattic.png',
1093 'is_integrated' => defined('WPPAYFORM_VERSION'),
1094 'description' => __('All-in-one payment gateway designed for WordPress', 'fluent-support'),
1095 'doc_url' => 'https://paymattic.com/docs/how-to-integrate-fluent-support-with-paymattic-in-wordpress/',
1096 ],
1097 'learn-dash' => [
1098 'title' => __('LearnDash', 'fluent-support'),
1099 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/learn-dash.png',
1100 'is_integrated' => defined('LEARNDASH_VERSION'),
1101 'description' => __('The leading course platform built for WordPress', 'fluent-support'),
1102 'doc_url' => 'https://docs.fluentsupport.com/learndash-integration',
1103 ],
1104 'learn-press' => [
1105 'title' => __('LearnPress', 'fluent-support'),
1106 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/learn-press.png',
1107 'is_integrated' => defined('LP_PLUGIN_FILE'),
1108 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
1109 'doc_url' => 'https://docs.fluentsupport.com/learnpress-integration',
1110 ],
1111 'google-drive' => [
1112 'title' => __('Google Drive', 'fluent-support'),
1113 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-drive.jpeg',
1114 'is_integrated' => self::getFSIntegrationStatus('google_drive_settings'),
1115 'description' => __('A cloud storage service by Google for storing, syncing, and sharing files.', 'fluent-support'),
1116 'doc_url' => 'https://docs.fluentsupport.com/google-drive-integration'
1117 ],
1118 'dropbox' => [
1119 'title' => __('Dropbox', 'fluent-support'),
1120 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/dropbox.png',
1121 'is_integrated' => self::getFSIntegrationStatus('dropbox_settings'),
1122 'description' => __('A cloud-based file storage and sharing service that allows users to store files online and sync them across devices.', 'fluent-support'),
1123 'doc_url' => 'https://docs.fluentsupport.com/dropbox-integration',
1124 ],
1125 'member-press' => [
1126 'title' => __('MemberPress', 'fluent-support'),
1127 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/member-press.png',
1128 'is_integrated' => class_exists('MeprUtils'),
1129 'description' => __('A WordPress plugin that enables the creation and management of membership sites, including content access control and subscription billing.', 'fluent-support'),
1130 'doc_url' => 'https://docs.fluentsupport.com/memberpress-integration'
1131 ],
1132 'google-recaptcha' => [
1133 'title' => __('Google reCAPTCHA', 'fluent-support'),
1134 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-recaptcha.png',
1135 'is_integrated' => self::getFSIntegrationStatus('recaptcha_setting'),
1136 'description' => __('A security service by Google designed to protect websites from bots and abuse by using challenges to distinguish between human and automated access.', 'fluent-support'),
1137 'doc_url' => 'https://docs.fluentsupport.com/google-recaptcha-integration',
1138 ],
1139 'fluent-boards' => [
1140 'title' => __('FluentBoards', 'fluent-support'),
1141 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-boards.png',
1142 'is_integrated' => defined('FLUENT_BOARDS'),
1143 'description' => __('A project management tool designed to streamline workflows and collaboration through customizable, kanban-style boards.', 'fluent-support'),
1144 'doc_url' => 'https://docs.fluentsupport.com/fluentboards-integrations#fluentboards-integration',
1145 ],
1146 'fluent-booking' => [
1147 'title' => __('FluentBooking', 'fluent-support'),
1148 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-booking.svg',
1149 'is_integrated' => defined('FLUENT_BOOKING_VERSION'),
1150 'description' => __('Appointment and booking management plugin for WordPress', 'fluent-support'),
1151 'doc_url' => 'https://docs.fluentsupport.com/fluentbooking-integration',
1152 ],
1153 ];
1154
1155 return $connections;
1156 }
1157
1158 public static function getGlobalSettingsMenu()
1159 {
1160 $menu = [
1161 [
1162 'title' => __('Global Settings', 'fluent-support'),
1163 'route_name' => 'global_settings',
1164 'icon' => 'settings',
1165 ],
1166 [
1167 'title' => __('Ticket Tags', 'fluent-support'),
1168 'route_name' => 'tags',
1169 'icon' => 'ticketTag',
1170 ],
1171 [
1172 'title' => __('Ticket Form Config', 'fluent-support'),
1173 'route_name' => 'ticket-form-config',
1174 'icon' => 'formConfig',
1175 ],
1176 [
1177 'title' => __('Custom Fields', 'fluent-support'),
1178 'route_name' => 'custom_fields',
1179 'icon' => 'customFields',
1180 ],
1181 [
1182 'title' => __('Products', 'fluent-support'),
1183 'route_name' => 'products',
1184 'icon' => 'products',
1185 ],
1186 [
1187 'title' => __('Support Staff', 'fluent-support'),
1188 'route_name' => 'support-staffs',
1189 'icon' => 'supportStaffs',
1190 ],
1191 [
1192 'title' => __('FluentCRM Integration', 'fluent-support'),
1193 'route_name' => 'fluentcrm_integration',
1194 'icon' => 'crmIntegration',
1195 ],
1196 [
1197 'title' => __('Incoming Webhook', 'fluent-support'),
1198 'route_name' => 'incoming-webhook',
1199 'icon' => 'incomingWebhook',
1200 ],
1201 [
1202 'title' => __('Notification Integrations', 'fluent-support'),
1203 'route_name' => 'integration',
1204 'icon' => 'notification',
1205 ],
1206 [
1207 'title' => __('File Upload Integrations', 'fluent-support'),
1208 'route_name' => 'upload_integration',
1209 'icon' => 'fileUpload',
1210 ],
1211 [
1212 'title' => __('Auto Close Settings', 'fluent-support'),
1213 'route_name' => 'auto_close',
1214 'icon' => 'autoClose',
1215 ],
1216 [
1217 'title' => __('Ticket Importer', 'fluent-support'),
1218 'route_name' => 'ticket_importer',
1219 'icon' => 'importer',
1220 ],
1221 [
1222 'title' => __('Recaptcha', 'fluent-support'),
1223 'route_name' => 'reCaptcha',
1224 'icon' => 'reCaptcha',
1225 ],
1226 [
1227 'title' => __('Integration Statuses', 'fluent-support'),
1228 'route_name' => 'integration_statuses',
1229 'icon' => 'status',
1230 ],
1231 [
1232 'title' => __('AI Integration', 'fluent-support'),
1233 'icon' => 'aiIntegration',
1234 'children' => [
1235 [
1236 'title' => __('AI Model Setup', 'fluent-support'),
1237 'route_name' => 'ai_integration',
1238 ],
1239 [
1240 'title' => __('MCP for AI Agents', 'fluent-support'),
1241 'route_name' => 'mcp_settings',
1242 ],
1243 ],
1244 ],
1245 ];
1246
1247 if (defined('FLUENT_SUPPORT_PRO_DIR_FILE')) {
1248 $menu[] = [
1249 'title' => __('License Management', 'fluent-support'),
1250 'route_name' => 'license',
1251 'icon' => 'license',
1252 ];
1253 }
1254
1255 return apply_filters('fluent_support/settings_menu_items', $menu);
1256 }
1257
1258 /** @internal Not called from core controllers — available for Pro/hook usage. */
1259 public static function getFSIntegrationStatus($connection_name)
1260 {
1261 $integrationMap = [
1262 'slack_settings' => 'slack_settings',
1263 'discord_settings' => 'discord_settings',
1264 'twilio_settings' => 'twilio_settings',
1265 'telegram_settings' => 'telegram_settings',
1266 'google_drive_settings' => 'google_drive_settings',
1267 'dropbox_settings' => 'dropbox_settings',
1268 'recaptcha_setting' => '_fs_recaptcha_settings'
1269 ];
1270
1271 if (array_key_exists($connection_name, $integrationMap)) {
1272 if ($connection_name == 'google_drive_settings' || $connection_name == 'dropbox_settings') {
1273 return self::checkUploadDriverStatus($connection_name);
1274 } elseif ($connection_name == 'recaptcha_setting') {
1275 return self::checkRecaptchaStatus();
1276 } else {
1277 return self::checkNotificationIntegrationStatus($integrationMap[$connection_name]);
1278 }
1279 }
1280
1281 return false;
1282 }
1283
1284 private static function checkNotificationIntegrationStatus($settingName)
1285 {
1286 $settings = self::getIntegrationOption($settingName, null);
1287 if ($settings) {
1288 $status = Arr::get($settings, 'status', false);
1289 return $status ? true : false;
1290 }
1291 return false;
1292 }
1293
1294 private static function checkUploadDriverStatus($settingName)
1295 {
1296 $settings = self::getIntegrationOption($settingName, null);
1297 if ($settings) {
1298 $enabled = Arr::get($settings, 'status', false);
1299 return $enabled ? true : false;
1300 }
1301 return false;
1302 }
1303
1304 private static function checkRecaptchaStatus()
1305 {
1306 $reCaptchaSettingsData = Meta::where('object_type', '_fs_recaptcha_settings')->first();
1307
1308 if ($reCaptchaSettingsData) {
1309 $settings = static::safeUnserialize($reCaptchaSettingsData->value);
1310 $status = Arr::get($settings, 'is_enabled', false);
1311 return $status == 'true' ? true : false;
1312 }
1313 return false;
1314 }
1315
1316 public static function getAIActivities($data)
1317 {
1318 $page = isset($data['page']) ? intval($data['page']) : 1;
1319 $perPage = isset($data['per_page']) ? intval($data['per_page']) : 10;
1320
1321 $activitiesQuery = AIActivityLogs::with([
1322 'person' => function ($query) {
1323 $query->select(['first_name', 'person_type', 'last_name', 'id', 'avatar']);
1324 },
1325 'ticket' => function ($query) {
1326 $query->select(['id', 'title']);
1327 }
1328 ])->latest('id');
1329
1330 $from = sanitize_text_field( Arr::get( $data, 'from', '' ) );
1331 $to = sanitize_text_field( Arr::get( $data, 'to', '') );
1332
1333 if ( $from != $to ) {
1334 $from = $from . ' ' . '00:00:00';
1335 $to = $to . ' ' . '23:59:59';
1336 }
1337
1338 if ( ( !empty($from) && !empty($to) ) && $from == $to ) {
1339 $activitiesQuery->whereDate('created_at', '=', $from);
1340 } elseif (!empty($from) && !empty($to)) {
1341 $activitiesQuery->whereBetween('created_at', [ $from, $to ]);
1342 }
1343
1344 $agentId = intval( Arr::get($data, 'filters.agent_id') );
1345
1346 if ($agentId) {
1347 $activitiesQuery->where('agent_id', $agentId);
1348 }
1349
1350 $activities = $activitiesQuery->paginate($perPage, ['*'], 'page', $page);
1351
1352 $settings = static::getSettings();
1353
1354 return [
1355 'data' => $activities->items(),
1356 'total' => $activities->total(),
1357 'per_page' => $activities->perPage(),
1358 'current_page' => $activities->currentPage(),
1359 'last_page' => $activities->lastPage(),
1360 'settings' => $settings['ai_activity_settings']
1361 ];
1362 }
1363
1364 public static function updateAISettings($settings)
1365 {
1366 $defaults = [
1367 'delete_days' => 14,
1368 'disable_logs' => 'no'
1369 ];
1370
1371 $settings = wp_parse_args($settings, $defaults);
1372 $settings['delete_days'] = (int)$settings['delete_days'];
1373
1374 Helper::updateOption('_ai_activity_settings', $settings);
1375
1376 return [
1377 'message' => __('AI Activity settings have been updated', 'fluent-support')
1378 ];
1379 }
1380
1381 public static function getSettings()
1382 {
1383 $settings = Helper::getOption('_ai_activity_settings', []);
1384
1385 $defaults = [
1386 'delete_days' => 14,
1387 'disable_logs' => 'no'
1388 ];
1389
1390 $settings = wp_parse_args($settings, $defaults);
1391
1392 if (! $settings ) throw new \Exception(esc_html__('No activity settings found', 'fluent-support'));
1393
1394 return [
1395 'ai_activity_settings' => $settings
1396 ];
1397 }
1398
1399 /**
1400 * Check if activity logs are disabled for a given option key
1401 * @param string $optionKey The option key to check
1402 * @return bool
1403 */
1404 public static function areLogsDisabled($optionKey)
1405 {
1406 if (empty($optionKey)) {
1407 return false;
1408 }
1409
1410 $settings = Helper::getOption($optionKey, []);
1411 $defaults = [
1412 'disable_logs' => 'no'
1413 ];
1414 $settings = wp_parse_args($settings, $defaults);
1415 return isset($settings['disable_logs']) && $settings['disable_logs'] === 'yes';
1416 }
1417
1418 public static function getIp($anonymize = false)
1419 {
1420 static $ipAddress;
1421
1422 if ($ipAddress) {
1423 return $ipAddress;
1424 }
1425
1426 if (empty($_SERVER['REMOTE_ADDR'])) {
1427 // It's a local cli request
1428 return '127.0.0.1';
1429 }
1430
1431 $ipAddress = '';
1432 $remoteAddr = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
1433
1434 if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
1435 //If it's a valid Cloudflare request
1436 if (self::isCfIp($remoteAddr)) {
1437 //Use the CF-Connecting-IP header.
1438 $ipAddress = sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP']));
1439 } else {
1440 //If it isn't valid, then use REMOTE_ADDR.
1441 $ipAddress = $remoteAddr;
1442 }
1443 } else if ($remoteAddr == '127.0.0.1') {
1444 // most probably it's local reverse proxy
1445 if (isset($_SERVER["HTTP_CLIENT_IP"])) {
1446 $ipAddress = sanitize_text_field(wp_unslash($_SERVER["HTTP_CLIENT_IP"]));
1447 } else if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1448 $forwardedFor = sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']));
1449 $splitResult = preg_split('/,/', $forwardedFor);
1450 $firstIp = is_array($splitResult) ? current($splitResult) : '';
1451 $ipAddress = (string)rest_is_ip_address(trim((string)$firstIp));
1452 }
1453 }
1454
1455 if (!$ipAddress) {
1456 $ipAddress = $remoteAddr;
1457 }
1458
1459 if ($ipAddress) {
1460 $ipAddress = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', $ipAddress);
1461 }
1462
1463 $ipAddress = apply_filters('fluent_auth/user_ip', $ipAddress);
1464
1465 if ($anonymize) {
1466 return wp_privacy_anonymize_ip($ipAddress);
1467 }
1468
1469 $ipAddress = sanitize_text_field(wp_unslash($ipAddress));
1470
1471 return $ipAddress;
1472 }
1473
1474 /**
1475 * Atomically increments the counter for $rateLimitKey and reports whether the
1476 * limit is now exceeded. On sites with a persistent external object cache
1477 * (Redis/Memcached), wp_cache_incr() is a real atomic increment, so concurrent
1478 * requests can't race past the limit. Without one, this falls back to a plain
1479 * transient read/write (same best-effort behavior as the rest of this codebase's
1480 * rate limiters, e.g. AuthController::incrementLoginAttempts).
1481 *
1482 * Note the counter is incremented before it is compared, so the returned value
1483 * already accounts for the current request.
1484 */
1485 public static function hitRateLimit($rateLimitKey, $limit, $window = null)
1486 {
1487 $window = $window ?: 15 * MINUTE_IN_SECONDS;
1488
1489 if (wp_using_ext_object_cache()) {
1490 $group = 'fs_rate_limit';
1491 if (false === wp_cache_get($rateLimitKey, $group)) {
1492 wp_cache_add($rateLimitKey, 0, $group, $window);
1493 }
1494 $attempts = wp_cache_incr($rateLimitKey, 1, $group);
1495
1496 // Fail closed: if the cache backend couldn't increment (evicted key, hiccup),
1497 // treat the request as rate-limited rather than silently letting it through.
1498 if ($attempts === false) {
1499 return true;
1500 }
1501
1502 return $attempts > $limit;
1503 }
1504
1505 $now = time();
1506 $record = get_transient($rateLimitKey);
1507
1508 // A record without a live deadline means the window is over (or the record predates
1509 // this format), so the count starts again. Anything else keeps the deadline it was
1510 // created with.
1511 //
1512 // The `<=` must not be loosened to `<`: it is what guarantees the TTL below is at
1513 // least 1. A request landing exactly on the deadline would otherwise compute a TTL
1514 // of 0, and set_transient() reads 0 as "never expires" — wedging this limiter shut
1515 // permanently.
1516 if (!is_array($record) || empty($record['expires']) || $record['expires'] <= $now) {
1517 $record = ['count' => 0, 'expires' => $now + $window];
1518 }
1519
1520 $record['count']++;
1521
1522 // The TTL is the time left until the original deadline, never the full window:
1523 // set_transient() rewrites expiry on every call, so passing $window here would let
1524 // rejected requests push the deadline forward and keep a tripped limiter tripped
1525 // for as long as traffic kept arriving.
1526 set_transient($rateLimitKey, $record, $record['expires'] - $now);
1527
1528 return $record['count'] > $limit;
1529 }
1530
1531 /** @internal Not called from core controllers — available for Pro/hook usage. */
1532 public static function isCfIp($ip = '')
1533 {
1534 if (!$ip) {
1535 $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
1536 }
1537 $cloudflareIPRanges = array(
1538 '103.21.244.0/22',
1539 '103.22.200.0/22',
1540 '103.31.4.0/22',
1541 '104.16.0.0/13',
1542 '104.24.0.0/14',
1543 '108.162.192.0/18',
1544 '131.0.72.0/22',
1545 '141.101.64.0/18',
1546 '162.158.0.0/15',
1547 '172.64.0.0/13',
1548 '173.245.48.0/20',
1549 '188.114.96.0/20',
1550 '190.93.240.0/20',
1551 '197.234.240.0/22',
1552 '198.41.128.0/17'
1553 );
1554 $validCFRequest = false;
1555 //Make sure that the request came via Cloudflare.
1556 foreach ($cloudflareIPRanges as $range) {
1557 //Use the ip_in_range function from Joomla.
1558 if (self::ipInRange($ip, $range)) {
1559 //IP is valid. Belongs to Cloudflare.
1560 return true;
1561 }
1562 }
1563
1564 return false;
1565 }
1566
1567 private static function ipInRange($ip, $range)
1568 {
1569 if (!$ip || !$range || !is_string($ip) || !is_string($range)) {
1570 return false;
1571 }
1572
1573 if (strpos($range, '/') !== false) {
1574 // $range is in IP/NETMASK format
1575 list($range, $netmask) = explode('/', $range, 2);
1576 if (strpos($netmask, '.') !== false) {
1577 // $netmask is a 255.255.0.0 format
1578 $netmask = str_replace('*', '0', $netmask);
1579 $netmask_dec = ip2long($netmask);
1580 return ((ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec));
1581 } else {
1582 // $netmask is a CIDR size block
1583 // fix the range argument
1584 $x = explode('.', $range);
1585 while (count($x) < 4) $x[] = '0';
1586 list($a, $b, $c, $d) = $x;
1587 $range = sprintf("%u.%u.%u.%u", empty($a) ? '0' : $a, empty($b) ? '0' : $b, empty($c) ? '0' : $c, empty($d) ? '0' : $d);
1588 $range_dec = ip2long($range);
1589 $ip_dec = ip2long($ip);
1590
1591 # Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s
1592 #$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0'));
1593
1594 # Strategy 2 - Use math to create it
1595 $wildcard_dec = pow(2, (32 - $netmask)) - 1;
1596 $netmask_dec = ~$wildcard_dec;
1597
1598 return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec));
1599 }
1600 } else {
1601 // range might be 255.255.*.* or 1.2.3.0-1.2.3.255
1602 if (strpos($range, '*') !== false) { // a.b.*.* format
1603 // Just convert to A-B format by setting * to 0 for A and 255 for B
1604 $lower = str_replace('*', '0', $range);
1605 $upper = str_replace('*', '255', $range);
1606 $range = "$lower-$upper";
1607 }
1608
1609 if (strpos($range, '-') !== false) { // A-B format
1610 list($lower, $upper) = explode('-', $range, 2);
1611 $lower_dec = (float)sprintf("%u", ip2long($lower));
1612 $upper_dec = (float)sprintf("%u", ip2long($upper));
1613 $ip_dec = (float)sprintf("%u", ip2long($ip));
1614 return (($ip_dec >= $lower_dec) && ($ip_dec <= $upper_dec));
1615 }
1616 return false;
1617 }
1618 }
1619
1620 public static function loadView($template, $data)
1621 {
1622 extract($data, EXTR_OVERWRITE);
1623
1624 $template = sanitize_file_name($template);
1625
1626 $template = str_replace('.', DIRECTORY_SEPARATOR, $template);
1627
1628 ob_start();
1629 include FLUENT_SUPPORT_PLUGIN_PATH . 'app/Views/emails/' . $template . '.php';
1630 return ob_get_clean();
1631 }
1632
1633 public static function isProductRequired()
1634 {
1635 $settings = Helper::getOption('_ticket_form_settings', []);
1636 return Arr::get($settings, 'product_required_field') === 'yes';
1637 }
1638
1639 /**
1640 * Build a fluentsupport.com upgrade/pricing link tagged with UTM params
1641 * per the standard "Upgrade to Pro" link spec:
1642 * utm_source = fluent-support (fixed vocabulary, never the wp.org slug)
1643 * utm_medium = free_plugin | pro_plugin (acquisition vs cross-sell)
1644 * utm_campaign= upgrade_pro (override via $args['campaign'])
1645 * utm_content = the exact placement, e.g. feature_lock_dropbox, upgrade_page
1646 * utm_term = plugin version that generated the link
1647 *
1648 * This is the single source of truth for upgrade URLs. The admin SPA does
1649 * not rebuild these client-side; it re-points utm_content on the URL this
1650 * method localizes into appVars (see the $upgradeUrl mixin in start.js).
1651 *
1652 * @param string $content The utm_content placement.
1653 * @param array $args Optional: 'campaign', 'base_url', plus any utm_* override.
1654 * @return string
1655 */
1656 public static function getUpgradeUrl($content = 'upgrade_page', $args = [])
1657 {
1658 $args = wp_parse_args($args, [
1659 'campaign' => 'upgrade_pro',
1660 'base_url' => apply_filters('fluent_support/pro_upgrade_base_url', 'https://fluentsupport.com/pricing'),
1661 ]);
1662
1663 $params = [
1664 'utm_source' => 'fluent-support',
1665 // Same constant Menu.php uses for appVars.has_pro — keep the two in sync.
1666 'utm_medium' => defined('FLUENTSUPPORTPRO_PLUGIN_VERSION') ? 'pro_plugin' : 'free_plugin',
1667 'utm_campaign' => $args['campaign'],
1668 'utm_content' => $content,
1669 'utm_term' => FLUENT_SUPPORT_VERSION,
1670 ];
1671
1672 // Drop any blank params (e.g. a missing version) so they never hit the URL.
1673 $params = array_filter($params, function ($value) {
1674 return $value !== '' && $value !== null;
1675 });
1676
1677 return add_query_arg($params, $args['base_url']);
1678 }
1679
1680 /** @internal Not called from core controllers — available for Pro/hook usage. */
1681 public static function getBusinessBox()
1682 {
1683 $businessEmailBoxes = MailBox::select(['id', 'name', 'email', 'mapped_email'])
1684 ->where('box_type', 'email')
1685 ->get();
1686 return $businessEmailBoxes;
1687 }
1688
1689 public static function tempImageMoveUploadDir($ticketId, $contentType, $replyId = null, $personId = null)
1690 {
1691 // Fetch content based on the content type
1692 $content = self::getContentByType($ticketId, $contentType , $replyId);
1693 if (empty($content)) {
1694 return;
1695 }
1696
1697 // Extract image URLs from the content
1698 $imageUrls = self::extractImageUrls($content);
1699 if (empty($imageUrls)) {
1700 return;
1701 }
1702
1703 // Move images to the upload directory and update content
1704 self::moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId, $personId);
1705 }
1706
1707 /**
1708 * Fetch content based on the content type.
1709 */
1710 private static function getContentByType($ticketId, $contentType, $replyId)
1711 {
1712 if ($contentType == 'ticket-create') {
1713 $ticket = Ticket::find($ticketId);
1714 return $ticket ? $ticket->content : null;
1715 }
1716
1717 $conversation = Conversation::find($replyId);
1718 return $conversation ? $conversation->content : null;
1719 }
1720
1721 /**
1722 * Extract image URLs from the content.
1723 */
1724 private static function extractImageUrls($content)
1725 {
1726 preg_match_all('/<img[^>]+src=(["\'])(.*?)\1/i', (string) $content, $matches);
1727 return $matches[2] ?? [];
1728 }
1729
1730 /**
1731 * Move images to the upload directory and update content.
1732 */
1733 private static function moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId, $personId)
1734 {
1735 // Get the current site's upload directory
1736 $uploadDirInfo = wp_upload_dir();
1737 $uploadsDir = $uploadDirInfo['basedir'];
1738 $tempDir = $uploadsDir . '/fluent-support/temp_files/';
1739 $signedAttachments = self::getSignedImageAttachments($imageUrls, $ticketId, $personId);
1740
1741 foreach ($imageUrls as $imageUrl) {
1742 $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1743 if ($fileHash && isset($signedAttachments[$fileHash]) && ($referenceUrl = self::finalizeSignedTempImage($signedAttachments[$fileHash], $ticketId, $contentType, $replyId, $personId))) {
1744 $content = str_replace($imageUrl, $referenceUrl, $content);
1745 continue;
1746 }
1747
1748 // Build the absolute path for the temporary file
1749 $imageRelativePath = $tempDir . basename($imageUrl);
1750 $absolutePath = $imageRelativePath;
1751
1752 // Move the file to the ticket-specific folder
1753 $newFileInfo = UploadService::copyFileTicketFolder($absolutePath, $ticketId);
1754
1755 // Check if the move was successful
1756 if (empty($newFileInfo['file_path'])) {
1757 continue; // Skip if the file couldn't be copied
1758 }
1759
1760 // Ensure the new URL is correctly constructed
1761 $newFileInfo['url'] = trailingslashit($uploadDirInfo['baseurl']) . 'fluent-support/ticket_' . $ticketId . '/' . basename($newFileInfo['file_path']);
1762
1763 // Replace the old URL with the new one in the content
1764 $content = str_replace($imageUrl, $newFileInfo['url'], $content);
1765 }
1766
1767 // Save the updated content
1768 self::saveUpdatedContent($ticketId, $contentType, $content, $replyId);
1769 }
1770
1771 private static function getSignedImageAttachments($imageUrls, $ticketId, $personId)
1772 {
1773 $fileHashes = [];
1774 foreach ($imageUrls as $imageUrl) {
1775 $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1776 if ($fileHash) {
1777 $fileHashes[] = $fileHash;
1778 }
1779 }
1780
1781 $fileHashes = array_values(array_unique($fileHashes));
1782 if (!$fileHashes) {
1783 return [];
1784 }
1785
1786 $attachments = Attachment::whereIn('file_hash', $fileHashes)
1787 ->where(function ($query) use ($ticketId, $personId) {
1788 $query->where('ticket_id', $ticketId);
1789
1790 if ($personId) {
1791 $query->orWhere(function ($query) use ($personId) {
1792 $query->whereNull('ticket_id')
1793 ->where('person_id', $personId);
1794 });
1795 }
1796 })
1797 ->get();
1798 $mappedAttachments = [];
1799 foreach ($attachments as $attachment) {
1800 $mappedAttachments[$attachment->file_hash] = $attachment;
1801 }
1802
1803 return $mappedAttachments;
1804 }
1805
1806 private static function finalizeSignedTempImage($attachment, $ticketId, $contentType, $replyId, $personId)
1807 {
1808 if (!$attachment || $attachment->driver !== 'local') {
1809 return false;
1810 }
1811
1812 if ($attachment->ticket_id && intval($attachment->ticket_id) !== intval($ticketId)) {
1813 return false;
1814 }
1815
1816 if (!$attachment->ticket_id && $personId && intval($attachment->person_id) !== intval($personId)) {
1817 return false;
1818 }
1819
1820 if ($attachment->conversation_id && $replyId && intval($attachment->conversation_id) !== intval($replyId)) {
1821 return false;
1822 }
1823
1824 if ($attachment->conversation_id && $contentType === 'ticket-create') {
1825 return false;
1826 }
1827
1828 if ($attachment->status !== 'in-active') {
1829 return $attachment->ticket_id ? self::getAttachmentReferenceUrl($attachment) : false;
1830 }
1831
1832 if (!$attachment->file_path || !file_exists($attachment->file_path)) {
1833 return false;
1834 }
1835
1836 $newFileInfo = UploadService::copyFileTicketFolder($attachment->file_path, $ticketId);
1837 if (empty($newFileInfo['file_path'])) {
1838 return false;
1839 }
1840
1841 $attachment->file_path = $newFileInfo['file_path'];
1842 $attachment->full_url = $newFileInfo['url'];
1843 $attachment->ticket_id = $attachment->ticket_id ?: $ticketId;
1844 $attachment->status = 'inline';
1845
1846 if ($contentType !== 'ticket-create' && $replyId) {
1847 $attachment->conversation_id = $replyId;
1848 }
1849
1850 $attachment->save();
1851
1852 return self::getAttachmentReferenceUrl($attachment);
1853 }
1854
1855 private static function extractAttachmentHashFromUrl($imageUrl)
1856 {
1857 $decodedUrl = html_entity_decode($imageUrl, ENT_QUOTES, 'UTF-8');
1858 $query = wp_parse_url($decodedUrl, PHP_URL_QUERY);
1859
1860 if (!$query) {
1861 return '';
1862 }
1863
1864 parse_str($query, $params);
1865
1866 return !empty($params['fst_file']) ? sanitize_text_field($params['fst_file']) : '';
1867 }
1868
1869 private static function getAttachmentReferenceUrl($attachment)
1870 {
1871 return add_query_arg([
1872 'fst_file' => $attachment->file_hash
1873 ], site_url('/index.php'));
1874 }
1875
1876 public static function refreshSignedAttachmentUrls($content, $ticketId = null)
1877 {
1878 $contents = self::refreshSignedAttachmentUrlsInContents([$content], $ticketId);
1879
1880 return $contents[0];
1881 }
1882
1883 public static function refreshSignedAttachmentUrlsInContents($contents, $ticketId = null)
1884 {
1885 $fileHashes = [];
1886
1887 foreach ($contents as $content) {
1888 foreach (self::extractImageUrls($content) as $imageUrl) {
1889 $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1890 if ($fileHash) {
1891 $fileHashes[] = $fileHash;
1892 }
1893 }
1894 }
1895
1896 $fileHashes = array_values(array_unique($fileHashes));
1897 if (!$fileHashes) {
1898 return $contents;
1899 }
1900
1901 $attachmentsQuery = Attachment::whereIn('file_hash', $fileHashes);
1902 if ($ticketId) {
1903 $attachmentsQuery->where('ticket_id', $ticketId);
1904 }
1905
1906 $attachments = $attachmentsQuery->get();
1907 $attachmentsByHash = [];
1908
1909 foreach ($attachments as $attachment) {
1910 $attachmentsByHash[$attachment->file_hash] = $attachment;
1911 }
1912
1913 foreach ($contents as $key => $content) {
1914 foreach (self::extractImageUrls($content) as $imageUrl) {
1915 $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1916 if (!$fileHash || empty($attachmentsByHash[$fileHash])) {
1917 continue;
1918 }
1919
1920 $content = str_replace($imageUrl, $attachmentsByHash[$fileHash]->secureUrl, $content);
1921 }
1922
1923 $contents[$key] = $content;
1924 }
1925
1926 return $contents;
1927 }
1928
1929 /**
1930 * Save the updated content based on the content type.
1931 */
1932 private static function saveUpdatedContent($ticketId, $contentType, $content, $replyId)
1933 {
1934 if ($contentType == 'ticket-create') {
1935 $ticket = Ticket::find($ticketId);
1936 if ($ticket) {
1937 $ticket->content = $content;
1938 $ticket->save();
1939 }
1940 } else {
1941 $conversation = Conversation::find($replyId);
1942 if ($conversation) {
1943 $conversation->content = $content;
1944 $conversation->save();
1945 }
1946 }
1947 }
1948
1949 /**
1950 * Throw a ValidationException with a safe error message.
1951 * In debug mode the real exception message is used; in production a generic message is returned.
1952 * Throwing instead of returning ensures the error bypasses the framework's HTTP exception
1953 * wrapper (which would prepend a status-code prefix to the message).
1954 *
1955 * @param \Throwable $e The original exception.
1956 * @throws \FluentSupport\Framework\Validator\ValidationException Always thrown.
1957 */
1958 public static function getSafeErrorMessage($e)
1959 {
1960 $message = $e->getMessage() ?: __('Something went wrong. Please try again later.', 'fluent-support');
1961
1962 // ValidationException carries the message through the framework's JSON error
1963 // handler — it is never echoed directly. $e is the chained previous exception,
1964 // not output. phpcs:disable covers the multi-line constructor call.
1965 // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped
1966 throw new \FluentSupport\Framework\Validator\ValidationException(
1967 '', 422, ($e instanceof \Exception ? $e : null), ['message' => $message]
1968 );
1969 // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
1970 }
1971
1972 /**
1973 * Safely unserialize data.
1974 *
1975 * @param string $data The serialized data.
1976 * @return mixed The unserialized data or the original data if not serialized.
1977 */
1978 public static function safeUnserialize($data)
1979 {
1980 if (is_serialized($data)) { // Don't attempt to unserialize data that wasn't serialized going in.
1981 return @unserialize(trim($data), ['allowed_classes' => false]);
1982 }
1983
1984 return $data;
1985 }
1986 }
1987