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/Http/Controllers/SettingsController.php +121 -182 2.4.02.1.0 View file →
@@ -7,9 +7,8 @@
7 7 use FluentSupport\App\Models\Meta;
8 8 use FluentSupport\App\Models\Product;
9 9 use FluentSupport\App\Services\EmailNotification\Settings;
10 10 use FluentSupport\App\Services\Helper;
11 -use FluentSupport\App\Services\Integrations\AI\AIProviderFactory;
12 11 use FluentSupport\Database\Migrations\AIActivityLogsMigrator;
13 12 use FluentSupport\Framework\Http\Request\Request;
14 13 use FluentSupport\App\Hooks\Handlers\ReCaptchaHandler;
15 14 use FluentSupport\Framework\Support\Arr;
@@ -361,103 +360,129 @@
361 360 'message' => __('Your reCAPTCHA settings added successfully.', 'fluent-support'),
362 361 ]);
363 362 }
364 363
365 - public function saveAIProviderSettings(Request $request)
364 + public function saveOpenAISettings(Request $request)
366 365 {
367 - $allowedProviders = AIProviderFactory::getAllowedProviders();
368 - $provider = $request->getSafe('provider', 'sanitize_text_field', 'openai');
366 + $data = [
367 + 'api_key' => $request->getSafe('api_key', 'sanitize_text_field', ''),
368 + 'model' => $request->getSafe('model', 'sanitize_text_field', ''),
369 + ];
369 370
370 - if (!in_array($provider, $allowedProviders, true)) {
371 + $response = Helper::authorizeChatGPTAPIKey($data);
372 +
373 + if (is_wp_error($response)) {
371 374 return $this->sendError([
372 - 'message' => __('Invalid AI provider selected.', 'fluent-support'),
375 + 'message' => __('There was an error verifying the API key.', 'fluent-support'),
373 376 ]);
374 377 }
375 378
376 - $enabled = $request->getSafe('enabled', 'sanitize_text_field', 'yes');
377 - $apiKey = $request->getSafe('api_key', 'sanitize_text_field', '');
378 - $model = $request->getSafe('model', 'sanitize_text_field', '');
379 + $body = json_decode(wp_remote_retrieve_body($response), true);
379 380
380 - if (empty($apiKey) || strpos($apiKey, '****') === 0) {
381 - $existing = Helper::getAIProviderSettings();
382 - if (($existing['provider'] ?? '') === $provider) {
383 - $apiKey = $existing['api_key'] ?? '';
384 - } else {
385 - $apiKey = '';
386 - }
381 + if (isset($body['error'])) {
382 + return $this->sendError([
383 + 'message' => __('Invalid API key. Please provide a valid ChatGPT API key.', 'fluent-support'),
384 + ]);
387 385 }
388 386
389 - $data = [
390 - 'enabled' => $enabled,
391 - 'provider' => $provider,
392 - 'api_key' => $apiKey,
393 - 'model' => $model,
394 - ];
395 -
396 387 try {
397 - Helper::saveAIProviderSettings($data);
398 - AIProviderFactory::clearCache();
399 - AIActivityLogsMigrator::migrate();
400 -
388 + $isDataSaved = Helper::saveOpenAIData('_fs_openai_settings', '_fs_openai_data', $data);
389 + if ($isDataSaved) {
390 + AIActivityLogsMigrator::migrate();
391 + }
401 392 return $this->sendSuccess([
402 - 'message' => __('AI settings have been successfully saved.', 'fluent-support'),
393 + 'message' => __('OpenAI settings have been successfully saved.', 'fluent-support'),
403 394 ]);
404 395 } catch (\Exception $e) {
396 + // translators: %s is the error message from the exception
397 + $translatedMessage = __('An error occurred while saving the settings: %s', 'fluent-support');
398 + $errorMessage = sprintf($translatedMessage, Helper::getSafeErrorMessage($e));
399 +
405 400 return $this->sendError([
406 - 'message' => sprintf(
407 - __('An error occurred while saving the settings: %s', 'fluent-support'),
408 - Helper::getSafeErrorMessage($e)
409 - ),
401 + 'message' => $errorMessage,
410 402 ]);
411 403 }
412 404 }
413 405
414 - public function disconnectAIProvider()
406 +
407 + public function disconnectOpenAI()
415 408 {
416 - Meta::where('object_type', '_fs_ai_provider_settings')->delete();
417 - Meta::where([
409 + $deletedRecords = Meta::where([
418 410 'object_type' => '_fs_openai_settings',
419 411 'key' => '_fs_openai_data',
420 412 ])->delete();
421 413
422 - AIProviderFactory::clearCache();
423 -
424 - return $this->sendSuccess([
425 - 'message' => __('AI provider settings have been successfully disconnected.', 'fluent-support'),
426 - ]);
414 + if ($deletedRecords) {
415 + return $this->sendSuccess([
416 + 'message' => __('OpenAI settings have been successfully disconnected.', 'fluent-support'),
417 + ]);
418 + } else {
419 + return $this->sendError([
420 + 'message' => __('Failed to disconnect OpenAI settings. No matching records found or an error occurred.', 'fluent-support'),
421 + ]);
422 + }
427 423 }
428 424
429 - public function getAIProviderSettings()
425 + public function getOpenAISettings()
430 426 {
431 - $settings = Helper::getAIProviderSettings();
427 + $modelOptions = $this->getOpenAIModelOptions();
428 + $supportedModels = array_column($modelOptions, 'value');
432 429
433 - $provider = $settings['provider'] ?? 'openai';
434 - $apiKey = $settings['api_key'] ?? '';
435 - $model = $settings['model'] ?? '';
436 - $defaultEnabled = !empty($apiKey) ? 'yes' : 'no';
437 - $enabled = $settings['enabled'] ?? $defaultEnabled;
430 + $settings = [
431 + 'api_key' => '',
432 + 'model' => 'gpt-5.2',
433 + ];
438 434
439 - $availableModels = [];
440 - foreach (['openai', 'gemini', 'anthropic'] as $p) {
441 - $instance = AIProviderFactory::make($p, '', '');
442 - if (!is_wp_error($instance)) {
443 - $availableModels[$p] = $instance->getAvailableModels();
435 + $chatGPTSettingsData = Meta::where('object_type', '_fs_openai_settings')->first();
436 + if ($chatGPTSettingsData) {
437 + $settings = Helper::safeUnserialize($chatGPTSettingsData->value);
438 +
439 + if (!empty($settings['model']) && !in_array($settings['model'], $supportedModels, true)) {
440 + $previousModel = $settings['model'];
441 + $settings['model'] = 'gpt-5.2';
442 + Helper::saveOpenAIData('_fs_openai_settings', '_fs_openai_data', $settings);
443 + $settings['previous_model'] = $previousModel;
444 + $settings['model_migrated'] = true;
444 445 }
445 446 }
446 447
447 - if (empty($model) && !empty($availableModels[$provider])) {
448 - $model = $availableModels[$provider][0]['value'];
449 - }
448 + $settings['model_options'] = $modelOptions;
450 449
451 - return $this->sendSuccess([
452 - 'enabled' => $enabled,
453 - 'provider' => $provider,
454 - 'api_key' => !empty($apiKey) ? '****' . substr($apiKey, -4) : '',
455 - 'model' => $model,
456 - 'available_models' => $availableModels,
457 - ]);
450 + return $this->sendSuccess($settings);
458 451 }
459 452
453 + private function getOpenAIModelOptions()
454 + {
455 + $models = [
456 + ['value' => 'gpt-5.2', 'label' => 'GPT-5.2'],
457 + ['value' => 'gpt-5.2-chat-latest', 'label' => 'GPT-5.2 Chat'],
458 + ['value' => 'gpt-4.1', 'label' => 'GPT-4.1'],
459 + ['value' => 'gpt-4.1-mini', 'label' => 'GPT-4.1 Mini'],
460 + ['value' => 'gpt-4.1-nano', 'label' => 'GPT-4.1 Nano'],
461 + ['value' => 'gpt-4o', 'label' => 'GPT-4o'],
462 + ['value' => 'gpt-4o-mini', 'label' => 'GPT-4o Mini'],
463 + ['value' => 'gpt-4o-2024-08-06', 'label' => 'GPT-4o (2024-08-06)'],
464 + ['value' => 'gpt-4o-2024-05-13', 'label' => 'GPT-4o (2024-05-13)'],
465 + ['value' => 'gpt-4o-mini-2024-07-18', 'label' => 'GPT-4o Mini (2024-07-18)'],
466 + ['value' => 'gpt-4-turbo', 'label' => 'GPT-4 Turbo'],
467 + ['value' => 'gpt-4-turbo-2024-04-09', 'label' => 'GPT-4 Turbo (2024-04-09)'],
468 + ['value' => 'gpt-4-turbo-preview', 'label' => 'GPT-4 Turbo Preview'],
469 + ['value' => 'gpt-4', 'label' => 'GPT-4'],
470 + ['value' => 'gpt-4-0613', 'label' => 'GPT-4 (0613)'],
471 + ['value' => 'gpt-3.5-turbo', 'label' => 'GPT-3.5 Turbo'],
472 + ['value' => 'gpt-3.5-turbo-0125', 'label' => 'GPT-3.5 Turbo (0125)'],
473 + ['value' => 'o3', 'label' => 'o3'],
474 + ['value' => 'o3-mini', 'label' => 'o3-mini'],
475 + ['value' => 'o4-mini', 'label' => 'o4-mini'],
476 + ['value' => 'o1', 'label' => 'o1'],
477 + ['value' => 'gpt-4-0314', 'label' => 'GPT-4 (0314) - Deprecated soon'],
478 + ['value' => 'gpt-4-1106-preview', 'label' => 'GPT-4 (1106 Preview) - Deprecated soon'],
479 + ['value' => 'gpt-4-0125-preview', 'label' => 'GPT-4 (0125 Preview) - Deprecated soon'],
480 + ];
481 +
482 + return apply_filters('fluent_support/supported_openai_models', $models);
483 + }
484 +
460 485 public function getReCaptchaSettings()
461 486 {
462 487 $reCaptchaSettingsData = Meta::where('object_type', '_fs_recaptcha_settings')->first();
463 488 if ($reCaptchaSettingsData) {
@@ -496,9 +521,11 @@
496 521 ];
497 522
498 523 $url = add_query_arg($data, 'https://wpmanageninja.com/');
499 524
500 - wp_remote_post($url);
525 + wp_remote_post($url, [
526 + 'sslverify' => false
527 + ]);
501 528 }
502 529
503 530 /**
504 531 * installFluentCRM method will install Fluent CRM plugin
@@ -733,9 +760,8 @@
733 760 'title' => 'Dropbox',
734 761 'has_config' => true,
735 762 'is_configured' => $dropBoxConfigured,
736 763 'require_pro' => !defined('FLUENTSUPPORTPRO'),
737 - 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_dropbox'),
738 764 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/dbox.svg',
739 765 'description' => __('Upload and store the files to your Dropbox Storage.', 'fluent-support')
740 766 ],
741 767 'google_drive' => [
@@ -743,9 +769,9 @@
743 769 'title' => 'Google Drive',
744 770 'has_config' => true,
745 771 'is_configured' => $googleDriveConfigured,
746 772 'require_pro' => !defined('FLUENTSUPPORTPRO'),
747 - 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_google_drive'),
773 + 'upgrade_url' => 'https://fluentsupport.com/pricing',
748 774 'description' => __('Upload and store the files to your Google Drive Storage.', 'fluent-support'),
749 775 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/drive.svg',
750 776 ],
751 777 'cloudflare_r2' => [
@@ -753,9 +779,9 @@
753 779 'title' => 'Cloudflare R2',
754 780 'has_config' => true,
755 781 'is_configured' => $cloudflareR2Configured,
756 782 'require_pro' => !defined('FLUENTSUPPORTPRO'),
757 - 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_cloudflare_r2'),
783 + 'upgrade_url' => 'https://fluentsupport.com/pricing',
758 784 'description' => __('Upload and store the files to Cloudflare R2 Storage with zero egress fees.', 'fluent-support'),
759 785 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/cloudflare-r2.svg',
760 786 ],
761 787 'amazon_s3' => [
@@ -763,9 +789,9 @@
763 789 'title' => 'Amazon S3',
764 790 'has_config' => true,
765 791 'is_configured' => $amazonS3Configured,
766 792 'require_pro' => !defined('FLUENTSUPPORTPRO'),
767 - 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_amazon_s3'),
793 + 'upgrade_url' => 'https://fluentsupport.com/pricing',
768 794 'description' => __('Upload and store the files to Amazon S3 cloud storage.', 'fluent-support'),
769 795 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/amazon-s3.svg',
770 796 ]
771 797 ]);
@@ -808,35 +834,13 @@
808 834 $meta = Meta::where([
809 835 'object_type' => 'fluent_bot_settings',
810 836 'object_id' => 1,
811 837 'key' => '_fs_fluent_bot_config'
812 - ])->orderByDesc('id')->first();
838 + ])->first();
813 839
814 840 $settings = $meta ? Helper::safeUnserialize($meta->value) : [];
815 841
816 - if (!is_array($settings)) {
817 - $settings = [];
818 - }
819 -
820 - // Write-only secret: never return the raw team API key to the browser
821 - // (it authenticates the FluentBot API and would leak to logs/extensions/
822 - // XSS). Expose only whether a key is stored so the form can show a
823 - // "saved" hint; the input stays blank and only overwrites on a new key.
824 - $hasApiKey = !empty($settings['generalApiKey']);
825 - unset($settings['generalApiKey']);
826 -
827 - // Per-mapping apiKey is unused (the key is team-wide).
828 - if (!empty($settings['productMappings']) && is_array($settings['productMappings'])) {
829 - $settings['productMappings'] = array_map(function ($mapping) {
830 - if (!is_array($mapping)) {
831 - return $mapping;
832 - }
833 - unset($mapping['apiKey']);
834 - return $mapping;
835 - }, $settings['productMappings']);
836 - }
837 -
838 - $productItems = Product::orderedByTitle()->get()->map(function ($product) {
842 + $productItems = Product::all()->map(function ($product) {
839 843 return [
840 844 'id' => $product->id,
841 845 'title' => $product->title
842 846 ];
@@ -841,54 +845,28 @@
841 845 'title' => $product->title
842 846 ];
843 847 })->values()->all();
844 848
845 - // Default generalBotEnabled to true for backward compatibility with configs saved
846 - // before this flag existed — existing installs expect general bot to work on GET.
847 - $defaults = [
848 - 'generalBotId' => '',
849 - 'generalApiKey' => '',
850 - 'generalBotEnabled' => true,
851 - 'isEnabled' => false,
852 - 'productMappings' => [],
853 - 'products' => $productItems,
854 - ];
855 -
856 - return array_merge($defaults, $settings, [
857 - 'products' => $productItems,
858 - 'hasApiKey' => $hasApiKey,
849 + return array_merge([
850 + 'generalApiKey' => '',
851 + 'generalBotId' => '',
852 + 'isEnabled' => false,
853 + 'productMappings' => [],
854 + 'products' => $productItems
855 + ], $settings, [
856 + 'products' => $productItems
859 857 ]);
860 858 }
861 859
862 860 public function saveFluentBotSettings(Request $request)
863 861 {
864 - $where = [
865 - 'object_type' => 'fluent_bot_settings',
866 - 'object_id' => 1,
867 - 'key' => '_fs_fluent_bot_config'
868 - ];
869 -
870 - $existing = Meta::where($where)->orderByDesc('id')->first();
871 - $existingConfig = $existing ? Helper::safeUnserialize($existing->value) : [];
872 - if (!is_array($existingConfig)) {
873 - $existingConfig = [];
874 - }
875 -
876 - // Write-only key: a blank submission means "keep the stored key" (the form
877 - // never round-trips the secret), so only overwrite when a new key is sent.
878 - $submittedApiKey = trim($request->getSafe('generalApiKey', 'sanitize_text_field'));
879 - $apiKey = $submittedApiKey !== '' ? $submittedApiKey : ($existingConfig['generalApiKey'] ?? '');
880 -
881 862 $data = [
882 - 'generalBotId' => $request->getSafe('generalBotId', 'sanitize_text_field'),
883 - 'generalApiKey' => $apiKey,
884 - 'generalBotEnabled' => filter_var($request->get('generalBotEnabled', true), FILTER_VALIDATE_BOOLEAN),
885 - 'isEnabled' => $request->getSafe('isEnabled', 'rest_sanitize_boolean'),
863 + 'generalBotId' => $request->getSafe('generalBotId', 'sanitize_text_field'),
864 + 'isEnabled' => $request->getSafe('isEnabled', 'rest_sanitize_boolean'),
886 865 'productMappings' => []
887 866 ];
888 867
889 868 $productMappings = (array) $request->get('productMappings', []);
890 - $seenProductIds = [];
891 869
892 870 foreach ($productMappings as $mapping) {
893 871 if (!is_array($mapping)) {
894 872 continue;
@@ -893,79 +871,40 @@
893 871 if (!is_array($mapping)) {
894 872 continue;
895 873 }
896 874
897 - $productId = intval($mapping['productId'] ?? 0);
898 - $botId = trim(sanitize_text_field($mapping['botId'] ?? ''));
899 -
900 - // Drop invalid rows: empty botId would override the general bot with nothing
901 - // at resolution time (resolveApiCredentials), producing runtime failures.
902 - if ($productId < 1 || $botId === '') {
903 - continue;
904 - }
905 -
906 - // Dedupe by productId — first valid mapping wins.
907 - if (isset($seenProductIds[$productId])) {
908 - continue;
909 - }
910 - $seenProductIds[$productId] = true;
911 -
912 875 $data['productMappings'][] = [
913 - 'productId' => $productId,
876 + 'productId' => intval($mapping['productId'] ?? 0),
914 877 'productTitle' => sanitize_text_field($mapping['productTitle'] ?? ''),
915 - 'botId' => $botId,
878 + 'botId' => sanitize_text_field($mapping['botId'] ?? ''),
916 879 ];
917 880 }
918 881
919 882 $serialized = maybe_serialize($data);
920 883
884 + $existing = Meta::where([
885 + 'object_type' => 'fluent_bot_settings',
886 + 'object_id' => 1,
887 + 'key' => '_fs_fluent_bot_config'
888 + ])->first();
889 +
921 890 if ($existing) {
922 - // Update the latest row; do not prune siblings — concurrent first-writes could
923 - // race and delete each other's inserts, leaving zero rows (data loss).
924 - // Reads use orderByDesc('id')->first() so duplicates are harmless at read time.
925 891 $existing->update(['value' => $serialized]);
926 892 } else {
927 - Meta::create(array_merge($where, ['value' => $serialized]));
893 + Meta::create([
894 + 'object_type' => 'fluent_bot_settings',
895 + 'object_id' => 1,
896 + 'key' => '_fs_fluent_bot_config',
897 + 'value' => $serialized
898 + ]);
899 +
928 900 AIActivityLogsMigrator::migrate();
929 901 }
930 902
931 - // Write-only: never echo the raw key back to the browser.
932 - $responseData = $data;
933 - unset($responseData['generalApiKey']);
934 - $responseData['hasApiKey'] = $apiKey !== '';
935 -
936 903 return [
937 904 'success' => true,
938 905 'message' => 'Settings saved successfully',
939 - 'data' => $responseData
940 - ];
941 - }
942 -
943 - public function getFluentBotPresets()
944 - {
945 - $service = new \FluentSupport\App\Services\Integrations\FluentBot\FluentBotService();
946 - $custom = $service->getCustomPresets();
947 -
948 - if (!empty($custom)) {
949 - return ['presets' => $custom];
950 - }
951 -
952 - // Return defaults without persisting — saving happens only when the user explicitly posts.
953 - $helper = new \FluentSupport\App\Services\Integrations\FluentBot\FluentBotHelper();
954 - return ['presets' => $helper->getPresetPrompts('createResponse')];
955 - }
956 -
957 - public function saveFluentBotPresets(Request $request)
958 - {
959 - $presets = (array) $request->get('presets', []);
960 -
961 - $service = new \FluentSupport\App\Services\Integrations\FluentBot\FluentBotService();
962 - $saved = $service->saveCustomPresets($presets);
963 -
964 - return [
965 - 'success' => true,
966 - 'message' => __('Prompt options saved successfully', 'fluent-support'),
967 - 'presets' => $saved
906 + 'data' => $data
968 907 ];
969 908 }
970 909
971 910 }