PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.3.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.3.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 1.5.6 All 67 releases
fluent-support / app / Services / Helper.php

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

1,880 lines 68.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 return Customer::where('user_id', get_current_user_id())->first();
664 }
665 }
666
667 public static function getCurrentPerson()
668 {
669 // If user is logged in then return the person(agent/customer) by user id.
670 // This `get_current_user_id` function is WP function and
671 // it returns user id if user is logged in.
672 if (get_current_user_id()) {
673 return Person::where('user_id', get_current_user_id())
674 ->orderBy('id', 'ASC')
675 ->first();
676 }
677 return null;
678 }
679
680 public static function getCustomerByID($customerid)
681 {
682 return Customer::where('id', $customerid)->first();
683 }
684
685 /** @internal Not called from core controllers — available for Pro/hook usage. */
686 public static function sanitizeOrderValue($orderType = '')
687 {
688 $orderBys = ['ASC', 'DESC'];
689
690 $orderType = trim(strtoupper((string)($orderType ?? '')));
691
692 return in_array($orderType, $orderBys) ? $orderType : 'DESC';
693 }
694
695 public static function getFluentCRMTagConfig()
696 {
697 if (!defined('FLUENTCRM')) {
698 return [
699 'can_add_tags' => false,
700 'tags' => [],
701 'lists' => [],
702 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluentcrm-logo.svg',
703 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluent-crm-icon.png',
704 ];
705 }
706
707 $canAddTags = \FluentCrm\App\Services\PermissionManager::currentUserCan('fcrm_manage_contacts');
708
709 $canAddTags = apply_filters('fluent_support/can_user_add_tags_to_customer', $canAddTags);
710 $crmTags = [];
711 $crmLists = [];
712 if ($canAddTags) {
713 $crmTags = \FluentCrm\App\Models\Tag::select(['id', 'title'])->oldest('title')->get();
714 $crmLists = \FluentCrm\App\Models\Lists::select(['id', 'title'])->oldest('title')->get();
715 }
716
717 $crmConfigs = [
718 'can_add_tags' => $canAddTags,
719 'tags' => $crmTags,
720 'lists' => $crmLists,
721 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluentcrm-logo.svg',
722 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluent-crm-icon.png',
723 ];
724
725 if (defined('FLUENTCRM')) {
726 $crmConfigs['contacts'] = []; //(new \FluentCrm\App\Models\Subscriber)->get();
727 }
728
729 return $crmConfigs;
730 }
731
732 /**
733 * getFluentCrmContactData method will get information from fluent crm using user email
734 * @param $customer
735 * @return array|false
736 */
737 public static function getFluentCrmContactData($customer)
738 {
739 if (!defined('FLUENTCRM')) {
740 return false;
741 }
742 //Get contact info from FluentCRM using customer email
743 $contact = \FluentCrmApi('contacts')->getContactByUserRef($customer->email);
744 if ($contact) {
745 $tags = $contact->tags;
746 $lists = $contact->lists;
747 $urlBase = apply_filters('fluentcrm_menu_url_base', admin_url('admin.php?page=fluentcrm-admin#/'));
748 $crmProfileUrl = $urlBase . 'subscribers/' . $contact->id;
749
750 //Return contact data
751 return [
752 'id' => $contact->id,
753 'first_name' => $contact->first_name,
754 'last_name' => $contact->last_name,
755 'full_name' => $contact->full_name,
756 'name_mismatch' => $contact->full_name != $customer->full_name,
757 'tags' => $tags,
758 'lists' => $lists,
759 'status' => $contact->status,
760 'stats' => $contact->stats(),
761 'view_url' => $crmProfileUrl
762 ];
763 }
764
765 return false;
766 }
767
768 public static function openAIIntegrationStatus()
769 {
770 $settings = static::getAIProviderSettings();
771
772 if (($settings['enabled'] ?? 'yes') === 'no') {
773 return false;
774 }
775
776 return !empty($settings['api_key']);
777 }
778
779 public static function fluentBotIntegrationStatus()
780 {
781 // Include object_id and order by id desc to always read the latest row; the
782 // write path (saveFluentBotSettings) does not prune siblings, so duplicates
783 // may exist and a non-deterministic read can return stale state.
784 $meta = Meta::where([
785 'object_type' => 'fluent_bot_settings',
786 'object_id' => 1,
787 'key' => '_fs_fluent_bot_config'
788 ])->orderByDesc('id')->first();
789
790 $settings = static::safeUnserialize($meta ? $meta->value : null);
791
792 return isset($settings['isEnabled']) && filter_var($settings['isEnabled'], FILTER_VALIDATE_BOOLEAN);
793 }
794
795
796 public static function showTicketSummaryAdminBar()
797 {
798 $data = self::getOption('global_business_settings');
799
800 if ($data && isset($data["enable_admin_bar_summary"]) && $data["enable_admin_bar_summary"] == 'yes') {
801 return true;
802 }
803
804 return false;
805 }
806
807 public static function generateMessageID($email)
808 {
809 if (!$email || !is_string($email)) {
810 return false;
811 }
812
813 $emailParts = explode('@', $email);
814 if (count($emailParts) != 2) {
815 return false;
816 }
817
818 $emailDomain = $emailParts[1];
819 try {
820 return sprintf(
821 "<%s.%s@%s>",
822 base_convert((int)microtime(true), 10, 36),
823 base_convert(bin2hex(openssl_random_pseudo_bytes(8)), 16, 36),
824 $emailDomain
825 );
826 } catch (\Exception $exception) {
827 return false;
828 }
829 }
830
831 public static function getExportOptions()
832 {
833 $data = [
834 'Agent First Name' => __('Agent First Name', 'fluent-support'),
835 'Agent Last Name' => __('Agent Last Name', 'fluent-support'),
836 'Agent Full Name' => __('Agent Full Name', 'fluent-support'),
837 'Responses' => __('Responses', 'fluent-support'),
838 'Interactions' => __('Interactions', 'fluent-support'),
839 'Open Tickets' => __('Open Tickets', 'fluent-support'),
840 'Closed' => __('Closed', 'fluent-support'),
841 'Waiting Tickets' => __('Waiting Tickets', 'fluent-support'),
842 'Average Waiting' => __('Average Waiting', 'fluent-support'),
843 'Max Waiting' => __('Max Waiting', 'fluent-support'),
844 ];
845
846 if (Helper::isAgentFeedbackEnabled()) {
847 $data['Likes'] = __('Likes', 'fluent-support');
848 $data['Dislikes'] = __('Dislikes', 'fluent-support');
849 }
850
851 return $data;
852 }
853
854 public static function getAuthProvider()
855 {
856 if (defined('FLUENT_AUTH_PLUGIN_PATH')) {
857 $settings = \FluentAuth\App\Helpers\Helper::getAuthFormsSettings();
858 if ($settings['enabled'] == 'yes') {
859 return 'fluent_auth';
860 }
861 }
862
863 return 'fluent_support';
864 }
865
866 /** @internal Not called from core controllers — available for Pro/hook usage. */
867 public static function getDriversKey(){
868 return [
869 'dropbox_settings',
870 'google_drive_settings',
871 'cloudflare_r2_settings',
872 'amazon_s3_settings',
873 'local'
874 ];
875 }
876
877
878 public static function getUploadDriverKey()
879 {
880 if (!defined('FLUENTSUPPORTPRO')) {
881 return 'local';
882 }
883
884 $driver = self::getOption('file_upload_driver');
885
886 if ($driver) {
887 return $driver;
888 }
889
890 // Now guess the driver and save it
891
892 // check if dropbox is enabled
893 $dropboxSettings = self::getIntegrationOption('dropbox_settings', null);
894 if ($dropboxSettings) {
895 $dropBoxEnabled = Meta::where('object_type', 'enabled_upload_drivers')
896 ->where('key', 'dropbox_settings')
897 ->where('value', 'yes')
898 ->first();
899
900 if ($dropBoxEnabled) {
901 $driver = 'dropbox';
902 self::updateOption('file_upload_driver', $driver);
903 return $driver;
904 }
905 }
906
907 // check if google drive is enabled
908 $googleDriveSettings = self::getIntegrationOption('google_drive_settings', null);
909
910 if ($googleDriveSettings) {
911 $googleDriveEnabled = Meta::where('object_type', 'enabled_upload_drivers')
912 ->where('key', 'google_drive_settings')
913 ->where('value', 'yes')
914 ->first();
915
916 if ($googleDriveEnabled) {
917 $driver = 'google_drive';
918 self::updateOption('file_upload_driver', $driver);
919 return $driver;
920 }
921 }
922
923 // check if cloudflare r2 is enabled
924 $cloudflareR2Settings = self::getIntegrationOption('cloudflare_r2_settings', null);
925
926 if ($cloudflareR2Settings) {
927 $cloudflareR2Enabled = Meta::where('object_type', 'enabled_upload_drivers')
928 ->where('key', 'cloudflare_r2_settings')
929 ->where('value', 'yes')
930 ->first();
931
932 if ($cloudflareR2Enabled) {
933 $driver = 'cloudflare_r2';
934 self::updateOption('file_upload_driver', $driver);
935 return $driver;
936 }
937 }
938
939 // check if amazon s3 is enabled
940 $amazonS3Settings = self::getIntegrationOption('amazon_s3_settings', null);
941
942 if ($amazonS3Settings) {
943 $amazonS3Enabled = Meta::where('object_type', 'enabled_upload_drivers')
944 ->where('key', 'amazon_s3_settings')
945 ->where('value', 'yes')
946 ->first();
947
948 if ($amazonS3Enabled) {
949 $driver = 'amazon_s3';
950 self::updateOption('file_upload_driver', $driver);
951 return $driver;
952 }
953 }
954
955 self::updateOption('file_upload_driver', 'local');
956 return 'local';
957 }
958
959 public static function getIntegrationStatuses()
960 {
961 $connections = [
962 'woocommerce' => [
963 'title' => __('WooCommerce', 'fluent-support'),
964 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/woocommerce.png',
965 'is_integrated' => defined('WC_PLUGIN_FILE'),
966 'description' => __('The most popular e-commerce platform for WordPress', 'fluent-support'),
967 'doc_url' => 'https://docs.fluentsupport.com/woocommerce-integration',
968 ],
969 'fluent-cart' => [
970 'title' => __('FluentCart', 'fluent-support'),
971 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-cart.webp',
972 'is_integrated' => defined('FLUENTCART_VERSION'),
973 'description' => __('A New Era of eCommerce with WordPress', 'fluent-support'),
974 'doc_url' => 'https://docs.fluentsupport.com/fluentcart-integration',
975 ],
976 'lifter-lms' => [
977 'title' => __('LifterLMS', 'fluent-support'),
978 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/lifter-lms.png',
979 'is_integrated' => defined('LLMS_PLUGIN_FILE'),
980 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
981 'doc_url' => 'https://docs.fluentsupport.com/lifterlms-integration',
982 ],
983 'slack' => [
984 'title' => __('Slack', 'fluent-support'),
985 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/slack.png',
986 'is_integrated' => self::getFSIntegrationStatus('slack_settings'),
987 'description' => __('Business communication platform designed to scale', 'fluent-support'),
988 'doc_url' => 'https://docs.fluentsupport.com/managing-tickets-using-slack',
989 ],
990 'pm-pro' => [
991 'title' => __('Paid Memberships Pro', 'fluent-support'),
992 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/pmpro.png',
993 'is_integrated' => defined('PMPRO_VERSION'),
994 'description' => __('The ultimate platform for any member-focused business', 'fluent-support'),
995 'doc_url' => 'https://docs.fluentsupport.com/paid-membership-pro-integration',
996 ],
997 'tutor-lms' => [
998 'title' => __('Tutor LMS', 'fluent-support'),
999 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/tutor-lms.png',
1000 'is_integrated' => defined('TUTOR_VERSION'),
1001 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
1002 'doc_url' => 'https://docs.fluentsupport.com/tutorlms-integration',
1003 ],
1004 'telegram' => [
1005 'title' => __('Telegram', 'fluent-support'),
1006 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/telegram.jpeg',
1007 'is_integrated' => self::getFSIntegrationStatus('telegram_settings'),
1008 'description' => __('Business communication platform designed for security', 'fluent-support'),
1009 'doc_url' => 'https://docs.fluentsupport.com/managing-tickets-using-telegram',
1010 ],
1011 'fluent-crm' => [
1012 'title' => __('FluentCRM', 'fluent-support'),
1013 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-crm.png',
1014 'is_integrated' => defined('FLUENTCRM'),
1015 'description' => __('Self-hosted email and marketing automation for WordPress', 'fluent-support'),
1016 'doc_url' => 'https://docs.fluentsupport.com/fluentcrm-integration',
1017 ],
1018 'fluent-community' => [
1019 'title' => __('FluentCommunity', 'fluent-support'),
1020 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-community.png',
1021 'is_integrated' => defined('FLUENT_COMMUNITY_PLUGIN_VERSION'),
1022 'description' => __('Build and manage vibrant online communities with integrated LMS features directly within WordPress.', 'fluent-support'),
1023 'doc_url' => 'https://docs.fluentsupport.com/fluent-community-integration',
1024 ],
1025 'fluent-forms' => [
1026 'title' => __('Fluent Forms', 'fluent-support'),
1027 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-forms.png',
1028 'is_integrated' => defined('FLUENTFORM'),
1029 'description' => __('A robust form plugin suitable for any business', 'fluent-support'),
1030 'doc_url' => 'https://docs.fluentsupport.com/fluent-form-integration',
1031 ],
1032 'buddy-boss' => [
1033 'title' => __('BuddyBoss', 'fluent-support'),
1034 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/buddy-boss.png',
1035 'is_integrated' => defined('BP_PLUGIN_DIR'),
1036 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
1037 'doc_url' => 'https://docs.fluentsupport.com/buddyboss-integration'
1038 ],
1039 'discord' => [
1040 'title' => __('Discord', 'fluent-support'),
1041 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/discord.png',
1042 'is_integrated' => self::getFSIntegrationStatus('discord_settings'),
1043 'description' => __('Business communication platform designed for tech', 'fluent-support'),
1044 'doc_url' => 'https://docs.fluentsupport.com/managing-tickets-using-discord',
1045 ],
1046 'wishlist-member' => [
1047 'title' => __('WishList Member', 'fluent-support'),
1048 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/wishlist-member.png',
1049 'is_integrated' => defined('WLM3_PLUGIN_VERSION'),
1050 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
1051 'doc_url' => 'https://docs.fluentsupport.com/wishlist-member-integration',
1052 ],
1053 'easy-digital-downloads' => [
1054 'title' => __('Easy Digital Downloads', 'fluent-support'),
1055 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/easy-digital-downloads.png',
1056 'is_integrated' => class_exists('\Easy_Digital_Downloads'),
1057 'description' => __('The ultimate WordPress platform for digital products', 'fluent-support'),
1058 'doc_url' => 'https://docs.fluentsupport.com/edd-integration',
1059 ],
1060 'restrict-content-pro' => [
1061 'title' => __('Restrict Content pro', 'fluent-support'),
1062 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/restrict-content-pro.png',
1063 'is_integrated' => class_exists('\Restrict_Content_Pro' ),
1064 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
1065 'doc_url' => 'https://docs.fluentsupport.com/restrict-content-pro-integration',
1066 ],
1067 'better-docs' => [
1068 'title' => __('BetterDocs', 'fluent-support'),
1069 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/better-docs.png',
1070 'is_integrated' => false,
1071 'description' => __('The standard plugin for knowledge base and documentation', 'fluent-support'),
1072 'doc_url' => 'https://docs.fluentsupport.com/betterdocs-integration',
1073 ],
1074 'whatsapp' => [
1075 'title' => __('WhatsApp', 'fluent-support'),
1076 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/whatsapp.jpeg',
1077 'is_integrated' => self::getFSIntegrationStatus('twilio_settings'),
1078 'description' => __('Business communication platform designed for privacy', 'fluent-support'),
1079 'doc_url' => 'https://docs.fluentsupport.com/whatsapp-integration-via-twilio',
1080 ],
1081 'paymattic' => [
1082 'title' => __('Paymattic', 'fluent-support'),
1083 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/paymattic.png',
1084 'is_integrated' => defined('WPPAYFORM_VERSION'),
1085 'description' => __('All-in-one payment gateway designed for WordPress', 'fluent-support'),
1086 'doc_url' => 'https://paymattic.com/docs/how-to-integrate-fluent-support-with-paymattic-in-wordpress/',
1087 ],
1088 'learn-dash' => [
1089 'title' => __('LearnDash', 'fluent-support'),
1090 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/learn-dash.png',
1091 'is_integrated' => defined('LEARNDASH_VERSION'),
1092 'description' => __('The leading course platform built for WordPress', 'fluent-support'),
1093 'doc_url' => 'https://docs.fluentsupport.com/learndash-integration',
1094 ],
1095 'learn-press' => [
1096 'title' => __('LearnPress', 'fluent-support'),
1097 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/learn-press.png',
1098 'is_integrated' => defined('LP_PLUGIN_FILE'),
1099 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
1100 'doc_url' => 'https://docs.fluentsupport.com/learnpress-integration',
1101 ],
1102 'google-drive' => [
1103 'title' => __('Google Drive', 'fluent-support'),
1104 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-drive.jpeg',
1105 'is_integrated' => self::getFSIntegrationStatus('google_drive_settings'),
1106 'description' => __('A cloud storage service by Google for storing, syncing, and sharing files.', 'fluent-support'),
1107 'doc_url' => 'https://docs.fluentsupport.com/google-drive-integration'
1108 ],
1109 'dropbox' => [
1110 'title' => __('Dropbox', 'fluent-support'),
1111 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/dropbox.png',
1112 'is_integrated' => self::getFSIntegrationStatus('dropbox_settings'),
1113 'description' => __('A cloud-based file storage and sharing service that allows users to store files online and sync them across devices.', 'fluent-support'),
1114 'doc_url' => 'https://docs.fluentsupport.com/dropbox-integration',
1115 ],
1116 'member-press' => [
1117 'title' => __('MemberPress', 'fluent-support'),
1118 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/member-press.png',
1119 'is_integrated' => class_exists('MeprUtils'),
1120 'description' => __('A WordPress plugin that enables the creation and management of membership sites, including content access control and subscription billing.', 'fluent-support'),
1121 'doc_url' => 'https://docs.fluentsupport.com/memberpress-integration'
1122 ],
1123 'google-recaptcha' => [
1124 'title' => __('Google reCAPTCHA', 'fluent-support'),
1125 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-recaptcha.png',
1126 'is_integrated' => self::getFSIntegrationStatus('recaptcha_setting'),
1127 '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'),
1128 'doc_url' => 'https://docs.fluentsupport.com/google-recaptcha-integration',
1129 ],
1130 'fluent-boards' => [
1131 'title' => __('FluentBoards', 'fluent-support'),
1132 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-boards.png',
1133 'is_integrated' => defined('FLUENT_BOARDS'),
1134 'description' => __('A project management tool designed to streamline workflows and collaboration through customizable, kanban-style boards.', 'fluent-support'),
1135 'doc_url' => '',
1136 ],
1137 'fluent-booking' => [
1138 'title' => __('FluentBooking', 'fluent-support'),
1139 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-booking.svg',
1140 'is_integrated' => defined('FLUENT_BOOKING_VERSION'),
1141 'description' => __('Appointment and booking management plugin for WordPress', 'fluent-support'),
1142 'doc_url' => 'https://docs.fluentsupport.com/fluentbooking-integration',
1143 ],
1144 ];
1145
1146 return $connections;
1147 }
1148
1149 public static function getGlobalSettingsMenu()
1150 {
1151 $menu = [
1152 [
1153 'title' => __('Global Settings', 'fluent-support'),
1154 'route_name' => 'global_settings',
1155 'icon' => 'settings',
1156 ],
1157 [
1158 'title' => __('Ticket Tags', 'fluent-support'),
1159 'route_name' => 'tags',
1160 'icon' => 'ticketTag',
1161 ],
1162 [
1163 'title' => __('Ticket Form Config', 'fluent-support'),
1164 'route_name' => 'ticket-form-config',
1165 'icon' => 'formConfig',
1166 ],
1167 [
1168 'title' => __('Custom Fields', 'fluent-support'),
1169 'route_name' => 'custom_fields',
1170 'icon' => 'customFields',
1171 ],
1172 [
1173 'title' => __('Products', 'fluent-support'),
1174 'route_name' => 'products',
1175 'icon' => 'products',
1176 ],
1177 [
1178 'title' => __('Support Staff', 'fluent-support'),
1179 'route_name' => 'support-staffs',
1180 'icon' => 'supportStaffs',
1181 ],
1182 [
1183 'title' => __('FluentCRM Integration', 'fluent-support'),
1184 'route_name' => 'fluentcrm_integration',
1185 'icon' => 'crmIntegration',
1186 ],
1187 [
1188 'title' => __('Incoming Webhook', 'fluent-support'),
1189 'route_name' => 'incoming-webhook',
1190 'icon' => 'incomingWebhook',
1191 ],
1192 [
1193 'title' => __('Notification Integrations', 'fluent-support'),
1194 'route_name' => 'integration',
1195 'icon' => 'notification',
1196 ],
1197 [
1198 'title' => __('File Upload Integrations', 'fluent-support'),
1199 'route_name' => 'upload_integration',
1200 'icon' => 'fileUpload',
1201 ],
1202 [
1203 'title' => __('Auto Close Settings', 'fluent-support'),
1204 'route_name' => 'auto_close',
1205 'icon' => 'autoClose',
1206 ],
1207 [
1208 'title' => __('Ticket Importer', 'fluent-support'),
1209 'route_name' => 'ticket_importer',
1210 'icon' => 'importer',
1211 ],
1212 [
1213 'title' => __('Recaptcha', 'fluent-support'),
1214 'route_name' => 'reCaptcha',
1215 'icon' => 'reCaptcha',
1216 ],
1217 [
1218 'title' => __('Integration Statuses', 'fluent-support'),
1219 'route_name' => 'integration_statuses',
1220 'icon' => 'status',
1221 ],
1222 [
1223 'title' => __('AI Integration', 'fluent-support'),
1224 'icon' => 'aiIntegration',
1225 'children' => [
1226 [
1227 'title' => __('AI Model Setup', 'fluent-support'),
1228 'route_name' => 'ai_integration',
1229 ],
1230 [
1231 'title' => __('MCP for AI Agents', 'fluent-support'),
1232 'route_name' => 'mcp_settings',
1233 ],
1234 ],
1235 ],
1236 ];
1237
1238 if (defined('FLUENT_SUPPORT_PRO_DIR_FILE')) {
1239 $menu[] = [
1240 'title' => __('License Management', 'fluent-support'),
1241 'route_name' => 'license',
1242 'icon' => 'license',
1243 ];
1244 }
1245
1246 return apply_filters('fluent_support/settings_menu_items', $menu);
1247 }
1248
1249 /** @internal Not called from core controllers — available for Pro/hook usage. */
1250 public static function getFSIntegrationStatus($connection_name)
1251 {
1252 $integrationMap = [
1253 'slack_settings' => 'slack_settings',
1254 'discord_settings' => 'discord_settings',
1255 'twilio_settings' => 'twilio_settings',
1256 'telegram_settings' => 'telegram_settings',
1257 'google_drive_settings' => 'google_drive_settings',
1258 'dropbox_settings' => 'dropbox_settings',
1259 'recaptcha_setting' => '_fs_recaptcha_settings'
1260 ];
1261
1262 if (array_key_exists($connection_name, $integrationMap)) {
1263 if ($connection_name == 'google_drive_settings' || $connection_name == 'dropbox_settings') {
1264 return self::checkUploadDriverStatus($connection_name);
1265 } elseif ($connection_name == 'recaptcha_setting') {
1266 return self::checkRecaptchaStatus();
1267 } else {
1268 return self::checkNotificationIntegrationStatus($integrationMap[$connection_name]);
1269 }
1270 }
1271
1272 return false;
1273 }
1274
1275 private static function checkNotificationIntegrationStatus($settingName)
1276 {
1277 $settings = self::getIntegrationOption($settingName, null);
1278 if ($settings) {
1279 $status = Arr::get($settings, 'status', false);
1280 return $status ? true : false;
1281 }
1282 return false;
1283 }
1284
1285 private static function checkUploadDriverStatus($settingName)
1286 {
1287 $settings = self::getIntegrationOption($settingName, null);
1288 if ($settings) {
1289 $enabled = Arr::get($settings, 'status', false);
1290 return $enabled ? true : false;
1291 }
1292 return false;
1293 }
1294
1295 private static function checkRecaptchaStatus()
1296 {
1297 $reCaptchaSettingsData = Meta::where('object_type', '_fs_recaptcha_settings')->first();
1298
1299 if ($reCaptchaSettingsData) {
1300 $settings = static::safeUnserialize($reCaptchaSettingsData->value);
1301 $status = Arr::get($settings, 'is_enabled', false);
1302 return $status == 'true' ? true : false;
1303 }
1304 return false;
1305 }
1306
1307 public static function getAIActivities($data)
1308 {
1309 $page = isset($data['page']) ? intval($data['page']) : 1;
1310 $perPage = isset($data['per_page']) ? intval($data['per_page']) : 10;
1311
1312 $activitiesQuery = AIActivityLogs::with([
1313 'person' => function ($query) {
1314 $query->select(['first_name', 'person_type', 'last_name', 'id', 'avatar']);
1315 },
1316 'ticket' => function ($query) {
1317 $query->select(['id', 'title']);
1318 }
1319 ])->latest('id');
1320
1321 $from = sanitize_text_field( Arr::get( $data, 'from', '' ) );
1322 $to = sanitize_text_field( Arr::get( $data, 'to', '') );
1323
1324 if ( $from != $to ) {
1325 $from = $from . ' ' . '00:00:00';
1326 $to = $to . ' ' . '23:59:59';
1327 }
1328
1329 if ( ( !empty($from) && !empty($to) ) && $from == $to ) {
1330 $activitiesQuery->whereDate('created_at', '=', $from);
1331 } elseif (!empty($from) && !empty($to)) {
1332 $activitiesQuery->whereBetween('created_at', [ $from, $to ]);
1333 }
1334
1335 $agentId = intval( Arr::get($data, 'filters.agent_id') );
1336
1337 if ($agentId) {
1338 $activitiesQuery->where('agent_id', $agentId);
1339 }
1340
1341 $activities = $activitiesQuery->paginate($perPage, ['*'], 'page', $page);
1342
1343 $settings = static::getSettings();
1344
1345 return [
1346 'data' => $activities->items(),
1347 'total' => $activities->total(),
1348 'per_page' => $activities->perPage(),
1349 'current_page' => $activities->currentPage(),
1350 'last_page' => $activities->lastPage(),
1351 'settings' => $settings['ai_activity_settings']
1352 ];
1353 }
1354
1355 public static function updateAISettings($settings)
1356 {
1357 $defaults = [
1358 'delete_days' => 14,
1359 'disable_logs' => 'no'
1360 ];
1361
1362 $settings = wp_parse_args($settings, $defaults);
1363 $settings['delete_days'] = (int)$settings['delete_days'];
1364
1365 Helper::updateOption('_ai_activity_settings', $settings);
1366
1367 return [
1368 'message' => __('AI Activity settings have been updated', 'fluent-support')
1369 ];
1370 }
1371
1372 public static function getSettings()
1373 {
1374 $settings = Helper::getOption('_ai_activity_settings', []);
1375
1376 $defaults = [
1377 'delete_days' => 14,
1378 'disable_logs' => 'no'
1379 ];
1380
1381 $settings = wp_parse_args($settings, $defaults);
1382
1383 if (! $settings ) throw new \Exception(esc_html__('No activity settings found', 'fluent-support'));
1384
1385 return [
1386 'ai_activity_settings' => $settings
1387 ];
1388 }
1389
1390 /**
1391 * Check if activity logs are disabled for a given option key
1392 * @param string $optionKey The option key to check
1393 * @return bool
1394 */
1395 public static function areLogsDisabled($optionKey)
1396 {
1397 if (empty($optionKey)) {
1398 return false;
1399 }
1400
1401 $settings = Helper::getOption($optionKey, []);
1402 $defaults = [
1403 'disable_logs' => 'no'
1404 ];
1405 $settings = wp_parse_args($settings, $defaults);
1406 return isset($settings['disable_logs']) && $settings['disable_logs'] === 'yes';
1407 }
1408
1409 public static function getIp($anonymize = false)
1410 {
1411 static $ipAddress;
1412
1413 if ($ipAddress) {
1414 return $ipAddress;
1415 }
1416
1417 if (empty($_SERVER['REMOTE_ADDR'])) {
1418 // It's a local cli request
1419 return '127.0.0.1';
1420 }
1421
1422 $ipAddress = '';
1423 $remoteAddr = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
1424
1425 if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
1426 //If it's a valid Cloudflare request
1427 if (self::isCfIp($remoteAddr)) {
1428 //Use the CF-Connecting-IP header.
1429 $ipAddress = sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP']));
1430 } else {
1431 //If it isn't valid, then use REMOTE_ADDR.
1432 $ipAddress = $remoteAddr;
1433 }
1434 } else if ($remoteAddr == '127.0.0.1') {
1435 // most probably it's local reverse proxy
1436 if (isset($_SERVER["HTTP_CLIENT_IP"])) {
1437 $ipAddress = sanitize_text_field(wp_unslash($_SERVER["HTTP_CLIENT_IP"]));
1438 } else if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1439 $forwardedFor = sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']));
1440 $splitResult = preg_split('/,/', $forwardedFor);
1441 $firstIp = is_array($splitResult) ? current($splitResult) : '';
1442 $ipAddress = (string)rest_is_ip_address(trim((string)$firstIp));
1443 }
1444 }
1445
1446 if (!$ipAddress) {
1447 $ipAddress = $remoteAddr;
1448 }
1449
1450 if ($ipAddress) {
1451 $ipAddress = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', $ipAddress);
1452 }
1453
1454 $ipAddress = apply_filters('fluent_auth/user_ip', $ipAddress);
1455
1456 if ($anonymize) {
1457 return wp_privacy_anonymize_ip($ipAddress);
1458 }
1459
1460 $ipAddress = sanitize_text_field(wp_unslash($ipAddress));
1461
1462 return $ipAddress;
1463 }
1464
1465 /** @internal Not called from core controllers — available for Pro/hook usage. */
1466 public static function isCfIp($ip = '')
1467 {
1468 if (!$ip) {
1469 $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
1470 }
1471 $cloudflareIPRanges = array(
1472 '103.21.244.0/22',
1473 '103.22.200.0/22',
1474 '103.31.4.0/22',
1475 '104.16.0.0/13',
1476 '104.24.0.0/14',
1477 '108.162.192.0/18',
1478 '131.0.72.0/22',
1479 '141.101.64.0/18',
1480 '162.158.0.0/15',
1481 '172.64.0.0/13',
1482 '173.245.48.0/20',
1483 '188.114.96.0/20',
1484 '190.93.240.0/20',
1485 '197.234.240.0/22',
1486 '198.41.128.0/17'
1487 );
1488 $validCFRequest = false;
1489 //Make sure that the request came via Cloudflare.
1490 foreach ($cloudflareIPRanges as $range) {
1491 //Use the ip_in_range function from Joomla.
1492 if (self::ipInRange($ip, $range)) {
1493 //IP is valid. Belongs to Cloudflare.
1494 return true;
1495 }
1496 }
1497
1498 return false;
1499 }
1500
1501 private static function ipInRange($ip, $range)
1502 {
1503 if (!$ip || !$range || !is_string($ip) || !is_string($range)) {
1504 return false;
1505 }
1506
1507 if (strpos($range, '/') !== false) {
1508 // $range is in IP/NETMASK format
1509 list($range, $netmask) = explode('/', $range, 2);
1510 if (strpos($netmask, '.') !== false) {
1511 // $netmask is a 255.255.0.0 format
1512 $netmask = str_replace('*', '0', $netmask);
1513 $netmask_dec = ip2long($netmask);
1514 return ((ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec));
1515 } else {
1516 // $netmask is a CIDR size block
1517 // fix the range argument
1518 $x = explode('.', $range);
1519 while (count($x) < 4) $x[] = '0';
1520 list($a, $b, $c, $d) = $x;
1521 $range = sprintf("%u.%u.%u.%u", empty($a) ? '0' : $a, empty($b) ? '0' : $b, empty($c) ? '0' : $c, empty($d) ? '0' : $d);
1522 $range_dec = ip2long($range);
1523 $ip_dec = ip2long($ip);
1524
1525 # Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s
1526 #$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0'));
1527
1528 # Strategy 2 - Use math to create it
1529 $wildcard_dec = pow(2, (32 - $netmask)) - 1;
1530 $netmask_dec = ~$wildcard_dec;
1531
1532 return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec));
1533 }
1534 } else {
1535 // range might be 255.255.*.* or 1.2.3.0-1.2.3.255
1536 if (strpos($range, '*') !== false) { // a.b.*.* format
1537 // Just convert to A-B format by setting * to 0 for A and 255 for B
1538 $lower = str_replace('*', '0', $range);
1539 $upper = str_replace('*', '255', $range);
1540 $range = "$lower-$upper";
1541 }
1542
1543 if (strpos($range, '-') !== false) { // A-B format
1544 list($lower, $upper) = explode('-', $range, 2);
1545 $lower_dec = (float)sprintf("%u", ip2long($lower));
1546 $upper_dec = (float)sprintf("%u", ip2long($upper));
1547 $ip_dec = (float)sprintf("%u", ip2long($ip));
1548 return (($ip_dec >= $lower_dec) && ($ip_dec <= $upper_dec));
1549 }
1550 return false;
1551 }
1552 }
1553
1554 public static function loadView($template, $data)
1555 {
1556 extract($data, EXTR_OVERWRITE);
1557
1558 $template = sanitize_file_name($template);
1559
1560 $template = str_replace('.', DIRECTORY_SEPARATOR, $template);
1561
1562 ob_start();
1563 include FLUENT_SUPPORT_PLUGIN_PATH . 'app/Views/emails/' . $template . '.php';
1564 return ob_get_clean();
1565 }
1566
1567 public static function isProductRequired()
1568 {
1569 $settings = Helper::getOption('_ticket_form_settings', []);
1570 return Arr::get($settings, 'product_required_field') === 'yes';
1571 }
1572
1573 /** @internal Not called from core controllers — available for Pro/hook usage. */
1574 public static function getBusinessBox()
1575 {
1576 $businessEmailBoxes = MailBox::select(['id', 'name', 'email', 'mapped_email'])
1577 ->where('box_type', 'email')
1578 ->get();
1579 return $businessEmailBoxes;
1580 }
1581
1582 public static function tempImageMoveUploadDir($ticketId, $contentType, $replyId = null, $personId = null)
1583 {
1584 // Fetch content based on the content type
1585 $content = self::getContentByType($ticketId, $contentType , $replyId);
1586 if (empty($content)) {
1587 return;
1588 }
1589
1590 // Extract image URLs from the content
1591 $imageUrls = self::extractImageUrls($content);
1592 if (empty($imageUrls)) {
1593 return;
1594 }
1595
1596 // Move images to the upload directory and update content
1597 self::moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId, $personId);
1598 }
1599
1600 /**
1601 * Fetch content based on the content type.
1602 */
1603 private static function getContentByType($ticketId, $contentType, $replyId)
1604 {
1605 if ($contentType == 'ticket-create') {
1606 $ticket = Ticket::find($ticketId);
1607 return $ticket ? $ticket->content : null;
1608 }
1609
1610 $conversation = Conversation::find($replyId);
1611 return $conversation ? $conversation->content : null;
1612 }
1613
1614 /**
1615 * Extract image URLs from the content.
1616 */
1617 private static function extractImageUrls($content)
1618 {
1619 preg_match_all('/<img[^>]+src=(["\'])(.*?)\1/i', (string) $content, $matches);
1620 return $matches[2] ?? [];
1621 }
1622
1623 /**
1624 * Move images to the upload directory and update content.
1625 */
1626 private static function moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId, $personId)
1627 {
1628 // Get the current site's upload directory
1629 $uploadDirInfo = wp_upload_dir();
1630 $uploadsDir = $uploadDirInfo['basedir'];
1631 $tempDir = $uploadsDir . '/fluent-support/temp_files/';
1632 $signedAttachments = self::getSignedImageAttachments($imageUrls, $ticketId, $personId);
1633
1634 foreach ($imageUrls as $imageUrl) {
1635 $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1636 if ($fileHash && isset($signedAttachments[$fileHash]) && ($referenceUrl = self::finalizeSignedTempImage($signedAttachments[$fileHash], $ticketId, $contentType, $replyId, $personId))) {
1637 $content = str_replace($imageUrl, $referenceUrl, $content);
1638 continue;
1639 }
1640
1641 // Build the absolute path for the temporary file
1642 $imageRelativePath = $tempDir . basename($imageUrl);
1643 $absolutePath = $imageRelativePath;
1644
1645 // Move the file to the ticket-specific folder
1646 $newFileInfo = UploadService::copyFileTicketFolder($absolutePath, $ticketId);
1647
1648 // Check if the move was successful
1649 if (empty($newFileInfo['file_path'])) {
1650 continue; // Skip if the file couldn't be copied
1651 }
1652
1653 // Ensure the new URL is correctly constructed
1654 $newFileInfo['url'] = trailingslashit($uploadDirInfo['baseurl']) . 'fluent-support/ticket_' . $ticketId . '/' . basename($newFileInfo['file_path']);
1655
1656 // Replace the old URL with the new one in the content
1657 $content = str_replace($imageUrl, $newFileInfo['url'], $content);
1658 }
1659
1660 // Save the updated content
1661 self::saveUpdatedContent($ticketId, $contentType, $content, $replyId);
1662 }
1663
1664 private static function getSignedImageAttachments($imageUrls, $ticketId, $personId)
1665 {
1666 $fileHashes = [];
1667 foreach ($imageUrls as $imageUrl) {
1668 $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1669 if ($fileHash) {
1670 $fileHashes[] = $fileHash;
1671 }
1672 }
1673
1674 $fileHashes = array_values(array_unique($fileHashes));
1675 if (!$fileHashes) {
1676 return [];
1677 }
1678
1679 $attachments = Attachment::whereIn('file_hash', $fileHashes)
1680 ->where(function ($query) use ($ticketId, $personId) {
1681 $query->where('ticket_id', $ticketId);
1682
1683 if ($personId) {
1684 $query->orWhere(function ($query) use ($personId) {
1685 $query->whereNull('ticket_id')
1686 ->where('person_id', $personId);
1687 });
1688 }
1689 })
1690 ->get();
1691 $mappedAttachments = [];
1692 foreach ($attachments as $attachment) {
1693 $mappedAttachments[$attachment->file_hash] = $attachment;
1694 }
1695
1696 return $mappedAttachments;
1697 }
1698
1699 private static function finalizeSignedTempImage($attachment, $ticketId, $contentType, $replyId, $personId)
1700 {
1701 if (!$attachment || $attachment->driver !== 'local') {
1702 return false;
1703 }
1704
1705 if ($attachment->ticket_id && intval($attachment->ticket_id) !== intval($ticketId)) {
1706 return false;
1707 }
1708
1709 if (!$attachment->ticket_id && $personId && intval($attachment->person_id) !== intval($personId)) {
1710 return false;
1711 }
1712
1713 if ($attachment->conversation_id && $replyId && intval($attachment->conversation_id) !== intval($replyId)) {
1714 return false;
1715 }
1716
1717 if ($attachment->conversation_id && $contentType === 'ticket-create') {
1718 return false;
1719 }
1720
1721 if ($attachment->status !== 'in-active') {
1722 return $attachment->ticket_id ? self::getAttachmentReferenceUrl($attachment) : false;
1723 }
1724
1725 if (!$attachment->file_path || !file_exists($attachment->file_path)) {
1726 return false;
1727 }
1728
1729 $newFileInfo = UploadService::copyFileTicketFolder($attachment->file_path, $ticketId);
1730 if (empty($newFileInfo['file_path'])) {
1731 return false;
1732 }
1733
1734 $attachment->file_path = $newFileInfo['file_path'];
1735 $attachment->full_url = $newFileInfo['url'];
1736 $attachment->ticket_id = $attachment->ticket_id ?: $ticketId;
1737 $attachment->status = 'inline';
1738
1739 if ($contentType !== 'ticket-create' && $replyId) {
1740 $attachment->conversation_id = $replyId;
1741 }
1742
1743 $attachment->save();
1744
1745 return self::getAttachmentReferenceUrl($attachment);
1746 }
1747
1748 private static function extractAttachmentHashFromUrl($imageUrl)
1749 {
1750 $decodedUrl = html_entity_decode($imageUrl, ENT_QUOTES, 'UTF-8');
1751 $query = wp_parse_url($decodedUrl, PHP_URL_QUERY);
1752
1753 if (!$query) {
1754 return '';
1755 }
1756
1757 parse_str($query, $params);
1758
1759 return !empty($params['fst_file']) ? sanitize_text_field($params['fst_file']) : '';
1760 }
1761
1762 private static function getAttachmentReferenceUrl($attachment)
1763 {
1764 return add_query_arg([
1765 'fst_file' => $attachment->file_hash
1766 ], site_url('/index.php'));
1767 }
1768
1769 public static function refreshSignedAttachmentUrls($content, $ticketId = null)
1770 {
1771 $contents = self::refreshSignedAttachmentUrlsInContents([$content], $ticketId);
1772
1773 return $contents[0];
1774 }
1775
1776 public static function refreshSignedAttachmentUrlsInContents($contents, $ticketId = null)
1777 {
1778 $fileHashes = [];
1779
1780 foreach ($contents as $content) {
1781 foreach (self::extractImageUrls($content) as $imageUrl) {
1782 $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1783 if ($fileHash) {
1784 $fileHashes[] = $fileHash;
1785 }
1786 }
1787 }
1788
1789 $fileHashes = array_values(array_unique($fileHashes));
1790 if (!$fileHashes) {
1791 return $contents;
1792 }
1793
1794 $attachmentsQuery = Attachment::whereIn('file_hash', $fileHashes);
1795 if ($ticketId) {
1796 $attachmentsQuery->where('ticket_id', $ticketId);
1797 }
1798
1799 $attachments = $attachmentsQuery->get();
1800 $attachmentsByHash = [];
1801
1802 foreach ($attachments as $attachment) {
1803 $attachmentsByHash[$attachment->file_hash] = $attachment;
1804 }
1805
1806 foreach ($contents as $key => $content) {
1807 foreach (self::extractImageUrls($content) as $imageUrl) {
1808 $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1809 if (!$fileHash || empty($attachmentsByHash[$fileHash])) {
1810 continue;
1811 }
1812
1813 $content = str_replace($imageUrl, $attachmentsByHash[$fileHash]->secureUrl, $content);
1814 }
1815
1816 $contents[$key] = $content;
1817 }
1818
1819 return $contents;
1820 }
1821
1822 /**
1823 * Save the updated content based on the content type.
1824 */
1825 private static function saveUpdatedContent($ticketId, $contentType, $content, $replyId)
1826 {
1827 if ($contentType == 'ticket-create') {
1828 $ticket = Ticket::find($ticketId);
1829 if ($ticket) {
1830 $ticket->content = $content;
1831 $ticket->save();
1832 }
1833 } else {
1834 $conversation = Conversation::find($replyId);
1835 if ($conversation) {
1836 $conversation->content = $content;
1837 $conversation->save();
1838 }
1839 }
1840 }
1841
1842 /**
1843 * Throw a ValidationException with a safe error message.
1844 * In debug mode the real exception message is used; in production a generic message is returned.
1845 * Throwing instead of returning ensures the error bypasses the framework's HTTP exception
1846 * wrapper (which would prepend a status-code prefix to the message).
1847 *
1848 * @param \Throwable $e The original exception.
1849 * @throws \FluentSupport\Framework\Validator\ValidationException Always thrown.
1850 */
1851 public static function getSafeErrorMessage($e)
1852 {
1853 $message = $e->getMessage() ?: __('Something went wrong. Please try again later.', 'fluent-support');
1854
1855 // ValidationException carries the message through the framework's JSON error
1856 // handler — it is never echoed directly. $e is the chained previous exception,
1857 // not output. phpcs:disable covers the multi-line constructor call.
1858 // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped
1859 throw new \FluentSupport\Framework\Validator\ValidationException(
1860 '', 422, ($e instanceof \Exception ? $e : null), ['message' => $message]
1861 );
1862 // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
1863 }
1864
1865 /**
1866 * Safely unserialize data.
1867 *
1868 * @param string $data The serialized data.
1869 * @return mixed The unserialized data or the original data if not serialized.
1870 */
1871 public static function safeUnserialize($data)
1872 {
1873 if (is_serialized($data)) { // Don't attempt to unserialize data that wasn't serialized going in.
1874 return @unserialize(trim($data), ['allowed_classes' => false]);
1875 }
1876
1877 return $data;
1878 }
1879 }
1880