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 +505 -27 1.5.52.4.0 View file →
@@ -4,11 +4,16 @@
4 4
5 5
6 6 use FluentSupport\App\Models\MailBox;
7 7 use FluentSupport\App\Models\Meta;
8 +use FluentSupport\App\Models\Product;
8 9 use FluentSupport\App\Services\EmailNotification\Settings;
9 10 use FluentSupport\App\Services\Helper;
10 -use FluentSupport\Framework\Request\Request;
11 +use FluentSupport\App\Services\Integrations\AI\AIProviderFactory;
12 +use FluentSupport\Database\Migrations\AIActivityLogsMigrator;
13 +use FluentSupport\Framework\Http\Request\Request;
14 +use FluentSupport\App\Hooks\Handlers\ReCaptchaHandler;
15 +use FluentSupport\Framework\Support\Arr;
11 16
12 17 /**
13 18 * SettingsController class is responsible for all settings
14 19 * This class is responsible for all request related to settings under global settings tab
@@ -24,9 +29,9 @@
24 29 * @return array|array[]
25 30 */
26 31 public function getSettings(Request $request)
27 32 {
28 - $settingsKey = $request->get('settings_key');
33 + $settingsKey = $request->getSafe('settings_key', 'sanitize_text_field');
29 34
30 35 return (new Settings)->get($settingsKey);
31 36 }
32 37
@@ -39,9 +44,9 @@
39 44 {
40 45 $settings = Meta::where('object_type', 'integration_settings')->get();
41 46 $integrationSettings = [];
42 47 foreach ($settings as $index => $setting) {
43 - $data = maybe_unserialize($setting->value);
48 + $data = Helper::safeUnserialize($setting->value);
44 49 if (!empty($data['status']) && $data && $data['status'] == 'yes') {
45 50 $integrationSettings[] = $setting->key;
46 51 }
47 52 }
@@ -54,11 +59,29 @@
54 59 * @return array
55 60 */
56 61 public function saveSettings(Request $request)
57 62 {
58 - $settingsKey = $request->get('settings_key');
59 - $settings = wp_unslash($request->get('settings'));
63 + $settingsKey = $request->getSafe('settings_key', 'sanitize_text_field');
64 + $settings = wp_unslash($request->get('settings', null));
60 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 +
61 84 (new Settings)->save($settingsKey, $settings);
62 85
63 86 return [
64 87 'message' => __('Settings has been updated', 'fluent-support')
@@ -83,9 +106,16 @@
83 106 * @throws \FluentSupport\Framework\Validator\ValidationException
84 107 */
85 108 public function setupPortal(Request $request)
86 109 {
87 - $mailbox = $request->get('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 +
88 118 $this->validate($mailbox, [
89 119 'name' => 'required',
90 120 'email' => 'required|email',
91 121 'box_type' => 'required'
@@ -90,16 +120,18 @@
90 120 'email' => 'required|email',
91 121 'box_type' => 'required'
92 122 ]);
93 123
94 - $settings = $request->get('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 + ] : [];
95 129
96 130 $createPage = $settings['create_portal_page'] == 'yes';
97 131
98 132 if (!$createPage && empty($settings['portal_page_id'])) {
99 - return $this->sendError([
100 - 'message' => __('Please select a page or enable create page', 'fluent-support')
101 - ]);
133 + Helper::getSafeErrorMessage(new \Exception(__('Please select a page or enable create page', 'fluent-support')));
102 134 }
103 135
104 136 if ($createPage) {
105 137 // we have to create the page
@@ -109,9 +141,9 @@
109 141 'ping_status' => 'close',
110 142 'post_author' => get_current_user_id(),
111 143 'post_title' => __('Support Portal', 'fluent-support'),
112 144 'post_status' => 'publish',
113 - 'post_content' => '[fluent_support_portal]',
145 + 'post_content' => '<!-- wp:shortcode -->[fluent_support_portal]<!-- /wp:shortcode -->',
114 146 'post_type' => 'page'
115 147 )
116 148 );
117 149 } else {
@@ -195,9 +227,9 @@
195 227 'inline_help' => __('Select the default status for new contacts. If you select pending and it\'s a new contact then a double optin email will be sent', 'fluent-support')
196 228 ],
197 229 'assigned_list' => [
198 230 'type' => 'input-options',
199 - 'label' => __('Add to FluentCRM list (optional)', 'fluent-crm'),
231 + 'label' => __('Add to FluentCRM list (optional)', 'fluent-support'),
200 232 'options' => \FluentCrm\App\Models\Lists::select(['id', 'title'])->orderBy('title', 'ASC')->get(),
201 233 'dependency' => [
202 234 'depends_on' => 'enabled',
203 235 'operator' => '=',
@@ -206,9 +238,9 @@
206 238 ],
207 239 'assigned_tags' => [
208 240 'type' => 'input-options',
209 241 'multiple' => true,
210 - 'label' => __('Add to Tags', 'fluent-crm'),
242 + 'label' => __('Add to Tags', 'fluent-support'),
211 243 'options' => \FluentCrm\App\Models\Tag::select(['id', 'title'])->orderBy('title', 'ASC')->get(),
212 244 'dependency' => [
213 245 'depends_on' => 'enabled',
214 246 'operator' => '=',
@@ -231,23 +263,22 @@
231 263 ];
232 264
233 265 }
234 266
235 -
236 267 public function setupInstallation(Request $request)
237 268 {
238 - $installFluentForm = $request->get('install_fluentform', 'no');
269 + $installFluentForm = $request->getSafe('install_fluentform', 'sanitize_text_field', 'no');
239 270
240 271 if ($installFluentForm == 'yes' && !defined('FLUENTFORM')) {
241 272 $this->installFluentForm();
242 273 }
243 274
244 - $optinEmail = $request->get('optin_email', 'no');
275 + $optinEmail = $request->getSafe('optin_email', 'sanitize_email', '');
245 276 if ($optinEmail && is_email($optinEmail)) {
246 277 $this->shareEmail($optinEmail);
247 278 }
248 279
249 - $shareEssential = $request->get('share_essentials', 'no');
280 + $shareEssential = $request->getSafe('share_essentials', 'sanitize_text_field', 'no');
250 281 if ($shareEssential == 'yes') {
251 282 Helper::updateOption('_share_essential', $shareEssential);
252 283 }
253 284
@@ -256,8 +287,188 @@
256 287 ]);
257 288
258 289 }
259 290
291 + public function saveReCaptchaSettings(Request $request)
292 + {
293 + $data = $request->get('reCaptcha');
294 +
295 + if (is_string($data) && 'clear-reCaptcha-settings' === sanitize_text_field($data)) {
296 + if (Meta::where('object_type', '_fs_recaptcha_settings')->delete()) {
297 + return $this->sendSuccess([
298 + 'message' => __('Your reCAPTCHA settings deleted successfully.', 'fluent-support'),
299 + ]);
300 + }
301 +
302 + return $this->sendError([
303 + 'message' => __('Unable to delete reCAPTCHA settings, try again', 'fluent-support'),
304 + ]);
305 + }
306 +
307 + if (!is_array($data)) {
308 + return $this->sendError([
309 + 'message' => __('Invalid reCAPTCHA data.', 'fluent-support'),
310 + ]);
311 + }
312 +
313 + $reCaptchaData = [
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'),
319 + ];
320 +
321 + $previousValue = Meta::where('object_type', '_fs_recaptcha_settings')->first();
322 +
323 + if ($previousValue === $reCaptchaData) {
324 + return $this->sendError([
325 + 'message' => __('Your recaptcha details are already saved.', 'fluent-support'),
326 + ]);
327 + }
328 +
329 + $captchaResponse = sanitize_text_field($data['captchaResponse'] ?? '');
330 +
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) {
340 + return $this->sendError([
341 + 'message' => __('Please verify reCAPTCHA before saving.', 'fluent-support'),
342 + ]);
343 + }
344 +
345 + if ($previousValue) {
346 + Meta::where('object_type', '_fs_recaptcha_settings')->update([
347 + 'value' => maybe_serialize($reCaptchaData)
348 + ]);
349 + return $this->sendSuccess([
350 + 'message' => __('Your reCAPTCHA settings updated successfully.', 'fluent-support'),
351 + ]);
352 + } else {
353 + Meta::insert([
354 + 'object_type' => '_fs_recaptcha_settings',
355 + 'key' => '_fs_recaptcha_data',
356 + 'value' => maybe_serialize($reCaptchaData)
357 + ]);
358 + }
359 +
360 + return $this->sendSuccess([
361 + 'message' => __('Your reCAPTCHA settings added successfully.', 'fluent-support'),
362 + ]);
363 + }
364 +
365 + public function saveAIProviderSettings(Request $request)
366 + {
367 + $allowedProviders = AIProviderFactory::getAllowedProviders();
368 + $provider = $request->getSafe('provider', 'sanitize_text_field', 'openai');
369 +
370 + if (!in_array($provider, $allowedProviders, true)) {
371 + return $this->sendError([
372 + 'message' => __('Invalid AI provider selected.', 'fluent-support'),
373 + ]);
374 + }
375 +
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 +
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 + }
387 + }
388 +
389 + $data = [
390 + 'enabled' => $enabled,
391 + 'provider' => $provider,
392 + 'api_key' => $apiKey,
393 + 'model' => $model,
394 + ];
395 +
396 + try {
397 + Helper::saveAIProviderSettings($data);
398 + AIProviderFactory::clearCache();
399 + AIActivityLogsMigrator::migrate();
400 +
401 + return $this->sendSuccess([
402 + 'message' => __('AI settings have been successfully saved.', 'fluent-support'),
403 + ]);
404 + } catch (\Exception $e) {
405 + return $this->sendError([
406 + 'message' => sprintf(
407 + __('An error occurred while saving the settings: %s', 'fluent-support'),
408 + Helper::getSafeErrorMessage($e)
409 + ),
410 + ]);
411 + }
412 + }
413 +
414 + public function disconnectAIProvider()
415 + {
416 + Meta::where('object_type', '_fs_ai_provider_settings')->delete();
417 + Meta::where([
418 + 'object_type' => '_fs_openai_settings',
419 + 'key' => '_fs_openai_data',
420 + ])->delete();
421 +
422 + AIProviderFactory::clearCache();
423 +
424 + return $this->sendSuccess([
425 + 'message' => __('AI provider settings have been successfully disconnected.', 'fluent-support'),
426 + ]);
427 + }
428 +
429 + public function getAIProviderSettings()
430 + {
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 + }
445 + }
446 +
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 + ]);
458 + }
459 +
460 + public function getReCaptchaSettings()
461 + {
462 + $reCaptchaSettingsData = Meta::where('object_type', '_fs_recaptcha_settings')->first();
463 + if ($reCaptchaSettingsData) {
464 + $settings = Helper::safeUnserialize($reCaptchaSettingsData->value);
465 + return $this->sendSuccess($settings);
466 + }
467 +
468 + return [];
469 + }
470 +
260 471 private function shareEmail($optinEmail)
261 472 {
262 473 $user = get_user_by('ID', get_current_user_id());
263 474 $data = [
@@ -285,11 +496,9 @@
285 496 ];
286 497
287 498 $url = add_query_arg($data, 'https://wpmanageninja.com/');
288 499
289 - wp_remote_post($url, [
290 - 'sslverify' => false
291 - ]);
500 + wp_remote_post($url);
292 501 }
293 502
294 503 /**
295 504 * installFluentCRM method will install Fluent CRM plugin
@@ -306,9 +515,9 @@
306 515 }
307 516
308 517 if (!current_user_can('install_plugins')) {
309 518 return $this->sendError([
310 - 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-crm')
519 + 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-support')
311 520 ]);
312 521 }
313 522
314 523 $plugin_id = 'fluent-crm';
@@ -343,22 +552,22 @@
343 552 }
344 553
345 554 if (!current_user_can('install_plugins')) {
346 555 return $this->sendError([
347 - 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-crm')
556 + 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-support')
348 557 ]);
349 558 }
350 559
351 - $plugin_id = 'fluent-crm';
560 + $plugin_id = 'fluentform';
352 561 $plugin = [
353 - 'name' => 'Fluent CRM',
354 - 'repo-slug' => 'fluent-crm',
355 - 'file' => 'fluent-crm.php',
562 + 'name' => 'Fluent Forms',
563 + 'repo-slug' => 'fluentform',
564 + 'file' => 'fluentform.php',
356 565 ];
357 566
358 567 $this->backgroundInstaller($plugin, $plugin_id);
359 568
360 - if (defined('FLUENTCRM')) {
569 + if (defined('FLUENTFORM')) {
361 570 return [
362 571 'is_installed' => true,
363 572 'message' => __('Fluent Forms plugin has been installed and activated successfully', 'fluent-support')
364 573 ];
@@ -487,7 +696,276 @@
487 696 $path = explode('/', $key);
488 697 $filename = end($path);
489 698 $plugins[$filename] = $key;
490 699 return $plugins;
700 + }
701 +
702 + public function getRemoteUploadSettings(Request $request)
703 + {
704 + $dropBoxConfigured = false;
705 + $googleDriveConfigured = false;
706 + $cloudflareR2Configured = false;
707 + $amazonS3Configured = false;
708 +
709 + if (defined('FLUENTSUPPORTPRO')) {
710 + $dropBoxSettings = Helper::getIntegrationOption('dropbox_settings');
711 + $dropBoxConfigured = $dropBoxSettings && !empty($dropBoxSettings['access_token']);
712 +
713 + $googleDriveSettings = Helper::getIntegrationOption('google_drive_settings');
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';
721 + }
722 +
723 + $drivers = apply_filters('fluent_support/storage_drivers_info', [
724 + 'local' => [
725 + 'title' => 'Default WordPress Storage',
726 + 'is_disabled' => false,
727 + 'is_configured' => true,
728 + 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/folder.svg',
729 + 'description' => __('Upload and store the files to your WordPress File System Storage.', 'fluent-support')
730 + ],
731 + 'dropbox' => [
732 + 'meta_key' => 'dropbox_settings',
733 + 'title' => 'Dropbox',
734 + 'has_config' => true,
735 + 'is_configured' => $dropBoxConfigured,
736 + 'require_pro' => !defined('FLUENTSUPPORTPRO'),
737 + 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_dropbox'),
738 + 'icon' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/icons/dbox.svg',
739 + 'description' => __('Upload and store the files to your Dropbox Storage.', 'fluent-support')
740 + ],
741 + 'google_drive' => [
742 + 'meta_key' => 'google_drive_settings',
743 + 'title' => 'Google Drive',
744 + 'has_config' => true,
745 + 'is_configured' => $googleDriveConfigured,
746 + 'require_pro' => !defined('FLUENTSUPPORTPRO'),
747 + 'upgrade_url' => Helper::getUpgradeUrl('feature_lock_google_drive'),
748 + 'description' => __('Upload and store the files to your Google Drive Storage.', 'fluent-support'),
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',
770 + ]
771 + ]);
772 +
773 + return [
774 + 'drivers' => $drivers,
775 + 'enabled_driver' => Helper::getUploadDriverKey()
776 + ];
777 + }
778 +
779 + public function updateRemoteUploadDriver(Request $request)
780 + {
781 + $driver = $request->getSafe('driver', 'sanitize_text_field');
782 + Helper::updateOption('file_upload_driver', $driver);
783 +
784 + return [
785 + 'message' => 'Upload driver has been updated successfully',
786 + 'driver' => $driver
787 + ];
788 + }
789 +
790 + /**
791 + * getIntegrationLogs method will return the integration logs
792 + * @return array
793 + */
794 + public function integrationStatuses()
795 + {
796 + return [
797 + 'connections' => Helper::getIntegrationStatuses()
798 + ];
799 + }
800 +
801 + public function getSettingsMenu()
802 + {
803 + return Helper::getGlobalSettingsMenu();
804 + }
805 +
806 + public function getFluentBotSettings()
807 + {
808 + $meta = Meta::where([
809 + 'object_type' => 'fluent_bot_settings',
810 + 'object_id' => 1,
811 + 'key' => '_fs_fluent_bot_config'
812 + ])->orderByDesc('id')->first();
813 +
814 + $settings = $meta ? Helper::safeUnserialize($meta->value) : [];
815 +
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) {
839 + return [
840 + 'id' => $product->id,
841 + 'title' => $product->title
842 + ];
843 + })->values()->all();
844 +
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,
859 + ]);
860 + }
861 +
862 + public function saveFluentBotSettings(Request $request)
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 +
881 + $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'),
886 + 'productMappings' => []
887 + ];
888 +
889 + $productMappings = (array) $request->get('productMappings', []);
890 + $seenProductIds = [];
891 +
892 + foreach ($productMappings as $mapping) {
893 + if (!is_array($mapping)) {
894 + continue;
895 + }
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 +
912 + $data['productMappings'][] = [
913 + 'productId' => $productId,
914 + 'productTitle' => sanitize_text_field($mapping['productTitle'] ?? ''),
915 + 'botId' => $botId,
916 + ];
917 + }
918 +
919 + $serialized = maybe_serialize($data);
920 +
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.
925 + $existing->update(['value' => $serialized]);
926 + } else {
927 + Meta::create(array_merge($where, ['value' => $serialized]));
928 + AIActivityLogsMigrator::migrate();
929 + }
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 +
936 + return [
937 + 'success' => true,
938 + '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
968 + ];
491 969 }
492 970
493 971 }