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
mlsimport / includes / mlsimport-metadata-gather.php

mlsimport-metadata-gather.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.2.1, at includes/mlsimport-metadata-gather.php

160 lines 7.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Connection-scoped MLS metadata gathering (issues #275/#281).
4 *
5 * WHY THIS FILE EXISTS
6 * --------------------
7 * Gathering MLS metadata (theme schema + field data + enums from GET clients)
8 * and reconciling the Field Configuration from it used to live only inside
9 * the admin AJAX handler, hard-bound to the CURRENT connection. The Add-MLS
10 * drawer (#281) must run the exact same gather for the connection it just
11 * created — that is how the new connection's field mapping ends up
12 * pre-seeded from theme defaults (decision #264: reconcile auto-enables
13 * theme-schema fields it has never seen on the first metadata gather).
14 *
15 * This file holds that single shared gather core, scoped by mls_id:
16 *
17 * - mls_id 0 / the current connection => exactly the historic behavior,
18 * including remembering the authoritative provider type globally;
19 * - any other registered connection => the same gather, but every write
20 * lands in that connection's "_{mls_id}"-suffixed options and NO global
21 * current-connection state (saved provider type) is touched.
22 *
23 * The admin AJAX handler (mlsimport_saas_get_metadata_function) is now a
24 * thin nonce/capability wrapper around this function.
25 *
26 * @since 7.2.0
27 * @package Mlsimport
28 */
29
30 if ( ! defined( 'ABSPATH' ) ) {
31 exit;
32 }
33
34 /**
35 * Gather MLS metadata from the SaaS and reconcile one connection's mapping.
36 *
37 * Step by step:
38 * 1. GET clients?theme_id=<configured theme> from the SaaS.
39 * 2. Shape guard: a failed request returns none of the metadata keys — STOP
40 * before touching anything, so a bad response can never overwrite good
41 * cached metadata or mark the site populated with an empty field list.
42 * 3. Refresh the connection registry from mls_entitlements when the SaaS
43 * sends it (#276); a legacy response without it changes nothing.
44 * 4. ONLY for the current connection: remember the authoritative provider
45 * type globally (the saved-type pair belongs to the current MLS — writing
46 * it for another connection would orphan the current one's saved type).
47 * 5. Persist: the theme schema stays GLOBAL (decision #263); the two MLS
48 * metadata blobs are stored per-connection (#275).
49 * 6. Reconcile the target connection's Field Configuration against the fresh
50 * metadata + theme schema. New metadata fields that exist in the theme
51 * schema are auto-enabled — this IS the "pre-seeded from theme defaults"
52 * behavior (#264). A reconcile failure clears the populated flag so the
53 * next page load retries.
54 * 7. Mark the connection's metadata as populated.
55 *
56 * @param int $mls_id Connection to gather for; 0 = the current connection.
57 * @return array{success:bool, code:string, message:string, detail:string,
58 * result:array, revision:int} Stable outcome for callers:
59 * code is '' on success, 'request_failed' (step 2) or
60 * 'reconcile_failed' (step 6) otherwise; result carries the raw
61 * reconcile result for the reconcile_failed case.
62 */
63 function mlsimport_gather_connection_metadata( int $mls_id = 0 ): array {
64 $theme_start = new ThemeImport();
65
66 // Step 1: GET the theme schema + MLS metadata for the configured theme,
67 // NAMING the MLS the gather is for (#293, closing the #268 gap): the
68 // legacy unscoped GET answered for whatever MLS the account record
69 // happened to sit on, which could seed this connection's slot with
70 // another MLS's metadata. mls_id 0 resolves to the current connection;
71 // only a site with no connection at all still sends the legacy URL.
72 $options = get_option( 'mlsimport_admin_options' );
73 $options = is_array( $options ) ? $options : array();
74 $target = $mls_id > 0 ? $mls_id : mlsimport_current_mls_id();
75 $url = 'clients?theme_id=' . intval( $options['mlsimport_theme_used'] ?? 0 );
76 if ( $target > 0 ) {
77 $url .= '&mls_id=' . $target;
78 }
79 $answer = $theme_start::globalApiRequestSaas( $url, array(), 'GET' );
80
81 // Step 2: refuse a response without the full metadata shape, untouched.
82 if ( ! is_array( $answer ) || ! isset( $answer['theme_schema'], $answer['mls_data']['mls_meta_data'], $answer['mls_data']['mls_meta_enums'] ) ) {
83 return array(
84 'success' => false,
85 'code' => 'request_failed',
86 'message' => esc_html__( 'Gathering MLS metadata failed. Nothing was changed - it will retry on the next page load.', 'mlsimport' ),
87 'detail' => is_array( $answer ) && isset( $answer['error_message'] ) ? $answer['error_message'] : '',
88 'result' => array(),
89 'revision' => 0,
90 );
91 }
92
93 // Step 2b (#293): a scoped request must get a CONFIRMED answer. The API
94 // echoes mls_id on every scoped reply (#292), so anything else — a
95 // mismatched echo, or no echo at all — means the answer cannot be
96 // attributed to this connection. Saving it would poison this connection's
97 // metadata slot, so refuse before ANY write (fail-closed, #279/#285).
98 // A request that named no MLS (a site with no connection yet) has nothing
99 // to confirm and keeps the legacy unscoped behavior.
100 if ( $target > 0 && ! mlsimport_mls_scoped_echo_ok( $answer, $target ) ) {
101 return array(
102 'success' => false,
103 'code' => 'wrong_mls',
104 'message' => esc_html__( 'The metadata service answered for a different MLS. Nothing was saved - please try again.', 'mlsimport' ),
105 'detail' => 'asked ' . $target . ', got ' . ( isset( $answer['mls_id'] ) ? intval( $answer['mls_id'] ) : 'no mls_id' ),
106 'result' => array(),
107 'revision' => 0,
108 );
109 }
110
111 // Step 3: refresh the registry (cap + matching records) from the response.
112 mlsimport_apply_entitlements( $answer );
113
114 // Step 4: the global saved-type pair belongs to the CURRENT connection
115 // only. A scoped gather for another connection must not overwrite it.
116 $is_current = 0 === $mls_id || mlsimport_current_mls_id() === $mls_id;
117 if ( $is_current && isset( $answer['mls_data']['type'], $options['mlsimport_mls_name'] ) ) {
118 Mlsimport_Provider_Family::remember_type(
119 $answer['mls_data']['type'],
120 $options['mlsimport_mls_name']
121 );
122 }
123
124 // Step 5: cache metadata — theme schema global, MLS blobs per-connection.
125 update_option( 'mlsimport_mls_metadata_theme_schema', $answer['theme_schema'] );
126 mlsimport_update_connection_option( 'mlsimport_mls_metadata_mls_data', $answer['mls_data']['mls_meta_data'], $mls_id );
127 mlsimport_update_connection_option( 'mlsimport_mls_metadata_mls_enums', $answer['mls_data']['mls_meta_enums'], $mls_id );
128
129 // Step 6: one server-side reconcile seeds/updates this connection's
130 // Field Configuration (theme-schema defaults auto-enable, #264).
131 $metadata = is_string( $answer['mls_data']['mls_meta_data'] )
132 ? json_decode( $answer['mls_data']['mls_meta_data'], true )
133 : $answer['mls_data']['mls_meta_data'];
134 $metadata = is_array( $metadata ) ? $metadata : array();
135 $result = mlsimport_reconcile_field_configuration( $metadata, mlsimport_hardocde_theme_schema(), $mls_id );
136 if ( ! $result['success'] ) {
137 mlsimport_delete_connection_option( 'mlsimport_mls_metadata_populated', $mls_id );
138 return array(
139 'success' => false,
140 'code' => 'reconcile_failed',
141 'message' => (string) ( $result['message'] ?? '' ),
142 'detail' => '',
143 'result' => $result,
144 'revision' => 0,
145 );
146 }
147
148 // Step 7: this connection's metadata is now gathered and reconciled.
149 mlsimport_update_connection_option( 'mlsimport_mls_metadata_populated', 'yes', $mls_id );
150
151 return array(
152 'success' => true,
153 'code' => '',
154 'message' => '',
155 'detail' => '',
156 'result' => $result,
157 'revision' => (int) $result['revision'],
158 );
159 }
160