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 +278 -100 1.10.22.4.0 View file →
@@ -7,11 +7,13 @@
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 -use FluentSupport\Framework\Request\Request;
13 +use FluentSupport\Framework\Http\Request\Request;
13 14 use FluentSupport\App\Hooks\Handlers\ReCaptchaHandler;
15 +use FluentSupport\Framework\Support\Arr;
14 16
15 17 /**
16 18 * SettingsController class is responsible for all settings
17 19 * This class is responsible for all request related to settings under global settings tab
@@ -58,9 +60,28 @@
58 60 */
59 61 public function saveSettings(Request $request)
60 62 {
61 63 $settingsKey = $request->getSafe('settings_key', 'sanitize_text_field');
62 - $settings = wp_unslash($request->getSafe('settings', null, []));
64 + $settings = wp_unslash($request->get('settings', null));
65 +
66 + // wp-editor fields: sanitize with wp_kses_post (same approach as Fluent Cart)
67 + $htmlFields = ['login_message'];
68 + $htmlValues = [];
69 + if (is_array($settings)) {
70 + foreach ($htmlFields as $field) {
71 + if (isset($settings[$field])) {
72 + $htmlValues[$field] = wp_kses_post($settings[$field]);
73 + }
74 + }
75 + }
76 +
77 + $settings = is_array($settings) ? map_deep($settings, 'sanitize_text_field') : [];
78 +
79 + // Restore HTML fields
80 + foreach ($htmlValues as $field => $value) {
81 + $settings[$field] = $value;
82 + }
83 +
63 84 (new Settings)->save($settingsKey, $settings);
64 85
65 86 return [
66 87 'message' => __('Settings has been updated', 'fluent-support')
@@ -85,9 +106,16 @@
85 106 * @throws \FluentSupport\Framework\Validator\ValidationException
86 107 */
87 108 public function setupPortal(Request $request)
88 109 {
89 - $mailbox = $request->getSafe('mailbox');
110 + $mailbox = $request->get('mailbox', null);
111 + $mailbox = is_array($mailbox) ? [
112 + 'name' => isset($mailbox['name']) ? sanitize_text_field($mailbox['name']) : '',
113 + 'email' => isset($mailbox['email']) ? sanitize_email($mailbox['email']) : '',
114 + 'box_type' => isset($mailbox['box_type']) ? sanitize_key($mailbox['box_type']) : '',
115 + 'is_default' => isset($mailbox['is_default']) ? sanitize_text_field($mailbox['is_default']) : 'yes',
116 + ] : [];
117 +
90 118 $this->validate($mailbox, [
91 119 'name' => 'required',
92 120 'email' => 'required|email',
93 121 'box_type' => 'required'
@@ -92,16 +120,18 @@
92 120 'email' => 'required|email',
93 121 'box_type' => 'required'
94 122 ]);
95 123
96 - $settings = $request->getSafe('global_settings');
124 + $settings = $request->get('global_settings', null);
125 + $settings = is_array($settings) ? [
126 + 'create_portal_page' => isset($settings['create_portal_page']) ? sanitize_text_field($settings['create_portal_page']) : 'no',
127 + 'portal_page_id' => isset($settings['portal_page_id']) ? intval($settings['portal_page_id']) : 0,
128 + ] : [];
97 129
98 130 $createPage = $settings['create_portal_page'] == 'yes';
99 131
100 132 if (!$createPage && empty($settings['portal_page_id'])) {
101 - return $this->sendError([
102 - 'message' => __('Please select a page or enable create page', 'fluent-support')
103 - ]);
133 + Helper::getSafeErrorMessage(new \Exception(__('Please select a page or enable create page', 'fluent-support')));
104 134 }
105 135
106 136 if ($createPage) {
107 137 // we have to create the page
@@ -235,15 +265,15 @@
235 265 }
236 266
237 267 public function setupInstallation(Request $request)
238 268 {
239 - $installFluentForm = $request->get('install_fluentform', 'no');
269 + $installFluentForm = $request->getSafe('install_fluentform', 'sanitize_text_field', 'no');
240 270
241 271 if ($installFluentForm == 'yes' && !defined('FLUENTFORM')) {
242 272 $this->installFluentForm();
243 273 }
244 274
245 - $optinEmail = $request->getSafe('optin_email', 'sanitize_text_field', 'no');
275 + $optinEmail = $request->getSafe('optin_email', 'sanitize_email', '');
246 276 if ($optinEmail && is_email($optinEmail)) {
247 277 $this->shareEmail($optinEmail);
248 278 }
249 279
@@ -261,9 +291,9 @@
261 291 public function saveReCaptchaSettings(Request $request)
262 292 {
263 293 $data = $request->get('reCaptcha');
264 294
265 - if ('clear-reCaptcha-settings' == $data) {
295 + if (is_string($data) && 'clear-reCaptcha-settings' === sanitize_text_field($data)) {
266 296 if (Meta::where('object_type', '_fs_recaptcha_settings')->delete()) {
267 297 return $this->sendSuccess([
268 298 'message' => __('Your reCAPTCHA settings deleted successfully.', 'fluent-support'),
269 299 ]);
@@ -273,14 +303,20 @@
273 303 'message' => __('Unable to delete reCAPTCHA settings, try again', 'fluent-support'),
274 304 ]);
275 305 }
276 306
307 + if (!is_array($data)) {
308 + return $this->sendError([
309 + 'message' => __('Invalid reCAPTCHA data.', 'fluent-support'),
310 + ]);
311 + }
312 +
277 313 $reCaptchaData = [
278 - 'reCaptcha_version' => sanitize_text_field($data['reCaptchaVersion']),
279 - 'siteKey' => sanitize_text_field($data['siteKey']),
280 - 'secretKey' => sanitize_text_field($data['secretKey']),
281 - 'formContainingReCaptcha' => array_map('sanitize_text_field', $data['formContainingReCaptcha']),
282 - 'is_enabled' => sanitize_text_field($data['reCaptchaEnabled'], 'no'),
314 + 'reCaptcha_version' => sanitize_text_field($data['reCaptchaVersion'] ?? ''),
315 + 'siteKey' => sanitize_text_field($data['siteKey'] ?? ''),
316 + 'secretKey' => sanitize_text_field($data['secretKey'] ?? ''),
317 + 'formContainingReCaptcha' => array_map('sanitize_text_field', (array) ($data['formContainingReCaptcha'] ?? [])),
318 + 'is_enabled' => sanitize_text_field($data['reCaptchaEnabled'] ?? 'no'),
283 319 ];
284 320
285 321 $previousValue = Meta::where('object_type', '_fs_recaptcha_settings')->first();
286 322
@@ -289,13 +325,21 @@
289 325 'message' => __('Your recaptcha details are already saved.', 'fluent-support'),
290 326 ]);
291 327 }
292 328
293 - $verifyReCaptcha = ReCaptchaHandler::validateRecaptcha($data['captchaResponse'], $data['secretKey'], $data['reCaptchaVersion']);
329 + $captchaResponse = sanitize_text_field($data['captchaResponse'] ?? '');
294 330
295 - if (!$verifyReCaptcha) {
331 + if ($captchaResponse) {
332 + $verifyReCaptcha = ReCaptchaHandler::validateRecaptcha($captchaResponse, $reCaptchaData['secretKey'], $reCaptchaData['reCaptcha_version']);
333 +
334 + if (!$verifyReCaptcha) {
335 + return $this->sendError([
336 + 'message' => __('Your reCAPTCHA settings are not valid.', 'fluent-support'),
337 + ]);
338 + }
339 + } elseif (!$previousValue) {
296 340 return $this->sendError([
297 - 'message' => __('Your reCAPTCHA settings are not valid.', 'fluent-support'),
341 + 'message' => __('Please verify reCAPTCHA before saving.', 'fluent-support'),
298 342 ]);
299 343 }
300 344
301 345 if ($previousValue) {
@@ -317,79 +361,101 @@
317 361 'message' => __('Your reCAPTCHA settings added successfully.', 'fluent-support'),
318 362 ]);
319 363 }
320 364
321 - public function saveOpenAISettings(Request $request)
365 + public function saveAIProviderSettings(Request $request)
322 366 {
323 - $data = $request->get();
324 - $data = [
325 - 'api_key' => sanitize_text_field($data['api_key']),
326 - 'model' => sanitize_text_field($data['model']),
327 - ];
367 + $allowedProviders = AIProviderFactory::getAllowedProviders();
368 + $provider = $request->getSafe('provider', 'sanitize_text_field', 'openai');
328 369
329 - $response = Helper::authorizeChatGPTAPIKey($data);
330 -
331 - if (is_wp_error($response)) {
370 + if (!in_array($provider, $allowedProviders, true)) {
332 371 return $this->sendError([
333 - 'message' => __('There was an error verifying the API key.', 'fluent-support'),
372 + 'message' => __('Invalid AI provider selected.', 'fluent-support'),
334 373 ]);
335 374 }
336 375
337 - $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', '');
338 379
339 - if (isset($body['error'])) {
340 - return $this->sendError([
341 - 'message' => __('Invalid API key. Please provide a valid ChatGPT API key.', 'fluent-support'),
342 - ]);
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 + }
343 387 }
344 388
389 + $data = [
390 + 'enabled' => $enabled,
391 + 'provider' => $provider,
392 + 'api_key' => $apiKey,
393 + 'model' => $model,
394 + ];
395 +
345 396 try {
346 - $isDataSaved = Helper::saveOpenAIData('_fs_openai_settings', '_fs_openai_data', $data);
347 - if ($isDataSaved) {
348 - AIActivityLogsMigrator::migrate();
349 - }
397 + Helper::saveAIProviderSettings($data);
398 + AIProviderFactory::clearCache();
399 + AIActivityLogsMigrator::migrate();
400 +
350 401 return $this->sendSuccess([
351 - 'message' => __('OpenAI settings have been successfully saved.', 'fluent-support'),
402 + 'message' => __('AI settings have been successfully saved.', 'fluent-support'),
352 403 ]);
353 404 } catch (\Exception $e) {
354 - // translators: %s is the error message from the exception
355 - $translatedMessage = __('An error occurred while saving the settings: %s', 'fluent-support');
356 - $errorMessage = sprintf($translatedMessage, $e->getMessage());
357 -
358 405 return $this->sendError([
359 - 'message' => $errorMessage,
406 + 'message' => sprintf(
407 + __('An error occurred while saving the settings: %s', 'fluent-support'),
408 + Helper::getSafeErrorMessage($e)
409 + ),
360 410 ]);
361 411 }
362 412 }
363 413
364 -
365 - public function disconnectOpenAI()
414 + public function disconnectAIProvider()
366 415 {
367 - $deletedRecords = Meta::where([
416 + Meta::where('object_type', '_fs_ai_provider_settings')->delete();
417 + Meta::where([
368 418 'object_type' => '_fs_openai_settings',
369 419 'key' => '_fs_openai_data',
370 420 ])->delete();
371 421
372 - if ($deletedRecords) {
373 - return $this->sendSuccess([
374 - 'message' => __('OpenAI settings have been successfully disconnected.', 'fluent-support'),
375 - ]);
376 - } else {
377 - return $this->sendError([
378 - 'message' => __('Failed to disconnect OpenAI settings. No matching records found or an error occurred.', 'fluent-support'),
379 - ]);
380 - }
422 + AIProviderFactory::clearCache();
423 +
424 + return $this->sendSuccess([
425 + 'message' => __('AI provider settings have been successfully disconnected.', 'fluent-support'),
426 + ]);
381 427 }
382 428
383 - public function getOpenAISettings()
429 + public function getAIProviderSettings()
384 430 {
385 - $chatGPTSettingsData = Meta::where('object_type', '_fs_openai_settings')->first();
386 - if ($chatGPTSettingsData) {
387 - $settings = Helper::safeUnserialize($chatGPTSettingsData->value);
388 - return $this->sendSuccess($settings);
431 + $settings = Helper::getAIProviderSettings();
432 +
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;
438 +
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();
444 + }
389 445 }
390 446
391 - return [];
447 + if (empty($model) && !empty($availableModels[$provider])) {
448 + $model = $availableModels[$provider][0]['value'];
449 + }
450 +
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 + ]);
392 458 }
393 459
394 460 public function getReCaptchaSettings()
395 461 {
@@ -430,11 +496,9 @@
430 496 ];
431 497
432 498 $url = add_query_arg($data, 'https://wpmanageninja.com/');
433 499
434 - wp_remote_post($url, [
435 - 'sslverify' => false
436 - ]);
500 + wp_remote_post($url);
437 501 }
438 502
439 503 /**
440 504 * installFluentCRM method will install Fluent CRM plugin
@@ -492,18 +556,18 @@
492 556 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-support')
493 557 ]);
494 558 }
495 559
496 - $plugin_id = 'fluent-crm';
560 + $plugin_id = 'fluentform';
497 561 $plugin = [
498 - 'name' => 'Fluent CRM',
499 - 'repo-slug' => 'fluent-crm',
500 - 'file' => 'fluent-crm.php',
562 + 'name' => 'Fluent Forms',
563 + 'repo-slug' => 'fluentform',
564 + 'file' => 'fluentform.php',
501 565 ];
502 566
503 567 $this->backgroundInstaller($plugin, $plugin_id);
504 568
505 - if (defined('FLUENTCRM')) {
569 + if (defined('FLUENTFORM')) {
506 570 return [
507 571 'is_installed' => true,
508 572 'message' => __('Fluent Forms plugin has been installed and activated successfully', 'fluent-support')
509 573 ];
@@ -638,8 +702,10 @@
638 702 public function getRemoteUploadSettings(Request $request)
639 703 {
640 704 $dropBoxConfigured = false;
641 705 $googleDriveConfigured = false;
706 + $cloudflareR2Configured = false;
707 + $amazonS3Configured = false;
642 708
643 709 if (defined('FLUENTSUPPORTPRO')) {
644 710 $dropBoxSettings = Helper::getIntegrationOption('dropbox_settings');
645 711 $dropBoxConfigured = $dropBoxSettings && !empty($dropBoxSettings['access_token']);
@@ -645,8 +711,14 @@
645 711 $dropBoxConfigured = $dropBoxSettings && !empty($dropBoxSettings['access_token']);
646 712
647 713 $googleDriveSettings = Helper::getIntegrationOption('google_drive_settings');
648 714 $googleDriveConfigured = $googleDriveSettings && !empty($googleDriveSettings['access_token']);
715 +
716 + $cloudflareR2Settings = Helper::getIntegrationOption('cloudflare_r2_settings');
717 + $cloudflareR2Configured = $cloudflareR2Settings && !empty($cloudflareR2Settings['secret_access_key']) && Arr::get($cloudflareR2Settings, 'status') == 'yes';
718 +
719 + $amazonS3Settings = Helper::getIntegrationOption('amazon_s3_settings');
720 + $amazonS3Configured = $amazonS3Settings && !empty($amazonS3Settings['secret_access_key']) && Arr::get($amazonS3Settings, 'status') == 'yes';
649 721 }
650 722
651 723 $drivers = apply_filters('fluent_support/storage_drivers_info', [
652 724 'local' => [
@@ -661,8 +733,9 @@
661 733 'title' => 'Dropbox',
662 734 'has_config' => true,
663 735 'is_configured' => $dropBoxConfigured,
664 736 'require_pro' => !defined('FLUENTSUPPORTPRO'),
737 + 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_dropbox'),
665 738 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/dbox.svg',
666 739 'description' => __('Upload and store the files to your Dropbox Storage.', 'fluent-support')
667 740 ],
668 741 'google_drive' => [
@@ -670,11 +743,31 @@
670 743 'title' => 'Google Drive',
671 744 'has_config' => true,
672 745 'is_configured' => $googleDriveConfigured,
673 746 'require_pro' => !defined('FLUENTSUPPORTPRO'),
674 - 'upgrade_url' => 'https://fluentsupport.com/pricing',
747 + 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_google_drive'),
675 748 'description' => __('Upload and store the files to your Google Drive Storage.', 'fluent-support'),
676 749 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/drive.svg',
750 + ],
751 + 'cloudflare_r2' => [
752 + 'meta_key' => 'cloudflare_r2_settings',
753 + 'title' => 'Cloudflare R2',
754 + 'has_config' => true,
755 + 'is_configured' => $cloudflareR2Configured,
756 + 'require_pro' => !defined('FLUENTSUPPORTPRO'),
757 + 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_cloudflare_r2'),
758 + 'description' => __('Upload and store the files to Cloudflare R2 Storage with zero egress fees.', 'fluent-support'),
759 + 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/cloudflare-r2.svg',
760 + ],
761 + 'amazon_s3' => [
762 + 'meta_key' => 'amazon_s3_settings',
763 + 'title' => 'Amazon S3',
764 + 'has_config' => true,
765 + 'is_configured' => $amazonS3Configured,
766 + 'require_pro' => !defined('FLUENTSUPPORTPRO'),
767 + 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_amazon_s3'),
768 + 'description' => __('Upload and store the files to Amazon S3 cloud storage.', 'fluent-support'),
769 + 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/amazon-s3.svg',
677 770 ]
678 771 ]);
679 772
680 773 return [
@@ -715,13 +808,35 @@
715 808 $meta = Meta::where([
716 809 'object_type' => 'fluent_bot_settings',
717 810 'object_id' => 1,
718 811 'key' => '_fs_fluent_bot_config'
719 - ])->first();
812 + ])->orderByDesc('id')->first();
720 813
721 814 $settings = $meta ? Helper::safeUnserialize($meta->value) : [];
722 815
723 - $productItems = Product::all()->map(function ($product) {
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) {
724 839 return [
725 840 'id' => $product->id,
726 841 'title' => $product->title
727 842 ];
@@ -726,29 +841,54 @@
726 841 'title' => $product->title
727 842 ];
728 843 })->values()->all();
729 844
730 - return array_merge([
731 - 'generalApiKey' => '',
732 - 'generalBotId' => '',
733 - 'isEnabled' => false,
734 - 'productMappings' => [],
735 - 'products' => $productItems
736 - ], $settings, [
737 - 'products' => $productItems
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,
738 859 ]);
739 860 }
740 861
741 862 public function saveFluentBotSettings(Request $request)
742 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 +
743 881 $data = [
744 - 'generalApiKey' => $request->getSafe('generalApiKey', 'sanitize_text_field'),
745 - 'generalBotId' => $request->getSafe('generalBotId', 'sanitize_text_field'),
746 - 'isEnabled' => $request->getSafe('isEnabled', 'sanitize_text_field'),
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'),
747 886 'productMappings' => []
748 887 ];
749 888
750 889 $productMappings = (array) $request->get('productMappings', []);
890 + $seenProductIds = [];
751 891
752 892 foreach ($productMappings as $mapping) {
753 893 if (!is_array($mapping)) {
754 894 continue;
@@ -753,41 +893,79 @@
753 893 if (!is_array($mapping)) {
754 894 continue;
755 895 }
756 896
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 +
757 912 $data['productMappings'][] = [
758 - 'productId' => intval($mapping['productId'] ?? 0),
913 + 'productId' => $productId,
759 914 'productTitle' => sanitize_text_field($mapping['productTitle'] ?? ''),
760 - 'apiKey' => sanitize_text_field($mapping['apiKey'] ?? ''),
761 - 'botId' => sanitize_text_field($mapping['botId'] ?? ''),
915 + 'botId' => $botId,
762 916 ];
763 917 }
764 918
765 919 $serialized = maybe_serialize($data);
766 920
767 - $existing = Meta::where([
768 - 'object_type' => 'fluent_bot_settings',
769 - 'object_id' => 1,
770 - 'key' => '_fs_fluent_bot_config'
771 - ])->first();
772 -
773 921 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.
774 925 $existing->update(['value' => $serialized]);
775 926 } else {
776 - Meta::create([
777 - 'object_type' => 'fluent_bot_settings',
778 - 'object_id' => 1,
779 - 'key' => '_fs_fluent_bot_config',
780 - 'value' => $serialized
781 - ]);
782 -
927 + Meta::create(array_merge($where, ['value' => $serialized]));
783 928 AIActivityLogsMigrator::migrate();
784 929 }
785 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 +
786 936 return [
787 937 'success' => true,
788 938 'message' => 'Settings saved successfully',
789 - 'data' => $data
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
790 968 ];
791 969 }
792 970
793 971 }