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 | enviroment/ResoBase.php +422 -4 7.0.6 → 7.2.1 View file →
@@ -1,11 +1,11 @@
1 1 <?php
2 2 /**
3 3 * ResoBase — base class for the RESO-standard MLS provider adapters.
4 4 *
5 - * Provider adapters in the enviroment/ directory (SparkResoClass, TresleResoClass,
6 - * BridgeResoClass, MlsgridResoClass, etc.) extend this class. It currently defines no
7 - * shared behavior and serves as a common parent/type for the provider adapters.
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 8 *
9 9 * @package MLSImport
10 10 */
11 11 // Abort if the file is accessed directly outside of WordPress.
@@ -23,9 +23,427 @@
23 23 * Description of ResoBase
24 24 *
25 25 * @author cretu
26 26 */
27 -// Empty base class; provider adapters extend this for a shared type.
28 27 class ResoBase {
29 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 + }
30 448
31 449 }