MLSImport - Important Change

"Delete Statuses" have been removed. The plugin now uses Protected Statuses only.

Properties with a Protected Status will be kept during reconciliation. All other properties no longer found in MLS will be deleted.

Action required: Go to each of your Import Tasks and set the Protected Statuses field to the statuses you want to keep (e.g. Active, Pending, Coming Soon).

multi MLS migration // (the migration file hooks itself on init, after the listing-key migration). require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-connections.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-multimls-migration.php'; // Per-connection option resolution + settings export/import transfer (#275): // every read/write of the "_{mls_id}"-suffixed per-MLS state goes through these. require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-connection-options.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-settings-transfer.php'; // N-entitlement SaaS contract consumer (#276): entitlements parsing, the // mls_id echo guard, and the stable not_entitled rejection handling. require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-entitlements.php'; // Import task connection binding + cron isolation (#277): a task belongs to // ONE connection for life; the hourly cron gates each task on ITS connection. require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-task-binding.php'; // Per-connection reconciliation (#279): the daily event runs the untouched // decision module once per connection, each with an echo-guarded scoped // snapshot and an inventory limited to that connection's stamped listings. require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-reconciliation-connections.php'; // Connections screen (#280): the settings-page Connections tab — screen data // (registry rows + activity + plan cap + account state) and its AJAX handlers // (drag-reorder priority, per-row connection test, account disconnect). require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-connections-screen.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-connections-ajax.php'; // Add-MLS drawer (#281): connection-scoped metadata gather (shared with the // admin metadata AJAX) + the drawer's refuse/test/register/seed flow. require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-metadata-gather.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-connections-add.php'; // The follow-up field-mapping seed the drawer posts after a saved add (#325). require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-connections-seed.php'; // Edit-connection drawer (tab consolidation): the retired "MLS Connection" // credentials tab's replacement — refuse/test/store flow that updates a // registered connection's credentials (and mirrors the current connection's // into the flat options). require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-connections-edit.php'; // Remove-connection flow: a row's Remove action — drops the record + its // per-connection options and promotes the next-priority connection when the // current one was removed. Imported listings and tasks stay. require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-connections-remove.php'; // Dedupe mechanics (#282): one visible copy per physical address across // connections — winner by priority, loser hidden (never deleted), promoted // back automatically when the winner disappears. Address normalization is // the pure half; the evaluator/hooks half depends on it. require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-dedupe-address.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-dedupe.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-stored-listing-wordpress-environment.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-stored-listing-write.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-stored-listing-adapter-factory.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/ThemeImport.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/StandaloneClass.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/ResidenceClass.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/EstateClass.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/HouzezClass.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/RealHomesClass.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/ResoBase.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/SparkResoClass.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/BridgeResoClass.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/TresleResoClass.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/MlsgridResoClass.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/BrightMlsResoClass.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/CentrisResoClass.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/ProviderResoClasses.php'; require_once plugin_dir_path( __FILE__ ) . 'enviroment/UnsupportedResoClass.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/addons/agents_offices.php'; // Why the last SaaS login failed (no subscription vs bad password) and the // one "not connected" box every screen prints. require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-account-status.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-onboarding.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/class-mlsimport-field-configuration.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-field-selector-functions.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-progressive-save.php'; // Per-connection field-mapping UI: the one scope-resolution rule shared by the // field_options tab, the field-configuration AJAX, and the metadata-gather AJAX. require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-field-mapping-scope.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-metadata-autotrigger.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-telemetry.php'; // #283: per-connection telemetry (sync stamps + the connections payload seam). require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-telemetry-connections.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-activity-log.php'; // #208: internal incident alerts (dedup + resolve) and import/connection health watch. require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-alerts.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/mlsimport-import-health.php'; /* * Standalone (theme_id 990) mode — own listings table, CPTs and taxonomies. * Registered on every load (ADR-0003); the table is created on activation and * upgraded behind a version guard on admin_init. */ require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/mlsimport-hooks.php'; // Hook reference + convention (loaded early). require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-table.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-cpt.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-term-select.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-shortcodes.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-block.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-ajax.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-reindex.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-assets.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-standalone-single.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/property-section-registry.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/property-print.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-section-shortcodes.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-section-blocks.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-section-elementor.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-lead.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/agent-sections.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/property-schema.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-metabox.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-agent-metabox.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-term-meta.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-property-columns.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-favorites.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/page-block-registry.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-page-block-shortcodes.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-page-block-blocks.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-page-block-elementor.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/standalone/class-mlsimport-customizer.php'; // Standalone design settings in the WP Customizer (990 mode). require_once plugin_dir_path( __FILE__ ) . 'includes/live/live-bootstrap.php'; // Live MLS passthrough mode (seam #1 — its only existing-file require). add_action( 'init', array( 'Mlsimport_Standalone_Cpt', 'register' ) ); // After register(): drop cached rewrite rules when the standalone-mode signature changed (#206). add_action( 'init', array( 'Mlsimport_Standalone_Cpt', 'maybe_flush_rewrites' ), 20 ); add_action( 'init', array( 'Mlsimport_Property_Metabox', 'register' ) ); add_action( 'init', array( 'Mlsimport_Agent_Metabox', 'register' ) ); add_action( 'init', array( 'Mlsimport_Term_Meta', 'register' ) ); add_action( 'init', array( 'Mlsimport_Property_Columns', 'register' ) ); add_action( 'init', array( 'Mlsimport_Standalone_Shortcodes', 'register' ) ); add_action( 'init', array( 'Mlsimport_Standalone_Block', 'register' ) ); add_action( 'init', array( 'Mlsimport_Standalone_Ajax', 'register' ) ); add_action( 'init', array( 'Mlsimport_Favorites', 'register' ) ); add_action( 'init', array( 'Mlsimport_Property_Section_Shortcodes', 'register' ) ); // Property-section Gutenberg blocks (mlsimport/property-*, "MLSImport — Property" // category) are intentionally NOT registered — we don't expose them as blocks. // The render dispatcher, shortcodes and Elementor widget behind them stay active. // add_action( 'init', array( 'Mlsimport_Property_Section_Blocks', 'register' ) ); add_action( 'init', array( 'Mlsimport_Page_Block_Shortcodes', 'register' ) ); add_action( 'init', array( 'Mlsimport_Page_Block_Blocks', 'register' ) ); // Two MLSImport categories in the block inserter: single-property section blocks // ("Property") and the page-builder blocks ("Real Estate"). add_filter( 'block_categories_all', static function ( $categories ) { return array_merge( array( array( 'slug' => 'mlsimport-property', 'title' => __( 'MLSImport — Property', 'mlsimport' ) ), array( 'slug' => 'mlsimport-real-estate', 'title' => __( 'MLSImport — Real Estate', 'mlsimport' ) ), ), $categories ); } ); add_action( 'init', array( 'Mlsimport_Property_Lead', 'register' ) ); // Priority 20 (not the default 10) so the plugin's self-contained BEM stylesheets // enqueue AFTER the active theme's own styles. Themes hook wp_enqueue_scripts at 10 // and, because plugins load before the theme, our default-10 callback would print // FIRST — losing every equal-specificity tie to a theme rule that prints later. // Hello Elementor's reset.css is the concrete case: it styles bare `button` / // `[type=button]` (pink #c36, inline-block, width:auto), which ties our single-class // `.mlsimport-*` button rules (0,1,0) and won on order, so the multiselect control, // range/beds toggles, popup buttons, submit and the card heart all rendered narrow // and pink. Printing after the theme lets our base rules win that tie. It stays a // SINGLE class (0,1,0), so component state rules (:focus / :hover / .is-open at // 0,1,1+) and any Elementor Style-tab control ({{WRAPPER}} … at 0,2,0+) still win — // this only reclaims the tie against a generic theme reset, and never matches a // non-plugin (e.g. Elementor) element. add_action( 'wp_enqueue_scripts', array( 'Mlsimport_Standalone_Assets', 'enqueue' ), 20 ); add_action( 'wp_enqueue_scripts', array( 'Mlsimport_Property_Section_Assets', 'ensure_registered' ) ); // Pre-enqueue the single-property section assets into the ; the single // template prints the header before any section renders, so the on-demand // enqueue at render time lands in the footer and flashes unstyled (issue #172). // Priority 20 for the same after-the-theme reason as the standalone assets above. add_action( 'wp_enqueue_scripts', array( 'Mlsimport_Property_Section_Assets', 'enqueue_for_single' ), 20 ); // Same for the single-agent page (issue #188). add_action( 'wp_enqueue_scripts', 'mlsimport_agent_enqueue_for_single', 20 ); // Load the same front-end CSS into the block editor so the dynamic blocks' // ServerSideRender previews match the front end (editor-guarded inside the method). add_action( 'enqueue_block_assets', array( 'Mlsimport_Standalone_Assets', 'enqueue_editor' ) ); add_filter( 'template_include', array( 'Mlsimport_Standalone_Single', 'template_include' ) ); add_action( 'wp_head', 'mlsimport_property_print_schema' ); Mlsimport_Property_Section_Elementor::register(); Mlsimport_Page_Block_Elementor::register(); // A contact-form lead carries no property and no agent; route those general // enquiries to the "Contact form recipients" setting (decision 3). add_filter( 'mlsimport_property_lead_recipient', static function ( $to, $property_id, $agent_id = 0 ) { if ( $property_id || $agent_id ) { return $to; } $recipients = trim( (string) mlsimport_standalone_option( 'contact_form_recipients', '' ) ); return '' !== $recipients ? $recipients : $to; }, 10, 3 ); add_action( 'admin_init', array( 'Mlsimport_Standalone_Table', 'maybe_upgrade' ) ); // One-time repair of comma-glued taxonomy terms written before 7.1.2 (#290). add_action( 'admin_init', array( 'Mlsimport_Standalone_Cpt', 'maybe_split_packed_terms' ) ); add_action( 'before_delete_post', array( 'StandaloneClass', 'cleanup_on_delete' ) ); // Trash/unpublish (not a permanent delete) must also drop the listings row, so the // search index only ever holds published listings. add_action( 'transition_post_status', array( 'StandaloneClass', 'cleanup_on_status_change' ), 10, 3 ); if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'mlsimport reindex', function () { $rebuilt = Mlsimport_Standalone_Reindex::rebuild_all(); WP_CLI::success( "Reindexed {$rebuilt} standalone listings." ); } ); } if ( ! wp_next_scheduled( 'event_mls_import_auto' ) ) { wp_schedule_event( time(), 'hourly', 'event_mls_import_auto' ); } /** * Scheduled event: Processes MLSimport items marked for cron processing, in memory-safe batches. * * This function is triggered by the 'event_mls_import_auto' action. * It fetches mlsimport_item post IDs in small batches (not all at once!) to minimize memory usage. * Only posts with meta 'mlsimport_item_stat_cron' = 1 are processed. * For each item, calls mlsimport_saas_start_cron_links_per_item(). * * Optimizations: * - Uses 'fields' => 'ids' so only post IDs are loaded (saves memory) * - Batches with posts_per_page/paged, so memory does not spike for large data sets * - Calls gc_collect_cycles() periodically to further reduce memory leaks * - Bails only when the SaaS token is missing (global, one account); each * task is then gated on its OWN connection (#277) — a broken or deleted * connection skips its tasks while healthy connections keep importing * * @return void */ add_action('event_mls_import_auto', 'mlsimport_saas_event_mls_import_auto_function'); /** * Scheduled event handler for MLS Import Auto (runs via WP Cron). * Processes mlsimport_item posts in batches and logs memory usage. */ function mlsimport_saas_event_mls_import_auto_function() { global $mlsimport; //error_log('[AutoCron] Start: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB'); // Watchdog backstop (issue #199): a chunked manual import whose worker // chain died is normally revived by the polled progress screen, but if // the administrator closed that screen nothing else watches the run. // Hourly cron picks it up here; revive() is a cheap no-op for anything // that is not a silent, stalled manual run. // Import-health checks (#208) run before anything can bail out below: // detect a previous cron run that died mid-loop, and a manual run stuck // at "Preparing". Both open one deduplicated internal incident. mlsimport_cron_heartbeat_check(); mlsimport_import_health_watch_manual_run(); $mlsimport_active_lock = get_option( 'mlsimport_import_run_lock', array() ); if ( is_array( $mlsimport_active_lock ) && ! empty( $mlsimport_active_lock['task_id'] ) ) { $mlsimport_revive = $mlsimport->admin->mlsimport_import_task_execution()->revive( (int) $mlsimport_active_lock['task_id'] ); if ( true === ( $mlsimport_revive['revived'] ?? false ) ) { 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' ); } elseif ( 'stalled' === ( $mlsimport_revive['reason'] ?? '' ) ) { 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' ); // The run was terminated as hopeless — tell the SaaS once (#208). mlsimport_alert_open( 'import_stalled:' . (int) $mlsimport_active_lock['task_id'], 'import_stalled', array( 'task_id' => (int) $mlsimport_active_lock['task_id'] ) ); } } // 0. Bail if a run is already in progress. Without this guard an overlapping // cron fire processes the same listings in parallel, causing duplicate // listing_key inserts and term_relationship/term_count deadlocks. The TTL is // the safety net if a run dies mid-loop without reaching the release below. if ( get_transient( 'mlsimport_cron_running' ) ) { return; } // 1. Get the API token from transient - exit if not set $token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient(); //error_log('[AutoCron] After token fetch: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB'); if (trim($token) === '') { // Silent exit made frozen sites undiagnosable (issue #207 finding 3): // record the failed attempt with a real class before bailing. mlsimport_telemetry_set( 'last_sync_failed', time() ); mlsimport_telemetry_set( 'last_sync_failed_code', 'no_token' ); //error_log('[AutoCron] No token, exiting.'); return; } // 2. The connection-status check is no longer a global bail-out (#277): // each task is gated on ITS OWN connection inside the loop below, so one // broken MLS never blocks tasks on healthy connections. Only the SaaS // token above stays global — one account, one token. // Claim the run lock now that we are committed to processing. set_transient( 'mlsimport_cron_running', 1, 15 * MINUTE_IN_SECONDS ); // Every operation after lock acquisition belongs to one committed cron // run. Keep that complete run inside a try/finally boundary so a Throwable // from task discovery, connection gating, or one Import Run cannot leave // the site locked until the transient TTL expires (#303). try { // Heartbeat (#208): record that a cron import is now running, so the next // cron entry can tell a clean finish from a process that died mid-loop. mlsimport_cron_heartbeat_start(); // Record sync attempt in telemetry. The 'syncs' counter is no longer // bumped here (#283): a run-level tick belongs to no single connection // and would double-count against the per-pull tick that // mlsimport_telemetry_record_sync_result() now records — one pull, one // tick, attributed to the pull's own connection, so the global syncs sum // stays equal to the sum of the per-connection buckets. mlsimport_telemetry_set( 'last_sync_attempt', time() ); // 3. Set batch size for gathering and initialize loop variables $batch_size = 100; $paged = 1; $total_processed = 0; // 4. Gather every cron-enabled task id first (ids only — a few bytes each, // still fetched in paged batches so the query never loads post objects). // Gathering before processing is what makes fair ordering possible below. $cron_task_ids = array(); do { // Prepare query: only IDs, filter by meta key, batch, paged, no_found_rows speeds up query $args = array( 'post_type' => 'mlsimport_item', 'post_status' => 'any', 'posts_per_page' => $batch_size, 'paged' => $paged, 'fields' => 'ids', 'meta_query' => array( array( 'key' => 'mlsimport_item_stat_cron', 'value' => 1, 'compare' => '=', ), ), 'no_found_rows' => true, ); // Get post IDs for this batch $post_ids = get_posts($args); // If nothing is returned, the gather is complete if (empty($post_ids)) { break; } foreach ($post_ids as $prop_id) { $cron_task_ids[] = (int) $prop_id; } // Prepare next batch $paged++; unset($post_ids); // Free memory } while (true); // 5. Order by starvation (issue #203): the query above returns tasks in // the same fixed order every hour, so when an early large task ate the // whole cycle the bottom tasks were skipped run after run. The queue key // is the newer of last success and last ATTEMPT (issue #330): ordering // by success alone let one task whose run never completes keep the // oldest stamp and hold first place every hour while the rest starved. $cron_task_watermarks = array(); foreach ($cron_task_ids as $prop_id) { $cron_task_watermarks[ $prop_id ] = mlsimport_cron_task_queue_key( (string) get_post_meta( $prop_id, 'mlsimport_last_date', true ), (string) get_post_meta( $prop_id, 'mlsimport_last_attempt', true ) ); } unset($cron_task_ids); // 6. Process every task, most starved first. A broken connection is // recorded ONCE per run, not once per task — the entry is identical and // re-writing telemetry for every skipped task would only amplify writes. $failed_connections = array(); foreach (mlsimport_cron_task_order($cron_task_watermarks) as $prop_id) { $logs = 'Loop custom post: ' . $prop_id . PHP_EOL; mlsimport_debuglogs_per_plugin($logs); // Per-connection failure isolation (#277): gate this task on ITS OWN // connection. A broken connection's tasks skip with a recorded reason; // a DELETED connection's tasks skip with a deduplicated health // incident and are never re-defaulted; healthy connections keep // importing in this same run. $mlsimport_gate = mlsimport_cron_task_gate( (int) $prop_id ); if ( 'import' !== $mlsimport_gate['action'] ) { if ( 'skip_missing' === $mlsimport_gate['action'] ) { mlsimport_alert_open( 'task_connection_missing:' . (int) $prop_id, 'task_connection_missing', array( 'task_id' => (int) $prop_id, 'mls_id' => $mlsimport_gate['mls_id'] ) ); } elseif ( ! isset( $failed_connections[ $mlsimport_gate['mls_id'] ] ) ) { $failed_connections[ $mlsimport_gate['mls_id'] ] = true; mlsimport_record_connection_sync_failure( $mlsimport_gate['mls_id'], $mlsimport_gate['code'] ); } mlsimport_debuglogs_per_plugin( 'Task ' . $prop_id . ' skipped: connection ' . $mlsimport_gate['mls_id'] . ' ' . $mlsimport_gate['action'] . ' (' . $mlsimport_gate['code'] . ')' . PHP_EOL ); continue; } // Call processing function for this item. The feed count it pulls // is recorded inside mlsimport_make_listing_requests() (last_feed_found). $mlsimport->admin->mlsimport_saas_start_cron_links_per_item($prop_id); $total_processed++; // Heartbeat (#208): measurable progress for the stuck-run check. mlsimport_cron_heartbeat_progress( $total_processed ); // Free memory every 100 processed items if ($total_processed % 100 === 0) { gc_collect_cycles(); //error_log("[AutoCron] Processed {$total_processed} total, memory: " . (memory_get_usage(true) / 1024 / 1024) . ' MB'); } } // Heartbeat (#208): clean finish — also resolves an open died-run incident. } finally { // A failed task still ends this cron invocation. Mark the heartbeat as // finished and release the overlap lock while PHP continues propagating // the original Throwable to the caller for logging and diagnosis. mlsimport_cron_heartbeat_finish(); delete_transient( 'mlsimport_cron_running' ); } // last_sync_success is no longer stamped here (issue #207 finding 1): the // end-of-loop stamp reported success even when every request failed, and // never fired when a run died mid-loop. Each listings request now records // its own outcome inside mlsimport_make_listing_requests(). //error_log('[AutoCron] Done, total processed: ' . $total_processed . ', end memory: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB'); } /* * Reconciliation Mechanism * * * **/ if ( ! wp_next_scheduled( 'mlsimport_reconciliation_event' ) ) { wp_schedule_event( time(), 'daily', 'mlsimport_reconciliation_event' ); } add_action( 'mlsimport_reconciliation_event', 'mlsimport_saas_reconciliation_event_function' ); add_action( 'mlsimport_reconciliation_retry_event', 'mlsimport_saas_reconciliation_event_function' ); if ( ! wp_next_scheduled( 'mlsimport_daily_telemetry_event' ) ) { wp_schedule_event( time(), 'daily', 'mlsimport_daily_telemetry_event' ); } add_action( 'mlsimport_daily_telemetry_event', 'mlsimport_telemetry_run_daily' ); /* * Force use of transient * * * **/ /** * Filter callback stub that returns the transient value unchanged. * * Left as a pass-through hook point; the commented `return false;` would force * a transient miss for debugging. * * @param mixed $value Incoming transient value. * @return mixed The value unchanged. */ function mlsimport_force_use_transient( $value ) { return $value; // return false; } // Instantiate the core orchestrator and register all admin/public hooks. global $mlsimport; $mlsimport = new Mlsimport(); $mlsimport->run(); // Map of supported theme IDs to human labels. 990 = standalone (no host theme // dependency); 991-994 are the four supported real-estate themes. $supported_theme = array( 990 => 'Standalone (any theme)', 991 => 'WpResidence', 992 => 'Houzez', 993 => 'Real Homes', 994 => 'Wpestate', ); // Expose the theme map as a constant for use across the plugin. define( 'MLSIMPORT_THEME', $supported_theme ); add_filter( 'action_scheduler_failure_period', 'mlsimport_saas_filter_timelimit' ); /** * Raise the Action Scheduler failure timeout so long imports are not marked failed. * * @param int $time_limit Default failure period in seconds (unused). * @return int Fixed failure period of 3000 seconds. */ function mlsimport_saas_filter_timelimit( $time_limit ) { return 3000; } /* * * Write logs * **/ /** * Append a timestamped line to a per-type import log file (when logging is on). * * @param string|array $message Message to log; arrays are JSON-encoded. * @param string $tip_import Log bucket: normal|cron|delete|server_cron. * @return void */ function mlsimport_saas_single_write_import_custom_logs( $message, $tip_import = 'normal' ) { // Check if logging is enabled $enable_logs = intval( get_option( 'mlsimport_disable_logs' ) ); // Bail unless the "logs enabled" option equals exactly 1. if ( 1 !== $enable_logs) { return; } // Encode array payloads to JSON so they can be written as text. if ( is_array( $message ) ) { $message = wp_json_encode( $message ); } // Prefix the message with a UTC timestamp. $formatted_message = gmdate( 'F j, Y, g:i a' ) . ' -> ' . $message; // Determine the log file path based on the import type $log_file_name = 'cron' === $tip_import ? 'cron_logs' : ( 'delete' === $tip_import ? 'delete_logs' : ( 'server_cron' === $tip_import ? 'server_cron_logs' : 'import_logs' ) ); // Construct the full path with a date suffix $log_file_path = WP_PLUGIN_DIR . "/mlsimport/logs/{$log_file_name}-" . gmdate( 'Y-m-d' ) . '.log'; // Error handling for file operations try { // Check and create the directory for logs if it does not exist $log_dir = dirname( $log_file_path ); if ( ! file_exists( $log_dir ) ) { mkdir( $log_dir, 0755, true ); } // Append the formatted message to the log file file_put_contents( $log_file_path, $formatted_message, FILE_APPEND | LOCK_EX ); } catch ( Exception $e ) { // Handle the exception, such as logging the error elsewhere or sending a notification } } /* * * * Write Status logs * * **/ /** * Legacy status-log writer (superseded by mlsimport_debuglogs_per_plugin()). * * Note: writes with LOCK_EX but no FILE_APPEND, so it overwrites status_logs.log * on each call. Retained under the _old suffix; not called anywhere. * * @param string|array $message Message to log; arrays are JSON-encoded. * @return void */ function mlsimport_debuglogs_per_plugin_old( $message ) { // Encode array payloads to JSON. if ( is_array( $message ) ) { $message = wp_json_encode( $message ); } global $wp_filesystem; if ( empty( $wp_filesystem ) ) { require_once ABSPATH . '/wp-admin/includes/file.php'; WP_Filesystem(); } $path_status = WP_PLUGIN_DIR . '/mlsimport/logs/status_logs.log'; file_put_contents( $path_status, $message, LOCK_EX ); } /** * Append a status/debug line to logs/status_logs.log. * * @param string|array $message Message to log; arrays are JSON-encoded. * @return void */ function mlsimport_debuglogs_per_plugin( $message ) { // Encode array payloads to JSON. if ( is_array( $message ) ) { $message = wp_json_encode( $message ); } // Nothing to write for an empty message. if ( empty( $message ) ) { return; // Exit the function if there's nothing to log } // Target log file inside the plugin's logs/ directory. $log_file_path = WP_PLUGIN_DIR . '/mlsimport/logs/status_logs.log'; // Check and create the directory for logs if it does not exist $log_dir = dirname( $log_file_path ); if ( ! file_exists( $log_dir ) ) { mkdir( $log_dir, 0755, true ); } // Error handling for file operations try { // Append the message to the log file with a newline and acquire an exclusive lock during writing file_put_contents( $log_file_path, $message . PHP_EOL, LOCK_EX ); } catch ( Exception $e ) { // Handle the exception, such as logging the error elsewhere or sending a notification } } /* * Cron job trigger * * * **/ // */5 * * * * wget http://example.com/check */2 add_action( 'init', 'mlsimport_trigger_cron_job' ); /** * Server-cron entry point hit via the ?mlsimport_cron=yes query parameter. * * Rate-limited to once every 2 hours. Note: the actual import call on the * throttled branch is currently commented out, so this only writes a log line. * * @return void */ function mlsimport_trigger_cron_job() { // ?mlsimport_cron=yes // Only proceed when the request carries mlsimport_cron=yes. if ( isset( $_REQUEST['mlsimport_cron'] ) && 'yes' === sanitize_text_field( wp_unslash( $_REQUEST['mlsimport_cron'] ) ) ) { // Timestamp of the previous server-cron run (0 if never run). $last_run = intval( get_option( 'mlsimport_last_server_cron' ) ); // Current time. $now = time(); // First-ever call: seed the last-run timestamp. if ( 0 === intval($last_run) ) { update_option( 'mlsimport_last_server_cron', $now ); } // Only run if at least 2 hours have elapsed since the last run. if ( $last_run < $now - ( 60 * 60 * 2 ) ) { // Build the "triggered" log line and record the new run time. $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; // mlsimport_saas_event_mls_import_auto_function(); update_option( 'mlsimport_last_server_cron', $now ); } else { $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; } mlsimport_saas_single_write_import_custom_logs( $log, 'server_cron' ); } } /** * Render the "Sign up for MLSImport" promo box (30-day free-trial CTA). * * Uses a WpResidence-specific affiliate URL when that theme is active. * * @return void */ function mlsimport_show_signup() { // Default sign-up URL. $affiliate_url = 'https://mlsimport.com'; // Swap in the WpResidence affiliate/campaign link when that theme is active. if ( function_exists( 'wp_estate_init' ) ) { $affiliate_url = 'https://mlsimport.com/ref/1/?campaign=wpresidence'; } // Output the promo markup. ?>

MLSImport.com

$taxonomy, 'hide_empty' => false, // Include terms with 0 count 'fields' => 'ids', // Get only the term IDs ]); if (!is_wp_error($terms) && !empty($terms)) { // Get term_taxonomy_ids for these terms $term_taxonomy_ids = $wpdb->get_col($wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE term_id IN (" . implode(',', array_map('intval', $terms)) . ")" )); // Update term counts if (!empty($term_taxonomy_ids)) { wp_update_term_count_now($term_taxonomy_ids, $taxonomy); } } } echo "Term counts have been recalculated for all taxonomies."; } // The theme