| 1 |
<?php |
| 2 |
/** |
| 3 |
* Provider Family module and legacy compatibility helpers. |
| 4 |
* |
| 5 |
* Saved provider type is authoritative. Numeric MLS ranges remain only for old |
| 6 |
* configurations that do not yet have a saved type. |
| 7 |
*/ |
| 8 |
|
| 9 |
if ( ! defined( 'ABSPATH' ) ) { |
| 10 |
exit; // Exit if accessed directly |
| 11 |
} |
| 12 |
|
| 13 |
/** |
| 14 |
* Whether an mls_id belongs to a PropTx / AMPRE RESO Web API board (e.g. TRREB). |
| 15 |
* |
| 16 |
* PropTx boards are allocated the 9000-9999 id block, consistent with the |
| 17 |
* exclusive-upper 1000-wide blocks used by the other providers. |
| 18 |
* |
| 19 |
* @param int|string $mls_id |
| 20 |
* @return bool |
| 21 |
*/ |
| 22 |
function mlsimport_is_proptx_provider( $mls_id ) { |
| 23 |
// Coerce to int so numeric strings ("9001") compare by value. |
| 24 |
$mls_id_int = (int) $mls_id; |
| 25 |
// True only inside the 9000..9999 PropTx block (upper bound exclusive). |
| 26 |
return $mls_id_int >= 9000 && $mls_id_int < 10000; |
| 27 |
} |
| 28 |
|
| 29 |
/** |
| 30 |
* Normalise a stored last-import date into a full OData DateTimeOffset literal. |
| 31 |
* |
| 32 |
* The plugin stores the last-import marker as 'Y-m-d\TH:i' (e.g. 2026-07-01T00:00), |
| 33 |
* which RESO Web API providers such as AMPRE reject in a |
| 34 |
* $filter=ModificationTimestamp comparison. This produces a UTC literal with a Z |
| 35 |
* offset (e.g. 2026-07-01T00:00:00.000Z). An empty input is returned unchanged so |
| 36 |
* callers can keep treating '' as "no incremental marker". |
| 37 |
* |
| 38 |
* @param string $last_date |
| 39 |
* @return string |
| 40 |
*/ |
| 41 |
function mlsimport_format_odata_modification_time( $last_date ) { |
| 42 |
// Empty marker passes through unchanged ("no incremental marker"). |
| 43 |
if ( '' === $last_date ) { |
| 44 |
return ''; |
| 45 |
} |
| 46 |
|
| 47 |
// Parse the stored value as UTC, then re-emit as a full DateTimeOffset (Z) literal. |
| 48 |
$date_time = new DateTime( $last_date, new DateTimeZone( 'UTC' ) ); |
| 49 |
return $date_time->format( 'Y-m-d\TH:i:s.000\Z' ); |
| 50 |
} |
| 51 |
|
| 52 |
/** |
| 53 |
* Public entry point for selecting one Provider Family adapter. |
| 54 |
* |
| 55 |
* Callers give this module the provider type saved with the MLS configuration |
| 56 |
* and the numeric MLS ID. The saved type is checked first so old numeric ranges |
| 57 |
* cannot override authoritative provider data. |
| 58 |
*/ |
| 59 |
class Mlsimport_Provider_Family { |
| 60 |
|
| 61 |
/** |
| 62 |
* Return the adapter selected by saved provider type. |
| 63 |
* |
| 64 |
* @param string $saved_type Provider type saved with the MLS configuration. |
| 65 |
* @param int|string $mls_id Numeric MLS identifier used only as fallback. |
| 66 |
* @param object|null $theme_importer Active theme importer when WordPress supplies one. |
| 67 |
* @return ResoBase Selected provider adapter. |
| 68 |
*/ |
| 69 |
public static function adapter( $saved_type, $mls_id, $theme_importer = null ) { |
| 70 |
// Normalize the saved value once before choosing the provider class. |
| 71 |
$type = strtolower( trim( (string) $saved_type ) ); |
| 72 |
$id = (int) $mls_id; |
| 73 |
|
| 74 |
// Only a missing saved type may use the old numeric-ID compatibility map. |
| 75 |
if ( '' === $type ) { |
| 76 |
$type = self::type_from_mls_id( $id ); |
| 77 |
} |
| 78 |
|
| 79 |
// The only provider-name map lives at this public module boundary. Each |
| 80 |
// mapped class owns its own behavior; callers never branch on these names. |
| 81 |
$classes = array( |
| 82 |
'bridge' => 'BridgeResoClass', |
| 83 |
'spark' => 'SparkResoClass', |
| 84 |
'trestle' => 'TresleResoClass', |
| 85 |
'mlsgrid' => 'MlsgridResoClass', |
| 86 |
'rmls' => 'RmlsResoClass', |
| 87 |
'utah_real_estate'=> 'UtahRealEstateResoClass', |
| 88 |
'realcomp' => 'RealcompResoClass', |
| 89 |
'realtorca' => 'RealtorCaResoClass', |
| 90 |
'brightmls' => 'BrightMlsResoClass', |
| 91 |
'rapattoni' => 'RapattoniResoClass', |
| 92 |
'paragon' => 'ParagonResoClass', |
| 93 |
'connectmls' => 'ConnectMlsResoClass', |
| 94 |
'proptx' => 'ProptxResoClass', |
| 95 |
'centris' => 'CentrisResoClass', |
| 96 |
); |
| 97 |
|
| 98 |
// A present recognized type is authoritative, regardless of numeric ID. |
| 99 |
if ( isset( $classes[ $type ] ) ) { |
| 100 |
$class_name = $classes[ $type ]; |
| 101 |
return new $class_name( $theme_importer ); |
| 102 |
} |
| 103 |
|
| 104 |
// Never turn a present but unknown type into Bridge. Return a normal |
| 105 |
// adapter-shaped error so admin and request callers handle it identically. |
| 106 |
return new UnsupportedResoClass( $type ); |
| 107 |
} |
| 108 |
|
| 109 |
/** |
| 110 |
* Return the saved provider type only when it belongs to the selected MLS. |
| 111 |
* |
| 112 |
* Keeping the MLS ID beside the type prevents a provider saved for the prior |
| 113 |
* selection from winning during the first request after an MLS change. |
| 114 |
* |
| 115 |
* @param int|string $mls_id Currently selected MLS identifier. |
| 116 |
* @return string Saved provider type, or empty when absent/stale. |
| 117 |
*/ |
| 118 |
public static function saved_type( $mls_id ) { |
| 119 |
$saved_mls_id = (string) get_option( 'mlsimport_provider_type_mls_id', '' ); |
| 120 |
if ( '' === $saved_mls_id || $saved_mls_id !== (string) $mls_id ) { |
| 121 |
return ''; |
| 122 |
} |
| 123 |
|
| 124 |
return strtolower( trim( (string) get_option( 'mlsimport_provider_type', '' ) ) ); |
| 125 |
} |
| 126 |
|
| 127 |
/** |
| 128 |
* Save the authoritative provider type returned for one MLS configuration. |
| 129 |
* |
| 130 |
* @param string $type Provider type returned by the SaaS configuration. |
| 131 |
* @param int|string $mls_id MLS identifier that owns the type. |
| 132 |
* @return void |
| 133 |
*/ |
| 134 |
public static function remember_type( $type, $mls_id ) { |
| 135 |
$clean_type = strtolower( trim( (string) $type ) ); |
| 136 |
if ( '' === $clean_type || '' === trim( (string) $mls_id ) ) { |
| 137 |
return; |
| 138 |
} |
| 139 |
|
| 140 |
update_option( 'mlsimport_provider_type', $clean_type ); |
| 141 |
update_option( 'mlsimport_provider_type_mls_id', (string) $mls_id ); |
| 142 |
} |
| 143 |
|
| 144 |
/** |
| 145 |
* Clear state that belongs only to the previously selected MLS. |
| 146 |
* |
| 147 |
* Saved credential fields are deliberately untouched so switching back to a |
| 148 |
* prior provider restores its inputs. Active type, config, connection flag, |
| 149 |
* metadata, and cached access tokens are removed immediately. |
| 150 |
* |
| 151 |
* @return void |
| 152 |
*/ |
| 153 |
public static function clear_active_state() { |
| 154 |
delete_option( 'mlsimport_provider_type' ); |
| 155 |
delete_option( 'mlsimport_provider_type_mls_id' ); |
| 156 |
delete_option( 'mlsimport_live_mls_config' ); |
| 157 |
delete_option( 'mlsimport_connection_test' ); |
| 158 |
// Flat legacy flag only: each connection's own "_{mls_id}" populated |
| 159 |
// flag (#275) stays, so switching back to a prior MLS keeps its |
| 160 |
// gathered state; the save path clears the newly saved MLS's flag. |
| 161 |
delete_option( 'mlsimport_mls_metadata_populated' ); |
| 162 |
|
| 163 |
self::clear_access_tokens(); |
| 164 |
} |
| 165 |
|
| 166 |
/** |
| 167 |
* Clear cached SaaS and Direct MLS access tokens after credentials change. |
| 168 |
* |
| 169 |
* @return void |
| 170 |
*/ |
| 171 |
public static function clear_access_tokens() { |
| 172 |
delete_transient( 'mlsimport_saas_token' ); |
| 173 |
self::clear_direct_access_tokens(); |
| 174 |
} |
| 175 |
|
| 176 |
/** |
| 177 |
* Clear every expiring token owned by a Direct MLS provider adapter. |
| 178 |
* |
| 179 |
* @return void |
| 180 |
*/ |
| 181 |
public static function clear_direct_access_tokens() { |
| 182 |
foreach ( array( 'trestle', 'realcomp', 'realtorca', 'brightmls', 'rapattoni' ) as $type ) { |
| 183 |
delete_transient( 'mlsimport_live_token_' . $type ); |
| 184 |
} |
| 185 |
} |
| 186 |
|
| 187 |
/** |
| 188 |
* Build the provider configuration handed to the plain admin JavaScript. |
| 189 |
* |
| 190 |
* Each listed MLS ID is resolved through the same adapter method used by PHP |
| 191 |
* requests. JavaScript receives final field names and only shows/hides them; |
| 192 |
* it never interprets provider ranges or names. |
| 193 |
* |
| 194 |
* @param array $mls_ids MLS IDs present in the autocomplete list. |
| 195 |
* @param string $saved_type Authoritative type for the current MLS. |
| 196 |
* @param int|string $saved_type_mls_id MLS ID that owns the saved type. |
| 197 |
* @return array{by_mls_id:array,all_credential_fields:array} |
| 198 |
*/ |
| 199 |
public static function browser_config_for_ids( array $mls_ids, $saved_type = '', $saved_type_mls_id = '' ) { |
| 200 |
$by_mls_id = array(); |
| 201 |
foreach ( $mls_ids as $mls_id ) { |
| 202 |
$id = (string) $mls_id; |
| 203 |
$type = $id === (string) $saved_type_mls_id ? $saved_type : ''; |
| 204 |
$provider = self::adapter( $type, $id ); |
| 205 |
$by_mls_id[ $id ] = array( |
| 206 |
'type' => $provider->type(), |
| 207 |
'credential_fields' => $provider->credential_fields(), |
| 208 |
); |
| 209 |
} |
| 210 |
|
| 211 |
// Collect the form's full credential catalog from real adapters, not a UI map. |
| 212 |
$all_fields = array(); |
| 213 |
foreach ( self::supported_types() as $type ) { |
| 214 |
$all_fields = array_merge( $all_fields, self::adapter( $type, 0 )->credential_fields() ); |
| 215 |
} |
| 216 |
|
| 217 |
return array( |
| 218 |
'by_mls_id' => $by_mls_id, |
| 219 |
'all_credential_fields' => array_values( array_unique( $all_fields ) ), |
| 220 |
); |
| 221 |
} |
| 222 |
|
| 223 |
/** |
| 224 |
* Return every recognized saved provider type. |
| 225 |
* |
| 226 |
* @return string[] |
| 227 |
*/ |
| 228 |
private static function supported_types() { |
| 229 |
return array( |
| 230 |
'bridge', |
| 231 |
'spark', |
| 232 |
'trestle', |
| 233 |
'mlsgrid', |
| 234 |
'rmls', |
| 235 |
'utah_real_estate', |
| 236 |
'realcomp', |
| 237 |
'realtorca', |
| 238 |
'brightmls', |
| 239 |
'rapattoni', |
| 240 |
'paragon', |
| 241 |
'connectmls', |
| 242 |
'proptx', |
| 243 |
'centris', |
| 244 |
); |
| 245 |
} |
| 246 |
|
| 247 |
/** |
| 248 |
* Translate the historic numeric MLS ranges into a provider type. |
| 249 |
* |
| 250 |
* This method exists only for older saved configurations that have no type. |
| 251 |
* New saved types always win before this compatibility method is called. |
| 252 |
* |
| 253 |
* @param int $mls_id Numeric MLS identifier. |
| 254 |
* @return string Provider type used by the legacy range map. |
| 255 |
*/ |
| 256 |
private static function type_from_mls_id( $mls_id ) { |
| 257 |
// Bright MLS is the reserved 8001 ID inside the ConnectMLS block. |
| 258 |
if ( 8001 === $mls_id ) { |
| 259 |
return 'brightmls'; |
| 260 |
} |
| 261 |
|
| 262 |
// Centris is the reserved 9001 ID inside the PropTx block. Centris is a |
| 263 |
// different provider with a different feed, so the surrounding range |
| 264 |
// must never resolve it to PropTx when no type has been saved yet. |
| 265 |
if ( 9001 === $mls_id ) { |
| 266 |
return 'centris'; |
| 267 |
} |
| 268 |
|
| 269 |
if ( $mls_id >= 9000 && $mls_id < 10000 ) { |
| 270 |
return 'proptx'; |
| 271 |
} |
| 272 |
if ( $mls_id >= 8000 && $mls_id < 9000 ) { |
| 273 |
return 'connectmls'; |
| 274 |
} |
| 275 |
if ( $mls_id >= 7000 && $mls_id < 8000 ) { |
| 276 |
return 'realtorca'; |
| 277 |
} |
| 278 |
if ( $mls_id >= 6000 && $mls_id < 7000 ) { |
| 279 |
return 'paragon'; |
| 280 |
} |
| 281 |
if ( $mls_id >= 5000 && $mls_id < 6000 ) { |
| 282 |
return 'rapattoni'; |
| 283 |
} |
| 284 |
if ( $mls_id > 900 && $mls_id < 3000 ) { |
| 285 |
return 'trestle'; |
| 286 |
} |
| 287 |
|
| 288 |
// The old code treated all remaining IDs as token-based Bridge. |
| 289 |
return 'bridge'; |
| 290 |
} |
| 291 |
} |
| 292 |
|