PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.1.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.1.0
2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 All 68 releases
← All changes | app/Services/Helper.php +49 -472 trunk2.1.0 View file →
@@ -3,9 +3,8 @@
3 3 namespace FluentSupport\App\Services;
4 4
5 5 use FluentSupport\App\App;
6 6 use FluentSupport\App\Models\Agent;
7 -use FluentSupport\App\Models\Attachment;
8 7 use FluentSupport\App\Models\Ticket;
9 8 use FluentSupport\App\Models\Conversation;
10 9 use FluentSupport\App\Models\Customer;
11 10 use FluentSupport\App\Models\MailBox;
@@ -342,10 +341,9 @@
342 341
343 342 public static function getTicketViewUrl($ticket)
344 343 {
345 344 $baseUrl = self::getPortalBaseUrl();
346 - $ticketNumber = $ticket->serial_number ?? $ticket->id;
347 - return $baseUrl . '/#/ticket/' . $ticketNumber . '/view';
345 + return $baseUrl . '/#/ticket/' . $ticket->id . '/view';
348 346 }
349 347
350 348 public static function getTicketViewSignedUrl($ticket)
351 349 {
@@ -353,17 +351,16 @@
353 351 return self::getTicketViewUrl($ticket);
354 352 }
355 353
356 354 $baseUrl = self::getPortalBaseUrl();
357 - $ticketNumber = $ticket->serial_number ?? $ticket->id;
358 355
359 356 $baseUrl = add_query_arg([
360 357 'fs_view' => 'ticket',
361 358 'support_hash' => $ticket->hash,
362 - 'ticket_id' => $ticketNumber,
359 + 'ticket_id' => $ticket->id,
363 360 ], $baseUrl);
364 361
365 - return $baseUrl . '#/ticket/' . $ticketNumber . '/view';
362 + return $baseUrl . '#/ticket/' . $ticket->id . '/view';
366 363 }
367 364
368 365 public static function saveOpenAIData($objectType, $key, $data)
369 366 {
@@ -384,98 +381,8 @@
384 381 }
385 382
386 383 }
387 384
388 - public static function saveAIProviderSettings(array $data)
389 - {
390 - if (!empty($data['api_key'])) {
391 - $data['api_key'] = static::encryptApiKey($data['api_key']);
392 - }
393 -
394 - $serializedData = maybe_serialize($data);
395 - $previous = Meta::where('object_type', '_fs_ai_provider_settings')->first();
396 -
397 - if ($previous) {
398 - return Meta::where('object_type', '_fs_ai_provider_settings')->update([
399 - 'value' => $serializedData,
400 - ]);
401 - }
402 -
403 - return Meta::insert([
404 - 'object_type' => '_fs_ai_provider_settings',
405 - 'key' => '_fs_ai_provider_data',
406 - 'value' => $serializedData,
407 - ]);
408 - }
409 -
410 - public static function getAIProviderSettings(): array
411 - {
412 - $record = Meta::where('object_type', '_fs_ai_provider_settings')->first();
413 -
414 - if (!$record) {
415 - $record = Meta::where('object_type', '_fs_openai_settings')->first();
416 - }
417 -
418 - if (!$record) {
419 - return [];
420 - }
421 -
422 - $settings = static::safeUnserialize($record->value);
423 -
424 - if (empty($settings) || !is_array($settings)) {
425 - return [];
426 - }
427 -
428 - if (!isset($settings['provider'])) {
429 - $settings['provider'] = 'openai';
430 - }
431 -
432 - if (!empty($settings['api_key'])) {
433 - $settings['api_key'] = static::decryptApiKey($settings['api_key']);
434 - }
435 -
436 - return $settings;
437 - }
438 -
439 - public static function encryptApiKey(string $apiKey): string
440 - {
441 - if (!extension_loaded('openssl')) {
442 - return $apiKey;
443 - }
444 -
445 - $cipher = 'AES-256-CBC';
446 - $key = substr(hash('sha256', AUTH_KEY . SECURE_AUTH_KEY, true), 0, 32);
447 - $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher));
448 - $encrypted = openssl_encrypt($apiKey, $cipher, $key, 0, $iv);
449 -
450 - if ($encrypted === false) {
451 - return $apiKey;
452 - }
453 -
454 - return 'fsai:' . base64_encode($iv . $encrypted);
455 - }
456 -
457 - public static function decryptApiKey(string $value): string
458 - {
459 - if (!extension_loaded('openssl') || strncmp($value, 'fsai:', 5) !== 0) {
460 - return $value;
461 - }
462 -
463 - $cipher = 'AES-256-CBC';
464 - $decoded = base64_decode(substr($value, 5));
465 - $ivLen = openssl_cipher_iv_length($cipher);
466 -
467 - if (strlen($decoded) <= $ivLen) {
468 - return $value;
469 - }
470 -
471 - $key = substr(hash('sha256', AUTH_KEY . SECURE_AUTH_KEY, true), 0, 32);
472 - $iv = substr($decoded, 0, $ivLen);
473 - $decrypted = openssl_decrypt(substr($decoded, $ivLen), $cipher, $key, 0, $iv);
474 -
475 - return $decrypted !== false ? $decrypted : $value;
476 - }
477 -
478 385 public static function authorizeChatGPTAPIKey($data)
479 386 {
480 387 return wp_remote_get('https://api.openai.com/v1/models', [
481 388 'headers' => [
@@ -504,48 +411,13 @@
504 411 */
505 412 public static function getPortalBaseUrl()
506 413 {
507 414 $businessSettings = self::getBusinessSettings();
508 - $portalType = Arr::get($businessSettings, 'ticket_link_portal', 'default');
509 - $baseUrl = null;
510 -
511 - if (self::isPortalActive($portalType)) {
512 - if ($portalType === 'woocommerce' && function_exists('wc_get_endpoint_url') && function_exists('wc_get_page_permalink')) {
513 - $accountUrl = wc_get_page_permalink('myaccount');
514 - if ($accountUrl && $accountUrl !== '#') {
515 - $baseUrl = wc_get_endpoint_url('support-tickets', '', $accountUrl);
516 - }
517 - } elseif ($portalType === 'fluent_cart') {
518 - $baseUrl = \FluentCart\App\Services\URL::getCustomerDashboardUrl('fluent-support') ?: null;
519 - } elseif ($portalType === 'fluent_community') {
520 - $baseUrl = \FluentCommunity\App\Services\Helper::baseUrl('support/') ?: null;
521 - }
522 - }
523 -
524 - if (!$baseUrl) {
525 - $baseUrl = get_permalink(absint(Arr::get($businessSettings, 'portal_page_id')));
526 - }
527 -
528 - return apply_filters('fluent_support/portal_base_url', rtrim((string) $baseUrl, '/\\'));
415 + $baseUrl = get_permalink($businessSettings['portal_page_id']);
416 + $baseUrl = rtrim($baseUrl, '/\\');
417 + return apply_filters('fluent_support/portal_base_url', $baseUrl);
529 418 }
530 419
531 - public static function isPortalActive($portalType)
532 - {
533 - if ($portalType === 'woocommerce') {
534 - return defined('FLUENTSUPPORTPRO_PLUGIN_VERSION') && defined('WC_PLUGIN_FILE');
535 - }
536 -
537 - if ($portalType === 'fluent_cart') {
538 - return defined('FLUENTCART_VERSION');
539 - }
540 -
541 - if ($portalType === 'fluent_community') {
542 - return defined('FLUENT_COMMUNITY_PLUGIN_VERSION');
543 - }
544 -
545 - return false;
546 - }
547 -
548 420 public static function getPortalAdminBaseUrl()
549 421 {
550 422 return apply_filters('fluent_support/portal_admin_base_url', admin_url('admin.php?page=fluent-support/#/'));
551 423 }
@@ -659,18 +531,9 @@
659 531 // If user is logged in then return the customer by user id.
660 532 // This `get_current_user_id` function is WP function and
661 533 // it returns user id if user is logged in.
662 534 if (get_current_user_id()) { //if user is logged in
663 - // Ordered for the same reason getCurrentPerson() and
664 - // Customer::getCustomerFromData() are: the user_id column carries a
665 - // plain index, not a unique one, so one account can have more than
666 - // one customer record. Without an order the record that wins is
667 - // whatever the storage engine returns first, and this helper could
668 - // disagree with the other two about which record a request belongs
669 - // to.
670 - return Customer::where('user_id', get_current_user_id())
671 - ->orderBy('id', 'ASC')
672 - ->first();
535 + return Customer::where('user_id', get_current_user_id())->first();
673 536 }
674 537 }
675 538
676 539 public static function getCurrentPerson()
@@ -773,33 +636,26 @@
773 636
774 637 return false;
775 638 }
776 639
777 - public static function openAIIntegrationStatus()
778 - {
779 - $settings = static::getAIProviderSettings();
640 + public static function openAIIntegrationStatus() {
641 + $chatGPTSettingsData = Meta::where('object_type', '_fs_openai_settings')->value('value');
780 642
781 - if (($settings['enabled'] ?? 'yes') === 'no') {
782 - return false;
643 + if ($chatGPTSettingsData) {
644 + $settings = static::safeUnserialize($chatGPTSettingsData);
645 + return !empty($settings['api_key']);
783 646 }
784 647
785 - return !empty($settings['api_key']);
648 + return false;
786 649 }
787 650
788 651 public static function fluentBotIntegrationStatus()
789 652 {
790 - // Include object_id and order by id desc to always read the latest row; the
791 - // write path (saveFluentBotSettings) does not prune siblings, so duplicates
792 - // may exist and a non-deterministic read can return stale state.
793 - $meta = Meta::where([
794 - 'object_type' => 'fluent_bot_settings',
795 - 'object_id' => 1,
796 - 'key' => '_fs_fluent_bot_config'
797 - ])->orderByDesc('id')->first();
653 + $settings = static::safeUnserialize(
654 + Meta::where('object_type', 'fluent_bot_settings')->value('value')
655 + );
798 656
799 - $settings = static::safeUnserialize($meta ? $meta->value : null);
800 -
801 - return isset($settings['isEnabled']) && filter_var($settings['isEnabled'], FILTER_VALIDATE_BOOLEAN);
657 + return !empty($settings['isEnabled']) && filter_var($settings['isEnabled'], FILTER_VALIDATE_BOOLEAN);
802 658 }
803 659
804 660
805 661 public static function showTicketSummaryAdminBar()
@@ -972,9 +828,9 @@
972 828 'title' => __('WooCommerce', 'fluent-support'),
973 829 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/woocommerce.png',
974 830 'is_integrated' => defined('WC_PLUGIN_FILE'),
975 831 'description' => __('The most popular e-commerce platform for WordPress', 'fluent-support'),
976 - 'doc_url' => 'https://docs.fluentsupport.com/woocommerce-integration',
832 + 'doc_url' => 'https://fluentsupport.com/docs/woocommerce-integration/',
977 833 ],
978 834 'fluent-cart' => [
979 835 'title' => __('FluentCart', 'fluent-support'),
980 836 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-cart.webp',
@@ -979,9 +835,9 @@
979 835 'title' => __('FluentCart', 'fluent-support'),
980 836 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-cart.webp',
981 837 'is_integrated' => defined('FLUENTCART_VERSION'),
982 838 'description' => __('A New Era of eCommerce with WordPress', 'fluent-support'),
983 - 'doc_url' => 'https://docs.fluentsupport.com/fluentcart-integration',
839 + 'doc_url' => 'https://fluentsupport.com/docs/fluentcart-integration/',
984 840 ],
985 841 'lifter-lms' => [
986 842 'title' => __('LifterLMS', 'fluent-support'),
987 843 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/lifter-lms.png',
@@ -986,9 +842,9 @@
986 842 'title' => __('LifterLMS', 'fluent-support'),
987 843 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/lifter-lms.png',
988 844 'is_integrated' => defined('LLMS_PLUGIN_FILE'),
989 845 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
990 - 'doc_url' => 'https://docs.fluentsupport.com/lifterlms-integration',
846 + 'doc_url' => 'https://fluentsupport.com/docs/lifterlms-integration/',
991 847 ],
992 848 'slack' => [
993 849 'title' => __('Slack', 'fluent-support'),
994 850 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/slack.png',
@@ -993,9 +849,9 @@
993 849 'title' => __('Slack', 'fluent-support'),
994 850 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/slack.png',
995 851 'is_integrated' => self::getFSIntegrationStatus('slack_settings'),
996 852 'description' => __('Business communication platform designed to scale', 'fluent-support'),
997 - 'doc_url' => 'https://docs.fluentsupport.com/managing-tickets-using-slack',
853 + 'doc_url' => 'https://fluentsupport.com/docs/managing-tickets-using-slack/',
998 854 ],
999 855 'pm-pro' => [
1000 856 'title' => __('Paid Memberships Pro', 'fluent-support'),
1001 857 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/pmpro.png',
@@ -1000,9 +856,9 @@
1000 856 'title' => __('Paid Memberships Pro', 'fluent-support'),
1001 857 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/pmpro.png',
1002 858 'is_integrated' => defined('PMPRO_VERSION'),
1003 859 'description' => __('The ultimate platform for any member-focused business', 'fluent-support'),
1004 - 'doc_url' => 'https://docs.fluentsupport.com/paid-membership-pro-integration',
860 + 'doc_url' => 'https://fluentsupport.com/docs/paid-membership-pro-integration/',
1005 861 ],
1006 862 'tutor-lms' => [
1007 863 'title' => __('Tutor LMS', 'fluent-support'),
1008 864 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/tutor-lms.png',
@@ -1007,9 +863,9 @@
1007 863 'title' => __('Tutor LMS', 'fluent-support'),
1008 864 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/tutor-lms.png',
1009 865 'is_integrated' => defined('TUTOR_VERSION'),
1010 866 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
1011 - 'doc_url' => 'https://docs.fluentsupport.com/tutorlms-integration',
867 + 'doc_url' => 'https://fluentsupport.com/docs/tutorlms-integration/',
1012 868 ],
1013 869 'telegram' => [
1014 870 'title' => __('Telegram', 'fluent-support'),
1015 871 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/telegram.jpeg',
@@ -1014,9 +870,9 @@
1014 870 'title' => __('Telegram', 'fluent-support'),
1015 871 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/telegram.jpeg',
1016 872 'is_integrated' => self::getFSIntegrationStatus('telegram_settings'),
1017 873 'description' => __('Business communication platform designed for security', 'fluent-support'),
1018 - 'doc_url' => 'https://docs.fluentsupport.com/managing-tickets-using-telegram',
874 + 'doc_url' => 'https://fluentsupport.com/docs/managing-tickets-using-telegram/',
1019 875 ],
1020 876 'fluent-crm' => [
1021 877 'title' => __('FluentCRM', 'fluent-support'),
1022 878 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-crm.png',
@@ -1021,9 +877,9 @@
1021 877 'title' => __('FluentCRM', 'fluent-support'),
1022 878 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-crm.png',
1023 879 'is_integrated' => defined('FLUENTCRM'),
1024 880 'description' => __('Self-hosted email and marketing automation for WordPress', 'fluent-support'),
1025 - 'doc_url' => 'https://docs.fluentsupport.com/fluentcrm-integration',
881 + 'doc_url' => 'https://fluentsupport.com/docs/fluentcrm-integration/',
1026 882 ],
1027 883 'fluent-community' => [
1028 884 'title' => __('FluentCommunity', 'fluent-support'),
1029 885 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-community.png',
@@ -1028,9 +884,9 @@
1028 884 'title' => __('FluentCommunity', 'fluent-support'),
1029 885 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-community.png',
1030 886 'is_integrated' => defined('FLUENT_COMMUNITY_PLUGIN_VERSION'),
1031 887 'description' => __('Build and manage vibrant online communities with integrated LMS features directly within WordPress.', 'fluent-support'),
1032 - 'doc_url' => 'https://docs.fluentsupport.com/fluent-community-integration',
888 + 'doc_url' => 'https://fluentsupport.com/docs/fluentcommunity-integration/',
1033 889 ],
1034 890 'fluent-forms' => [
1035 891 'title' => __('Fluent Forms', 'fluent-support'),
1036 892 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-forms.png',
@@ -1035,9 +891,9 @@
1035 891 'title' => __('Fluent Forms', 'fluent-support'),
1036 892 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-forms.png',
1037 893 'is_integrated' => defined('FLUENTFORM'),
1038 894 'description' => __('A robust form plugin suitable for any business', 'fluent-support'),
1039 - 'doc_url' => 'https://docs.fluentsupport.com/fluent-form-integration',
895 + 'doc_url' => 'https://fluentsupport.com/docs/fluent-form-integration/',
1040 896 ],
1041 897 'buddy-boss' => [
1042 898 'title' => __('BuddyBoss', 'fluent-support'),
1043 899 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/buddy-boss.png',
@@ -1042,9 +898,9 @@
1042 898 'title' => __('BuddyBoss', 'fluent-support'),
1043 899 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/buddy-boss.png',
1044 900 'is_integrated' => defined('BP_PLUGIN_DIR'),
1045 901 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
1046 - 'doc_url' => 'https://docs.fluentsupport.com/buddyboss-integration'
902 + 'doc_url' => 'https://fluentsupport.com/docs/buddyboss-integration/'
1047 903 ],
1048 904 'discord' => [
1049 905 'title' => __('Discord', 'fluent-support'),
1050 906 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/discord.png',
@@ -1049,9 +905,9 @@
1049 905 'title' => __('Discord', 'fluent-support'),
1050 906 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/discord.png',
1051 907 'is_integrated' => self::getFSIntegrationStatus('discord_settings'),
1052 908 'description' => __('Business communication platform designed for tech', 'fluent-support'),
1053 - 'doc_url' => 'https://docs.fluentsupport.com/managing-tickets-using-discord',
909 + 'doc_url' => 'https://fluentsupport.com/docs/managing-tickets-using-discord/',
1054 910 ],
1055 911 'wishlist-member' => [
1056 912 'title' => __('WishList Member', 'fluent-support'),
1057 913 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/wishlist-member.png',
@@ -1056,9 +912,9 @@
1056 912 'title' => __('WishList Member', 'fluent-support'),
1057 913 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/wishlist-member.png',
1058 914 'is_integrated' => defined('WLM3_PLUGIN_VERSION'),
1059 915 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
1060 - 'doc_url' => 'https://docs.fluentsupport.com/wishlist-member-integration',
916 + 'doc_url' => 'https://fluentsupport.com/docs/wishlist-member-integration/',
1061 917 ],
1062 918 'easy-digital-downloads' => [
1063 919 'title' => __('Easy Digital Downloads', 'fluent-support'),
1064 920 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/easy-digital-downloads.png',
@@ -1063,9 +919,9 @@
1063 919 'title' => __('Easy Digital Downloads', 'fluent-support'),
1064 920 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/easy-digital-downloads.png',
1065 921 'is_integrated' => class_exists('\Easy_Digital_Downloads'),
1066 922 'description' => __('The ultimate WordPress platform for digital products', 'fluent-support'),
1067 - 'doc_url' => 'https://docs.fluentsupport.com/edd-integration',
923 + 'doc_url' => 'https://fluentsupport.com/docs/edd-integration/',
1068 924 ],
1069 925 'restrict-content-pro' => [
1070 926 'title' => __('Restrict Content pro', 'fluent-support'),
1071 927 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/restrict-content-pro.png',
@@ -1070,9 +926,9 @@
1070 926 'title' => __('Restrict Content pro', 'fluent-support'),
1071 927 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/restrict-content-pro.png',
1072 928 'is_integrated' => class_exists('\Restrict_Content_Pro' ),
1073 929 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
1074 - 'doc_url' => 'https://docs.fluentsupport.com/restrict-content-pro-integration',
930 + 'doc_url' => 'https://fluentsupport.com/docs/restrict-content-pro-integration/',
1075 931 ],
1076 932 'better-docs' => [
1077 933 'title' => __('BetterDocs', 'fluent-support'),
1078 934 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/better-docs.png',
@@ -1077,9 +933,9 @@
1077 933 'title' => __('BetterDocs', 'fluent-support'),
1078 934 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/better-docs.png',
1079 935 'is_integrated' => false,
1080 936 'description' => __('The standard plugin for knowledge base and documentation', 'fluent-support'),
1081 - 'doc_url' => 'https://docs.fluentsupport.com/betterdocs-integration',
937 + 'doc_url' => 'https://fluentsupport.com/docs/betterdocs-integration/',
1082 938 ],
1083 939 'whatsapp' => [
1084 940 'title' => __('WhatsApp', 'fluent-support'),
1085 941 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/whatsapp.jpeg',
@@ -1084,9 +940,9 @@
1084 940 'title' => __('WhatsApp', 'fluent-support'),
1085 941 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/whatsapp.jpeg',
1086 942 'is_integrated' => self::getFSIntegrationStatus('twilio_settings'),
1087 943 'description' => __('Business communication platform designed for privacy', 'fluent-support'),
1088 - 'doc_url' => 'https://docs.fluentsupport.com/whatsapp-integration-via-twilio',
944 + 'doc_url' => 'https://fluentsupport.com/docs/whatsapp-integration-via-twilio/',
1089 945 ],
1090 946 'paymattic' => [
1091 947 'title' => __('Paymattic', 'fluent-support'),
1092 948 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/paymattic.png',
@@ -1098,9 +954,9 @@
1098 954 'title' => __('LearnDash', 'fluent-support'),
1099 955 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/learn-dash.png',
1100 956 'is_integrated' => defined('LEARNDASH_VERSION'),
1101 957 'description' => __('The leading course platform built for WordPress', 'fluent-support'),
1102 - 'doc_url' => 'https://docs.fluentsupport.com/learndash-integration',
958 + 'doc_url' => 'https://fluentsupport.com/docs/learndash-integration/',
1103 959 ],
1104 960 'learn-press' => [
1105 961 'title' => __('LearnPress', 'fluent-support'),
1106 962 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/learn-press.png',
@@ -1105,9 +961,9 @@
1105 961 'title' => __('LearnPress', 'fluent-support'),
1106 962 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/learn-press.png',
1107 963 'is_integrated' => defined('LP_PLUGIN_FILE'),
1108 964 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
1109 - 'doc_url' => 'https://docs.fluentsupport.com/learnpress-integration',
965 + 'doc_url' => 'https://fluentsupport.com/docs/learnpress-integration/',
1110 966 ],
1111 967 'google-drive' => [
1112 968 'title' => __('Google Drive', 'fluent-support'),
1113 969 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-drive.jpeg',
@@ -1112,9 +968,9 @@
1112 968 'title' => __('Google Drive', 'fluent-support'),
1113 969 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-drive.jpeg',
1114 970 'is_integrated' => self::getFSIntegrationStatus('google_drive_settings'),
1115 971 'description' => __('A cloud storage service by Google for storing, syncing, and sharing files.', 'fluent-support'),
1116 - 'doc_url' => 'https://docs.fluentsupport.com/google-drive-integration'
972 + 'doc_url' => 'https://fluentsupport.com/docs/google-drive-integration/'
1117 973 ],
1118 974 'dropbox' => [
1119 975 'title' => __('Dropbox', 'fluent-support'),
1120 976 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/dropbox.png',
@@ -1119,9 +975,9 @@
1119 975 'title' => __('Dropbox', 'fluent-support'),
1120 976 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/dropbox.png',
1121 977 'is_integrated' => self::getFSIntegrationStatus('dropbox_settings'),
1122 978 'description' => __('A cloud-based file storage and sharing service that allows users to store files online and sync them across devices.', 'fluent-support'),
1123 - 'doc_url' => 'https://docs.fluentsupport.com/dropbox-integration',
979 + 'doc_url' => 'https://fluentsupport.com/docs/dropbox-integration/',
1124 980 ],
1125 981 'member-press' => [
1126 982 'title' => __('MemberPress', 'fluent-support'),
1127 983 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/member-press.png',
@@ -1126,9 +982,9 @@
1126 982 'title' => __('MemberPress', 'fluent-support'),
1127 983 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/member-press.png',
1128 984 'is_integrated' => class_exists('MeprUtils'),
1129 985 'description' => __('A WordPress plugin that enables the creation and management of membership sites, including content access control and subscription billing.', 'fluent-support'),
1130 - 'doc_url' => 'https://docs.fluentsupport.com/memberpress-integration'
986 + 'doc_url' => 'https://fluentsupport.com/docs/memberpress-integration/'
1131 987 ],
1132 988 'google-recaptcha' => [
1133 989 'title' => __('Google reCAPTCHA', 'fluent-support'),
1134 990 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-recaptcha.png',
@@ -1133,9 +989,9 @@
1133 989 'title' => __('Google reCAPTCHA', 'fluent-support'),
1134 990 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-recaptcha.png',
1135 991 'is_integrated' => self::getFSIntegrationStatus('recaptcha_setting'),
1136 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'),
1137 - 'doc_url' => 'https://docs.fluentsupport.com/google-recaptcha-integration',
993 + 'doc_url' => 'https://fluentsupport.com/docs/google-recaptcha-integration/',
1138 994 ],
1139 995 'fluent-boards' => [
1140 996 'title' => __('FluentBoards', 'fluent-support'),
1141 997 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-boards.png',
@@ -1140,17 +996,10 @@
1140 996 'title' => __('FluentBoards', 'fluent-support'),
1141 997 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-boards.png',
1142 998 'is_integrated' => defined('FLUENT_BOARDS'),
1143 999 'description' => __('A project management tool designed to streamline workflows and collaboration through customizable, kanban-style boards.', 'fluent-support'),
1144 - 'doc_url' => 'https://docs.fluentsupport.com/fluentboards-integrations#fluentboards-integration',
1000 + 'doc_url' => '',
1145 1001 ],
1146 - 'fluent-booking' => [
1147 - 'title' => __('FluentBooking', 'fluent-support'),
1148 - 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-booking.svg',
1149 - 'is_integrated' => defined('FLUENT_BOOKING_VERSION'),
1150 - 'description' => __('Appointment and booking management plugin for WordPress', 'fluent-support'),
1151 - 'doc_url' => 'https://docs.fluentsupport.com/fluentbooking-integration',
1152 - ],
1153 1002 ];
1154 1003
1155 1004 return $connections;
1156 1005 }
@@ -1228,20 +1077,11 @@
1228 1077 'route_name' => 'integration_statuses',
1229 1078 'icon' => 'status',
1230 1079 ],
1231 1080 [
1232 - 'title' => __('AI Integration', 'fluent-support'),
1233 - 'icon' => 'aiIntegration',
1234 - 'children' => [
1235 - [
1236 - 'title' => __('AI Model Setup', 'fluent-support'),
1237 - 'route_name' => 'ai_integration',
1238 - ],
1239 - [
1240 - 'title' => __('MCP for AI Agents', 'fluent-support'),
1241 - 'route_name' => 'mcp_settings',
1242 - ],
1243 - ],
1081 + 'title' => __('OpenAI Integration', 'fluent-support'),
1082 + 'route_name' => 'openai_integration',
1083 + 'icon' => 'aiIntegration',
1244 1084 ],
1245 1085 ];
1246 1086
1247 1087 if (defined('FLUENT_SUPPORT_PRO_DIR_FILE')) {
@@ -1470,65 +1310,8 @@
1470 1310
1471 1311 return $ipAddress;
1472 1312 }
1473 1313
1474 - /**
1475 - * Atomically increments the counter for $rateLimitKey and reports whether the
1476 - * limit is now exceeded. On sites with a persistent external object cache
1477 - * (Redis/Memcached), wp_cache_incr() is a real atomic increment, so concurrent
1478 - * requests can't race past the limit. Without one, this falls back to a plain
1479 - * transient read/write (same best-effort behavior as the rest of this codebase's
1480 - * rate limiters, e.g. AuthController::incrementLoginAttempts).
1481 - *
1482 - * Note the counter is incremented before it is compared, so the returned value
1483 - * already accounts for the current request.
1484 - */
1485 - public static function hitRateLimit($rateLimitKey, $limit, $window = null)
1486 - {
1487 - $window = $window ?: 15 * MINUTE_IN_SECONDS;
1488 -
1489 - if (wp_using_ext_object_cache()) {
1490 - $group = 'fs_rate_limit';
1491 - if (false === wp_cache_get($rateLimitKey, $group)) {
1492 - wp_cache_add($rateLimitKey, 0, $group, $window);
1493 - }
1494 - $attempts = wp_cache_incr($rateLimitKey, 1, $group);
1495 -
1496 - // Fail closed: if the cache backend couldn't increment (evicted key, hiccup),
1497 - // treat the request as rate-limited rather than silently letting it through.
1498 - if ($attempts === false) {
1499 - return true;
1500 - }
1501 -
1502 - return $attempts > $limit;
1503 - }
1504 -
1505 - $now = time();
1506 - $record = get_transient($rateLimitKey);
1507 -
1508 - // A record without a live deadline means the window is over (or the record predates
1509 - // this format), so the count starts again. Anything else keeps the deadline it was
1510 - // created with.
1511 - //
1512 - // The `<=` must not be loosened to `<`: it is what guarantees the TTL below is at
1513 - // least 1. A request landing exactly on the deadline would otherwise compute a TTL
1514 - // of 0, and set_transient() reads 0 as "never expires" — wedging this limiter shut
1515 - // permanently.
1516 - if (!is_array($record) || empty($record['expires']) || $record['expires'] <= $now) {
1517 - $record = ['count' => 0, 'expires' => $now + $window];
1518 - }
1519 -
1520 - $record['count']++;
1521 -
1522 - // The TTL is the time left until the original deadline, never the full window:
1523 - // set_transient() rewrites expiry on every call, so passing $window here would let
1524 - // rejected requests push the deadline forward and keep a tripped limiter tripped
1525 - // for as long as traffic kept arriving.
1526 - set_transient($rateLimitKey, $record, $record['expires'] - $now);
1527 -
1528 - return $record['count'] > $limit;
1529 - }
1530 -
1531 1314 /** @internal Not called from core controllers — available for Pro/hook usage. */
1532 1315 public static function isCfIp($ip = '')
1533 1316 {
1534 1317 if (!$ip) {
@@ -1635,49 +1418,8 @@
1635 1418 $settings = Helper::getOption('_ticket_form_settings', []);
1636 1419 return Arr::get($settings, 'product_required_field') === 'yes';
1637 1420 }
1638 1421
1639 - /**
1640 - * Build a fluentsupport.com upgrade/pricing link tagged with UTM params
1641 - * per the standard "Upgrade to Pro" link spec:
1642 - * utm_source = fluent-support (fixed vocabulary, never the wp.org slug)
1643 - * utm_medium = free_plugin | pro_plugin (acquisition vs cross-sell)
1644 - * utm_campaign= upgrade_pro (override via $args['campaign'])
1645 - * utm_content = the exact placement, e.g. feature_lock_dropbox, upgrade_page
1646 - * utm_term = plugin version that generated the link
1647 - *
1648 - * This is the single source of truth for upgrade URLs. The admin SPA does
1649 - * not rebuild these client-side; it re-points utm_content on the URL this
1650 - * method localizes into appVars (see the $upgradeUrl mixin in start.js).
1651 - *
1652 - * @param string $content The utm_content placement.
1653 - * @param array $args Optional: 'campaign', 'base_url', plus any utm_* override.
1654 - * @return string
1655 - */
1656 - public static function getUpgradeUrl($content = 'upgrade_page', $args = [])
1657 - {
1658 - $args = wp_parse_args($args, [
1659 - 'campaign' => 'upgrade_pro',
1660 - 'base_url' => apply_filters('fluent_support/pro_upgrade_base_url', 'https://fluentsupport.com/pricing'),
1661 - ]);
1662 -
1663 - $params = [
1664 - 'utm_source' => 'fluent-support',
1665 - // Same constant Menu.php uses for appVars.has_pro — keep the two in sync.
1666 - 'utm_medium' => defined('FLUENTSUPPORTPRO_PLUGIN_VERSION') ? 'pro_plugin' : 'free_plugin',
1667 - 'utm_campaign' => $args['campaign'],
1668 - 'utm_content' => $content,
1669 - 'utm_term' => FLUENT_SUPPORT_VERSION,
1670 - ];
1671 -
1672 - // Drop any blank params (e.g. a missing version) so they never hit the URL.
1673 - $params = array_filter($params, function ($value) {
1674 - return $value !== '' && $value !== null;
1675 - });
1676 -
1677 - return add_query_arg($params, $args['base_url']);
1678 - }
1679 -
1680 1422 /** @internal Not called from core controllers — available for Pro/hook usage. */
1681 1423 public static function getBusinessBox()
1682 1424 {
1683 1425 $businessEmailBoxes = MailBox::select(['id', 'name', 'email', 'mapped_email'])
@@ -1685,9 +1427,9 @@
1685 1427 ->get();
1686 1428 return $businessEmailBoxes;
1687 1429 }
1688 1430
1689 - public static function tempImageMoveUploadDir($ticketId, $contentType, $replyId = null, $personId = null)
1431 + public static function tempImageMoveUploadDir($ticketId, $contentType, $replyId = null)
1690 1432 {
1691 1433 // Fetch content based on the content type
1692 1434 $content = self::getContentByType($ticketId, $contentType , $replyId);
1693 1435 if (empty($content)) {
@@ -1700,9 +1442,9 @@
1700 1442 return;
1701 1443 }
1702 1444
1703 1445 // Move images to the upload directory and update content
1704 - self::moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId, $personId);
1446 + self::moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId);
1705 1447 }
1706 1448
1707 1449 /**
1708 1450 * Fetch content based on the content type.
@@ -1722,30 +1464,23 @@
1722 1464 * Extract image URLs from the content.
1723 1465 */
1724 1466 private static function extractImageUrls($content)
1725 1467 {
1726 - preg_match_all('/<img[^>]+src=(["\'])(.*?)\1/i', (string) $content, $matches);
1727 - return $matches[2] ?? [];
1468 + preg_match_all('/<img[^>]+src="([^">]+)"/', $content, $matches);
1469 + return $matches[1] ?? [];
1728 1470 }
1729 1471
1730 1472 /**
1731 1473 * Move images to the upload directory and update content.
1732 1474 */
1733 - private static function moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId, $personId)
1475 + private static function moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId)
1734 1476 {
1735 1477 // Get the current site's upload directory
1736 1478 $uploadDirInfo = wp_upload_dir();
1737 1479 $uploadsDir = $uploadDirInfo['basedir'];
1738 1480 $tempDir = $uploadsDir . '/fluent-support/temp_files/';
1739 - $signedAttachments = self::getSignedImageAttachments($imageUrls, $ticketId, $personId);
1740 1481
1741 1482 foreach ($imageUrls as $imageUrl) {
1742 - $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1743 - if ($fileHash && isset($signedAttachments[$fileHash]) && ($referenceUrl = self::finalizeSignedTempImage($signedAttachments[$fileHash], $ticketId, $contentType, $replyId, $personId))) {
1744 - $content = str_replace($imageUrl, $referenceUrl, $content);
1745 - continue;
1746 - }
1747 -
1748 1483 // Build the absolute path for the temporary file
1749 1484 $imageRelativePath = $tempDir . basename($imageUrl);
1750 1485 $absolutePath = $imageRelativePath;
1751 1486
@@ -1765,166 +1500,8 @@
1765 1500 }
1766 1501
1767 1502 // Save the updated content
1768 1503 self::saveUpdatedContent($ticketId, $contentType, $content, $replyId);
1769 - }
1770 -
1771 - private static function getSignedImageAttachments($imageUrls, $ticketId, $personId)
1772 - {
1773 - $fileHashes = [];
1774 - foreach ($imageUrls as $imageUrl) {
1775 - $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1776 - if ($fileHash) {
1777 - $fileHashes[] = $fileHash;
1778 - }
1779 - }
1780 -
1781 - $fileHashes = array_values(array_unique($fileHashes));
1782 - if (!$fileHashes) {
1783 - return [];
1784 - }
1785 -
1786 - $attachments = Attachment::whereIn('file_hash', $fileHashes)
1787 - ->where(function ($query) use ($ticketId, $personId) {
1788 - $query->where('ticket_id', $ticketId);
1789 -
1790 - if ($personId) {
1791 - $query->orWhere(function ($query) use ($personId) {
1792 - $query->whereNull('ticket_id')
1793 - ->where('person_id', $personId);
1794 - });
1795 - }
1796 - })
1797 - ->get();
1798 - $mappedAttachments = [];
1799 - foreach ($attachments as $attachment) {
1800 - $mappedAttachments[$attachment->file_hash] = $attachment;
1801 - }
1802 -
1803 - return $mappedAttachments;
1804 - }
1805 -
1806 - private static function finalizeSignedTempImage($attachment, $ticketId, $contentType, $replyId, $personId)
1807 - {
1808 - if (!$attachment || $attachment->driver !== 'local') {
1809 - return false;
1810 - }
1811 -
1812 - if ($attachment->ticket_id && intval($attachment->ticket_id) !== intval($ticketId)) {
1813 - return false;
1814 - }
1815 -
1816 - if (!$attachment->ticket_id && $personId && intval($attachment->person_id) !== intval($personId)) {
1817 - return false;
1818 - }
1819 -
1820 - if ($attachment->conversation_id && $replyId && intval($attachment->conversation_id) !== intval($replyId)) {
1821 - return false;
1822 - }
1823 -
1824 - if ($attachment->conversation_id && $contentType === 'ticket-create') {
1825 - return false;
1826 - }
1827 -
1828 - if ($attachment->status !== 'in-active') {
1829 - return $attachment->ticket_id ? self::getAttachmentReferenceUrl($attachment) : false;
1830 - }
1831 -
1832 - if (!$attachment->file_path || !file_exists($attachment->file_path)) {
1833 - return false;
1834 - }
1835 -
1836 - $newFileInfo = UploadService::copyFileTicketFolder($attachment->file_path, $ticketId);
1837 - if (empty($newFileInfo['file_path'])) {
1838 - return false;
1839 - }
1840 -
1841 - $attachment->file_path = $newFileInfo['file_path'];
1842 - $attachment->full_url = $newFileInfo['url'];
1843 - $attachment->ticket_id = $attachment->ticket_id ?: $ticketId;
1844 - $attachment->status = 'inline';
1845 -
1846 - if ($contentType !== 'ticket-create' && $replyId) {
1847 - $attachment->conversation_id = $replyId;
1848 - }
1849 -
1850 - $attachment->save();
1851 -
1852 - return self::getAttachmentReferenceUrl($attachment);
1853 - }
1854 -
1855 - private static function extractAttachmentHashFromUrl($imageUrl)
1856 - {
1857 - $decodedUrl = html_entity_decode($imageUrl, ENT_QUOTES, 'UTF-8');
1858 - $query = wp_parse_url($decodedUrl, PHP_URL_QUERY);
1859 -
1860 - if (!$query) {
1861 - return '';
1862 - }
1863 -
1864 - parse_str($query, $params);
1865 -
1866 - return !empty($params['fst_file']) ? sanitize_text_field($params['fst_file']) : '';
1867 - }
1868 -
1869 - private static function getAttachmentReferenceUrl($attachment)
1870 - {
1871 - return add_query_arg([
1872 - 'fst_file' => $attachment->file_hash
1873 - ], site_url('/index.php'));
1874 - }
1875 -
1876 - public static function refreshSignedAttachmentUrls($content, $ticketId = null)
1877 - {
1878 - $contents = self::refreshSignedAttachmentUrlsInContents([$content], $ticketId);
1879 -
1880 - return $contents[0];
1881 - }
1882 -
1883 - public static function refreshSignedAttachmentUrlsInContents($contents, $ticketId = null)
1884 - {
1885 - $fileHashes = [];
1886 -
1887 - foreach ($contents as $content) {
1888 - foreach (self::extractImageUrls($content) as $imageUrl) {
1889 - $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1890 - if ($fileHash) {
1891 - $fileHashes[] = $fileHash;
1892 - }
1893 - }
1894 - }
1895 -
1896 - $fileHashes = array_values(array_unique($fileHashes));
1897 - if (!$fileHashes) {
1898 - return $contents;
1899 - }
1900 -
1901 - $attachmentsQuery = Attachment::whereIn('file_hash', $fileHashes);
1902 - if ($ticketId) {
1903 - $attachmentsQuery->where('ticket_id', $ticketId);
1904 - }
1905 -
1906 - $attachments = $attachmentsQuery->get();
1907 - $attachmentsByHash = [];
1908 -
1909 - foreach ($attachments as $attachment) {
1910 - $attachmentsByHash[$attachment->file_hash] = $attachment;
1911 - }
1912 -
1913 - foreach ($contents as $key => $content) {
1914 - foreach (self::extractImageUrls($content) as $imageUrl) {
1915 - $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1916 - if (!$fileHash || empty($attachmentsByHash[$fileHash])) {
1917 - continue;
1918 - }
1919 -
1920 - $content = str_replace($imageUrl, $attachmentsByHash[$fileHash]->secureUrl, $content);
1921 - }
1922 -
1923 - $contents[$key] = $content;
1924 - }
1925 -
1926 - return $contents;
1927 1504 }
1928 1505
1929 1506 /**
1930 1507 * Save the updated content based on the content type.