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

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

1,564 lines 58.1 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\Ticket;
8 use FluentSupport\App\Models\Conversation;
9 use FluentSupport\App\Models\Customer;
10 use FluentSupport\App\Models\MailBox;
11 use FluentSupport\App\Models\Meta;
12 use FluentSupport\App\Models\AIActivityLogs;
13 use FluentSupport\App\Models\Person;
14 use FluentSupport\App\Models\Product;
15 use FluentSupport\App\Services\Includes\UploadService;
16 use FluentSupport\App\Services\EmailNotification\Settings;
17 use FluentSupport\Framework\Support\Arr;
18
19 /**
20 * Helper - REST API Helper Class
21 *
22 * App helper for REST API
23 *
24 * @package FluentSupport\App\Services
25 *
26 * @version 1.0.0
27 */
28 class Helper
29 {
30 public static function FluentSupport($module = null)
31 {
32 return App::getInstance($module);
33 }
34
35 /**
36 * Get agent information by user id
37 * The function will get user id as parameter or get id from session and return agent information
38 * @param null $userId
39 * @return false | Agent
40 */
41 public static function getAgentByUserId($userId = null)
42 {
43 if ($userId === null) {
44 $userId = get_current_user_id();
45 }
46 if (!$userId) {
47 return false;
48 }
49 return Agent::where('user_id', $userId)->first();
50 }
51
52 /**
53 * This function will return the list of ticket priorities list for customer
54 *
55 * @return mixed
56 */
57 public static function customerTicketPriorities()
58 {
59 return apply_filters('fluent_support/customer_ticket_priorities', [
60 'normal' => __('Normal', 'fluent-support'),
61 'medium' => __('Medium', 'fluent-support'),
62 'critical' => __('Critical', 'fluent-support')
63 ]);
64 }
65
66 /**
67 * This function will return the list of ticket priorities list for Admin
68 *
69 * @return mixed
70 */
71 public static function adminTicketPriorities()
72 {
73 return apply_filters('fluent_support/admin_ticket_priorities', [
74 'normal' => __('Normal', 'fluent-support'),
75 'medium' => __('Medium', 'fluent-support'),
76 'critical' => __('Critical', 'fluent-support')
77 ]);
78 }
79
80
81 /**
82 * This function will return ticket status group
83 *
84 * @return mixed
85 */
86 public static function ticketStatusGroups()
87 {
88 return apply_filters('fluent_support/ticket_status_groups', [
89 'open' => ['new', 'active'],
90 'active' => ['active'],
91 'closed' => ['closed'],
92 'new' => ['new'],
93 'all' => []
94 ]);
95 }
96
97 /**
98 * This function will return custom ticket status group
99 *
100 * @return mixed
101 */
102 public static function changeableTicketStatuses()
103 {
104 $ticketStatus = static::ticketStatusGroups();
105
106 unset($ticketStatus['all']);
107 unset($ticketStatus['open']);
108
109 return apply_filters('fluent_support/changeable_ticket_statuses', $ticketStatus);
110 }
111
112 /**
113 * This function will return ticket status list
114 *
115 * @return mixed
116 */
117 public static function ticketStatuses()
118 {
119 return apply_filters('fluent_support/ticket_statuses', [
120 'new' => __('New', 'fluent-support'),
121 'active' => __('Active', 'fluent-support'),
122 'closed' => __('Closed', 'fluent-support'),
123 ]);
124 }
125
126 public static function getTkStatusesByGroupName($groupName)
127 {
128 $groups = self::ticketStatusGroups();
129 return Arr::get($groups, $groupName, []);
130 }
131
132 public static function ticketAcceptedFileMiles()
133 {
134 $groups = self::getMimeGroups();
135 $globalSettings = (new Settings())->globalBusinessSettings();
136
137 if (empty($globalSettings['accepted_file_types'])) {
138 return apply_filters('fluent_support/accepted_ticket_mimes', []);
139 }
140
141 $mimes = [];
142 $typesGroups = Arr::only($groups, $globalSettings['accepted_file_types']);
143 foreach ($typesGroups as $mimesGroup) {
144 $mimes = array_merge($mimes, $mimesGroup['mimes']);
145 }
146
147 return apply_filters('fluent_support/accepted_ticket_mimes', $mimes);
148 }
149
150 public static function getAcceptedMimeHeadings()
151 {
152 $groups = self::getMimeGroups();
153 $globalSettings = (new Settings())->globalBusinessSettings();
154
155 if (empty($globalSettings['accepted_file_types'])) {
156 return [];
157 }
158
159 $mimeNames = [];
160 $typesGroups = Arr::only($groups, $globalSettings['accepted_file_types']);
161 foreach ($typesGroups as $mimesGroup) {
162 $mimeNames[] = $mimesGroup['title'];
163 }
164
165 return $mimeNames;
166 }
167
168 public static function getFileUploadMessage()
169 {
170 $mimeHeadings = self::getAcceptedMimeHeadings();
171 $settings = (new Settings())->globalBusinessSettings();
172 $maxFileSize = floatval($settings['max_file_size']);
173
174 // translators: %1$s is a comma-separated list of supported file types, %2$.01f is the maximum file size in megabytes
175 return sprintf(__('Supported Types: %1$s and max file size: %2$.01fMB', 'fluent-support'), implode(', ', $mimeHeadings), $maxFileSize);
176 }
177
178 public static function getMimeGroups()
179 {
180 return apply_filters('fluent_support/mime_groups', [
181 'images' => [
182 'title' => __('Photos', 'fluent-support'),
183 'mimes' => [
184 'image/gif',
185 'image/ief',
186 'image/jpeg',
187 'image/webp',
188 'image/pjpeg',
189 'image/ktx',
190 'image/png'
191 ]
192 ],
193 'csv' => [
194 'title' => __('CSV', 'fluent-support'),
195 'mimes' => [
196 'application/csv',
197 'application/txt',
198 'text/csv',
199 'text/plain',
200 'text/comma-separated-values',
201 'text/anytext',
202 ]
203 ],
204 'documents' => [
205 'title' => __('PDF/Docs', 'fluent-support'),
206 'mimes' => [
207 'application/excel',
208 'application/vnd.ms-excel',
209 'application/vnd.msexcel',
210 'application/octet-stream',
211 'application/pdf',
212 'application/msword',
213 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
214 ]
215 ],
216 'zip' => [
217 'title' => __('Zip', 'fluent-support'),
218 'mimes' => [
219 'application/zip'
220 ]
221 ],
222 'json' => [
223 'title' => __('JSON', 'fluent-support'),
224 'mimes' => [
225 'application/json',
226 'application/jsonml+json'
227 ]
228 ]
229 ]);
230 }
231
232 /**
233 * getOption method will return settings using key
234 * This method will get key as parameter, fetch data from database, beautify the data and return
235 * @param $key
236 * @param string $default
237 * @return mixed|string
238 */
239 public static function getOption($key, $default = '')
240 {
241 //Get settings from meta table using the key
242 $data = Meta::where('object_type', 'option')
243 ->where('key', $key)
244 ->first();
245
246 if ($data) {
247 $value = static::safeUnserialize($data->value);
248 if ($value) {
249 return $value;
250 }
251 }
252
253 return $default;
254 }
255
256 /**
257 * updateOption method will update or insert settings
258 * 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
259 * @param $key
260 * @param $value
261 * @return mixed
262 */
263 public static function updateOption($key, $value)
264 {
265 //Get settings from meta table using the key
266 $data = Meta::where('object_type', 'option')
267 ->where('key', $key)
268 ->first();
269
270 //If data is available, update existing data and return
271 if ($data) {
272 return Meta::where('id', $data->id)
273 ->update([
274 'value' => maybe_serialize($value)
275 ]);
276 }
277
278 //If newly submit, create new record and return
279 return Meta::insert([
280 'object_type' => 'option',
281 'key' => $key,
282 'value' => maybe_serialize($value)
283 ]);
284 }
285
286 /** @internal Not called from core controllers — available for Pro/hook usage. */
287 public static function deleteOption($key)
288 {
289 return Meta::where('object_type', 'option')
290 ->where('key', $key)
291 ->delete();
292 }
293
294 /**
295 * getIntegrationOption method will return the integration settings by integration key
296 * @param $key
297 * @param string $default
298 * @return mixed|string
299 */
300 public static function getIntegrationOption($key, $default = '')
301 {
302 $data = Meta::where('object_type', 'integration_settings')
303 ->where('key', $key)
304 ->first();
305
306 if ($data) {
307 $value = static::safeUnserialize($data->value);
308 if ($value) {
309 return $value;
310 }
311 }
312
313 return $default;
314 }
315
316 /**
317 * updateIntegrationOption method will update existing settings or create new settings by integration key
318 * @param $key
319 * @param $value
320 * @return mixed
321 */
322 public static function updateIntegrationOption($key, $value)
323 {
324 $data = Meta::where('object_type', 'integration_settings')
325 ->where('key', $key)
326 ->first();
327
328 if ($data) {
329 return Meta::where('id', $data->id)
330 ->update([
331 'value' => maybe_serialize($value)
332 ]);
333 }
334
335 return Meta::insert([
336 'object_type' => 'integration_settings',
337 'key' => $key,
338 'value' => maybe_serialize($value)
339 ]);
340 }
341
342 public static function getTicketViewUrl($ticket)
343 {
344 $baseUrl = self::getPortalBaseUrl();
345 return $baseUrl . '/#/ticket/' . $ticket->id . '/view';
346 }
347
348 public static function getTicketViewSignedUrl($ticket)
349 {
350 if (!self::isPublicSignedTicketEnabled()) {
351 return self::getTicketViewUrl($ticket);
352 }
353
354 $baseUrl = self::getPortalBaseUrl();
355
356 $baseUrl = add_query_arg([
357 'fs_view' => 'ticket',
358 'support_hash' => $ticket->hash,
359 'ticket_id' => $ticket->id,
360 ], $baseUrl);
361
362 return $baseUrl . '#/ticket/' . $ticket->id . '/view';
363 }
364
365 public static function saveOpenAIData($objectType, $key, $data)
366 {
367 $serializedData = maybe_serialize($data);
368
369 $previousValue = Meta::where('object_type', $objectType)->first();
370
371 if ($previousValue) {
372 return Meta::where('object_type', $objectType)->update([
373 'value' => $serializedData
374 ]);
375 } else {
376 return Meta::insert([
377 'object_type' => $objectType,
378 'key' => $key,
379 'value' => $serializedData
380 ]);
381 }
382
383 }
384
385 public static function authorizeChatGPTAPIKey($data)
386 {
387 return wp_remote_get('https://api.openai.com/v1/models', [
388 'headers' => [
389 'Authorization' => 'Bearer ' . $data['api_key'],
390 'Content-Type' => 'application/json'
391 ]
392 ]);
393 }
394
395 public static function isPublicSignedTicketEnabled()
396 {
397 $businessSettings = self::getBusinessSettings();
398
399 return (Arr::get($businessSettings, 'disable_public_ticket') != 'yes');
400 }
401
402 public static function getTicketAdminUrl($ticket)
403 {
404 $baseUrl = self::getPortalAdminBaseUrl();
405 return $baseUrl . 'tickets/' . $ticket->id . '/view';
406 }
407
408 /**
409 * getPortalBaseUrl will get the portal page id and return link of the page
410 * @return mixed
411 */
412 public static function getPortalBaseUrl()
413 {
414 $businessSettings = self::getBusinessSettings();
415 $baseUrl = get_permalink($businessSettings['portal_page_id']);
416 $baseUrl = rtrim($baseUrl, '/\\');
417 return apply_filters('fluent_support/portal_base_url', $baseUrl);
418 }
419
420 public static function getPortalAdminBaseUrl()
421 {
422 return apply_filters('fluent_support/portal_admin_base_url', admin_url('admin.php?page=fluent-support/#/'));
423 }
424
425 public static function getBusinessSettings($key = null)
426 {
427 static $settings;
428
429 if ($settings && $key) {
430 return Arr::get($settings, $key);
431 }
432
433 if ($settings) {
434 return $settings;
435 }
436
437 $settings = (new Settings())->globalBusinessSettings();
438
439 if ($key) {
440 return Arr::get($settings, $key);
441 }
442 return $settings;
443 }
444
445 public static function isAgentFeedbackEnabled()
446 {
447 return self::getBusinessSettings('agent_feedback_rating', 'no') == 'yes';
448 }
449
450 public static function getTicketMeta($ticketId, $key, $default = '')
451 {
452 $data = Meta::where('object_type', 'ticket_meta')
453 ->where('key', $key)
454 ->where('object_id', $ticketId)
455 ->first();
456
457 if ($data) {
458 $value = static::safeUnserialize($data->value);
459 if ($value) {
460 return $value;
461 }
462 }
463
464 return $default;
465 }
466
467 public static function updateTicketMeta($ticketId, $key, $value)
468 {
469 $data = Meta::where('object_type', 'ticket_meta')
470 ->where('key', $key)
471 ->where('object_id', $ticketId)
472 ->first();
473
474 if ($data) {
475 return Meta::where('id', $data->id)
476 ->update([
477 'value' => maybe_serialize($value)
478 ]);
479 }
480
481 return Meta::insert([
482 'object_type' => 'ticket_meta',
483 'object_id' => $ticketId,
484 'key' => $key,
485 'value' => maybe_serialize($value)
486 ]);
487 }
488
489 public static function getWPPages()
490 {
491 $pages = (self::FluentSupport())->app->db
492 ->table('posts')
493 ->select(['ID', 'post_title'])
494 ->where('post_type', 'page')
495 ->where('post_status', 'publish')
496 ->latest('ID')
497 ->get();
498 $formattedPages = [];
499 foreach ($pages as $page) {
500 $formattedPages[] = [
501 'id' => intval($page->ID),
502 'title' => $page->post_title ?: __('(no title)', 'fluent-support')
503 ];
504 }
505 return $formattedPages;
506 }
507
508 public static function getDefaultMailBox()
509 {
510 $mailbox = MailBox::where('is_default', 'yes')->first();
511
512 if ($mailbox) {
513 return $mailbox;
514 }
515
516 return MailBox::oldest('id')->first();
517 }
518
519 public static function getCurrentAgent()
520 {
521 // If user is logged in then return the agent by user id.
522 // This `get_current_user_id` function is WP function and
523 // it returns user id if user is logged in.
524 if (get_current_user_id()) {
525 return Agent::where('user_id', get_current_user_id())->first();
526 }
527 }
528
529 public static function getCurrentCustomer()
530 {
531 // If user is logged in then return the customer by user id.
532 // This `get_current_user_id` function is WP function and
533 // it returns user id if user is logged in.
534 if (get_current_user_id()) { //if user is logged in
535 return Customer::where('user_id', get_current_user_id())->first();
536 }
537 }
538
539 public static function getCurrentPerson()
540 {
541 // If user is logged in then return the person(agent/customer) by user id.
542 // This `get_current_user_id` function is WP function and
543 // it returns user id if user is logged in.
544 if (get_current_user_id()) {
545 return Person::where('user_id', get_current_user_id())
546 ->orderBy('id', 'ASC')
547 ->first();
548 }
549 return null;
550 }
551
552 public static function getCustomerByID($customerid)
553 {
554 return Customer::where('id', $customerid)->first();
555 }
556
557 /** @internal Not called from core controllers — available for Pro/hook usage. */
558 public static function sanitizeOrderValue($orderType = '')
559 {
560 $orderBys = ['ASC', 'DESC'];
561
562 $orderType = trim(strtoupper((string)($orderType ?? '')));
563
564 return in_array($orderType, $orderBys) ? $orderType : 'DESC';
565 }
566
567 public static function getFluentCRMTagConfig()
568 {
569 if (!defined('FLUENTCRM')) {
570 return [
571 'can_add_tags' => false,
572 'tags' => [],
573 'lists' => [],
574 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluentcrm-logo.svg',
575 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluent-crm-icon.png',
576 ];
577 }
578
579 $canAddTags = \FluentCrm\App\Services\PermissionManager::currentUserCan('fcrm_manage_contacts');
580
581 $canAddTags = apply_filters('fluent_support/can_user_add_tags_to_customer', $canAddTags);
582 $crmTags = [];
583 $crmLists = [];
584 if ($canAddTags) {
585 $crmTags = \FluentCrm\App\Models\Tag::select(['id', 'title'])->oldest('title')->get();
586 $crmLists = \FluentCrm\App\Models\Lists::select(['id', 'title'])->oldest('title')->get();
587 }
588
589 $crmConfigs = [
590 'can_add_tags' => $canAddTags,
591 'tags' => $crmTags,
592 'lists' => $crmLists,
593 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluentcrm-logo.svg',
594 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluent-crm-icon.png',
595 ];
596
597 if (defined('FLUENTCRM')) {
598 $crmConfigs['contacts'] = []; //(new \FluentCrm\App\Models\Subscriber)->get();
599 }
600
601 return $crmConfigs;
602 }
603
604 /**
605 * getFluentCrmContactData method will get information from fluent crm using user email
606 * @param $customer
607 * @return array|false
608 */
609 public static function getFluentCrmContactData($customer)
610 {
611 if (!defined('FLUENTCRM')) {
612 return false;
613 }
614 //Get contact info from FluentCRM using customer email
615 $contact = \FluentCrmApi('contacts')->getContactByUserRef($customer->email);
616 if ($contact) {
617 $tags = $contact->tags;
618 $lists = $contact->lists;
619 $urlBase = apply_filters('fluentcrm_menu_url_base', admin_url('admin.php?page=fluentcrm-admin#/'));
620 $crmProfileUrl = $urlBase . 'subscribers/' . $contact->id;
621
622 //Return contact data
623 return [
624 'id' => $contact->id,
625 'first_name' => $contact->first_name,
626 'last_name' => $contact->last_name,
627 'full_name' => $contact->full_name,
628 'name_mismatch' => $contact->full_name != $customer->full_name,
629 'tags' => $tags,
630 'lists' => $lists,
631 'status' => $contact->status,
632 'stats' => $contact->stats(),
633 'view_url' => $crmProfileUrl
634 ];
635 }
636
637 return false;
638 }
639
640 public static function openAIIntegrationStatus() {
641 $chatGPTSettingsData = Meta::where('object_type', '_fs_openai_settings')->value('value');
642
643 if ($chatGPTSettingsData) {
644 $settings = static::safeUnserialize($chatGPTSettingsData);
645 return !empty($settings['api_key']);
646 }
647
648 return false;
649 }
650
651 public static function fluentBotIntegrationStatus()
652 {
653 $settings = static::safeUnserialize(
654 Meta::where('object_type', 'fluent_bot_settings')->value('value')
655 );
656
657 return !empty($settings['isEnabled']) && filter_var($settings['isEnabled'], FILTER_VALIDATE_BOOLEAN);
658 }
659
660
661 public static function showTicketSummaryAdminBar()
662 {
663 $data = self::getOption('global_business_settings');
664
665 if ($data && isset($data["enable_admin_bar_summary"]) && $data["enable_admin_bar_summary"] == 'yes') {
666 return true;
667 }
668
669 return false;
670 }
671
672 public static function generateMessageID($email)
673 {
674 if (!$email || !is_string($email)) {
675 return false;
676 }
677
678 $emailParts = explode('@', $email);
679 if (count($emailParts) != 2) {
680 return false;
681 }
682
683 $emailDomain = $emailParts[1];
684 try {
685 return sprintf(
686 "<%s.%s@%s>",
687 base_convert((int)microtime(true), 10, 36),
688 base_convert(bin2hex(openssl_random_pseudo_bytes(8)), 16, 36),
689 $emailDomain
690 );
691 } catch (\Exception $exception) {
692 return false;
693 }
694 }
695
696 public static function getExportOptions()
697 {
698 $data = [
699 'Agent First Name' => __('Agent First Name', 'fluent-support'),
700 'Agent Last Name' => __('Agent Last Name', 'fluent-support'),
701 'Agent Full Name' => __('Agent Full Name', 'fluent-support'),
702 'Responses' => __('Responses', 'fluent-support'),
703 'Interactions' => __('Interactions', 'fluent-support'),
704 'Open Tickets' => __('Open Tickets', 'fluent-support'),
705 'Closed' => __('Closed', 'fluent-support'),
706 'Waiting Tickets' => __('Waiting Tickets', 'fluent-support'),
707 'Average Waiting' => __('Average Waiting', 'fluent-support'),
708 'Max Waiting' => __('Max Waiting', 'fluent-support'),
709 ];
710
711 if (Helper::isAgentFeedbackEnabled()) {
712 $data['Likes'] = __('Likes', 'fluent-support');
713 $data['Dislikes'] = __('Dislikes', 'fluent-support');
714 }
715
716 return $data;
717 }
718
719 public static function getAuthProvider()
720 {
721 if (defined('FLUENT_AUTH_PLUGIN_PATH')) {
722 $settings = \FluentAuth\App\Helpers\Helper::getAuthFormsSettings();
723 if ($settings['enabled'] == 'yes') {
724 return 'fluent_auth';
725 }
726 }
727
728 return 'fluent_support';
729 }
730
731 /** @internal Not called from core controllers — available for Pro/hook usage. */
732 public static function getDriversKey(){
733 return [
734 'dropbox_settings',
735 'google_drive_settings',
736 'cloudflare_r2_settings',
737 'amazon_s3_settings',
738 'local'
739 ];
740 }
741
742
743 public static function getUploadDriverKey()
744 {
745 if (!defined('FLUENTSUPPORTPRO')) {
746 return 'local';
747 }
748
749 $driver = self::getOption('file_upload_driver');
750
751 if ($driver) {
752 return $driver;
753 }
754
755 // Now guess the driver and save it
756
757 // check if dropbox is enabled
758 $dropboxSettings = self::getIntegrationOption('dropbox_settings', null);
759 if ($dropboxSettings) {
760 $dropBoxEnabled = Meta::where('object_type', 'enabled_upload_drivers')
761 ->where('key', 'dropbox_settings')
762 ->where('value', 'yes')
763 ->first();
764
765 if ($dropBoxEnabled) {
766 $driver = 'dropbox';
767 self::updateOption('file_upload_driver', $driver);
768 return $driver;
769 }
770 }
771
772 // check if google drive is enabled
773 $googleDriveSettings = self::getIntegrationOption('google_drive_settings', null);
774
775 if ($googleDriveSettings) {
776 $googleDriveEnabled = Meta::where('object_type', 'enabled_upload_drivers')
777 ->where('key', 'google_drive_settings')
778 ->where('value', 'yes')
779 ->first();
780
781 if ($googleDriveEnabled) {
782 $driver = 'google_drive';
783 self::updateOption('file_upload_driver', $driver);
784 return $driver;
785 }
786 }
787
788 // check if cloudflare r2 is enabled
789 $cloudflareR2Settings = self::getIntegrationOption('cloudflare_r2_settings', null);
790
791 if ($cloudflareR2Settings) {
792 $cloudflareR2Enabled = Meta::where('object_type', 'enabled_upload_drivers')
793 ->where('key', 'cloudflare_r2_settings')
794 ->where('value', 'yes')
795 ->first();
796
797 if ($cloudflareR2Enabled) {
798 $driver = 'cloudflare_r2';
799 self::updateOption('file_upload_driver', $driver);
800 return $driver;
801 }
802 }
803
804 // check if amazon s3 is enabled
805 $amazonS3Settings = self::getIntegrationOption('amazon_s3_settings', null);
806
807 if ($amazonS3Settings) {
808 $amazonS3Enabled = Meta::where('object_type', 'enabled_upload_drivers')
809 ->where('key', 'amazon_s3_settings')
810 ->where('value', 'yes')
811 ->first();
812
813 if ($amazonS3Enabled) {
814 $driver = 'amazon_s3';
815 self::updateOption('file_upload_driver', $driver);
816 return $driver;
817 }
818 }
819
820 self::updateOption('file_upload_driver', 'local');
821 return 'local';
822 }
823
824 public static function getIntegrationStatuses()
825 {
826 $connections = [
827 'woocommerce' => [
828 'title' => __('WooCommerce', 'fluent-support'),
829 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/woocommerce.png',
830 'is_integrated' => defined('WC_PLUGIN_FILE'),
831 'description' => __('The most popular e-commerce platform for WordPress', 'fluent-support'),
832 'doc_url' => 'https://fluentsupport.com/docs/woocommerce-integration/',
833 ],
834 'fluent-cart' => [
835 'title' => __('FluentCart', 'fluent-support'),
836 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-cart.webp',
837 'is_integrated' => defined('FLUENTCART_VERSION'),
838 'description' => __('A New Era of eCommerce with WordPress', 'fluent-support'),
839 'doc_url' => 'https://fluentsupport.com/docs/fluentcart-integration/',
840 ],
841 'lifter-lms' => [
842 'title' => __('LifterLMS', 'fluent-support'),
843 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/lifter-lms.png',
844 'is_integrated' => defined('LLMS_PLUGIN_FILE'),
845 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
846 'doc_url' => 'https://fluentsupport.com/docs/lifterlms-integration/',
847 ],
848 'slack' => [
849 'title' => __('Slack', 'fluent-support'),
850 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/slack.png',
851 'is_integrated' => self::getFSIntegrationStatus('slack_settings'),
852 'description' => __('Business communication platform designed to scale', 'fluent-support'),
853 'doc_url' => 'https://fluentsupport.com/docs/managing-tickets-using-slack/',
854 ],
855 'pm-pro' => [
856 'title' => __('Paid Memberships Pro', 'fluent-support'),
857 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/pmpro.png',
858 'is_integrated' => defined('PMPRO_VERSION'),
859 'description' => __('The ultimate platform for any member-focused business', 'fluent-support'),
860 'doc_url' => 'https://fluentsupport.com/docs/paid-membership-pro-integration/',
861 ],
862 'tutor-lms' => [
863 'title' => __('Tutor LMS', 'fluent-support'),
864 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/tutor-lms.png',
865 'is_integrated' => defined('TUTOR_VERSION'),
866 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
867 'doc_url' => 'https://fluentsupport.com/docs/tutorlms-integration/',
868 ],
869 'telegram' => [
870 'title' => __('Telegram', 'fluent-support'),
871 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/telegram.jpeg',
872 'is_integrated' => self::getFSIntegrationStatus('telegram_settings'),
873 'description' => __('Business communication platform designed for security', 'fluent-support'),
874 'doc_url' => 'https://fluentsupport.com/docs/managing-tickets-using-telegram/',
875 ],
876 'fluent-crm' => [
877 'title' => __('FluentCRM', 'fluent-support'),
878 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-crm.png',
879 'is_integrated' => defined('FLUENTCRM'),
880 'description' => __('Self-hosted email and marketing automation for WordPress', 'fluent-support'),
881 'doc_url' => 'https://fluentsupport.com/docs/fluentcrm-integration/',
882 ],
883 'fluent-community' => [
884 'title' => __('FluentCommunity', 'fluent-support'),
885 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-community.png',
886 'is_integrated' => defined('FLUENT_COMMUNITY_PLUGIN_VERSION'),
887 'description' => __('Build and manage vibrant online communities with integrated LMS features directly within WordPress.', 'fluent-support'),
888 'doc_url' => 'https://fluentsupport.com/docs/fluentcommunity-integration/',
889 ],
890 'fluent-forms' => [
891 'title' => __('Fluent Forms', 'fluent-support'),
892 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-forms.png',
893 'is_integrated' => defined('FLUENTFORM'),
894 'description' => __('A robust form plugin suitable for any business', 'fluent-support'),
895 'doc_url' => 'https://fluentsupport.com/docs/fluent-form-integration/',
896 ],
897 'buddy-boss' => [
898 'title' => __('BuddyBoss', 'fluent-support'),
899 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/buddy-boss.png',
900 'is_integrated' => defined('BP_PLUGIN_DIR'),
901 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
902 'doc_url' => 'https://fluentsupport.com/docs/buddyboss-integration/'
903 ],
904 'discord' => [
905 'title' => __('Discord', 'fluent-support'),
906 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/discord.png',
907 'is_integrated' => self::getFSIntegrationStatus('discord_settings'),
908 'description' => __('Business communication platform designed for tech', 'fluent-support'),
909 'doc_url' => 'https://fluentsupport.com/docs/managing-tickets-using-discord/',
910 ],
911 'wishlist-member' => [
912 'title' => __('WishList Member', 'fluent-support'),
913 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/wishlist-member.png',
914 'is_integrated' => defined('WLM3_PLUGIN_VERSION'),
915 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
916 'doc_url' => 'https://fluentsupport.com/docs/wishlist-member-integration/',
917 ],
918 'easy-digital-downloads' => [
919 'title' => __('Easy Digital Downloads', 'fluent-support'),
920 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/easy-digital-downloads.png',
921 'is_integrated' => class_exists('\Easy_Digital_Downloads'),
922 'description' => __('The ultimate WordPress platform for digital products', 'fluent-support'),
923 'doc_url' => 'https://fluentsupport.com/docs/edd-integration/',
924 ],
925 'restrict-content-pro' => [
926 'title' => __('Restrict Content pro', 'fluent-support'),
927 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/restrict-content-pro.png',
928 'is_integrated' => class_exists('\Restrict_Content_Pro' ),
929 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
930 'doc_url' => 'https://fluentsupport.com/docs/restrict-content-pro-integration/',
931 ],
932 'better-docs' => [
933 'title' => __('BetterDocs', 'fluent-support'),
934 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/better-docs.png',
935 'is_integrated' => false,
936 'description' => __('The standard plugin for knowledge base and documentation', 'fluent-support'),
937 'doc_url' => 'https://fluentsupport.com/docs/betterdocs-integration/',
938 ],
939 'whatsapp' => [
940 'title' => __('WhatsApp', 'fluent-support'),
941 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/whatsapp.jpeg',
942 'is_integrated' => self::getFSIntegrationStatus('twilio_settings'),
943 'description' => __('Business communication platform designed for privacy', 'fluent-support'),
944 'doc_url' => 'https://fluentsupport.com/docs/whatsapp-integration-via-twilio/',
945 ],
946 'paymattic' => [
947 'title' => __('Paymattic', 'fluent-support'),
948 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/paymattic.png',
949 'is_integrated' => defined('WPPAYFORM_VERSION'),
950 'description' => __('All-in-one payment gateway designed for WordPress', 'fluent-support'),
951 'doc_url' => 'https://paymattic.com/docs/how-to-integrate-fluent-support-with-paymattic-in-wordpress/',
952 ],
953 'learn-dash' => [
954 'title' => __('LearnDash', 'fluent-support'),
955 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/learn-dash.png',
956 'is_integrated' => defined('LEARNDASH_VERSION'),
957 'description' => __('The leading course platform built for WordPress', 'fluent-support'),
958 'doc_url' => 'https://fluentsupport.com/docs/learndash-integration/',
959 ],
960 'learn-press' => [
961 'title' => __('LearnPress', 'fluent-support'),
962 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/learn-press.png',
963 'is_integrated' => defined('LP_PLUGIN_FILE'),
964 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
965 'doc_url' => 'https://fluentsupport.com/docs/learnpress-integration/',
966 ],
967 'google-drive' => [
968 'title' => __('Google Drive', 'fluent-support'),
969 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-drive.jpeg',
970 'is_integrated' => self::getFSIntegrationStatus('google_drive_settings'),
971 'description' => __('A cloud storage service by Google for storing, syncing, and sharing files.', 'fluent-support'),
972 'doc_url' => 'https://fluentsupport.com/docs/google-drive-integration/'
973 ],
974 'dropbox' => [
975 'title' => __('Dropbox', 'fluent-support'),
976 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/dropbox.png',
977 'is_integrated' => self::getFSIntegrationStatus('dropbox_settings'),
978 'description' => __('A cloud-based file storage and sharing service that allows users to store files online and sync them across devices.', 'fluent-support'),
979 'doc_url' => 'https://fluentsupport.com/docs/dropbox-integration/',
980 ],
981 'member-press' => [
982 'title' => __('MemberPress', 'fluent-support'),
983 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/member-press.png',
984 'is_integrated' => class_exists('MeprUtils'),
985 'description' => __('A WordPress plugin that enables the creation and management of membership sites, including content access control and subscription billing.', 'fluent-support'),
986 'doc_url' => 'https://fluentsupport.com/docs/memberpress-integration/'
987 ],
988 'google-recaptcha' => [
989 'title' => __('Google reCAPTCHA', 'fluent-support'),
990 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-recaptcha.png',
991 'is_integrated' => self::getFSIntegrationStatus('recaptcha_setting'),
992 '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'),
993 'doc_url' => 'https://fluentsupport.com/docs/google-recaptcha-integration/',
994 ],
995 'fluent-boards' => [
996 'title' => __('FluentBoards', 'fluent-support'),
997 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-boards.png',
998 'is_integrated' => defined('FLUENT_BOARDS'),
999 'description' => __('A project management tool designed to streamline workflows and collaboration through customizable, kanban-style boards.', 'fluent-support'),
1000 'doc_url' => '',
1001 ],
1002 ];
1003
1004 return $connections;
1005 }
1006
1007 public static function getGlobalSettingsMenu()
1008 {
1009 $menu = [
1010 [
1011 'title' => __('Global Settings', 'fluent-support'),
1012 'route_name' => 'global_settings',
1013 'icon' => 'settings',
1014 ],
1015 [
1016 'title' => __('Ticket Tags', 'fluent-support'),
1017 'route_name' => 'tags',
1018 'icon' => 'ticketTag',
1019 ],
1020 [
1021 'title' => __('Ticket Form Config', 'fluent-support'),
1022 'route_name' => 'ticket-form-config',
1023 'icon' => 'formConfig',
1024 ],
1025 [
1026 'title' => __('Custom Fields', 'fluent-support'),
1027 'route_name' => 'custom_fields',
1028 'icon' => 'customFields',
1029 ],
1030 [
1031 'title' => __('Products', 'fluent-support'),
1032 'route_name' => 'products',
1033 'icon' => 'products',
1034 ],
1035 [
1036 'title' => __('Support Staff', 'fluent-support'),
1037 'route_name' => 'support-staffs',
1038 'icon' => 'supportStaffs',
1039 ],
1040 [
1041 'title' => __('FluentCRM Integration', 'fluent-support'),
1042 'route_name' => 'fluentcrm_integration',
1043 'icon' => 'crmIntegration',
1044 ],
1045 [
1046 'title' => __('Incoming Webhook', 'fluent-support'),
1047 'route_name' => 'incoming-webhook',
1048 'icon' => 'incomingWebhook',
1049 ],
1050 [
1051 'title' => __('Notification Integrations', 'fluent-support'),
1052 'route_name' => 'integration',
1053 'icon' => 'notification',
1054 ],
1055 [
1056 'title' => __('File Upload Integrations', 'fluent-support'),
1057 'route_name' => 'upload_integration',
1058 'icon' => 'fileUpload',
1059 ],
1060 [
1061 'title' => __('Auto Close Settings', 'fluent-support'),
1062 'route_name' => 'auto_close',
1063 'icon' => 'autoClose',
1064 ],
1065 [
1066 'title' => __('Ticket Importer', 'fluent-support'),
1067 'route_name' => 'ticket_importer',
1068 'icon' => 'importer',
1069 ],
1070 [
1071 'title' => __('Recaptcha', 'fluent-support'),
1072 'route_name' => 'reCaptcha',
1073 'icon' => 'reCaptcha',
1074 ],
1075 [
1076 'title' => __('Integration Statuses', 'fluent-support'),
1077 'route_name' => 'integration_statuses',
1078 'icon' => 'status',
1079 ],
1080 [
1081 'title' => __('OpenAI Integration', 'fluent-support'),
1082 'route_name' => 'openai_integration',
1083 'icon' => 'aiIntegration',
1084 ],
1085 ];
1086
1087 if (defined('FLUENT_SUPPORT_PRO_DIR_FILE')) {
1088 $menu[] = [
1089 'title' => __('License Management', 'fluent-support'),
1090 'route_name' => 'license',
1091 'icon' => 'license',
1092 ];
1093 }
1094
1095 return apply_filters('fluent_support/settings_menu_items', $menu);
1096 }
1097
1098 /** @internal Not called from core controllers — available for Pro/hook usage. */
1099 public static function getFSIntegrationStatus($connection_name)
1100 {
1101 $integrationMap = [
1102 'slack_settings' => 'slack_settings',
1103 'discord_settings' => 'discord_settings',
1104 'twilio_settings' => 'twilio_settings',
1105 'telegram_settings' => 'telegram_settings',
1106 'google_drive_settings' => 'google_drive_settings',
1107 'dropbox_settings' => 'dropbox_settings',
1108 'recaptcha_setting' => '_fs_recaptcha_settings'
1109 ];
1110
1111 if (array_key_exists($connection_name, $integrationMap)) {
1112 if ($connection_name == 'google_drive_settings' || $connection_name == 'dropbox_settings') {
1113 return self::checkUploadDriverStatus($connection_name);
1114 } elseif ($connection_name == 'recaptcha_setting') {
1115 return self::checkRecaptchaStatus();
1116 } else {
1117 return self::checkNotificationIntegrationStatus($integrationMap[$connection_name]);
1118 }
1119 }
1120
1121 return false;
1122 }
1123
1124 private static function checkNotificationIntegrationStatus($settingName)
1125 {
1126 $settings = self::getIntegrationOption($settingName, null);
1127 if ($settings) {
1128 $status = Arr::get($settings, 'status', false);
1129 return $status ? true : false;
1130 }
1131 return false;
1132 }
1133
1134 private static function checkUploadDriverStatus($settingName)
1135 {
1136 $settings = self::getIntegrationOption($settingName, null);
1137 if ($settings) {
1138 $enabled = Arr::get($settings, 'status', false);
1139 return $enabled ? true : false;
1140 }
1141 return false;
1142 }
1143
1144 private static function checkRecaptchaStatus()
1145 {
1146 $reCaptchaSettingsData = Meta::where('object_type', '_fs_recaptcha_settings')->first();
1147
1148 if ($reCaptchaSettingsData) {
1149 $settings = static::safeUnserialize($reCaptchaSettingsData->value);
1150 $status = Arr::get($settings, 'is_enabled', false);
1151 return $status == 'true' ? true : false;
1152 }
1153 return false;
1154 }
1155
1156 public static function getAIActivities($data)
1157 {
1158 $page = isset($data['page']) ? intval($data['page']) : 1;
1159 $perPage = isset($data['per_page']) ? intval($data['per_page']) : 10;
1160
1161 $activitiesQuery = AIActivityLogs::with([
1162 'person' => function ($query) {
1163 $query->select(['first_name', 'person_type', 'last_name', 'id', 'avatar']);
1164 },
1165 'ticket' => function ($query) {
1166 $query->select(['id', 'title']);
1167 }
1168 ])->latest('id');
1169
1170 $from = sanitize_text_field( Arr::get( $data, 'from', '' ) );
1171 $to = sanitize_text_field( Arr::get( $data, 'to', '') );
1172
1173 if ( $from != $to ) {
1174 $from = $from . ' ' . '00:00:00';
1175 $to = $to . ' ' . '23:59:59';
1176 }
1177
1178 if ( ( !empty($from) && !empty($to) ) && $from == $to ) {
1179 $activitiesQuery->whereDate('created_at', '=', $from);
1180 } elseif (!empty($from) && !empty($to)) {
1181 $activitiesQuery->whereBetween('created_at', [ $from, $to ]);
1182 }
1183
1184 $agentId = intval( Arr::get($data, 'filters.agent_id') );
1185
1186 if ($agentId) {
1187 $activitiesQuery->where('agent_id', $agentId);
1188 }
1189
1190 $activities = $activitiesQuery->paginate($perPage, ['*'], 'page', $page);
1191
1192 $settings = static::getSettings();
1193
1194 return [
1195 'data' => $activities->items(),
1196 'total' => $activities->total(),
1197 'per_page' => $activities->perPage(),
1198 'current_page' => $activities->currentPage(),
1199 'last_page' => $activities->lastPage(),
1200 'settings' => $settings['ai_activity_settings']
1201 ];
1202 }
1203
1204 public static function updateAISettings($settings)
1205 {
1206 $defaults = [
1207 'delete_days' => 14,
1208 'disable_logs' => 'no'
1209 ];
1210
1211 $settings = wp_parse_args($settings, $defaults);
1212 $settings['delete_days'] = (int)$settings['delete_days'];
1213
1214 Helper::updateOption('_ai_activity_settings', $settings);
1215
1216 return [
1217 'message' => __('AI Activity settings have been updated', 'fluent-support')
1218 ];
1219 }
1220
1221 public static function getSettings()
1222 {
1223 $settings = Helper::getOption('_ai_activity_settings', []);
1224
1225 $defaults = [
1226 'delete_days' => 14,
1227 'disable_logs' => 'no'
1228 ];
1229
1230 $settings = wp_parse_args($settings, $defaults);
1231
1232 if (! $settings ) throw new \Exception(esc_html__('No activity settings found', 'fluent-support'));
1233
1234 return [
1235 'ai_activity_settings' => $settings
1236 ];
1237 }
1238
1239 /**
1240 * Check if activity logs are disabled for a given option key
1241 * @param string $optionKey The option key to check
1242 * @return bool
1243 */
1244 public static function areLogsDisabled($optionKey)
1245 {
1246 if (empty($optionKey)) {
1247 return false;
1248 }
1249
1250 $settings = Helper::getOption($optionKey, []);
1251 $defaults = [
1252 'disable_logs' => 'no'
1253 ];
1254 $settings = wp_parse_args($settings, $defaults);
1255 return isset($settings['disable_logs']) && $settings['disable_logs'] === 'yes';
1256 }
1257
1258 public static function getIp($anonymize = false)
1259 {
1260 static $ipAddress;
1261
1262 if ($ipAddress) {
1263 return $ipAddress;
1264 }
1265
1266 if (empty($_SERVER['REMOTE_ADDR'])) {
1267 // It's a local cli request
1268 return '127.0.0.1';
1269 }
1270
1271 $ipAddress = '';
1272 $remoteAddr = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
1273
1274 if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
1275 //If it's a valid Cloudflare request
1276 if (self::isCfIp($remoteAddr)) {
1277 //Use the CF-Connecting-IP header.
1278 $ipAddress = sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP']));
1279 } else {
1280 //If it isn't valid, then use REMOTE_ADDR.
1281 $ipAddress = $remoteAddr;
1282 }
1283 } else if ($remoteAddr == '127.0.0.1') {
1284 // most probably it's local reverse proxy
1285 if (isset($_SERVER["HTTP_CLIENT_IP"])) {
1286 $ipAddress = sanitize_text_field(wp_unslash($_SERVER["HTTP_CLIENT_IP"]));
1287 } else if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1288 $forwardedFor = sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']));
1289 $splitResult = preg_split('/,/', $forwardedFor);
1290 $firstIp = is_array($splitResult) ? current($splitResult) : '';
1291 $ipAddress = (string)rest_is_ip_address(trim((string)$firstIp));
1292 }
1293 }
1294
1295 if (!$ipAddress) {
1296 $ipAddress = $remoteAddr;
1297 }
1298
1299 if ($ipAddress) {
1300 $ipAddress = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', $ipAddress);
1301 }
1302
1303 $ipAddress = apply_filters('fluent_auth/user_ip', $ipAddress);
1304
1305 if ($anonymize) {
1306 return wp_privacy_anonymize_ip($ipAddress);
1307 }
1308
1309 $ipAddress = sanitize_text_field(wp_unslash($ipAddress));
1310
1311 return $ipAddress;
1312 }
1313
1314 /** @internal Not called from core controllers — available for Pro/hook usage. */
1315 public static function isCfIp($ip = '')
1316 {
1317 if (!$ip) {
1318 $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
1319 }
1320 $cloudflareIPRanges = array(
1321 '103.21.244.0/22',
1322 '103.22.200.0/22',
1323 '103.31.4.0/22',
1324 '104.16.0.0/13',
1325 '104.24.0.0/14',
1326 '108.162.192.0/18',
1327 '131.0.72.0/22',
1328 '141.101.64.0/18',
1329 '162.158.0.0/15',
1330 '172.64.0.0/13',
1331 '173.245.48.0/20',
1332 '188.114.96.0/20',
1333 '190.93.240.0/20',
1334 '197.234.240.0/22',
1335 '198.41.128.0/17'
1336 );
1337 $validCFRequest = false;
1338 //Make sure that the request came via Cloudflare.
1339 foreach ($cloudflareIPRanges as $range) {
1340 //Use the ip_in_range function from Joomla.
1341 if (self::ipInRange($ip, $range)) {
1342 //IP is valid. Belongs to Cloudflare.
1343 return true;
1344 }
1345 }
1346
1347 return false;
1348 }
1349
1350 private static function ipInRange($ip, $range)
1351 {
1352 if (!$ip || !$range || !is_string($ip) || !is_string($range)) {
1353 return false;
1354 }
1355
1356 if (strpos($range, '/') !== false) {
1357 // $range is in IP/NETMASK format
1358 list($range, $netmask) = explode('/', $range, 2);
1359 if (strpos($netmask, '.') !== false) {
1360 // $netmask is a 255.255.0.0 format
1361 $netmask = str_replace('*', '0', $netmask);
1362 $netmask_dec = ip2long($netmask);
1363 return ((ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec));
1364 } else {
1365 // $netmask is a CIDR size block
1366 // fix the range argument
1367 $x = explode('.', $range);
1368 while (count($x) < 4) $x[] = '0';
1369 list($a, $b, $c, $d) = $x;
1370 $range = sprintf("%u.%u.%u.%u", empty($a) ? '0' : $a, empty($b) ? '0' : $b, empty($c) ? '0' : $c, empty($d) ? '0' : $d);
1371 $range_dec = ip2long($range);
1372 $ip_dec = ip2long($ip);
1373
1374 # Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s
1375 #$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0'));
1376
1377 # Strategy 2 - Use math to create it
1378 $wildcard_dec = pow(2, (32 - $netmask)) - 1;
1379 $netmask_dec = ~$wildcard_dec;
1380
1381 return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec));
1382 }
1383 } else {
1384 // range might be 255.255.*.* or 1.2.3.0-1.2.3.255
1385 if (strpos($range, '*') !== false) { // a.b.*.* format
1386 // Just convert to A-B format by setting * to 0 for A and 255 for B
1387 $lower = str_replace('*', '0', $range);
1388 $upper = str_replace('*', '255', $range);
1389 $range = "$lower-$upper";
1390 }
1391
1392 if (strpos($range, '-') !== false) { // A-B format
1393 list($lower, $upper) = explode('-', $range, 2);
1394 $lower_dec = (float)sprintf("%u", ip2long($lower));
1395 $upper_dec = (float)sprintf("%u", ip2long($upper));
1396 $ip_dec = (float)sprintf("%u", ip2long($ip));
1397 return (($ip_dec >= $lower_dec) && ($ip_dec <= $upper_dec));
1398 }
1399 return false;
1400 }
1401 }
1402
1403 public static function loadView($template, $data)
1404 {
1405 extract($data, EXTR_OVERWRITE);
1406
1407 $template = sanitize_file_name($template);
1408
1409 $template = str_replace('.', DIRECTORY_SEPARATOR, $template);
1410
1411 ob_start();
1412 include FLUENT_SUPPORT_PLUGIN_PATH . 'app/Views/emails/' . $template . '.php';
1413 return ob_get_clean();
1414 }
1415
1416 public static function isProductRequired()
1417 {
1418 $settings = Helper::getOption('_ticket_form_settings', []);
1419 return Arr::get($settings, 'product_required_field') === 'yes';
1420 }
1421
1422 /** @internal Not called from core controllers — available for Pro/hook usage. */
1423 public static function getBusinessBox()
1424 {
1425 $businessEmailBoxes = MailBox::select(['id', 'name', 'email', 'mapped_email'])
1426 ->where('box_type', 'email')
1427 ->get();
1428 return $businessEmailBoxes;
1429 }
1430
1431 public static function tempImageMoveUploadDir($ticketId, $contentType, $replyId = null)
1432 {
1433 // Fetch content based on the content type
1434 $content = self::getContentByType($ticketId, $contentType , $replyId);
1435 if (empty($content)) {
1436 return;
1437 }
1438
1439 // Extract image URLs from the content
1440 $imageUrls = self::extractImageUrls($content);
1441 if (empty($imageUrls)) {
1442 return;
1443 }
1444
1445 // Move images to the upload directory and update content
1446 self::moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId);
1447 }
1448
1449 /**
1450 * Fetch content based on the content type.
1451 */
1452 private static function getContentByType($ticketId, $contentType, $replyId)
1453 {
1454 if ($contentType == 'ticket-create') {
1455 $ticket = Ticket::find($ticketId);
1456 return $ticket ? $ticket->content : null;
1457 }
1458
1459 $conversation = Conversation::find($replyId);
1460 return $conversation ? $conversation->content : null;
1461 }
1462
1463 /**
1464 * Extract image URLs from the content.
1465 */
1466 private static function extractImageUrls($content)
1467 {
1468 preg_match_all('/<img[^>]+src="([^">]+)"/', $content, $matches);
1469 return $matches[1] ?? [];
1470 }
1471
1472 /**
1473 * Move images to the upload directory and update content.
1474 */
1475 private static function moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId)
1476 {
1477 // Get the current site's upload directory
1478 $uploadDirInfo = wp_upload_dir();
1479 $uploadsDir = $uploadDirInfo['basedir'];
1480 $tempDir = $uploadsDir . '/fluent-support/temp_files/';
1481
1482 foreach ($imageUrls as $imageUrl) {
1483 // Build the absolute path for the temporary file
1484 $imageRelativePath = $tempDir . basename($imageUrl);
1485 $absolutePath = $imageRelativePath;
1486
1487 // Move the file to the ticket-specific folder
1488 $newFileInfo = UploadService::copyFileTicketFolder($absolutePath, $ticketId);
1489
1490 // Check if the move was successful
1491 if (empty($newFileInfo['file_path'])) {
1492 continue; // Skip if the file couldn't be copied
1493 }
1494
1495 // Ensure the new URL is correctly constructed
1496 $newFileInfo['url'] = trailingslashit($uploadDirInfo['baseurl']) . 'fluent-support/ticket_' . $ticketId . '/' . basename($newFileInfo['file_path']);
1497
1498 // Replace the old URL with the new one in the content
1499 $content = str_replace($imageUrl, $newFileInfo['url'], $content);
1500 }
1501
1502 // Save the updated content
1503 self::saveUpdatedContent($ticketId, $contentType, $content, $replyId);
1504 }
1505
1506 /**
1507 * Save the updated content based on the content type.
1508 */
1509 private static function saveUpdatedContent($ticketId, $contentType, $content, $replyId)
1510 {
1511 if ($contentType == 'ticket-create') {
1512 $ticket = Ticket::find($ticketId);
1513 if ($ticket) {
1514 $ticket->content = $content;
1515 $ticket->save();
1516 }
1517 } else {
1518 $conversation = Conversation::find($replyId);
1519 if ($conversation) {
1520 $conversation->content = $content;
1521 $conversation->save();
1522 }
1523 }
1524 }
1525
1526 /**
1527 * Throw a ValidationException with a safe error message.
1528 * In debug mode the real exception message is used; in production a generic message is returned.
1529 * Throwing instead of returning ensures the error bypasses the framework's HTTP exception
1530 * wrapper (which would prepend a status-code prefix to the message).
1531 *
1532 * @param \Throwable $e The original exception.
1533 * @throws \FluentSupport\Framework\Validator\ValidationException Always thrown.
1534 */
1535 public static function getSafeErrorMessage($e)
1536 {
1537 $message = $e->getMessage() ?: __('Something went wrong. Please try again later.', 'fluent-support');
1538
1539 // ValidationException carries the message through the framework's JSON error
1540 // handler — it is never echoed directly. $e is the chained previous exception,
1541 // not output. phpcs:disable covers the multi-line constructor call.
1542 // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped
1543 throw new \FluentSupport\Framework\Validator\ValidationException(
1544 '', 422, ($e instanceof \Exception ? $e : null), ['message' => $message]
1545 );
1546 // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
1547 }
1548
1549 /**
1550 * Safely unserialize data.
1551 *
1552 * @param string $data The serialized data.
1553 * @return mixed The unserialized data or the original data if not serialized.
1554 */
1555 public static function safeUnserialize($data)
1556 {
1557 if (is_serialized($data)) { // Don't attempt to unserialize data that wasn't serialized going in.
1558 return @unserialize(trim($data), ['allowed_classes' => false]);
1559 }
1560
1561 return $data;
1562 }
1563 }
1564