PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.1.1
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.1.1
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.1, at mlsimport.php

982 lines 43.1 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.1
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.1');
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 add_action( 'before_delete_post', array( 'StandaloneClass', 'cleanup_on_delete' ) );
377 // Trash/unpublish (not a permanent delete) must also drop the listings row, so the
378 // search index only ever holds published listings.
379 add_action( 'transition_post_status', array( 'StandaloneClass', 'cleanup_on_status_change' ), 10, 3 );
380
381 if ( defined( 'WP_CLI' ) && WP_CLI ) {
382 WP_CLI::add_command(
383 'mlsimport reindex',
384 function () {
385 $rebuilt = Mlsimport_Standalone_Reindex::rebuild_all();
386 WP_CLI::success( "Reindexed {$rebuilt} standalone listings." );
387 }
388 );
389 }
390
391 if ( ! wp_next_scheduled( 'event_mls_import_auto' ) ) {
392 wp_schedule_event( time(), 'hourly', 'event_mls_import_auto' );
393 }
394
395
396 /**
397 * Scheduled event: Processes MLSimport items marked for cron processing, in memory-safe batches.
398 *
399 * This function is triggered by the 'event_mls_import_auto' action.
400 * It fetches mlsimport_item post IDs in small batches (not all at once!) to minimize memory usage.
401 * Only posts with meta 'mlsimport_item_stat_cron' = 1 are processed.
402 * For each item, calls mlsimport_saas_start_cron_links_per_item().
403 *
404 * Optimizations:
405 * - Uses 'fields' => 'ids' so only post IDs are loaded (saves memory)
406 * - Batches with posts_per_page/paged, so memory does not spike for large data sets
407 * - Calls gc_collect_cycles() periodically to further reduce memory leaks
408 * - Skips processing if MLS is not connected or token is missing
409 *
410 * @return void
411 */
412 add_action('event_mls_import_auto', 'mlsimport_saas_event_mls_import_auto_function');
413 /**
414 * Scheduled event handler for MLS Import Auto (runs via WP Cron).
415 * Processes mlsimport_item posts in batches and logs memory usage.
416 */
417 function mlsimport_saas_event_mls_import_auto_function() {
418 global $mlsimport;
419
420 //error_log('[AutoCron] Start: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB');
421
422 // Watchdog backstop (issue #199): a chunked manual import whose worker
423 // chain died is normally revived by the polled progress screen, but if
424 // the administrator closed that screen nothing else watches the run.
425 // Hourly cron picks it up here; revive() is a cheap no-op for anything
426 // that is not a silent, stalled manual run.
427 // Import-health checks (#208) run before anything can bail out below:
428 // detect a previous cron run that died mid-loop, and a manual run stuck
429 // at "Preparing". Both open one deduplicated internal incident.
430 mlsimport_cron_heartbeat_check();
431 mlsimport_import_health_watch_manual_run();
432
433 $mlsimport_active_lock = get_option( 'mlsimport_import_run_lock', array() );
434 if ( is_array( $mlsimport_active_lock ) && ! empty( $mlsimport_active_lock['task_id'] ) ) {
435 $mlsimport_revive = $mlsimport->admin->mlsimport_import_task_execution()->revive( (int) $mlsimport_active_lock['task_id'] );
436 if ( true === ( $mlsimport_revive['revived'] ?? false ) ) {
437 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' );
438 } elseif ( 'stalled' === ( $mlsimport_revive['reason'] ?? '' ) ) {
439 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' );
440 // The run was terminated as hopeless — tell the SaaS once (#208).
441 mlsimport_alert_open(
442 'import_stalled:' . (int) $mlsimport_active_lock['task_id'],
443 'import_stalled',
444 array( 'task_id' => (int) $mlsimport_active_lock['task_id'] )
445 );
446 }
447 }
448
449 // 0. Bail if a run is already in progress. Without this guard an overlapping
450 // cron fire processes the same listings in parallel, causing duplicate
451 // listing_key inserts and term_relationship/term_count deadlocks. The TTL is
452 // the safety net if a run dies mid-loop without reaching the release below.
453 if ( get_transient( 'mlsimport_cron_running' ) ) {
454 return;
455 }
456
457 // 1. Get the API token from transient - exit if not set
458 $token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient();
459 //error_log('[AutoCron] After token fetch: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB');
460 if (trim($token) === '') {
461 // Silent exit made frozen sites undiagnosable (issue #207 finding 3):
462 // record the failed attempt with a real class before bailing.
463 mlsimport_telemetry_set( 'last_sync_failed', time() );
464 mlsimport_telemetry_set( 'last_sync_failed_code', 'no_token' );
465 //error_log('[AutoCron] No token, exiting.');
466 return;
467 }
468
469 // 2. Check if MLS connection is valid - exit if not
470 $is_mls_connected = get_option('mlsimport_connection_test', '');
471 //error_log('[AutoCron] After connection check: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB');
472 if ('yes' !== $is_mls_connected) {
473 // Same rule as the token exit above: a sync attempt that cannot run
474 // records why, so the heartbeat can surface it.
475 mlsimport_telemetry_set( 'last_sync_failed', time() );
476 mlsimport_telemetry_set( 'last_sync_failed_code', 'mls_not_connected' );
477 //error_log('[AutoCron] No valid connection, exiting.');
478 return;
479 }
480
481 // Claim the run lock now that we are committed to processing.
482 set_transient( 'mlsimport_cron_running', 1, 15 * MINUTE_IN_SECONDS );
483
484 // Heartbeat (#208): record that a cron import is now running, so the next
485 // cron entry can tell a clean finish from a process that died mid-loop.
486 mlsimport_cron_heartbeat_start();
487
488 // Record sync attempt in telemetry
489 mlsimport_telemetry_bump( 'syncs' );
490 mlsimport_telemetry_set( 'last_sync_attempt', time() );
491
492 // 3. Set batch size for gathering and initialize loop variables
493 $batch_size = 100;
494 $paged = 1;
495 $total_processed = 0;
496
497 // 4. Gather every cron-enabled task id first (ids only — a few bytes each,
498 // still fetched in paged batches so the query never loads post objects).
499 // Gathering before processing is what makes fair ordering possible below.
500 $cron_task_ids = array();
501 do {
502 // Prepare query: only IDs, filter by meta key, batch, paged, no_found_rows speeds up query
503 $args = array(
504 'post_type' => 'mlsimport_item',
505 'post_status' => 'any',
506 'posts_per_page' => $batch_size,
507 'paged' => $paged,
508 'fields' => 'ids',
509 'meta_query' => array(
510 array(
511 'key' => 'mlsimport_item_stat_cron',
512 'value' => 1,
513 'compare' => '=',
514 ),
515 ),
516 'no_found_rows' => true,
517 );
518
519 // Get post IDs for this batch
520 $post_ids = get_posts($args);
521
522 // If nothing is returned, the gather is complete
523 if (empty($post_ids)) {
524 break;
525 }
526
527 foreach ($post_ids as $prop_id) {
528 $cron_task_ids[] = (int) $prop_id;
529 }
530
531 // Prepare next batch
532 $paged++;
533 unset($post_ids); // Free memory
534
535 } while (true);
536
537 // 5. Order by starvation (issue #203): the query above returns tasks in
538 // the same fixed order every hour, so when an early large task ate the
539 // whole cycle the bottom tasks were skipped run after run. Sorting by the
540 // last-sync watermark puts the longest-unsynced region first in line.
541 $cron_task_watermarks = array();
542 foreach ($cron_task_ids as $prop_id) {
543 $cron_task_watermarks[ $prop_id ] = (string) get_post_meta( $prop_id, 'mlsimport_last_date', true );
544 }
545 unset($cron_task_ids);
546
547 // 6. Process every task, most starved first
548 foreach (mlsimport_cron_task_order($cron_task_watermarks) as $prop_id) {
549 $logs = 'Loop custom post: ' . $prop_id . PHP_EOL;
550 mlsimport_debuglogs_per_plugin($logs);
551
552 // Call processing function for this item. The feed count it pulls
553 // is recorded inside mlsimport_make_listing_requests() (last_feed_found).
554 $mlsimport->admin->mlsimport_saas_start_cron_links_per_item($prop_id);
555
556 $total_processed++;
557
558 // Heartbeat (#208): measurable progress for the stuck-run check.
559 mlsimport_cron_heartbeat_progress( $total_processed );
560
561 // Free memory every 100 processed items
562 if ($total_processed % 100 === 0) {
563 gc_collect_cycles();
564 //error_log("[AutoCron] Processed {$total_processed} total, memory: " . (memory_get_usage(true) / 1024 / 1024) . ' MB');
565 }
566 }
567
568 // Heartbeat (#208): clean finish — also resolves an open died-run incident.
569 mlsimport_cron_heartbeat_finish();
570
571 // Release the run lock so the next scheduled run can proceed.
572 delete_transient( 'mlsimport_cron_running' );
573
574 // last_sync_success is no longer stamped here (issue #207 finding 1): the
575 // end-of-loop stamp reported success even when every request failed, and
576 // never fired when a run died mid-loop. Each listings request now records
577 // its own outcome inside mlsimport_make_listing_requests().
578
579 //error_log('[AutoCron] Done, total processed: ' . $total_processed . ', end memory: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB');
580 }
581
582
583
584 /*
585 * Reconciliation Mechanism
586 *
587 *
588 *
589 **/
590
591 if ( ! wp_next_scheduled( 'mlsimport_reconciliation_event' ) ) {
592 wp_schedule_event( time(), 'daily', 'mlsimport_reconciliation_event' );
593 }
594
595 add_action( 'mlsimport_reconciliation_event', 'mlsimport_saas_reconciliation_event_function' );
596 add_action( 'mlsimport_reconciliation_retry_event', 'mlsimport_saas_reconciliation_event_function' );
597
598 if ( ! wp_next_scheduled( 'mlsimport_daily_telemetry_event' ) ) {
599 wp_schedule_event( time(), 'daily', 'mlsimport_daily_telemetry_event' );
600 }
601
602 add_action( 'mlsimport_daily_telemetry_event', 'mlsimport_telemetry_run_daily' );
603
604
605 /*
606 * Force use of transient
607 *
608 *
609 *
610 **/
611
612 /**
613 * Filter callback stub that returns the transient value unchanged.
614 *
615 * Left as a pass-through hook point; the commented `return false;` would force
616 * a transient miss for debugging.
617 *
618 * @param mixed $value Incoming transient value.
619 * @return mixed The value unchanged.
620 */
621 function mlsimport_force_use_transient( $value ) {
622 return $value;
623 // return false;
624 }
625
626
627
628
629 // Instantiate the core orchestrator and register all admin/public hooks.
630 global $mlsimport;
631 $mlsimport = new Mlsimport();
632 $mlsimport->run();
633
634
635
636
637
638 // Map of supported theme IDs to human labels. 990 = standalone (no host theme
639 // dependency); 991-994 are the four supported real-estate themes.
640 $supported_theme = array(
641 990 => 'Standalone (any theme)',
642 991 => 'WpResidence',
643 992 => 'Houzez',
644 993 => 'Real Homes',
645 994 => 'Wpestate',
646
647 );
648
649 // Expose the theme map as a constant for use across the plugin.
650 define( 'MLSIMPORT_THEME', $supported_theme );
651
652 add_filter( 'action_scheduler_failure_period', 'mlsimport_saas_filter_timelimit' );
653 /**
654 * Raise the Action Scheduler failure timeout so long imports are not marked failed.
655 *
656 * @param int $time_limit Default failure period in seconds (unused).
657 * @return int Fixed failure period of 3000 seconds.
658 */
659 function mlsimport_saas_filter_timelimit( $time_limit ) {
660 return 3000;
661 }
662
663
664
665 /*
666 *
667 * Write logs
668 *
669 **/
670
671 /**
672 * Append a timestamped line to a per-type import log file (when logging is on).
673 *
674 * @param string|array $message Message to log; arrays are JSON-encoded.
675 * @param string $tip_import Log bucket: normal|cron|delete|server_cron.
676 * @return void
677 */
678 function mlsimport_saas_single_write_import_custom_logs( $message, $tip_import = 'normal' ) {
679 // Check if logging is enabled
680 $enable_logs = intval( get_option( 'mlsimport_disable_logs' ) );
681 // Bail unless the "logs enabled" option equals exactly 1.
682 if ( 1 !== $enable_logs) {
683 return;
684 }
685
686 // Encode array payloads to JSON so they can be written as text.
687 if ( is_array( $message ) ) {
688 $message = wp_json_encode( $message );
689 }
690
691 // Prefix the message with a UTC timestamp.
692 $formatted_message = gmdate( 'F j, Y, g:i a' ) . ' -> ' . $message;
693
694 // Determine the log file path based on the import type
695 $log_file_name = 'cron' === $tip_import ? 'cron_logs' :
696 ( 'delete' === $tip_import ? 'delete_logs' :
697 ( 'server_cron' === $tip_import ? 'server_cron_logs' : 'import_logs' ) );
698
699 // Construct the full path with a date suffix
700 $log_file_path = WP_PLUGIN_DIR . "/mlsimport/logs/{$log_file_name}-" . gmdate( 'Y-m-d' ) . '.log';
701
702 // Error handling for file operations
703 try {
704 // Check and create the directory for logs if it does not exist
705 $log_dir = dirname( $log_file_path );
706 if ( ! file_exists( $log_dir ) ) {
707 mkdir( $log_dir, 0755, true );
708 }
709
710 // Append the formatted message to the log file
711 file_put_contents( $log_file_path, $formatted_message, FILE_APPEND | LOCK_EX );
712 } catch ( Exception $e ) {
713 // Handle the exception, such as logging the error elsewhere or sending a notification
714 }
715 }
716
717
718
719 /*
720 *
721 *
722 * Write Status logs
723 *
724 *
725 **/
726
727
728
729 /**
730 * Legacy status-log writer (superseded by mlsimport_debuglogs_per_plugin()).
731 *
732 * Note: writes with LOCK_EX but no FILE_APPEND, so it overwrites status_logs.log
733 * on each call. Retained under the _old suffix; not called anywhere.
734 *
735 * @param string|array $message Message to log; arrays are JSON-encoded.
736 * @return void
737 */
738 function mlsimport_debuglogs_per_plugin_old( $message ) {
739
740 // Encode array payloads to JSON.
741 if ( is_array( $message ) ) {
742 $message = wp_json_encode( $message );
743 }
744
745 global $wp_filesystem;
746 if ( empty( $wp_filesystem ) ) {
747 require_once ABSPATH . '/wp-admin/includes/file.php';
748 WP_Filesystem();
749 }
750
751 $path_status = WP_PLUGIN_DIR . '/mlsimport/logs/status_logs.log';
752 file_put_contents( $path_status, $message, LOCK_EX );
753 }
754 /**
755 * Append a status/debug line to logs/status_logs.log.
756 *
757 * @param string|array $message Message to log; arrays are JSON-encoded.
758 * @return void
759 */
760 function mlsimport_debuglogs_per_plugin( $message ) {
761
762 // Encode array payloads to JSON.
763 if ( is_array( $message ) ) {
764 $message = wp_json_encode( $message );
765 }
766
767 // Nothing to write for an empty message.
768 if ( empty( $message ) ) {
769 return; // Exit the function if there's nothing to log
770 }
771
772 // Target log file inside the plugin's logs/ directory.
773 $log_file_path = WP_PLUGIN_DIR . '/mlsimport/logs/status_logs.log';
774
775 // Check and create the directory for logs if it does not exist
776 $log_dir = dirname( $log_file_path );
777 if ( ! file_exists( $log_dir ) ) {
778 mkdir( $log_dir, 0755, true );
779 }
780
781 // Error handling for file operations
782 try {
783 // Append the message to the log file with a newline and acquire an exclusive lock during writing
784 file_put_contents( $log_file_path, $message . PHP_EOL, LOCK_EX );
785 } catch ( Exception $e ) {
786 // Handle the exception, such as logging the error elsewhere or sending a notification
787 }
788 }
789
790
791
792
793
794 /*
795 * Cron job trigger
796 *
797 *
798 *
799 **/
800
801
802 // */5 * * * * wget http://example.com/check */2
803 add_action( 'init', 'mlsimport_trigger_cron_job' );
804 /**
805 * Server-cron entry point hit via the ?mlsimport_cron=yes query parameter.
806 *
807 * Rate-limited to once every 2 hours. Note: the actual import call on the
808 * throttled branch is currently commented out, so this only writes a log line.
809 *
810 * @return void
811 */
812 function mlsimport_trigger_cron_job() {
813 // ?mlsimport_cron=yes
814 // Only proceed when the request carries mlsimport_cron=yes.
815 if ( isset( $_REQUEST['mlsimport_cron'] ) && 'yes' === sanitize_text_field( wp_unslash( $_REQUEST['mlsimport_cron'] ) ) ) {
816 // Timestamp of the previous server-cron run (0 if never run).
817 $last_run = intval( get_option( 'mlsimport_last_server_cron' ) );
818 // Current time.
819 $now = time();
820 // First-ever call: seed the last-run timestamp.
821 if ( 0 === intval($last_run) ) {
822 update_option( 'mlsimport_last_server_cron', $now );
823 }
824
825 // Only run if at least 2 hours have elapsed since the last run.
826 if ( $last_run < $now - ( 60 * 60 * 2 ) ) {
827 // Build the "triggered" log line and record the new run time.
828 $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;
829 // mlsimport_saas_event_mls_import_auto_function();
830 update_option( 'mlsimport_last_server_cron', $now );
831 } else {
832 $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;
833 }
834
835 mlsimport_saas_single_write_import_custom_logs( $log, 'server_cron' );
836 }
837 }
838
839
840
841 /**
842 * Render the "Sign up for MLSImport" promo box (30-day free-trial CTA).
843 *
844 * Uses a WpResidence-specific affiliate URL when that theme is active.
845 *
846 * @return void
847 */
848 function mlsimport_show_signup() {
849 // Default sign-up URL.
850 $affiliate_url = 'https://mlsimport.com';
851 // Swap in the WpResidence affiliate/campaign link when that theme is active.
852 if ( function_exists( 'wp_estate_init' ) ) {
853 $affiliate_url = 'https://mlsimport.com/ref/1/?campaign=wpresidence';
854 }
855 // Output the promo markup.
856 ?>
857 <div class="mlsimport_signup">
858 <h3><?php esc_html_e('Import MLS Listings into your Real Estate website', 'mlsimport'); ?></h3>
859 <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>
860 <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>
861 </div>
862 <?php
863 }
864
865
866 //add_action('admin_init', 'force_recount_all_terms');
867 /**
868 * Maintenance utility: recalculate term counts for every taxonomy.
869 *
870 * Not hooked by default (the add_action above is commented out); intended to be
871 * run manually to fix drifted term counts. Echoes a completion message.
872 *
873 * @return void
874 */
875 function force_recount_all_terms() {
876 global $wpdb;
877
878 // Get all taxonomies
879 $taxonomies = get_taxonomies([], 'names');
880
881 // Recount terms for each registered taxonomy.
882 foreach ($taxonomies as $taxonomy) {
883 // Get all terms for the taxonomy
884 $terms = get_terms([
885 'taxonomy' => $taxonomy,
886 'hide_empty' => false, // Include terms with 0 count
887 'fields' => 'ids', // Get only the term IDs
888 ]);
889
890 if (!is_wp_error($terms) && !empty($terms)) {
891 // Get term_taxonomy_ids for these terms
892 $term_taxonomy_ids = $wpdb->get_col($wpdb->prepare(
893 "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE term_id IN (" . implode(',', array_map('intval', $terms)) . ")"
894 ));
895
896 // Update term counts
897 if (!empty($term_taxonomy_ids)) {
898 wp_update_term_count_now($term_taxonomy_ids, $taxonomy);
899 }
900 }
901 }
902
903 echo "Term counts have been recalculated for all taxonomies.";
904 }
905
906
907 // The theme <select> builder lives in includes/mlsimport-theme-detection.php,
908 // next to mlsimport_resolve_theme_id() whose answer it renders (#242).
909
910
911
912 // mlsimport_save_account_callback() (wp_ajax_mlsimport_save_account) moved to
913 // includes/mlsimport-onboarding.php next to its sibling handler
914 // mlsimport_ajax_test_account_connection(), so both credential-save paths live
915 // together and are covered by tests/unit/save-account-token-purge-test.php.
916
917
918
919
920
921
922 add_action('wp_ajax_mlsimport_save_mls_data', 'mlsimport_save_mls_data_callback');
923 function mlsimport_save_mls_data_callback() {
924 check_ajax_referer('mlsimport_onboarding_nonce', 'security');
925
926 $options = get_option('mlsimport_admin_options', []);
927 $options = is_array( $options ) ? $options : array();
928 $previous_mls_id = isset( $options['mlsimport_mls_name'] )
929 ? (string) $options['mlsimport_mls_name']
930 : '';
931
932 foreach ($_POST as $key => $value) {
933 if (strpos($key, 'mlsimport_') === 0 && $key !== 'mlsimport_username' && $key !== 'mlsimport_password') {
934 $options[$key] = sanitize_text_field($value);
935 }
936 }
937
938 // An MLS change invalidates only state owned by the old selection. Provider
939 // credentials remain saved so returning to that provider restores its fields.
940 $new_mls_id = isset( $options['mlsimport_mls_name'] )
941 ? (string) $options['mlsimport_mls_name']
942 : '';
943 if ( $previous_mls_id !== $new_mls_id ) {
944 Mlsimport_Provider_Family::clear_active_state();
945 } else {
946 Mlsimport_Provider_Family::clear_access_tokens();
947 delete_option( 'mlsimport_connection_test' );
948 delete_option( 'mlsimport_mls_metadata_populated' );
949 }
950
951 update_option('mlsimport_admin_options', $options);
952
953 // Always test the newly saved selection. Reusing a prior "yes" flag could
954 // incorrectly report that a different MLS or changed credentials succeeded.
955 global $mlsimport;
956 $mlsimport->admin->mlsimport_saas_setting_up();
957 $mlsimport->admin->mlsimport_saas_check_mls_connection();
958 $is_mls_connected = get_option('mlsimport_connection_test', '');
959
960 ob_start();
961 if ('yes' === $is_mls_connected) {
962 ?>
963 <div class="mlsimport_warning mlsimport_validated">
964 <?php esc_html_e('You’re now connected to your MLS.', 'mlsimport'); ?>
965 </div>
966 <?php
967 } else {
968 ?>
969 <div class="mlsimport_warning">
970 <?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'); ?>
971 </div>
972 <?php
973 }
974 $html = ob_get_clean();
975
976 wp_send_json_success([
977 'message' => __('MLS data saved', 'mlsimport'),
978 'html' => $html,
979 'connected' => $is_mls_connected === 'yes',
980 ]);
981 }
982