PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Services / Integrations / GlobalIntegrationService.php

GlobalIntegrationService.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.14, at app/Services/Integrations/GlobalIntegrationService.php

160 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Services\Integrations;
4
5 use Exception;
6 use FluentForm\App\Modules\AddOnModule;
7 use FluentForm\Framework\Support\Arr;
8 class GlobalIntegrationService
9 {
10 // Sentinel written over connected credential values on read (FINDING-16) and
11 // detected again on save so a re-posted mask never overwrites a live secret.
12 const REDACTION_MASK = '********';
13
14 public function get($attr)
15 {
16 $settingsKey = sanitize_text_field(Arr::get($attr, 'settings_key'));
17 $settings = apply_filters_deprecated(
18 'fluentform_global_integration_settings_' . $settingsKey,
19 [
20 []
21 ],
22 FLUENTFORM_FRAMEWORK_UPGRADE,
23 'fluentform/global_integration_settings_' . $settingsKey,
24 'Use fluentform/global_integration_settings_' . $settingsKey . ' instead of fluentform_global_integration_settings_' . $settingsKey
25 );
26 $settings = apply_filters('fluentform/global_integration_settings_' . $settingsKey, $settings);
27 $fieldSettings = apply_filters_deprecated(
28 'fluentform_global_integration_fields_' . $settingsKey,
29 [
30 []
31 ],
32 FLUENTFORM_FRAMEWORK_UPGRADE,
33 'fluentform/global_integration_fields_' . $settingsKey,
34 'Use fluentform/global_integration_fields_' . $settingsKey . ' instead of fluentform_global_integration_fields_' . $settingsKey
35 );
36 $fieldSettings = apply_filters('fluentform/global_integration_fields_' . $settingsKey, $fieldSettings);
37 if (!$fieldSettings) {
38 $message = __('Sorry! No integration failed found with: ', 'fluentform').$settingsKey;
39 return [
40 'status' => false,
41 'message'=> $message,
42 'integration' => $settings,
43 'settings' => $fieldSettings,
44 ];
45 }
46
47 if (!Arr::exists($fieldSettings,'save_button_text')) {
48 $fieldSettings['save_button_text'] = __('Save Settings', 'fluentform');
49 }
50
51 if (!Arr::exists($fieldSettings,'valid_message')) {
52 $fieldSettings['valid_message'] = __('Your API Key is valid', 'fluentform');
53 }
54
55 if (!Arr::exists($fieldSettings,'invalid_message')) {
56 $fieldSettings['invalid_message'] = __('Your API Key is not valid', 'fluentform');
57 }
58
59 // SECURITY (FINDING-16): the stored settings carry long-lived third-party credentials
60 // (API keys, tokens). This read endpoint is reachable by delegated roles and the payload
61 // returned them verbatim — hide_on_valid only hid the field in the Vue UI, not in the REST
62 // response. When the integration is connected and its credential fields are hidden anyway
63 // (hide_on_valid), redact the declared field values so the browser never receives the
64 // secret. The save path restores any field posted back still masked (see
65 // unmaskCredentials), so a re-saved connected integration — e.g. the "Verify Connection
66 // Again" button, which re-POSTs the loaded payload — can never overwrite a live credential
67 // with the mask.
68 if (is_array($settings) && !empty($fieldSettings['hide_on_valid']) && !empty($settings['status'])) {
69 foreach (array_keys((array) Arr::get($fieldSettings, 'fields', [])) as $credentialKey) {
70 if (!empty($settings[$credentialKey]) && is_string($settings[$credentialKey])) {
71 $settings[$credentialKey] = self::REDACTION_MASK;
72 }
73 }
74 }
75
76 return [
77 'status' => true,
78 'integration' => $settings,
79 'settings' => $fieldSettings,
80 ];
81 }
82
83 /**
84 * Restore any credential the browser posted back still masked (REDACTION_MASK)
85 * to its real stored value before it is persisted. get() redacts connected
86 * credentials on read; without this, re-saving a connected integration — the
87 * "Verify Connection Again" button re-POSTs the loaded payload verbatim, and
88 * several handlers persist BEFORE their auth test — would overwrite a live key
89 * with '********'. Stored settings come from the same filter get() reads.
90 */
91 public function unmaskCredentials($settingsKey, $integration)
92 {
93 if (!is_array($integration) || !in_array(self::REDACTION_MASK, $integration, true)) {
94 return $integration;
95 }
96
97 $stored = apply_filters('fluentform/global_integration_settings_' . $settingsKey, []);
98 if (!is_array($stored)) {
99 return $integration;
100 }
101
102 foreach ($integration as $key => $value) {
103 if (self::REDACTION_MASK === $value && isset($stored[$key]) && is_string($stored[$key])) {
104 $integration[$key] = $stored[$key];
105 }
106 }
107
108 return $integration;
109 }
110
111 public function isEnabled($integrationKey)
112 {
113 $globalModules = get_option('fluentform_global_modules_status');
114 $isEnabled = $globalModules && isset($globalModules[$integrationKey]) && 'yes' == $globalModules[$integrationKey];
115
116 return apply_filters('fluentform/is_integration_enabled_'.$integrationKey, $isEnabled);
117 }
118
119 /**
120 * @param $args - key value pair array
121 * @throws Exception
122 * @return void
123 */
124 public function updateModuleStatus($args) {
125 $moduleKey = sanitize_text_field(Arr::get($args, 'module_key'));
126 $moduleStatus = sanitize_text_field(Arr::get($args, 'module_status'));
127 if (!$moduleKey || !in_array($moduleStatus, ['yes', 'no'])) {
128 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
129 throw new Exception(__('Status update failed. Not valid module or status', 'fluentform'));
130 }
131 $modules = (array)get_option('fluentform_global_modules_status');
132
133 if (!$this->isTogglableModuleKey($moduleKey, $modules)) {
134 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
135 throw new Exception(__('Status update failed. Not valid module or status', 'fluentform'));
136 }
137
138 try {
139 $modules[$moduleKey] = $moduleStatus;
140 update_option('fluentform_global_modules_status', $modules, 'no');
141 } catch (Exception $e) {
142 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
143 throw new Exception($e->getMessage());
144 }
145 }
146
147 /**
148 * Read from the live registry, never a second list here, so a newly registered add-on stays togglable.
149 * Administrators bypass it: this REST request misses add-ons that register only in wp-admin context.
150 */
151 private function isTogglableModuleKey($moduleKey, array $storedModules)
152 {
153 if (current_user_can('manage_options') || array_key_exists($moduleKey, $storedModules)) {
154 return true;
155 }
156
157 return array_key_exists($moduleKey, (array) (new AddOnModule())->getRegisteredAddOns());
158 }
159 }
160