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.2.2 7.2.1 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 All 37 releases
mlsimport / enviroment / HouzezClass.php

HouzezClass.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.0.4, at enviroment/HouzezClass.php

577 lines 27.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Theme adapter: Houzez.
4 *
5 * One of the per-theme adapters in enviroment/. Maps RESO-standard MLS data
6 * onto Houzez post meta (fave_* keys), its property/agent post types, custom
7 * taxonomies and the "additional_features" repeater. Also registers filters so
8 * remotely-hosted MLS images (attachments flagged is_mlsimport) display in the
9 * media library and via wp_get_attachment_image() without a local file.
10 */
11 if ( ! defined( 'ABSPATH' ) ) {
12 // Block direct web access — only load when WordPress is bootstrapped.
13 exit; // Exit if accessed directly
14 }
15
16 /**
17 * Description of HouzezClass
18 *
19 * @author mlsimport
20 */
21 class HouzezClass {
22
23
24 /**
25 * Wire the filters that let remote MLS images render through Houzez/WP media.
26 */
27 public function __construct() {
28 // Enable support for remote MLS images in media JS and thumbnails
29 // Inject remote URL + fake sizes into media-library JS payloads.
30 add_filter( 'wp_prepare_attachment_for_js', [ $this, 'inject_remote_image_data' ], 20 );
31 // Short-circuit image_downsize so WP returns the remote URL + fake dimensions.
32 add_filter( 'image_downsize', [ $this, 'override_image_downsize' ], 10, 3 );
33 }
34
35
36 /**
37 * return custom post field
38 *
39 * @since 1.0.0
40 * @access protected
41 * @var string $plugin_name
42 */
43 public function get_property_post_type() {
44 // Houzez property CPT slug — imported listings are stored here.
45 return 'property';
46 }
47
48
49 /**
50 * return custom post field
51 *
52 * @since 1.0.0
53 * @access protected
54 * @var string $plugin_name
55 */
56 public function get_agent_post_type() {
57 // Houzez stores contacts under two CPTs: agency and agent.
58 return array('houzez_agency','houzez_agent');
59 }
60
61 /**
62 * image save
63 *
64 * @since 1.0.0
65 * @access protected
66 * @var string $plugin_name
67 */
68 public function enviroment_image_save( $property_id, $attach_id ) {
69 // Append the attachment ID to Houzez's gallery meta (one row per image).
70 add_post_meta( $property_id, 'fave_property_images', intval( $attach_id ) );
71 }
72
73 /**
74 * gallery save
75 *
76 * @since 1.0.0
77 * @access protected
78 * @var string $plugin_name
79 */
80 public function enviroment_image_save_gallery( $property_id, $post_attachments ) {
81 // No-op — Houzez stores gallery images one-by-one via enviroment_image_save().
82 return;
83 }
84
85
86 /**
87 * Format extra meta values into a safe string representation.
88 *
89 * Flattens an array meta value into a comma-joined string (JSON-encoding
90 * any nested arrays, dropping empty entries); scalars are trimmed and
91 * their comma spacing normalized.
92 *
93 * @param mixed $meta_value Raw RESO meta value (scalar or array).
94 * @return string Normalized single-line string.
95 */
96 private function normalizeExtraMetaValue($meta_value) {
97 // Array value: build a normalized list of string parts.
98 if (is_array($meta_value)) {
99 $normalized = array();
100
101 foreach ($meta_value as $value) {
102 // Nested array → JSON-encode it as one entry.
103 if (is_array($value)) {
104 $encoded = function_exists('wp_json_encode') ? wp_json_encode($value) : json_encode($value);
105
106 // Keep only a successful encode.
107 if (false !== $encoded && null !== $encoded) {
108 $normalized[] = $encoded;
109 }
110 } else {
111 // Scalar → trim and keep if non-empty.
112 $value = trim((string) $value);
113 if ('' !== $value) {
114 $normalized[] = $value;
115 }
116 }
117 }
118
119 // Nothing survived → empty string.
120 if (empty($normalized)) {
121 return '';
122 }
123
124 // Join the collected parts.
125 return implode(', ', $normalized);
126 }
127
128 // Scalar value: trim and normalize comma spacing to ", ".
129 return preg_replace('/\s*,\s*/', ', ', trim((string) $meta_value));
130 }
131
132 /**
133 * Format Rooms extra meta entries into a single string.
134 *
135 * Turns the RESO Rooms collection (each entry an array with RoomType,
136 * RoomLevel, RoomLength/Width and units) into a human-readable
137 * "Type: level, LxW units | Type: …" string. Entries without a RoomType
138 * are skipped.
139 *
140 * @param mixed $rooms RESO Rooms array (array of room detail arrays).
141 * @return string Pipe-separated room summary, or '' when empty.
142 */
143 private function formatRoomsExtraMeta($rooms) {
144 // Guard: nothing to format unless it's a non-empty array.
145 if (!is_array($rooms) || empty($rooms)) {
146 return '';
147 }
148
149 $formatted_rooms = array();
150
151 foreach ($rooms as $room_details) {
152 // Skip malformed (non-array) room entries.
153 if (!is_array($room_details)) {
154 continue;
155 }
156
157 // Pull each RESO room field, trimmed, defaulting to ''.
158 $room_type = isset($room_details['RoomType']) ? trim((string) $room_details['RoomType']) : '';
159 $room_level = isset($room_details['RoomLevel']) ? trim((string) $room_details['RoomLevel']) : '';
160 $room_length = isset($room_details['RoomLength']) ? trim((string) $room_details['RoomLength']) : '';
161 $room_width = isset($room_details['RoomWidth']) ? trim((string) $room_details['RoomWidth']) : '';
162 $room_units = isset($room_details['RoomLengthWidthUnits']) ? trim((string) $room_details['RoomLengthWidthUnits']) : '';
163
164 // RoomType is required — skip entries without one.
165 if ($room_type === '') {
166 continue;
167 }
168
169 // Collect optional detail parts (level, dimensions).
170 $details = array();
171 if ($room_level !== '') {
172 $details[] = $room_level;
173 }
174
175 // Build a dimension string from whichever of length/width is present.
176 $dimension = '';
177 if ($room_length !== '' && $room_width !== '') {
178 $dimension = $room_length . ' x ' . $room_width;
179 } elseif ($room_length !== '') {
180 $dimension = $room_length;
181 } elseif ($room_width !== '') {
182 $dimension = $room_width;
183 }
184
185 // Append units to the dimension, or keep units alone if no dimension.
186 if ($dimension !== '') {
187 if ($room_units !== '') {
188 $dimension .= ' ' . $room_units;
189 }
190 $details[] = $dimension;
191 } elseif ($room_units !== '') {
192 $details[] = $room_units;
193 }
194
195 // Compose "Type: detail, detail" for this room.
196 $formatted_value = $room_type;
197 if (!empty($details)) {
198 $formatted_value .= ': ' . implode(', ', $details);
199 }
200
201 $formatted_rooms[] = $formatted_value;
202 }
203
204 // Join all rooms with a pipe separator.
205 return implode(' | ', $formatted_rooms);
206 }
207
208 /**
209 * Return the stored "additional features" repeater for a property.
210 *
211 * @param int $property_id Houzez property post ID.
212 * @return mixed The additional_features meta value.
213 */
214 public function return_extra_fields
215 ( $property_id ) {
216 // Read Houzez's additional-features repeater meta.
217 return get_post_meta( $property_id, 'additional_features', true );
218 }
219
220
221 /**
222 * Deal with extra meta
223 *
224 * Maps a property's RESO extra_meta onto Houzez. First derives the map
225 * coordinates (fave_property_location / property_location) from whichever
226 * lat/long pair the payload provides. Then, for each user-enabled field, routes
227 * it to one of: the dedicated 'rooms' meta, a mapped custom post meta key, a
228 * mapped custom taxonomy term, an ordered entry in the 'additional_features'
229 * repeater (frontend display), or a plain post meta key (admin field). Returns
230 * the running history/log strings.
231 *
232 * @param int $property_id Houzez property post ID.
233 * @param array $property SaaS property payload (expects 'meta', 'extra_meta').
234 * @return array { property_history, extra_meta_log } log strings.
235 */
236 public function mlsimportSaasSetExtraMeta( $property_id, $property ) {
237 // Log accumulators and the ordered additional-features buffer.
238 $property_history = '';
239 $extra_meta_log = '';
240 $answer = array();
241 $extra_fields = array();
242 // Selected-fields config: enabled MLS fields plus their meta/taxonomy mappings.
243 $options = get_option('mlsimport_admin_fields_select');
244 $permited_meta = isset($options['mls-fields']) ? $options['mls-fields'] : array();
245
246 // Coordinates: prefer the houzez_geolocation_* pair when present.
247 if ( isset( $property['meta']['houzez_geolocation_long'] ) && isset( $property['meta']['houzez_geolocation_lat'] ) ) {
248 $savingx = $property['meta']['houzez_geolocation_lat'] . ',' . $property['meta']['houzez_geolocation_long'];
249 update_post_meta( $property_id, 'fave_property_location', $savingx );
250 update_post_meta( $property_id, 'property_location', $savingx );
251 $property_history .= 'Update Coordinates Meta (having houzez_geolocation_long ) with ' . $savingx . '</br>';
252 $extra_meta_log .= 'Property with ID ' . $property_id . ' Update Coordinates Meta with ' . $savingx . PHP_EOL;
253 } elseif ( isset( $property['meta']['property_longitude'] ) && isset( $property['meta']['property_latitude'] ) ) {
254 // Fallback to the generic property_latitude/longitude pair.
255 $savingx = $property['meta']['property_latitude'] . ',' . $property['meta']['property_longitude'];
256 update_post_meta( $property_id, 'fave_property_location', $savingx );
257 update_post_meta( $property_id, 'property_location', $savingx );
258 $property_history .= 'Update Coordinates Meta (having property_longitude) with ' . $savingx . '</br>';
259 $extra_meta_log .= 'Property with ID ' . $property_id . ' Update Coordinates Meta with ' . $savingx . PHP_EOL;
260 }
261
262 // Process the extra_meta collection when present.
263 if ( isset( $property['extra_meta'] ) && is_array( $property['extra_meta'] ) ) {
264 $meta_properties = $property['extra_meta'];
265
266 foreach ( $meta_properties as $meta_name => $meta_value ) {
267 // Skip fields the user did not enable in field selection.
268 if ( ! isset( $permited_meta[ $meta_name ] ) || intval( $permited_meta[ $meta_name ] ) === 0 ) {
269 continue;
270 }
271
272 // Rooms → dedicated 'rooms' meta; reuse the formatted string downstream.
273 if ( 'Rooms' === $meta_name && is_array( $meta_value ) ) {
274 $formatted_rooms = $this->formatRoomsExtraMeta( $meta_value );
275 if ( '' === $formatted_rooms ) {
276 continue;
277 }
278
279 // Save the formatted value on the dedicated meta key and
280 // reuse the string for the rest of the processing pipeline
281 // (custom mappings, additional features, etc.).
282 update_post_meta( $property_id, 'rooms', $formatted_rooms );
283 $property_history .= 'Updated EXTRA Meta rooms</br>';
284 $extra_meta_log .= 'Property with ID ' . $property_id . ' Updated EXTRA Meta rooms with value ' . $formatted_rooms . PHP_EOL;
285
286 $meta_value = $formatted_rooms;
287 }
288
289 // Normalize the value; resolve the display label (falls back to the field name).
290 $meta_value = $this->normalizeExtraMetaValue( $meta_value );
291 $orignal_meta_name = $meta_name;
292 $feature_name = isset( $options['mls-fields-label'][ $meta_name ] ) && '' !== $options['mls-fields-label'][ $meta_name ] ? $options['mls-fields-label'][ $meta_name ] : $meta_name;
293
294 // Route 1: field mapped to a custom post meta key → write there and stop.
295 if ( isset( $options['mls-fields-map-postmeta'][ $orignal_meta_name ] ) && $options['mls-fields-map-postmeta'][ $orignal_meta_name ] !== '' ) {
296 $new_post_meta_key = $options['mls-fields-map-postmeta'][ $orignal_meta_name ];
297 update_post_meta( $property_id, $new_post_meta_key, $meta_value );
298 $log_msg = 'Property with ID ' . $property_id . ' Updated CUSTOM post meta ' . $new_post_meta_key . ' original ' . $meta_name . ' with value ' . $meta_value;
299 $property_history .= $log_msg . '</br>';
300 $extra_meta_log .= $log_msg . PHP_EOL;
301 continue;
302 }
303
304 // Route 2: field mapped to a custom taxonomy → set the term and stop.
305 if ( isset( $options['mls-fields-map-taxonomy'][ $orignal_meta_name ] ) && $options['mls-fields-map-taxonomy'][ $orignal_meta_name ] !== '' ) {
306 $new_taxonomy = $options['mls-fields-map-taxonomy'][ $orignal_meta_name ];
307 // Optional label prefix for the term ('none' means no prefix).
308 $custom_label = isset( $options['mls-fields-label'][ $orignal_meta_name ] ) ? $options['mls-fields-label'][ $orignal_meta_name ] : '';
309 if ( 'none' === $custom_label ) {
310 $custom_label = '';
311 }
312 // Compose the term text and assign it (appending, not replacing).
313 $term_value = trim( $custom_label . ' ' . $meta_value );
314 $meta_value_with_label = array();
315 if ( '' !== $term_value ) {
316 $meta_value_with_label[] = $term_value;
317 wp_set_object_terms( $property_id, $meta_value_with_label, $new_taxonomy, true );
318 clean_term_cache( $property_id, $new_taxonomy );
319 }
320 $property_history .= 'Updated CUSTOM TAX: ' . $new_taxonomy . '<-- original ' . $orignal_meta_name . '/' . $meta_name . '/' . $custom_label . ' value ' . json_encode( $meta_value_with_label );
321 continue;
322 }
323
324 // Route 3: unmapped but enabled with a non-empty value.
325 if (
326 '' !== $meta_value &&
327 isset( $options['mls-fields'][ $meta_name ] ) &&
328 1 === intval( $options['mls-fields'][ $meta_name ] )
329 ) {
330 // 3a: NOT admin-only → queue for the ordered additional_features repeater.
331 if (
332 isset( $options['mls-fields-admin'][ $meta_name ] ) &&
333 0 === intval( $options['mls-fields-admin'][ $meta_name ] )
334 ) {
335 // Resolve a sort order: explicit map value, else position in field_order, else last.
336 if ( isset( $options['field_order'][ $orignal_meta_name ] ) ) {
337 $order = intval( $options['field_order'][ $orignal_meta_name ] );
338 } else {
339 $index = array_search( $orignal_meta_name, $options['field_order'], true );
340 $order = ( false !== $index ) ? intval( $index ) : 9999;
341 }
342 $extra_fields[] = array(
343 'meta_name' => $orignal_meta_name,
344 'fave_additional_feature_title' => $feature_name,
345 'fave_additional_feature_value' => $meta_value,
346 'order' => $order,
347 );
348 // 3b: admin-only → store as a plain lowercased-label post meta.
349 } elseif (
350 isset( $options['mls-fields-admin'][ $meta_name ] ) &&
351 1 === intval( $options['mls-fields-admin'][ $meta_name ] )
352 ) {
353 update_post_meta( $property_id, strtolower( $feature_name ), $meta_value );
354 $log_msg = 'Property with ID ' . $property_id . ' Updated EXTRA Meta ' . $meta_name . ' label ' . $feature_name . ' with value ' . $meta_value;
355 $property_history .= $log_msg . '</br>';
356 $extra_meta_log .= $log_msg . PHP_EOL;
357 }
358 }
359 }
360
361 // Sort the queued features by their resolved order.
362 usort(
363 $extra_fields,
364 function ( $a, $b ) {
365 $orderA = isset( $a['order'] ) ? intval( $a['order'] ) : 9999;
366 $orderB = isset( $b['order'] ) ? intval( $b['order'] ) : 9999;
367 return $orderA <=> $orderB;
368 }
369 );
370
371 // Strip the internal 'order'/'meta_name' keys down to Houzez's title/value pairs.
372 $ordered_extra_fields = array();
373 foreach ( $extra_fields as $field ) {
374 $ordered_extra_fields[] = array(
375 'fave_additional_feature_title' => $field['fave_additional_feature_title'],
376 'fave_additional_feature_value' => $field['fave_additional_feature_value'],
377 );
378 }
379
380 // Save the ordered repeater to the property.
381 update_post_meta( $property_id, 'additional_features', $ordered_extra_fields );
382 }
383
384 // Return the collected logs to the importer.
385 $answer['property_history'] = $property_history;
386 $answer['extra_meta_log'] = $extra_meta_log;
387
388 return $answer;
389 }
390
391 /**
392 * set hardcode fields after updated
393 *
394 * @since 1.0.0
395 * @access protected
396 * @var string $plugin_name
397 */
398 public function correlationUpdateAfter( $is_insert, $property_id, $global_extra_fields, $new_agent ) {
399 // Only run on a fresh insert, not on updates of existing listings.
400 if ( 'yes' === $is_insert ) {
401 // Assign the configured default agent from MLS sync settings, if any.
402 $options_mls = get_option( 'mlsimport_admin_mls_sync' );
403 if ( is_array( $options_mls ) && isset( $options_mls['property_agent'] ) ) {
404 update_post_meta( $property_id, 'fave_agents', $options_mls['property_agent'] );
405 }
406 // Then override with the agent resolved for this specific listing.
407 update_post_meta( $property_id, 'fave_agents', $new_agent );
408 update_post_meta( $property_id, 'fave_agent_display_option', 'agent_info' );
409 update_post_meta( $property_id, 'fave_featured', 0 );
410 // fave_agents
411
412 // Seed Houzez single-property display defaults (map, layout areas, features).
413 update_post_meta( $property_id, 'fave_property_map', '1' );
414 update_post_meta( $property_id, 'fave_property_map_street_view', 'show' );
415 update_post_meta( $property_id, 'fave_single_top_area', 'global' );
416 update_post_meta( $property_id, 'fave_single_content_area', 'global' );
417 update_post_meta( $property_id, 'fave_additional_features_enable', 'enable' );
418
419 // Default the size-unit prefix only when not already set.
420 $fave_property_size_prefix = get_post_meta( $property_id, 'fave_property_size_prefix', true );
421 if ( '' === $fave_property_size_prefix ) {
422 update_post_meta( $property_id, 'fave_property_size_prefix', 'Sq Ft' );
423 }
424
425 // Default the land-unit postfix only when not already set.
426 $fave_property_land_postfix = get_post_meta( $property_id, 'fave_property_land_postfix', true );
427 if ( '' === $fave_property_land_postfix ) {
428 update_post_meta( $property_id, 'fave_property_land_postfix', 'Sq Ft' );
429 }
430 }
431 }
432
433
434 /**
435 * save custom fields per environment
436 */
437 public function enviroment_custom_fields( $option_name ) {
438 // Disabled for Houzez — returns immediately; the code below is unreachable.
439 return;
440 $theme_options = get_option( 'wpresidence_admin' );
441 $custom_fields = $theme_options['wpestate_custom_fields_list'];
442 $custom_field_no = 100;
443
444 $options = get_option( $option_name . '_admin_fields_select' );
445
446 $custom_fields_admin = array();
447
448 $test = 0;
449 foreach ( $options['mls-fields'] as $key => $value ) {
450 ++$test;
451
452 if ( 1 === $value && 0 === intval( $options['mls-fields-admin'][$key]) ) {
453 if ( !in_array( $key, $custom_fields['add_field_name'] ) && '' !== $key ) {
454 ++$custom_field_no;
455 $custom_fields['add_field_name'][] = $key;
456 $custom_fields['add_field_label'][] = $key;
457 $custom_fields['add_field_type'][] = 'short text';
458 $custom_fields['add_field_order'][] = $custom_field_no;
459 $custom_fields['add_dropdown_order'][] = '';
460 }
461 } else {
462 // remove item from custom fields
463 $key_remove = array_search( $key, $custom_fields['add_field_name'] );
464
465 unset( $custom_fields['add_field_name'][ $key_remove ] );
466 unset( $custom_fields['add_field_label'][ $key_remove ] );
467 unset( $custom_fields['add_field_type'][ $key_remove ] );
468 unset( $custom_fields['add_field_order'][ $key_remove ] );
469 unset( $custom_fields['add_dropdown_order'][ $key_remove ] );
470 }
471 }
472
473 $theme_options['wpestate_custom_fields_list'] = $custom_fields;
474 update_option( 'wpresidence_admin', $theme_options );
475 }
476
477 /**
478 * return theme schema
479 *
480 * @since 1.0.0
481 * @access protected
482 * @var string $plugin_name
483 * property_type - is type, property_status is action,property_feature is features,property_label is status,
484 * property_city is city, property_area is area,property_state is county state
485 */
486 public function return_theme_schema() {
487 // No schema returned for Houzez (see the taxonomy mapping notes above).
488
489 }
490
491
492
493 /**
494 * Filter wp_prepare_attachment_for_js to inject remote image URL and fake sizes.
495 *
496 * This ensures Meta Box and the WordPress media library display thumbnails
497 * for images that are stored remotely (i.e., imported via MLS).
498 *
499 * @param array $response Attachment response data prepared for JS.
500 * @return array Modified response with remote image URL and sizes if applicable.
501 */
502 public function inject_remote_image_data( $response ) {
503 if ( empty( $response['id'] ) ) {
504 return $response;
505 }
506
507 $attachment_id = $response['id'];
508
509 // Only modify attachments imported by MLS
510 if ( intval( get_post_meta( $attachment_id, 'is_mlsimport', true ) ) === 1 ) {
511 $attachment = get_post( $attachment_id );
512 if ( ! $attachment ) {
513 return $response;
514 }
515
516 // Determine the correct remote URL
517 $url = filter_var( $attachment->guid, FILTER_VALIDATE_URL )
518 ? $attachment->guid
519 : get_post_meta( $attachment_id, 'houzez_external_url', true );
520
521 // Inject the URL and fake thumbnail size into the response
522 if ( $url ) {
523 $response['url'] = $url;
524 $response['sizes'] = [
525 'thumbnail' => [
526 'url' => $url,
527 'width' => 150,
528 'height' => 150,
529 ],
530 ];
531
532 }
533 }
534
535 return $response;
536 }
537
538 /**
539 * Filter image_downsize to return remote image data instead of local file paths.
540 *
541 * This enables WordPress functions like wp_get_attachment_image() to work
542 * with remote images by returning a URL and fake dimensions.
543 *
544 * @param bool|array $out Whether to short-circuit the image downsize. Default false.
545 * @param int $id Attachment ID.
546 * @param string|array $size Requested image size.
547 * @return array|false Array of image data (URL, width, height, crop) or false to fall back.
548 */
549 public function override_image_downsize( $out, $id, $size ) {
550 // Only override for MLS-imported attachments
551 if ( intval( get_post_meta( $id, 'is_mlsimport', true ) ) !== 1 ) {
552 return false;
553 }
554
555 $attachment = get_post( $id );
556 if ( ! $attachment ) {
557 return false;
558 }
559
560 // Determine the remote image URL
561 $url = filter_var( $attachment->guid, FILTER_VALIDATE_URL )
562 ? $attachment->guid
563 : get_post_meta( $id, 'houzez_external_url', true );
564
565 if ( ! $url ) {
566 return false;
567 }
568
569
570
571 // Return URL with fake dimensions (used for thumbnail display)
572 return [ $url, 150, 150, false ];
573 }
574
575
576 }
577