PluginProbe ʕ •ᴥ•ʔ
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.3
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.3
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 5 months ago settings-secret-keys.php 5 months ago update-global-settings.php 1 month ago
update-global-settings.php
400 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 ],
51 'validation-messages' => [
52 'srfm_url_block_required_text',
53 'srfm_input_block_required_text',
54 'srfm_input_block_unique_text',
55 'srfm_address_block_required_text',
56 'srfm_phone_block_required_text',
57 'srfm_phone_block_unique_text',
58 'srfm_number_block_required_text',
59 'srfm_textarea_block_required_text',
60 'srfm_multi_choice_block_required_text',
61 'srfm_checkbox_block_required_text',
62 'srfm_gdpr_block_required_text',
63 'srfm_email_block_required_text',
64 'srfm_email_block_unique_text',
65 'srfm_dropdown_block_required_text',
66 'srfm_valid_phone_number',
67 'srfm_valid_url',
68 'srfm_confirm_email_same',
69 'srfm_valid_email',
70 'srfm_textarea_min_chars',
71 'srfm_email_local_max_length',
72 'srfm_email_domain_max_length',
73 'srfm_input_min_value',
74 'srfm_input_max_value',
75 'srfm_dropdown_min_selections',
76 'srfm_dropdown_max_selections',
77 'srfm_multi_choice_min_selections',
78 'srfm_multi_choice_max_selections',
79 ],
80 'email-summary' => [
81 'srfm_email_summary',
82 'srfm_email_sent_to',
83 'srfm_schedule_report',
84 ],
85 'security' => [
86 'srfm_v2_checkbox_site_key',
87 'srfm_v2_checkbox_secret_key',
88 'srfm_v2_invisible_site_key',
89 'srfm_v2_invisible_secret_key',
90 'srfm_v3_site_key',
91 'srfm_v3_secret_key',
92 'srfm_cf_appearance_mode',
93 'srfm_cf_turnstile_site_key',
94 'srfm_cf_turnstile_secret_key',
95 'srfm_hcaptcha_site_key',
96 'srfm_hcaptcha_secret_key',
97 'srfm_honeypot',
98 ],
99 ];
100
101 /**
102 * Constructor.
103 *
104 * @since 2.6.0
105 */
106 public function __construct() {
107 $this->id = 'sureforms/update-global-settings';
108 $this->label = __( 'Update Global Settings', 'sureforms' );
109 $this->description = __( 'Update SureForms global settings for a specific category: general, validation-messages, email-summary, or security.', 'sureforms' );
110 $this->capability = 'manage_options';
111 $this->gated = 'srfm_abilities_api_edit';
112 }
113
114 /**
115 * {@inheritDoc}
116 *
117 * @since 2.6.0
118 */
119 public function get_annotations() {
120 return [
121 'readonly' => false,
122 'destructive' => true,
123 'idempotent' => true,
124 'priority' => 2.0,
125 'openWorldHint' => false,
126 '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.',
127 ];
128 }
129
130 /**
131 * {@inheritDoc}
132 *
133 * @since 2.6.0
134 */
135 public function get_input_schema() {
136 return [
137 'type' => 'object',
138 'additionalProperties' => false,
139 'properties' => [
140 'category' => [
141 'type' => 'string',
142 'description' => __( 'The settings category to update.', 'sureforms' ),
143 'enum' => [ 'general', 'validation-messages', 'email-summary', 'security' ],
144 ],
145 'settings' => [
146 'type' => 'object',
147 'description' => __( 'Key-value pairs of settings to update.', 'sureforms' ),
148 ],
149 ],
150 'required' => [ 'category', 'settings' ],
151 ];
152 }
153
154 /**
155 * {@inheritDoc}
156 *
157 * @since 2.6.0
158 */
159 public function get_output_schema() {
160 return [
161 'type' => 'object',
162 'properties' => [
163 'saved' => [ 'type' => 'boolean' ],
164 'category' => [ 'type' => 'string' ],
165 ],
166 ];
167 }
168
169 /**
170 * Execute the update-global-settings ability.
171 *
172 * @param array<string,mixed> $input Validated input data.
173 * @since 2.6.0
174 * @return array<string,mixed>|\WP_Error
175 */
176 public function execute( $input ) {
177 $category = sanitize_text_field( Helper::get_string_value( $input['category'] ?? '' ) );
178 $settings = $input['settings'] ?? [];
179
180 if ( empty( $category ) ) {
181 return new \WP_Error(
182 'srfm_missing_category',
183 __( 'Settings category is required.', 'sureforms' ),
184 [ 'status' => 400 ]
185 );
186 }
187
188 if ( empty( $settings ) || ! is_array( $settings ) ) {
189 return new \WP_Error(
190 'srfm_missing_settings',
191 __( 'Settings data is required.', 'sureforms' ),
192 [ 'status' => 400 ]
193 );
194 }
195
196 // Filter to allowed keys only — prevents arbitrary option injection.
197 $settings = $this->filter_allowed_keys( $category, $settings );
198
199 if ( empty( $settings ) ) {
200 return new \WP_Error(
201 'srfm_no_valid_keys',
202 __( 'No valid settings keys provided for this category.', 'sureforms' ),
203 [ 'status' => 400 ]
204 );
205 }
206
207 // Sanitize settings per category before saving.
208 $settings = $this->sanitize_settings( $category, $settings );
209
210 $saved = false;
211
212 switch ( $category ) {
213 case 'general':
214 $saved = Global_Settings::srfm_save_general_settings( $settings );
215 break;
216 case 'validation-messages':
217 $saved = Global_Settings::srfm_save_general_settings_dynamic_opt( $settings );
218 break;
219 case 'email-summary':
220 $saved = Global_Settings::srfm_save_email_summary_settings( $settings );
221 break;
222 case 'security':
223 $saved = $this->save_security_settings( $settings );
224 break;
225 default:
226 return new \WP_Error(
227 'srfm_invalid_category',
228 __( 'Invalid settings category.', 'sureforms' ),
229 [ 'status' => 400 ]
230 );
231 }
232
233 return [
234 'saved' => (bool) $saved,
235 'category' => $category,
236 ];
237 }
238
239 /**
240 * Filter settings to only include allowed keys for the given category.
241 *
242 * @param string $category Settings category.
243 * @param array<string,mixed> $settings Raw settings values.
244 * @since 2.6.0
245 * @return array<string,mixed> Filtered settings containing only whitelisted keys.
246 */
247 private function filter_allowed_keys( $category, $settings ) {
248 if ( ! isset( self::ALLOWED_KEYS[ $category ] ) ) {
249 return [];
250 }
251
252 return array_intersect_key( $settings, array_flip( self::ALLOWED_KEYS[ $category ] ) );
253 }
254
255 /**
256 * Sanitize settings values based on category and known key types.
257 *
258 * @param string $category Settings category.
259 * @param array<string,mixed> $settings Raw settings values.
260 * @since 2.6.0
261 * @return array<string,mixed> Sanitized settings.
262 */
263 private function sanitize_settings( $category, $settings ) {
264 switch ( $category ) {
265 case 'general':
266 return $this->sanitize_general_settings( $settings );
267 case 'email-summary':
268 return $this->sanitize_email_summary_settings( $settings );
269 case 'security':
270 return $this->sanitize_security_settings( $settings );
271 case 'validation-messages':
272 return $this->sanitize_validation_messages( $settings );
273 default:
274 return $settings;
275 }
276 }
277
278 /**
279 * Sanitize general settings.
280 *
281 * @param array<string,mixed> $settings Raw settings.
282 * @since 2.6.0
283 * @return array<string,mixed>
284 */
285 private function sanitize_general_settings( $settings ) {
286 $boolean_keys = [ 'srfm_ip_log', 'srfm_form_analytics', 'srfm_bsf_analytics', 'srfm_admin_notification' ];
287
288 foreach ( $boolean_keys as $key ) {
289 if ( isset( $settings[ $key ] ) ) {
290 $settings[ $key ] = rest_sanitize_boolean( Helper::get_string_value( $settings[ $key ] ) );
291 }
292 }
293
294 return $settings;
295 }
296
297 /**
298 * Sanitize email summary settings.
299 *
300 * @param array<string,mixed> $settings Raw settings.
301 * @since 2.6.0
302 * @return array<string,mixed>
303 */
304 private function sanitize_email_summary_settings( $settings ) {
305 if ( isset( $settings['srfm_email_summary'] ) ) {
306 $settings['srfm_email_summary'] = rest_sanitize_boolean( Helper::get_string_value( $settings['srfm_email_summary'] ) );
307 }
308
309 if ( isset( $settings['srfm_email_sent_to'] ) ) {
310 $settings['srfm_email_sent_to'] = sanitize_email( Helper::get_string_value( $settings['srfm_email_sent_to'] ) );
311 }
312
313 if ( isset( $settings['srfm_schedule_report'] ) ) {
314 $settings['srfm_schedule_report'] = sanitize_text_field( Helper::get_string_value( $settings['srfm_schedule_report'] ) );
315 }
316
317 return $settings;
318 }
319
320 /**
321 * Sanitize security settings (applied before sentinel check).
322 *
323 * @param array<string,mixed> $settings Raw settings.
324 * @since 2.6.0
325 * @return array<string,mixed>
326 */
327 private function sanitize_security_settings( $settings ) {
328 // Sanitize all site key and secret key values as text fields.
329 $text_keys = [
330 'srfm_v2_checkbox_site_key',
331 'srfm_v2_checkbox_secret_key',
332 'srfm_v2_invisible_site_key',
333 'srfm_v2_invisible_secret_key',
334 'srfm_v3_site_key',
335 'srfm_v3_secret_key',
336 'srfm_cf_turnstile_site_key',
337 'srfm_cf_turnstile_secret_key',
338 'srfm_hcaptcha_site_key',
339 'srfm_hcaptcha_secret_key',
340 'srfm_cf_appearance_mode',
341 ];
342
343 foreach ( $text_keys as $key ) {
344 if ( isset( $settings[ $key ] ) ) {
345 $settings[ $key ] = sanitize_text_field( Helper::get_string_value( $settings[ $key ] ) );
346 }
347 }
348
349 if ( isset( $settings['srfm_honeypot'] ) ) {
350 $settings['srfm_honeypot'] = rest_sanitize_boolean( Helper::get_string_value( $settings['srfm_honeypot'] ) );
351 }
352
353 return $settings;
354 }
355
356 /**
357 * Sanitize validation message settings.
358 *
359 * @param array<string,mixed> $settings Raw settings.
360 * @since 2.6.0
361 * @return array<string,mixed>
362 */
363 private function sanitize_validation_messages( $settings ) {
364 foreach ( $settings as $key => $value ) {
365 if ( is_string( $value ) ) {
366 $settings[ $key ] = sanitize_text_field( $value );
367 }
368 }
369
370 return $settings;
371 }
372
373 /**
374 * Save security settings, preserving masked sentinel values.
375 *
376 * When the caller sends the SECRET_SENTINEL for a secret key, the stored
377 * value is preserved instead of being overwritten with the sentinel.
378 *
379 * @param array<string,mixed> $settings Settings to save.
380 * @since 2.6.0
381 * @return bool
382 */
383 private function save_security_settings( $settings ) {
384 $existing = get_option( 'srfm_security_settings_options', [] );
385
386 if ( ! is_array( $existing ) ) {
387 $existing = [];
388 }
389
390 // Replace masked sentinel values with stored values.
391 foreach ( self::$secret_keys as $key ) {
392 if ( isset( $settings[ $key ] ) && self::SECRET_SENTINEL === $settings[ $key ] && isset( $existing[ $key ] ) ) {
393 $settings[ $key ] = $existing[ $key ];
394 }
395 }
396
397 return Global_Settings::srfm_save_security_settings( $settings );
398 }
399 }
400