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

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