PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 6.0.7
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v6.0.7
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.7, at includes/ThemeImport.php

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