PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.2.8
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.2.8
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.2.8, at admin/classes/admin-options/class-merchant-settings-saver.php

236 lines 7.2 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 self::maybe_decode_json_payload();
50
51 $save = ! empty( $_POST['merchant_save'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in verify_save_request().
52 $reset = ! empty( $_POST['merchant_reset'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in verify_save_request().
53 $options = get_option( 'merchant', array() );
54
55 if ( $save && ! empty( $settings['fields'] ) ) {
56 $options[ $settings['module'] ] = self::process_save_fields( $settings['fields'], $settings['module'], $options );
57 } elseif ( $reset ) {
58 $options[ $settings['module'] ] = array();
59 }
60
61 update_option( 'merchant', $options );
62
63 /**
64 * Hook: merchant_options_saved, fired after saving module options.
65 *
66 * @param string $module module ID.
67 * @param array $options module options.
68 *
69 * @since 1.9.3
70 */
71 do_action( 'merchant_options_saved', $settings['module'], $options[ $settings['module'] ] );
72
73 /**
74 * Hook: merchant_options_saved, fired after saving specific module options.
75 *
76 * @param array $options module options.
77 *
78 * @since 1.9.3
79 */
80 do_action( "merchant_options_saved_{$settings['module']}", $options[ $settings['module'] ] );
81 }
82
83 /**
84 * Verify the save request has valid nonce and user capabilities.
85 *
86 * @since 1.9.3
87 *
88 * @return bool True if the request is valid and the user can save.
89 */
90 private static function verify_save_request() {
91 $nonce = isset( $_POST['merchant_nonce'] )
92 ? sanitize_text_field( wp_unslash( $_POST['merchant_nonce'] ) )
93 : '';
94
95 if ( ! wp_verify_nonce( $nonce, 'merchant_nonce' ) ) {
96 return false;
97 }
98
99 return current_user_can( 'manage_options' );
100 }
101
102 /**
103 * Process and sanitize submitted field values for saving.
104 *
105 * Iterates each field definition, preprocesses through the field
106 * registry, sanitizes the value, and returns the updated module
107 * options array.
108 *
109 * @since 1.9.3
110 *
111 * @param array<int, array<string, mixed>> $fields Array of field definition arrays.
112 * @param string $module The module ID.
113 * @param array<string, mixed> $options All saved merchant options.
114 *
115 * @return array<string, mixed> Updated module options.
116 */
117 private static function process_save_fields( $fields, $module, $options ) {
118 $module_options = $options[ $module ] ?? array();
119
120 foreach ( $fields as $field ) {
121 if ( ! isset( $field['id'] ) ) {
122 continue;
123 }
124
125 if ( ! merchant_is_pro_active() && isset( $field['pro'] ) && $field['pro'] === true ) {
126 continue;
127 }
128
129 $value = null;
130
131 if ( isset( $_POST['merchant'][ $field['id'] ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in verify_save_request().
132 $raw_value = wp_unslash( $_POST['merchant'][ $field['id'] ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing -- verified in verify_save_request().
133 $value = self::sanitize_field_value( $field, $raw_value );
134 } else {
135 $value = self::sanitize( $field, $value );
136 }
137
138 $module_options[ $field['id'] ] = $value;
139 }
140
141 return $module_options;
142 }
143
144 /**
145 * Sanitize a single field value through the registry or fallback.
146 *
147 * Expects `$raw_value` to already be unslashed.
148 *
149 * @since 1.9.3
150 *
151 * @param array<string, mixed> $field The field configuration array.
152 * @param mixed $raw_value The raw submitted value (already unslashed).
153 *
154 * @return mixed The sanitized value.
155 */
156 private static function sanitize_field_value( $field, $raw_value ) {
157 $type = $field['type'] ?? '';
158 $registry = Merchant_Field_Registry::instance();
159
160 // Preprocess first (only during save, not in the public API).
161 if ( $registry->has( $type ) ) {
162 $field_instance = $registry->create( $type, $field, $raw_value );
163 if ( $field_instance !== null ) {
164 $raw_value = $field_instance->preprocess( $raw_value );
165 }
166 }
167
168 // Then sanitize through the single consolidated path.
169 return self::sanitize( $field, $raw_value );
170 }
171
172 /**
173 * Sanitize options.
174 *
175 * Delegates type-specific sanitization to the field registry.
176 * Each field class implements its own sanitize_value() method.
177 *
178 * @since 1.9.3
179 *
180 * @param array<string, mixed> $field The field configuration.
181 * @param mixed $value The raw submitted value.
182 *
183 * @return mixed The sanitized value.
184 */
185 public static function sanitize( $field, $value ) {
186 // Custom sanitize callback takes priority.
187 if ( ! empty( $field['sanitize'] ) && is_callable( $field['sanitize'] ) ) {
188 return call_user_func( $field['sanitize'], $value );
189 }
190
191 $type = $field['type'] ?? '';
192 $registry = Merchant_Field_Registry::instance();
193
194 if ( $registry->has( $type ) ) {
195 $field_instance = $registry->create( $type, $field, $value );
196
197 if ( $field_instance !== null ) {
198 return $field_instance->sanitize( $value );
199 }
200 }
201
202 // Fallback for unregistered types.
203 return sanitize_text_field( $value );
204 }
205
206 /**
207 * Decode JSON payload submitted by the admin JS to bypass max_input_vars.
208 *
209 * When the form has many fields (e.g. flexible_content with 20+ campaigns),
210 * the JS serializes all merchant[*] fields into a single JSON string posted
211 * as `merchant_json_payload`. This method decodes it back into $_POST['merchant'].
212 *
213 * Falls back silently to standard POST if the payload is missing or malformed.
214 *
215 * @since 2.2.5
216 *
217 * @return void
218 */
219 private static function maybe_decode_json_payload() {
220 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in verify_save_request().
221 if ( ! isset( $_POST['merchant_json_payload'] ) ) {
222 return;
223 }
224
225 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded, each field sanitized downstream.
226 $raw = wp_unslash( $_POST['merchant_json_payload'] );
227 $decoded = json_decode( $raw, true );
228
229 if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) {
230 return; // Malformed — fall back to standard POST.
231 }
232
233 $_POST['merchant'] = $decoded; // phpcs:ignore WordPress.Security.NonceVerification.Missing
234 }
235 }
236