PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 6.0.4
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v6.0.4
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
mlsimport / includes / ThemeImport.php

ThemeImport.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 6.0.4, at includes/ThemeImport.php

1,800 lines 60.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit; // Exit if accessed directly
4 }
5
6 /**
7 * Description of ThemeImport
8 *
9 * @class ThemeImport
10 */
11 class ThemeImport {
12
13
14 public $theme;
15 public $plugin_name;
16 public $enviroment;
17 public $encoded_values;
18
19
20 /**
21 * Api Request to MLSimport API using CURL
22 *
23 * @param string $method The API method to call.
24 * @param array $values_array The values to pass to the API.
25 * @param string $type The request type (default is 'GET').
26 * @return mixed The API response or error message.
27 */
28
29 public function globalApiRequestCurlSaas($method, $valuesArray, $type = 'GET') {
30 global $mlsimport;
31 $url = MLSIMPORT_API_URL . $method;
32 $headers = ['Content-Type' => 'text/plain'];
33
34 if ($method !== 'token') {
35 $token = self::getApiToken();
36 $headers = [
37 'Content-Type' => 'application/json',
38 'Authorization' => 'Bearer '.$token,
39 ];
40 }
41
42 $args = [
43 'method' => $type,
44 'headers' => $headers,
45 'body' => !empty($valuesArray) ? wp_json_encode($valuesArray) : null,
46 'timeout' => 120,
47 'redirection' => 10,
48 'httpversion' => '1.1',
49 'blocking' => true,
50 'user-agent' => $_SERVER['HTTP_USER_AGENT'],
51 ];
52
53
54 $response = $type === 'GET' ? wp_remote_get($url, $args) : wp_remote_post($url, $args);
55
56
57
58 if (is_wp_error($response)) {
59 return $response->get_error_message();
60 } else {
61 $body = wp_remote_retrieve_body($response);
62
63 $toReturn = json_decode($body, true);
64 if (json_last_error() !== JSON_ERROR_NONE) {
65 return 'JSON decode error: ' . json_last_error_msg();
66 }
67 return $toReturn;
68 }
69 }
70
71
72 /**
73 * Retrieve the API token
74 *
75 * @return string The API token.
76 */
77 private static function getApiToken() {
78 global $mlsimport;
79 return $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient();
80 }
81
82
83 /**
84 * Api Request to MLSimport API
85 *
86 * @param string $method The API method to call.
87 * @param array $valuesArray The values to pass to the API.
88 * @param string $type The request type (default is 'GET').
89 * @return array The API response data.
90 */
91
92 public static function globalApiRequestSaas($method, $valuesArray, $type = 'GET') {
93 global $mlsimport;
94 $url = MLSIMPORT_API_URL . $method;
95
96 $headers = [];
97 if ($method !== 'token' && $method !== 'mls') {
98 $token = self::getApiToken();
99 $headers = [
100 'Authorization' => 'Bearer '.$token,
101 'Content-Type' => 'application/json',
102 ];
103 }
104
105
106 $args = [
107 'method' => $type,
108 'timeout' => 45,
109 'redirection' => 5,
110 'httpversion' => '1.0',
111 'blocking' => true,
112 'headers' => $headers,
113 'cookies' => [],
114 'body' => !empty($valuesArray) ? wp_json_encode($valuesArray) : null,
115 ];
116 $response = wp_remote_post($url, $args);
117
118 // print_r($response);
119
120
121 if (is_wp_error($response)) {
122 return [
123 'success' => false,
124 'error_code' => $response->get_error_code(),
125 'error_message' => esc_html($response->get_error_message())
126 ];
127 }
128
129 if (isset($response['response']['code']) && $response['response']['code'] === 200) {
130 $receivedData = json_decode(wp_remote_retrieve_body($response), true);
131 return $receivedData;
132 } else {
133 return ['success' => false];
134 }
135
136 exit();
137 }
138
139
140
141
142
143 /**
144 * Parse Result Array with //error_log memory tracking and enhanced memory management
145 *
146 * @param array $readyToParseArray The array ready to be parsed.
147 * @param array $itemIdArray The item ID array.
148 * @param string $batchKey The batch key.
149 * @param array $mlsimportItemOptionData The item option data.
150 */
151 public function mlsimportSaasParseSearchArrayPerItem($readyToParseArray, $itemIdArray, $batchKey, $mlsimportItemOptionData) {
152 // Start with aggressive memory cleanup
153 $this->cleanUpMemory(true);
154
155 // Log initial memory usage
156 $initialMemory = memory_get_usage(true);
157 //error_log("MLS Import - START batch {$batchKey} - Memory: " . round($initialMemory / 1048576, 2) . " MB");
158
159 $counterProp = 0;
160 $processedData = [];
161
162 if (isset($readyToParseArray['data']) && is_array($readyToParseArray['data'])) {
163 // Log total items to process
164 $totalItems = count($readyToParseArray['data']);
165 //error_log("MLS Import - Processing {$totalItems} properties in batch {$batchKey}");
166
167
168
169
170
171 // Only keep essential data in memory, discard the rest
172 foreach ($readyToParseArray['data'] as $key => $property) {
173
174
175
176 // Save only what's needed from each property
177 if (isset($property['ListingKey'])) {
178 $processedData[$key] = $property;
179 }
180 // Remove from original array to free memory
181 unset($readyToParseArray['data'][$key]);
182 }
183
184 // Complete unset of the original array
185 unset($readyToParseArray);
186 $this->cleanUpMemory();
187
188
189 $mlsimportItemId = intval($itemIdArray['item_id']);
190
191 $current_prop_value = (int) get_post_meta( $mlsimportItemId, 'mlsimport_progress_properties', true );
192 //error_log('for itemID '.$mlsimportItemId.' count1 -> before bathich '.$current_prop_value);
193
194
195 // Process each property
196 foreach ($processedData as $key => $property) {
197 ++$counterProp;
198
199 // Memory usage before processing property
200 $memoryBefore = memory_get_usage(true);
201 $memoryBeforeMB = round($memoryBefore / 1048576, 2);
202
203 $listingKey = isset($property['ListingKey']) ? $property['ListingKey'] : 'unknown';
204 //error_log("MLS Import - Before property {$counterProp}/{$totalItems} (ListingKey: {$listingKey}) - Memory: {$memoryBeforeMB} MB");
205
206 // Clear out database caches that might be polluted
207 wp_cache_delete('mlsimport_force_stop_' . $itemIdArray['item_id'], 'options');
208 $GLOBALS['wpdb']->queries = array();
209
210 $status = get_option('mlsimport_force_stop_' . $itemIdArray['item_id']);
211
212 if ($status === 'no') {
213
214
215 $current_prop_value = $current_prop_value + 1;
216 update_post_meta( $mlsimportItemId, 'mlsimport_progress_properties', $current_prop_value );
217 //error_log(' count2 -> after updateing '.$current_prop_value);
218
219
220
221 // Process property and track memory
222 $this->mlsimportSaasPrepareToImportPerItem($property, $itemIdArray, 'normal', $mlsimportItemOptionData);
223
224 // Memory after processing property
225 $memoryAfter = memory_get_usage(true);
226 $memoryAfterMB = round($memoryAfter / 1048576, 2);
227 $memoryDiff = round(($memoryAfter - $memoryBefore) / 1048576, 2);
228
229 //error_log("MLS Import - After property {$counterProp}/{$totalItems} - Memory: {$memoryAfterMB} MB, Diff: {$memoryDiff} MB");
230
231 // Check for memory leak pattern
232 if ($memoryDiff > 10) {
233 //error_log("MLS Import - WARNING: Significant memory increase of {$memoryDiff} MB after property {$counterProp}");
234 // Force cleanup on large increases
235 $this->cleanUpMemory(true);
236 }
237
238 // Aggressively clean after each property
239 unset($property);
240
241 // Periodic more intensive cleanup
242 if ($counterProp % 3 == 0) {
243 $this->cleanUpMemory(true);
244
245 // Free database query cache
246 $GLOBALS['wpdb']->flush();
247
248 // Clear autoloaded options cache, which can grow large
249 wp_cache_delete('alloptions', 'options');
250
251 // Log memory after cleanup
252 $memoryAfterCleanup = memory_get_usage(true);
253 $freedMemory = round(($memoryAfter - $memoryAfterCleanup) / 1048576, 2);
254 //error_log("MLS Import - After cleanup: {$freedMemory} MB freed, Current: " . round($memoryAfterCleanup / 1048576, 2) . " MB");
255 }
256 } else {
257 //error_log("MLS Import - Import stopped by user command at property {$counterProp}");
258 update_post_meta($itemIdArray['item_id'], 'mlsimport_spawn_status', 'completed');
259 break;
260 }
261
262 // Clear property from processed data to free memory
263 unset($processedData[$key]);
264 }
265 } else {
266 //error_log("MLS Import - No valid data in batch {$batchKey}");
267 }
268
269 // Final cleanup
270 unset($processedData);
271 $this->cleanUpMemory(true);
272
273 // Log final memory stats
274 $finalMemory = memory_get_usage(true);
275 $finalMemoryMB = round($finalMemory / 1048576, 2);
276 $totalMemoryDiff = round(($finalMemory - $initialMemory) / 1048576, 2);
277 $peakMemory = round(memory_get_peak_usage(true) / 1048576, 2);
278
279 //error_log("MLS Import - END batch {$batchKey} - Processed {$counterProp} properties");
280 //error_log("MLS Import - Final Memory: {$finalMemoryMB} MB, Diff: {$totalMemoryDiff} MB, Peak: {$peakMemory} MB");
281 }
282
283
284 /**
285 * Comprehensive memory cleanup function
286 *
287 * @param bool $intensive Whether to perform intensive cleanup
288 */
289 private function cleanUpMemory($intensive = false) {
290 // Basic cleanup
291 wp_cache_flush();
292 gc_collect_cycles();
293
294 if ($intensive) {
295 // Clear WordPress object cache
296 global $wp_object_cache;
297 if (is_object($wp_object_cache) && method_exists($wp_object_cache, 'flush')) {
298 $wp_object_cache->flush();
299 }
300
301 // Clear WordPress post caches
302 clean_post_cache(0);
303
304 // Safe term cache clearing - avoid SQL errors
305 wp_cache_delete('get_terms', 'terms');
306 wp_cache_delete('term_meta', 'terms');
307 delete_option('category_children');
308
309 // Clear taxonomy-specific caches for common taxonomies
310 $taxonomies = array('category', 'post_tag', 'property_status', 'property_type', 'property_feature', 'property_label', 'property_area', 'property_city', 'property_state', 'property_neighborhood');
311 foreach ($taxonomies as $taxonomy) {
312 wp_cache_delete($taxonomy . '_relationships', 'terms');
313 }
314
315 // Clear WordPress database cache
316 global $wpdb;
317 if (is_object($wpdb)) {
318 $wpdb->queries = array();
319 if (method_exists($wpdb, 'flush')) {
320 $wpdb->flush();
321 }
322 }
323
324 // Multiple garbage collection passes can sometimes help
325 gc_collect_cycles();
326 gc_collect_cycles();
327 }
328 }
329
330
331
332
333
334
335
336 /**
337 * Write logs for import process
338 *
339 * @param string $logs The log message to write.
340 * @param string $type The type of log.
341 */
342 private function writeImportLogs($logs, $type) {
343 mlsimport_saas_single_write_import_custom_logs($logs, $type);
344 }
345
346 /**
347 * Get memory usage
348 *
349 * @return string The memory usage in MB.
350 */
351 public function mlsimportMemUsage() {
352 $memUsage = memory_get_usage(true);
353 $memUsageShow = round($memUsage / 1048576, 2);
354 return $memUsageShow . 'mb ';
355 }
356
357
358
359
360
361
362 /**
363 * Parse and import property data for a single MLSimport item in CRON.
364 * Logs memory usage for each significant operation.
365 *
366 * @param array $readyToParseArray The array with listing data (from API).
367 * @param array $itemIdArray The array with current MLSimport item info.
368 * @param string $batchKey The batch identifier for logging.
369 */
370 public function mlsimportSaasCronParseSearchArrayPerItem($readyToParseArray, $itemIdArray, $batchKey) {
371 // Gather relevant meta for this MLSimport item
372 $mlsimportItemOptionData = [
373 'mlsimport_item_standardstatus' => get_post_meta($itemIdArray['item_id'], 'mlsimport_item_standardstatus', true),
374 'mlsimport_item_standardstatusdelete' => get_post_meta($itemIdArray['item_id'], 'mlsimport_item_standardstatusdelete', true),
375 'mlsimport_item_property_user' => get_post_meta($itemIdArray['item_id'], 'mlsimport_item_property_user', true),
376 'mlsimport_item_agent' => get_post_meta($itemIdArray['item_id'], 'mlsimport_item_agent', true),
377 'mlsimport_item_property_status' => get_post_meta($itemIdArray['item_id'], 'mlsimport_item_property_status', true),
378 ];
379
380 $count = isset($readyToParseArray['data']) && is_array($readyToParseArray['data']) ? count($readyToParseArray['data']) : 0;
381 $log = '[Memory] Start batch ' . $batchKey . ' with ' . $count . ' listings: ' . (memory_get_usage(true) / 1024 / 1024) . ' MB';
382 $this->writeImportLogs($log, 'cron');
383
384 if ($count === 0) {
385 $this->writeImportLogs('[Memory] No data to parse in batch ' . $batchKey, 'cron');
386 return;
387 }
388
389 foreach ($readyToParseArray['data'] as $key => $property) {
390 // Log at the start of each property (optional, comment out if too verbose)
391 //$log = '[Memory] Before import property #' . $key . ': ' . (memory_get_usage(true) / 1024 / 1024) . ' MB';
392 //$this->writeImportLogs($log, 'cron');
393
394 $logs = 'In CRON parse search array, listing no ' . $key . ' from batch ' . $batchKey . ' with ListingKey: ' . $property['ListingKey'] . PHP_EOL;
395 $this->writeImportLogs($logs, 'cron');
396
397 // Main per-property import function (handles mapping/import/update)
398 $this->mlsimportSaasPrepareToImportPerItem($property, $itemIdArray, 'cron', $mlsimportItemOptionData);
399
400 // Clean up per-iteration memory
401 unset($property);
402 if (($key + 1) % 20 === 0) {
403 gc_collect_cycles();
404 $log = '[Memory] After importing ' . ($key + 1) . ' listings in batch ' . $batchKey . ': ' . (memory_get_usage(true) / 1024 / 1024) . ' MB';
405 $this->writeImportLogs($log, 'cron');
406 }
407 }
408 // Final memory log for this batch
409 $this->writeImportLogs('[Memory] End batch ' . $batchKey . ': ' . (memory_get_usage(true) / 1024 / 1024) . ' MB', 'cron');
410
411 // Housekeeping
412 unset($readyToParseArray, $mlsimportItemOptionData);
413 gc_collect_cycles();
414 }
415
416
417
418
419
420
421
422
423
424
425
426 /**
427 * Check if property already imported
428 *
429 * @param string $key The key to search for.
430 * @param string $postType The post type to search within (default is 'estate_property').
431 * @return int The post ID if found, or 0 if not found.
432 */
433 public function mlsimportSaasRetrievePropertyById($key, $postType = 'estate_property') {
434 $args = [
435 'post_type' => $postType,
436 'post_status' => 'any',
437 'meta_query' => [
438 [
439 'key' => 'ListingKey',
440 'value' => $key,
441 'compare' => '=',
442 ],
443 ],
444 'fields' => 'ids',
445 ];
446
447 $query = new WP_Query($args);
448 if ($query->have_posts()) {
449 $query->the_post();
450 $propertyId = get_the_ID();
451 wp_reset_postdata();
452 return $propertyId;
453 } else {
454 wp_reset_postdata();
455 return 0;
456 }
457 }
458
459
460
461
462 /**
463 * Clear taxonomy
464 *
465 * @param int $propertyId The property ID.
466 * @param array $taxonomies The taxonomies to clear.
467 */
468 public function mlsimportSaasClearPropertyForTaxonomy($propertyId, $taxonomies) {
469 if (is_array($taxonomies)) {
470 foreach ($taxonomies as $taxonomy => $term) {
471 if (is_wp_error($taxonomy)) {
472
473 continue; // Skip this iteration
474 }
475
476 if (taxonomy_exists($taxonomy)) {
477 wp_delete_object_term_relationships($propertyId, $taxonomy);
478 } else {
479 // //error_log("Taxonomy does not exist: {$taxonomy}");
480 }
481 }
482 }
483 }
484
485
486
487
488
489 /**
490 * Set taxonomy for property
491 *
492 * @param string $taxonomy The taxonomy to set.
493 * @param int $propertyId The property ID.
494 * @param mixed $fieldValues The values to set.
495 */
496 public function mlsimportSaasUpdateTaxonomyForProperty($taxonomy, $propertyId, $fieldValues) {
497 global $wpdb;
498
499 // Convert comma-separated values to array if necessary
500 if (!is_array($fieldValues)) {
501 $fieldValues = strpos($fieldValues, ',') !== false ? explode(',', $fieldValues) : [$fieldValues];
502 }
503
504 // Trim values and remove empty ones
505 $fieldValues = array_filter(array_map('trim', $fieldValues));
506
507 // Start a database transaction
508 $wpdb->query('START TRANSACTION');
509 $taxLog = [];
510
511 foreach (array_chunk($fieldValues, 5) as $chunk) {
512 foreach ($chunk as $value) {
513 if (!empty($value)) {
514 // Check if the term already exists
515 $term = $wpdb->get_row($wpdb->prepare(
516 "SELECT t.*, tt.* FROM $wpdb->terms t
517 INNER JOIN $wpdb->term_taxonomy tt ON t.term_id = tt.term_id
518 WHERE t.name = %s AND tt.taxonomy = %s",
519 $value, $taxonomy
520 ));
521
522 $taxLog[] = json_encode($term);
523 if (is_null($term)) {
524 // Insert the term if it doesn't exist
525 $wpdb->insert($wpdb->terms, [
526 'name' => $value,
527 'slug' => sanitize_title($value),
528 'term_group' => 0
529 ]);
530
531 $termId = $wpdb->insert_id;
532
533 if ($termId) {
534 // Insert term taxonomy
535 $wpdb->insert($wpdb->term_taxonomy, [
536 'term_id' => $termId,
537 'taxonomy' => $taxonomy,
538 'description' => '',
539 'parent' => 0,
540 'count' => 0
541 ]);
542
543 $termTaxonomyId = $wpdb->insert_id;
544 } else {
545 $taxLog[] = 'Error inserting term';
546 continue;
547 }
548 } else {
549 // Term exists, get term_id and term_taxonomy_id
550 $termId = $term->term_id;
551 $termTaxonomyId = $wpdb->get_var($wpdb->prepare(
552 "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE term_id = %d AND taxonomy = %s",
553 $termId, $taxonomy
554 ));
555 }
556
557 if (!empty($termTaxonomyId)) {
558 // Insert term relationship
559 $wpdb->replace($wpdb->term_relationships, [
560 'object_id' => $propertyId,
561 'term_taxonomy_id' => $termTaxonomyId
562 ]);
563 // Increment the term count
564 $wpdb->query($wpdb->prepare(
565 "UPDATE $wpdb->term_taxonomy SET count = count + 1 WHERE term_taxonomy_id = %d",
566 $termTaxonomyId
567 ));
568 } else {
569 $taxLog[] = 'Error: term_taxonomy_id is null';
570 }
571 }
572 }
573 // Flush the cache to free up memory
574 wp_cache_flush();
575 // Run garbage collection
576 gc_collect_cycles();
577 }
578 // Commit the transaction
579 $wpdb->query('COMMIT');
580
581 // Clear term cache selectively
582 wp_cache_delete("{$taxonomy}_terms", 'terms');
583 wp_cache_delete("{$taxonomy}_children", 'terms');
584
585 // Restore the term metadata filter
586 add_filter('get_term_metadata', [$wpdb->terms, 'cache_term_counts'], 10, 2);
587
588 // Log memory usage
589 // if (!empty($taxLog)) {
590 // $taxLogStr = implode(PHP_EOL, $taxLog);
591 // mlsimport_saas_single_write_import_custom_logs($taxLogStr, 'normal');
592 // unset($taxLogStr);
593 // }
594 }
595
596
597
598
599 /**
600 * Set Property Title
601 *
602 * @param int $propertyId The property ID.
603 * @param int $mlsImportPostId The MLS import post ID.
604 * @param array $property The property data.
605 * @return string The updated title format.
606 */
607 public function mlsimportSaasUpdatePropertyTitle($propertyId, $mlsImportPostId, $property) {
608 global $mlsimport;
609
610 $titleFormat = esc_html(get_post_meta($mlsImportPostId, 'mlsimport_item_title_format', true));
611
612 if ('' === $titleFormat) {
613 $options = get_option('mlsimport_admin_mls_sync');
614 $titleFormat = $options['title_format'];
615 }
616
617 $titleArray = $this->strBetweenAll($titleFormat, '{', '}');
618
619 $propertyExtraMetaArrayLowerCase = array_change_key_case($property['extra_meta'], CASE_LOWER);
620
621 foreach ($titleArray as $key => $value) {
622 $replace = '';
623 switch ($value) {
624 case 'Address':
625 $replace = $property['adr_title'] ?? '';
626 break;
627 case 'City':
628 $replace = $property['adr_city'] ?? '';
629 break;
630 case 'CountyOrParish':
631 $replace = $property['adr_county'] ?? '';
632 break;
633 case 'PropertyType':
634 $replace = $property['adr_type'] ?? '';
635 break;
636 case 'Bedrooms':
637 $replace = $property['adr_bedrooms'] ?? '';
638 break;
639 case 'Bathrooms':
640 $replace = $property['adr_bathrooms'] ?? '';
641 break;
642 case 'ListingKey':
643 $replace = $property['ListingKey'];
644 break;
645 case 'ListingId':
646 $replace = $property['adr_listingid'] ?? '';
647 break;
648 case 'StateOrProvince':
649 $replace = $property['extra_meta']['StateOrProvince'] ?? '';
650 break;
651 case 'PostalCode':
652 $replace = $property['meta']['property_zip'] ?? $property['meta']['fave_property_zip'] ?? '';
653 $replace = is_array($replace) ? strval($replace[0]) : strval($replace);
654 break;
655 case 'StreetNumberNumeric':
656 $replace = $propertyExtraMetaArrayLowerCase['streetnumbernumeric'] ?? '';
657 break;
658 case 'StreetName':
659 $replace = $propertyExtraMetaArrayLowerCase['streetname'] ?? '';
660 break;
661 }
662 $titleFormat = str_replace('{' . $value . '}', $replace, $titleFormat);
663 }
664
665 $post = [
666 'ID' => $propertyId,
667 'post_title' => $titleFormat,
668 'post_name' => $titleFormat,
669 ];
670
671 wp_update_post($post);
672
673 return $titleFormat;
674 }
675
676
677
678
679
680
681 /**
682 * Prepare meta data for property
683 *
684 * @param array $property The property data.
685 * @return array The property data with prepared meta.
686 */
687 public function mlsimportSaasPrepareMetaForProperty($property) {
688 if (isset($property['extra_meta']['BathroomsTotalDecimal']) && floatval($property['extra_meta']['BathroomsTotalDecimal']) > 0) {
689 $bathrooms = floatval($property['extra_meta']['BathroomsTotalDecimal']);
690 $property['meta']['property_bathrooms'] = $bathrooms;
691 $property['meta']['fave_property_bathrooms'] = $bathrooms;
692 $property['meta']['REAL_HOMES_property_bathrooms'] = $bathrooms;
693 }
694 return $property;
695 }
696
697
698
699
700
701 /**
702 * Attach media to post
703 *
704 * @param int $propertyId The property ID.
705 * @param array $media The media data.
706 * @param string $isInsert Whether the property is being inserted.
707 * @return string The media history log.
708 */
709 public function mlsimportSassAttachMediaToPost($propertyId, $media, $isInsert,$media_attachments) {
710
711 $mediaHistory = [];
712 //error_log("MLSImport: Starting image processing for property ID: $propertyId");
713
714 if ($isInsert === 'no') {
715 $mediaHistory[] = 'Media - We have edit - images are not replaced';
716 //error_log("MLSImport: Edit mode detected, images not replaced.");
717 return $media_attachments;
718 //return implode('</br>', $mediaHistory);
719 }
720
721 global $mlsimport;
722 include_once ABSPATH . 'wp-admin/includes/image.php';
723 $hasFeatured = false;
724
725
726
727 add_filter('intermediate_image_sizes_advanced', [$this, 'wpcUnsetImageSizes']);
728
729 // Sorting media
730 if (isset($media[0]['Order'])) {
731 $order = array_column($media, 'Order');
732 array_multisort($order, SORT_ASC, $media);
733 //error_log("MLSImport: Media sorted by 'Order'");
734 }
735
736 if (is_array($media)) {
737 foreach ($media as $image) {
738 if (isset($image['MediaCategory']) && $image['MediaCategory'] !== 'Property Photo' && $image['MediaCategory'] !== 'Photo') {
739 //error_log("MLSImport: Skipping non-photo media category: " . $image['MediaCategory']);
740 continue;
741 }
742
743 if ( empty( $image['MediaURL'] ) ) {
744 //error_log('empty mediaURL');
745 continue;
746 }
747
748
749
750 if (isset($image['MediaURL'])) {
751 $file = $image['MediaURL'];
752 $attachment = [
753 'guid' => $file,
754 'post_status' => 'inherit',
755 'post_content' => '',
756 'post_parent' => $propertyId,
757 'post_mime_type' => $image['MimeType'] ?? 'image/jpeg',
758 'post_title' => $image['MediaKey'] ?? '',
759 ];
760
761 $attachId = wp_insert_attachment($attachment, $file);
762 if (is_wp_error($attachId)) {
763 //error_log("MLSImport: Failed to insert attachment for $file. Error: " . $attachId->get_error_message());
764 } else {
765 //error_log("MLSImport: Inserted attachment ID $attachId for file $file");
766 $mediaHistory[] = 'Media - Added ' . $file . ' as attachment ' . $attachId;
767 $media_attachments[]=$attachId;
768
769
770 $mlsimport->admin->env_data->enviroment_image_save($propertyId, $attachId);
771 update_post_meta($attachId, 'is_mlsimport', 1);
772 if (!$hasFeatured) {
773 set_post_thumbnail($propertyId, $attachId);
774 //error_log("MLSImport: Set attachment ID $attachId as featured image");
775 $hasFeatured = true;
776 }
777 }
778 } else {
779 //error_log("MLSImport: Media item missing 'MediaURL', skipping.");
780 }
781 }
782 } else {
783 $mediaHistory[] = 'Media data is blank - there are no images';
784 //error_log("MLSImport: Media array is not valid or empty.");
785 }
786
787 remove_filter('intermediate_image_sizes_advanced', [$this, 'wpcUnsetImageSizes']);
788
789 return $media_attachments;
790 //return implode('</br>', $mediaHistory);
791 }
792
793
794 /**
795 * Unset image sizes
796 *
797 * @param array $sizes The sizes to unset.
798 * @return array The modified sizes array.
799 */
800 public function wpcUnsetImageSizes($sizes) {
801 return [];
802 }
803
804
805
806
807
808
809
810 /**
811 * Return user option
812 *
813 * @param int $selected The selected user ID.
814 * @return string The HTML option elements for users.
815 */
816 public function mlsimportSaasThemeImportSelectUser($selected) {
817 $userOptions = '';
818 $blogusers = get_users(['blog_id' => 1, 'orderby' => 'nicename']);
819 foreach ($blogusers as $user) {
820 $userOptions .= '<option value="' . esc_attr($user->ID) . '"';
821 if ($user->ID == $selected) {
822 $userOptions .= ' selected="selected"';
823 }
824 $userOptions .= '>' . esc_html($user->user_login) . '</option>';
825 }
826 return $userOptions;
827 }
828
829
830
831
832
833
834
835 /**
836 * Return agent option
837 *
838 * @param int $selected The selected agent ID.
839 * @return string The HTML option elements for agents.
840 */
841 public function mlsimportSaasThemeImportSelectAgent($selected) {
842 global $mlsimport;
843 $args = [
844 'post_type' => $mlsimport->admin->env_data->get_agent_post_type(),
845 'post_status' => 'publish',
846 'posts_per_page' => 150,
847 ];
848
849 $agentSelection = new WP_Query($args);
850 $agentOptions = '<option value=""></option>';
851
852 while ($agentSelection->have_posts()) {
853 $agentSelection->the_post();
854 $agentId = get_the_ID();
855
856 $agentOptions .= '<option value="' . esc_attr($agentId) . '"';
857 if ($agentId == $selected) {
858 $agentOptions .= ' selected="selected"';
859 }
860 $agentOptions .= '>' . esc_html(get_the_title()) . '</option>';
861 }
862 wp_reset_postdata();
863
864 return $agentOptions;
865 }
866
867
868
869
870
871
872
873
874 /**
875 * Delete property
876 *
877 * @param int $deleteId The ID of the property to delete.
878 * @param string $ListingKey The listing key of the property.
879 */
880 public function deleteProperty($deleteId, $ListingKey) {
881 if ($deleteId > 0) {
882 $args = [
883 'numberposts' => -1,
884 'post_type' => 'attachment',
885 'post_parent' => $deleteId,
886 'post_status' => null,
887 'orderby' => 'menu_order',
888 'order' => 'ASC',
889 ];
890 $postAttachments = get_posts($args);
891
892 foreach ($postAttachments as $attachment) {
893 wp_delete_post($attachment->ID);
894 }
895
896 wp_delete_post($deleteId);
897 $logEntry = 'Property with id ' . $deleteId . ' and ' . $ListingKey . ' was deleted on ' . current_time('Y-m-d\TH:i') . PHP_EOL;
898 $this->writeImportLogs($logEntry, 'delete');
899 }
900 }
901
902
903
904
905 /**
906 * Return array with title items
907 *
908 * @param string $string The input string.
909 * @param string $start The start delimiter.
910 * @param string $end The end delimiter.
911 * @param bool $includeDelimiters Whether to include the delimiters in the result.
912 * @param int $offset The offset to start searching from.
913 * @return array The array of strings found between the delimiters.
914 */
915 public function strBetweenAll(string $string, string $start, string $end, bool $includeDelimiters = false, int &$offset = 0): array {
916 $strings = [];
917 $length = strlen($string);
918
919 while ($offset < $length) {
920 $found = $this->strBetween($string, $start, $end, $includeDelimiters, $offset);
921 if ($found === null) {
922 break;
923 }
924
925 $strings[] = $found;
926 $offset += strlen($includeDelimiters ? $found : $start . $found . $end); // move offset to the end of the newfound string
927 }
928
929 return $strings;
930 }
931
932 /**
933 * Find string between delimiters
934 *
935 * @param string $string The input string.
936 * @param string $start The start delimiter.
937 * @param string $end The end delimiter.
938 * @param bool $includeDelimiters Whether to include the delimiters in the result.
939 * @param int $offset The offset to start searching from.
940 * @return string|null The string found between the delimiters, or null if not found.
941 */
942 public function strBetween(string $string, string $start, string $end, bool $includeDelimiters = false, int &$offset = 0): ?string {
943 if ($string === '' || $start === '' || $end === '') {
944 return null;
945 }
946
947 $startLength = strlen($start);
948 $endLength = strlen($end);
949
950 $startPos = strpos($string, $start, $offset);
951 if ($startPos === false) {
952 return null;
953 }
954
955 $endPos = strpos($string, $end, $startPos + $startLength);
956 if ($endPos === false) {
957 return null;
958 }
959
960 $length = $endPos - $startPos + ($includeDelimiters ? $endLength : -$startLength);
961 if (!$length) {
962 return '';
963 }
964
965 $offset = $startPos + ($includeDelimiters ? 0 : $startLength);
966
967 return substr($string, $offset, $length);
968 }
969
970
971
972
973
974 /**
975 * Delete property via SQL
976 *
977 * @param int $deleteId The ID of the property to delete.
978 * @param string $ListingKey The listing key of the property.
979 */
980 public function mlsimportSaasDeletePropertyViaMysql($deleteId, $ListingKey) {
981 global $mlsimport;
982
983 $postType = get_post_type($deleteId);
984 $propertyPostType = '';
985 if (isset($mlsimport->admin->env_data) && method_exists($mlsimport->admin->env_data, 'get_property_post_type')) {
986 $propertyPostType = $mlsimport->admin->env_data->get_property_post_type();
987 }
988
989 if ($postType === $propertyPostType || in_array($postType, ['estate_property', 'property'])) {
990 // Delete attachments using WordPress functions so the files are removed as well
991 $attachments = get_posts([
992 'numberposts' => -1,
993 'post_type' => 'attachment',
994 'post_parent' => $deleteId,
995 'post_status' => null,
996 'fields' => 'ids',
997 ]);
998
999 foreach ($attachments as $attachmentId) {
1000 wp_delete_attachment($attachmentId, true);
1001 }
1002
1003 $termObjList = get_the_terms($deleteId, 'property_status');
1004 $deleteIdStatus = join(', ', wp_list_pluck($termObjList, 'name'));
1005
1006 $ListingKey = get_post_meta($deleteId, 'ListingKey', true);
1007 if ('' === $ListingKey) { // manually added listing
1008 $logEntry = 'User added listing with id ' . $deleteId . ' (' . $postType . ') (status ' . $deleteIdStatus . ') and ' . $ListingKey . ' NOT DELETED' . PHP_EOL;
1009 $this->writeImportLogs($logEntry, 'delete');
1010 return;
1011 }
1012
1013 global $wpdb;
1014 $wpdb->query($wpdb->prepare("DELETE FROM $wpdb->postmeta WHERE `post_id` = %d", $deleteId));
1015 $wpdb->query($wpdb->prepare("DELETE FROM $wpdb->posts WHERE `post_parent` = %d OR `ID` = %d", $deleteId, $deleteId));
1016
1017 $logEntry = 'MYSQL DELETE -> Property with id ' . $deleteId . ' (' . $postType . ') (status ' . $deleteIdStatus . ') and ' . $ListingKey . ' was deleted on ' . current_time('Y-m-d\TH:i') . PHP_EOL;
1018 $this->writeImportLogs($logEntry, 'delete');
1019 }
1020 }
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030 /**
1031 * Prepare to import per item
1032 *
1033 * @param array $property The property data.
1034 * @param array $itemIdArray The item ID array.
1035 * @param string $tipImport The import type.
1036 * @param array $mlsimportItemOptionData The item option data.
1037 */
1038 public function mlsimportSaasPrepareToImportPerItem($property, $itemIdArray, $tipImport, $mlsimportItemOptionData) {
1039 // Pre-execution memory optimization
1040 wp_cache_flush();
1041 gc_collect_cycles();
1042
1043 // Temporarily disable WordPress hooks that might add to memory usage
1044 global $wp_filter;
1045 $saved_filters = array();
1046 if (isset($wp_filter['transition_post_status'])) {
1047 $saved_filters['transition_post_status'] = $wp_filter['transition_post_status'];
1048 unset($wp_filter['transition_post_status']);
1049 }
1050 if (isset($wp_filter['save_post'])) {
1051 $saved_filters['save_post'] = $wp_filter['save_post'];
1052 $wp_filter['save_post'] = new WP_Hook();
1053 }
1054 set_time_limit(0);
1055 global $mlsimport;
1056
1057 // Log initial memory
1058 $memStart = memory_get_usage(true);
1059 $memStartMB = round($memStart / 1048576, 2);
1060 //error_log("PROPERTY IMPORT START - ListingKey: " . ($property['ListingKey'] ?? 'unknown') . " - Memory: {$memStartMB} MB");
1061
1062 $mlsImportItemStatus = $mlsimportItemOptionData['mlsimport_item_standardstatus'];
1063 $mlsImportItemStatusDelete = $mlsimportItemOptionData['mlsimport_item_standardstatusdelete'];
1064 $newAuthor = $mlsimportItemOptionData['mlsimport_item_property_user'];
1065 $newAgent = $mlsimportItemOptionData['mlsimport_item_agent'];
1066 $propertyStatus = $mlsimportItemOptionData['mlsimport_item_property_status'];
1067
1068 if (is_array($mlsImportItemStatus)) {
1069 $mlsImportItemStatus = array_map('strtolower', $mlsImportItemStatus);
1070 }
1071
1072 if (!isset($property['ListingKey']) || empty($property['ListingKey'])) {
1073 $this->writeImportLogs('ERROR: No Listing Key ' . PHP_EOL, $tipImport);
1074 return;
1075 }
1076
1077 ob_start();
1078
1079 $ListingKey = $property['ListingKey'];
1080 $listingPostType = $mlsimport->admin->env_data->get_property_post_type();
1081
1082 // Memory before property ID lookup
1083 $memBeforeRetrieve = memory_get_usage(true);
1084 //error_log("PROPERTY IMPORT - Before property ID lookup - Memory: " . round($memBeforeRetrieve / 1048576, 2) . " MB");
1085
1086 $propertyId = intval($this->mlsimportSaasRetrievePropertyById($ListingKey, $listingPostType));
1087
1088 // Memory after property ID lookup
1089 $memAfterRetrieve = memory_get_usage(true);
1090 //error_log("PROPERTY IMPORT - After property ID lookup - Memory: " . round($memAfterRetrieve / 1048576, 2) . " MB, Diff: " . round(($memAfterRetrieve - $memBeforeRetrieve) / 1048576, 2) . " MB");
1091
1092 $status = isset($property['StandardStatus']) ? strtolower($property['StandardStatus']) : strtolower($property['extra_meta']['MlsStatus']);
1093
1094 $this->writeImportLogs('FIxing: on inserting ' .$status.'-->'.json_encode($mlsImportItemStatus). PHP_EOL, $tipImport);
1095
1096 $isInsert = $this->shouldInsertProperty($propertyId, $status, $mlsImportItemStatus, $tipImport);
1097
1098 $log = $this->mlsimportMemUsage() . '==========' . wp_json_encode($mlsImportItemStatus) . '/' . $newAuthor . '/' . $newAgent . '/' . $propertyStatus . '/ We have property with $ListingKey=' . $ListingKey . ' id=' . $propertyId . ' with status ' . $status . ' is insert? ' . $isInsert . PHP_EOL;
1099 $this->writeImportLogs($log, $tipImport);
1100
1101 $propertyHistory = [];
1102 $content = $property['content'] ?? '';
1103 $submitTitle = $ListingKey;
1104
1105 // Memory before insert/update
1106 $memBeforeInsert = memory_get_usage(true);
1107 //error_log("PROPERTY IMPORT - Before " . ($isInsert === 'yes' ? "insert" : "update/check") . " - Memory: " . round($memBeforeInsert / 1048576, 2) . " MB");
1108
1109 if ($isInsert === 'yes') {
1110 $post = [
1111 'post_title' => $submitTitle,
1112 'post_content' => $content,
1113 'post_status' => $propertyStatus,
1114 'post_type' => $listingPostType,
1115 'post_author' => $newAuthor,
1116 ];
1117
1118 $propertyId = wp_insert_post($post);
1119 if (is_wp_error($propertyId)) {
1120 $this->writeImportLogs('ERROR: on inserting ' . PHP_EOL, $tipImport);
1121 //error_log("PROPERTY IMPORT ERROR - Failed to insert property: " . $propertyId->get_error_message());
1122 } else {
1123 update_post_meta($propertyId, 'ListingKey', $ListingKey);
1124 update_post_meta($propertyId, 'MLSimport_item_inserted', $itemIdArray['item_id'],);
1125 update_post_meta($propertyId, 'mlsImportItemStatusDelete', $mlsImportItemStatusDelete);
1126
1127 $propertyHistory[] = date('F j, Y, g:i a') . ': We Inserted the property with Default title : ' . $submitTitle . ' and received id:' . $propertyId;
1128 }
1129
1130 clean_post_cache($propertyId);
1131
1132 } elseif ($propertyId !== 0) {
1133 // Memory before checking existing property
1134 $memBeforeCheck = memory_get_usage(true);
1135 //error_log("PROPERTY IMPORT - Before checking existing property - Memory: " . round($memBeforeCheck / 1048576, 2) . " MB");
1136
1137 $keep = $this->check_if_delete_when_status($propertyId,$mlsImportItemStatus,$mlsImportItemStatusDelete);
1138
1139
1140 if(!$keep){
1141 $log = 'Property with ID ' . $propertyId . ' and with name ' . get_the_title($propertyId) . ' has a status of <strong>' . $status . ' </strong> and will be deleted' . PHP_EOL;
1142
1143 // Memory before delete
1144 $memBeforeDelete = memory_get_usage(true);
1145 //error_log("PROPERTY IMPORT - Before deleting property - Memory: " . round($memBeforeDelete / 1048576, 2) . " MB");
1146
1147 $this->deleteProperty($propertyId, $ListingKey);
1148
1149 // Memory after delete
1150 $memAfterDelete = memory_get_usage(true);
1151 //error_log("PROPERTY IMPORT - After deleting property - Memory: " . round($memAfterDelete / 1048576, 2) . " MB, Diff: " . round(($memAfterDelete - $memBeforeDelete) / 1048576, 2) . " MB");
1152
1153 $this->writeImportLogs($log, $tipImport);
1154 } else {
1155 update_post_meta($propertyId, 'mlsImportItemStatusDelete', $mlsImportItemStatusDelete);
1156
1157 // Memory before updating
1158 $memBeforeUpdate = memory_get_usage(true);
1159 //error_log("PROPERTY IMPORT - Before updating existing property - Memory: " . round($memBeforeUpdate / 1048576, 2) . " MB");
1160
1161 $propertyHistory = $this->updateExistingProperty($propertyId,$mlsImportItemStatusDelete, $content, $listingPostType, $newAuthor, $status, $mlsImportItemStatus, $propertyHistory, $tipImport, $ListingKey);
1162
1163 // Memory after updating
1164 $memAfterUpdate = memory_get_usage(true);
1165 //error_log("PROPERTY IMPORT - After updating existing property - Memory: " . round($memAfterUpdate / 1048576, 2) . " MB, Diff: " . round(($memAfterUpdate - $memBeforeUpdate) / 1048576, 2) . " MB");
1166 }
1167 }
1168
1169 // Memory after insert/update
1170 $memAfterInsert = memory_get_usage(true);
1171 //error_log("PROPERTY IMPORT - After " . ($isInsert === 'yes' ? "insert" : "update/check") . " - Memory: " . round($memAfterInsert / 1048576, 2) . " MB, Diff: " . round(($memAfterInsert - $memBeforeInsert) / 1048576, 2) . " MB");
1172
1173 if ($propertyId === 0) {
1174 $this->writeImportLogs('ERROR property id is 0' . PHP_EOL, $tipImport);
1175 return;
1176 }
1177
1178 // Memory before processing details
1179 $memBeforeDetails = memory_get_usage(true);
1180 //error_log("PROPERTY IMPORT - Before processing property details - Memory: " . round($memBeforeDetails / 1048576, 2) . " MB");
1181
1182 $newTitle = $this->processPropertyDetails($property, $propertyId, $tipImport, $propertyHistory, $newAgent, $itemIdArray,$isInsert);
1183
1184 // Memory after processing details
1185 $memAfterDetails = memory_get_usage(true);
1186 //error_log("PROPERTY IMPORT - After processing property details - Memory: " . round($memAfterDetails / 1048576, 2) . " MB, Diff: " . round(($memAfterDetails - $memBeforeDetails) / 1048576, 2) . " MB");
1187
1188 $log = PHP_EOL . 'Ending on Property ' . $propertyId . ', ListingKey: ' . $ListingKey . ' , is insert? ' . $isInsert . ' with new title: ' . $newTitle . ' ' . PHP_EOL;
1189 $this->writeImportLogs($log, $tipImport);
1190
1191 clean_post_cache($propertyId);
1192
1193 // More aggressive memory cleanup
1194 // First clear specific large arrays in property data
1195 if (isset($property['Media']) && is_array($property['Media'])) {
1196 foreach ($property['Media'] as $key => $media) {
1197 unset($property['Media'][$key]);
1198 }
1199 }
1200 if (isset($property['extra_meta']) && is_array($property['extra_meta'])) {
1201 foreach ($property['extra_meta'] as $key => $value) {
1202 unset($property['extra_meta'][$key]);
1203 }
1204 }
1205 if (isset($property['meta']) && is_array($property['meta'])) {
1206 foreach ($property['meta'] as $key => $value) {
1207 unset($property['meta'][$key]);
1208 }
1209 }
1210 if (isset($property['taxonomies']) && is_array($property['taxonomies'])) {
1211 foreach ($property['taxonomies'] as $key => $value) {
1212 unset($property['taxonomies'][$key]);
1213 }
1214 }
1215
1216 // Then unset the main arrays
1217 unset($property['Media']);
1218 unset($property['extra_meta']);
1219 unset($property['meta']);
1220 unset($property['taxonomies']);
1221 unset($property);
1222
1223 // Clear any post caches that might have been created
1224 clean_post_cache($propertyId);
1225
1226 // Clear other variables that hold large data
1227 unset($log);
1228 unset($propertyHistory);
1229 $GLOBALS['wpdb']->queries = array();
1230
1231 // Clear WordPress specific caches
1232 wp_cache_delete('get_term_meta', 'terms');
1233 wp_cache_delete('terms', 'terms');
1234 wp_cache_delete('term_meta', 'terms');
1235 wp_cache_delete('get_terms', 'terms');
1236
1237 // Clear post related caches
1238 wp_cache_delete('post_meta_' . $propertyId, 'post_meta');
1239 wp_cache_delete($propertyId, 'posts');
1240
1241 // Force multiple garbage collection cycles
1242 gc_collect_cycles();
1243 gc_collect_cycles();
1244
1245 // Close and discard any output buffer content
1246 ob_end_clean();
1247
1248 // Try to trigger PHP's internal memory cleanup
1249 $dummy = str_repeat('x', 1024 * 1024);
1250 unset($dummy);
1251
1252 // Final memory usage
1253 $memEnd = memory_get_usage(true);
1254 $memEndMB = round($memEnd / 1048576, 2);
1255 $memDiff = round(($memEnd - $memStart) / 1048576, 2);
1256 //error_log("PROPERTY IMPORT COMPLETE - ListingKey: {$ListingKey} - Final Memory: {$memEndMB} MB, Total Change: {$memDiff} MB");
1257
1258 // If we see a significant memory increase, log a warning
1259 if ($memDiff > 5) {
1260 //error_log("WARNING: Significant memory increase of {$memDiff} MB after processing property {$ListingKey}");
1261 }
1262
1263 // Restore WordPress hooks
1264 global $wp_filter;
1265 if (!empty($saved_filters)) {
1266 foreach ($saved_filters as $hook => $filter) {
1267 $wp_filter[$hook] = $filter;
1268 }
1269 }
1270 }
1271
1272
1273
1274
1275
1276
1277 /**
1278 * Check if the property should be inserted
1279 *
1280 * @param int $propertyId The property ID.
1281 * @param string $status The property status.
1282 * @param array $mlsImportItemStatus The MLS import item status.
1283 * @param string $tipImport The import type.
1284 * @return string 'yes' or 'no' indicating if the property should be inserted.
1285 */
1286 private function shouldInsertProperty($propertyId, $status, $mlsImportItemStatus, $tipImport): string{
1287 $this->writeImportLogs(
1288 "Checking: on inserting {$propertyId}={$status} vs " .
1289 json_encode($mlsImportItemStatus) . " -- {$tipImport}" . PHP_EOL,
1290 $tipImport
1291 );
1292
1293 if ($propertyId !== 0 || !is_array($mlsImportItemStatus)) {
1294 //error_log('mlsImportItemStatus 0 ');
1295 return 'no';
1296
1297 }
1298
1299 $activeStatuses = [
1300 'active',
1301 'active under contract',
1302 'active with contract',
1303 'activewithcontract',
1304 'status',
1305 'activeundercontract',
1306 'comingsoon',
1307 'coming soon',
1308 'pending'
1309 ];
1310 if(is_array($mlsImportItemStatus)){
1311 if (!in_array(strtolower($status), $mlsImportItemStatus, true)) {
1312 //error_log('mlsImportItemStatus 1 ');
1313 return 'no';
1314 }
1315
1316 if ($tipImport === 'cron' && !in_array($status, $mlsImportItemStatus, true)) {
1317 //error_log('mlsImportItemStatus 2');
1318 return 'no';
1319 }
1320
1321 }else{
1322 if(!in_array($status, $activeStatuses, true) ){
1323 //error_log('mlsImportItemStatus 3 ');
1324 return 'no';
1325 }
1326 }
1327
1328 return 'yes';
1329 }
1330
1331
1332 /**
1333 * Check for property status against MLS item delete status to see if we keep or delete the listing.
1334 * @param int $property_id
1335 * @param string|array $mlsImportItemStatus
1336 * @param string|array $mlsImportItemStatusDelete
1337 * @return bool True to keep, false to delete
1338 */
1339 public function check_if_delete_when_status($property_id, $mlsImportItemStatus, $mlsImportItemStatusDelete) {
1340 // Normalize status arrays/strings to lowercase
1341 $mlsImportItemStatus = is_array($mlsImportItemStatus)
1342 ? array_map('strtolower', $mlsImportItemStatus)
1343 : strtolower($mlsImportItemStatus);
1344
1345 $mlsImportItemStatusDelete = is_array($mlsImportItemStatusDelete)
1346 ? array_map('strtolower', $mlsImportItemStatusDelete)
1347 : strtolower($mlsImportItemStatusDelete);
1348
1349 // Get post_status based on post type/taxonomy
1350 $post_status = '';
1351 if (post_type_exists('estate_property')) {
1352 $terms = get_the_terms($property_id, 'property_status');
1353 if (!empty($terms) && is_array($terms)) {
1354 $post_status = strtolower($terms[0]->name);
1355 }
1356 } elseif (post_type_exists('property') && taxonomy_exists('property_label')) {
1357 $terms = get_the_terms($property_id, 'property_label');
1358 if (!empty($terms) && is_array($terms)) {
1359 $post_status = strtolower($terms[0]->name);
1360 }
1361 } else {
1362 $post_status = strtolower(get_post_meta($property_id, 'inspiry_property_label', true));
1363 }
1364
1365 // //error_log for debugging (can comment out in production)
1366 // //error_log("[MLSImport] post_status: {$post_status} | keep_status: " . json_encode($mlsImportItemStatus) . " | delete_status: " . json_encode($mlsImportItemStatusDelete));
1367
1368 // Keep if status matches "keep" status
1369 if ((is_array($mlsImportItemStatus) && in_array($post_status, $mlsImportItemStatus, true)) ||
1370 (!is_array($mlsImportItemStatus) && $post_status === $mlsImportItemStatus)) {
1371 return true;
1372 }
1373
1374 // Delete if status matches "delete" status
1375 if (!empty($mlsImportItemStatusDelete)) {
1376 if ((is_array($mlsImportItemStatusDelete) && in_array($post_status, $mlsImportItemStatusDelete, true)) ||
1377 (!is_array($mlsImportItemStatusDelete) && $post_status === $mlsImportItemStatusDelete)) {
1378 return false;
1379 }
1380 }
1381
1382 // Default: keep
1383 return true;
1384 }
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394 /**
1395 * Check if we should keep or delete the listing when still in MLS.
1396 */
1397 public function check_if_delete_when_status_when_in_mls($property_id,$mlsimport_item_standardstatus) {
1398 // Get the inserted item and its MLS standard status
1399
1400
1401
1402 // Default to false
1403 $post_status = '';
1404
1405 // Check for post status based on post type/taxonomy
1406 if (post_type_exists('estate_property')) {
1407 // WPResidence
1408 $terms = get_the_terms($property_id, 'property_status');
1409 if (!empty($terms) && is_array($terms)) {
1410 $post_status = $terms[0]->name;
1411 }
1412 } elseif (post_type_exists('property') && taxonomy_exists('property_label')) {
1413 // Houzez
1414 $terms = get_the_terms($property_id, 'property_label');
1415 if (!empty($terms) && is_array($terms)) {
1416 $post_status = $terms[0]->name;
1417 }
1418 } else {
1419 // RealHomes
1420 $post_status = get_post_meta($property_id, 'inspiry_property_label', true);
1421 }
1422
1423 // Early return if MLS status empty
1424 if (empty($mlsimport_item_standardstatus)) {
1425 return true; // default: keep if no status set
1426 }
1427
1428 // If it's an array, check for post status in it; otherwise, compare string
1429 if (is_array($mlsimport_item_standardstatus)) {
1430 return in_array($post_status, $mlsimport_item_standardstatus, true);
1431 }
1432 return $post_status === $mlsimport_item_standardstatus;
1433 }
1434
1435
1436
1437
1438
1439
1440
1441 /**
1442 * Update existing property
1443 *
1444 * @param int $propertyId The property ID.
1445 * @param string $content The post content.
1446 * @param string $listingPostType The listing post type.
1447 * @param int $newAuthor The new author ID.
1448 * @param string $status The property status.
1449 * @param array $mlsImportItemStatus The MLS import item status.
1450 * @param array $propertyHistory The property history.
1451 * @param string $tipImport The import type.
1452 * @param string $ListingKey The listing key.
1453 * @return array Updated property history.
1454 */
1455 private function updateExistingProperty($propertyId,$mlsImportItemStatusDelete, $content, $listingPostType, $newAuthor, $status, $mlsImportItemStatus, &$propertyHistory, $tipImport, $ListingKey) {
1456
1457
1458 $post = [
1459 'ID' => $propertyId,
1460 'post_content' => $content,
1461 'post_type' => $listingPostType,
1462 'post_author' => $newAuthor,
1463 ];
1464
1465 $log = 'Property with ID ' . $propertyId . ' and with name ' . get_the_title($propertyId) . ' has a status of <strong>' . $status . '</strong> and will be Edited</br>';
1466 $this->writeImportLogs($log, $tipImport);
1467
1468 $propertyId = wp_update_post($post);
1469 if (is_wp_error($propertyId)) {
1470 $this->writeImportLogs('ERROR: on edit ' . PHP_EOL, $tipImport);
1471 } else {
1472 $submitTitle = get_the_title($propertyId);
1473 $propertyHistory[] = gmdate('F j, Y, g:i a') . ': Property with title: ' . $submitTitle . ', id:' . $propertyId . ', ListingKey:' . $ListingKey . ', Status:' . $status . ' will be edited';
1474 }
1475 clean_post_cache( $propertyId );
1476
1477 return $propertyHistory;
1478 }
1479
1480 /**
1481 * Process property details with memory tracking and optimization
1482 *
1483 * @param array $property The property data.
1484 * @param int $propertyId The property ID.
1485 * @param string $tipImport The import type.
1486 * @param array $propertyHistory The property history.
1487 * @param int $newAgent The new agent ID.
1488 * @param array $itemIdArray The item ID array.
1489 * @param string $isInsert If is a property insert
1490 */
1491 private function processPropertyDetails($property, $propertyId, $tipImport, &$propertyHistory, $newAgent, $itemIdArray, $isInsert) {
1492 global $mlsimport, $wpdb;
1493
1494 //error_log("Property Data: " . print_r($property, true));
1495
1496 // Normalize timestamp fields in extra_meta to format like "May 17, 2025 at 06:26am"
1497 if (isset($property['extra_meta']) && is_array($property['extra_meta'])) {
1498 $timestampFields = [
1499 'StatusChangeTimestamp',
1500 'STELLAR_BOMDate',
1501 'PriceChangeTimestamp',
1502 'PhotosChangeTimestamp',
1503 'BridgeModificationTimestamp',
1504 'ModificationTimestamp',
1505 'OriginalEntryTimestamp',
1506 'MajorChangeTimestamp'
1507 ];
1508
1509 foreach ($timestampFields as $tsField) {
1510 if (!empty($property['extra_meta'][$tsField])) {
1511 $timestamp = strtotime($property['extra_meta'][$tsField]);
1512 if ($timestamp !== false) {
1513 $property['extra_meta'][$tsField] = gmdate('F j, Y \a\t h:ia', $timestamp);
1514 }
1515 }
1516 }
1517 }
1518
1519
1520
1521 // 1. DISABLE AUTOCOMMIT FOR BATCH PROCESSING
1522 // This reduces memory by preventing DB auto-commits between operations
1523 if (method_exists($wpdb, 'query')) {
1524 $wpdb->query('SET autocommit = 0');
1525 }
1526
1527 // 2. TEMPORARY DISABLE ACTIONS THAT CONSUME MEMORY
1528 $suspended_actions = [];
1529 foreach (['save_post', 'added_post_meta', 'updated_post_meta'] as $action) {
1530 if (has_action($action)) {
1531 $suspended_actions[$action] = true;
1532 remove_all_actions($action);
1533 }
1534 }
1535
1536 // Initial memory
1537 $memStart = memory_get_usage(true);
1538 //error_log("PROPERTY DETAILS START - Property ID: {$propertyId} - Initial Memory: " . round($memStart / 1048576, 2) . " MB");
1539
1540 $log = PHP_EOL . $this->mlsimportMemUsage() . '====before tax======' . PHP_EOL;
1541 $this->writeImportLogs($log, $tipImport);
1542
1543 // 3. OPTIMIZE TAXONOMY PROCESSING
1544 if (isset($property['taxonomies']) && is_array($property['taxonomies'])) {
1545 $memBeforeTax = memory_get_usage(true);
1546 //error_log("PROPERTY DETAILS - Before taxonomy processing - Memory: " . round($memBeforeTax / 1048576, 2) . " MB");
1547
1548 // Load taxonomy mapping options
1549 $options = get_option('mlsimport_admin_fields_select');
1550 $theme_schema = mlsimport_hardocde_theme_schema();
1551 $taxonomy_overrides = array();
1552 if (isset($options['mls-fields-map-taxonomy']) && is_array($options['mls-fields-map-taxonomy'])) {
1553 foreach ($options['mls-fields-map-taxonomy'] as $field_key => $mapped_tax) {
1554 if ($mapped_tax === '') {
1555 continue;
1556 }
1557 if (isset($theme_schema[$field_key]) && isset($theme_schema[$field_key]['type']) &&
1558 $theme_schema[$field_key]['type'] === 'taxonomy' && isset($theme_schema[$field_key]['name'])) {
1559 $default_tax = $theme_schema[$field_key]['name'];
1560 if ($default_tax !== $mapped_tax) {
1561 $taxonomy_overrides[$default_tax] = $mapped_tax;
1562 }
1563 }
1564 }
1565 }
1566
1567 // Disable term counting temporarily (major memory saver)
1568 wp_defer_term_counting(true);
1569
1570 remove_filter('get_term_metadata', 'lazyload_term_meta', 10);
1571 wp_cache_delete('get_ancestors', 'taxonomy');
1572
1573 // Clear existing taxonomies
1574 $this->mlsimportSaasClearPropertyForTaxonomy($propertyId, $property['taxonomies']);
1575
1576 // 4. PROCESS TAXONOMIES IN CHUNKS
1577 $taxChunks = array_chunk($property['taxonomies'], 5, true);
1578 foreach ($taxChunks as $taxChunk) {
1579 foreach ($taxChunk as $taxonomy => $term) {
1580 if (isset($taxonomy_overrides[$taxonomy])) {
1581 $taxonomy = $taxonomy_overrides[$taxonomy];
1582 }
1583 wp_cache_delete("{$taxonomy}_term_counts", 'counts');
1584 $this->mlsimportSaasUpdateTaxonomyForProperty($taxonomy, $propertyId, $term);
1585 $propertyHistory[] = 'Updated Taxonomy ' . $taxonomy . ' with terms ' . wp_json_encode($term);
1586
1587 // Memory cleanup after each taxonomy
1588 wp_cache_delete('term_meta', 'terms');
1589 wp_cache_delete($taxonomy, 'terms');
1590 }
1591
1592 // 5. FORCE GC AFTER EACH CHUNK
1593 gc_collect_cycles();
1594 }
1595
1596 // Restore term filter and clean up
1597 add_filter('get_term_metadata', 'lazyload_term_meta', 10, 2);
1598 delete_option('category_children');
1599
1600 // Re-enable term counting
1601 wp_defer_term_counting(false);
1602
1603 $memAfterTax = memory_get_usage(true);
1604 //error_log("PROPERTY DETAILS - After all taxonomy processing - Memory: " . round($memAfterTax / 1048576, 2) .
1605 // " MB, Total Diff: " . round(($memAfterTax - $memBeforeTax) / 1048576, 2) . " MB");
1606 }
1607
1608 // 6. FLUSH SPECIFIC CACHES INSTEAD OF ALL
1609 // More targeted than wp_cache_flush()
1610 wp_cache_delete('terms', 'terms');
1611 wp_cache_delete('term_meta', 'terms');
1612 wp_cache_delete("post_meta_{$propertyId}", 'post_meta');
1613 wp_cache_delete($propertyId, 'posts');
1614
1615 // Prepare meta data
1616 $property = $this->mlsimportSaasPrepareMetaForProperty($property);
1617
1618 // 7. BATCH META UPDATES
1619 if (isset($property['meta']) && is_array($property['meta'])) {
1620 $memBeforeMeta = memory_get_usage(true);
1621 $metaCount = count($property['meta']);
1622
1623 // Use direct SQL for batch meta updates if many fields
1624 if ($metaCount > 20 && method_exists($wpdb, 'prepare')) {
1625 $meta_values = [];
1626 foreach ($property['meta'] as $metaName => $metaValue) {
1627 if (is_array($metaValue)) {
1628 $metaValue = implode(',', $metaValue);
1629 }
1630
1631 // Build history separately
1632 $propertyHistory[] = 'Updated Meta ' . $metaName . ' with meta_value ' . $metaValue;
1633
1634 // First delete existing
1635 $wpdb->delete(
1636 $wpdb->postmeta,
1637 ['post_id' => $propertyId, 'meta_key' => $metaName],
1638 ['%d', '%s']
1639 );
1640
1641 // Collect for batch insert
1642 $meta_values[] = $wpdb->prepare(
1643 "(%d, %s, %s)",
1644 $propertyId,
1645 $metaName,
1646 $metaValue
1647 );
1648 }
1649
1650 // Batch insert all meta at once
1651 if (!empty($meta_values)) {
1652 $wpdb->query("INSERT INTO {$wpdb->postmeta} (post_id, meta_key, meta_value) VALUES " .
1653 implode(", ", $meta_values));
1654 }
1655 } else {
1656 // Standard approach for fewer meta fields
1657 foreach ($property['meta'] as $metaName => $metaValue) {
1658 if (is_array($metaValue)) {
1659 $metaValue = implode(',', $metaValue);
1660 }
1661 update_post_meta($propertyId, $metaName, $metaValue);
1662 $propertyHistory[] = 'Updated Meta ' . $metaName . ' with meta_value ' . $metaValue;
1663 }
1664 }
1665
1666 $memAfterMeta = memory_get_usage(true);
1667 //error_log("PROPERTY DETAILS - After meta processing - Memory: " . round($memAfterMeta / 1048576, 2) .
1668 // " MB, Diff: " . round(($memAfterMeta - $memBeforeMeta) / 1048576, 2) . " MB");
1669 }
1670
1671 // Extra meta processing
1672 $extraMetaResult = $mlsimport->admin->env_data->mlsimportSaasSetExtraMeta($propertyId, $property);
1673 if (isset($extraMetaResult['property_history'])) {
1674 $propertyHistory = array_merge($propertyHistory, (array)$extraMetaResult['property_history']);
1675 }
1676
1677 // 8. PROCESS MEDIA IN CHUNKS
1678 $memBeforeMedia = memory_get_usage(true);
1679
1680
1681 if (isset($property['Media']) && is_array($property['Media'])) {
1682 $media_attachments=array();
1683
1684 $mediaCount = count($property['Media']);
1685 //error_log("PROPERTY DETAILS - Processing {$mediaCount} media items");
1686
1687 // Process in chunks of 5
1688 $mediaChunks = array_chunk($property['Media'], 5);
1689 $mediaHistoryParts = [];
1690
1691 // Clear original array to free memory
1692 $originalMedia = $property['Media'];
1693 unset($property['Media']);
1694
1695 if ($isInsert !== 'no') {
1696 delete_post_meta($propertyId, 'fave_property_images');
1697 delete_post_meta($propertyId, 'REAL_HOMES_property_images');
1698 delete_post_meta($propertyId, 'wpestate_property_gallery');
1699 //error_log("MLSImport: Deleted existing image meta for property ID: $propertyId");
1700 }
1701
1702
1703 foreach ($mediaChunks as $index => $mediaChunk) {
1704 $media_attachments = $this->mlsimportSassAttachMediaToPost($propertyId, $mediaChunk, $isInsert,$media_attachments);
1705 // $mediaHistoryParts[] = $chunkHistory;
1706
1707 // Free memory
1708 unset($mediaChunk);
1709 //unset($chunkHistory);
1710 gc_collect_cycles();
1711
1712 // Incremental progress report
1713 //error_log("PROPERTY DETAILS - Processed media chunk " . ($index + 1) . "/" . count($mediaChunks));
1714 }
1715
1716
1717 $mlsimport->admin->env_data->enviroment_image_save_gallery($propertyId, $media_attachments);
1718
1719 // Combine all chunks
1720 // $mediaHistory = implode('</br>', $mediaHistoryParts);
1721 // $propertyHistory = array_merge($propertyHistory, (array)$mediaHistory);
1722
1723 // Clean up
1724 unset($mediaChunks);
1725 unset($mediaHistoryParts);
1726 unset($mediaHistory);
1727 unset($originalMedia);
1728 } else {
1729 $mediaHistory = $this->mlsimportSassAttachMediaToPost($propertyId, $property['Media'] ?? [], $isInsert);
1730 $propertyHistory = array_merge($propertyHistory, (array)$mediaHistory);
1731 }
1732
1733
1734 $memAfterMedia = memory_get_usage(true);
1735 //error_log("PROPERTY DETAILS - After media processing - Memory: " . round($memAfterMedia / 1048576, 2) .
1736 // " MB, Diff: " . round(($memAfterMedia - $memBeforeMedia) / 1048576, 2) . " MB");
1737
1738 // Update title
1739 $newTitle = $this->mlsimportSaasUpdatePropertyTitle($propertyId, $itemIdArray['item_id'], $property);
1740 $propertyHistory[] = 'Updated title to ' . $newTitle . '</br>';
1741
1742 // Correlation update
1743 $mlsimport->admin->env_data->correlationUpdateAfter($isInsert, $propertyId, [], $newAgent);
1744
1745 // 9. COMMIT TRANSACTION
1746 if (method_exists($wpdb, 'query')) {
1747 $wpdb->query('COMMIT');
1748 $wpdb->query('SET autocommit = 1');
1749 }
1750
1751 // Save property history - using direct SQL if history is large
1752 if (!empty($propertyHistory)) {
1753 if (intval(get_option('mlsimport-disable-history', 1)) === 1) {
1754 $propertyHistory[] = '---------------------------------------------------------------</br>';
1755 $propertyHistory = implode('</br>', $propertyHistory);
1756
1757 // 10. USE DIRECT SQL FOR LARGE HISTORY
1758 if (strlen($propertyHistory) > 10000 && method_exists($wpdb, 'update')) {
1759 $wpdb->update(
1760 $wpdb->postmeta,
1761 ['meta_value' => $propertyHistory],
1762 ['post_id' => $propertyId, 'meta_key' => 'mlsimport_property_history'],
1763 ['%s'],
1764 ['%d', '%s']
1765 );
1766 } else {
1767 update_post_meta($propertyId, 'mlsimport_property_history', $propertyHistory);
1768 }
1769 }
1770 }
1771
1772 // 11. RESTORE ACTIONS
1773 if (!empty($suspended_actions)) {
1774 foreach ($suspended_actions as $action => $true) {
1775 add_action($action, '_wp_action_exists_' . $action);
1776 remove_action($action, '_wp_action_exists_' . $action);
1777 }
1778 }
1779
1780 // 12. FINAL CLEANUP
1781 $property = null;
1782 $propertyHistory = null;
1783 wp_cache_flush();
1784 gc_collect_cycles();
1785
1786 // Final memory stats
1787 $memEnd = memory_get_usage(true);
1788 //error_log("PROPERTY DETAILS COMPLETE - Property ID: {$propertyId} - " .
1789 // "Final Memory: " . round($memEnd / 1048576, 2) . " MB, " .
1790 // "Total Change: " . round(($memEnd - $memStart) / 1048576, 2) . " MB");
1791
1792 return $newTitle;
1793 }
1794
1795
1796
1797
1798
1799 }
1800