PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.2.1
Fluent Support – Helpdesk & Customer Support Ticket System v2.2.1
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.2.1, at app/Services/Helper.php

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