PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.0.4
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.0.4
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 / includes / live / live-connection.php

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

273 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Live mode: the auth header for direct MLS requests.
4 *
5 * Bridge/Spark/MLSGrid/RMLS use the stored MLS token as-is. The expiring-token
6 * providers run an automatic credentials -> access-token swap — the identical
7 * grants the AWS token functions use — cached in a transient for the token's
8 * lifetime. Ported swaps: Trestle, Realcomp (Trestle credential slots),
9 * Realtor.ca, Rapattoni, BrightMLS (Okta). Providers with no as-is token and
10 * no ported swap (Paragon, ConnectMLS) have no usable credentials, which
11 * keeps the live gate off for them.
12 *
13 * @package Mlsimport
14 */
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 /**
21 * Providers whose stored token is sent as-is (no swap needed).
22 */
23 function mlsimport_live_token_as_is_types(): array {
24 // Providers whose stored MLS token is a long-lived bearer sent verbatim.
25 return array( 'bridge', 'spark', 'mlsgrid', 'rmls', 'utah_real_estate' );
26 }
27
28 /**
29 * Whether the credentials needed for the configured provider are saved.
30 *
31 * @return bool
32 */
33 function mlsimport_live_credentials_present(): bool {
34 // No config, no credentials to speak of.
35 $config = mlsimport_live_config();
36 if ( array() === $config ) {
37 return false;
38 }
39
40 // As-is providers need a non-empty stored token; everything else needs a
41 // buildable swap spec (which itself checks its credential slots).
42 $type = isset( $config['type'] ) ? (string) $config['type'] : 'bridge';
43 if ( in_array( $type, mlsimport_live_token_as_is_types(), true ) ) {
44 return '' !== mlsimport_live_stored_token();
45 }
46
47 return null !== mlsimport_live_token_swap_spec( $type, $config );
48 }
49
50 /**
51 * The access token for the configured provider.
52 *
53 * @param array $config Per-MLS config.
54 * @return string Empty string when unavailable (missing creds, unported
55 * provider, or a rejected swap).
56 */
57 function mlsimport_live_access_token( array $config ): string {
58 $type = isset( $config['type'] ) ? (string) $config['type'] : 'bridge';
59
60 // As-is providers: hand back the stored token unchanged.
61 if ( in_array( $type, mlsimport_live_token_as_is_types(), true ) ) {
62 return mlsimport_live_stored_token();
63 }
64
65 // Expiring-token providers: run (or reuse a cached) credentials->token swap.
66 return mlsimport_live_swap_token( $type, $config );
67 }
68
69 /**
70 * The MLS token saved by the connection setup, as-is.
71 *
72 * @return string
73 */
74 function mlsimport_live_stored_token(): string {
75 // The token saved during connection setup, trimmed; '' when never stored.
76 $options = get_option( 'mlsimport_admin_options' );
77 return is_array( $options ) && isset( $options['mlsimport_mls_token'] )
78 ? trim( (string) $options['mlsimport_mls_token'] )
79 : '';
80 }
81
82 /**
83 * The token-swap request for an expiring-token provider — the same grant the
84 * matching AWS token function runs.
85 *
86 * @param string $type Provider type.
87 * @param array $config Per-MLS config (rapattoni reads api_token_url).
88 * @return array|null {url, body, json} or null when the provider is unported
89 * or its credentials are incomplete.
90 */
91 function mlsimport_live_token_swap_spec( string $type, array $config ) {
92 // Credential slots all live in the admin options; $opt reads one, trimmed.
93 $options = get_option( 'mlsimport_admin_options' );
94 $options = is_array( $options ) ? $options : array();
95 $opt = static function ( string $key ) use ( $options ): string {
96 return isset( $options[ $key ] ) ? trim( (string) $options[ $key ] ) : '';
97 };
98
99 // One case per ported provider; each returns its exact token-endpoint POST.
100 switch ( $type ) {
101 case 'trestle':
102 // Trestle client_credentials grant (form-encoded).
103 $id = $opt( 'mlsimport_tresle_client_id' );
104 $secret = $opt( 'mlsimport_tresle_client_secret' );
105 if ( '' === $id || '' === $secret ) {
106 return null;
107 }
108 return array(
109 'url' => 'https://api-trestle.corelogic.com/trestle/oidc/connect/token',
110 'body' => array(
111 'client_id' => $id,
112 'client_secret' => $secret,
113 'grant_type' => 'client_credentials',
114 'scope' => 'api',
115 ),
116 'json' => false,
117 );
118
119 case 'realcomp':
120 // Realcomp re-uses the Trestle credential slots (JSON-body grant).
121 $id = $opt( 'mlsimport_tresle_client_id' );
122 $secret = $opt( 'mlsimport_tresle_client_secret' );
123 if ( '' === $id || '' === $secret ) {
124 return null;
125 }
126 return array(
127 'url' => 'https://auth.realcomp.com/Token',
128 'body' => array(
129 'client_id' => $id,
130 'client_secret' => $secret,
131 'audience' => 'rcapi.realcomp.com',
132 ),
133 'json' => true,
134 );
135
136 case 'realtorca':
137 // Realtor.ca / CREA DDF client_credentials grant (scope DDFApi_Read).
138 $id = $opt( 'mlsimport_realtorca_client_id' );
139 $secret = $opt( 'mlsimport_realtorca_client_secret' );
140 if ( '' === $id || '' === $secret ) {
141 return null;
142 }
143 return array(
144 'url' => 'https://identity.crea.ca/connect/token',
145 'body' => array(
146 'client_id' => $id,
147 'client_secret' => $secret,
148 'grant_type' => 'client_credentials',
149 'scope' => 'DDFApi_Read',
150 ),
151 'json' => false,
152 );
153
154 case 'brightmls':
155 // BrightMLS Okta client_credentials grant.
156 $id = $opt( 'mlsimport_brightmls_client_id' );
157 $secret = $opt( 'mlsimport_brightmls_client_secret' );
158 if ( '' === $id || '' === $secret ) {
159 return null;
160 }
161 return array(
162 'url' => 'https://brightmls.okta.com/oauth2/default/v1/token',
163 'body' => array(
164 'client_id' => $id,
165 'client_secret' => $secret,
166 'grant_type' => 'client_credentials',
167 ),
168 'json' => false,
169 );
170
171 case 'rapattoni':
172 // Rapattoni password grant: needs client id/secret + a user login,
173 // and its token URL comes from the per-MLS config, not a constant.
174 $id = $opt( 'mlsimport_rapattoni_client_id' );
175 $secret = $opt( 'mlsimport_rapattoni_client_secret' );
176 $user = $opt( 'mlsimport_rapattoni_username' );
177 $pass = $opt( 'mlsimport_rapattoni_password' );
178 $token_url = isset( $config['api_token_url'] ) ? trim( (string) $config['api_token_url'] ) : '';
179 // Any missing slot (incl. the token URL) makes the swap impossible.
180 if ( '' === $id || '' === $secret || '' === $user || '' === $pass || '' === $token_url ) {
181 return null;
182 }
183 return array(
184 'url' => $token_url,
185 'body' => array(
186 'client_id' => $id,
187 'client_secret' => $secret,
188 'username' => $user,
189 'password' => $pass,
190 'grant_type' => 'password',
191 ),
192 'json' => false,
193 );
194 }
195
196 // Unported provider type: no swap spec.
197 return null;
198 }
199
200 /**
201 * Run (or reuse) a provider's token swap. The token is transient-cached for
202 * its lifetime; a rejected swap caches a short 'fail' sentinel so a broken
203 * credential doesn't re-POST on every request.
204 *
205 * @param string $type Provider type.
206 * @param array $config Per-MLS config.
207 * @return string The access token, or '' when unavailable.
208 */
209 function mlsimport_live_swap_token( string $type, array $config ): string {
210 // No spec means the provider is unported or its credentials are incomplete.
211 $spec = mlsimport_live_token_swap_spec( $type, $config );
212 if ( null === $spec ) {
213 return '';
214 }
215
216 // Reuse a cached token: a 'fail' sentinel means a recent swap was rejected,
217 // so don't re-POST — return '' until the short sentinel TTL lapses.
218 $transient = 'mlsimport_live_token_' . $type;
219 $cached = get_transient( $transient );
220 if ( is_string( $cached ) && '' !== $cached ) {
221 return 'fail' === $cached ? '' : $cached;
222 }
223
224 // Build the POST body: JSON-body grants set the content-type + encode;
225 // the rest send a form-encoded array.
226 $args = array( 'timeout' => 15 );
227 if ( $spec['json'] ) {
228 $args['headers'] = array( 'Content-Type' => 'application/json' );
229 $args['body'] = wp_json_encode( $spec['body'] );
230 } else {
231 $args['body'] = $spec['body'];
232 }
233
234 // Fire the token request and decode the response.
235 $response = wp_remote_post( $spec['url'], $args );
236 $body = json_decode( (string) wp_remote_retrieve_body( $response ), true );
237 $code = (int) wp_remote_retrieve_response_code( $response );
238
239 // Any transport error, non-200, or missing access_token: cache a short
240 // 'fail' sentinel so a broken credential doesn't hammer the endpoint.
241 if ( is_wp_error( $response ) || 200 !== $code || ! is_array( $body ) || empty( $body['access_token'] ) ) {
242 set_transient( $transient, 'fail', 5 * MINUTE_IN_SECONDS );
243 return '';
244 }
245
246 // Success: cache the token for its lifetime, shaved 60s (or ~1h when the
247 // provider omits expires_in), and return it.
248 $token = (string) $body['access_token'];
249 $ttl = isset( $body['expires_in'] ) ? max( 60, (int) $body['expires_in'] - 60 ) : HOUR_IN_SECONDS - 60;
250 set_transient( $transient, $token, $ttl );
251
252 return $token;
253 }
254
255 /**
256 * Request headers for a direct MLS call.
257 *
258 * @return array
259 */
260 function mlsimport_live_auth_headers(): array {
261 // Resolve the access token for the configured provider.
262 $token = mlsimport_live_access_token( mlsimport_live_config() );
263 // No token: return no headers (the caller treats this as "can't reach MLS").
264 if ( '' === $token ) {
265 return array();
266 }
267 // Standard bearer auth + JSON accept for the direct MLS request.
268 return array(
269 'Authorization' => 'Bearer ' . $token,
270 'Accept' => 'application/json',
271 );
272 }
273