PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.1.2
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.1.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 6.0.5 All 36 releases
mlsimport / includes / mlsimport-progressive-save.php

mlsimport-progressive-save.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.1.2, at includes/mlsimport-progressive-save.php

271 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WordPress persistence adapter for the Field Configuration module.
4 *
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.
11 *
12 * @package MLSImport
13 */
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * Decode current 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.
25 *
26 * @return array Current MLS metadata keyed by RESO field name.
27 */
28 function mlsimport_field_configuration_metadata(): array {
29 $metadata = get_option( 'mlsimport_mls_metadata_mls_data', '' );
30 $metadata = is_string( $metadata ) ? json_decode( $metadata, true ) : $metadata;
31
32 return is_array( $metadata ) ? $metadata : array();
33 }
34
35 /**
36 * Return taxonomy destinations registered for the active property post type.
37 *
38 * The adapter is resolved on demand because metadata gathering and AJAX calls
39 * run after the plugin has created its active theme environment.
40 *
41 * @return array Taxonomy slug-to-label map accepted by mutation validation.
42 */
43 function mlsimport_field_configuration_taxonomies(): array {
44 global $mlsimport;
45
46 $post_type = '';
47 if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'get_property_post_type' ) ) {
48 $post_type = $mlsimport->admin->env_data->get_property_post_type();
49 }
50
51 $taxonomies = mlsimport_get_custom_post_type_taxonomies( $post_type );
52
53 return is_array( $taxonomies ) ? $taxonomies : array();
54 }
55
56 /**
57 * Atomically replace the one Field Configuration option.
58 *
59 * WordPress's update_option() has no expected-value condition, so two tabs can
60 * both pass an application-level revision check and the slower request can
61 * overwrite the newer one. The UPDATE below includes the exact serialized old
62 * value in its WHERE clause. One request wins; the other updates zero rows and
63 * is reported by the module as stale. Cache and public option hooks are updated
64 * once only after the database confirms the replacement.
65 *
66 * @param array $expected Exact option value previously loaded.
67 * @param array $replacement Complete normalized replacement.
68 * @return bool True only when this caller won the compare-and-swap.
69 */
70 function mlsimport_field_configuration_compare_and_swap( array $expected, array $replacement ): bool {
71 global $wpdb;
72
73 $option_name = 'mlsimport_admin_fields_select';
74 $row = $wpdb->get_row(
75 $wpdb->prepare(
76 "SELECT option_value, autoload FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
77 $option_name
78 ),
79 ARRAY_A
80 );
81
82 // add_option() uses the option_name unique key as the atomic first-write gate.
83 if ( null === $row ) {
84 if ( array() !== $expected ) {
85 return false;
86 }
87
88 $added = add_option( $option_name, $replacement, '', false );
89 if ( ! $added ) {
90 wp_cache_delete( $option_name, 'options' );
91 wp_cache_delete( 'alloptions', 'options' );
92 wp_cache_delete( 'notoptions', 'options' );
93 } elseif ( function_exists( 'mlsimport_active_field_configuration' ) ) {
94 mlsimport_active_field_configuration( true );
95 }
96
97 return $added;
98 }
99
100 $old_serialized = maybe_serialize( $expected );
101 $new_serialized = maybe_serialize( $replacement );
102 $updated = $wpdb->query(
103 $wpdb->prepare(
104 "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND BINARY option_value = BINARY %s",
105 $new_serialized,
106 $option_name,
107 $old_serialized
108 )
109 );
110 if ( 1 !== $updated ) {
111 // Another request won after this process loaded its option. Discard every
112 // local option-cache route so the module can report the winner's revision.
113 wp_cache_delete( $option_name, 'options' );
114 wp_cache_delete( 'alloptions', 'options' );
115 wp_cache_delete( 'notoptions', 'options' );
116 return false;
117 }
118
119 // Mirror update_option() cache behavior while preserving the existing
120 // autoload choice. Large 1,000-field configurations are not newly autoloaded.
121 $alloptions = wp_load_alloptions( true );
122 if ( isset( $alloptions[ $option_name ] ) ) {
123 $alloptions[ $option_name ] = $new_serialized;
124 wp_cache_set( 'alloptions', $alloptions, 'options' );
125 } else {
126 wp_cache_set( $option_name, $new_serialized, 'options' );
127 }
128
129 mlsimport_active_field_configuration( true );
130 do_action( "update_option_{$option_name}", $expected, $replacement, $option_name );
131 do_action( 'updated_option', $option_name, $expected, $replacement );
132
133 return true;
134 }
135
136 /**
137 * Construct the authoritative module around the WordPress option store.
138 *
139 * The loader always returns an array and the writer delegates to the exact
140 * compare-and-swap adapter above. Each request gets a small stateless service;
141 * all durable state remains in the one backward-compatible WordPress option.
142 *
143 * @return Mlsimport_Field_Configuration Configured domain service.
144 */
145 function mlsimport_field_configuration(): Mlsimport_Field_Configuration {
146 return new Mlsimport_Field_Configuration(
147 static function () {
148 $value = get_option( 'mlsimport_admin_fields_select', array() );
149 return is_array( $value ) ? $value : array();
150 },
151 'mlsimport_field_configuration_compare_and_swap'
152 );
153 }
154
155 /**
156 * Return normalized durable state for consumers that must remove dormant data.
157 *
158 * Theme custom-field registries need both active fields to add and dormant
159 * fields to remove from theme-owned display definitions. This read still enters
160 * through the module, preserving normalization and taxonomy rules without
161 * exposing a direct option read as an alternate persistence boundary.
162 *
163 * @return array Normalized active-and-dormant Field Configuration.
164 */
165 function mlsimport_normalized_field_configuration(): array {
166 return mlsimport_field_configuration()->read(
167 mlsimport_field_configuration_metadata(),
168 mlsimport_hardocde_theme_schema(),
169 mlsimport_field_configuration_taxonomies()
170 );
171 }
172
173 /**
174 * Return the active-only configuration for import and display consumers.
175 *
176 * The durable option retains Dormant MLS Fields so their choices can return.
177 * Runtime consumers use this projection to exclude those fields consistently
178 * without each theme reimplementing metadata intersection and array ordering.
179 *
180 * The projection is cached for the request because import adapters consult it
181 * once per listing. Rebuilding and sorting 1,000 parallel fields for every
182 * listing would turn schema safety into an avoidable import bottleneck.
183 *
184 * @param bool $refresh Rebuild after this request has changed the option.
185 * @return array Normalized configuration containing current metadata fields only.
186 */
187 function mlsimport_active_field_configuration( bool $refresh = false ): array {
188 static $configuration = null;
189
190 if ( null !== $configuration && ! $refresh ) {
191 return $configuration;
192 }
193
194 $configuration = mlsimport_field_configuration()->read_active(
195 mlsimport_field_configuration_metadata(),
196 mlsimport_hardocde_theme_schema(),
197 mlsimport_field_configuration_taxonomies()
198 );
199
200 return $configuration;
201 }
202
203 /**
204 * Persist metadata initialization/reconciliation once on the server.
205 *
206 * @param array $metadata Newly gathered MLS metadata.
207 * @param array $theme_schema Active theme defaults.
208 * @return array Field Configuration Result.
209 */
210 function mlsimport_reconcile_field_configuration( array $metadata, array $theme_schema ): array {
211 return mlsimport_field_configuration()->reconcile( $metadata, $theme_schema, mlsimport_field_configuration_taxonomies() );
212 }
213
214 /**
215 * Import an exported configuration through the same schema and storage owner.
216 *
217 * @param array $incoming Exported legacy-compatible option array.
218 * @return array Field Configuration Result.
219 */
220 function mlsimport_import_field_configuration( array $incoming ): array {
221 return mlsimport_field_configuration()->import_configuration(
222 $incoming,
223 mlsimport_field_configuration_metadata(),
224 mlsimport_hardocde_theme_schema(),
225 mlsimport_field_configuration_taxonomies()
226 );
227 }
228
229 /**
230 * Handle the sole browser Field Configuration mutation endpoint.
231 *
232 * Security and request-shape validation happen before decoding the compact
233 * command. The module then validates domain rules and either returns an
234 * authoritative saved result or a stable error code used by the queue UI.
235 *
236 * The handler terminates through WordPress JSON helpers. It intentionally has
237 * no return type because those helpers stop execution after sending a response.
238 */
239 function mlsimport_ajax_change_field_configuration() {
240 check_ajax_referer( 'mlsimport_field_selector_nonce', 'security' );
241
242 if ( ! current_user_can( 'manage_options' ) ) {
243 wp_send_json_error( array( 'error' => array( 'code' => 'forbidden', 'message' => 'You are not allowed to change Field Configuration.' ) ), 403 );
244 }
245
246 if ( ! isset( $_POST['revision'], $_POST['command'] ) || ! is_scalar( $_POST['revision'] ) || ! is_scalar( $_POST['command'] ) ) {
247 wp_send_json_error( array( 'error' => array( 'code' => 'invalid_request', 'message' => 'Revision and command are required.' ) ), 400 );
248 }
249
250 $revision = max( 0, (int) wp_unslash( $_POST['revision'] ) );
251 $command = json_decode( wp_unslash( (string) $_POST['command'] ), true );
252 if ( ! is_array( $command ) ) {
253 wp_send_json_error( array( 'error' => array( 'code' => 'invalid_json', 'message' => 'The Field Configuration command is not valid JSON.' ) ), 400 );
254 }
255
256 $result = mlsimport_field_configuration()->change(
257 $revision,
258 $command,
259 mlsimport_field_configuration_metadata(),
260 mlsimport_field_configuration_taxonomies(),
261 mlsimport_hardocde_theme_schema()
262 );
263 if ( ! $result['success'] ) {
264 $status = 'stale_revision' === $result['error']['code'] ? 409 : ( 'persistence_failed' === $result['error']['code'] ? 500 : 422 );
265 wp_send_json_error( $result, $status );
266 }
267
268 wp_send_json_success( $result );
269 }
270 add_action( 'wp_ajax_mlsimport_change_field_configuration', 'mlsimport_ajax_change_field_configuration' );
271