PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.2.1
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.2.1
7.2.2 7.2.1 7.2 7.1.2 7.1.1 7.1 7.0.4 7.0.6 7.0.7 6.3.8 6.3.7 6.3.6 6.3.5 6.3.4 6.3.3 6.3.1 trunk 5.7.3 5.7.5 5.8.1 5.8.2 5.8.3 5.8.4 5.8.6 6.0.4 All 37 releases
← All changes | includes/mlsimport-progressive-save.php +260 -615 6.0.47.2.1 View file →
@@ -1,676 +1,321 @@
1 1 <?php
2 2 /**
3 - * MLS Import Progressive Save Handlers
3 + * WordPress persistence adapter for the Field Configuration module.
4 4 *
5 - * Server-side handlers for the progressive save system:
6 - * - Initial save detection and chunked saving
7 - * - Individual field option saving
8 - * - Optimized field order saving
5 + * The browser exposes one mutation endpoint with four fixed POST variables:
6 + * action, nonce, revision, and one JSON command. This file translates that
7 + * request into the domain module, performs an exact database compare-and-swap,
8 + * refreshes WordPress's option cache, and emits the authoritative result. The
9 + * former chunk, individual, bulk, and position handlers intentionally do not
10 + * survive as alternate mutation paths.
9 11 *
10 - * @package MLSImport
11 - * @subpackage MLSImport/includes
12 + * @package MLSImport
12 13 */
13 14
14 -// Exit if accessed directly
15 15 if ( ! defined( 'ABSPATH' ) ) {
16 - exit;
16 + exit;
17 17 }
18 18
19 -
20 19 /**
21 - * AJAX handler for saving field order
22 - * This stores the order directly in the mlsimport_admin_fields_select option
20 + * Decode one connection's MLS metadata into the domain module's field map.
21 + *
22 + * Metadata can be stored as the original JSON string or as an already decoded
23 + * array. Both shapes are accepted here; malformed or absent metadata becomes
24 + * an empty map so every caller reaches the same normalization path. Since
25 + * multi-MLS (#275) the blob is per-connection: the resolver returns the
26 + * "_{mls_id}"-suffixed option for the given (or current) connection.
27 + *
28 + * @param int $mls_id Connection whose metadata to read; 0 = current connection.
29 + * @return array That connection's MLS metadata keyed by RESO field name.
23 30 */
24 -function mlsimport_ajax_save_field_order() {
25 - // Add detailed logging for debugging
31 +function mlsimport_field_configuration_metadata( int $mls_id = 0 ): array {
32 + $metadata = mlsimport_get_connection_option( 'mlsimport_mls_metadata_mls_data', '', $mls_id );
33 + $metadata = is_string( $metadata ) ? json_decode( $metadata, true ) : $metadata;
26 34
27 - // Check nonce
28 - if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'mlsimport_field_selector_nonce')) {
29 - //error_log('Invalid security token in field order save');
30 - wp_send_json_error('Invalid security token');
31 - return;
32 - }
33 -
34 - // Check for both parameter formats
35 - $field_order = array();
36 -
37 - // Check for standard array format (field_order[])
38 - if (isset($_POST['field_order']) && is_array($_POST['field_order'])) {
39 - $field_order = $_POST['field_order'];
40 - //error_log('Using field_order parameter: ' . count($field_order) . ' items');
41 - }
42 - // Check for older format (fields[])
43 - else if (isset($_POST['fields']) && is_array($_POST['fields'])) {
44 - $field_order = $_POST['fields'];
45 - //error_log('Using fields parameter: ' . count($field_order) . ' items');
46 - }
47 - // Otherwise, try to get values directly by key pattern
48 - else {
49 - //error_log('No direct array found, checking for field_order[] pattern');
50 - // Check if we need to manually extract from the POST data (for field_order[])
51 - foreach ($_POST as $key => $value) {
52 - if (preg_match('/^field_order(\[.*\])?$/', $key)) {
53 - if (is_array($value)) {
54 - $field_order = $value;
55 - //error_log('Found field_order as array with ' . count($field_order) . ' items');
56 - break;
57 - }
58 - }
59 - }
60 -
61 - // If we still don't have field order data, try to extract it directly from POST
62 - if (empty($field_order)) {
63 - //error_log('Trying to manually build array from field_order[]');
64 - foreach ($_POST as $key => $value) {
65 - if (strpos($key, 'field_order[') === 0) {
66 - $field_order[] = $value;
67 - }
68 - }
69 - //error_log('Manually built array with ' . count($field_order) . ' items');
70 - }
71 - }
72 -
73 - // Check if we have any field order data
74 - if (empty($field_order)) {
75 - //error_log('No field order data found in request');
76 - wp_send_json_error('No field order provided');
77 - return;
78 - }
79 -
80 - // Log field count for debugging
81 - //error_log('Saving ' . count($field_order) . ' fields in order');
82 -
83 - // Get current options
84 - $options = get_option('mlsimport_admin_fields_select', array());
85 -
86 - // Make sure $options is an array
87 - if (!is_array($options)) {
88 - $options = array();
89 - }
90 -
91 - // Store the field order directly in the options
92 - $options['field_order'] = array_map('sanitize_text_field', $field_order);
93 -
94 - // Save the updated options
95 - $result = update_option('mlsimport_admin_fields_select', $options);
96 -
97 - if ($result) {
98 - global $mlsimport;
99 - if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'enviroment_custom_fields' ) ) {
100 - $mlsimport->admin->env_data->enviroment_custom_fields( $mlsimport->get_plugin_name() );
101 - }
102 - //error_log('Field order saved successfully');
103 - wp_send_json_success('Field order saved successfully');
104 - } else {
105 - //error_log('Failed to save field order - update_option returned false');
106 - wp_send_json_error('Failed to save field order - update_option returned false');
107 - }
35 + return is_array( $metadata ) ? $metadata : array();
108 36 }
109 37
110 -
111 -
112 38 /**
113 - * Add this function to your mlsimport-admin-fields-select.php file
114 - * Outputs the nonce field needed for field ordering
39 + * Return taxonomy destinations registered for the active property post type.
40 + *
41 + * The adapter is resolved on demand because metadata gathering and AJAX calls
42 + * run after the plugin has created its active theme environment.
43 + *
44 + * @return array Taxonomy slug-to-label map accepted by mutation validation.
115 45 */
116 -function mlsimport_add_field_selector_nonce() {
117 - wp_nonce_field('mlsimport_field_selector_nonce', 'mlsimport_field_selector_nonce');
118 -}
46 +function mlsimport_field_configuration_taxonomies(): array {
47 + global $mlsimport;
119 48
49 + $post_type = '';
50 + if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'get_property_post_type' ) ) {
51 + $post_type = $mlsimport->admin->env_data->get_property_post_type();
52 + }
120 53
54 + $taxonomies = mlsimport_get_custom_post_type_taxonomies( $post_type );
121 55
56 + return is_array( $taxonomies ) ? $taxonomies : array();
57 +}
122 58
123 -
124 -
125 59 /**
126 - * Register AJAX handlers for progressive saving
60 + * Atomically replace the one Field Configuration option.
61 + *
62 + * WordPress's update_option() has no expected-value condition, so two tabs can
63 + * both pass an application-level revision check and the slower request can
64 + * overwrite the newer one. The UPDATE below includes the exact serialized old
65 + * value in its WHERE clause. One request wins; the other updates zero rows and
66 + * is reported by the module as stale. Cache and public option hooks are updated
67 + * once only after the database confirms the replacement.
68 + *
69 + * @param array $expected Exact option value previously loaded.
70 + * @param array $replacement Complete normalized replacement.
71 + * @param string $option_name Concrete (per-connection) option to swap; ''
72 + * resolves the current connection's option (#275).
73 + * @return bool True only when this caller won the compare-and-swap.
127 74 */
128 -function mlsimport_register_progressive_save_handlers() {
129 - // Check if initial save is needed
130 - add_action('wp_ajax_mlsimport_check_initial_save_needed', 'mlsimport_ajax_check_initial_save_needed');
131 -
132 - // Save a chunk of fields
133 - add_action('wp_ajax_mlsimport_save_field_chunk', 'mlsimport_ajax_save_field_chunk');
75 +function mlsimport_field_configuration_compare_and_swap( array $expected, array $replacement, string $option_name = '' ): bool {
76 + global $wpdb;
134 77
135 - // Save an individual field option
136 - add_action('wp_ajax_mlsimport_save_field_option', 'mlsimport_ajax_save_field_option');
78 + if ( '' === $option_name ) {
79 + $option_name = mlsimport_connection_option_name( 'mlsimport_admin_fields_select' );
80 + }
81 + $row = $wpdb->get_row(
82 + $wpdb->prepare(
83 + "SELECT option_value, autoload FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
84 + $option_name
85 + ),
86 + ARRAY_A
87 + );
137 88
138 - // Bulk save import selections
139 - add_action('wp_ajax_mlsimport_save_bulk_import', 'mlsimport_ajax_save_bulk_import');
89 + // add_option() uses the option_name unique key as the atomic first-write gate.
90 + if ( null === $row ) {
91 + if ( array() !== $expected ) {
92 + return false;
93 + }
140 94
141 - // Bulk save admin visibility selections
142 - add_action('wp_ajax_mlsimport_save_bulk_admin', 'mlsimport_ajax_save_bulk_admin');
143 -}
144 -add_action('init', 'mlsimport_register_progressive_save_handlers');
95 + $added = add_option( $option_name, $replacement, '', false );
96 + if ( ! $added ) {
97 + wp_cache_delete( $option_name, 'options' );
98 + wp_cache_delete( 'alloptions', 'options' );
99 + wp_cache_delete( 'notoptions', 'options' );
100 + } else {
101 + if ( function_exists( 'mlsimport_active_field_configuration' ) ) {
102 + mlsimport_active_field_configuration( true );
103 + }
104 + // The theme-adapter resync listens on the BASE option's hook name
105 + // (loader binds add/update_option_mlsimport_admin_fields_select).
106 + // Per-connection storage (#275) uses a suffixed option name, so
107 + // WordPress fires "add_option_{$option_name}" instead — announce the
108 + // field-selection change on the base name explicitly.
109 + if ( 'mlsimport_admin_fields_select' !== $option_name ) {
110 + do_action( 'update_option_mlsimport_admin_fields_select', $expected, $replacement, $option_name );
111 + }
112 + }
145 113
114 + return $added;
115 + }
146 116
117 + $old_serialized = maybe_serialize( $expected );
118 + $new_serialized = maybe_serialize( $replacement );
119 + $updated = $wpdb->query(
120 + $wpdb->prepare(
121 + "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND BINARY option_value = BINARY %s",
122 + $new_serialized,
123 + $option_name,
124 + $old_serialized
125 + )
126 + );
127 + if ( 1 !== $updated ) {
128 + // Another request won after this process loaded its option. Discard every
129 + // local option-cache route so the module can report the winner's revision.
130 + wp_cache_delete( $option_name, 'options' );
131 + wp_cache_delete( 'alloptions', 'options' );
132 + wp_cache_delete( 'notoptions', 'options' );
133 + return false;
134 + }
147 135
136 + // Mirror update_option() cache behavior while preserving the existing
137 + // autoload choice. Large 1,000-field configurations are not newly autoloaded.
138 + $alloptions = wp_load_alloptions( true );
139 + if ( isset( $alloptions[ $option_name ] ) ) {
140 + $alloptions[ $option_name ] = $new_serialized;
141 + wp_cache_set( 'alloptions', $alloptions, 'options' );
142 + } else {
143 + wp_cache_set( $option_name, $new_serialized, 'options' );
144 + }
148 145
146 + mlsimport_active_field_configuration( true );
147 + // Announce on the BASE hook name: the theme-adapter resync listener is
148 + // bound to update_option_mlsimport_admin_fields_select regardless of which
149 + // connection's suffixed option (#275) actually stored the change.
150 + do_action( 'update_option_mlsimport_admin_fields_select', $expected, $replacement, 'mlsimport_admin_fields_select' );
151 + do_action( 'updated_option', $option_name, $expected, $replacement );
149 152
153 + return true;
154 +}
150 155
151 -function mlsimport_ajax_save_field_chunk() {
152 - // Check nonce
153 - if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'mlsimport_field_selector_nonce')) {
154 - wp_send_json_error('Invalid security token');
155 - return;
156 - }
157 -
158 - // Check if fields were provided
159 - if (!isset($_POST['fields']) || !is_array($_POST['fields'])) {
160 - wp_send_json_error('No field data provided');
161 - return;
162 - }
163 -
164 - // Get chunk info
165 - $chunk_index = isset($_POST['chunk_index']) ? intval($_POST['chunk_index']) : 0;
166 - $total_chunks = isset($_POST['total_chunks']) ? intval($_POST['total_chunks']) : 1;
167 - $is_last_chunk = ($chunk_index + 1) == $total_chunks;
168 -
169 - // Get current options
170 - $options = get_option('mlsimport_admin_fields_select', array());
171 -
172 - // Initialize arrays if they don't exist
173 - if (!is_array($options)) {
174 - $options = array();
175 - }
176 -
177 - if (!isset($options['mls-fields'])) {
178 - $options['mls-fields'] = array();
179 - }
180 -
181 - if (!isset($options['mls-fields-admin'])) {
182 - $options['mls-fields-admin'] = array();
183 - }
184 -
185 - if (!isset($options['mls-fields-label'])) {
186 - $options['mls-fields-label'] = array();
187 - }
188 -
189 - if (!isset($options['mls-fields-map-postmeta'])) {
190 - $options['mls-fields-map-postmeta'] = array();
191 - }
192 -
193 - if (!isset($options['mls-fields-map-taxonomy'])) {
194 - $options['mls-fields-map-taxonomy'] = array();
195 - }
156 +/**
157 + * Construct the authoritative module around one connection's option store.
158 + *
159 + * Step by step (#275 — per-connection field mapping):
160 + * 1. Resolve the concrete option name once: the given connection's suffixed
161 + * 'mlsimport_admin_fields_select_{mls_id}', or the current connection's
162 + * when no mls_id is passed (every legacy call site).
163 + * 2. Bind BOTH the loader and the compare-and-swap writer to that one name,
164 + * so load and persistence can never address different connections. The
165 + * revision key and CAS semantics are unchanged — just scoped per option.
166 + *
167 + * @param int $mls_id Connection to bind to; 0 = current connection.
168 + * @return Mlsimport_Field_Configuration Configured domain service.
169 + */
170 +function mlsimport_field_configuration( int $mls_id = 0 ): Mlsimport_Field_Configuration {
171 + // Step 1: one resolution, shared by both storage callbacks.
172 + $option_name = mlsimport_connection_option_name( 'mlsimport_admin_fields_select', $mls_id );
196 173
197 - if (!isset($options['field_order'])) {
198 - $options['field_order'] = array();
199 - $i = 0;
200 - }else{
201 - $i = count($options['field_order'] );
202 - }
203 -
204 -
205 -
206 - // Process fields in the chunk
207 - foreach ($_POST['fields'] as $field_key => $field_data) {
208 -
209 -
210 - // Process field data as before
211 - if (isset($field_data['import'])) {
212 - $options['mls-fields'][$field_key] = intval($field_data['import']);
213 - }
214 -
215 - if (isset($field_data['admin'])) {
216 - $options['mls-fields-admin'][$field_key] = intval($field_data['admin']);
217 - }
218 -
219 - if (isset($field_data['label'])) {
220 - $options['mls-fields-label'][$field_key] = $field_data['label'];
221 - }
222 -
223 - if (isset($field_data['postmeta'])) {
224 - $options['mls-fields-map-postmeta'][$field_key] = $field_data['postmeta'];
225 - }
226 -
227 - if (isset($field_data['taxonomy'])) {
228 - $options['mls-fields-map-taxonomy'][$field_key] = $field_data['taxonomy'];
229 - }
230 -
231 -
232 - $options['field_order'][$field_key] = $i++;
233 -
234 - }
235 -
236 -
237 - // print_r($options);
238 -
239 - // Save the options
240 - $saved = update_option('mlsimport_admin_fields_select', $options);
241 - if ( $saved ) {
242 - global $mlsimport;
243 - if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'enviroment_custom_fields' ) ) {
244 - $mlsimport->admin->env_data->enviroment_custom_fields( $mlsimport->get_plugin_name() );
245 - }
246 - }
247 -
248 - wp_send_json_success(array(
249 -
250 - 'field_order'=> $options['field_order'],
251 - 'message' => 'Field chunk ' . ($chunk_index + 1) . ' of ' . $total_chunks . ' saved successfully',
252 - 'chunkIndex' => $chunk_index,
253 - 'totalChunks' => $total_chunks,
254 - 'isLastChunk' => $is_last_chunk,
255 -
256 - ));
174 + // Step 2: loader and CAS are closures over the same resolved name.
175 + return new Mlsimport_Field_Configuration(
176 + static function () use ( $option_name ) {
177 + $value = get_option( $option_name, array() );
178 + return is_array( $value ) ? $value : array();
179 + },
180 + static function ( array $expected, array $replacement ) use ( $option_name ) {
181 + return mlsimport_field_configuration_compare_and_swap( $expected, $replacement, $option_name );
182 + }
183 + );
257 184 }
258 185
259 186 /**
260 - * AJAX handler to save import selections in bulk
187 + * Return normalized durable state for consumers that must remove dormant data.
188 + *
189 + * Theme custom-field registries need both active fields to add and dormant
190 + * fields to remove from theme-owned display definitions. This read still enters
191 + * through the module, preserving normalization and taxonomy rules without
192 + * exposing a direct option read as an alternate persistence boundary.
193 + *
194 + * @return array Normalized active-and-dormant Field Configuration.
261 195 */
262 -function mlsimport_ajax_save_bulk_import() {
263 - if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'mlsimport_field_selector_nonce')) {
264 - wp_send_json_error('Invalid security token');
265 - }
266 -
267 - if (!isset($_POST['fields']) || !is_array($_POST['fields'])) {
268 - wp_send_json_error('No field data provided');
269 - }
270 -
271 - $options = get_option('mlsimport_admin_fields_select', array());
272 - if (!isset($options['mls-fields']) || !is_array($options['mls-fields'])) {
273 - $options['mls-fields'] = array();
274 - }
275 -
276 - foreach ($_POST['fields'] as $key => $value) {
277 - $options['mls-fields'][sanitize_text_field($key)] = intval($value);
278 - }
279 -
280 - $result = update_option('mlsimport_admin_fields_select', $options);
281 - if ($result) {
282 - global $mlsimport;
283 - if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'enviroment_custom_fields' ) ) {
284 - $mlsimport->admin->env_data->enviroment_custom_fields( $mlsimport->get_plugin_name() );
285 - }
286 - wp_send_json_success('Bulk import selections saved');
287 - } else {
288 - wp_send_json_error('Failed to save selections');
289 - }
196 +function mlsimport_normalized_field_configuration(): array {
197 + return mlsimport_field_configuration()->read(
198 + mlsimport_field_configuration_metadata(),
199 + mlsimport_hardocde_theme_schema(),
200 + mlsimport_field_configuration_taxonomies()
201 + );
290 202 }
291 203
292 204 /**
293 - * AJAX handler to save admin visibility selections in bulk
205 + * Return the active-only configuration for import and display consumers.
206 + *
207 + * The durable option retains Dormant MLS Fields so their choices can return.
208 + * Runtime consumers use this projection to exclude those fields consistently
209 + * without each theme reimplementing metadata intersection and array ordering.
210 + *
211 + * The projection is cached for the request because import adapters consult it
212 + * once per listing. Rebuilding and sorting 1,000 parallel fields for every
213 + * listing would turn schema safety into an avoidable import bottleneck. The
214 + * cache is keyed by connection (#277 — a task-bound import may read another
215 + * connection's projection); a refresh drops EVERY cached projection so the
216 + * existing compare-and-swap invalidation stays a single call.
217 + *
218 + * @param bool $refresh Rebuild after this request has changed the option.
219 + * @param int $mls_id Connection to read; 0 = the current connection.
220 + * @return array Normalized configuration containing current metadata fields only.
294 221 */
295 -function mlsimport_ajax_save_bulk_admin() {
296 - if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'mlsimport_field_selector_nonce')) {
297 - wp_send_json_error('Invalid security token');
298 - }
222 +function mlsimport_active_field_configuration( bool $refresh = false, int $mls_id = 0 ): array {
223 + static $configurations = array();
299 224
300 - if (!isset($_POST['fields']) || !is_array($_POST['fields'])) {
301 - wp_send_json_error('No field data provided');
302 - }
225 + // One cache slot per resolved connection; 0 resolves to the current one so
226 + // legacy callers and task-scoped callers share a slot when they coincide.
227 + $slot = $mls_id > 0 ? $mls_id : mlsimport_current_mls_id();
303 228
304 - $options = get_option('mlsimport_admin_fields_select', array());
305 - if (!isset($options['mls-fields-admin']) || !is_array($options['mls-fields-admin'])) {
306 - $options['mls-fields-admin'] = array();
307 - }
229 + if ( $refresh ) {
230 + $configurations = array();
231 + }
232 + if ( isset( $configurations[ $slot ] ) ) {
233 + return $configurations[ $slot ];
234 + }
308 235
309 - foreach ($_POST['fields'] as $key => $value) {
310 - $options['mls-fields-admin'][sanitize_text_field($key)] = intval($value);
311 - }
236 + $configurations[ $slot ] = mlsimport_field_configuration( $mls_id )->read_active(
237 + mlsimport_field_configuration_metadata( $mls_id ),
238 + mlsimport_hardocde_theme_schema(),
239 + mlsimport_field_configuration_taxonomies()
240 + );
312 241
313 - $result = update_option('mlsimport_admin_fields_select', $options);
314 - if ($result) {
315 - global $mlsimport;
316 - if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'enviroment_custom_fields' ) ) {
317 - $mlsimport->admin->env_data->enviroment_custom_fields( $mlsimport->get_plugin_name() );
318 - }
319 - wp_send_json_success('Bulk admin selections saved');
320 - } else {
321 - wp_send_json_error('Failed to save selections');
322 - }
242 + return $configurations[ $slot ];
323 243 }
324 244
325 -
326 -
327 -
328 -
329 -
330 -
331 -
332 -
333 -
334 -
335 -
336 -
337 -
338 -
339 -
340 -
341 -
342 -
343 -
344 -
345 -
346 -
347 245 /**
348 - * AJAX handler to check if initial save is needed
246 + * Persist metadata initialization/reconciliation once on the server.
247 + *
248 + * @param array $metadata Newly gathered MLS metadata.
249 + * @param array $theme_schema Active theme defaults.
250 + * @param int $mls_id Connection to reconcile; 0 = current connection.
251 + * @return array Field Configuration Result.
349 252 */
350 -function mlsimport_ajax_check_initial_save_needed() {
351 - // Check nonce
352 - if ( ! isset( $_POST['security'] ) || ! wp_verify_nonce( $_POST['security'], 'mlsimport_field_selector_nonce' ) ) {
353 - wp_send_json_error( 'Invalid security token' );
354 - }
355 -
356 - // Check if option exists and has content
357 - $options = get_option( 'mlsimport_admin_fields_select' );
358 - $mlsimport_mls_metadata_mls_data = get_option( 'mlsimport_mls_metadata_mls_data', '' );
359 -
360 - // Decode MLS data
361 - $mls_data = json_decode( $mlsimport_mls_metadata_mls_data, true );
362 -
363 - // Check if we need an initial save
364 - $initial_save_needed = false;
365 -
366 - // If MLS data exists but option is empty/incomplete
367 - // had !empty( $mls_data ) &&
368 - if ( (
369 - empty( $options ) ||
370 - empty( $options['mls-fields'] )
371 - ) ) {
372 - $initial_save_needed = true;
373 - }
374 -
375 - wp_send_json_success( array(
376 - 'initialSaveNeeded' => $initial_save_needed,
377 -
378 - 'options'=>$options,
379 - 'fieldsInOption' => is_array( $options ) && isset( $options['mls-fields'] ) ? count( $options['mls-fields'] ) : 0,
380 - 'fieldsInMlsData' => is_array( $mls_data ) ? count( $mls_data ) : 0
381 - ) );
253 +function mlsimport_reconcile_field_configuration( array $metadata, array $theme_schema, int $mls_id = 0 ): array {
254 + return mlsimport_field_configuration( $mls_id )->reconcile( $metadata, $theme_schema, mlsimport_field_configuration_taxonomies() );
382 255 }
383 256
384 257 /**
385 - * AJAX handler to save an individual field option
258 + * Import an exported configuration through the same schema and storage owner.
259 + *
260 + * The target connection's own metadata blob drives normalization; fields the
261 + * blob does not know become dormant until that MLS's metadata is gathered.
262 + *
263 + * @param array $incoming Exported legacy-compatible option array.
264 + * @param int $mls_id Connection to import into; 0 = current connection.
265 + * @return array Field Configuration Result.
386 266 */
387 -function mlsimport_ajax_save_field_ssswsd2option() {
388 - // Check nonce
389 - if ( ! isset( $_POST['security'] ) || ! wp_verify_nonce( $_POST['security'], 'mlsimport_field_selector_nonce' ) ) {
390 - wp_send_json_error( 'Invalid security token' );
391 - }
392 -
393 - // Check if field key and option type were provided
394 - if ( ! isset( $_POST['field_key'] ) || ! isset( $_POST['option_type'] ) ) {
395 - wp_send_json_error( 'Missing field key or option type' );
396 - }
397 -
398 - // Sanitize inputs
399 - $field_key = sanitize_text_field( $_POST['field_key'] );
400 - $option_type = sanitize_text_field( $_POST['option_type'] );
401 - $value = isset( $_POST['value'] ) ? $_POST['value'] : '';
402 -
403 - // Validate option type
404 - $valid_option_types = array( 'import', 'admin', 'label', 'postmeta', 'taxonomy' );
405 -
406 - if ( ! in_array( $option_type, $valid_option_types ) ) {
407 - wp_send_json_error( 'Invalid option type' );
408 - }
409 -
410 - // Get current options
411 - $options = get_option( 'mlsimport_admin_fields_select', array() );
412 - // Keep a copy of the original options to detect changes
413 - $original_options = $options;
414 -
415 - // Map option type to options array key
416 - $option_map = array(
417 - 'import' => 'mls-fields',
418 - 'admin' => 'mls-fields-admin',
419 - 'label' => 'mls-fields-label',
420 - 'postmeta' => 'mls-fields-map-postmeta',
421 - 'taxonomy' => 'mls-fields-map-taxonomy'
422 - );
423 -
424 - $option_key = $option_map[ $option_type ];
425 -
426 - // Initialize array if it doesn't exist
427 - if ( ! isset( $options[ $option_key ] ) ) {
428 - $options[ $option_key ] = array();
429 - }
430 -
431 - // Sanitize and update value based on option type
432 - switch ( $option_type ) {
433 - case 'import':
434 - case 'admin':
435 - $options[ $option_key ][ $field_key ] = intval( $value );
436 - break;
437 -
438 - case 'label':
439 - case 'postmeta':
440 - case 'taxonomy':
441 - $options[ $option_key ][ $field_key ] = sanitize_text_field( $value );
442 - break;
443 - }
444 -
445 - // If nothing changed, treat as success without calling update_option
446 - if ( $options === $original_options ) {
447 - wp_send_json_success( array(
448 - 'message' => 'Field option saved successfully',
449 - 'fieldKey' => $field_key,
450 - 'optionType' => $option_type,
451 - 'value' => $value,
452 - ) );
453 - }
454 -
455 - // Save updated options
456 - $result = update_option( 'mlsimport_admin_fields_select', $options );
457 -
458 - if ( false !== $result ) {
459 - wp_send_json_success( array(
460 - 'message' => 'Field option saved successfully',
461 - 'fieldKey' => $field_key,
462 - 'optionType' => $option_type,
463 - 'value' => $value,
464 - ) );
465 - } else {
466 - wp_send_json_error( 'Failed to save field option' );
467 - }
267 +function mlsimport_import_field_configuration( array $incoming, int $mls_id = 0 ): array {
268 + return mlsimport_field_configuration( $mls_id )->import_configuration(
269 + $incoming,
270 + mlsimport_field_configuration_metadata( $mls_id ),
271 + mlsimport_hardocde_theme_schema(),
272 + mlsimport_field_configuration_taxonomies()
273 + );
468 274 }
469 -function mlsimport_ajax_save_field_option() {
470 - // Check nonce
471 - if ( ! isset( $_POST['security'] ) || ! wp_verify_nonce( $_POST['security'], 'mlsimport_field_selector_nonce' ) ) {
472 - //error_log( '[mlsimport] Invalid security token' );
473 - wp_send_json_error( 'Invalid security token' );
474 - }
475 275
476 - // Check if field key and option type were provided
477 - if ( ! isset( $_POST['field_key'] ) || ! isset( $_POST['option_type'] ) ) {
478 - //error_log( '[mlsimport] Missing field key or option type' );
479 - wp_send_json_error( 'Missing field key or option type' );
480 - }
276 +/**
277 + * Handle the sole browser Field Configuration mutation endpoint.
278 + *
279 + * Security and request-shape validation happen before decoding the compact
280 + * command. The module then validates domain rules and either returns an
281 + * authoritative saved result or a stable error code used by the queue UI.
282 + *
283 + * The handler terminates through WordPress JSON helpers. It intentionally has
284 + * no return type because those helpers stop execution after sending a response.
285 + */
286 +function mlsimport_ajax_change_field_configuration() {
287 + check_ajax_referer( 'mlsimport_field_selector_nonce', 'security' );
481 288
482 - // Sanitize inputs
483 - $field_key = sanitize_text_field( $_POST['field_key'] );
484 - $option_type = sanitize_text_field( $_POST['option_type'] );
485 - $value = isset( $_POST['value'] ) ? $_POST['value'] : '';
289 + if ( ! current_user_can( 'manage_options' ) ) {
290 + wp_send_json_error( array( 'error' => array( 'code' => 'forbidden', 'message' => 'You are not allowed to change Field Configuration.' ) ), 403 );
291 + }
486 292
487 - // Error log for debugging
488 - //error_log( '[mlsimport] Field Key: ' . $field_key );
489 - //error_log( '[mlsimport] Option Type: ' . $option_type );
490 - //error_log( '[mlsimport] Raw Value: ' . print_r( $value, true ) );
293 + if ( ! isset( $_POST['revision'], $_POST['command'] ) || ! is_scalar( $_POST['revision'] ) || ! is_scalar( $_POST['command'] ) ) {
294 + wp_send_json_error( array( 'error' => array( 'code' => 'invalid_request', 'message' => 'Revision and command are required.' ) ), 400 );
295 + }
491 296
492 - // Validate option type
493 - $valid_option_types = array( 'import', 'admin', 'label', 'postmeta', 'taxonomy' );
494 - if ( ! in_array( $option_type, $valid_option_types ) ) {
495 - //error_log( '[mlsimport] Invalid option type: ' . $option_type );
496 - wp_send_json_error( 'Invalid option type' );
497 - }
297 + $revision = max( 0, (int) wp_unslash( $_POST['revision'] ) );
298 + $command = json_decode( wp_unslash( (string) $_POST['command'] ), true );
299 + if ( ! is_array( $command ) ) {
300 + wp_send_json_error( array( 'error' => array( 'code' => 'invalid_json', 'message' => 'The Field Configuration command is not valid JSON.' ) ), 400 );
301 + }
498 302
499 - // Get current options
500 - $options = get_option( 'mlsimport_admin_fields_select', array() );
501 - $original_options = $options;
303 + // Per-connection scope: the tab posts the mls_id it was rendered for; a
304 + // request without one (legacy) resolves to the current connection.
305 + $mls_id = mlsimport_field_mapping_request_scope( isset( $_POST['mls_id'] ) && is_scalar( $_POST['mls_id'] ) ? wp_unslash( $_POST['mls_id'] ) : null );
502 306
503 - // Map option type to options array key
504 - $option_map = array(
505 - 'import' => 'mls-fields',
506 - 'admin' => 'mls-fields-admin',
507 - 'label' => 'mls-fields-label',
508 - 'postmeta' => 'mls-fields-map-postmeta',
509 - 'taxonomy' => 'mls-fields-map-taxonomy'
510 - );
307 + $result = mlsimport_field_configuration( $mls_id )->change(
308 + $revision,
309 + $command,
310 + mlsimport_field_configuration_metadata( $mls_id ),
311 + mlsimport_field_configuration_taxonomies(),
312 + mlsimport_hardocde_theme_schema()
313 + );
314 + if ( ! $result['success'] ) {
315 + $status = 'stale_revision' === $result['error']['code'] ? 409 : ( 'persistence_failed' === $result['error']['code'] ? 500 : 422 );
316 + wp_send_json_error( $result, $status );
317 + }
511 318
512 - $option_key = $option_map[ $option_type ];
513 -
514 - if ( ! isset( $options[ $option_key ] ) ) {
515 - $options[ $option_key ] = array();
516 - //error_log( '[mlsimport] Initialized option key: ' . $option_key );
517 - }
518 -
519 - // Sanitize and update value
520 - switch ( $option_type ) {
521 - case 'import':
522 - case 'admin':
523 - $options[ $option_key ][ $field_key ] = intval( $value );
524 - break;
525 -
526 - case 'label':
527 - case 'postmeta':
528 - case 'taxonomy':
529 - $options[ $option_key ][ $field_key ] = sanitize_text_field( $value );
530 - break;
531 - }
532 -
533 - // //error_log( '[mlsimport] Updated Options: ' . print_r( $options, true ) );
534 -
535 - // No changes
536 - if ( $options === $original_options ) {
537 - //error_log( '[mlsimport] No changes detected for option update.' );
538 - wp_send_json_success( array(
539 - 'message' => 'Field option saved successfully',
540 - 'fieldKey' => $field_key,
541 - 'optionType' => $option_type,
542 - 'value' => $value,
543 - ) );
544 - }
545 - //error_log('saving mls-fields '.count($options['mls-fields']));
546 - //error_log('saving mls-fields-admin '.count($options['mls-fields-admin']));
547 - //error_log('saving mls-fields-label '.count($options['mls-fields-label']));
548 - //error_log('saving mls-fields-map-postmeta '.count($options['mls-fields-map-postmeta']));
549 - //error_log('saving mls-fields-map-taxonomy '.count($options['mls-fields-map-taxonomy']));
550 - //error_log('saving field_order '.count($options['field_order']));
551 -
552 - // Save updated options
553 - $result = update_option( 'mlsimport_admin_fields_select', $options );
554 -
555 - if ( false !== $result ) {
556 - //error_log( '[mlsimport] Options updated successfully.' );
557 - wp_send_json_success( array(
558 - 'message' => 'Field option saved successfully',
559 - 'fieldKey' => $field_key,
560 - 'optionType' => $option_type,
561 - 'value' => $value,
562 - ) );
563 - } else {
564 - //error_log( '[mlsimport] Failed to update options.' );
565 - wp_send_json_error( 'Failed to save field option' );
566 - }
319 + wp_send_json_success( $result );
567 320 }
568 -
569 -
570 -
571 -function mlsimport_ajax_save_field_position() {
572 - // Verify nonce
573 - if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'mlsimport_field_selector_nonce')) {
574 - wp_send_json_error('Invalid security token');
575 - }
576 -
577 - // Validate required fields
578 - if (!isset($_POST['moving_index'], $_POST['target_index'], $_POST['position'])) {
579 - wp_send_json_error('Missing parameters');
580 - }
581 -
582 - $moving_index = intval($_POST['moving_index']);
583 - $target_index = intval($_POST['target_index']);
584 - $position = sanitize_text_field($_POST['position']); // 'before' or 'after'
585 -
586 - // Load current options
587 - $options = get_option('mlsimport_admin_fields_select', []);
588 -
589 - if (empty($options['field_order']) || !is_array($options['field_order'])) {
590 - wp_send_json_error('Field order not found');
591 - }
592 -
593 -
594 -
595 -
596 - // Normalize field_order to ensure correct index order
597 - $order_map = $options['field_order'];
598 -
599 - asort($order_map); // Sort by index
600 - $ordered_keys = array_keys($order_map);
601 -
602 - // Safety checks
603 - if (!isset($ordered_keys[$moving_index]) || !isset($ordered_keys[$target_index])) {
604 - wp_send_json_error('Invalid indexes');
605 - }
606 -
607 - $moving_key = $ordered_keys[$moving_index];
608 - $target_key = $ordered_keys[$target_index];
609 -
610 - // Remove moving key
611 - array_splice($ordered_keys, $moving_index, 1);
612 -
613 - // Adjust target index if needed
614 - if ($position === 'after' && $moving_index < $target_index) {
615 - $target_index--;
616 - }
617 -
618 - // Calculate new position
619 - $insert_index = ($position === 'before') ? $target_index : $target_index + 1;
620 -
621 - // Insert the field key at the new position
622 - array_splice($ordered_keys, $insert_index, 0, $moving_key);
623 -
624 - // Rebuild the new order map
625 - $new_order = [];
626 - foreach ($ordered_keys as $i => $key) {
627 - $new_order[$key] = $i;
628 - }
629 -
630 - // Save new field order
631 - $options['field_order'] = $new_order;
632 -
633 - // Apply order to all related field maps
634 - $targets = [
635 - 'mls-fields',
636 - 'mls-fields-map-taxonomy',
637 - 'mls-fields-map-postmeta',
638 - 'mls-fields-map-admin',
639 - 'mls-fields-map-label',
640 - ];
641 -
642 - foreach ($targets as $target) {
643 - if (!empty($options[$target]) && is_array($options[$target])) {
644 - $reordered = [];
645 -
646 - foreach ($new_order as $field_key => $index) {
647 - if (isset($options[$target][$field_key])) {
648 - $reordered[$field_key] = $options[$target][$field_key];
649 - }
650 - }
651 -
652 - // Preserve any extra keys not in field_order
653 - foreach ($options[$target] as $key => $value) {
654 - if (!isset($reordered[$key])) {
655 - $reordered[$key] = $value;
656 - }
657 - }
658 -
659 - $options[$target] = $reordered;
660 - }
661 - }
662 -
663 - // Save the updated options
664 - $saved = update_option('mlsimport_admin_fields_select', $options);
665 - if ( $saved ) {
666 - global $mlsimport;
667 - if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'enviroment_custom_fields' ) ) {
668 - $mlsimport->admin->env_data->enviroment_custom_fields( $mlsimport->get_plugin_name() );
669 - }
670 - }
671 -
672 - wp_send_json_success('Field order updated');
673 -}
674 -
675 -// Register the AJAX handler
676 -add_action('wp_ajax_mlsimport_save_field_position', 'mlsimport_ajax_save_field_position');
321 +add_action( 'wp_ajax_mlsimport_change_field_configuration', 'mlsimport_ajax_change_field_configuration' );