PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.2
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.2
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 6.0.7 All 35 releases
mlsimport / includes / live / live-source.php

live-source.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.2, at includes/live/live-source.php

207 lines 6.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Direct MLS access: the read functions everything else calls.
4 *
5 * Only mlsimport_live_search() and mlsimport_live_get() talk HTTP to the
6 * MLS; both go through the cache. Query building and response parsing stay
7 * in the pure modules.
8 *
9 * @package Mlsimport
10 */
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 /**
17 * Search listings directly on the MLS.
18 *
19 * @param array $params Standalone filter params (city, status, limit, page, …).
20 * @return array{records:array,total:int}|null Null when the MLS is unreachable
21 * and no warm cache exists.
22 */
23 function mlsimport_live_search( array $params ) {
24 $config = mlsimport_live_config();
25 if ( array() === $config ) {
26 return null;
27 }
28
29 // Quantized bbox (~110m outward grid) BEFORE the query is built: nearby
30 // map pans produce the identical query and reuse one cache entry.
31 $params = mlsimport_live_quantize_bbox( $params );
32
33 $query = mlsimport_live_build_query( $params, $config );
34 $key = mlsimport_live_cache_key(
35 'search',
36 array(
37 'q' => $query,
38 'u' => $config['api_import_url'],
39 'm' => (string) ( $config['api_media_url'] ?? '' ),
40 )
41 );
42
43 return mlsimport_live_remember(
44 $key,
45 static function () use ( $query ) {
46 return mlsimport_live_request( $query );
47 }
48 );
49 }
50
51 /**
52 * Fetch one listing by ListingKey.
53 *
54 * @param string $listing_key RESO ListingKey (string — never cast to int).
55 * @return array|null The raw RESO record, or null when not found/unreachable.
56 */
57 function mlsimport_live_get( string $listing_key ) {
58 $listing_key = trim( $listing_key );
59 if ( '' === $listing_key ) {
60 return null;
61 }
62
63 $config = mlsimport_live_config();
64 if ( array() === $config ) {
65 return null;
66 }
67
68 $type = isset( $config['type'] ) ? (string) $config['type'] : '';
69 $provider = Mlsimport_Provider_Family::adapter( $type, $config['mls_id'] ?? 0 );
70 $query = $provider->build_direct_get_query( $listing_key, $config );
71
72 // 'q' keys the config shape too (dialect, $expand): a provider-type or
73 // expand change refetches instead of serving the stale-shaped record.
74 $key = mlsimport_live_cache_key(
75 'get',
76 array(
77 'k' => $listing_key,
78 'q' => $query,
79 'u' => $config['api_import_url'],
80 'm' => (string) ( $config['api_media_url'] ?? '' ),
81 )
82 );
83 $result = mlsimport_live_remember(
84 $key,
85 static function () use ( $query ) {
86 return mlsimport_live_request( $query );
87 }
88 );
89
90 return is_array( $result ) && ! empty( $result['records'][0] ) ? $result['records'][0] : null;
91 }
92
93 /**
94 * Fetch an explicit set of listings by ListingKey, in the given order.
95 *
96 * One search request (ListingKey list, no status baseline), reordered to the
97 * input order — keys the MLS no longer knows are simply absent.
98 *
99 * @param string[] $keys ListingKeys (strings — never cast to int).
100 * @return array[]|null Raw RESO records in input order, or null when the MLS
101 * is unreachable and no warm cache exists.
102 */
103 function mlsimport_live_keys( array $keys ) {
104 $keys = array_values( array_filter( array_map( 'trim', array_map( 'strval', $keys ) ), 'strlen' ) );
105 if ( array() === $keys ) {
106 return array();
107 }
108
109 $result = mlsimport_live_search(
110 array(
111 'keys' => $keys,
112 'limit' => count( $keys ),
113 )
114 );
115 if ( ! is_array( $result ) ) {
116 return null;
117 }
118
119 $by_key = array();
120 foreach ( $result['records'] as $record ) {
121 if ( is_array( $record ) && isset( $record['ListingKey'] ) ) {
122 $by_key[ (string) $record['ListingKey'] ] = $record;
123 }
124 }
125
126 $ordered = array();
127 foreach ( $keys as $key ) {
128 if ( isset( $by_key[ $key ] ) ) {
129 $ordered[] = $by_key[ $key ];
130 }
131 }
132 return $ordered;
133 }
134
135 /**
136 * The endpoint live requests hit. For Bridge, the configured OData URL is
137 * rewritten to the native listings API (…/OData/{ds}/Property →
138 * …/{ds}/listings): the native surface returns Media inline where OData
139 * hides it for client tokens (verified vs Stellar 2026-07-03).
140 *
141 * @param array $config Per-MLS config.
142 * @return string
143 */
144 function mlsimport_live_endpoint( array $config ): string {
145 $type = isset( $config['type'] ) ? (string) $config['type'] : '';
146 $provider = Mlsimport_Provider_Family::adapter( $type, $config['mls_id'] ?? 0 );
147 return $provider->direct_endpoint( $config );
148 }
149
150 /**
151 * One direct HTTP GET against the MLS import URL.
152 *
153 * @param string $query OData query string beginning with '?'.
154 * @return array{records:array,total:int}|null Null on transport/auth/parse failure.
155 */
156 function mlsimport_live_request( string $query ) {
157 $config = mlsimport_live_config();
158 $type = isset( $config['type'] ) ? (string) $config['type'] : '';
159 $provider = Mlsimport_Provider_Family::adapter( $type, $config['mls_id'] ?? 0 );
160 $headers = mlsimport_live_auth_headers();
161 if ( array() === $config || ! $provider->supports_direct_access() || array() === $headers ) {
162 return null;
163 }
164
165 $base = $provider->direct_endpoint( $config );
166 // The base URL may already carry a query part; keep exactly one '?'.
167 $url = false === strpos( $base, '?' )
168 ? $base . $query
169 : $base . '&' . ltrim( $query, '?&' );
170
171 // OData filters carry spaces and quotes; encode the ones URLs can't.
172 $url = str_replace( array( ' ', "'" ), array( '%20', '%27' ), $url );
173
174 $response = wp_remote_get(
175 $url,
176 array(
177 'timeout' => 20,
178 'headers' => $headers,
179 )
180 );
181
182 if ( is_wp_error( $response ) ) {
183 error_log( 'MLSImport Direct MLS request failed: request_failed' );
184 return null;
185 }
186
187 $outcome = $provider->read_direct_response(
188 (int) wp_remote_retrieve_response_code( $response ),
189 (string) wp_remote_retrieve_body( $response )
190 );
191 if ( empty( $outcome['success'] ) ) {
192 $code = isset( $outcome['error']['code'] ) ? $outcome['error']['code'] : 'request_failed';
193 error_log( 'MLSImport Direct MLS request failed: ' . $code );
194 return null;
195 }
196
197 $outcome = $provider->enrich_direct_media( $outcome, $config, $headers );
198 if ( ! empty( $outcome['warning']['code'] ) ) {
199 error_log( 'MLSImport Direct MLS warning: ' . $outcome['warning']['code'] );
200 }
201
202 return array(
203 'records' => $outcome['records'],
204 'total' => $outcome['total'],
205 );
206 }
207