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.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 / enviroment / ResoBase.php

ResoBase.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.2.1, at enviroment/ResoBase.php

450 lines 13.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ResoBase — base class for the RESO-standard MLS provider adapters.
4 *
5 * Provider adapters in the enviroment/ directory extend this class. It defines
6 * only the public behavior that is identical for every supported Provider
7 * Family. Provider-specific credentials and request rules stay in subclasses.
8 *
9 * @package MLSImport
10 */
11 // Abort if the file is accessed directly outside of WordPress.
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly
14 }
15
16 /*
17 * To change this license header, choose License Headers in Project Properties.
18 * To change this template file, choose Tools | Templates
19 * and open the template in the editor.
20 */
21
22 /**
23 * Description of ResoBase
24 *
25 * @author cretu
26 */
27 class ResoBase {
28
29 /** @var string Stable provider type stored with the MLS configuration. */
30 protected $provider_type = '';
31
32 /** @var string[] Saved option keys required to authenticate this provider. */
33 protected $provider_credential_fields = array( 'mlsimport_mls_token' );
34
35 /** @var array<string,string> Saved option key to SaaS request key aliases. */
36 protected $connection_payload_keys = array(
37 'mlsimport_mls_token' => 'mls_token',
38 );
39
40 /** @var bool Whether this provider's Direct MLS request is implemented. */
41 protected $direct_access_supported = true;
42
43 /** @var bool Whether Direct MLS sends the saved provider token unchanged. */
44 protected $direct_uses_stored_token = false;
45
46 /** @var object|null Active theme importer used by Stored mode. */
47 public $theme_importer;
48
49 /**
50 * Keep the active theme importer for callers that save Stored-mode listings.
51 *
52 * @param object|null $theme_importer Active theme importer, or null in pure tests.
53 */
54 public function __construct( $theme_importer = null ) {
55 $this->theme_importer = $theme_importer;
56 }
57
58 /**
59 * Return the stable saved type owned by this adapter.
60 *
61 * @return string
62 */
63 public function type() {
64 return $this->provider_type;
65 }
66
67 /**
68 * Real provider adapters are usable. UnsupportedResoClass overrides this.
69 *
70 * @return bool
71 */
72 public function supported() {
73 return true;
74 }
75
76 /**
77 * Return the exact saved option keys required by this Provider Family.
78 *
79 * The settings screen and connection test both consume this list, preventing
80 * either caller from maintaining a second provider credential map.
81 *
82 * @return string[] Ordered credential option keys.
83 */
84 public function credential_fields() {
85 return $this->provider_credential_fields;
86 }
87
88 /**
89 * Build the safe payload sent to the SaaS connection-test endpoint.
90 *
91 * Step 1 adds the selected MLS ID. Step 2 reads only the credential keys
92 * declared by this adapter. Step 3 trims their values and reports any blank
93 * required keys. No inactive provider credential can enter the payload.
94 *
95 * @param array $saved_options Saved plugin settings for every provider.
96 * @param int|string $mls_id Selected numeric MLS identifier.
97 * @return array{success:bool,payload:array,missing:array,error:?array}
98 */
99 public function connection_test_payload( array $saved_options, $mls_id ) {
100 $payload = array( 'mls_id' => trim( (string) $mls_id ) );
101 $missing = array();
102
103 // Copy only this adapter's declared credentials into the outgoing request.
104 foreach ( $this->credential_fields() as $field ) {
105 $value = isset( $saved_options[ $field ] )
106 ? trim( (string) $saved_options[ $field ] )
107 : '';
108
109 if ( '' === $value ) {
110 $missing[] = $field;
111 continue;
112 }
113
114 $request_key = isset( $this->connection_payload_keys[ $field ] )
115 ? $this->connection_payload_keys[ $field ]
116 : $field;
117 $payload[ $request_key ] = $value;
118 }
119
120 return array(
121 'success' => array() === $missing,
122 'payload' => $payload,
123 'missing' => $missing,
124 'error' => array() === $missing
125 ? null
126 : array(
127 'code' => 'missing_credentials',
128 'message' => 'Required MLS provider credentials are missing.',
129 ),
130 );
131 }
132
133 /**
134 * Apply this provider's WordPress-side rules to a Stored-mode request.
135 *
136 * Shared request fields arrive fully assembled by the Import Task caller.
137 * This adapter adds only the provider-specific timestamp here; adapters with
138 * further requirements override this method and then return the same outcome.
139 *
140 * @param array $arguments Shared Stored-mode request arguments.
141 * @param string $last_date Last Successful Sync Time, or an empty string.
142 * @return array{success:bool,arguments:array,error:?array}
143 */
144 public function prepare_stored_request( array $arguments, $last_date = '' ) {
145 if ( '' !== trim( (string) $last_date ) ) {
146 $arguments['modification_time'] = $this->format_stored_timestamp( $last_date );
147 }
148
149 return array(
150 'success' => true,
151 'arguments' => $arguments,
152 'error' => null,
153 );
154 }
155
156 /**
157 * Prepare the Import Task field catalog for this Provider Family.
158 *
159 * Most providers use the shared fields unchanged. An adapter overrides this
160 * method only when its MLS API requires a different input shape.
161 *
162 * @param array $fields Import Task fields keyed by RESO field name.
163 * @return array Provider-ready Import Task fields.
164 */
165 public function prepare_import_task_fields( array $fields ) {
166 return $fields;
167 }
168
169 /**
170 * Keep timestamps unchanged for providers with no WordPress-side rule.
171 *
172 * @param string $value Saved sync timestamp.
173 * @return string
174 */
175 protected function format_stored_timestamp( $value ) {
176 return trim( (string) $value );
177 }
178
179 /**
180 * Convert a saved timestamp to one exact UTC shape.
181 *
182 * @param string $value Saved sync timestamp.
183 * @param bool $milliseconds Whether to include `.000`.
184 * @param bool $utc_suffix Whether to append the UTC `Z` suffix.
185 * @return string Formatted timestamp, or the trimmed input when invalid.
186 */
187 protected function format_utc_timestamp( $value, $milliseconds, $utc_suffix ) {
188 $clean = trim( (string) $value );
189
190 try {
191 $date = new DateTimeImmutable( $clean, new DateTimeZone( 'UTC' ) );
192 } catch ( Exception $exception ) {
193 return $clean;
194 }
195
196 $format = 'Y-m-d\TH:i:s';
197 if ( $milliseconds ) {
198 $format .= '.000';
199 }
200 if ( $utc_suffix ) {
201 $format .= '\Z';
202 }
203
204 return $date->setTimezone( new DateTimeZone( 'UTC' ) )->format( $format );
205 }
206
207 /**
208 * Supported adapters have no provider-selection error.
209 *
210 * @return null
211 */
212 public function error() {
213 return null;
214 }
215
216 /**
217 * Report whether this adapter can execute Direct MLS requests today.
218 *
219 * @return bool
220 */
221 public function supports_direct_access() {
222 return $this->direct_access_supported;
223 }
224
225 /**
226 * Build this provider's Direct MLS query through the shared OData encoder.
227 *
228 * The encoder knows filter syntax only. The selected adapter supplies every
229 * provider decision in direct_query_rules(), so the helper never switches on
230 * provider type.
231 *
232 * @param array $params Standalone filter and paging values.
233 * @param array $config Per-MLS URLs, expand value, and field_corellation map.
234 * @return string Query beginning with `?`, or empty when Direct MLS is unsupported.
235 */
236 public function build_direct_query( array $params, array $config ) {
237 if ( ! $this->supports_direct_access() ) {
238 return '';
239 }
240
241 return mlsimport_live_build_query_odata( $params, $config, $this->direct_query_rules() );
242 }
243
244 /**
245 * Build the OData query for one exact ListingKey.
246 *
247 * @param string $listing_key RESO ListingKey; never cast to an integer.
248 * @param array $config Per-MLS config including expand/field_corellation.
249 * @return string Query beginning with `?`.
250 */
251 public function build_direct_get_query( $listing_key, array $config ) {
252 $query = '?';
253 if ( ! empty( $config['expand'] ) ) {
254 $query .= '&$expand=' . $config['expand'];
255 }
256 $query .= '&$top=1&$filter=' . mlsimport_live_filter_list_segment(
257 mlsimport_live_field_alias( 'ListingKey', $config ),
258 array( (string) $listing_key )
259 );
260
261 return (string) preg_replace( '/ and $/', '', $query );
262 }
263
264 /**
265 * Return OData choices that are truly identical for this provider.
266 *
267 * @return array<string,bool|int|string>
268 */
269 protected function direct_query_rules() {
270 return array();
271 }
272
273 /**
274 * Build the provider-owned Direct MLS authentication plan.
275 *
276 * The plan contains no inactive credentials. Token providers return their
277 * saved bearer. Expiring-token providers return the exact POST description
278 * their adapter owns; the WordPress HTTP caller only executes the plan.
279 *
280 * @param array $saved_options Saved plugin credential options.
281 * @param array $config Per-MLS config, including provider token URL.
282 * @return array Authentication plan or stable failure result.
283 */
284 public function direct_auth_plan( array $saved_options, array $config ) {
285 if ( ! $this->supports_direct_access() ) {
286 $result = $this->direct_unsupported_result();
287 $result['mode'] = null;
288 return $result;
289 }
290
291 $missing = array();
292 foreach ( $this->credential_fields() as $field ) {
293 if ( ! isset( $saved_options[ $field ] ) || '' === trim( (string) $saved_options[ $field ] ) ) {
294 $missing[] = $field;
295 }
296 }
297 if ( array() !== $missing ) {
298 return array(
299 'success' => false,
300 'mode' => null,
301 'missing' => $missing,
302 'error' => array(
303 'code' => 'missing_credentials',
304 'message' => 'Required MLS provider credentials are missing.',
305 ),
306 );
307 }
308
309 if ( $this->direct_uses_stored_token ) {
310 return array(
311 'success' => true,
312 'mode' => 'stored_token',
313 'token' => trim( (string) $saved_options['mlsimport_mls_token'] ),
314 'error' => null,
315 );
316 }
317
318 $request = $this->direct_token_request( $saved_options, $config );
319 if ( null === $request ) {
320 return array(
321 'success' => false,
322 'mode' => null,
323 'error' => array(
324 'code' => 'login_configuration_missing',
325 'message' => 'The MLS login configuration is incomplete.',
326 ),
327 );
328 }
329
330 return array(
331 'success' => true,
332 'mode' => 'token_request',
333 'request' => $request,
334 'error' => null,
335 );
336 }
337
338 /**
339 * Return an expiring-token request, or null for token-as-is providers.
340 *
341 * @param array $saved_options Active provider credentials.
342 * @param array $config Per-MLS configuration.
343 * @return array|null
344 */
345 protected function direct_token_request( array $saved_options, array $config ) {
346 return null;
347 }
348
349 /**
350 * Return the standard result for a provider not yet ported to Direct MLS.
351 *
352 * @return array{success:bool,records:array,total:int,error:array}
353 */
354 public function direct_unsupported_result() {
355 return array(
356 'success' => false,
357 'records' => array(),
358 'total' => 0,
359 'error' => array(
360 'code' => 'unsupported_direct_provider',
361 'message' => 'Direct MLS access is not supported for this provider.',
362 ),
363 );
364 }
365
366 /**
367 * Read one Direct MLS HTTP response into a Provider Request Outcome.
368 *
369 * Status is classified before the body is parsed: authentication failures,
370 * rejected queries, and provider/server failures receive distinct safe codes.
371 * A valid envelope with zero records remains a successful outcome.
372 *
373 * @param int $status_code HTTP response status.
374 * @param string $body Raw provider response body.
375 * @return array{success:bool,records:array,total:int,error:?array,warning:?array}
376 */
377 public function read_direct_response( $status_code, $body ) {
378 $status_code = (int) $status_code;
379 if ( 401 === $status_code || 403 === $status_code ) {
380 return $this->direct_failure( 'login_failure', 'The MLS rejected the saved credentials.' );
381 }
382 if ( $status_code >= 400 && $status_code < 500 ) {
383 return $this->direct_failure( 'rejected_query', 'The MLS rejected the listings query.' );
384 }
385 if ( $status_code < 200 || $status_code >= 300 ) {
386 return $this->direct_failure( 'request_failed', 'The MLS request could not be completed.' );
387 }
388
389 $parsed = mlsimport_live_parse_response( (string) $body, array() );
390 if ( null === $parsed ) {
391 return $this->direct_failure( 'invalid_response', 'The MLS returned an invalid listings response.' );
392 }
393
394 return array(
395 'success' => true,
396 'records' => $parsed['records'],
397 'total' => $parsed['total'],
398 'error' => null,
399 'warning' => null,
400 );
401 }
402
403 /**
404 * Return the Direct MLS listing endpoint for this provider.
405 *
406 * @param array $config Per-MLS configuration.
407 * @return string
408 */
409 public function direct_endpoint( array $config ) {
410 return isset( $config['api_import_url'] ) ? (string) $config['api_import_url'] : '';
411 }
412
413 /**
414 * Allow a provider to add separate media after listings succeed.
415 *
416 * Most providers return media with the listing response, so their default is
417 * a no-op. Bright MLS overrides this because it owns a second media request.
418 *
419 * @param array $outcome Successful Provider Request Outcome.
420 * @param array $config Per-MLS configuration.
421 * @param array $headers Authorization headers used for listings.
422 * @param callable|null $http_get Optional fake HTTP getter for tests.
423 * @return array Provider Request Outcome, possibly with media or a warning.
424 */
425 public function enrich_direct_media( array $outcome, array $config, array $headers, $http_get = null ) {
426 return $outcome;
427 }
428
429 /**
430 * Build one failed Provider Request Outcome without exposing provider data.
431 *
432 * @param string $code Stable machine-readable failure code.
433 * @param string $message Short safe administrator message.
434 * @return array{success:bool,records:array,total:int,error:array,warning:null}
435 */
436 protected function direct_failure( $code, $message ) {
437 return array(
438 'success' => false,
439 'records' => array(),
440 'total' => 0,
441 'error' => array(
442 'code' => (string) $code,
443 'message' => (string) $message,
444 ),
445 'warning' => null,
446 );
447 }
448
449 }
450