| @@ -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. |