empty string. if (empty($normalized)) { return ''; } // Join collected pieces with commas. return implode(', ', $normalized); } // Scalar input: trim and normalize whitespace around commas. return preg_replace('/\s*,\s*/', ', ', trim((string) $meta_value)); } /** * Format Rooms extra meta entries into a single string. * * Turns the RESO "Rooms" collection into a human-readable string like * "Kitchen: Main, 10 x 12 ft | Bedroom: Upper" for storage in a single meta field. * * @param array $rooms Array of room detail arrays (RESO Rooms structure). * @return string Pipe-separated room summaries, or '' when nothing usable. */ private function formatRoomsExtraMeta($rooms) { // Guard: nothing to format. if (!is_array($rooms) || empty($rooms)) { return ''; } $formatted_rooms = array(); // Build a summary line for each room entry. foreach ($rooms as $room_details) { // Skip malformed (non-array) entries. if (!is_array($room_details)) { continue; } // Pull the RESO room fields, trimming each to a string. $room_type = isset($room_details['RoomType']) ? trim((string) $room_details['RoomType']) : ''; $room_level = isset($room_details['RoomLevel']) ? trim((string) $room_details['RoomLevel']) : ''; $room_length = isset($room_details['RoomLength']) ? trim((string) $room_details['RoomLength']) : ''; $room_width = isset($room_details['RoomWidth']) ? trim((string) $room_details['RoomWidth']) : ''; $room_units = isset($room_details['RoomLengthWidthUnits']) ? trim((string) $room_details['RoomLengthWidthUnits']) : ''; // A room with no type is not worth listing. if ($room_type === '') { continue; } $details = array(); // Include the floor/level when present. if ($room_level !== '') { $details[] = $room_level; } // Compose the dimension string from length and/or width. $dimension = ''; if ($room_length !== '' && $room_width !== '') { $dimension = $room_length . ' x ' . $room_width; } elseif ($room_length !== '') { $dimension = $room_length; } elseif ($room_width !== '') { $dimension = $room_width; } // Append units to the dimension, or use units alone if no dimension. if ($dimension !== '') { if ($room_units !== '') { $dimension .= ' ' . $room_units; } $details[] = $dimension; } elseif ($room_units !== '') { $details[] = $room_units; } // Start with the room type, then append its details after a colon. $formatted_value = $room_type; if (!empty($details)) { $formatted_value .= ': ' . implode(', ', $details); } $formatted_rooms[] = $formatted_value; } // Join all room summaries with a pipe separator. return implode(' | ', $formatted_rooms); } /** * Deal with extra meta * * Persists the property's coordinate meta and iterates every selected RESO "extra" * field, routing each to one of: the theme's REAL_HOMES_property_location meta, a * mapped custom post meta key, a mapped taxonomy term, the theme's additional-details * list, or a lowercased-label post meta — according to the admin field-selection * options in 'mlsimport_admin_fields_select'. * * @param int $property_id The property post ID. * @param array $property Parsed property payload (meta, extra_meta, etc.). * @return array Log strings: 'property_history' (HTML) and 'extra_meta_log' (plain). */ public function mlsimportSaasSetExtraMeta( $property_id, $property ) { // Accumulators for the two log formats returned to the caller. $property_history = ''; $extra_meta_log = ''; $answer = array(); $extra_fields = array(); // Admin's field-selection options drive which meta is imported and how. $options = get_option( 'mlsimport_admin_fields_select' ); if ( ! is_array( $options ) ) { $options = array(); } // Map of field-name => enabled flag, and the desired display order. $permited_meta = isset( $options['mls-fields'] ) && is_array( $options['mls-fields'] ) ? $options['mls-fields'] : array(); $field_order = isset( $options['field_order'] ) && is_array( $options['field_order'] ) ? $options['field_order'] : array(); // Store lat/long as a single "lat,long" string in the theme's location meta. if ( isset( $property['meta']['property_longitude'] ) && isset( $property['meta']['property_latitude'] ) ) { $savingx = $property['meta']['property_latitude'] . ',' . $property['meta']['property_longitude']; update_post_meta( $property_id, 'REAL_HOMES_property_location', $savingx ); $property_history .= 'Update Coordinates Meta with ' . $savingx . '
'; $extra_meta_log .= 'Property with ID ' . $property_id . ' Update Coordinates Meta with ' . $savingx . PHP_EOL; } // Process each extra MLS field carried on the property payload. if ( isset( $property['extra_meta'] ) && is_array( $property['extra_meta'] ) ) { $meta_properties = $property['extra_meta']; foreach ( $meta_properties as $meta_name => $meta_value ) { // Skip fields not present in the selection map. if ( ! isset( $permited_meta[ $meta_name ] ) ) { continue; // Skip fields explicitly disabled (flag === 0). } elseif ( isset( $permited_meta[ $meta_name ] ) && intval( $permited_meta[ $meta_name ] ) === 0 ) { continue; } // Special-case the RESO "Rooms" collection -> single formatted meta. if ( 'Rooms' === $meta_name && is_array( $meta_value ) ) { // Flatten the rooms collection and store it in the 'rooms' meta. $formatted_rooms = $this->formatRoomsExtraMeta( $meta_value ); if ( '' !== $formatted_rooms ) { update_post_meta( $property_id, 'rooms', $formatted_rooms ); $property_history .= 'Updated EXTRA Meta rooms
'; $extra_meta_log .= 'Property with ID ' . $property_id . ' Update EXTRA Meta rooms with value ' . $formatted_rooms . PHP_EOL; } continue; } // Normalize the value and resolve the display label for this field. $meta_value = $this->normalizeExtraMetaValue( $meta_value ); $orignal_meta_name = $meta_name; $feature_label = isset( $options['mls-fields-label'][ $meta_name ] ) && '' !== $options['mls-fields-label'][ $meta_name ] ? $options['mls-fields-label'][ $meta_name ] : $meta_name; // Route 1: field mapped to an explicit custom post meta key. if ( isset( $options['mls-fields-map-postmeta'][ $orignal_meta_name ] ) && '' !== $options['mls-fields-map-postmeta'][ $orignal_meta_name ] ) { $new_post_meta_key = $options['mls-fields-map-postmeta'][ $orignal_meta_name ]; update_post_meta( $property_id, $new_post_meta_key, $meta_value ); $property_history .= 'Updated CUSTOM post meta ' . $new_post_meta_key . ' original ' . $meta_name . ' and value ' . $meta_value . '
'; // Route 2: field mapped to a taxonomy -> attach a labeled term. } elseif ( isset( $options['mls-fields-map-taxonomy'][ $orignal_meta_name ] ) && '' !== $options['mls-fields-map-taxonomy'][ $orignal_meta_name ] ) { $new_taxonomy = $options['mls-fields-map-taxonomy'][ $orignal_meta_name ]; $custom_label = $options['mls-fields-label'][ $orignal_meta_name ]; // Prefix the value with its custom label to form the term name. $meta_value_with_label = array( trim( $custom_label . ' ' . $meta_value ) ); // Append (append=true) the term and refresh the term cache. wp_set_object_terms( $property_id, $meta_value_with_label, $new_taxonomy, true ); clean_term_cache( $property_id, $new_taxonomy ); $property_history .= 'Updated CUSTOM TAX: ' . $new_taxonomy . '<-- original ' . $orignal_meta_name . '/' . $meta_name . '/' . $custom_label . ' and value ' . json_encode( $meta_value_with_label ); // Route 3: no explicit mapping -> additional-details list or plain meta. } else { // Only when the value is non-empty and the field is enabled. if ( '' !== $meta_value && isset( $options['mls-fields'][ $meta_name ] ) && 1 === intval( $options['mls-fields'][ $meta_name ] ) ) { // admin flag 0 -> collect for the ordered additional-details list. if ( isset( $options['mls-fields-admin'][ $meta_name ] ) && 0 === intval( $options['mls-fields-admin'][ $meta_name ] ) ) { // Resolve the sort order: direct key, else index lookup, else 9999. if ( isset( $field_order[ $orignal_meta_name ] ) ) { $order = intval( $field_order[ $orignal_meta_name ] ); } else { $index = array_search( $orignal_meta_name, $field_order, true ); $order = ( false !== $index ) ? intval( $index ) : 9999; } // Buffer the field for later sorting/writing. $extra_fields[] = array( 'label' => $feature_label, 'value' => $meta_value, 'order' => $order, ); // admin flag 1 -> store directly as lowercased-label meta. } elseif ( isset( $options['mls-fields-admin'][ $meta_name ] ) && 1 === intval( $options['mls-fields-admin'][ $meta_name ] ) ) { update_post_meta( $property_id, strtolower( $feature_label ), $meta_value ); } } } // Record this field in both log formats. $property_history .= 'Updated EXTRA Meta ' . $meta_name . ' with label ' . $feature_label . ' and value ' . $meta_value . '
'; $extra_meta_log .= 'Property with ID ' . $property_id . ' Update EXTRA Meta ' . $meta_name . ' with value ' . $meta_value . PHP_EOL; } // Sort buffered additional-detail fields by their resolved order. usort( $extra_fields, function ( $a, $b ) { $orderA = isset( $a['order'] ) ? intval( $a['order'] ) : 9999; $orderB = isset( $b['order'] ) ? intval( $b['order'] ) : 9999; return $orderA <=> $orderB; } ); // Flatten to a label => value map in sorted order. $ordered_extra_fields = array(); foreach ( $extra_fields as $field ) { $ordered_extra_fields[ $field['label'] ] = $field['value']; } // Write the theme's additional-details meta (two keys the theme reads). update_post_meta( $property_id, 'REAL_HOMES_additional_details', $ordered_extra_fields ); update_post_meta( $property_id, 'REAL_HOMES_additional_details_list', $ordered_extra_fields ); // Return the logs to the caller. $answer['property_history'] = $property_history; $answer['extra_meta_log'] = $extra_meta_log; } return $answer; } /** * set hardcode fields after updated * * On first insert of a property, seed the theme's default meta (featured flag, * agent display, assigned agent, map flag, label color, size/lot-size units). * * @since 1.0.0 * @access protected * @var string $plugin_name * @param string $is_insert 'yes' when the property was newly inserted. * @param int $property_id The property post ID. * @param array $global_extra_fields Extra fields (unused here). * @param mixed $new_agent Agent ID to assign to the property. */ public function correlationUpdateAfter( $is_insert, $property_id, $global_extra_fields, $new_agent ) { // Only run defaults for freshly inserted properties. if ( 'yes' === $is_insert ) { // Not featured by default. update_post_meta( $property_id, 'REAL_HOMES_featured', 0 ); // Show the agent info box on the property page. update_post_meta( $property_id, 'REAL_HOMES_agent_display_option', 'agent_info' ); $options_mls = get_option( 'mlsimport_admin_mls_sync' ); // update_post_meta($property_id, 'REAL_HOMES_agents', $options_mls['property_agent']); // Assign the imported/default agent to the property. update_post_meta( $property_id, 'REAL_HOMES_agents', $new_agent ); // Disable the per-property map by default. update_post_meta( $property_id, 'REAL_HOMES_property_map', 0 ); // Default the listing label color. update_post_meta( $property_id, 'inspiry_property_label_color', '#fb641c' ); // Default the floor-area unit to "Sq Ft" only if not already set. $fave_property_size_prefix = get_post_meta( $property_id, 'REAL_HOMES_property_size_postfix', true ); if ( '' === $fave_property_size_prefix ) { update_post_meta( $property_id, 'REAL_HOMES_property_size_postfix', 'Sq Ft' ); } // Default the lot-size unit to "Sq Ft" only if not already set. $fave_property_land_postfix = get_post_meta( $property_id, 'REAL_HOMES_property_lot_size_postfix', true ); if ( '' === $fave_property_land_postfix ) { update_post_meta( $property_id, 'REAL_HOMES_property_lot_size_postfix', 'Sq Ft' ); } } } /** * save custom fields per environment * * Syncs the plugin's selected MLS fields into the theme's custom-fields registry * (stored in the 'wpresidence_admin' option as 'wpestate_custom_fields_list'). * Enabled non-admin, non-taxonomy fields are added/updated; others are removed. * Finally the list is re-sorted by field order. * * @since 1.0.0 * @access protected * @var string $plugin_name * @param string $option_name Option prefix used to read '{prefix}_admin_fields_select'. */ public function enviroment_custom_fields( $option_name ) { // Load the theme option that holds the custom-fields registry. $theme_options = get_option( 'wpresidence_admin' ); $custom_fields = array(); if ( isset( $theme_options['wpestate_custom_fields_list'] ) ) { $custom_fields = $theme_options['wpestate_custom_fields_list']; } // Ensure we have an array to work with. if ( ! is_array( $custom_fields ) ) { $custom_fields = array(); } // Guarantee every parallel column exists as an array. foreach ( array( 'add_field_name', 'add_field_label', 'add_field_type', 'add_field_order', 'add_dropdown_order' ) as $field_key ) { if ( ! isset( $custom_fields[ $field_key ] ) || ! is_array( $custom_fields[ $field_key ] ) ) { $custom_fields[ $field_key ] = array(); } } // Load the plugin's field-selection options for this prefix. $options = get_option( $option_name . '_admin_fields_select' ); if ( ! is_array( $options ) ) { $options = array(); } // Extract the sub-maps that decide import/admin/taxonomy/order per field. $mls_fields = isset( $options['mls-fields'] ) && is_array( $options['mls-fields'] ) ? $options['mls-fields'] : array(); $mls_fields_admin = isset( $options['mls-fields-admin'] ) && is_array( $options['mls-fields-admin'] ) ? $options['mls-fields-admin'] : array(); $mls_fields_tax = isset( $options['mls-fields-map-taxonomy'] ) && is_array( $options['mls-fields-map-taxonomy'] ) ? $options['mls-fields-map-taxonomy'] : array(); $field_order = isset( $options['field_order'] ) && is_array( $options['field_order'] ) ? $options['field_order'] : array(); // Reconcile each MLS field against the theme's custom-fields registry. foreach ( $mls_fields as $key => $value ) { // Per-field flags: enabled, admin-managed, taxonomy target, order. $import = intval( $value ); $admin = isset( $mls_fields_admin[ $key ] ) ? intval( $mls_fields_admin[ $key ] ) : 0; $taxonomy = isset( $mls_fields_tax[ $key ] ) ? $mls_fields_tax[ $key ] : ''; $order_value = isset( $field_order[ $key ] ) ? intval( $field_order[ $key ] ) + 100 : 100; // Add/update only enabled, non-admin, non-taxonomy fields. if ( 1 === $import && 0 === $admin && '' === $taxonomy ) { // Is this field already registered? $existing_index = array_search( $key, $custom_fields['add_field_name'], true ); if ( false === $existing_index ) { // New field: append across all parallel columns. $custom_fields['add_field_name'][] = $key; $label = isset( $options['mls-fields-label'][ $key ] ) && '' !== $options['mls-fields-label'][ $key ] ? $options['mls-fields-label'][ $key ] : $key; $custom_fields['add_field_label'][] = $label; $custom_fields['add_field_type'][] = 'short text'; $custom_fields['add_field_order'][] = $order_value; $custom_fields['add_dropdown_order'][] = ''; } else { // Existing field: refresh its label and order in place. $label = isset( $options['mls-fields-label'][ $key ] ) && '' !== $options['mls-fields-label'][ $key ] ? $options['mls-fields-label'][ $key ] : $key; $custom_fields['add_field_label'][ $existing_index ] = $label; $custom_fields['add_field_order'][ $existing_index ] = $order_value; } } else { // Not eligible: remove it from every parallel column if present. $remove_index = array_search( $key, $custom_fields['add_field_name'], true ); if ( false !== $remove_index ) { unset( $custom_fields['add_field_name'][ $remove_index ] ); unset( $custom_fields['add_field_label'][ $remove_index ] ); unset( $custom_fields['add_field_type'][ $remove_index ] ); unset( $custom_fields['add_field_order'][ $remove_index ] ); unset( $custom_fields['add_dropdown_order'][ $remove_index ] ); } } } // Re-sort all columns by field order so the registry stays ordered. if ( ! empty( $custom_fields['add_field_order'] ) ) { // Sort the order column, preserving its keys as the new sequence. asort( $custom_fields['add_field_order'] ); $ordered = array( 'add_field_name' => array(), 'add_field_label' => array(), 'add_field_type' => array(), 'add_field_order' => array(), 'add_dropdown_order' => array(), ); // Rebuild each column following the sorted order of indices. foreach ( array_keys( $custom_fields['add_field_order'] ) as $idx ) { $ordered['add_field_name'][] = $custom_fields['add_field_name'][ $idx ]; $ordered['add_field_label'][] = $custom_fields['add_field_label'][ $idx ]; $ordered['add_field_type'][] = $custom_fields['add_field_type'][ $idx ]; $ordered['add_field_order'][] = $custom_fields['add_field_order'][ $idx ]; $ordered['add_dropdown_order'][] = $custom_fields['add_dropdown_order'][ $idx ]; } $custom_fields = $ordered; } // Persist the updated registry back to the theme option. $theme_options['wpestate_custom_fields_list'] = $custom_fields; update_option( 'wpresidence_admin', $theme_options ); } /** * return theme schema * * No-op placeholder: Real Homes provides no field schema through this adapter. * * @since 1.0.0 * @access protected * @var string $plugin_name */ public function return_theme_schema() { // No schema to return for this theme. return; } /** * Filter wp_prepare_attachment_for_js to inject remote image URL and fake sizes. * * This ensures Meta Box and the WordPress media library display thumbnails * for images that are stored remotely (i.e., imported via MLS). * * @param array $response Attachment response data prepared for JS. * @return array Modified response with remote image URL and sizes if applicable. */ public function inject_remote_image_data( $response ) { // Nothing to do without an attachment ID. if ( empty( $response['id'] ) ) { return $response; } $attachment_id = $response['id']; // Only modify attachments imported by MLS if ( intval( get_post_meta( $attachment_id, 'is_mlsimport', true ) ) === 1 ) { // Bail if the attachment post cannot be loaded. $attachment = get_post( $attachment_id ); if ( ! $attachment ) { return $response; } // Determine the correct remote URL // Prefer a valid URL in the guid, else fall back to the stored external URL. $url = filter_var( $attachment->guid, FILTER_VALIDATE_URL ) ? $attachment->guid : get_post_meta( $attachment_id, 'houzez_external_url', true ); // Inject the URL and fake thumbnail size into the response if ( $url ) { $response['url'] = $url; $response['sizes'] = [ 'thumbnail' => [ 'url' => $url, 'width' => 150, 'height' => 150, ], ]; } } return $response; } /** * Filter image_downsize to return remote image data instead of local file paths. * * This enables WordPress functions like wp_get_attachment_image() to work * with remote images by returning a URL and fake dimensions. * * @param bool|array $out Whether to short-circuit the image downsize. Default false. * @param int $id Attachment ID. * @param string|array $size Requested image size. * @return array|false Array of image data (URL, width, height, crop) or false to fall back. */ public function override_image_downsize( $out, $id, $size ) { // Only override for MLS-imported attachments if ( intval( get_post_meta( $id, 'is_mlsimport', true ) ) !== 1 ) { return false; } // Bail if the attachment post cannot be loaded. $attachment = get_post( $id ); if ( ! $attachment ) { return false; } // Determine the remote image URL // Prefer a valid URL in the guid, else the stored external URL. $url = filter_var( $attachment->guid, FILTER_VALIDATE_URL ) ? $attachment->guid : get_post_meta( $id, 'houzez_external_url', true ); // No URL resolved -> let WordPress handle it normally. if ( ! $url ) { return false; } // Return URL with fake dimensions (used for thumbnail display) return [ $url, 150, 150, false ]; } }