PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.1.2
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.1.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 6.1.0 All 34 releases
mlsimport / mlsimport.php

mlsimport.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.1.2, at mlsimport.php

984 lines 43.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: MlsImport
4 * Plugin URI: https://mlsimport.com/
5 * Description: MLS Import - The MLSImport plugin facilitates the connection to your real estate MLS database, allowing you to download and synchronize real estate property data from the MLS.
6 * Version: 7.1.2
7 * Requires at least: 5.2
8 * Requires PHP: 7.4
9 * License: GPLv3
10 * License URI: https://www.gnu.org/licenses/gpl-3.0.html
11 * Author: MlsImport
12 * Text Domain: mlsimport
13 * Domain Path: /languages
14 */
15
16 /*
17 * ---------------------------------------------------------------------------
18 * FILE ROLE: main plugin bootstrap.
19 * ---------------------------------------------------------------------------
20 * Responsibilities of this file, in load order:
21 * 1. Define global constants (version, API endpoint, paths, cron batch size).
22 * 2. Register activation/deactivation hooks and one-time upgrade notices.
23 * 3. Track the installed version and flag upgrade modals.
24 * 4. require_once every plugin PHP file (core, API client, theme/provider
25 * adapters, onboarding, telemetry, and the whole standalone/ module).
26 * 5. Wire the standalone (theme_id 990) init/enqueue/template hooks.
27 * 6. Schedule the WP-Cron events (hourly import, daily reconciliation, daily
28 * telemetry) and define their handler functions.
29 * 7. Instantiate the core Mlsimport class and call run() to register hooks.
30 * 8. Define assorted global helper functions (logging, dropdowns, onboarding
31 * AJAX save handlers).
32 * ---------------------------------------------------------------------------
33 */
34
35 // If this file is called directly, abort.
36
37 if ( ! defined( 'WPINC' ) ) {
38 die;
39 }
40
41
42 // Current plugin version (kept in sync with the header above and the readme).
43 define( 'MLSIMPORT_VERSION', '7.1.2');
44 // Marketing/portal host used to build sign-up and affiliate links.
45 define( 'MLSIMPORT_CLUBLINK', 'mlsimport.com' );
46 // Scheme for the portal host links.
47 define( 'MLSIMPORT_CLUBLINKSSL', 'https' );
48 // Default per-request import batch size (listings pulled per cron step).
49 define( 'MLSIMPORT_CRON_STEP', 20 );
50 // Absolute filesystem path to this plugin directory (trailing slash).
51 define( 'MLSIMPORT_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
52 // Public URL to this plugin directory (trailing slash).
53 define( 'MLSIMPORT_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
54
55
56 // SaaS API base URL (AWS API Gateway). The two commented lines are the legacy
57 // vanity host and the old "dev" stage; the active endpoint is the "blue" stage.
58 //define( 'MLSIMPORT_API_URL', 'https://requests.mlsimport.com/' );
59 //define( 'MLSIMPORT_API_URL', 'https://pyjzsilw7b.execute-api.us-east-1.amazonaws.com/dev/' );
60 define( 'MLSIMPORT_API_URL', 'https://srky9ddikl.execute-api.us-east-1.amazonaws.com/blue/');
61
62
63
64
65
66
67 // Allow a site to pre-define this constant (e.g. in wp-config.php) to hide the
68 // "finish setup" admin notice; default to showing it when not already defined.
69 if ( ! defined( 'MLSIMPORT_HIDE_SETUP_NOTICE' ) ) {
70 define( 'MLSIMPORT_HIDE_SETUP_NOTICE', false );
71 }
72
73
74
75 /**
76 * The code that runs during plugin activation.
77 * This action is documented in includes/class-mlsimport-activator.php
78 */
79 function mlsimport_activate() {
80 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-activator.php';
81 Mlsimport_Activator::activate();
82 mlsimport_telemetry_set_once( 'installed_at', time() );
83
84 // Standalone (theme_id 990) search table.
85 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-table.php';
86 Mlsimport_Standalone_Table::create();
87 }
88
89
90
91 /**
92 * The code that runs during plugin deactivation.
93 * This action is documented in includes/class-mlsimport-deactivator.php
94 */
95 function mlsimport_deactivate() {
96 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-deactivator.php';
97 wp_clear_scheduled_hook( 'event_mls_import_auto' );
98 wp_clear_scheduled_hook( 'mlsimport_reconciliation_event' );
99 wp_clear_scheduled_hook( 'mlsimport_reconciliation_retry_event' );
100 wp_clear_scheduled_hook( 'mlsimport_daily_telemetry_event' );
101 delete_option( 'mlsimport_reconciliation_running' );
102 Mlsimport_Deactivator::deactivate();
103 }
104
105
106
107 register_activation_hook( __FILE__, 'mlsimport_activate' );
108 register_deactivation_hook( __FILE__, 'mlsimport_deactivate' );
109
110 /**
111 * Show one-time modal about the switch from Delete Statuses to Protected Statuses.
112 */
113 add_action( 'admin_footer', 'mlsimport_protected_statuses_upgrade_modal' );
114 function mlsimport_protected_statuses_upgrade_modal() {
115 // Bail if the user already acknowledged/dismissed the notice.
116 if ( get_option( 'mlsimport_dismiss_protected_status_notice' ) ) {
117 return;
118 }
119 // Only show for sites that had a version before 6.2 (not fresh installs)
120 $show_modal = get_option( 'mlsimport_show_protected_status_modal' );
121 // Bail when the upgrade flag was never set for this site.
122 if ( ! $show_modal ) {
123 return;
124 }
125 // Nonce for the dismissal AJAX call embedded in the modal's inline script.
126 $nonce = wp_create_nonce( 'mlsimport_dismiss_protected_notice' );
127 // Link the user to their Import Tasks list to set Protected Statuses.
128 $import_tasks_url = admin_url( 'edit.php?post_type=mlsimport_item' );
129 // Emit the modal markup + inline dismissal script into the admin footer.
130 ?>
131 <div id="mlsimport-protected-status-modal" style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:999999;display:flex;align-items:center;justify-content:center;">
132 <div style="background:#fff;max-width:520px;width:90%;border-radius:8px;padding:30px;box-shadow:0 4px 20px rgba(0,0,0,0.3);">
133 <h2 style="margin-top:0;color:#d63638;">MLSImport - Important Change</h2>
134 <p><strong>"Delete Statuses"</strong> have been removed. The plugin now uses <strong>Protected Statuses</strong> only.</p>
135 <p>Properties with a Protected Status will be kept during reconciliation. All other properties no longer found in MLS <strong>will be deleted</strong>.</p>
136 <p style="background:#fff3cd;border-left:4px solid #dba617;padding:10px 14px;"><strong>Action required:</strong> Go to each of your <a href="<?php echo esc_url( $import_tasks_url ); ?>">Import Tasks</a> and set the Protected Statuses field to the statuses you want to keep (e.g. Active, Pending, Coming Soon).</p>
137 <button id="mlsimport-acknowledge-btn" class="button button-primary" style="margin-top:10px;font-size:14px;padding:6px 24px;">I acknowledge</button>
138 </div>
139 </div>
140 <script>
141 document.getElementById('mlsimport-acknowledge-btn').addEventListener('click', function() {
142 var btn = this;
143 btn.disabled = true;
144 btn.textContent = 'Saving...';
145 var xhr = new XMLHttpRequest();
146 xhr.open('POST', ajaxurl);
147 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
148 xhr.onload = function() {
149 document.getElementById('mlsimport-protected-status-modal').style.display = 'none';
150 };
151 xhr.send('action=mlsimport_dismiss_protected_notice&_wpnonce=<?php echo esc_js( $nonce ); ?>');
152 });
153 </script>
154 <?php
155 }
156 add_action( 'wp_ajax_mlsimport_dismiss_protected_notice', 'mlsimport_handle_dismiss_protected_notice' );
157 /**
158 * AJAX handler: persist the user's dismissal of the protected-statuses modal.
159 *
160 * Verifies the nonce, records the permanent "dismissed" flag, clears the
161 * "show modal" flag, and returns a success JSON response.
162 *
163 * @return void
164 */
165 function mlsimport_handle_dismiss_protected_notice() {
166 // Verify the nonce created in the modal markup above.
167 check_ajax_referer( 'mlsimport_dismiss_protected_notice' );
168 // Persist that this notice has been acknowledged so it never shows again.
169 update_option( 'mlsimport_dismiss_protected_status_notice', true );
170 // Remove the one-time "show modal" flag.
171 delete_option( 'mlsimport_show_protected_status_modal' );
172 // Return an empty success payload to the inline script.
173 wp_send_json_success();
174 }
175
176 /**
177 * Track installed version and flag upgrade modals.
178 * Fresh installs get current version immediately, so no upgrade modal.
179 * Upgrades from < 6.2 flag the protected status modal.
180 */
181 $mlsimport_prev_version = get_option( 'mlsimport_installed_version', '' );
182 if ( $mlsimport_prev_version !== MLSIMPORT_VERSION ) {
183 if ( ! empty( $mlsimport_prev_version ) && version_compare( $mlsimport_prev_version, '6.2', '<' ) ) {
184 update_option( 'mlsimport_show_protected_status_modal', true );
185 }
186 update_option( 'mlsimport_installed_version', MLSIMPORT_VERSION );
187 }
188
189 /**
190 * The core plugin class that is used to define internationalization,
191 * admin-specific hooks, and public-facing site hooks.
192 */
193
194 /*
195 * Action Scheduler — the plugin's only third-party runtime dependency.
196 *
197 * This is deliberately a direct require rather than `vendor/autoload.php`.
198 * Composer's generated autoloader is written differently depending on whether
199 * dev dependencies (phpunit, php_codesniffer, myclabs/deep-copy) happen to be
200 * installed at the time it was generated. Those dev packages are never shipped,
201 * so a dev-generated autoloader committed to the repo makes `autoload_files.php`
202 * eagerly require files that do not exist in the released plugin — a fatal error
203 * on activation, before any plugin code runs.
204 *
205 * Action Scheduler is built to be dropped into a plugin and included directly
206 * (that is how WooCommerce loads it); it registers its own class loader and
207 * negotiates versions with any other copy already loaded on the site. So there
208 * is nothing left for the Composer autoloader to do at runtime, and not shipping
209 * it removes that whole failure mode. Composer is still used for dev tooling.
210 */
211 require_once plugin_dir_path( __FILE__ ) . 'vendor/woocommerce/action-scheduler/action-scheduler.php';
212 // Core includes: RESO field defs + helpers, provider map, cron/reconciliation
213 // guards, status taxonomy/normalizer, then the orchestrator and API client.
214 require_once plugin_dir_path( __FILE__ ) . 'includes/help_functions.php';
215 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-theme-detection.php';
216 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-credentials.php';
217 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-provider-map.php';
218 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-enum-labels.php';
219 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-country.php';
220 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-reconciliation-guard.php';
221 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-cron-guard.php';
222 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-task-health.php';
223 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-status-taxonomy.php';
224 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-reconciliation.php';
225 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-reconciliation-wordpress-environment.php';
226 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-import-task-execution.php';
227 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-import-task-execution-wordpress-environment.php';
228 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-status-normalize.php';
229 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-stored-listing-fields.php';
230 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-stored-listing-title.php';
231 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-stored-listing-media.php';
232 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-listing-key-migration.php';
233 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-stored-listing-wordpress-environment.php';
234 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-stored-listing-write.php';
235 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-stored-listing-adapter-factory.php';
236 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport.php';
237 require_once plugin_dir_path( __FILE__ ) . 'includes/ThemeImport.php';
238 require_once plugin_dir_path( __FILE__ ) . 'enviroment/StandaloneClass.php';
239 require_once plugin_dir_path( __FILE__ ) . 'enviroment/ResidenceClass.php';
240 require_once plugin_dir_path( __FILE__ ) . 'enviroment/EstateClass.php';
241 require_once plugin_dir_path( __FILE__ ) . 'enviroment/HouzezClass.php';
242 require_once plugin_dir_path( __FILE__ ) . 'enviroment/RealHomesClass.php';
243 require_once plugin_dir_path( __FILE__ ) . 'enviroment/ResoBase.php';
244 require_once plugin_dir_path( __FILE__ ) . 'enviroment/SparkResoClass.php';
245 require_once plugin_dir_path( __FILE__ ) . 'enviroment/BridgeResoClass.php';
246 require_once plugin_dir_path( __FILE__ ) . 'enviroment/TresleResoClass.php';
247 require_once plugin_dir_path( __FILE__ ) . 'enviroment/MlsgridResoClass.php';
248 require_once plugin_dir_path( __FILE__ ) . 'enviroment/BrightMlsResoClass.php';
249 require_once plugin_dir_path( __FILE__ ) . 'enviroment/CentrisResoClass.php';
250 require_once plugin_dir_path( __FILE__ ) . 'enviroment/ProviderResoClasses.php';
251 require_once plugin_dir_path( __FILE__ ) . 'enviroment/UnsupportedResoClass.php';
252 require_once plugin_dir_path( __FILE__ ) . 'includes/addons/agents_offices.php';
253 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-onboarding.php';
254
255 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-field-configuration.php';
256 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-field-selector-functions.php';
257 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-progressive-save.php';
258 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-metadata-autotrigger.php';
259 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-telemetry.php';
260 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-activity-log.php';
261 // #208: internal incident alerts (dedup + resolve) and import/connection health watch.
262 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-alerts.php';
263 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-import-health.php';
264
265 /*
266 * Standalone (theme_id 990) mode — own listings table, CPTs and taxonomies.
267 * Registered on every load (ADR-0003); the table is created on activation and
268 * upgraded behind a version guard on admin_init.
269 */
270 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/mlsimport-hooks.php'; // Hook reference + convention (loaded early).
271 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-table.php';
272 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-cpt.php';
273 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-term-select.php';
274 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-shortcodes.php';
275 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-block.php';
276 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-ajax.php';
277 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-reindex.php';
278 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-assets.php';
279 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-single.php';
280 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/property-section-registry.php';
281 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/property-print.php';
282 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-section-shortcodes.php';
283 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-section-blocks.php';
284 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-section-elementor.php';
285 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-lead.php';
286 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/agent-sections.php';
287 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/property-schema.php';
288 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-metabox.php';
289 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-agent-metabox.php';
290 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-term-meta.php';
291 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-columns.php';
292 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-favorites.php';
293 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/page-block-registry.php';
294 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-page-block-shortcodes.php';
295 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-page-block-blocks.php';
296 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-page-block-elementor.php';
297 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-customizer.php'; // Standalone design settings in the WP Customizer (990 mode).
298 require_once plugin_dir_path( __FILE__ ) . 'includes/live/live-bootstrap.php'; // Live MLS passthrough mode (seam #1 — its only existing-file require).
299 add_action( 'init', array( 'Mlsimport_Standalone_Cpt', 'register' ) );
300 // After register(): drop cached rewrite rules when the standalone-mode signature changed (#206).
301 add_action( 'init', array( 'Mlsimport_Standalone_Cpt', 'maybe_flush_rewrites' ), 20 );
302 add_action( 'init', array( 'Mlsimport_Property_Metabox', 'register' ) );
303 add_action( 'init', array( 'Mlsimport_Agent_Metabox', 'register' ) );
304 add_action( 'init', array( 'Mlsimport_Term_Meta', 'register' ) );
305 add_action( 'init', array( 'Mlsimport_Property_Columns', 'register' ) );
306 add_action( 'init', array( 'Mlsimport_Standalone_Shortcodes', 'register' ) );
307 add_action( 'init', array( 'Mlsimport_Standalone_Block', 'register' ) );
308 add_action( 'init', array( 'Mlsimport_Standalone_Ajax', 'register' ) );
309 add_action( 'init', array( 'Mlsimport_Favorites', 'register' ) );
310 add_action( 'init', array( 'Mlsimport_Property_Section_Shortcodes', 'register' ) );
311 // Property-section Gutenberg blocks (mlsimport/property-*, "MLSImport — Property"
312 // category) are intentionally NOT registered — we don't expose them as blocks.
313 // The render dispatcher, shortcodes and Elementor widget behind them stay active.
314 // add_action( 'init', array( 'Mlsimport_Property_Section_Blocks', 'register' ) );
315 add_action( 'init', array( 'Mlsimport_Page_Block_Shortcodes', 'register' ) );
316 add_action( 'init', array( 'Mlsimport_Page_Block_Blocks', 'register' ) );
317 // Two MLSImport categories in the block inserter: single-property section blocks
318 // ("Property") and the page-builder blocks ("Real Estate").
319 add_filter(
320 'block_categories_all',
321 static function ( $categories ) {
322 return array_merge(
323 array(
324 array( 'slug' => 'mlsimport-property', 'title' => __( 'MLSImport — Property', 'mlsimport' ) ),
325 array( 'slug' => 'mlsimport-real-estate', 'title' => __( 'MLSImport — Real Estate', 'mlsimport' ) ),
326 ),
327 $categories
328 );
329 }
330 );
331 add_action( 'init', array( 'Mlsimport_Property_Lead', 'register' ) );
332 // Priority 20 (not the default 10) so the plugin's self-contained BEM stylesheets
333 // enqueue AFTER the active theme's own styles. Themes hook wp_enqueue_scripts at 10
334 // and, because plugins load before the theme, our default-10 callback would print
335 // FIRST — losing every equal-specificity tie to a theme rule that prints later.
336 // Hello Elementor's reset.css is the concrete case: it styles bare `button` /
337 // `[type=button]` (pink #c36, inline-block, width:auto), which ties our single-class
338 // `.mlsimport-*` button rules (0,1,0) and won on order, so the multiselect control,
339 // range/beds toggles, popup buttons, submit and the card heart all rendered narrow
340 // and pink. Printing after the theme lets our base rules win that tie. It stays a
341 // SINGLE class (0,1,0), so component state rules (:focus / :hover / .is-open at
342 // 0,1,1+) and any Elementor Style-tab control ({{WRAPPER}} … at 0,2,0+) still win —
343 // this only reclaims the tie against a generic theme reset, and never matches a
344 // non-plugin (e.g. Elementor) element.
345 add_action( 'wp_enqueue_scripts', array( 'Mlsimport_Standalone_Assets', 'enqueue' ), 20 );
346 add_action( 'wp_enqueue_scripts', array( 'Mlsimport_Property_Section_Assets', 'ensure_registered' ) );
347 // Pre-enqueue the single-property section assets into the <head>; the single
348 // template prints the header before any section renders, so the on-demand
349 // enqueue at render time lands in the footer and flashes unstyled (issue #172).
350 // Priority 20 for the same after-the-theme reason as the standalone assets above.
351 add_action( 'wp_enqueue_scripts', array( 'Mlsimport_Property_Section_Assets', 'enqueue_for_single' ), 20 );
352 // Same for the single-agent page (issue #188).
353 add_action( 'wp_enqueue_scripts', 'mlsimport_agent_enqueue_for_single', 20 );
354 // Load the same front-end CSS into the block editor so the dynamic blocks'
355 // ServerSideRender previews match the front end (editor-guarded inside the method).
356 add_action( 'enqueue_block_assets', array( 'Mlsimport_Standalone_Assets', 'enqueue_editor' ) );
357 add_filter( 'template_include', array( 'Mlsimport_Standalone_Single', 'template_include' ) );
358 add_action( 'wp_head', 'mlsimport_property_print_schema' );
359 Mlsimport_Property_Section_Elementor::register();
360 Mlsimport_Page_Block_Elementor::register();
361 // A contact-form lead carries no property and no agent; route those general
362 // enquiries to the "Contact form recipients" setting (decision 3).
363 add_filter(
364 'mlsimport_property_lead_recipient',
365 static function ( $to, $property_id, $agent_id = 0 ) {
366 if ( $property_id || $agent_id ) {
367 return $to;
368 }
369 $recipients = trim( (string) mlsimport_standalone_option( 'contact_form_recipients', '' ) );
370 return '' !== $recipients ? $recipients : $to;
371 },
372 10,
373 3
374 );
375 add_action( 'admin_init', array( 'Mlsimport_Standalone_Table', 'maybe_upgrade' ) );
376 // One-time repair of comma-glued taxonomy terms written before 7.1.2 (#290).
377 add_action( 'admin_init', array( 'Mlsimport_Standalone_Cpt', 'maybe_split_packed_terms' ) );
378 add_action( 'before_delete_post', array( 'StandaloneClass', 'cleanup_on_delete' ) );
379 // Trash/unpublish (not a permanent delete) must also drop the listings row, so the
380 // search index only ever holds published listings.
381 add_action( 'transition_post_status', array( 'StandaloneClass', 'cleanup_on_status_change' ), 10, 3 );
382
383 if ( defined( 'WP_CLI' ) && WP_CLI ) {
384 WP_CLI::add_command(
385 'mlsimport reindex',
386 function () {
387 $rebuilt = Mlsimport_Standalone_Reindex::rebuild_all();
388 WP_CLI::success( "Reindexed {$rebuilt} standalone listings." );
389 }
390 );
391 }
392
393 if ( ! wp_next_scheduled( 'event_mls_import_auto' ) ) {
394 wp_schedule_event( time(), 'hourly', 'event_mls_import_auto' );
395 }
396
397
398 /**
399 * Scheduled event: Processes MLSimport items marked for cron processing, in memory-safe batches.
400 *
401 * This function is triggered by the 'event_mls_import_auto' action.
402 * It fetches mlsimport_item post IDs in small batches (not all at once!) to minimize memory usage.
403 * Only posts with meta 'mlsimport_item_stat_cron' = 1 are processed.
404 * For each item, calls mlsimport_saas_start_cron_links_per_item().
405 *
406 * Optimizations:
407 * - Uses 'fields' => 'ids' so only post IDs are loaded (saves memory)
408 * - Batches with posts_per_page/paged, so memory does not spike for large data sets
409 * - Calls gc_collect_cycles() periodically to further reduce memory leaks
410 * - Skips processing if MLS is not connected or token is missing
411 *
412 * @return void
413 */
414 add_action('event_mls_import_auto', 'mlsimport_saas_event_mls_import_auto_function');
415 /**
416 * Scheduled event handler for MLS Import Auto (runs via WP Cron).
417 * Processes mlsimport_item posts in batches and logs memory usage.
418 */
419 function mlsimport_saas_event_mls_import_auto_function() {
420 global $mlsimport;
421
422 //error_log('[AutoCron] Start: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB');
423
424 // Watchdog backstop (issue #199): a chunked manual import whose worker
425 // chain died is normally revived by the polled progress screen, but if
426 // the administrator closed that screen nothing else watches the run.
427 // Hourly cron picks it up here; revive() is a cheap no-op for anything
428 // that is not a silent, stalled manual run.
429 // Import-health checks (#208) run before anything can bail out below:
430 // detect a previous cron run that died mid-loop, and a manual run stuck
431 // at "Preparing". Both open one deduplicated internal incident.
432 mlsimport_cron_heartbeat_check();
433 mlsimport_import_health_watch_manual_run();
434
435 $mlsimport_active_lock = get_option( 'mlsimport_import_run_lock', array() );
436 if ( is_array( $mlsimport_active_lock ) && ! empty( $mlsimport_active_lock['task_id'] ) ) {
437 $mlsimport_revive = $mlsimport->admin->mlsimport_import_task_execution()->revive( (int) $mlsimport_active_lock['task_id'] );
438 if ( true === ( $mlsimport_revive['revived'] ?? false ) ) {
439 mlsimport_saas_single_write_import_custom_logs( 'Hourly watchdog revived the import worker chain for task ' . (int) $mlsimport_active_lock['task_id'] . '.' . PHP_EOL, 'manual' );
440 } elseif ( 'stalled' === ( $mlsimport_revive['reason'] ?? '' ) ) {
441 mlsimport_saas_single_write_import_custom_logs( 'Hourly watchdog declared the import run for task ' . (int) $mlsimport_active_lock['task_id'] . ' stalled and failed it.' . PHP_EOL, 'manual' );
442 // The run was terminated as hopeless — tell the SaaS once (#208).
443 mlsimport_alert_open(
444 'import_stalled:' . (int) $mlsimport_active_lock['task_id'],
445 'import_stalled',
446 array( 'task_id' => (int) $mlsimport_active_lock['task_id'] )
447 );
448 }
449 }
450
451 // 0. Bail if a run is already in progress. Without this guard an overlapping
452 // cron fire processes the same listings in parallel, causing duplicate
453 // listing_key inserts and term_relationship/term_count deadlocks. The TTL is
454 // the safety net if a run dies mid-loop without reaching the release below.
455 if ( get_transient( 'mlsimport_cron_running' ) ) {
456 return;
457 }
458
459 // 1. Get the API token from transient - exit if not set
460 $token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient();
461 //error_log('[AutoCron] After token fetch: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB');
462 if (trim($token) === '') {
463 // Silent exit made frozen sites undiagnosable (issue #207 finding 3):
464 // record the failed attempt with a real class before bailing.
465 mlsimport_telemetry_set( 'last_sync_failed', time() );
466 mlsimport_telemetry_set( 'last_sync_failed_code', 'no_token' );
467 //error_log('[AutoCron] No token, exiting.');
468 return;
469 }
470
471 // 2. Check if MLS connection is valid - exit if not
472 $is_mls_connected = get_option('mlsimport_connection_test', '');
473 //error_log('[AutoCron] After connection check: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB');
474 if ('yes' !== $is_mls_connected) {
475 // Same rule as the token exit above: a sync attempt that cannot run
476 // records why, so the heartbeat can surface it.
477 mlsimport_telemetry_set( 'last_sync_failed', time() );
478 mlsimport_telemetry_set( 'last_sync_failed_code', 'mls_not_connected' );
479 //error_log('[AutoCron] No valid connection, exiting.');
480 return;
481 }
482
483 // Claim the run lock now that we are committed to processing.
484 set_transient( 'mlsimport_cron_running', 1, 15 * MINUTE_IN_SECONDS );
485
486 // Heartbeat (#208): record that a cron import is now running, so the next
487 // cron entry can tell a clean finish from a process that died mid-loop.
488 mlsimport_cron_heartbeat_start();
489
490 // Record sync attempt in telemetry
491 mlsimport_telemetry_bump( 'syncs' );
492 mlsimport_telemetry_set( 'last_sync_attempt', time() );
493
494 // 3. Set batch size for gathering and initialize loop variables
495 $batch_size = 100;
496 $paged = 1;
497 $total_processed = 0;
498
499 // 4. Gather every cron-enabled task id first (ids only — a few bytes each,
500 // still fetched in paged batches so the query never loads post objects).
501 // Gathering before processing is what makes fair ordering possible below.
502 $cron_task_ids = array();
503 do {
504 // Prepare query: only IDs, filter by meta key, batch, paged, no_found_rows speeds up query
505 $args = array(
506 'post_type' => 'mlsimport_item',
507 'post_status' => 'any',
508 'posts_per_page' => $batch_size,
509 'paged' => $paged,
510 'fields' => 'ids',
511 'meta_query' => array(
512 array(
513 'key' => 'mlsimport_item_stat_cron',
514 'value' => 1,
515 'compare' => '=',
516 ),
517 ),
518 'no_found_rows' => true,
519 );
520
521 // Get post IDs for this batch
522 $post_ids = get_posts($args);
523
524 // If nothing is returned, the gather is complete
525 if (empty($post_ids)) {
526 break;
527 }
528
529 foreach ($post_ids as $prop_id) {
530 $cron_task_ids[] = (int) $prop_id;
531 }
532
533 // Prepare next batch
534 $paged++;
535 unset($post_ids); // Free memory
536
537 } while (true);
538
539 // 5. Order by starvation (issue #203): the query above returns tasks in
540 // the same fixed order every hour, so when an early large task ate the
541 // whole cycle the bottom tasks were skipped run after run. Sorting by the
542 // last-sync watermark puts the longest-unsynced region first in line.
543 $cron_task_watermarks = array();
544 foreach ($cron_task_ids as $prop_id) {
545 $cron_task_watermarks[ $prop_id ] = (string) get_post_meta( $prop_id, 'mlsimport_last_date', true );
546 }
547 unset($cron_task_ids);
548
549 // 6. Process every task, most starved first
550 foreach (mlsimport_cron_task_order($cron_task_watermarks) as $prop_id) {
551 $logs = 'Loop custom post: ' . $prop_id . PHP_EOL;
552 mlsimport_debuglogs_per_plugin($logs);
553
554 // Call processing function for this item. The feed count it pulls
555 // is recorded inside mlsimport_make_listing_requests() (last_feed_found).
556 $mlsimport->admin->mlsimport_saas_start_cron_links_per_item($prop_id);
557
558 $total_processed++;
559
560 // Heartbeat (#208): measurable progress for the stuck-run check.
561 mlsimport_cron_heartbeat_progress( $total_processed );
562
563 // Free memory every 100 processed items
564 if ($total_processed % 100 === 0) {
565 gc_collect_cycles();
566 //error_log("[AutoCron] Processed {$total_processed} total, memory: " . (memory_get_usage(true) / 1024 / 1024) . ' MB');
567 }
568 }
569
570 // Heartbeat (#208): clean finish — also resolves an open died-run incident.
571 mlsimport_cron_heartbeat_finish();
572
573 // Release the run lock so the next scheduled run can proceed.
574 delete_transient( 'mlsimport_cron_running' );
575
576 // last_sync_success is no longer stamped here (issue #207 finding 1): the
577 // end-of-loop stamp reported success even when every request failed, and
578 // never fired when a run died mid-loop. Each listings request now records
579 // its own outcome inside mlsimport_make_listing_requests().
580
581 //error_log('[AutoCron] Done, total processed: ' . $total_processed . ', end memory: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB');
582 }
583
584
585
586 /*
587 * Reconciliation Mechanism
588 *
589 *
590 *
591 **/
592
593 if ( ! wp_next_scheduled( 'mlsimport_reconciliation_event' ) ) {
594 wp_schedule_event( time(), 'daily', 'mlsimport_reconciliation_event' );
595 }
596
597 add_action( 'mlsimport_reconciliation_event', 'mlsimport_saas_reconciliation_event_function' );
598 add_action( 'mlsimport_reconciliation_retry_event', 'mlsimport_saas_reconciliation_event_function' );
599
600 if ( ! wp_next_scheduled( 'mlsimport_daily_telemetry_event' ) ) {
601 wp_schedule_event( time(), 'daily', 'mlsimport_daily_telemetry_event' );
602 }
603
604 add_action( 'mlsimport_daily_telemetry_event', 'mlsimport_telemetry_run_daily' );
605
606
607 /*
608 * Force use of transient
609 *
610 *
611 *
612 **/
613
614 /**
615 * Filter callback stub that returns the transient value unchanged.
616 *
617 * Left as a pass-through hook point; the commented `return false;` would force
618 * a transient miss for debugging.
619 *
620 * @param mixed $value Incoming transient value.
621 * @return mixed The value unchanged.
622 */
623 function mlsimport_force_use_transient( $value ) {
624 return $value;
625 // return false;
626 }
627
628
629
630
631 // Instantiate the core orchestrator and register all admin/public hooks.
632 global $mlsimport;
633 $mlsimport = new Mlsimport();
634 $mlsimport->run();
635
636
637
638
639
640 // Map of supported theme IDs to human labels. 990 = standalone (no host theme
641 // dependency); 991-994 are the four supported real-estate themes.
642 $supported_theme = array(
643 990 => 'Standalone (any theme)',
644 991 => 'WpResidence',
645 992 => 'Houzez',
646 993 => 'Real Homes',
647 994 => 'Wpestate',
648
649 );
650
651 // Expose the theme map as a constant for use across the plugin.
652 define( 'MLSIMPORT_THEME', $supported_theme );
653
654 add_filter( 'action_scheduler_failure_period', 'mlsimport_saas_filter_timelimit' );
655 /**
656 * Raise the Action Scheduler failure timeout so long imports are not marked failed.
657 *
658 * @param int $time_limit Default failure period in seconds (unused).
659 * @return int Fixed failure period of 3000 seconds.
660 */
661 function mlsimport_saas_filter_timelimit( $time_limit ) {
662 return 3000;
663 }
664
665
666
667 /*
668 *
669 * Write logs
670 *
671 **/
672
673 /**
674 * Append a timestamped line to a per-type import log file (when logging is on).
675 *
676 * @param string|array $message Message to log; arrays are JSON-encoded.
677 * @param string $tip_import Log bucket: normal|cron|delete|server_cron.
678 * @return void
679 */
680 function mlsimport_saas_single_write_import_custom_logs( $message, $tip_import = 'normal' ) {
681 // Check if logging is enabled
682 $enable_logs = intval( get_option( 'mlsimport_disable_logs' ) );
683 // Bail unless the "logs enabled" option equals exactly 1.
684 if ( 1 !== $enable_logs) {
685 return;
686 }
687
688 // Encode array payloads to JSON so they can be written as text.
689 if ( is_array( $message ) ) {
690 $message = wp_json_encode( $message );
691 }
692
693 // Prefix the message with a UTC timestamp.
694 $formatted_message = gmdate( 'F j, Y, g:i a' ) . ' -> ' . $message;
695
696 // Determine the log file path based on the import type
697 $log_file_name = 'cron' === $tip_import ? 'cron_logs' :
698 ( 'delete' === $tip_import ? 'delete_logs' :
699 ( 'server_cron' === $tip_import ? 'server_cron_logs' : 'import_logs' ) );
700
701 // Construct the full path with a date suffix
702 $log_file_path = WP_PLUGIN_DIR . "/mlsimport/logs/{$log_file_name}-" . gmdate( 'Y-m-d' ) . '.log';
703
704 // Error handling for file operations
705 try {
706 // Check and create the directory for logs if it does not exist
707 $log_dir = dirname( $log_file_path );
708 if ( ! file_exists( $log_dir ) ) {
709 mkdir( $log_dir, 0755, true );
710 }
711
712 // Append the formatted message to the log file
713 file_put_contents( $log_file_path, $formatted_message, FILE_APPEND | LOCK_EX );
714 } catch ( Exception $e ) {
715 // Handle the exception, such as logging the error elsewhere or sending a notification
716 }
717 }
718
719
720
721 /*
722 *
723 *
724 * Write Status logs
725 *
726 *
727 **/
728
729
730
731 /**
732 * Legacy status-log writer (superseded by mlsimport_debuglogs_per_plugin()).
733 *
734 * Note: writes with LOCK_EX but no FILE_APPEND, so it overwrites status_logs.log
735 * on each call. Retained under the _old suffix; not called anywhere.
736 *
737 * @param string|array $message Message to log; arrays are JSON-encoded.
738 * @return void
739 */
740 function mlsimport_debuglogs_per_plugin_old( $message ) {
741
742 // Encode array payloads to JSON.
743 if ( is_array( $message ) ) {
744 $message = wp_json_encode( $message );
745 }
746
747 global $wp_filesystem;
748 if ( empty( $wp_filesystem ) ) {
749 require_once ABSPATH . '/wp-admin/includes/file.php';
750 WP_Filesystem();
751 }
752
753 $path_status = WP_PLUGIN_DIR . '/mlsimport/logs/status_logs.log';
754 file_put_contents( $path_status, $message, LOCK_EX );
755 }
756 /**
757 * Append a status/debug line to logs/status_logs.log.
758 *
759 * @param string|array $message Message to log; arrays are JSON-encoded.
760 * @return void
761 */
762 function mlsimport_debuglogs_per_plugin( $message ) {
763
764 // Encode array payloads to JSON.
765 if ( is_array( $message ) ) {
766 $message = wp_json_encode( $message );
767 }
768
769 // Nothing to write for an empty message.
770 if ( empty( $message ) ) {
771 return; // Exit the function if there's nothing to log
772 }
773
774 // Target log file inside the plugin's logs/ directory.
775 $log_file_path = WP_PLUGIN_DIR . '/mlsimport/logs/status_logs.log';
776
777 // Check and create the directory for logs if it does not exist
778 $log_dir = dirname( $log_file_path );
779 if ( ! file_exists( $log_dir ) ) {
780 mkdir( $log_dir, 0755, true );
781 }
782
783 // Error handling for file operations
784 try {
785 // Append the message to the log file with a newline and acquire an exclusive lock during writing
786 file_put_contents( $log_file_path, $message . PHP_EOL, LOCK_EX );
787 } catch ( Exception $e ) {
788 // Handle the exception, such as logging the error elsewhere or sending a notification
789 }
790 }
791
792
793
794
795
796 /*
797 * Cron job trigger
798 *
799 *
800 *
801 **/
802
803
804 // */5 * * * * wget http://example.com/check */2
805 add_action( 'init', 'mlsimport_trigger_cron_job' );
806 /**
807 * Server-cron entry point hit via the ?mlsimport_cron=yes query parameter.
808 *
809 * Rate-limited to once every 2 hours. Note: the actual import call on the
810 * throttled branch is currently commented out, so this only writes a log line.
811 *
812 * @return void
813 */
814 function mlsimport_trigger_cron_job() {
815 // ?mlsimport_cron=yes
816 // Only proceed when the request carries mlsimport_cron=yes.
817 if ( isset( $_REQUEST['mlsimport_cron'] ) && 'yes' === sanitize_text_field( wp_unslash( $_REQUEST['mlsimport_cron'] ) ) ) {
818 // Timestamp of the previous server-cron run (0 if never run).
819 $last_run = intval( get_option( 'mlsimport_last_server_cron' ) );
820 // Current time.
821 $now = time();
822 // First-ever call: seed the last-run timestamp.
823 if ( 0 === intval($last_run) ) {
824 update_option( 'mlsimport_last_server_cron', $now );
825 }
826
827 // Only run if at least 2 hours have elapsed since the last run.
828 if ( $last_run < $now - ( 60 * 60 * 2 ) ) {
829 // Build the "triggered" log line and record the new run time.
830 $log = 'Server Cron Job triggered on ' . date( 'l jS \of F Y h:i:s A', $last_run ) . ' vs ' . gmdate( 'l jS \of F Y h:i:s A', $now ) . PHP_EOL;
831 // mlsimport_saas_event_mls_import_auto_function();
832 update_option( 'mlsimport_last_server_cron', $now );
833 } else {
834 $log = 'Server Cron Job Called but not triggered. Last run on ' . gmdate( 'l jS \of F Y h:i:s A', $last_run ) . ' vs ' . gmdate( 'l jS \of F Y h:i:s A', $now ) . PHP_EOL;
835 }
836
837 mlsimport_saas_single_write_import_custom_logs( $log, 'server_cron' );
838 }
839 }
840
841
842
843 /**
844 * Render the "Sign up for MLSImport" promo box (30-day free-trial CTA).
845 *
846 * Uses a WpResidence-specific affiliate URL when that theme is active.
847 *
848 * @return void
849 */
850 function mlsimport_show_signup() {
851 // Default sign-up URL.
852 $affiliate_url = 'https://mlsimport.com';
853 // Swap in the WpResidence affiliate/campaign link when that theme is active.
854 if ( function_exists( 'wp_estate_init' ) ) {
855 $affiliate_url = 'https://mlsimport.com/ref/1/?campaign=wpresidence';
856 }
857 // Output the promo markup.
858 ?>
859 <div class="mlsimport_signup">
860 <h3><?php esc_html_e('Import MLS Listings into your Real Estate website', 'mlsimport'); ?></h3>
861 <p><?php esc_html_e('Signup now and get 30-Days Free trial, no setup fee & cancel anytime at ', 'mlsimport'); ?><a href="https://mlsimport.com/mls-import-plugin-pricing/" target="_blank">MLSImport.com</a></p>
862 <a href="https://mlsimport.com/mls-import-plugin-pricing" class="button mlsimport_button mlsimport_signup_button" target="_blank"><?php esc_html_e('Create My Account', 'mlsimport'); ?></a>
863 </div>
864 <?php
865 }
866
867
868 //add_action('admin_init', 'force_recount_all_terms');
869 /**
870 * Maintenance utility: recalculate term counts for every taxonomy.
871 *
872 * Not hooked by default (the add_action above is commented out); intended to be
873 * run manually to fix drifted term counts. Echoes a completion message.
874 *
875 * @return void
876 */
877 function force_recount_all_terms() {
878 global $wpdb;
879
880 // Get all taxonomies
881 $taxonomies = get_taxonomies([], 'names');
882
883 // Recount terms for each registered taxonomy.
884 foreach ($taxonomies as $taxonomy) {
885 // Get all terms for the taxonomy
886 $terms = get_terms([
887 'taxonomy' => $taxonomy,
888 'hide_empty' => false, // Include terms with 0 count
889 'fields' => 'ids', // Get only the term IDs
890 ]);
891
892 if (!is_wp_error($terms) && !empty($terms)) {
893 // Get term_taxonomy_ids for these terms
894 $term_taxonomy_ids = $wpdb->get_col($wpdb->prepare(
895 "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE term_id IN (" . implode(',', array_map('intval', $terms)) . ")"
896 ));
897
898 // Update term counts
899 if (!empty($term_taxonomy_ids)) {
900 wp_update_term_count_now($term_taxonomy_ids, $taxonomy);
901 }
902 }
903 }
904
905 echo "Term counts have been recalculated for all taxonomies.";
906 }
907
908
909 // The theme <select> builder lives in includes/mlsimport-theme-detection.php,
910 // next to mlsimport_resolve_theme_id() whose answer it renders (#242).
911
912
913
914 // mlsimport_save_account_callback() (wp_ajax_mlsimport_save_account) moved to
915 // includes/mlsimport-onboarding.php next to its sibling handler
916 // mlsimport_ajax_test_account_connection(), so both credential-save paths live
917 // together and are covered by tests/unit/save-account-token-purge-test.php.
918
919
920
921
922
923
924 add_action('wp_ajax_mlsimport_save_mls_data', 'mlsimport_save_mls_data_callback');
925 function mlsimport_save_mls_data_callback() {
926 check_ajax_referer('mlsimport_onboarding_nonce', 'security');
927
928 $options = get_option('mlsimport_admin_options', []);
929 $options = is_array( $options ) ? $options : array();
930 $previous_mls_id = isset( $options['mlsimport_mls_name'] )
931 ? (string) $options['mlsimport_mls_name']
932 : '';
933
934 foreach ($_POST as $key => $value) {
935 if (strpos($key, 'mlsimport_') === 0 && $key !== 'mlsimport_username' && $key !== 'mlsimport_password') {
936 $options[$key] = sanitize_text_field($value);
937 }
938 }
939
940 // An MLS change invalidates only state owned by the old selection. Provider
941 // credentials remain saved so returning to that provider restores its fields.
942 $new_mls_id = isset( $options['mlsimport_mls_name'] )
943 ? (string) $options['mlsimport_mls_name']
944 : '';
945 if ( $previous_mls_id !== $new_mls_id ) {
946 Mlsimport_Provider_Family::clear_active_state();
947 } else {
948 Mlsimport_Provider_Family::clear_access_tokens();
949 delete_option( 'mlsimport_connection_test' );
950 delete_option( 'mlsimport_mls_metadata_populated' );
951 }
952
953 update_option('mlsimport_admin_options', $options);
954
955 // Always test the newly saved selection. Reusing a prior "yes" flag could
956 // incorrectly report that a different MLS or changed credentials succeeded.
957 global $mlsimport;
958 $mlsimport->admin->mlsimport_saas_setting_up();
959 $mlsimport->admin->mlsimport_saas_check_mls_connection();
960 $is_mls_connected = get_option('mlsimport_connection_test', '');
961
962 ob_start();
963 if ('yes' === $is_mls_connected) {
964 ?>
965 <div class="mlsimport_warning mlsimport_validated">
966 <?php esc_html_e('You’re now connected to your MLS.', 'mlsimport'); ?>
967 </div>
968 <?php
969 } else {
970 ?>
971 <div class="mlsimport_warning">
972 <?php esc_html_e('The connection to your MLS was NOT successful. Please check the authentication token is correct and check your MLS Data Access Application is approved.', 'mlsimport'); ?>
973 </div>
974 <?php
975 }
976 $html = ob_get_clean();
977
978 wp_send_json_success([
979 'message' => __('MLS data saved', 'mlsimport'),
980 'html' => $html,
981 'connected' => $is_mls_connected === 'yes',
982 ]);
983 }
984