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 +875 -15 1.4.02.4.0 View file →
@@ -4,28 +4,50 @@
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
17 +/**
18 + * SettingsController class is responsible for all settings
19 + * This class is responsible for all request related to settings under global settings tab
20 + * @package FluentSupport\App\Http\Controllers
21 + *
22 + * @version 1.0.0
23 + */
12 24 class SettingsController extends Controller
13 25 {
26 + /**
27 + * getSettings method will return the settings by settings key
28 + * @param Request $request
29 + * @return array|array[]
30 + */
14 31 public function getSettings(Request $request)
15 32 {
16 - $settingsKey = $request->get('settings_key');
33 + $settingsKey = $request->getSafe('settings_key', 'sanitize_text_field');
17 34
18 35 return (new Settings)->get($settingsKey);
19 36 }
20 37
38 + /**
39 + * getIntegrationSettings method will return the settings for integration
40 + * @param Request $request
41 + * @return array
42 + */
21 43 public function getIntegrationSettings(Request $request)
22 44 {
23 45 $settings = Meta::where('object_type', 'integration_settings')->get();
24 46 $integrationSettings = [];
25 47 foreach ($settings as $index => $setting) {
26 - $data = maybe_unserialize($setting->value);
27 - if(!empty($data['status']) && $data && $data['status'] == 'yes') {
48 + $data = Helper::safeUnserialize($setting->value);
49 + if (!empty($data['status']) && $data && $data['status'] == 'yes') {
28 50 $integrationSettings[] = $setting->key;
29 51 }
30 52 }
31 53 return $integrationSettings;
@@ -30,13 +52,36 @@
30 52 }
31 53 return $integrationSettings;
32 54 }
33 55
56 + /**
57 + * saveSettings method will save the requested settings data by setting key
58 + * @param Request $request
59 + * @return array
60 + */
34 61 public function saveSettings(Request $request)
35 62 {
36 - $settingsKey = $request->get('settings_key');
37 - $settings = wp_unslash($request->get('settings'));
63 + $settingsKey = $request->getSafe('settings_key', 'sanitize_text_field');
64 + $settings = wp_unslash($request->get('settings', null));
38 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 +
39 84 (new Settings)->save($settingsKey, $settings);
40 85
41 86 return [
42 87 'message' => __('Settings has been updated', 'fluent-support')
@@ -42,8 +87,12 @@
42 87 'message' => __('Settings has been updated', 'fluent-support')
43 88 ];
44 89 }
45 90
91 + /**
92 + * getPages method will return the list of pages created in WP
93 + * @return array
94 + */
46 95 public function getPages()
47 96 {
48 97 return [
49 98 'pages' => Helper::getWPPages()
@@ -49,11 +98,24 @@
49 98 'pages' => Helper::getWPPages()
50 99 ];
51 100 }
52 101
102 + /**
103 + * setupPortal method will setup the support portal
104 + * @param Request $request
105 + * @return array
106 + * @throws \FluentSupport\Framework\Validator\ValidationException
107 + */
53 108 public function setupPortal(Request $request)
54 109 {
55 - $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 +
56 118 $this->validate($mailbox, [
57 119 'name' => 'required',
58 120 'email' => 'required|email',
59 121 'box_type' => 'required'
@@ -58,20 +120,22 @@
58 120 'email' => 'required|email',
59 121 'box_type' => 'required'
60 122 ]);
61 123
62 - $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 + ] : [];
63 129
64 130 $createPage = $settings['create_portal_page'] == 'yes';
65 131
66 132 if (!$createPage && empty($settings['portal_page_id'])) {
67 - return $this->sendError([
68 - 'message' => __('Please select a page or enable create page', 'fluent-support')
69 - ]);
133 + Helper::getSafeErrorMessage(new \Exception(__('Please select a page or enable create page', 'fluent-support')));
70 134 }
71 135
72 136 if ($createPage) {
73 - // we have to created the page
137 + // we have to create the page
74 138 $page_id = wp_insert_post(
75 139 array(
76 140 'comment_status' => 'close',
77 141 'ping_status' => 'close',
@@ -77,9 +141,9 @@
77 141 'ping_status' => 'close',
78 142 'post_author' => get_current_user_id(),
79 143 'post_title' => __('Support Portal', 'fluent-support'),
80 144 'post_status' => 'publish',
81 - 'post_content' => '[fluent_support_portal]',
145 + 'post_content' => '<!-- wp:shortcode -->[fluent_support_portal]<!-- /wp:shortcode -->',
82 146 'post_type' => 'page'
83 147 )
84 148 );
85 149 } else {
@@ -100,12 +164,808 @@
100 164 $globalSettings['portal_page_id'] = $page_id;
101 165
102 166 $settingsClass->save('global_business_settings', $globalSettings);
103 167
168 +
169 + if (defined('WC_PLUGIN_FILE')) {
170 + // URL Flash
171 + flush_rewrite_rules(false);
172 + }
173 +
104 174 return [
105 - 'mailbox' => $newMailBox,
175 + 'mailbox' => $newMailBox,
106 176 'global_settings' => $globalSettings,
107 - 'mailboxes' => MailBox::select(['id', 'name', 'settings'])->get()
177 + 'mailboxes' => MailBox::select(['id', 'name', 'settings'])->get(),
178 + 'has_fluentform' => defined('FLUENTFORM')
108 179 ];
109 180
110 181 }
182 +
183 + /**
184 + * getFluentCRMSettings method will return the settings for Fluent CRM
185 + * @param Request $request
186 + * @return array
187 + */
188 + public function getFluentCRMSettings(Request $request)
189 + {
190 + if (defined('FLUENTCRM')) {
191 + $settingDefault = [
192 + 'enabled' => 'no',
193 + 'default_status' => 'subscribed',
194 + 'assigned_list' => '',
195 + 'assigned_tags' => []
196 + ];
197 +
198 + $settings = Helper::getOption('_fluentcrm_intergration_settings');
199 +
200 + $settings = wp_parse_args($settings, $settingDefault);
201 +
202 + $settingsFields = [
203 + 'enabled' => [
204 + 'type' => 'inline-checkbox',
205 + 'true_label' => 'yes',
206 + 'false_label' => 'no',
207 + 'checkbox_label' => __('Enable FluentCRM Integration', 'fluent-support')
208 + ],
209 + 'default_status' => [
210 + 'type' => 'input-radio',
211 + 'label' => __('Default status for new contacts', 'fluent-support'),
212 + 'options' => [
213 + [
214 + 'id' => 'subscribed',
215 + 'label' => __('Subscribed', 'fluent-support')
216 + ],
217 + [
218 + 'id' => 'pending',
219 + 'label' => __('Pending', 'fluent-support')
220 + ]
221 + ],
222 + 'dependency' => [
223 + 'depends_on' => 'enabled',
224 + 'operator' => '=',
225 + 'value' => 'yes'
226 + ],
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')
228 + ],
229 + 'assigned_list' => [
230 + 'type' => 'input-options',
231 + 'label' => __('Add to FluentCRM list (optional)', 'fluent-support'),
232 + 'options' => \FluentCrm\App\Models\Lists::select(['id', 'title'])->orderBy('title', 'ASC')->get(),
233 + 'dependency' => [
234 + 'depends_on' => 'enabled',
235 + 'operator' => '=',
236 + 'value' => 'yes'
237 + ],
238 + ],
239 + 'assigned_tags' => [
240 + 'type' => 'input-options',
241 + 'multiple' => true,
242 + 'label' => __('Add to Tags', 'fluent-support'),
243 + 'options' => \FluentCrm\App\Models\Tag::select(['id', 'title'])->orderBy('title', 'ASC')->get(),
244 + 'dependency' => [
245 + 'depends_on' => 'enabled',
246 + 'operator' => '=',
247 + 'value' => 'yes'
248 + ]
249 + ]
250 + ];
251 +
252 + return [
253 + 'is_installed' => true,
254 + 'settings' => $settings,
255 + 'settings_fields' => $settingsFields,
256 + 'fluentcrm_logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluentcrm-logo.svg'
257 + ];
258 + }
259 +
260 + return [
261 + 'is_installed' => false,
262 + 'fluentcrm_logo' => FLUENT_SUPPORT_PLUGIN_URL . 'assets/images/fluentcrm-logo.svg'
263 + ];
264 +
265 + }
266 +
267 + public function setupInstallation(Request $request)
268 + {
269 + $installFluentForm = $request->getSafe('install_fluentform', 'sanitize_text_field', 'no');
270 +
271 + if ($installFluentForm == 'yes' && !defined('FLUENTFORM')) {
272 + $this->installFluentForm();
273 + }
274 +
275 + $optinEmail = $request->getSafe('optin_email', 'sanitize_email', '');
276 + if ($optinEmail && is_email($optinEmail)) {
277 + $this->shareEmail($optinEmail);
278 + }
279 +
280 + $shareEssential = $request->getSafe('share_essentials', 'sanitize_text_field', 'no');
281 + if ($shareEssential == 'yes') {
282 + Helper::updateOption('_share_essential', $shareEssential);
283 + }
284 +
285 + return $this->sendSuccess([
286 + 'message' => __('Installation has been completed', 'fluent-support')
287 + ]);
288 +
289 + }
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 +
471 + private function shareEmail($optinEmail)
472 + {
473 + $user = get_user_by('ID', get_current_user_id());
474 + $data = [
475 + 'answers' => [
476 + 'website' => site_url(),
477 + 'email' => $optinEmail,
478 + 'first_name' => $user->first_name,
479 + 'last_name' => $user->last_name,
480 + 'name' => $user->display_name,
481 + 'has_fluentform' => defined('FLUENTFORM') ? 'yes' : 'no'
482 + ],
483 + 'questions' => [
484 + 'website' => 'website',
485 + 'first_name' => 'first_name',
486 + 'last_name' => 'last_name',
487 + 'email' => 'email',
488 + 'name' => 'name',
489 + 'has_fluentform' => 'has_fluentform'
490 + ],
491 + 'user' => [
492 + 'email' => $optinEmail
493 + ],
494 + 'fb_capture' => 1,
495 + 'form_id' => 77
496 + ];
497 +
498 + $url = add_query_arg($data, 'https://wpmanageninja.com/');
499 +
500 + wp_remote_post($url);
501 + }
502 +
503 + /**
504 + * installFluentCRM method will install Fluent CRM plugin
505 + * @return array
506 + */
507 + public function installFluentCRM()
508 + {
509 +
510 + if (defined('FLUENTCRM')) {
511 + return [
512 + 'is_installed' => true,
513 + 'message' => __('FluentCRM plugin has been installed and activated successfully', 'fluent-support')
514 + ];
515 + }
516 +
517 + if (!current_user_can('install_plugins')) {
518 + return $this->sendError([
519 + 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-support')
520 + ]);
521 + }
522 +
523 + $plugin_id = 'fluent-crm';
524 + $plugin = [
525 + 'name' => 'Fluent CRM',
526 + 'repo-slug' => 'fluent-crm',
527 + 'file' => 'fluent-crm.php',
528 + ];
529 +
530 + $this->backgroundInstaller($plugin, $plugin_id);
531 +
532 + if (defined('FLUENTCRM')) {
533 + return [
534 + 'is_installed' => true,
535 + 'message' => __('FluentCRM plugin has been installed and activated successfully', 'fluent-support')
536 + ];
537 + } else {
538 + return $this->sendError([
539 + 'message' => __('Sorry! FluentCRM could not be installed. Please install manually', 'fluent-support')
540 + ]);
541 + }
542 + }
543 +
544 + public function installFluentForm()
545 + {
546 +
547 + if (defined('FLUENTFORM')) {
548 + return [
549 + 'is_installed' => true,
550 + 'message' => __('Fluent Forms plugin has been installed and activated successfully', 'fluent-support')
551 + ];
552 + }
553 +
554 + if (!current_user_can('install_plugins')) {
555 + return $this->sendError([
556 + 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-support')
557 + ]);
558 + }
559 +
560 + $plugin_id = 'fluentform';
561 + $plugin = [
562 + 'name' => 'Fluent Forms',
563 + 'repo-slug' => 'fluentform',
564 + 'file' => 'fluentform.php',
565 + ];
566 +
567 + $this->backgroundInstaller($plugin, $plugin_id);
568 +
569 + if (defined('FLUENTFORM')) {
570 + return [
571 + 'is_installed' => true,
572 + 'message' => __('Fluent Forms plugin has been installed and activated successfully', 'fluent-support')
573 + ];
574 + } else {
575 + return [
576 + 'is_installed' => false,
577 + 'message' => __('Fluent Forms could not be installed', 'fluent-support')
578 + ];
579 + }
580 + }
581 +
582 + private function backgroundInstaller($plugin_to_install, $plugin_id)
583 + {
584 + if (!empty($plugin_to_install['repo-slug'])) {
585 + require_once ABSPATH . 'wp-admin/includes/file.php';
586 + require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
587 + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
588 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
589 +
590 + WP_Filesystem();
591 +
592 + $skin = new \Automatic_Upgrader_Skin();
593 + $upgrader = new \WP_Upgrader($skin);
594 + $installed_plugins = array_reduce(array_keys(\get_plugins()), array($this, 'associate_plugin_file'), array());
595 + $plugin_slug = $plugin_to_install['repo-slug'];
596 + $plugin_file = isset($plugin_to_install['file']) ? $plugin_to_install['file'] : $plugin_slug . '.php';
597 + $installed = false;
598 + $activate = false;
599 +
600 + // See if the plugin is installed already.
601 + if (isset($installed_plugins[$plugin_file])) {
602 + $installed = true;
603 + $activate = !is_plugin_active($installed_plugins[$plugin_file]);
604 + }
605 +
606 + // Install this thing!
607 + if (!$installed) {
608 + // Suppress feedback.
609 + ob_start();
610 +
611 + try {
612 + $plugin_information = plugins_api(
613 + 'plugin_information',
614 + array(
615 + 'slug' => $plugin_slug,
616 + 'fields' => array(
617 + 'short_description' => false,
618 + 'sections' => false,
619 + 'requires' => false,
620 + 'rating' => false,
621 + 'ratings' => false,
622 + 'downloaded' => false,
623 + 'last_updated' => false,
624 + 'added' => false,
625 + 'tags' => false,
626 + 'homepage' => false,
627 + 'donate_link' => false,
628 + 'author_profile' => false,
629 + 'author' => false,
630 + ),
631 + )
632 + );
633 +
634 + if (is_wp_error($plugin_information)) {
635 + throw new \Exception($plugin_information->get_error_message());
636 + }
637 +
638 + $package = $plugin_information->download_link;
639 + $download = $upgrader->download_package($package);
640 +
641 + if (is_wp_error($download)) {
642 + throw new \Exception($download->get_error_message());
643 + }
644 +
645 + $working_dir = $upgrader->unpack_package($download, true);
646 +
647 + if (is_wp_error($working_dir)) {
648 + throw new \Exception($working_dir->get_error_message());
649 + }
650 +
651 + $result = $upgrader->install_package(
652 + array(
653 + 'source' => $working_dir,
654 + 'destination' => WP_PLUGIN_DIR,
655 + 'clear_destination' => false,
656 + 'abort_if_destination_exists' => false,
657 + 'clear_working' => true,
658 + 'hook_extra' => array(
659 + 'type' => 'plugin',
660 + 'action' => 'install',
661 + ),
662 + )
663 + );
664 +
665 + if (is_wp_error($result)) {
666 + throw new \Exception($result->get_error_message());
667 + }
668 +
669 + $activate = true;
670 +
671 + } catch (\Exception $e) {
672 + }
673 +
674 + // Discard feedback.
675 + ob_end_clean();
676 + }
677 +
678 + wp_clean_plugins_cache();
679 +
680 + // Activate this thing.
681 + if ($activate) {
682 + try {
683 + $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
684 +
685 + if (is_wp_error($result)) {
686 + throw new \Exception($result->get_error_message());
687 + }
688 + } catch (\Exception $e) {
689 + }
690 + }
691 + }
692 + }
693 +
694 + private function associate_plugin_file($plugins, $key)
695 + {
696 + $path = explode('/', $key);
697 + $filename = end($path);
698 + $plugins[$filename] = $key;
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 + ];
969 + }
970 +
111 971 }