PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.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 +101 -104 2.2.02.4.0 View file →
@@ -7,8 +7,9 @@
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;
11 12 use FluentSupport\Database\Migrations\AIActivityLogsMigrator;
12 13 use FluentSupport\Framework\Http\Request\Request;
13 14 use FluentSupport\App\Hooks\Handlers\ReCaptchaHandler;
14 15 use FluentSupport\Framework\Support\Arr;
@@ -360,129 +361,103 @@
360 361 'message' => __('Your reCAPTCHA settings added successfully.', 'fluent-support'),
361 362 ]);
362 363 }
363 364
364 - public function saveOpenAISettings(Request $request)
365 + public function saveAIProviderSettings(Request $request)
365 366 {
366 - $data = [
367 - 'api_key' => $request->getSafe('api_key', 'sanitize_text_field', ''),
368 - 'model' => $request->getSafe('model', 'sanitize_text_field', ''),
369 - ];
367 + $allowedProviders = AIProviderFactory::getAllowedProviders();
368 + $provider = $request->getSafe('provider', 'sanitize_text_field', 'openai');
370 369
371 - $response = Helper::authorizeChatGPTAPIKey($data);
372 -
373 - if (is_wp_error($response)) {
370 + if (!in_array($provider, $allowedProviders, true)) {
374 371 return $this->sendError([
375 - 'message' => __('There was an error verifying the API key.', 'fluent-support'),
372 + 'message' => __('Invalid AI provider selected.', 'fluent-support'),
376 373 ]);
377 374 }
378 375
379 - $body = json_decode(wp_remote_retrieve_body($response), true);
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', '');
380 379
381 - if (isset($body['error'])) {
382 - return $this->sendError([
383 - 'message' => __('Invalid API key. Please provide a valid ChatGPT API key.', 'fluent-support'),
384 - ]);
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 + }
385 387 }
386 388
389 + $data = [
390 + 'enabled' => $enabled,
391 + 'provider' => $provider,
392 + 'api_key' => $apiKey,
393 + 'model' => $model,
394 + ];
395 +
387 396 try {
388 - $isDataSaved = Helper::saveOpenAIData('_fs_openai_settings', '_fs_openai_data', $data);
389 - if ($isDataSaved) {
390 - AIActivityLogsMigrator::migrate();
391 - }
397 + Helper::saveAIProviderSettings($data);
398 + AIProviderFactory::clearCache();
399 + AIActivityLogsMigrator::migrate();
400 +
392 401 return $this->sendSuccess([
393 - 'message' => __('OpenAI settings have been successfully saved.', 'fluent-support'),
402 + 'message' => __('AI settings have been successfully saved.', 'fluent-support'),
394 403 ]);
395 404 } 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 -
400 405 return $this->sendError([
401 - 'message' => $errorMessage,
406 + 'message' => sprintf(
407 + __('An error occurred while saving the settings: %s', 'fluent-support'),
408 + Helper::getSafeErrorMessage($e)
409 + ),
402 410 ]);
403 411 }
404 412 }
405 413
406 -
407 - public function disconnectOpenAI()
414 + public function disconnectAIProvider()
408 415 {
409 - $deletedRecords = Meta::where([
416 + Meta::where('object_type', '_fs_ai_provider_settings')->delete();
417 + Meta::where([
410 418 'object_type' => '_fs_openai_settings',
411 419 'key' => '_fs_openai_data',
412 420 ])->delete();
413 421
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 - }
422 + AIProviderFactory::clearCache();
423 +
424 + return $this->sendSuccess([
425 + 'message' => __('AI provider settings have been successfully disconnected.', 'fluent-support'),
426 + ]);
423 427 }
424 428
425 - public function getOpenAISettings()
429 + public function getAIProviderSettings()
426 430 {
427 - $modelOptions = $this->getOpenAIModelOptions();
428 - $supportedModels = array_column($modelOptions, 'value');
431 + $settings = Helper::getAIProviderSettings();
429 432
430 - $settings = [
431 - 'api_key' => '',
432 - 'model' => 'gpt-5.2',
433 - ];
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;
434 438
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;
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();
445 444 }
446 445 }
447 446
448 - $settings['model_options'] = $modelOptions;
447 + if (empty($model) && !empty($availableModels[$provider])) {
448 + $model = $availableModels[$provider][0]['value'];
449 + }
449 450
450 - return $this->sendSuccess($settings);
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 + ]);
451 458 }
452 459
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 -
485 460 public function getReCaptchaSettings()
486 461 {
487 462 $reCaptchaSettingsData = Meta::where('object_type', '_fs_recaptcha_settings')->first();
488 463 if ($reCaptchaSettingsData) {
@@ -521,11 +496,9 @@
521 496 ];
522 497
523 498 $url = add_query_arg($data, 'https://wpmanageninja.com/');
524 499
525 - wp_remote_post($url, [
526 - 'sslverify' => false
527 - ]);
500 + wp_remote_post($url);
528 501 }
529 502
530 503 /**
531 504 * installFluentCRM method will install Fluent CRM plugin
@@ -760,8 +733,9 @@
760 733 'title' => 'Dropbox',
761 734 'has_config' => true,
762 735 'is_configured' => $dropBoxConfigured,
763 736 'require_pro' => !defined('FLUENTSUPPORTPRO'),
737 + 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_dropbox'),
764 738 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/dbox.svg',
765 739 'description' => __('Upload and store the files to your Dropbox Storage.', 'fluent-support')
766 740 ],
767 741 'google_drive' => [
@@ -769,9 +743,9 @@
769 743 'title' => 'Google Drive',
770 744 'has_config' => true,
771 745 'is_configured' => $googleDriveConfigured,
772 746 'require_pro' => !defined('FLUENTSUPPORTPRO'),
773 - 'upgrade_url' => 'https://fluentsupport.com/pricing',
747 + 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_google_drive'),
774 748 'description' => __('Upload and store the files to your Google Drive Storage.', 'fluent-support'),
775 749 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/drive.svg',
776 750 ],
777 751 'cloudflare_r2' => [
@@ -779,9 +753,9 @@
779 753 'title' => 'Cloudflare R2',
780 754 'has_config' => true,
781 755 'is_configured' => $cloudflareR2Configured,
782 756 'require_pro' => !defined('FLUENTSUPPORTPRO'),
783 - 'upgrade_url' => 'https://fluentsupport.com/pricing',
757 + 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_cloudflare_r2'),
784 758 'description' => __('Upload and store the files to Cloudflare R2 Storage with zero egress fees.', 'fluent-support'),
785 759 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/cloudflare-r2.svg',
786 760 ],
787 761 'amazon_s3' => [
@@ -789,9 +763,9 @@
789 763 'title' => 'Amazon S3',
790 764 'has_config' => true,
791 765 'is_configured' => $amazonS3Configured,
792 766 'require_pro' => !defined('FLUENTSUPPORTPRO'),
793 - 'upgrade_url' => 'https://fluentsupport.com/pricing',
767 + 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_amazon_s3'),
794 768 'description' => __('Upload and store the files to Amazon S3 cloud storage.', 'fluent-support'),
795 769 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/amazon-s3.svg',
796 770 ]
797 771 ]);
@@ -842,10 +816,16 @@
842 816 if (!is_array($settings)) {
843 817 $settings = [];
844 818 }
845 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']);
846 825 unset($settings['generalApiKey']);
847 826
827 + // Per-mapping apiKey is unused (the key is team-wide).
848 828 if (!empty($settings['productMappings']) && is_array($settings['productMappings'])) {
849 829 $settings['productMappings'] = array_map(function ($mapping) {
850 830 if (!is_array($mapping)) {
851 831 return $mapping;
@@ -854,9 +834,9 @@
854 834 return $mapping;
855 835 }, $settings['productMappings']);
856 836 }
857 837
858 - $productItems = Product::all()->map(function ($product) {
838 + $productItems = Product::orderedByTitle()->get()->map(function ($product) {
859 839 return [
860 840 'id' => $product->id,
861 841 'title' => $product->title
862 842 ];
@@ -865,8 +845,9 @@
865 845 // Default generalBotEnabled to true for backward compatibility with configs saved
866 846 // before this flag existed — existing installs expect general bot to work on GET.
867 847 $defaults = [
868 848 'generalBotId' => '',
849 + 'generalApiKey' => '',
869 850 'generalBotEnabled' => true,
870 851 'isEnabled' => false,
871 852 'productMappings' => [],
872 853 'products' => $productItems,
@@ -872,16 +853,35 @@
872 853 'products' => $productItems,
873 854 ];
874 855
875 856 return array_merge($defaults, $settings, [
876 - 'products' => $productItems
857 + 'products' => $productItems,
858 + 'hasApiKey' => $hasApiKey,
877 859 ]);
878 860 }
879 861
880 862 public function saveFluentBotSettings(Request $request)
881 863 {
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 +
882 881 $data = [
883 882 'generalBotId' => $request->getSafe('generalBotId', 'sanitize_text_field'),
883 + 'generalApiKey' => $apiKey,
884 884 'generalBotEnabled' => filter_var($request->get('generalBotEnabled', true), FILTER_VALIDATE_BOOLEAN),
885 885 'isEnabled' => $request->getSafe('isEnabled', 'rest_sanitize_boolean'),
886 886 'productMappings' => []
887 887 ];
@@ -917,16 +917,8 @@
917 917 }
918 918
919 919 $serialized = maybe_serialize($data);
920 920
921 - $where = [
922 - 'object_type' => 'fluent_bot_settings',
923 - 'object_id' => 1,
924 - 'key' => '_fs_fluent_bot_config'
925 - ];
926 -
927 - $existing = Meta::where($where)->orderByDesc('id')->first();
928 -
929 921 if ($existing) {
930 922 // Update the latest row; do not prune siblings — concurrent first-writes could
931 923 // race and delete each other's inserts, leaving zero rows (data loss).
932 924 // Reads use orderByDesc('id')->first() so duplicates are harmless at read time.
@@ -935,12 +927,17 @@
935 927 Meta::create(array_merge($where, ['value' => $serialized]));
936 928 AIActivityLogsMigrator::migrate();
937 929 }
938 930
931 + // Write-only: never echo the raw key back to the browser.
932 + $responseData = $data;
933 + unset($responseData['generalApiKey']);
934 + $responseData['hasApiKey'] = $apiKey !== '';
935 +
939 936 return [
940 937 'success' => true,
941 938 'message' => 'Settings saved successfully',
942 - 'data' => $data
939 + 'data' => $responseData
943 940 ];
944 941 }
945 942
946 943 public function getFluentBotPresets()