PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.2
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.2
7.2 7.1.2 7.1.1 7.1 7.0.4 7.0.6 7.0.7 6.3.8 6.3.7 6.3.6 6.3.5 6.3.4 6.3.3 6.3.1 trunk 5.7.3 5.7.5 5.8.1 5.8.2 5.8.3 5.8.4 5.8.6 6.0.4 6.0.5 6.0.7 All 35 releases
← All changes | includes/help_functions.php +295 -14 6.0.77.2 View file →
@@ -1,12 +1,68 @@
1 -<?php
1 +<?php
2 +/**
3 + * Helper functions for the MLSImport plugin.
4 + *
5 + * Grab-bag of stand-alone utilities used across the admin/import layers:
6 + * - Standalone-mode detection (theme_id 990).
7 + * - The canonical RESO -> theme field-mapping schema (mlsimport_hardocde_theme_schema),
8 + * a large associative array telling the importer where each RESO field lands
9 + * (post meta, taxonomy, media, or post content/title).
10 + * - Taxonomy discovery for a post type, allowed-HTML whitelists, SaaS "ready to go
11 + * MLS" list fetching, recursive array sanitisation, the reconciliation cron entry
12 + * point, the Import Tasks admin list-table columns, and a plugin data reset routine.
13 + *
14 + * Loaded early in the bootstrap so these helpers are available to both admin and
15 + * public code paths.
16 + */
2 17 if ( ! defined( 'ABSPATH' ) ) {
3 18 exit; // Exit if accessed directly
4 19 }
5 20
6 21
22 +/**
23 + * Whether the site runs in standalone (theme-agnostic) mode — theme_id 990.
24 + *
25 + * Drives the standalone write path / front-end gating. Reads the configured
26 + * theme from mlsimport_admin_options['mlsimport_theme_used'].
27 + *
28 + * @return bool
29 + */
30 +function mlsimport_is_standalone_mode() {
31 + $options = get_option( 'mlsimport_admin_options' );
32 + return is_array( $options )
33 + && isset( $options['mlsimport_theme_used'] )
34 + && 990 === intval( $options['mlsimport_theme_used'] );
35 +}
36 +
37 +
38 +/**
39 + * Hard-coded RESO -> theme field-mapping schema (fallback / reference map).
40 + *
41 + * Returns a large associative array keyed by RESO Data Dictionary field name
42 + * (e.g. 'ListPrice', 'BedroomsTotal', 'City'). Each value is a descriptor array
43 + * describing where that RESO field is written on the WordPress side:
44 + * - 'type' => one of:
45 + * 'meta' - stored as a post meta value ('name' = meta key),
46 + * 'taxonomy' - assigned as a term of taxonomy 'name'; an optional
47 + * 'insert' gives the fixed term label to add (used for
48 + * boolean *YN flags, e.g. 'Has Garage'),
49 + * 'media' - handled by the media/gallery importer,
50 + * 'content' - written to the post content (PublicRemarks).
51 + * - 'name' => the destination meta key / taxonomy slug / field on the theme side.
52 + * - 'insert' => (taxonomy only, optional) literal term label to insert when the
53 + * RESO boolean is truthy.
54 + *
55 + * The keys are ordered alphabetically by RESO field name. Section markers below
56 + * flag the larger logical clusters (agent/office contact blocks, distance-to-*,
57 + * tax, etc.) but most fields are simple 1:1 meta passthroughs.
58 + *
59 + * @return array Field-name => descriptor map.
60 + */
7 61 function mlsimport_hardocde_theme_schema(){
62 + // Build and return the full descriptor map in one literal array.
8 63 $theme_schema= array(
64 + // ---- Above/Below-grade area, access & accessibility (A) ----
9 65 'AboveGradeFinishedArea' => array(
10 66 'type' => 'meta',
11 67 'name' => 'abovegradefinishedarea',
12 68 ),
@@ -62,8 +118,9 @@
62 118 'ArchitecturalStyle' => array(
63 119 'type' => 'meta',
64 120 'name' => 'architecturalstyle',
65 121 ),
122 + // ---- HOA / association fees & names ----
66 123 'AssociationAmenities' => array(
67 124 'type' => 'meta',
68 125 'name' => 'associationamenities',
69 126 ),
@@ -137,8 +194,9 @@
137 194 'type' => 'taxonomy',
138 195 'name' => 'property_features',
139 196 'insert' => 'Has Basement',
140 197 ),
198 + // ---- Bath / bedroom counts (BathroomsTotalInteger & BedroomsTotal map to theme core fields) ----
141 199 'BathroomsFull' => array(
142 200 'type' => 'meta',
143 201 'name' => 'bathroomsfull',
144 202 ),
@@ -233,8 +291,9 @@
233 291 'BusinessType' => array(
234 292 'type' => 'meta',
235 293 'name' => 'businesstype',
236 294 ),
295 + // ---- Buyer agent contact block (name/phone/email/license variants) ----
237 296 'BuyerAgent' => array(
238 297 'type' => 'meta',
239 298 'name' => 'buyeragent',
240 299 ),
@@ -353,8 +412,9 @@
353 412 'BuyerFinancing' => array(
354 413 'type' => 'meta',
355 414 'name' => 'buyerfinancing',
356 415 ),
416 + // ---- Buyer office / brokerage block ----
357 417 'BuyerOffice' => array(
358 418 'type' => 'meta',
359 419 'name' => 'buyeroffice',
360 420 ),
@@ -434,8 +494,9 @@
434 494 'CarrierRoute' => array(
435 495 'type' => 'meta',
436 496 'name' => 'carrierroute',
437 497 ),
498 + // ---- Location taxonomies: City -> property_city, CityRegion -> property_area, etc. ----
438 499 'City' => array(
439 500 'type' => 'taxonomy',
440 501 'name' => 'property_city',
441 502 ),
@@ -450,8 +511,9 @@
450 511 'ClosePrice' => array(
451 512 'type' => 'meta',
452 513 'name' => 'closeprice',
453 514 ),
515 + // ---- Co-buyer agent contact block ----
454 516 'CoBuyerAgent' => array(
455 517 'type' => 'meta',
456 518 'name' => 'cobuyeragent',
457 519 ),
@@ -558,8 +620,9 @@
558 620 'CoBuyerAgentVoiceMailExt' => array(
559 621 'type' => 'meta',
560 622 'name' => 'cobuyeragentvoicemailext',
561 623 ),
624 + // ---- Co-buyer office / brokerage block ----
562 625 'CoBuyerOffice' => array(
563 626 'type' => 'meta',
564 627 'name' => 'cobuyeroffice',
565 628 ),
@@ -602,8 +665,9 @@
602 665 'CoBuyerOfficeURL' => array(
603 666 'type' => 'meta',
604 667 'name' => 'cobuyerofficeurl',
605 668 ),
669 + // ---- Co-listing agent contact block ----
606 670 'CoListAgent' => array(
607 671 'type' => 'meta',
608 672 'name' => 'colistagent',
609 673 ),
@@ -710,8 +774,9 @@
710 774 'CoListAgentVoiceMailExt' => array(
711 775 'type' => 'meta',
712 776 'name' => 'colistagentvoicemailext',
713 777 ),
778 + // ---- Co-listing office / brokerage block ----
714 779 'CoListOffice' => array(
715 780 'type' => 'meta',
716 781 'name' => 'colistoffice',
717 782 ),
@@ -893,8 +958,9 @@
893 958 'Disclosures' => array(
894 959 'type' => 'meta',
895 960 'name' => 'disclosures',
896 961 ),
962 + // ---- DistanceTo* family: comments/numeric/units triplets for each amenity ----
897 963 'DistanceToBusComments' => array(
898 964 'type' => 'meta',
899 965 'name' => 'distancetobuscomments',
900 966 ),
@@ -1459,8 +1525,9 @@
1459 1525 'LicensesExpense' => array(
1460 1526 'type' => 'meta',
1461 1527 'name' => 'licensesexpense',
1462 1528 ),
1529 + // ---- Listing agent contact block ----
1463 1530 'ListAgent' => array(
1464 1531 'type' => 'meta',
1465 1532 'name' => 'listagent',
1466 1533 ),
@@ -1603,8 +1670,9 @@
1603 1670 'ListingURLDescription' => array(
1604 1671 'type' => 'meta',
1605 1672 'name' => 'listingurldescription',
1606 1673 ),
1674 + // ---- Listing office / brokerage block ----
1607 1675 'ListOffice' => array(
1608 1676 'type' => 'meta',
1609 1677 'name' => 'listoffice',
1610 1678 ),
@@ -1647,8 +1715,9 @@
1647 1715 'ListOfficeURL' => array(
1648 1716 'type' => 'meta',
1649 1717 'name' => 'listofficeurl',
1650 1718 ),
1719 + // ---- Core theme fields: ListPrice/LivingArea/LotSize/Lat-Long map to theme meta keys ----
1651 1720 'ListPrice' => array(
1652 1721 'type' => 'meta',
1653 1722 'name' => 'property_price',
1654 1723 ),
@@ -1767,8 +1836,9 @@
1767 1836 'MapURL' => array(
1768 1837 'type' => 'meta',
1769 1838 'name' => 'mapurl',
1770 1839 ),
1840 + // ---- Media: routed to the gallery/attachment importer rather than a meta key ----
1771 1841 'Media' => array(
1772 1842 'type' => 'media',
1773 1843 'name' => 'media',
1774 1844 ),
@@ -2142,8 +2212,9 @@
2142 2212 'PropertyType' => array(
2143 2213 'type' => 'taxonomy',
2144 2214 'name' => 'property_action_category',
2145 2215 ),
2216 + // ---- PublicRemarks -> the post content body (the public listing description) ----
2146 2217 'PublicRemarks' => array(
2147 2218 'type' => 'content',
2148 2219 'name' => 'content',
2149 2220 ),
@@ -2427,8 +2498,9 @@
2427 2498 'SyndicationRemarks' => array(
2428 2499 'type' => 'meta',
2429 2500 'name' => 'syndicationremarks',
2430 2501 ),
2502 + // ---- Tax / assessment block ----
2431 2503 'TaxAnnualAmount' => array(
2432 2504 'type' => 'meta',
2433 2505 'name' => 'taxannualamount',
2434 2506 ),
@@ -2621,8 +2693,9 @@
2621 2693 'WorkmansCompensationExpense' => array(
2622 2694 'type' => 'meta',
2623 2695 'name' => 'workmanscompensationexpense',
2624 2696 ),
2697 + // ---- Year built / establishment & zoning (tail of the map) ----
2625 2698 'YearBuilt' => array(
2626 2699 'type' => 'meta',
2627 2700 'name' => 'yearbuilt',
2628 2701 ),
@@ -2654,12 +2727,19 @@
2654 2727 'type' => 'meta',
2655 2728 'name' => 'zoningdescription',
2656 2729 ),
2657 2730 );
2731 + // Hand the completed map back to the caller.
2658 2732 return $theme_schema;
2659 2733 }
2660 2734
2661 2735
2736 +/**
2737 + * Build a slug => label map of the taxonomies registered for a post type.
2738 + *
2739 + * @param string $post_type Post type name to inspect.
2740 + * @return array Associative array of taxonomy slug => human-readable label.
2741 + */
2662 2742 function mlsimport_get_custom_post_type_taxonomies($post_type) {
2663 2743 // Get the taxonomies associated with the custom post type
2664 2744 $taxonomies = get_object_taxonomies($post_type , 'objects');
2665 2745
@@ -2667,8 +2747,9 @@
2667 2747 $taxonomy_array = array();
2668 2748
2669 2749 // Loop through the taxonomies and fill the array
2670 2750 foreach ($taxonomies as $taxonomy_slug => $taxonomy) {
2751 + // Key by slug, store the taxonomy's display label.
2671 2752 $taxonomy_array[$taxonomy_slug] = $taxonomy->label;
2672 2753 }
2673 2754
2674 2755 return $taxonomy_array;
@@ -2681,8 +2762,18 @@
2681 2762 *
2682 2763 *
2683 2764 */
2684 2765
2766 +/**
2767 + * Whitelist of HTML tags/attributes allowed in MLSImport-rendered content.
2768 + *
2769 + * Intended for use with wp_kses() when echoing plugin-generated markup: keeps a
2770 + * small set of formatting tags plus the form controls (select/option/input/
2771 + * fieldset/label) the admin UI emits. (The preceding block comment is a stale
2772 + * copy/paste and does not describe this function.)
2773 + *
2774 + * @return array Tag => allowed-attributes map in wp_kses() format.
2775 + */
2685 2776 function mlsimport_allowed_html_tags_content() {
2686 2777 // Define the allowable HTML tags and their attributes
2687 2778 $allowed_tags = array(
2688 2779 'a' => array(
@@ -2756,23 +2847,46 @@
2756 2847 * Request list of ready to go MLS
2757 2848 *
2758 2849 *
2759 2850 */
2851 +/**
2852 + * Fetch the SaaS list of "ready to go" MLS systems, formatted for an autocomplete.
2853 + *
2854 + * Result is cached in a 24h transient. On a fresh fetch the raw MLS list is turned
2855 + * into an array of {label, value} pairs (with a "My MLS is not on this list" entry
2856 + * prepended) and JSON-encoded; on API failure a single-element error array is returned.
2857 + *
2858 + * @return string|array JSON string of autofill pairs on success, or an array
2859 + * carrying an error message under key '0' on failure.
2860 + */
2760 2861 function mlsimport_saas_request_list() {
2761 2862
2863 + // Serve from the cached transient when present.
2762 2864 $mls_data = get_transient( 'mlsimport_ready_to_go_mlsimport_data' );
2763 2865
2866 + // Cache miss: hit the SaaS API for the current MLS list.
2764 2867 if ( false === $mls_data ) {
2765 2868 $theme_Start = new ThemeImport();
2766 2869 $values = array();
2767 2870
2871 + // GET the MLS catalogue from the SaaS endpoint.
2768 2872 $answer = $theme_Start::globalApiRequestSaas( 'mls', $values, 'GET' );
2769 2873
2874 + // Success: reshape the list into autocomplete {label,value} pairs.
2770 2875 if ( isset( $answer['success'] ) && true === $answer['success'] ) {
2771 2876 $mls_data = $answer['mls_list'];
2877 +
2878 + // Fresh catalogue in hand: refresh registered connections' display
2879 + // names so an MLS renamed upstream shows its new name everywhere
2880 + // (Connections table, import task picker) after a cache clear.
2881 + if ( is_array( $mls_data ) && class_exists( 'Mlsimport_Connections' ) ) {
2882 + Mlsimport_Connections::sync_names( $mls_data );
2883 + }
2884 + // Prepend the "not listed" opt-out choice.
2772 2885 $mls_data['0'] = esc_html__( 'My MLS is not on this list', 'mlsimport' );
2773 2886
2774 2887 $autofill_array = array();
2888 + // Convert each key => label into a label/value pair.
2775 2889 foreach ( $mls_data as $key => $value ) {
2776 2890 $temp_array = array(
2777 2891 'label' => $value,
2778 2892 'value' => $key,
@@ -2779,16 +2893,21 @@
2779 2893 );
2780 2894 $autofill_array[] = $temp_array;
2781 2895 }
2782 2896
2897 + // Encode the pairs and cache for 24 hours.
2783 2898 $mls_data = wp_json_encode( $autofill_array );
2784 2899
2785 2900 set_transient( 'mlsimport_ready_to_go_mlsimport_data', $mls_data, 60 * 60 * 24 );
2786 - } else {
2787 - $mls_data = array();
2788 - $mls_data['0'] = esc_html__( 'We could not connect to MLSimport Api', 'mlsimport' );
2789 - }
2790 - }
2901 + } else {
2902 + // Failure: return a single-entry array carrying the error message.
2903 + $mls_data = array();
2904 + $error_message = isset( $answer['error_message'] ) && ! empty( $answer['error_message'] )
2905 + ? $answer['error_message']
2906 + : esc_html__( 'We could not connect to MLSimport Api', 'mlsimport' );
2907 + $mls_data['0'] = esc_html( $error_message );
2908 + }
2909 + }
2791 2910
2792 2911 return $mls_data;
2793 2912 }
2794 2913
@@ -2797,11 +2916,22 @@
2797 2916 * sanitize multidimensional array
2798 2917 *
2799 2918 *
2800 2919 * */
2920 +/**
2921 + * Recursively sanitise a value (scalar or nested array) for safe storage.
2922 + *
2923 + * Walks arrays depth-first, applying sanitize_text_field( wp_unslash() ) to every
2924 + * leaf; a scalar input is sanitised directly.
2925 + *
2926 + * @param mixed $data Array or scalar to sanitise.
2927 + * @return mixed Sanitised value of the same shape as the input.
2928 + */
2801 2929 function mlsimport_sanitize_multi_dimensional_array($data){
2930 + // Arrays: recurse into each element.
2802 2931 if ( is_array( $data ) ) {
2803 2932 foreach ( $data as $key => $value ) {
2933 + // Nested array -> recurse; otherwise sanitise the leaf value.
2804 2934 if ( is_array( $value ) ) {
2805 2935 $data[ $key ] = mlsimport_sanitize_multi_dimensional_array( $value );
2806 2936 } else {
2807 2937
@@ -2808,8 +2938,9 @@
2808 2938 $data[ $key ] = sanitize_text_field( wp_unslash( $value ));
2809 2939 }
2810 2940 }
2811 2941 } else {
2942 + // Scalar input: sanitise directly.
2812 2943 $data = sanitize_text_field( wp_unslash( $data) );
2813 2944 }
2814 2945
2815 2946 return $data;
@@ -2824,17 +2955,50 @@
2824 2955 *
2825 2956 *
2826 2957 * */
2827 2958
2959 +/**
2960 + * Cron entry point for the daily SaaS reconciliation pass.
2961 + *
2962 + * Bails out early unless at least one non-trashed import task has a title, then
2963 + * hands off to the per-connection runner (#279) when an MLS name is configured:
2964 + * one sub-run of the deep reconciliation module per registered connection,
2965 + * or the single legacy unscoped run while no connections are registered.
2966 + *
2967 + * The same function handles the deduplicated one-hour retry hook — a retry
2968 + * re-runs all connections, and completed ones converge to no-op keeps. Trigger
2969 + * code intentionally owns no snapshot, status, batching, or deletion decisions.
2970 + *
2971 + * @return array<int, array<string, int|string>>|null Outcome per connection, or null when ineligible.
2972 + */
2828 2973 function mlsimport_saas_reconciliation_event_function() {
2829 2974
2830 - global $mlsimport;
2975 + // Pull the API token (side effect: ensures a fresh token) and plugin options.
2976 + global $mlsimport, $wpdb;
2831 2977 $token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient();
2832 2978 $options = get_option( 'mlsimport_admin_options' );
2833 2979
2980 + // Only run reconciliation if at least one import task has a title.
2981 + $has_titled_task = $wpdb->get_var(
2982 + "SELECT ID FROM {$wpdb->posts}
2983 + WHERE post_type = 'mlsimport_item'
2984 + AND post_status != 'trash'
2985 + AND post_title != ''
2986 + LIMIT 1"
2987 + );
2988 + // No titled task -> nothing to reconcile, abort.
2989 + if ( ! $has_titled_task ) {
2990 + return null;
2991 + }
2992 +
2993 + // Only reconcile when an MLS name is configured. The runner sequences one
2994 + // scoped sub-run per registered connection (legacy unscoped run when the
2995 + // registry is empty) and records/logs every outcome itself.
2834 2996 if ( isset( $options['mlsimport_mls_name'] ) && '' !== $options['mlsimport_mls_name'] ) {
2835 - $mlsimport->admin->mlsimport_saas_start_doing_reconciliation();
2997 + return mlsimport_reconciliation_run_connections();
2836 2998 }
2999 +
3000 + return null;
2837 3001 }
2838 3002
2839 3003 /*
2840 3004 * Admin extra columns for MlsImport Items
@@ -2844,22 +3008,39 @@
2844 3008 *
2845 3009 *
2846 3010 * */
2847 3011
3012 +// Register the custom admin list-table columns for the Import Tasks post type.
2848 3013 add_filter( 'manage_edit-mlsimport_item_columns', 'mlsimport_items_columns_admin' );
2849 3014
3015 +// Guard against redeclaration when the filter/file is loaded more than once.
2850 3016 if ( ! function_exists( 'mlsimport_items_columns_admin' ) ) :
2851 3017
3018 + /**
3019 + * Add MLSImport-specific columns to the Import Tasks admin list table.
3020 + *
3021 + * Drops the default comments column and appends Import Parameters, Last action
3022 + * and Auto Update Enabled columns.
3023 + *
3024 + * @param array $columns Existing column id => label map.
3025 + * @return array Modified column map.
3026 + */
2852 3027 function mlsimport_items_columns_admin( $columns ) {
3028 + // Keep a copy of two columns starting at offset 2 (to re-append later).
2853 3029 $slice = array_slice( $columns, 2, 2 );
3030 + // Remove the comments column from both the full set and the slice.
2854 3031 unset( $columns['comments'] );
2855 3032 unset( $slice['comments'] );
3033 + // Trim the original column set down to the first two entries.
2856 3034 $splice = array_splice( $columns, 2 );
2857 3035
3036 + // Append the plugin's own columns.
2858 3037 $columns['mlsimport_items_params'] = esc_html__( 'Import Parameters', 'mlsimport' );
3038 + $columns['mlsimport_task_health'] = esc_html__( 'Status', 'mlsimport' );
2859 3039 $columns['mlsimport_last_action'] = esc_html__( 'Last action', 'mlsimport' );
2860 3040 $columns['mlsimport_autoupdates'] = esc_html__( 'Auto Update Enabled', 'mlsimport' );
2861 3041
3042 + // Return the plugin columns followed by the preserved slice (reversed).
2862 3043 return array_merge( $columns, array_reverse( $slice ) );
2863 3044 }
2864 3045
2865 3046 endif; // end wpestate_my_columns
@@ -2874,16 +3055,27 @@
2874 3055 *
2875 3056 *
2876 3057 *
2877 3058 * */
3059 +/**
3060 + * Flatten an import-parameter value into a comma-separated display string.
3061 + *
3062 + * Arrays are joined with commas (trailing comma trimmed); scalars pass through.
3063 + *
3064 + * @param mixed $value Array of values or a single scalar.
3065 + * @return string Comma-separated (or scalar) display string.
3066 + */
2878 3067 function mlsimport_populate_columns_params_display_value( $value ) {
2879 3068 $display_value = '';
3069 + // Array: concatenate each element with a comma separator.
2880 3070 if ( is_array( $value ) ) {
2881 3071 foreach ( $value as $key_item => $item_name ) :
2882 3072 $display_value .= $item_name . ',';
2883 3073 endforeach;
3074 + // Drop the trailing comma left by the loop.
2884 3075 $display_value = rtrim( $display_value, ',' );
2885 3076 } else {
3077 + // Scalar: use as-is.
2886 3078 $display_value = $value;
2887 3079 }
2888 3080
2889 3081 return $display_value;
@@ -2896,20 +3088,34 @@
2896 3088 *
2897 3089 *
2898 3090 * */
2899 3091
3092 +/**
3093 + * Echo the configured import parameters for one Import Task into the admin column.
3094 + *
3095 + * Iterates the MLS field definitions and, for each non-hidden field, prints the
3096 + * task's stored value. Fields NOT in the $select_all_none list show "ALL" when
3097 + * their per-field checkbox meta is set; the rest always show the raw stored value.
3098 + *
3099 + * @param int $postID Import Task post ID.
3100 + * @return void Output is echoed directly.
3101 + */
2900 3102 function mlsimport_populate_columns_params_display( $postID ) {
3103 + // Field definitions come from the admin class, scoped to the TASK's own
3104 + // connection (#277) so each row's parameters render against its MLS.
2901 3105 global $mlsimport;
2902 - $field_import = $mlsimport->admin->mlsimport_saas_return_mls_fields();
3106 + $field_import = $mlsimport->admin->mlsimport_saas_return_mls_fields( mlsimport_task_mls_id( (int) $postID ) );
2903 3107
3108 + // Fields whose stored value is always shown verbatim (never collapsed to "ALL").
2904 3109 $select_all_none = array(
2905 3110 'InternetAddressDisplayYN',
2906 3111 'InternetEntireListingDisplayYN',
2907 3112 'PostalCode',
2908 - 'ListAgentKey',
2909 - 'ListAgentMlsId',
2910 - 'ListOfficeKey',
2911 - 'ListOfficeMlsId',
3113 + 'ListAgentKey',
3114 + 'ListAgentMlsId',
3115 + 'BuyerAgentMlsId',
3116 + 'ListOfficeKey',
3117 + 'ListOfficeMlsId',
2912 3118 'ListingID',
2913 3119 'StandardStatus',
2914 3120 'extraCity',
2915 3121 'extraCounty',
@@ -2916,25 +3122,35 @@
2916 3122 'Exclude_ListOfficeKey',
2917 3123 'Exclude_ListOfficeMlsId',
2918 3124 'Exclude_ListAgentKey',
2919 3125 'Exclude_ListAgentMlsId',
3126 + 'CustomParameters',
2920 3127 'MLSAreaMajor',
2921 3128 'SubdivisionName',
2922 3129
2923 3130 );
3131 + // Walk every defined MLS field for this task.
2924 3132 foreach ( $field_import as $key => $field ) :
3133 + // Skip fields flagged hidden.
3134 + if ( ! empty( $field['hidden'] ) ) {
3135 + continue;
3136 + }
2925 3137 $display_value = '';
3138 + // Derive the per-field meta keys (value + its "check" companion).
2926 3139 $name_check = strtolower( 'mlsimport_item_' . $key . '_check' );
2927 3140 $name = strtolower( 'mlsimport_item_' . $key );
2928 3141
3142 + // Read the stored value and its checkbox flag.
2929 3143 $value = get_post_meta( $postID, $name, true );
2930 3144 $value_check = get_post_meta( $postID, $name_check, true );
2931 3145
3146 + // Normalise the checkbox flag to 0/1.
2932 3147 $is_checkbox_admin = 0;
2933 3148 if ( 1 === intval($value_check) ) {
2934 3149 $is_checkbox_admin = 1;
2935 3150 }
2936 3151
3152 + // For collapsible fields a set checkbox means "ALL"; otherwise show the value.
2937 3153 if ( ! in_array( $key, $select_all_none ) ) {
2938 3154 if ( 1 === intval($is_checkbox_admin) ) {
2939 3155 $display_value = esc_html__( 'ALL', 'mlsimport' );
2940 3156 } else {
@@ -2940,14 +3156,17 @@
2940 3156 } else {
2941 3157 $display_value = mlsimport_populate_columns_params_display_value( $value );
2942 3158 }
2943 3159 } else {
3160 + // Always-verbatim fields: print the stored value.
2944 3161 $display_value = mlsimport_populate_columns_params_display_value( $value );
2945 3162 }
2946 3163
3164 + // Render "<Label> : <value>" only when there is something to show.
2947 3165 if ( '' !== $display_value ) { ?>
2948 3166 <strong>
2949 3167 <?php
3168 + // Field label with the leading "Select " prefix stripped.
2950 3169 print esc_html( ucfirst( str_replace( 'Select ', '', $field['label'] ) ) );
2951 3170 ?>
2952 3171 :</strong>
2953 3172 <?php
@@ -2958,16 +3177,30 @@
2958 3177 }
2959 3178
2960 3179
2961 3180
3181 +// Render cell contents for the custom Import Tasks columns.
2962 3182 add_action( 'manage_posts_custom_column', 'mlsimport_populate_columns' );
3183 +// Guard against redeclaration.
2963 3184 if ( ! function_exists( 'mlsimport_populate_columns' ) ) :
2964 3185
3186 + /**
3187 + * Output the value for each custom Import Tasks admin column.
3188 + *
3189 + * Handles the three plugin columns: import parameters (price range + field
3190 + * params), last action date, and whether auto-update (cron) is enabled.
3191 + *
3192 + * @param string $column Column id being rendered.
3193 + * @return void Output is echoed directly.
3194 + */
2965 3195 function mlsimport_populate_columns( $column ) {
2966 3196
3197 + // Current row's post is available via the global.
2967 3198 global $post;
2968 3199
3200 + // Import Parameters column: price range then the field parameter list.
2969 3201 if ( 'mlsimport_items_params' === $column ) {
3202 + // Read the configured min/max price filters as floats.
2970 3203 $mlsimport_item_min_price = floatval( get_post_meta( $post->ID, 'mlsimport_item_min_price', true ) );
2971 3204 $mlsimport_item_max_price = floatval( get_post_meta( $post->ID, 'mlsimport_item_max_price', true ) );
2972 3205 ?>
2973 3206
@@ -2985,20 +3218,54 @@
2985 3218
2986 3219 <?php echo esc_html($mlsimport_item_max_price); ?><br>
2987 3220 <?php
2988 3221
3222 + // Then the per-field import parameters.
2989 3223 mlsimport_populate_columns_params_display( $post->ID );
3224 + } elseif ( 'mlsimport_task_health' === $column ) {
3225 + // Status column (GitHub issue #200): surface stuck, failed, and
3226 + // sync-overdue tasks directly in the list. All inputs are already
3227 + // recorded — the run status meta (state, progress, heartbeat, error),
3228 + // the sync watermark, and the auto-sync flag. The badge decision
3229 + // itself lives in the pure, unit-tested mlsimport_task_health().
3230 + $health_status = get_post_meta( $post->ID, 'mlsimport_import_run_status', true );
3231 + $health = mlsimport_task_health(
3232 + is_array( $health_status ) ? $health_status : array(),
3233 + (string) get_post_meta( $post->ID, 'mlsimport_last_date', true ),
3234 + 1 === (int) get_post_meta( $post->ID, 'mlsimport_item_stat_cron', true ),
3235 + time(),
3236 + // Watermarks are stored with wp_date() in site-local time, so
3237 + // the overdue cutoff must be built the same way to compare.
3238 + wp_date( 'Y-m-d\TH:i', time() - MLSIMPORT_TASK_HEALTH_OVERDUE_AFTER ),
3239 + // The hourly runner's own eligibility rule (GitHub issue #330):
3240 + // a task it will never pick up must not read as merely overdue.
3241 + mlsimport_cron_task_is_eligible(
3242 + (int) get_post_meta( $post->ID, 'mlsimport_initial_import_completed', true ),
3243 + (string) get_post_meta( $post->ID, 'mlsimport_spawn_status', true )
3244 + )
3245 + );
3246 + ?>
3247 + <span class="mlsimport-task-health mlsimport-task-health--<?php echo esc_attr( $health['level'] ); ?>">
3248 + <?php echo esc_html( $health['label'] ); ?>
3249 + </span>
3250 + <p class="mlsimport-task-health__message"><?php echo esc_html( $health['message'] ); ?></p>
3251 + <?php
2990 3252 } elseif ( 'mlsimport_last_action' === $column ) {
3253 + // Last action column: date of the most recent import activity.
2991 3254 $last_date = get_post_meta( $post->ID, 'mlsimport_last_date', true );
2992 3255 if ( '' !== $last_date ) {
3256 + // Have a date: show it with an explanatory note.
2993 3257 print esc_html($last_date) . ' </br>';
2994 3258 esc_html_e( 'On this date we found new or edited listings.', 'mlsimport' );
2995 3259 } else {
3260 + // No date recorded (sync likely disabled).
2996 3261 esc_html_e( 'Not available - sync option may be off.', 'mlsimport' );
2997 3262 }
2998 3263 } elseif ( 'mlsimport_autoupdates' === $column ) {
3264 + // Auto Update column: reflects the cron-enabled meta flag.
2999 3265 $mlsimport_item_stat_cron = esc_html( get_post_meta( $post->ID, 'mlsimport_item_stat_cron', true ) );
3000 3266
3267 + // Positive flag -> "yes", otherwise "no".
3001 3268 if ( intval( $mlsimport_item_stat_cron ) > 0 ) {
3002 3269 ?>
3003 3270 yes
3004 3271 <?php
@@ -3012,8 +3279,17 @@
3012 3279 endif;
3013 3280
3014 3281
3015 3282
3283 +/**
3284 + * Wipe all MLSImport-created options and transients from the database.
3285 + *
3286 + * Deletes every option whose name begins with "mlsimport_" (both single-site and
3287 + * network variants) and every matching transient (site and normal). Does NOT touch
3288 + * imported posts/terms — options and transients only.
3289 + *
3290 + * @return bool Always true.
3291 + */
3016 3292 function mlsimport_reset_plugin_data() {
3017 3293
3018 3294
3019 3295 global $wpdb;
@@ -3018,9 +3294,11 @@
3018 3294
3019 3295 global $wpdb;
3020 3296
3021 3297 // Remove all options stored by MLSImport
3298 + // Collect every option name prefixed with "mlsimport_".
3022 3299 $option_names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( 'mlsimport_' ) . '%' ) );
3300 + // Delete each as both a normal and a network option.
3023 3301 foreach ( $option_names as $option_name ) {
3024 3302 delete_option( $option_name );
3025 3303 delete_site_option( $option_name );
3026 3304 }
@@ -3025,12 +3303,15 @@
3025 3303 delete_site_option( $option_name );
3026 3304 }
3027 3305
3028 3306 // Remove transients created by MLSImport
3307 + // Match both normal and site transient row-name prefixes.
3029 3308 $transient_patterns = array( '_transient_mlsimport_%', '_site_transient_mlsimport_%' );
3030 3309 foreach ( $transient_patterns as $pattern ) {
3310 + // Find the raw option rows backing these transients.
3031 3311 $names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $pattern ) );
3032 3312 foreach ( $names as $name ) {
3313 + // Strip the storage prefix and delete via the matching transient API.
3033 3314 if ( strpos( $name, '_site_transient_' ) === 0 ) {
3034 3315 $transient = substr( $name, strlen( '_site_transient_' ) );
3035 3316 delete_site_transient( $transient );
3036 3317 } else {
@@ -3040,5 +3321,5 @@
3040 3321 }
3041 3322 }
3042 3323
3043 3324 return true;
3044 -}
3325 +}