PluginProbe ʕ •ᴥ•ʔ
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.6
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.6
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8 0.0.9 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.0 1.1.1 1.1.2 1.10.0 1.10.1 1.11.0 1.12.0 1.12.1 1.12.2 1.12.3 1.13.0 1.13.1 1.13.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.8.0 1.9.0 1.9.1 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.0 2.5.2 2.6.0
sureforms / inc / abilities / settings / update-global-settings.php
sureforms / inc / abilities / settings Last commit date
get-global-settings.php 3 days ago settings-secret-keys.php 5 months ago update-global-settings.php 3 days ago
update-global-settings.php
402 lines
1 <?php
2 /**
3 * Update Global Settings Ability.
4 *
5 * @package sureforms
6 * @since 2.6.0
7 */
8
9 namespace SRFM\Inc\Abilities\Settings;
10
11 use SRFM\Inc\Abilities\Abstract_Ability;
12 use SRFM\Inc\Global_Settings\Global_Settings;
13 use SRFM\Inc\Helper;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 /**
20 * Update_Global_Settings ability class.
21 *
22 * Updates SureForms global settings by category.
23 *
24 * @since 2.6.0
25 */
26 class Update_Global_Settings extends Abstract_Ability {
27 use Settings_Secret_Keys;
28
29 /**
30 * Sentinel value used to represent masked secret keys.
31 *
32 * @since 2.6.0
33 */
34 private const SECRET_SENTINEL = '@@SRFM_SECRET_UNCHANGED@@';
35
36 /**
37 * Allowed setting keys per category.
38 *
39 * Only keys in this whitelist will be passed to the save functions.
40 * This prevents arbitrary option injection via crafted key names.
41 *
42 * @since 2.6.0
43 */
44 private const ALLOWED_KEYS = [
45 'general' => [
46 'srfm_ip_log',
47 'srfm_form_analytics',
48 'srfm_bsf_analytics',
49 'srfm_admin_notification',
50 'srfm_form_views_tracking',
51 'srfm_enable_logs',
52 ],
53 'validation-messages' => [
54 'srfm_url_block_required_text',
55 'srfm_input_block_required_text',
56 'srfm_input_block_unique_text',
57 'srfm_address_block_required_text',
58 'srfm_phone_block_required_text',
59 'srfm_phone_block_unique_text',
60 'srfm_number_block_required_text',
61 'srfm_textarea_block_required_text',
62 'srfm_multi_choice_block_required_text',
63 'srfm_checkbox_block_required_text',
64 'srfm_gdpr_block_required_text',
65 'srfm_email_block_required_text',
66 'srfm_email_block_unique_text',
67 'srfm_dropdown_block_required_text',
68 'srfm_valid_phone_number',
69 'srfm_valid_url',
70 'srfm_confirm_email_same',
71 'srfm_valid_email',
72 'srfm_textarea_min_chars',
73 'srfm_email_local_max_length',
74 'srfm_email_domain_max_length',
75 'srfm_input_min_value',
76 'srfm_input_max_value',
77 'srfm_dropdown_min_selections',
78 'srfm_dropdown_max_selections',
79 'srfm_multi_choice_min_selections',
80 'srfm_multi_choice_max_selections',
81 ],
82 'email-summary' => [
83 'srfm_email_summary',
84 'srfm_email_sent_to',
85 'srfm_schedule_report',
86 ],
87 'security' => [
88 'srfm_v2_checkbox_site_key',
89 'srfm_v2_checkbox_secret_key',
90 'srfm_v2_invisible_site_key',
91 'srfm_v2_invisible_secret_key',
92 'srfm_v3_site_key',
93 'srfm_v3_secret_key',
94 'srfm_cf_appearance_mode',
95 'srfm_cf_turnstile_site_key',
96 'srfm_cf_turnstile_secret_key',
97 'srfm_hcaptcha_site_key',
98 'srfm_hcaptcha_secret_key',
99 'srfm_honeypot',
100 ],
101 ];
102
103 /**
104 * Constructor.
105 *
106 * @since 2.6.0
107 */
108 public function __construct() {
109 $this->id = 'sureforms/update-global-settings';
110 $this->label = __( 'Update Global Settings', 'sureforms' );
111 $this->description = __( 'Update SureForms global settings for a specific category: general, validation-messages, email-summary, or security.', 'sureforms' );
112 $this->capability = 'manage_options';
113 $this->gated = 'srfm_abilities_api_edit';
114 }
115
116 /**
117 * {@inheritDoc}
118 *
119 * @since 2.6.0
120 */
121 public function get_annotations() {
122 return [
123 'readonly' => false,
124 'destructive' => true,
125 'idempotent' => true,
126 'priority' => 2.0,
127 'openWorldHint' => false,
128 'instructions' => 'Confirm the settings category and the specific keys being changed with the user before executing. Security keys are sensitive — never display real secret values.',
129 ];
130 }
131
132 /**
133 * {@inheritDoc}
134 *
135 * @since 2.6.0
136 */
137 public function get_input_schema() {
138 return [
139 'type' => 'object',
140 'additionalProperties' => false,
141 'properties' => [
142 'category' => [
143 'type' => 'string',
144 'description' => __( 'The settings category to update.', 'sureforms' ),
145 'enum' => [ 'general', 'validation-messages', 'email-summary', 'security' ],
146 ],
147 'settings' => [
148 'type' => 'object',
149 'description' => __( 'Key-value pairs of settings to update.', 'sureforms' ),
150 ],
151 ],
152 'required' => [ 'category', 'settings' ],
153 ];
154 }
155
156 /**
157 * {@inheritDoc}
158 *
159 * @since 2.6.0
160 */
161 public function get_output_schema() {
162 return [
163 'type' => 'object',
164 'properties' => [
165 'saved' => [ 'type' => 'boolean' ],
166 'category' => [ 'type' => 'string' ],
167 ],
168 ];
169 }
170
171 /**
172 * Execute the update-global-settings ability.
173 *
174 * @param array<string,mixed> $input Validated input data.
175 * @since 2.6.0
176 * @return array<string,mixed>|\WP_Error
177 */
178 public function execute( $input ) {
179 $category = sanitize_text_field( Helper::get_string_value( $input['category'] ?? '' ) );
180 $settings = $input['settings'] ?? [];
181
182 if ( empty( $category ) ) {
183 return new \WP_Error(
184 'srfm_missing_category',
185 __( 'Settings category is required.', 'sureforms' ),
186 [ 'status' => 400 ]
187 );
188 }
189
190 if ( empty( $settings ) || ! is_array( $settings ) ) {
191 return new \WP_Error(
192 'srfm_missing_settings',
193 __( 'Settings data is required.', 'sureforms' ),
194 [ 'status' => 400 ]
195 );
196 }
197
198 // Filter to allowed keys only — prevents arbitrary option injection.
199 $settings = $this->filter_allowed_keys( $category, $settings );
200
201 if ( empty( $settings ) ) {
202 return new \WP_Error(
203 'srfm_no_valid_keys',
204 __( 'No valid settings keys provided for this category.', 'sureforms' ),
205 [ 'status' => 400 ]
206 );
207 }
208
209 // Sanitize settings per category before saving.
210 $settings = $this->sanitize_settings( $category, $settings );
211
212 $saved = false;
213
214 switch ( $category ) {
215 case 'general':
216 $saved = Global_Settings::srfm_save_general_settings( $settings );
217 break;
218 case 'validation-messages':
219 $saved = Global_Settings::srfm_save_general_settings_dynamic_opt( $settings );
220 break;
221 case 'email-summary':
222 $saved = Global_Settings::srfm_save_email_summary_settings( $settings );
223 break;
224 case 'security':
225 $saved = $this->save_security_settings( $settings );
226 break;
227 default:
228 return new \WP_Error(
229 'srfm_invalid_category',
230 __( 'Invalid settings category.', 'sureforms' ),
231 [ 'status' => 400 ]
232 );
233 }
234
235 return [
236 'saved' => (bool) $saved,
237 'category' => $category,
238 ];
239 }
240
241 /**
242 * Filter settings to only include allowed keys for the given category.
243 *
244 * @param string $category Settings category.
245 * @param array<string,mixed> $settings Raw settings values.
246 * @since 2.6.0
247 * @return array<string,mixed> Filtered settings containing only whitelisted keys.
248 */
249 private function filter_allowed_keys( $category, $settings ) {
250 if ( ! isset( self::ALLOWED_KEYS[ $category ] ) ) {
251 return [];
252 }
253
254 return array_intersect_key( $settings, array_flip( self::ALLOWED_KEYS[ $category ] ) );
255 }
256
257 /**
258 * Sanitize settings values based on category and known key types.
259 *
260 * @param string $category Settings category.
261 * @param array<string,mixed> $settings Raw settings values.
262 * @since 2.6.0
263 * @return array<string,mixed> Sanitized settings.
264 */
265 private function sanitize_settings( $category, $settings ) {
266 switch ( $category ) {
267 case 'general':
268 return $this->sanitize_general_settings( $settings );
269 case 'email-summary':
270 return $this->sanitize_email_summary_settings( $settings );
271 case 'security':
272 return $this->sanitize_security_settings( $settings );
273 case 'validation-messages':
274 return $this->sanitize_validation_messages( $settings );
275 default:
276 return $settings;
277 }
278 }
279
280 /**
281 * Sanitize general settings.
282 *
283 * @param array<string,mixed> $settings Raw settings.
284 * @since 2.6.0
285 * @return array<string,mixed>
286 */
287 private function sanitize_general_settings( $settings ) {
288 $boolean_keys = [ 'srfm_ip_log', 'srfm_form_analytics', 'srfm_bsf_analytics', 'srfm_admin_notification', 'srfm_form_views_tracking', 'srfm_enable_logs' ];
289
290 foreach ( $boolean_keys as $key ) {
291 if ( isset( $settings[ $key ] ) ) {
292 $settings[ $key ] = rest_sanitize_boolean( Helper::get_string_value( $settings[ $key ] ) );
293 }
294 }
295
296 return $settings;
297 }
298
299 /**
300 * Sanitize email summary settings.
301 *
302 * @param array<string,mixed> $settings Raw settings.
303 * @since 2.6.0
304 * @return array<string,mixed>
305 */
306 private function sanitize_email_summary_settings( $settings ) {
307 if ( isset( $settings['srfm_email_summary'] ) ) {
308 $settings['srfm_email_summary'] = rest_sanitize_boolean( Helper::get_string_value( $settings['srfm_email_summary'] ) );
309 }
310
311 if ( isset( $settings['srfm_email_sent_to'] ) ) {
312 $settings['srfm_email_sent_to'] = sanitize_email( Helper::get_string_value( $settings['srfm_email_sent_to'] ) );
313 }
314
315 if ( isset( $settings['srfm_schedule_report'] ) ) {
316 $settings['srfm_schedule_report'] = sanitize_text_field( Helper::get_string_value( $settings['srfm_schedule_report'] ) );
317 }
318
319 return $settings;
320 }
321
322 /**
323 * Sanitize security settings (applied before sentinel check).
324 *
325 * @param array<string,mixed> $settings Raw settings.
326 * @since 2.6.0
327 * @return array<string,mixed>
328 */
329 private function sanitize_security_settings( $settings ) {
330 // Sanitize all site key and secret key values as text fields.
331 $text_keys = [
332 'srfm_v2_checkbox_site_key',
333 'srfm_v2_checkbox_secret_key',
334 'srfm_v2_invisible_site_key',
335 'srfm_v2_invisible_secret_key',
336 'srfm_v3_site_key',
337 'srfm_v3_secret_key',
338 'srfm_cf_turnstile_site_key',
339 'srfm_cf_turnstile_secret_key',
340 'srfm_hcaptcha_site_key',
341 'srfm_hcaptcha_secret_key',
342 'srfm_cf_appearance_mode',
343 ];
344
345 foreach ( $text_keys as $key ) {
346 if ( isset( $settings[ $key ] ) ) {
347 $settings[ $key ] = sanitize_text_field( Helper::get_string_value( $settings[ $key ] ) );
348 }
349 }
350
351 if ( isset( $settings['srfm_honeypot'] ) ) {
352 $settings['srfm_honeypot'] = rest_sanitize_boolean( Helper::get_string_value( $settings['srfm_honeypot'] ) );
353 }
354
355 return $settings;
356 }
357
358 /**
359 * Sanitize validation message settings.
360 *
361 * @param array<string,mixed> $settings Raw settings.
362 * @since 2.6.0
363 * @return array<string,mixed>
364 */
365 private function sanitize_validation_messages( $settings ) {
366 foreach ( $settings as $key => $value ) {
367 if ( is_string( $value ) ) {
368 $settings[ $key ] = sanitize_text_field( $value );
369 }
370 }
371
372 return $settings;
373 }
374
375 /**
376 * Save security settings, preserving masked sentinel values.
377 *
378 * When the caller sends the SECRET_SENTINEL for a secret key, the stored
379 * value is preserved instead of being overwritten with the sentinel.
380 *
381 * @param array<string,mixed> $settings Settings to save.
382 * @since 2.6.0
383 * @return bool
384 */
385 private function save_security_settings( $settings ) {
386 $existing = get_option( 'srfm_security_settings_options', [] );
387
388 if ( ! is_array( $existing ) ) {
389 $existing = [];
390 }
391
392 // Replace masked sentinel values with stored values.
393 foreach ( self::$secret_keys as $key ) {
394 if ( isset( $settings[ $key ] ) && self::SECRET_SENTINEL === $settings[ $key ] && isset( $existing[ $key ] ) ) {
395 $settings[ $key ] = $existing[ $key ];
396 }
397 }
398
399 return Global_Settings::srfm_save_security_settings( $settings );
400 }
401 }
402