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
← All changes | app/Services/Helper.php +48 -445 2.3.22.1.1 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 }
@@ -764,33 +636,26 @@
764 636
765 637 return false;
766 638 }
767 639
768 - public static function openAIIntegrationStatus()
769 - {
770 - $settings = static::getAIProviderSettings();
640 + public static function openAIIntegrationStatus() {
641 + $chatGPTSettingsData = Meta::where('object_type', '_fs_openai_settings')->value('value');
771 642
772 - if (($settings['enabled'] ?? 'yes') === 'no') {
773 - return false;
643 + if ($chatGPTSettingsData) {
644 + $settings = static::safeUnserialize($chatGPTSettingsData);
645 + return !empty($settings['api_key']);
774 646 }
775 647
776 - return !empty($settings['api_key']);
648 + return false;
777 649 }
778 650
779 651 public static function fluentBotIntegrationStatus()
780 652 {
781 - // Include object_id and order by id desc to always read the latest row; the
782 - // write path (saveFluentBotSettings) does not prune siblings, so duplicates
783 - // may exist and a non-deterministic read can return stale state.
784 - $meta = Meta::where([
785 - 'object_type' => 'fluent_bot_settings',
786 - 'object_id' => 1,
787 - 'key' => '_fs_fluent_bot_config'
788 - ])->orderByDesc('id')->first();
653 + $settings = static::safeUnserialize(
654 + Meta::where('object_type', 'fluent_bot_settings')->value('value')
655 + );
789 656
790 - $settings = static::safeUnserialize($meta ? $meta->value : null);
791 -
792 - return isset($settings['isEnabled']) && filter_var($settings['isEnabled'], FILTER_VALIDATE_BOOLEAN);
657 + return !empty($settings['isEnabled']) && filter_var($settings['isEnabled'], FILTER_VALIDATE_BOOLEAN);
793 658 }
794 659
795 660
796 661 public static function showTicketSummaryAdminBar()
@@ -963,9 +828,9 @@
963 828 'title' => __('WooCommerce', 'fluent-support'),
964 829 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/woocommerce.png',
965 830 'is_integrated' => defined('WC_PLUGIN_FILE'),
966 831 'description' => __('The most popular e-commerce platform for WordPress', 'fluent-support'),
967 - 'doc_url' => 'https://docs.fluentsupport.com/woocommerce-integration',
832 + 'doc_url' => 'https://fluentsupport.com/docs/woocommerce-integration/',
968 833 ],
969 834 'fluent-cart' => [
970 835 'title' => __('FluentCart', 'fluent-support'),
971 836 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-cart.webp',
@@ -970,9 +835,9 @@
970 835 'title' => __('FluentCart', 'fluent-support'),
971 836 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-cart.webp',
972 837 'is_integrated' => defined('FLUENTCART_VERSION'),
973 838 'description' => __('A New Era of eCommerce with WordPress', 'fluent-support'),
974 - 'doc_url' => 'https://docs.fluentsupport.com/fluentcart-integration',
839 + 'doc_url' => 'https://fluentsupport.com/docs/fluentcart-integration/',
975 840 ],
976 841 'lifter-lms' => [
977 842 'title' => __('LifterLMS', 'fluent-support'),
978 843 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/lifter-lms.png',
@@ -977,9 +842,9 @@
977 842 'title' => __('LifterLMS', 'fluent-support'),
978 843 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/lifter-lms.png',
979 844 'is_integrated' => defined('LLMS_PLUGIN_FILE'),
980 845 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
981 - 'doc_url' => 'https://docs.fluentsupport.com/lifterlms-integration',
846 + 'doc_url' => 'https://fluentsupport.com/docs/lifterlms-integration/',
982 847 ],
983 848 'slack' => [
984 849 'title' => __('Slack', 'fluent-support'),
985 850 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/slack.png',
@@ -984,9 +849,9 @@
984 849 'title' => __('Slack', 'fluent-support'),
985 850 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/slack.png',
986 851 'is_integrated' => self::getFSIntegrationStatus('slack_settings'),
987 852 'description' => __('Business communication platform designed to scale', 'fluent-support'),
988 - 'doc_url' => 'https://docs.fluentsupport.com/managing-tickets-using-slack',
853 + 'doc_url' => 'https://fluentsupport.com/docs/managing-tickets-using-slack/',
989 854 ],
990 855 'pm-pro' => [
991 856 'title' => __('Paid Memberships Pro', 'fluent-support'),
992 857 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/pmpro.png',
@@ -991,9 +856,9 @@
991 856 'title' => __('Paid Memberships Pro', 'fluent-support'),
992 857 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/pmpro.png',
993 858 'is_integrated' => defined('PMPRO_VERSION'),
994 859 'description' => __('The ultimate platform for any member-focused business', 'fluent-support'),
995 - 'doc_url' => 'https://docs.fluentsupport.com/paid-membership-pro-integration',
860 + 'doc_url' => 'https://fluentsupport.com/docs/paid-membership-pro-integration/',
996 861 ],
997 862 'tutor-lms' => [
998 863 'title' => __('Tutor LMS', 'fluent-support'),
999 864 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/tutor-lms.png',
@@ -998,9 +863,9 @@
998 863 'title' => __('Tutor LMS', 'fluent-support'),
999 864 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/tutor-lms.png',
1000 865 'is_integrated' => defined('TUTOR_VERSION'),
1001 866 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
1002 - 'doc_url' => 'https://docs.fluentsupport.com/tutorlms-integration',
867 + 'doc_url' => 'https://fluentsupport.com/docs/tutorlms-integration/',
1003 868 ],
1004 869 'telegram' => [
1005 870 'title' => __('Telegram', 'fluent-support'),
1006 871 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/telegram.jpeg',
@@ -1005,9 +870,9 @@
1005 870 'title' => __('Telegram', 'fluent-support'),
1006 871 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/telegram.jpeg',
1007 872 'is_integrated' => self::getFSIntegrationStatus('telegram_settings'),
1008 873 'description' => __('Business communication platform designed for security', 'fluent-support'),
1009 - 'doc_url' => 'https://docs.fluentsupport.com/managing-tickets-using-telegram',
874 + 'doc_url' => 'https://fluentsupport.com/docs/managing-tickets-using-telegram/',
1010 875 ],
1011 876 'fluent-crm' => [
1012 877 'title' => __('FluentCRM', 'fluent-support'),
1013 878 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-crm.png',
@@ -1012,9 +877,9 @@
1012 877 'title' => __('FluentCRM', 'fluent-support'),
1013 878 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-crm.png',
1014 879 'is_integrated' => defined('FLUENTCRM'),
1015 880 'description' => __('Self-hosted email and marketing automation for WordPress', 'fluent-support'),
1016 - 'doc_url' => 'https://docs.fluentsupport.com/fluentcrm-integration',
881 + 'doc_url' => 'https://fluentsupport.com/docs/fluentcrm-integration/',
1017 882 ],
1018 883 'fluent-community' => [
1019 884 'title' => __('FluentCommunity', 'fluent-support'),
1020 885 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-community.png',
@@ -1019,9 +884,9 @@
1019 884 'title' => __('FluentCommunity', 'fluent-support'),
1020 885 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-community.png',
1021 886 'is_integrated' => defined('FLUENT_COMMUNITY_PLUGIN_VERSION'),
1022 887 'description' => __('Build and manage vibrant online communities with integrated LMS features directly within WordPress.', 'fluent-support'),
1023 - 'doc_url' => 'https://docs.fluentsupport.com/fluent-community-integration',
888 + 'doc_url' => 'https://fluentsupport.com/docs/fluentcommunity-integration/',
1024 889 ],
1025 890 'fluent-forms' => [
1026 891 'title' => __('Fluent Forms', 'fluent-support'),
1027 892 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-forms.png',
@@ -1026,9 +891,9 @@
1026 891 'title' => __('Fluent Forms', 'fluent-support'),
1027 892 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-forms.png',
1028 893 'is_integrated' => defined('FLUENTFORM'),
1029 894 'description' => __('A robust form plugin suitable for any business', 'fluent-support'),
1030 - 'doc_url' => 'https://docs.fluentsupport.com/fluent-form-integration',
895 + 'doc_url' => 'https://fluentsupport.com/docs/fluent-form-integration/',
1031 896 ],
1032 897 'buddy-boss' => [
1033 898 'title' => __('BuddyBoss', 'fluent-support'),
1034 899 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/buddy-boss.png',
@@ -1033,9 +898,9 @@
1033 898 'title' => __('BuddyBoss', 'fluent-support'),
1034 899 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/buddy-boss.png',
1035 900 'is_integrated' => defined('BP_PLUGIN_DIR'),
1036 901 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
1037 - 'doc_url' => 'https://docs.fluentsupport.com/buddyboss-integration'
902 + 'doc_url' => 'https://fluentsupport.com/docs/buddyboss-integration/'
1038 903 ],
1039 904 'discord' => [
1040 905 'title' => __('Discord', 'fluent-support'),
1041 906 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/discord.png',
@@ -1040,9 +905,9 @@
1040 905 'title' => __('Discord', 'fluent-support'),
1041 906 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/discord.png',
1042 907 'is_integrated' => self::getFSIntegrationStatus('discord_settings'),
1043 908 'description' => __('Business communication platform designed for tech', 'fluent-support'),
1044 - 'doc_url' => 'https://docs.fluentsupport.com/managing-tickets-using-discord',
909 + 'doc_url' => 'https://fluentsupport.com/docs/managing-tickets-using-discord/',
1045 910 ],
1046 911 'wishlist-member' => [
1047 912 'title' => __('WishList Member', 'fluent-support'),
1048 913 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/wishlist-member.png',
@@ -1047,9 +912,9 @@
1047 912 'title' => __('WishList Member', 'fluent-support'),
1048 913 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/wishlist-member.png',
1049 914 'is_integrated' => defined('WLM3_PLUGIN_VERSION'),
1050 915 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
1051 - 'doc_url' => 'https://docs.fluentsupport.com/wishlist-member-integration',
916 + 'doc_url' => 'https://fluentsupport.com/docs/wishlist-member-integration/',
1052 917 ],
1053 918 'easy-digital-downloads' => [
1054 919 'title' => __('Easy Digital Downloads', 'fluent-support'),
1055 920 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/easy-digital-downloads.png',
@@ -1054,9 +919,9 @@
1054 919 'title' => __('Easy Digital Downloads', 'fluent-support'),
1055 920 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/easy-digital-downloads.png',
1056 921 'is_integrated' => class_exists('\Easy_Digital_Downloads'),
1057 922 'description' => __('The ultimate WordPress platform for digital products', 'fluent-support'),
1058 - 'doc_url' => 'https://docs.fluentsupport.com/edd-integration',
923 + 'doc_url' => 'https://fluentsupport.com/docs/edd-integration/',
1059 924 ],
1060 925 'restrict-content-pro' => [
1061 926 'title' => __('Restrict Content pro', 'fluent-support'),
1062 927 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/restrict-content-pro.png',
@@ -1061,9 +926,9 @@
1061 926 'title' => __('Restrict Content pro', 'fluent-support'),
1062 927 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/restrict-content-pro.png',
1063 928 'is_integrated' => class_exists('\Restrict_Content_Pro' ),
1064 929 'description' => __('Powerful platform for any member-focused business', 'fluent-support'),
1065 - 'doc_url' => 'https://docs.fluentsupport.com/restrict-content-pro-integration',
930 + 'doc_url' => 'https://fluentsupport.com/docs/restrict-content-pro-integration/',
1066 931 ],
1067 932 'better-docs' => [
1068 933 'title' => __('BetterDocs', 'fluent-support'),
1069 934 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/better-docs.png',
@@ -1068,9 +933,9 @@
1068 933 'title' => __('BetterDocs', 'fluent-support'),
1069 934 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/better-docs.png',
1070 935 'is_integrated' => false,
1071 936 'description' => __('The standard plugin for knowledge base and documentation', 'fluent-support'),
1072 - 'doc_url' => 'https://docs.fluentsupport.com/betterdocs-integration',
937 + 'doc_url' => 'https://fluentsupport.com/docs/betterdocs-integration/',
1073 938 ],
1074 939 'whatsapp' => [
1075 940 'title' => __('WhatsApp', 'fluent-support'),
1076 941 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/whatsapp.jpeg',
@@ -1075,9 +940,9 @@
1075 940 'title' => __('WhatsApp', 'fluent-support'),
1076 941 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/whatsapp.jpeg',
1077 942 'is_integrated' => self::getFSIntegrationStatus('twilio_settings'),
1078 943 'description' => __('Business communication platform designed for privacy', 'fluent-support'),
1079 - 'doc_url' => 'https://docs.fluentsupport.com/whatsapp-integration-via-twilio',
944 + 'doc_url' => 'https://fluentsupport.com/docs/whatsapp-integration-via-twilio/',
1080 945 ],
1081 946 'paymattic' => [
1082 947 'title' => __('Paymattic', 'fluent-support'),
1083 948 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/paymattic.png',
@@ -1089,9 +954,9 @@
1089 954 'title' => __('LearnDash', 'fluent-support'),
1090 955 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/learn-dash.png',
1091 956 'is_integrated' => defined('LEARNDASH_VERSION'),
1092 957 'description' => __('The leading course platform built for WordPress', 'fluent-support'),
1093 - 'doc_url' => 'https://docs.fluentsupport.com/learndash-integration',
958 + 'doc_url' => 'https://fluentsupport.com/docs/learndash-integration/',
1094 959 ],
1095 960 'learn-press' => [
1096 961 'title' => __('LearnPress', 'fluent-support'),
1097 962 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/learn-press.png',
@@ -1096,9 +961,9 @@
1096 961 'title' => __('LearnPress', 'fluent-support'),
1097 962 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/learn-press.png',
1098 963 'is_integrated' => defined('LP_PLUGIN_FILE'),
1099 964 'description' => __('Course and e-learning platform built for WordPress', 'fluent-support'),
1100 - 'doc_url' => 'https://docs.fluentsupport.com/learnpress-integration',
965 + 'doc_url' => 'https://fluentsupport.com/docs/learnpress-integration/',
1101 966 ],
1102 967 'google-drive' => [
1103 968 'title' => __('Google Drive', 'fluent-support'),
1104 969 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-drive.jpeg',
@@ -1103,9 +968,9 @@
1103 968 'title' => __('Google Drive', 'fluent-support'),
1104 969 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-drive.jpeg',
1105 970 'is_integrated' => self::getFSIntegrationStatus('google_drive_settings'),
1106 971 'description' => __('A cloud storage service by Google for storing, syncing, and sharing files.', 'fluent-support'),
1107 - 'doc_url' => 'https://docs.fluentsupport.com/google-drive-integration'
972 + 'doc_url' => 'https://fluentsupport.com/docs/google-drive-integration/'
1108 973 ],
1109 974 'dropbox' => [
1110 975 'title' => __('Dropbox', 'fluent-support'),
1111 976 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/dropbox.png',
@@ -1110,9 +975,9 @@
1110 975 'title' => __('Dropbox', 'fluent-support'),
1111 976 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/dropbox.png',
1112 977 'is_integrated' => self::getFSIntegrationStatus('dropbox_settings'),
1113 978 'description' => __('A cloud-based file storage and sharing service that allows users to store files online and sync them across devices.', 'fluent-support'),
1114 - 'doc_url' => 'https://docs.fluentsupport.com/dropbox-integration',
979 + 'doc_url' => 'https://fluentsupport.com/docs/dropbox-integration/',
1115 980 ],
1116 981 'member-press' => [
1117 982 'title' => __('MemberPress', 'fluent-support'),
1118 983 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/member-press.png',
@@ -1117,9 +982,9 @@
1117 982 'title' => __('MemberPress', 'fluent-support'),
1118 983 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/member-press.png',
1119 984 'is_integrated' => class_exists('MeprUtils'),
1120 985 'description' => __('A WordPress plugin that enables the creation and management of membership sites, including content access control and subscription billing.', 'fluent-support'),
1121 - 'doc_url' => 'https://docs.fluentsupport.com/memberpress-integration'
986 + 'doc_url' => 'https://fluentsupport.com/docs/memberpress-integration/'
1122 987 ],
1123 988 'google-recaptcha' => [
1124 989 'title' => __('Google reCAPTCHA', 'fluent-support'),
1125 990 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-recaptcha.png',
@@ -1124,9 +989,9 @@
1124 989 'title' => __('Google reCAPTCHA', 'fluent-support'),
1125 990 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/google-recaptcha.png',
1126 991 'is_integrated' => self::getFSIntegrationStatus('recaptcha_setting'),
1127 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'),
1128 - 'doc_url' => 'https://docs.fluentsupport.com/google-recaptcha-integration',
993 + 'doc_url' => 'https://fluentsupport.com/docs/google-recaptcha-integration/',
1129 994 ],
1130 995 'fluent-boards' => [
1131 996 'title' => __('FluentBoards', 'fluent-support'),
1132 997 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-boards.png',
@@ -1131,17 +996,10 @@
1131 996 'title' => __('FluentBoards', 'fluent-support'),
1132 997 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-boards.png',
1133 998 'is_integrated' => defined('FLUENT_BOARDS'),
1134 999 'description' => __('A project management tool designed to streamline workflows and collaboration through customizable, kanban-style boards.', 'fluent-support'),
1135 - 'doc_url' => 'https://docs.fluentsupport.com/fluentboards-integrations#fluentboards-integration',
1000 + 'doc_url' => '',
1136 1001 ],
1137 - 'fluent-booking' => [
1138 - 'title' => __('FluentBooking', 'fluent-support'),
1139 - 'logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/integrations/fluent-booking.svg',
1140 - 'is_integrated' => defined('FLUENT_BOOKING_VERSION'),
1141 - 'description' => __('Appointment and booking management plugin for WordPress', 'fluent-support'),
1142 - 'doc_url' => 'https://docs.fluentsupport.com/fluentbooking-integration',
1143 - ],
1144 1002 ];
1145 1003
1146 1004 return $connections;
1147 1005 }
@@ -1219,20 +1077,11 @@
1219 1077 'route_name' => 'integration_statuses',
1220 1078 'icon' => 'status',
1221 1079 ],
1222 1080 [
1223 - 'title' => __('AI Integration', 'fluent-support'),
1224 - 'icon' => 'aiIntegration',
1225 - 'children' => [
1226 - [
1227 - 'title' => __('AI Model Setup', 'fluent-support'),
1228 - 'route_name' => 'ai_integration',
1229 - ],
1230 - [
1231 - 'title' => __('MCP for AI Agents', 'fluent-support'),
1232 - 'route_name' => 'mcp_settings',
1233 - ],
1234 - ],
1081 + 'title' => __('OpenAI Integration', 'fluent-support'),
1082 + 'route_name' => 'openai_integration',
1083 + 'icon' => 'aiIntegration',
1235 1084 ],
1236 1085 ];
1237 1086
1238 1087 if (defined('FLUENT_SUPPORT_PRO_DIR_FILE')) {
@@ -1461,65 +1310,8 @@
1461 1310
1462 1311 return $ipAddress;
1463 1312 }
1464 1313
1465 - /**
1466 - * Atomically increments the counter for $rateLimitKey and reports whether the
1467 - * limit is now exceeded. On sites with a persistent external object cache
1468 - * (Redis/Memcached), wp_cache_incr() is a real atomic increment, so concurrent
1469 - * requests can't race past the limit. Without one, this falls back to a plain
1470 - * transient read/write (same best-effort behavior as the rest of this codebase's
1471 - * rate limiters, e.g. AuthController::incrementLoginAttempts).
1472 - *
1473 - * Note the counter is incremented before it is compared, so the returned value
1474 - * already accounts for the current request.
1475 - */
1476 - public static function hitRateLimit($rateLimitKey, $limit, $window = null)
1477 - {
1478 - $window = $window ?: 15 * MINUTE_IN_SECONDS;
1479 -
1480 - if (wp_using_ext_object_cache()) {
1481 - $group = 'fs_rate_limit';
1482 - if (false === wp_cache_get($rateLimitKey, $group)) {
1483 - wp_cache_add($rateLimitKey, 0, $group, $window);
1484 - }
1485 - $attempts = wp_cache_incr($rateLimitKey, 1, $group);
1486 -
1487 - // Fail closed: if the cache backend couldn't increment (evicted key, hiccup),
1488 - // treat the request as rate-limited rather than silently letting it through.
1489 - if ($attempts === false) {
1490 - return true;
1491 - }
1492 -
1493 - return $attempts > $limit;
1494 - }
1495 -
1496 - $now = time();
1497 - $record = get_transient($rateLimitKey);
1498 -
1499 - // A record without a live deadline means the window is over (or the record predates
1500 - // this format), so the count starts again. Anything else keeps the deadline it was
1501 - // created with.
1502 - //
1503 - // The `<=` must not be loosened to `<`: it is what guarantees the TTL below is at
1504 - // least 1. A request landing exactly on the deadline would otherwise compute a TTL
1505 - // of 0, and set_transient() reads 0 as "never expires" — wedging this limiter shut
1506 - // permanently.
1507 - if (!is_array($record) || empty($record['expires']) || $record['expires'] <= $now) {
1508 - $record = ['count' => 0, 'expires' => $now + $window];
1509 - }
1510 -
1511 - $record['count']++;
1512 -
1513 - // The TTL is the time left until the original deadline, never the full window:
1514 - // set_transient() rewrites expiry on every call, so passing $window here would let
1515 - // rejected requests push the deadline forward and keep a tripped limiter tripped
1516 - // for as long as traffic kept arriving.
1517 - set_transient($rateLimitKey, $record, $record['expires'] - $now);
1518 -
1519 - return $record['count'] > $limit;
1520 - }
1521 -
1522 1314 /** @internal Not called from core controllers — available for Pro/hook usage. */
1523 1315 public static function isCfIp($ip = '')
1524 1316 {
1525 1317 if (!$ip) {
@@ -1626,32 +1418,8 @@
1626 1418 $settings = Helper::getOption('_ticket_form_settings', []);
1627 1419 return Arr::get($settings, 'product_required_field') === 'yes';
1628 1420 }
1629 1421
1630 - /**
1631 - * Build a fluentsupport.com upgrade/pricing link tagged with UTM params
1632 - * per the standard "Upgrade to Pro" link spec (utm_source is always
1633 - * "fluent-support"; utm_medium reflects free vs pro install).
1634 - */
1635 - public static function getUpgradeUrl($content, $args = [])
1636 - {
1637 - $args = wp_parse_args($args, [
1638 - 'campaign' => 'upgrade_pro',
1639 - 'base_url' => 'https://fluentsupport.com/pricing'
1640 - ]);
1641 -
1642 - $params = [
1643 - 'utm_source' => 'fluent-support',
1644 - 'utm_medium' => defined('FLUENTSUPPORTPRO') ? 'pro_plugin' : 'free_plugin',
1645 - 'utm_campaign' => $args['campaign'],
1646 - 'utm_content' => $content,
1647 - 'utm_term' => FLUENT_SUPPORT_VERSION,
1648 - 'utm_id' => ''
1649 - ];
1650 -
1651 - return add_query_arg($params, $args['base_url']);
1652 - }
1653 -
1654 1422 /** @internal Not called from core controllers — available for Pro/hook usage. */
1655 1423 public static function getBusinessBox()
1656 1424 {
1657 1425 $businessEmailBoxes = MailBox::select(['id', 'name', 'email', 'mapped_email'])
@@ -1659,9 +1427,9 @@
1659 1427 ->get();
1660 1428 return $businessEmailBoxes;
1661 1429 }
1662 1430
1663 - public static function tempImageMoveUploadDir($ticketId, $contentType, $replyId = null, $personId = null)
1431 + public static function tempImageMoveUploadDir($ticketId, $contentType, $replyId = null)
1664 1432 {
1665 1433 // Fetch content based on the content type
1666 1434 $content = self::getContentByType($ticketId, $contentType , $replyId);
1667 1435 if (empty($content)) {
@@ -1674,9 +1442,9 @@
1674 1442 return;
1675 1443 }
1676 1444
1677 1445 // Move images to the upload directory and update content
1678 - self::moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId, $personId);
1446 + self::moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId);
1679 1447 }
1680 1448
1681 1449 /**
1682 1450 * Fetch content based on the content type.
@@ -1696,30 +1464,23 @@
1696 1464 * Extract image URLs from the content.
1697 1465 */
1698 1466 private static function extractImageUrls($content)
1699 1467 {
1700 - preg_match_all('/<img[^>]+src=(["\'])(.*?)\1/i', (string) $content, $matches);
1701 - return $matches[2] ?? [];
1468 + preg_match_all('/<img[^>]+src="([^">]+)"/', $content, $matches);
1469 + return $matches[1] ?? [];
1702 1470 }
1703 1471
1704 1472 /**
1705 1473 * Move images to the upload directory and update content.
1706 1474 */
1707 - private static function moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId, $personId)
1475 + private static function moveImagesAndUpdateContent($imageUrls, $ticketId, $contentType, $content, $replyId)
1708 1476 {
1709 1477 // Get the current site's upload directory
1710 1478 $uploadDirInfo = wp_upload_dir();
1711 1479 $uploadsDir = $uploadDirInfo['basedir'];
1712 1480 $tempDir = $uploadsDir . '/fluent-support/temp_files/';
1713 - $signedAttachments = self::getSignedImageAttachments($imageUrls, $ticketId, $personId);
1714 1481
1715 1482 foreach ($imageUrls as $imageUrl) {
1716 - $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1717 - if ($fileHash && isset($signedAttachments[$fileHash]) && ($referenceUrl = self::finalizeSignedTempImage($signedAttachments[$fileHash], $ticketId, $contentType, $replyId, $personId))) {
1718 - $content = str_replace($imageUrl, $referenceUrl, $content);
1719 - continue;
1720 - }
1721 -
1722 1483 // Build the absolute path for the temporary file
1723 1484 $imageRelativePath = $tempDir . basename($imageUrl);
1724 1485 $absolutePath = $imageRelativePath;
1725 1486
@@ -1739,166 +1500,8 @@
1739 1500 }
1740 1501
1741 1502 // Save the updated content
1742 1503 self::saveUpdatedContent($ticketId, $contentType, $content, $replyId);
1743 - }
1744 -
1745 - private static function getSignedImageAttachments($imageUrls, $ticketId, $personId)
1746 - {
1747 - $fileHashes = [];
1748 - foreach ($imageUrls as $imageUrl) {
1749 - $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1750 - if ($fileHash) {
1751 - $fileHashes[] = $fileHash;
1752 - }
1753 - }
1754 -
1755 - $fileHashes = array_values(array_unique($fileHashes));
1756 - if (!$fileHashes) {
1757 - return [];
1758 - }
1759 -
1760 - $attachments = Attachment::whereIn('file_hash', $fileHashes)
1761 - ->where(function ($query) use ($ticketId, $personId) {
1762 - $query->where('ticket_id', $ticketId);
1763 -
1764 - if ($personId) {
1765 - $query->orWhere(function ($query) use ($personId) {
1766 - $query->whereNull('ticket_id')
1767 - ->where('person_id', $personId);
1768 - });
1769 - }
1770 - })
1771 - ->get();
1772 - $mappedAttachments = [];
1773 - foreach ($attachments as $attachment) {
1774 - $mappedAttachments[$attachment->file_hash] = $attachment;
1775 - }
1776 -
1777 - return $mappedAttachments;
1778 - }
1779 -
1780 - private static function finalizeSignedTempImage($attachment, $ticketId, $contentType, $replyId, $personId)
1781 - {
1782 - if (!$attachment || $attachment->driver !== 'local') {
1783 - return false;
1784 - }
1785 -
1786 - if ($attachment->ticket_id && intval($attachment->ticket_id) !== intval($ticketId)) {
1787 - return false;
1788 - }
1789 -
1790 - if (!$attachment->ticket_id && $personId && intval($attachment->person_id) !== intval($personId)) {
1791 - return false;
1792 - }
1793 -
1794 - if ($attachment->conversation_id && $replyId && intval($attachment->conversation_id) !== intval($replyId)) {
1795 - return false;
1796 - }
1797 -
1798 - if ($attachment->conversation_id && $contentType === 'ticket-create') {
1799 - return false;
1800 - }
1801 -
1802 - if ($attachment->status !== 'in-active') {
1803 - return $attachment->ticket_id ? self::getAttachmentReferenceUrl($attachment) : false;
1804 - }
1805 -
1806 - if (!$attachment->file_path || !file_exists($attachment->file_path)) {
1807 - return false;
1808 - }
1809 -
1810 - $newFileInfo = UploadService::copyFileTicketFolder($attachment->file_path, $ticketId);
1811 - if (empty($newFileInfo['file_path'])) {
1812 - return false;
1813 - }
1814 -
1815 - $attachment->file_path = $newFileInfo['file_path'];
1816 - $attachment->full_url = $newFileInfo['url'];
1817 - $attachment->ticket_id = $attachment->ticket_id ?: $ticketId;
1818 - $attachment->status = 'inline';
1819 -
1820 - if ($contentType !== 'ticket-create' && $replyId) {
1821 - $attachment->conversation_id = $replyId;
1822 - }
1823 -
1824 - $attachment->save();
1825 -
1826 - return self::getAttachmentReferenceUrl($attachment);
1827 - }
1828 -
1829 - private static function extractAttachmentHashFromUrl($imageUrl)
1830 - {
1831 - $decodedUrl = html_entity_decode($imageUrl, ENT_QUOTES, 'UTF-8');
1832 - $query = wp_parse_url($decodedUrl, PHP_URL_QUERY);
1833 -
1834 - if (!$query) {
1835 - return '';
1836 - }
1837 -
1838 - parse_str($query, $params);
1839 -
1840 - return !empty($params['fst_file']) ? sanitize_text_field($params['fst_file']) : '';
1841 - }
1842 -
1843 - private static function getAttachmentReferenceUrl($attachment)
1844 - {
1845 - return add_query_arg([
1846 - 'fst_file' => $attachment->file_hash
1847 - ], site_url('/index.php'));
1848 - }
1849 -
1850 - public static function refreshSignedAttachmentUrls($content, $ticketId = null)
1851 - {
1852 - $contents = self::refreshSignedAttachmentUrlsInContents([$content], $ticketId);
1853 -
1854 - return $contents[0];
1855 - }
1856 -
1857 - public static function refreshSignedAttachmentUrlsInContents($contents, $ticketId = null)
1858 - {
1859 - $fileHashes = [];
1860 -
1861 - foreach ($contents as $content) {
1862 - foreach (self::extractImageUrls($content) as $imageUrl) {
1863 - $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1864 - if ($fileHash) {
1865 - $fileHashes[] = $fileHash;
1866 - }
1867 - }
1868 - }
1869 -
1870 - $fileHashes = array_values(array_unique($fileHashes));
1871 - if (!$fileHashes) {
1872 - return $contents;
1873 - }
1874 -
1875 - $attachmentsQuery = Attachment::whereIn('file_hash', $fileHashes);
1876 - if ($ticketId) {
1877 - $attachmentsQuery->where('ticket_id', $ticketId);
1878 - }
1879 -
1880 - $attachments = $attachmentsQuery->get();
1881 - $attachmentsByHash = [];
1882 -
1883 - foreach ($attachments as $attachment) {
1884 - $attachmentsByHash[$attachment->file_hash] = $attachment;
1885 - }
1886 -
1887 - foreach ($contents as $key => $content) {
1888 - foreach (self::extractImageUrls($content) as $imageUrl) {
1889 - $fileHash = self::extractAttachmentHashFromUrl($imageUrl);
1890 - if (!$fileHash || empty($attachmentsByHash[$fileHash])) {
1891 - continue;
1892 - }
1893 -
1894 - $content = str_replace($imageUrl, $attachmentsByHash[$fileHash]->secureUrl, $content);
1895 - }
1896 -
1897 - $contents[$key] = $content;
1898 - }
1899 -
1900 - return $contents;
1901 1504 }
1902 1505
1903 1506 /**
1904 1507 * Save the updated content based on the content type.