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

969 lines 39.0 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.0.7
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.0.7');
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_daily_telemetry_event' );
100 Mlsimport_Deactivator::deactivate();
101 }
102
103
104
105 register_activation_hook( __FILE__, 'mlsimport_activate' );
106 register_deactivation_hook( __FILE__, 'mlsimport_deactivate' );
107
108 /**
109 * Show one-time modal about the switch from Delete Statuses to Protected Statuses.
110 */
111 add_action( 'admin_footer', 'mlsimport_protected_statuses_upgrade_modal' );
112 function mlsimport_protected_statuses_upgrade_modal() {
113 // Bail if the user already acknowledged/dismissed the notice.
114 if ( get_option( 'mlsimport_dismiss_protected_status_notice' ) ) {
115 return;
116 }
117 // Only show for sites that had a version before 6.2 (not fresh installs)
118 $show_modal = get_option( 'mlsimport_show_protected_status_modal' );
119 // Bail when the upgrade flag was never set for this site.
120 if ( ! $show_modal ) {
121 return;
122 }
123 // Nonce for the dismissal AJAX call embedded in the modal's inline script.
124 $nonce = wp_create_nonce( 'mlsimport_dismiss_protected_notice' );
125 // Link the user to their Import Tasks list to set Protected Statuses.
126 $import_tasks_url = admin_url( 'edit.php?post_type=mlsimport_item' );
127 // Emit the modal markup + inline dismissal script into the admin footer.
128 ?>
129 <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;">
130 <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);">
131 <h2 style="margin-top:0;color:#d63638;">MLSImport - Important Change</h2>
132 <p><strong>"Delete Statuses"</strong> have been removed. The plugin now uses <strong>Protected Statuses</strong> only.</p>
133 <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>
134 <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>
135 <button id="mlsimport-acknowledge-btn" class="button button-primary" style="margin-top:10px;font-size:14px;padding:6px 24px;">I acknowledge</button>
136 </div>
137 </div>
138 <script>
139 document.getElementById('mlsimport-acknowledge-btn').addEventListener('click', function() {
140 var btn = this;
141 btn.disabled = true;
142 btn.textContent = 'Saving...';
143 var xhr = new XMLHttpRequest();
144 xhr.open('POST', ajaxurl);
145 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
146 xhr.onload = function() {
147 document.getElementById('mlsimport-protected-status-modal').style.display = 'none';
148 };
149 xhr.send('action=mlsimport_dismiss_protected_notice&_wpnonce=<?php echo esc_js( $nonce ); ?>');
150 });
151 </script>
152 <?php
153 }
154 add_action( 'wp_ajax_mlsimport_dismiss_protected_notice', 'mlsimport_handle_dismiss_protected_notice' );
155 /**
156 * AJAX handler: persist the user's dismissal of the protected-statuses modal.
157 *
158 * Verifies the nonce, records the permanent "dismissed" flag, clears the
159 * "show modal" flag, and returns a success JSON response.
160 *
161 * @return void
162 */
163 function mlsimport_handle_dismiss_protected_notice() {
164 // Verify the nonce created in the modal markup above.
165 check_ajax_referer( 'mlsimport_dismiss_protected_notice' );
166 // Persist that this notice has been acknowledged so it never shows again.
167 update_option( 'mlsimport_dismiss_protected_status_notice', true );
168 // Remove the one-time "show modal" flag.
169 delete_option( 'mlsimport_show_protected_status_modal' );
170 // Return an empty success payload to the inline script.
171 wp_send_json_success();
172 }
173
174 /**
175 * Track installed version and flag upgrade modals.
176 * Fresh installs get current version immediately, so no upgrade modal.
177 * Upgrades from < 6.2 flag the protected status modal.
178 */
179 $mlsimport_prev_version = get_option( 'mlsimport_installed_version', '' );
180 if ( $mlsimport_prev_version !== MLSIMPORT_VERSION ) {
181 if ( ! empty( $mlsimport_prev_version ) && version_compare( $mlsimport_prev_version, '6.2', '<' ) ) {
182 update_option( 'mlsimport_show_protected_status_modal', true );
183 }
184 update_option( 'mlsimport_installed_version', MLSIMPORT_VERSION );
185 }
186
187 /**
188 * The core plugin class that is used to define internationalization,
189 * admin-specific hooks, and public-facing site hooks.
190 */
191
192 /*
193 * Action Scheduler — the plugin's only third-party runtime dependency.
194 *
195 * This is deliberately a direct require rather than `vendor/autoload.php`.
196 * Composer's generated autoloader is written differently depending on whether
197 * dev dependencies (phpunit, php_codesniffer, myclabs/deep-copy) happen to be
198 * installed at the time it was generated. Those dev packages are never shipped,
199 * so a dev-generated autoloader committed to the repo makes `autoload_files.php`
200 * eagerly require files that do not exist in the released plugin — a fatal error
201 * on activation, before any plugin code runs.
202 *
203 * Action Scheduler is built to be dropped into a plugin and included directly
204 * (that is how WooCommerce loads it); it registers its own class loader and
205 * negotiates versions with any other copy already loaded on the site. So there
206 * is nothing left for the Composer autoloader to do at runtime, and not shipping
207 * it removes that whole failure mode. Composer is still used for dev tooling.
208 */
209 require_once plugin_dir_path( __FILE__ ) . 'vendor/woocommerce/action-scheduler/action-scheduler.php';
210 // Core includes: RESO field defs + helpers, provider map, cron/reconciliation
211 // guards, status taxonomy/normalizer, then the orchestrator and API client.
212 require_once plugin_dir_path( __FILE__ ) . 'includes/help_functions.php';
213 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-provider-map.php';
214 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-reconciliation-guard.php';
215 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-cron-guard.php';
216 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-status-taxonomy.php';
217 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-status-normalize.php';
218 require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport.php';
219 require_once plugin_dir_path( __FILE__ ) . 'includes/ThemeImport.php';
220 require_once plugin_dir_path( __FILE__ ) . 'enviroment/StandaloneClass.php';
221 require_once plugin_dir_path( __FILE__ ) . 'enviroment/ResidenceClass.php';
222 require_once plugin_dir_path( __FILE__ ) . 'enviroment/EstateClass.php';
223 require_once plugin_dir_path( __FILE__ ) . 'enviroment/HouzezClass.php';
224 require_once plugin_dir_path( __FILE__ ) . 'enviroment/RealHomesClass.php';
225 require_once plugin_dir_path( __FILE__ ) . 'enviroment/ResoBase.php';
226 require_once plugin_dir_path( __FILE__ ) . 'enviroment/SparkResoClass.php';
227 require_once plugin_dir_path( __FILE__ ) . 'enviroment/BridgeResoClass.php';
228 require_once plugin_dir_path( __FILE__ ) . 'enviroment/TresleResoClass.php';
229 require_once plugin_dir_path( __FILE__ ) . 'enviroment/MlsgridResoClass.php';
230 require_once plugin_dir_path( __FILE__ ) . 'enviroment/MlsgridResoClass.php';
231 require_once plugin_dir_path( __FILE__ ) . 'includes/addons/agents_offices.php';
232 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-onboarding.php';
233
234 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-field-selector-functions.php';
235 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-progressive-save.php';
236 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-telemetry.php';
237 require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-activity-log.php';
238
239 /*
240 * Standalone (theme_id 990) mode — own listings table, CPTs and taxonomies.
241 * Registered on every load (ADR-0003); the table is created on activation and
242 * upgraded behind a version guard on admin_init.
243 */
244 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/mlsimport-hooks.php'; // Hook reference + convention (loaded early).
245 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-table.php';
246 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-cpt.php';
247 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-term-select.php';
248 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-shortcodes.php';
249 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-block.php';
250 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-ajax.php';
251 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-reindex.php';
252 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-assets.php';
253 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-single.php';
254 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/property-section-registry.php';
255 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/property-print.php';
256 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-section-shortcodes.php';
257 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-section-blocks.php';
258 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-section-elementor.php';
259 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-lead.php';
260 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/agent-sections.php';
261 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/property-schema.php';
262 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-metabox.php';
263 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-agent-metabox.php';
264 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-term-meta.php';
265 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-columns.php';
266 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-favorites.php';
267 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/page-block-registry.php';
268 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-page-block-shortcodes.php';
269 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-page-block-blocks.php';
270 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-page-block-elementor.php';
271 require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-customizer.php'; // Standalone design settings in the WP Customizer (990 mode).
272 require_once plugin_dir_path( __FILE__ ) . 'includes/live/live-bootstrap.php'; // Live MLS passthrough mode (seam #1 — its only existing-file require).
273 add_action( 'init', array( 'Mlsimport_Standalone_Cpt', 'register' ) );
274 add_action( 'init', array( 'Mlsimport_Property_Metabox', 'register' ) );
275 add_action( 'init', array( 'Mlsimport_Agent_Metabox', 'register' ) );
276 add_action( 'init', array( 'Mlsimport_Term_Meta', 'register' ) );
277 add_action( 'init', array( 'Mlsimport_Property_Columns', 'register' ) );
278 add_action( 'init', array( 'Mlsimport_Standalone_Shortcodes', 'register' ) );
279 add_action( 'init', array( 'Mlsimport_Standalone_Block', 'register' ) );
280 add_action( 'init', array( 'Mlsimport_Standalone_Ajax', 'register' ) );
281 add_action( 'init', array( 'Mlsimport_Favorites', 'register' ) );
282 add_action( 'init', array( 'Mlsimport_Property_Section_Shortcodes', 'register' ) );
283 // Property-section Gutenberg blocks (mlsimport/property-*, "MLSImport — Property"
284 // category) are intentionally NOT registered — we don't expose them as blocks.
285 // The render dispatcher, shortcodes and Elementor widget behind them stay active.
286 // add_action( 'init', array( 'Mlsimport_Property_Section_Blocks', 'register' ) );
287 add_action( 'init', array( 'Mlsimport_Page_Block_Shortcodes', 'register' ) );
288 add_action( 'init', array( 'Mlsimport_Page_Block_Blocks', 'register' ) );
289 // Two MLSImport categories in the block inserter: single-property section blocks
290 // ("Property") and the page-builder blocks ("Real Estate").
291 add_filter(
292 'block_categories_all',
293 static function ( $categories ) {
294 return array_merge(
295 array(
296 array( 'slug' => 'mlsimport-property', 'title' => __( 'MLSImport — Property', 'mlsimport' ) ),
297 array( 'slug' => 'mlsimport-real-estate', 'title' => __( 'MLSImport — Real Estate', 'mlsimport' ) ),
298 ),
299 $categories
300 );
301 }
302 );
303 add_action( 'init', array( 'Mlsimport_Property_Lead', 'register' ) );
304 // Priority 20 (not the default 10) so the plugin's self-contained BEM stylesheets
305 // enqueue AFTER the active theme's own styles. Themes hook wp_enqueue_scripts at 10
306 // and, because plugins load before the theme, our default-10 callback would print
307 // FIRST — losing every equal-specificity tie to a theme rule that prints later.
308 // Hello Elementor's reset.css is the concrete case: it styles bare `button` /
309 // `[type=button]` (pink #c36, inline-block, width:auto), which ties our single-class
310 // `.mlsimport-*` button rules (0,1,0) and won on order, so the multiselect control,
311 // range/beds toggles, popup buttons, submit and the card heart all rendered narrow
312 // and pink. Printing after the theme lets our base rules win that tie. It stays a
313 // SINGLE class (0,1,0), so component state rules (:focus / :hover / .is-open at
314 // 0,1,1+) and any Elementor Style-tab control ({{WRAPPER}} … at 0,2,0+) still win —
315 // this only reclaims the tie against a generic theme reset, and never matches a
316 // non-plugin (e.g. Elementor) element.
317 add_action( 'wp_enqueue_scripts', array( 'Mlsimport_Standalone_Assets', 'enqueue' ), 20 );
318 add_action( 'wp_enqueue_scripts', array( 'Mlsimport_Property_Section_Assets', 'ensure_registered' ) );
319 // Pre-enqueue the single-property section assets into the <head>; the single
320 // template prints the header before any section renders, so the on-demand
321 // enqueue at render time lands in the footer and flashes unstyled (issue #172).
322 // Priority 20 for the same after-the-theme reason as the standalone assets above.
323 add_action( 'wp_enqueue_scripts', array( 'Mlsimport_Property_Section_Assets', 'enqueue_for_single' ), 20 );
324 // Same for the single-agent page (issue #188).
325 add_action( 'wp_enqueue_scripts', 'mlsimport_agent_enqueue_for_single', 20 );
326 // Load the same front-end CSS into the block editor so the dynamic blocks'
327 // ServerSideRender previews match the front end (editor-guarded inside the method).
328 add_action( 'enqueue_block_assets', array( 'Mlsimport_Standalone_Assets', 'enqueue_editor' ) );
329 add_filter( 'template_include', array( 'Mlsimport_Standalone_Single', 'template_include' ) );
330 add_action( 'wp_head', 'mlsimport_property_print_schema' );
331 Mlsimport_Property_Section_Elementor::register();
332 Mlsimport_Page_Block_Elementor::register();
333 // A contact-form lead carries no property and no agent; route those general
334 // enquiries to the "Contact form recipients" setting (decision 3).
335 add_filter(
336 'mlsimport_property_lead_recipient',
337 static function ( $to, $property_id, $agent_id = 0 ) {
338 if ( $property_id || $agent_id ) {
339 return $to;
340 }
341 $recipients = trim( (string) mlsimport_standalone_option( 'contact_form_recipients', '' ) );
342 return '' !== $recipients ? $recipients : $to;
343 },
344 10,
345 3
346 );
347 add_action( 'admin_init', array( 'Mlsimport_Standalone_Table', 'maybe_upgrade' ) );
348 add_action( 'before_delete_post', array( 'StandaloneClass', 'cleanup_on_delete' ) );
349 // Trash/unpublish (not a permanent delete) must also drop the listings row, so the
350 // search index only ever holds published listings.
351 add_action( 'transition_post_status', array( 'StandaloneClass', 'cleanup_on_status_change' ), 10, 3 );
352
353 if ( defined( 'WP_CLI' ) && WP_CLI ) {
354 WP_CLI::add_command(
355 'mlsimport reindex',
356 function () {
357 $rebuilt = Mlsimport_Standalone_Reindex::rebuild_all();
358 WP_CLI::success( "Reindexed {$rebuilt} standalone listings." );
359 }
360 );
361 }
362
363 if ( ! wp_next_scheduled( 'event_mls_import_auto' ) ) {
364 wp_schedule_event( time(), 'hourly', 'event_mls_import_auto' );
365 }
366
367
368 /**
369 * Scheduled event: Processes MLSimport items marked for cron processing, in memory-safe batches.
370 *
371 * This function is triggered by the 'event_mls_import_auto' action.
372 * It fetches mlsimport_item post IDs in small batches (not all at once!) to minimize memory usage.
373 * Only posts with meta 'mlsimport_item_stat_cron' = 1 are processed.
374 * For each item, calls mlsimport_saas_start_cron_links_per_item().
375 *
376 * Optimizations:
377 * - Uses 'fields' => 'ids' so only post IDs are loaded (saves memory)
378 * - Batches with posts_per_page/paged, so memory does not spike for large data sets
379 * - Calls gc_collect_cycles() periodically to further reduce memory leaks
380 * - Skips processing if MLS is not connected or token is missing
381 *
382 * @return void
383 */
384 add_action('event_mls_import_auto', 'mlsimport_saas_event_mls_import_auto_function');
385 /**
386 * Scheduled event handler for MLS Import Auto (runs via WP Cron).
387 * Processes mlsimport_item posts in batches and logs memory usage.
388 */
389 function mlsimport_saas_event_mls_import_auto_function() {
390 global $mlsimport;
391
392 //error_log('[AutoCron] Start: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB');
393
394 // 0. Bail if a run is already in progress. Without this guard an overlapping
395 // cron fire processes the same listings in parallel, causing duplicate
396 // listing_key inserts and term_relationship/term_count deadlocks. The TTL is
397 // the safety net if a run dies mid-loop without reaching the release below.
398 if ( get_transient( 'mlsimport_cron_running' ) ) {
399 return;
400 }
401
402 // 1. Get the API token from transient - exit if not set
403 $token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient();
404 //error_log('[AutoCron] After token fetch: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB');
405 if (trim($token) === '') {
406 //error_log('[AutoCron] No token, exiting.');
407 return;
408 }
409
410 // 2. Check if MLS connection is valid - exit if not
411 $is_mls_connected = get_option('mlsimport_connection_test', '');
412 //error_log('[AutoCron] After connection check: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB');
413 if ('yes' !== $is_mls_connected) {
414 //error_log('[AutoCron] No valid connection, exiting.');
415 return;
416 }
417
418 // Claim the run lock now that we are committed to processing.
419 set_transient( 'mlsimport_cron_running', 1, 15 * MINUTE_IN_SECONDS );
420
421 // Record sync attempt in telemetry
422 mlsimport_telemetry_bump( 'syncs' );
423 mlsimport_telemetry_set( 'last_sync_attempt', time() );
424
425 // 3. Set batch size for processing and initialize loop variables
426 $batch_size = 100;
427 $paged = 1;
428 $total_processed = 0;
429
430 // 4. Process in batches until no more items are found
431 do {
432 // Prepare query: only IDs, filter by meta key, batch, paged, no_found_rows speeds up query
433 $args = array(
434 'post_type' => 'mlsimport_item',
435 'post_status' => 'any',
436 'posts_per_page' => $batch_size,
437 'paged' => $paged,
438 'fields' => 'ids',
439 'meta_query' => array(
440 array(
441 'key' => 'mlsimport_item_stat_cron',
442 'value' => 1,
443 'compare' => '=',
444 ),
445 ),
446 'no_found_rows' => true,
447 );
448
449 // Get post IDs for this batch
450 $post_ids = get_posts($args);
451 //error_log("[AutoCron] Batch {$paged} fetched " . count($post_ids) . " items, memory: " . (memory_get_usage(true) / 1024 / 1024) . ' MB');
452
453 // If nothing is returned, break the loop
454 if (empty($post_ids)) {
455 break;
456 }
457
458 // 5. Loop through each post ID in this batch
459 foreach ($post_ids as $prop_id) {
460 $logs = 'Loop custom post: ' . $prop_id . PHP_EOL;
461 mlsimport_debuglogs_per_plugin($logs);
462
463 // Call processing function for this item. The feed count it pulls
464 // is recorded inside mlsimport_make_listing_requests() (last_feed_found).
465 $mlsimport->admin->mlsimport_saas_start_cron_links_per_item($prop_id);
466
467 $total_processed++;
468
469 // Free memory every 100 processed items
470 if ($total_processed % 100 === 0) {
471 gc_collect_cycles();
472 //error_log("[AutoCron] Processed {$total_processed} total, memory: " . (memory_get_usage(true) / 1024 / 1024) . ' MB');
473 }
474 }
475
476 // 6. Prepare next batch
477 $paged++;
478 unset($post_ids); // Free memory
479 gc_collect_cycles(); // Trigger garbage collection
480 //error_log("[AutoCron] After batch {$paged}, memory: " . (memory_get_usage(true) / 1024 / 1024) . ' MB');
481
482 } while (true);
483
484 // Release the run lock so the next scheduled run can proceed.
485 delete_transient( 'mlsimport_cron_running' );
486
487 mlsimport_telemetry_set( 'last_sync_success', time() );
488
489 //error_log('[AutoCron] Done, total processed: ' . $total_processed . ', end memory: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB');
490 }
491
492
493
494 /*
495 * Reconciliation Mechanism
496 *
497 *
498 *
499 **/
500
501 if ( ! wp_next_scheduled( 'mlsimport_reconciliation_event' ) ) {
502 wp_schedule_event( time(), 'daily', 'mlsimport_reconciliation_event' );
503 }
504
505 add_action( 'mlsimport_reconciliation_event', 'mlsimport_saas_reconciliation_event_function' );
506
507 if ( ! wp_next_scheduled( 'mlsimport_daily_telemetry_event' ) ) {
508 wp_schedule_event( time(), 'daily', 'mlsimport_daily_telemetry_event' );
509 }
510
511 add_action( 'mlsimport_daily_telemetry_event', 'mlsimport_telemetry_run_daily' );
512
513
514 /*
515 * Force use of transient
516 *
517 *
518 *
519 **/
520
521 /**
522 * Filter callback stub that returns the transient value unchanged.
523 *
524 * Left as a pass-through hook point; the commented `return false;` would force
525 * a transient miss for debugging.
526 *
527 * @param mixed $value Incoming transient value.
528 * @return mixed The value unchanged.
529 */
530 function mlsimport_force_use_transient( $value ) {
531 return $value;
532 // return false;
533 }
534
535
536
537
538 // Instantiate the core orchestrator and register all admin/public hooks.
539 global $mlsimport;
540 $mlsimport = new Mlsimport();
541 $mlsimport->run();
542
543
544
545
546
547 // Map of supported theme IDs to human labels. 990 = standalone (no host theme
548 // dependency); 991-994 are the four supported real-estate themes.
549 $supported_theme = array(
550 990 => 'Standalone (any theme)',
551 991 => 'WpResidence',
552 992 => 'Houzez',
553 993 => 'Real Homes',
554 994 => 'Wpestate',
555
556 );
557
558 // Expose the theme map as a constant for use across the plugin.
559 define( 'MLSIMPORT_THEME', $supported_theme );
560
561 add_filter( 'action_scheduler_failure_period', 'mlsimport_saas_filter_timelimit' );
562 /**
563 * Raise the Action Scheduler failure timeout so long imports are not marked failed.
564 *
565 * @param int $time_limit Default failure period in seconds (unused).
566 * @return int Fixed failure period of 3000 seconds.
567 */
568 function mlsimport_saas_filter_timelimit( $time_limit ) {
569 return 3000;
570 }
571
572
573
574 /*
575 *
576 * Write logs
577 *
578 **/
579
580 /**
581 * Append a timestamped line to a per-type import log file (when logging is on).
582 *
583 * @param string|array $message Message to log; arrays are JSON-encoded.
584 * @param string $tip_import Log bucket: normal|cron|delete|server_cron.
585 * @return void
586 */
587 function mlsimport_saas_single_write_import_custom_logs( $message, $tip_import = 'normal' ) {
588 // Check if logging is enabled
589 $enable_logs = intval( get_option( 'mlsimport_disable_logs' ) );
590 // Bail unless the "logs enabled" option equals exactly 1.
591 if ( 1 !== $enable_logs) {
592 return;
593 }
594
595 // Encode array payloads to JSON so they can be written as text.
596 if ( is_array( $message ) ) {
597 $message = wp_json_encode( $message );
598 }
599
600 // Prefix the message with a UTC timestamp.
601 $formatted_message = gmdate( 'F j, Y, g:i a' ) . ' -> ' . $message;
602
603 // Determine the log file path based on the import type
604 $log_file_name = 'cron' === $tip_import ? 'cron_logs' :
605 ( 'delete' === $tip_import ? 'delete_logs' :
606 ( 'server_cron' === $tip_import ? 'server_cron_logs' : 'import_logs' ) );
607
608 // Construct the full path with a date suffix
609 $log_file_path = WP_PLUGIN_DIR . "/mlsimport/logs/{$log_file_name}-" . gmdate( 'Y-m-d' ) . '.log';
610
611 // Error handling for file operations
612 try {
613 // Check and create the directory for logs if it does not exist
614 $log_dir = dirname( $log_file_path );
615 if ( ! file_exists( $log_dir ) ) {
616 mkdir( $log_dir, 0755, true );
617 }
618
619 // Append the formatted message to the log file
620 file_put_contents( $log_file_path, $formatted_message, FILE_APPEND | LOCK_EX );
621 } catch ( Exception $e ) {
622 // Handle the exception, such as logging the error elsewhere or sending a notification
623 }
624 }
625
626
627
628 /*
629 *
630 *
631 * Write Status logs
632 *
633 *
634 **/
635
636
637
638 /**
639 * Legacy status-log writer (superseded by mlsimport_debuglogs_per_plugin()).
640 *
641 * Note: writes with LOCK_EX but no FILE_APPEND, so it overwrites status_logs.log
642 * on each call. Retained under the _old suffix; not called anywhere.
643 *
644 * @param string|array $message Message to log; arrays are JSON-encoded.
645 * @return void
646 */
647 function mlsimport_debuglogs_per_plugin_old( $message ) {
648
649 // Encode array payloads to JSON.
650 if ( is_array( $message ) ) {
651 $message = wp_json_encode( $message );
652 }
653
654 global $wp_filesystem;
655 if ( empty( $wp_filesystem ) ) {
656 require_once ABSPATH . '/wp-admin/includes/file.php';
657 WP_Filesystem();
658 }
659
660 $path_status = WP_PLUGIN_DIR . '/mlsimport/logs/status_logs.log';
661 file_put_contents( $path_status, $message, LOCK_EX );
662 }
663 /**
664 * Append a status/debug line to logs/status_logs.log.
665 *
666 * @param string|array $message Message to log; arrays are JSON-encoded.
667 * @return void
668 */
669 function mlsimport_debuglogs_per_plugin( $message ) {
670
671 // Encode array payloads to JSON.
672 if ( is_array( $message ) ) {
673 $message = wp_json_encode( $message );
674 }
675
676 // Nothing to write for an empty message.
677 if ( empty( $message ) ) {
678 return; // Exit the function if there's nothing to log
679 }
680
681 // Target log file inside the plugin's logs/ directory.
682 $log_file_path = WP_PLUGIN_DIR . '/mlsimport/logs/status_logs.log';
683
684 // Check and create the directory for logs if it does not exist
685 $log_dir = dirname( $log_file_path );
686 if ( ! file_exists( $log_dir ) ) {
687 mkdir( $log_dir, 0755, true );
688 }
689
690 // Error handling for file operations
691 try {
692 // Append the message to the log file with a newline and acquire an exclusive lock during writing
693 file_put_contents( $log_file_path, $message . PHP_EOL, LOCK_EX );
694 } catch ( Exception $e ) {
695 // Handle the exception, such as logging the error elsewhere or sending a notification
696 }
697 }
698
699
700
701
702
703 /*
704 * Cron job trigger
705 *
706 *
707 *
708 **/
709
710
711 // */5 * * * * wget http://example.com/check */2
712 add_action( 'init', 'mlsimport_trigger_cron_job' );
713 /**
714 * Server-cron entry point hit via the ?mlsimport_cron=yes query parameter.
715 *
716 * Rate-limited to once every 2 hours. Note: the actual import call on the
717 * throttled branch is currently commented out, so this only writes a log line.
718 *
719 * @return void
720 */
721 function mlsimport_trigger_cron_job() {
722 // ?mlsimport_cron=yes
723 // Only proceed when the request carries mlsimport_cron=yes.
724 if ( isset( $_REQUEST['mlsimport_cron'] ) && 'yes' === sanitize_text_field( wp_unslash( $_REQUEST['mlsimport_cron'] ) ) ) {
725 // Timestamp of the previous server-cron run (0 if never run).
726 $last_run = intval( get_option( 'mlsimport_last_server_cron' ) );
727 // Current time.
728 $now = time();
729 // First-ever call: seed the last-run timestamp.
730 if ( 0 === intval($last_run) ) {
731 update_option( 'mlsimport_last_server_cron', $now );
732 }
733
734 // Only run if at least 2 hours have elapsed since the last run.
735 if ( $last_run < $now - ( 60 * 60 * 2 ) ) {
736 // Build the "triggered" log line and record the new run time.
737 $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;
738 // mlsimport_saas_event_mls_import_auto_function();
739 update_option( 'mlsimport_last_server_cron', $now );
740 } else {
741 $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;
742 }
743
744 mlsimport_saas_single_write_import_custom_logs( $log, 'server_cron' );
745 }
746 }
747
748
749
750 /**
751 * Render the "Sign up for MLSImport" promo box (30-day free-trial CTA).
752 *
753 * Uses a WpResidence-specific affiliate URL when that theme is active.
754 *
755 * @return void
756 */
757 function mlsimport_show_signup() {
758 // Default sign-up URL.
759 $affiliate_url = 'https://mlsimport.com';
760 // Swap in the WpResidence affiliate/campaign link when that theme is active.
761 if ( function_exists( 'wp_estate_init' ) ) {
762 $affiliate_url = 'https://mlsimport.com/ref/1/?campaign=wpresidence';
763 }
764 // Output the promo markup.
765 ?>
766 <div class="mlsimport_signup">
767 <h3><?php esc_html_e('Import MLS Listings into your Real Estate website', 'mlsimport'); ?></h3>
768 <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>
769 <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>
770 </div>
771 <?php
772 }
773
774
775 //add_action('admin_init', 'force_recount_all_terms');
776 /**
777 * Maintenance utility: recalculate term counts for every taxonomy.
778 *
779 * Not hooked by default (the add_action above is commented out); intended to be
780 * run manually to fix drifted term counts. Echoes a completion message.
781 *
782 * @return void
783 */
784 function force_recount_all_terms() {
785 global $wpdb;
786
787 // Get all taxonomies
788 $taxonomies = get_taxonomies([], 'names');
789
790 // Recount terms for each registered taxonomy.
791 foreach ($taxonomies as $taxonomy) {
792 // Get all terms for the taxonomy
793 $terms = get_terms([
794 'taxonomy' => $taxonomy,
795 'hide_empty' => false, // Include terms with 0 count
796 'fields' => 'ids', // Get only the term IDs
797 ]);
798
799 if (!is_wp_error($terms) && !empty($terms)) {
800 // Get term_taxonomy_ids for these terms
801 $term_taxonomy_ids = $wpdb->get_col($wpdb->prepare(
802 "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE term_id IN (" . implode(',', array_map('intval', $terms)) . ")"
803 ));
804
805 // Update term counts
806 if (!empty($term_taxonomy_ids)) {
807 wp_update_term_count_now($term_taxonomy_ids, $taxonomy);
808 }
809 }
810 }
811
812 echo "Term counts have been recalculated for all taxonomies.";
813 }
814
815
816 /*
817 *
818 * create dropdown list
819 *
820 *
821 */
822 /**
823 * Build a <select> for a mlsimport_admin_options[$key] field.
824 *
825 * @param string $key Option key (used as id and name suffix).
826 * @param mixed $value Currently selected option value.
827 * @param array $data_array Map of option value => label.
828 * @return string The rendered <select> HTML.
829 */
830 function mlsiport_mls_select_list( $key, $value, $data_array ) {
831 // Open the select, binding it to the mlsimport_admin_options[$key] field.
832 $select = '<select class="mlsimport-2025-select" id="' . esc_attr( $key ) . '" name="mlsimport_admin_options[' . $key . ']">';
833 // Only build options when given an array of choices.
834 if ( is_array( $data_array ) ) :
835 // Emit one <option> per choice.
836 foreach ( $data_array as $key => $mls_item ) {
837 $select .= '<option value="' .esc_attr( $key ). '"';
838 // Mark the option matching the current value as selected.
839 if ( intval( $value ) === intval( $key ) ) {
840 $select .= ' selected ';
841 }
842 $select .= '>' .esc_html( $mls_item ). '</option>';
843 }
844 endif;
845 // Close the select and return the assembled markup.
846 $select .= '</select>';
847 return $select;
848 }
849
850
851
852 add_action('wp_ajax_mlsimport_save_account', 'mlsimport_save_account_callback');
853 /**
854 * AJAX handler: save the MLSImport account username/password and test the login.
855 *
856 * Verifies the onboarding nonce, stores credentials in mlsimport_admin_options,
857 * fetches a fresh API token, and returns connected/not-connected HTML + flag.
858 *
859 * @return void
860 */
861 function mlsimport_save_account_callback() {
862 // Verify the shared onboarding AJAX nonce.
863 check_ajax_referer('mlsimport_onboarding_nonce', 'security');
864
865 // Load current plugin options.
866 $options = get_option('mlsimport_admin_options', []);
867 // Persist the submitted credentials only when both are present.
868 if ( ! empty($_POST['mlsimport_username']) && ! empty($_POST['mlsimport_password']) ) {
869 $options['mlsimport_username'] = sanitize_text_field($_POST['mlsimport_username']);
870 $options['mlsimport_password'] = sanitize_text_field($_POST['mlsimport_password']);
871 update_option('mlsimport_admin_options', $options);
872 }
873
874 global $mlsimport;
875
876 // Refresh token
877 $token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient();
878
879 // Empty token means the credentials did not authenticate.
880 if (trim($token) === '') {
881 // Buffer the "not connected" warning markup.
882 ob_start();
883
884 ?>
885 <div class="mlsimport_warning">
886 <?php esc_html_e('You are not connected to MlsImport - Please check your Username and Password.', 'mlsimport'); ?>
887 </div>
888 <?php
889 $html = ob_get_clean();
890
891 // Return failure HTML + connected=false.
892 wp_send_json_success([
893 'message' => __('You are not connected.', 'mlsimport'),
894 'html' => $html,
895 'connected' => false
896 ]);
897 } else {
898 ob_start();
899 ?>
900 <div class="mlsimport_warning mlsimport_validated">
901 <?php esc_html_e('You are connected to your MlsImport account!', 'mlsimport'); ?>
902 </div>
903 <?php
904 $html = ob_get_clean();
905
906 wp_send_json_success([
907 'message' => __('Connected successfully!', 'mlsimport'),
908 'html' => $html,
909 'connected' => true
910 ]);
911 }
912 }
913
914
915
916
917
918
919
920
921 add_action('wp_ajax_mlsimport_save_mls_data', 'mlsimport_save_mls_data_callback');
922 function mlsimport_save_mls_data_callback() {
923 check_ajax_referer('mlsimport_onboarding_nonce', 'security');
924
925 $options = get_option('mlsimport_admin_options', []);
926
927 foreach ($_POST as $key => $value) {
928 if (strpos($key, 'mlsimport_') === 0 && $key !== 'mlsimport_username' && $key !== 'mlsimport_password') {
929 $options[$key] = sanitize_text_field($value);
930 }
931 }
932
933 update_option('mlsimport_admin_options', $options);
934
935 // Run MLS connection check
936 global $mlsimport;
937 $is_mls_connected = get_option('mlsimport_connection_test', '');
938 $mlsimport->admin->mlsimport_saas_setting_up();
939
940 if ('yes' !== $is_mls_connected) {
941 $mlsimport->admin->mlsimport_saas_check_mls_connection();
942 $is_mls_connected = get_option('mlsimport_connection_test', '');
943 }
944
945 ob_start();
946 if ('yes' === $is_mls_connected) {
947 ?>
948 <div class="mlsimport_warning mlsimport_validated">
949 <?php esc_html_e('You’re now connected to your MLS.', 'mlsimport'); ?>
950 </div>
951 <?php
952 } else {
953 ?>
954 <div class="mlsimport_warning">
955 <?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'); ?>
956 </div>
957 <?php
958 }
959 $html = ob_get_clean();
960
961 wp_send_json_success([
962 'message' => __('MLS data saved', 'mlsimport'),
963 'html' => $html,
964 'connected' => $is_mls_connected === 'yes',
965 ]);
966 }
967
968
969