PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.3.2
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.3.2
2.3.2 2.3.1 2.3.0 2.2.8 2.2.7 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.11.0 1.11.1 1.11.2 1.6 1.7 1.8 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.9.10 1.9.11 All 60 releases
merchant / admin / classes / admin-options / class-merchant-settings-saver.php

class-merchant-settings-saver.php in Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant 2.3.2, at admin/classes/admin-options/class-merchant-settings-saver.php

248 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Merchant Settings Saver.
4 *
5 * Handles saving, sanitizing, and preprocessing of module
6 * option values submitted via the admin settings form.
7 * Extracted from {@see Merchant_Admin_Options}.
8 *
9 * @package Merchant
10 * @since 1.9.3
11 */
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Merchant_Settings_Saver
19 *
20 * Processes submitted form data: verifies nonces/capabilities,
21 * preprocesses and sanitizes values via the field registry,
22 * and persists settings to the database.
23 *
24 * @since 1.9.3
25 */
26 class Merchant_Settings_Saver {
27
28 /**
29 * Save module options from a form submission.
30 *
31 * Verifies the request, then iterates each field definition to
32 * preprocess, sanitize, and persist the values. Supports
33 * both save and reset operations.
34 *
35 * Fires `merchant_options_saved` and `merchant_options_saved_{module_id}`
36 * actions after a successful save.
37 *
38 * @since 1.0
39 *
40 * @param array<string, mixed> $settings Module settings configuration with 'module' and 'fields' keys.
41 *
42 * @return void
43 */
44 public static function save_options( $settings ) {
45 if ( ! self::verify_save_request() ) {
46 return;
47 }
48
49 // A locked section is read-only: its inputs are disabled in the UI, and
50 // this guard is what makes that a boundary rather than a courtesy.
51 if ( ! empty( $settings['locked'] ) ) {
52 return;
53 }
54
55 self::maybe_decode_json_payload();
56
57 $save = ! empty( $_POST['merchant_save'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in verify_save_request().
58 $reset = ! empty( $_POST['merchant_reset'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in verify_save_request().
59 $options = get_option( 'merchant', array() );
60
61 if ( $save && ! empty( $settings['fields'] ) ) {
62 $options[ $settings['module'] ] = self::process_save_fields( $settings['fields'], $settings['module'], $options );
63 } elseif ( $reset ) {
64 $options[ $settings['module'] ] = array();
65 }
66
67 update_option( 'merchant', $options );
68
69 /**
70 * Hook: merchant_options_saved, fired after saving module options.
71 *
72 * @param string $module module ID.
73 * @param array $options module options.
74 *
75 * @since 1.9.3
76 */
77 do_action( 'merchant_options_saved', $settings['module'], $options[ $settings['module'] ] );
78
79 /**
80 * Hook: merchant_options_saved, fired after saving specific module options.
81 *
82 * @param array $options module options.
83 *
84 * @since 1.9.3
85 */
86 do_action( "merchant_options_saved_{$settings['module']}", $options[ $settings['module'] ] );
87 }
88
89 /**
90 * Verify the save request has valid nonce and user capabilities.
91 *
92 * @since 1.9.3
93 *
94 * @return bool True if the request is valid and the user can save.
95 */
96 private static function verify_save_request() {
97 $nonce = isset( $_POST['merchant_nonce'] )
98 ? sanitize_text_field( wp_unslash( $_POST['merchant_nonce'] ) )
99 : '';
100
101 if ( ! wp_verify_nonce( $nonce, 'merchant_nonce' ) ) {
102 return false;
103 }
104
105 return current_user_can( 'manage_options' );
106 }
107
108 /**
109 * Process and sanitize submitted field values for saving.
110 *
111 * Iterates each field definition, preprocesses through the field
112 * registry, sanitizes the value, and returns the updated module
113 * options array.
114 *
115 * @since 1.9.3
116 *
117 * @param array<int, array<string, mixed>> $fields Array of field definition arrays.
118 * @param string $module The module ID.
119 * @param array<string, mixed> $options All saved merchant options.
120 *
121 * @return array<string, mixed> Updated module options.
122 */
123 private static function process_save_fields( $fields, $module, $options ) {
124 $module_options = $options[ $module ] ?? array();
125
126 foreach ( $fields as $field ) {
127 if ( ! isset( $field['id'] ) ) {
128 continue;
129 }
130
131 if ( ! merchant_is_pro_active() && isset( $field['pro'] ) && $field['pro'] === true ) {
132 continue;
133 }
134
135 // A locked field renders disabled, so the browser never posts it —
136 // falling through would sanitize nothing back over the stored value.
137 if ( ! empty( $field['locked'] ) ) {
138 continue;
139 }
140
141 $value = null;
142
143 if ( isset( $_POST['merchant'][ $field['id'] ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in verify_save_request().
144 $raw_value = wp_unslash( $_POST['merchant'][ $field['id'] ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing -- verified in verify_save_request().
145 $value = self::sanitize_field_value( $field, $raw_value );
146 } else {
147 $value = self::sanitize( $field, $value );
148 }
149
150 $module_options[ $field['id'] ] = $value;
151 }
152
153 return $module_options;
154 }
155
156 /**
157 * Sanitize a single field value through the registry or fallback.
158 *
159 * Expects `$raw_value` to already be unslashed.
160 *
161 * @since 1.9.3
162 *
163 * @param array<string, mixed> $field The field configuration array.
164 * @param mixed $raw_value The raw submitted value (already unslashed).
165 *
166 * @return mixed The sanitized value.
167 */
168 private static function sanitize_field_value( $field, $raw_value ) {
169 $type = $field['type'] ?? '';
170 $registry = Merchant_Field_Registry::instance();
171
172 // Preprocess first (only during save, not in the public API).
173 if ( $registry->has( $type ) ) {
174 $field_instance = $registry->create( $type, $field, $raw_value );
175 if ( $field_instance !== null ) {
176 $raw_value = $field_instance->preprocess( $raw_value );
177 }
178 }
179
180 // Then sanitize through the single consolidated path.
181 return self::sanitize( $field, $raw_value );
182 }
183
184 /**
185 * Sanitize options.
186 *
187 * Delegates type-specific sanitization to the field registry.
188 * Each field class implements its own sanitize_value() method.
189 *
190 * @since 1.9.3
191 *
192 * @param array<string, mixed> $field The field configuration.
193 * @param mixed $value The raw submitted value.
194 *
195 * @return mixed The sanitized value.
196 */
197 public static function sanitize( $field, $value ) {
198 // Custom sanitize callback takes priority.
199 if ( ! empty( $field['sanitize'] ) && is_callable( $field['sanitize'] ) ) {
200 return call_user_func( $field['sanitize'], $value );
201 }
202
203 $type = $field['type'] ?? '';
204 $registry = Merchant_Field_Registry::instance();
205
206 if ( $registry->has( $type ) ) {
207 $field_instance = $registry->create( $type, $field, $value );
208
209 if ( $field_instance !== null ) {
210 return $field_instance->sanitize( $value );
211 }
212 }
213
214 // Fallback for unregistered types.
215 return sanitize_text_field( $value );
216 }
217
218 /**
219 * Decode JSON payload submitted by the admin JS to bypass max_input_vars.
220 *
221 * When the form has many fields (e.g. flexible_content with 20+ campaigns),
222 * the JS serializes all merchant[*] fields into a single JSON string posted
223 * as `merchant_json_payload`. This method decodes it back into $_POST['merchant'].
224 *
225 * Falls back silently to standard POST if the payload is missing or malformed.
226 *
227 * @since 2.2.5
228 *
229 * @return void
230 */
231 private static function maybe_decode_json_payload() {
232 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in verify_save_request().
233 if ( ! isset( $_POST['merchant_json_payload'] ) ) {
234 return;
235 }
236
237 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded, each field sanitized downstream.
238 $raw = wp_unslash( $_POST['merchant_json_payload'] );
239 $decoded = json_decode( $raw, true );
240
241 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) {
242 return; // Malformed — fall back to standard POST.
243 }
244
245 $_POST['merchant'] = $decoded; // phpcs:ignore WordPress.Security.NonceVerification.Missing
246 }
247 }
248