PluginProbe
Property Hive / 2.3.1
Property Hive v2.3.1
2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 1.4.61 All 261 releases
propertyhive / includes / admin / class-ph-admin-merge-contacts.php

class-ph-admin-merge-contacts.php in Property Hive 2.3.1, at includes/admin/class-ph-admin-merge-contacts.php

1,078 lines 43.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 // ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
4
5 /**
6 * PropertyHive Admin Merge Duplicate Contacts Class.
7 *
8 * @author PropertyHive
9 * @category Admin
10 * @package PropertyHive/Admin
11 * @version 1.0.0
12 */
13
14 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
15
16 if ( ! class_exists( 'PH_Admin_Merge_Contacts' ) ) :
17
18 /**
19 * PH_Admin_Merge_Contacts
20 */
21 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_Admin_Merge_Contacts; preserving the existing PH_* class name is required for plugin and extension compatibility.
22 class PH_Admin_Merge_Contacts {
23
24 /**
25 * Handles the display of contacts to merge, to allow the user to choose a primary contact
26 *
27 * @access public
28 * @return void
29 */
30 public function output()
31 {
32 // This endpoint only renders the merge review screen. The state-changing merge
33 // request is sent to the separate AJAX handler with its own nonce and capability checks.
34 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- The GET value only selects the read-only merge review screen; the AJAX mutation verifies its nonce.
35 $request_get = wp_unslash( $_GET );
36 $ids_to_merge = array();
37 $merge_ids_input = ( isset( $request_get['merge_ids'] ) && is_scalar( $request_get['merge_ids'] ) ) ? sanitize_text_field( $request_get['merge_ids'] ) : '';
38 if ( $merge_ids_input !== '' ) {
39 foreach ( explode( '|', $merge_ids_input ) as $merge_id_input ) {
40 $merge_id = absint( $merge_id_input );
41 if ( $merge_id && 'contact' === get_post_type( $merge_id ) ) {
42 $ids_to_merge[] = $merge_id;
43 }
44 }
45 $ids_to_merge = array_values( array_unique( $ids_to_merge ) );
46 }
47 $merge_ids_for_request = implode( '|', $ids_to_merge );
48
49 ?>
50 <div class="wrap propertyhive">
51
52 <h1><?php echo esc_html(__('Merge Contacts', 'propertyhive')); ?></h1>
53
54 <p>When you click 'Merge Contacts', all associated records will be moved onto the contact selected as the primary contact, and all other contacts will be deleted.</p>
55
56 <p><strong>Note:</strong> This action is irreversible.</p>
57
58 <?php
59 if ( isset( $ids_to_merge ) && count( $ids_to_merge ) > 1 )
60 {
61 foreach ( $ids_to_merge as $i => $contact_id )
62 {
63 echo '<div style="border:1px solid #CCC; padding:25px; background:#FFF; position:relative">';
64 $contact = new PH_Contact( (int)$contact_id );
65
66 // Initialise array to hold information about each contact
67 $contact_parts = array( '<h3 style="margin:0">' . ( trim($contact->post_title) != '' ? $contact->post_title : '(' . __('Unnamed Contact', 'propertyhive' ) . ')' ) . '</h3>' ) ;
68
69 $date_posted = get_the_date( '', $contact->id );
70 $contact_parts[] = 'Added on ' . $date_posted;
71
72 // Add contact address to contact parts
73 if ( $contact->get_formatted_full_address() != '' )
74 {
75 $contact_parts[] = $contact->get_formatted_full_address();
76 }
77
78 // Add email address and number to contact parts, if set
79 $email_address = $contact->_email_address;
80 $telephone_number = $contact->_telephone_number;
81 if ( !empty( $email_address ) || !empty( $telephone_number ) )
82 {
83 $contact_details_array = array_filter(array(
84 $contact->_email_address,
85 $contact->_telephone_number
86 ));
87 $contact_parts[] = implode( ', ', $contact_details_array );
88 }
89
90 // Display all selected contact types. This may be redundant when all profiles are displayed
91 $contact_types = $contact->_contact_types;
92 if ( !empty( $contact_types ) )
93 {
94 $contact_types_lookup = array(
95 'owner' => __( 'Owner', 'propertyhive' ),
96 'potentialowner' => __( 'Potential Owner', 'propertyhive' ),
97 'applicant' => __( 'Applicant', 'propertyhive' ),
98 'hotapplicant' => __( 'Hot Applicant', 'propertyhive' ),
99 'thirdparty' => __( 'Third Party Contact', 'propertyhive' ),
100 );
101
102 $contact_types_array = array();
103
104 foreach( $contact_types as $contact_type )
105 {
106 if ( isset( $contact_types_lookup[$contact_type] ) )
107 {
108 $contact_types_array[] = $contact_types_lookup[$contact_type];
109 }
110 else
111 {
112 $contact_types_array[] = ucfirst($contact_type);
113 }
114 }
115
116 $contact_parts[] = implode( ', ', $contact_types_array );
117 }
118
119 $contact_parts = $this->get_property_owner_records( $contact, $contact_parts );
120
121 $contact_parts = $this->get_potential_owner_records( $contact, $contact_parts );
122
123 $contact_parts = $this->get_third_party_records( $contact, $contact_parts );
124
125 $contact_parts = $this->get_applicant_records( $contact, $contact_parts );
126
127 $contact_parts = $this->get_enquiry_records( $contact, $contact_parts );
128
129 $contact_parts = $this->get_viewing_records( $contact, $contact_parts );
130
131 $contact_parts = $this->get_offer_records( $contact, $contact_parts );
132
133 $contact_parts = $this->get_sale_records( $contact, $contact_parts );
134
135 $contact_parts = $this->get_tenancy_records( $contact, $contact_parts );
136
137 $contact_parts = $this->get_note_records( $contact, $contact_parts );
138
139 // Stored contact details may contain markup; sanitize before the trusted extension HTML filter.
140 $contact_parts = array_map( 'wp_kses_post', $contact_parts );
141 $contact_parts = apply_filters( 'propertyhive_merge_contact_parts', $contact_parts );
142
143 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in contact summary HTML is sanitized immediately before the trusted propertyhive_merge_contact_parts extension filter.
144 echo implode( '<br>', $contact_parts );
145 ?>
146 <label style="position:absolute; right:25px; top:25px;">
147 <?php echo esc_html(__( 'Use as Primary Contact', 'propertyhive' )); ?>
148 <input type="radio" name="primary_merge_contact" value="<?php echo esc_attr((int)$contact_id); ?>"<?php if ( $i == 0 ) { echo ' checked'; } ?>>
149 </label>
150 <?php
151
152 echo "</div><br>";
153 }
154 ?>
155 <p class="form-field">
156 <input type="button" value="<?php echo esc_attr(__( 'Merge Contacts', 'propertyhive' )); ?>" class="button-primary" id="merge_contacts_button">
157 <a href="<?php echo esc_url(wp_get_raw_referer()); ?>" class="button" id="cancel_merge_button"><?php echo esc_html(__( 'Cancel', 'propertyhive' )); ?></a>
158 </p>
159
160 <script>
161
162 jQuery( function(jQuery) {
163
164 jQuery('#merge_contacts_button').click(function(e)
165 {
166 var selected_primary = jQuery('input[name="primary_merge_contact"]:checked').val();
167 if ( typeof(selected_primary) == 'undefined' )
168 {
169 alert('A primary contact must be selected');
170 return false;
171 }
172
173 var confirmBox = confirm('All records will now be merged onto the primary contact. Do you want to continue?');
174
175 if (confirmBox)
176 {
177 jQuery('#merge_contacts_button').val('Merging Contacts...');
178 jQuery('#merge_contacts_button').attr('disabled', 'disabled');
179
180 var data = {
181 action: 'propertyhive_merge_contact_records',
182 contact_ids : '<?php echo esc_js( $merge_ids_for_request ); ?>',
183 primary_contact_id: selected_primary,
184 nonce: '<?php echo esc_js( wp_create_nonce( 'propertyhive_merge_contact' ) ); ?>',
185 };
186
187 jQuery.post( '<?php echo esc_url(admin_url('admin-ajax.php')); ?>', data, function(response) {
188
189 if (response.error)
190 {
191 alert(response.error);
192
193 jQuery('#merge_contacts_button').val('Merge Contacts');
194 jQuery('#merge_contacts_button').attr('disabled', false);
195 }
196 if (response.success)
197 {
198 // Redirect to referrer, adding message in admin_notices
199 window.location.href = <?php echo wp_json_encode( admin_url( 'edit.php?post_type=contact&propertyhive_contacts_merged=1' ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); ?>;
200 }
201 });
202 }
203 });
204 });
205
206 </script>
207 <?php
208 }
209 else
210 {
211 ?>
212 <p>Could not find multiple contacts to merge. Please <a href="<?php echo esc_url(wp_get_raw_referer()); ?>">return to the contact list</a> and try again.</p>
213 <?php
214 }
215
216 ?>
217 </div>
218 <?php
219 }
220
221 /**
222 * Adds details of any associated properties to the list of contact parts
223 *
224 * @access private
225 * @return array
226 */
227 private function get_property_owner_records( $contact, $contact_parts )
228 {
229 // get properties where this is the owner
230 $args = array(
231 'post_type' => 'property',
232 'nopaging' => true,
233 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact merge must find every record linked through contact metadata so summaries and reassignment do not omit relationships.
234 'meta_query' => array(
235 'relation' => 'OR',
236 array(
237 'key' => '_owner_contact_id',
238 'value' => $contact->id,
239 'compare' => '='
240 ),
241 array(
242 'key' => '_owner_contact_id',
243 'value' => ':"' . $contact->id . '"',
244 'compare' => 'LIKE'
245 ),
246 array(
247 'key' => '_owner_contact_id',
248 'value' => ':' . $contact->id . ';',
249 'compare' => 'LIKE'
250 )
251 )
252 );
253 $property_query = new WP_Query($args);
254
255 if ($property_query->have_posts())
256 {
257 while ($property_query->have_posts())
258 {
259 $property_query->the_post();
260
261 $property = new PH_Property( get_the_ID() );
262
263 $property_department = ucwords( str_replace( '-', ' ', $property->_department) );
264
265 $contact_parts[] = $property_department . ' Owner: ' . $property->get_formatted_full_address();
266 }
267 }
268 wp_reset_postdata();
269 return $contact_parts;
270 }
271
272 /**
273 * Adds details of any potential owner records to the list of contact parts
274 *
275 * @access private
276 * @return array
277 */
278 private function get_potential_owner_records( $contact, $contact_parts )
279 {
280 // get appraisals where this is the owner and where not instructed
281 $args = array(
282 'post_type' => 'appraisal',
283 'nopaging' => true,
284 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact merge must find every record linked through contact metadata so summaries and reassignment do not omit relationships.
285 'meta_query' => array(
286 array(
287 'key' => '_property_owner_contact_id',
288 'value' => $contact->id,
289 'compare' => '='
290 ),
291 )
292 );
293
294 $appraisal_query = new WP_Query($args);
295
296 if ($appraisal_query->have_posts())
297 {
298 while ($appraisal_query->have_posts())
299 {
300 $appraisal_query->the_post();
301
302 $appraisal = new PH_Appraisal( get_the_ID() );
303
304 $property_department = ucwords( str_replace( '-', ' ', $appraisal->_department) );
305
306 $contact_parts[] = $property_department . ' Appraisal Owner: ' . $appraisal->get_formatted_full_address();
307 }
308 }
309 wp_reset_postdata();
310 return $contact_parts;
311 }
312
313 /**
314 * Adds details of any third party contact records to the list of contact parts
315 *
316 * @access private
317 * @return array
318 */
319 private function get_third_party_records( $contact, $contact_parts )
320 {
321 $third_party_categories = get_post_meta( $contact->id, '_third_party_categories', TRUE );
322 if ( is_array($third_party_categories) && !empty($third_party_categories) )
323 {
324 $third_party_category_names = array();
325
326 $ph_third_party_contacts = new PH_Third_Party_Contacts();
327
328 foreach ( $third_party_categories as $third_party_category )
329 {
330 if ( $third_party_category != '' && $third_party_category != 0 )
331 {
332 $category_name = $ph_third_party_contacts->get_category( $third_party_category );
333 if ( $category_name !== false )
334 {
335 $third_party_category_names[] = $category_name;
336 }
337 }
338 else
339 {
340 $third_party_category_names[] = 'General';
341 }
342 }
343
344 $contact_parts[] = 'Third Party Contact: ' . implode( ', ', $third_party_category_names);
345 }
346 return $contact_parts;
347 }
348
349 /**
350 * Adds details of any applicant records to the list of contact parts
351 *
352 * @access private
353 * @return array
354 */
355 private function get_applicant_records( $contact, $contact_parts )
356 {
357 $num_applicant_profiles = get_post_meta( $contact->id, '_applicant_profiles', TRUE );
358 if ( !empty( $num_applicant_profiles ) )
359 {
360 for ( $i = 0; $i < $num_applicant_profiles; ++$i )
361 {
362 $applicant_profile = get_post_meta( $contact->id, '_applicant_profile_' . $i, TRUE );
363
364 $applicant_department = isset( $applicant_profile['department'] ) ? $applicant_profile['department'] : 'Empty';
365
366 $applicant_profile_parts = array();
367
368 if ( $applicant_department == 'residential-sales' || ph_get_custom_department_based_on($applicant_department) == 'residential-sales' )
369 {
370 if ( isset( $applicant_profile['max_price'] ) && !empty( $applicant_profile['max_price'] ) )
371 {
372 $applicant_profile_parts[] = '£' . ph_display_price_field( $applicant_profile['max_price'] );
373 }
374
375 if ( isset( $applicant_profile['min_beds'] ) && !empty( $applicant_profile['min_beds'] ) )
376 {
377 $applicant_profile_parts[] = $applicant_profile['min_beds'] . ' bedrooms';
378 }
379 }
380
381 if ( $applicant_department == 'residential-lettings' || ph_get_custom_department_based_on($applicant_department) == 'residential-lettings' )
382 {
383 if ( isset( $applicant_profile['max_rent'] ) && !empty( $applicant_profile['max_rent'] ) )
384 {
385 $maximum_rent = '£' . ph_display_price_field( $applicant_profile['max_rent'] );
386 if ( isset( $applicant_profile['rent_frequency'] ) && !empty( $applicant_profile['rent_frequency'] ) )
387 {
388 $maximum_rent .= ' ' . $applicant_profile['rent_frequency'];
389 }
390 else
391 {
392 $maximum_rent .= 'pcm';
393 }
394 $applicant_profile_parts[] = $maximum_rent;
395 }
396
397 if ( isset( $applicant_profile['min_beds'] ) && !empty( $applicant_profile['min_beds'] ) )
398 {
399 $applicant_profile_parts[] = $applicant_profile['min_beds'] . ' bedrooms';
400 }
401 }
402
403 if ( $applicant_department == 'commercial' || ph_get_custom_department_based_on($applicant_department) == 'commercial' )
404 {
405 if ( isset( $applicant_profile['available_as'] ) && !empty( $applicant_profile['available_as'] ) )
406 {
407 $available_as = implode( '/', $applicant_profile['available_as'] );
408 $applicant_profile_parts[] = 'Available as ' . $available_as;
409 }
410 }
411 $applicant_profile_parts = array_filter( $applicant_profile_parts );
412
413 $applicant_profile_text = ucwords( str_replace( '-', ' ', $applicant_department ) ) . ' Applicant';
414 if ( count($applicant_profile_parts) > 0 )
415 {
416 $applicant_profile_text .= ': ' . implode( ', ', $applicant_profile_parts );
417 }
418
419 $contact_parts[] = $applicant_profile_text;
420 }
421 }
422 return $contact_parts;
423 }
424
425 /**
426 * Adds count of any enquiry records to the list of contact parts
427 *
428 * @access private
429 * @return array
430 */
431 private function get_enquiry_records( $contact, $contact_parts )
432 {
433 $meta_query = array(
434 'relation' => 'OR',
435 array(
436 'key' => '_contact_id',
437 'value' => $contact->id,
438 ),
439 );
440
441 // Not including email address lookup in enquiry count as they won't be moved or removed by the merge
442 $contact_email_address = $contact->_email_address;
443 if ( !empty($contact_email_address) )
444 {
445 $meta_query[] = array(
446 'key' => 'email',
447 'value' => $contact_email_address,
448 );
449
450 $meta_query[] = array(
451 'key' => 'email_address',
452 'value' => $contact_email_address,
453 );
454 }
455
456 $args = array(
457 'post_type' => 'enquiry',
458 'posts_per_page' => 1,
459 'no_found_rows' => false,
460 'fields' => 'ids',
461 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- One-ID query retains found_rows for the complete related-enquiry count.
462 'meta_query' => $meta_query,
463 );
464 $enquiries_query = new WP_Query( $args );
465 $enquiries_count = $enquiries_query->found_posts;
466 wp_reset_postdata();
467
468 if ( $enquiries_count > 0 )
469 {
470 $contact_parts[] = $enquiries_count . ' enquir' . ( $enquiries_count != 1 ? 'ies' : 'y' );
471 }
472 return $contact_parts;
473 }
474
475 /**
476 * Adds count of any viewing records to the list of contact parts
477 *
478 * @access private
479 * @return array
480 */
481 private function get_viewing_records( $contact, $contact_parts )
482 {
483 $args = array(
484 'post_type' => 'viewing',
485 'posts_per_page' => 1,
486 'fields' => 'ids',
487 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Viewing/offer/sale/tenancy merge summaries use posts_per_page=1, fields=ids and found_posts to display counts for one contact. Each query has a single _applicant_contact_id equality and posts_per_page=1; found_rows remains enabled for the count.
488 'meta_query' => array(
489 array(
490 'key' => '_applicant_contact_id',
491 'value' => $contact->id,
492 )
493 )
494 );
495 $viewings_query = new WP_Query( $args );
496 $viewings_count = $viewings_query->found_posts;
497 wp_reset_postdata();
498
499 if ( $viewings_count > 0 )
500 {
501 $contact_parts[] = $viewings_count . ' viewing' . ( $viewings_count != 1 ? 's' : '' );
502 }
503 return $contact_parts;
504 }
505
506 /**
507 * Adds count of any offer records to the list of contact parts
508 *
509 * @access private
510 * @return array
511 */
512 private function get_offer_records( $contact, $contact_parts )
513 {
514 $args = array(
515 'post_type' => 'offer',
516 'posts_per_page' => 1,
517 'fields' => 'ids',
518 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Viewing/offer/sale/tenancy merge summaries use posts_per_page=1, fields=ids and found_posts to display counts for one contact. Each query has a single _applicant_contact_id equality and posts_per_page=1; found_rows remains enabled for the count.
519 'meta_query' => array(
520 array(
521 'key' => '_applicant_contact_id',
522 'value' => $contact->id,
523 )
524 )
525 );
526 $offers_query = new WP_Query( $args );
527 $offers_count = $offers_query->found_posts;
528 wp_reset_postdata();
529
530 if ( $offers_count > 0 )
531 {
532 $contact_parts[] = $offers_count . ' offer' . ( $offers_count != 1 ? 's' : '' );
533 }
534 return $contact_parts;
535 }
536
537 /**
538 * Adds count of any sale records to the list of contact parts
539 *
540 * @access private
541 * @return array
542 */
543 private function get_sale_records( $contact, $contact_parts )
544 {
545 $args = array(
546 'post_type' => 'sale',
547 'posts_per_page' => 1,
548 'fields' => 'ids',
549 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Viewing/offer/sale/tenancy merge summaries use posts_per_page=1, fields=ids and found_posts to display counts for one contact. Each query has a single _applicant_contact_id equality and posts_per_page=1; found_rows remains enabled for the count.
550 'meta_query' => array(
551 array(
552 'key' => '_applicant_contact_id',
553 'value' => $contact->id,
554 )
555 )
556 );
557 $sales_query = new WP_Query( $args );
558 $sales_count = $sales_query->found_posts;
559 wp_reset_postdata();
560
561 if ( $sales_count > 0 )
562 {
563 $contact_parts[] = $sales_count . ' sale' . ( $sales_count != 1 ? 's' : '' );
564 }
565 return $contact_parts;
566 }
567
568 /**
569 * Adds count of any tenancy records to the list of contact parts
570 *
571 * @access private
572 * @return array
573 */
574 private function get_tenancy_records( $contact, $contact_parts )
575 {
576 $args = array(
577 'post_type' => 'tenancy',
578 'posts_per_page' => 1,
579 'fields' => 'ids',
580 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Viewing/offer/sale/tenancy merge summaries use posts_per_page=1, fields=ids and found_posts to display counts for one contact. Each query has a single _applicant_contact_id equality and posts_per_page=1; found_rows remains enabled for the count.
581 'meta_query' => array(
582 array(
583 'key' => '_applicant_contact_id',
584 'value' => $contact->id
585 )
586 )
587 );
588 $tenancies_query = new WP_Query( $args );
589 $tenancies_count = $tenancies_query->found_posts;
590 wp_reset_postdata();
591
592 if ( $tenancies_count > 0 )
593 {
594 $contact_parts[] = $tenancies_count . ' tenanc' . ( $tenancies_count != 1 ? 'ies' : 'y' );
595 }
596 return $contact_parts;
597 }
598
599 /**
600 * Adds count of any note records to the list of contact parts
601 *
602 * @access private
603 * @return array
604 */
605 private function get_note_records( $contact, $contact_parts )
606 {
607 global $post;
608
609 $post = get_post((int)$contact->id);
610
611 $args = array(
612 'post_id' => (int)$contact->id,
613 'type' => 'propertyhive_note',
614 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact merge must find every record linked through contact metadata so summaries and reassignment do not omit relationships.
615 'meta_query' => array(
616 array(
617 'key' => 'related_to',
618 'value' => '"' . (int)$contact->id . '"',
619 'compare' => 'LIKE',
620 ),
621 )
622 );
623 $notes = get_comments( $args );
624
625 $notes_count = count($notes);
626
627 if ( $notes_count > 0 )
628 {
629 $contact_parts[] = $notes_count . ' note' . ( $notes_count != 1 ? 's' : '' );
630 }
631
632 return $contact_parts;
633 }
634
635 public function do_merge( $primary_contact_id, $contacts_to_merge )
636 {
637 global $wpdb, $post;
638
639 // Merge contact_types into primary
640 $primary_contact_types = get_post_meta( $primary_contact_id, '_contact_types', true );
641 if ( $primary_contact_types == '' || !is_array($primary_contact_types) )
642 {
643 $primary_contact_types = array();
644 }
645
646 foreach ( $contacts_to_merge as $child_contact_id )
647 {
648 $child_contact_types = get_post_meta( $child_contact_id, '_contact_types', true );
649 if ( is_array($child_contact_types) )
650 {
651 $primary_contact_types = array_merge( $primary_contact_types, $child_contact_types );
652 }
653 }
654
655 update_post_meta( $primary_contact_id, '_contact_types', array_unique( $primary_contact_types ) );
656
657 // Merge third party categories into primary
658 $primary_third_party_categories = get_post_meta( $primary_contact_id, '_third_party_categories', true );
659 if ( $primary_third_party_categories == '' || !is_array($primary_third_party_categories) )
660 {
661 $primary_third_party_categories = array();
662 }
663
664 foreach ( $contacts_to_merge as $child_contact_id )
665 {
666 $child_third_party_categories = get_post_meta( $child_contact_id, '_third_party_categories', true );
667 if ( is_array($child_third_party_categories) )
668 {
669 $primary_third_party_categories = array_merge( $primary_third_party_categories, $child_third_party_categories );
670 }
671 }
672
673 if ( count($primary_third_party_categories) > 0 )
674 {
675 update_post_meta( $primary_contact_id, '_third_party_categories', array_unique( $primary_third_party_categories ) );
676 }
677
678 // Move applicant profiles to primary
679 $primary_applicant_profiles = get_post_meta( $primary_contact_id, '_applicant_profiles', true );
680 if ( empty($primary_applicant_profiles) )
681 {
682 $primary_applicant_profiles = 0;
683 }
684
685 $hot_applicant = '';
686 foreach ( $contacts_to_merge as $child_contact_id )
687 {
688 $child_applicant_profiles = get_post_meta( $child_contact_id, '_applicant_profiles', true );
689 if ( !empty($child_applicant_profiles) )
690 {
691 for ( $i = 0; $i < $child_applicant_profiles; ++$i )
692 {
693 $child_applicant_profile = get_post_meta( $child_contact_id, '_applicant_profile_' . $i, true );
694 if ( !empty($child_applicant_profile) )
695 {
696 update_post_meta( $primary_contact_id, '_applicant_profile_' . $primary_applicant_profiles, $child_applicant_profile );
697
698 $child_applicant_profile_match_history = get_post_meta( $child_contact_id, '_applicant_profile_' . $i . '_match_history', true );
699 if ( !empty($child_applicant_profile_match_history) )
700 {
701 update_post_meta( $primary_contact_id, '_applicant_profile_' . $primary_applicant_profiles . '_match_history', $child_applicant_profile_match_history );
702 }
703
704 if ( isset($child_applicant_profile['grading']) && ph_clean($child_applicant_profile['grading']) == 'hot' )
705 {
706 $hot_applicant = 'yes';
707 }
708
709 ++$primary_applicant_profiles;
710 }
711 }
712 }
713 }
714
715 if ( $hot_applicant == 'yes' )
716 {
717 update_post_meta( $primary_contact_id, '_hot_applicant', $hot_applicant );
718 }
719
720 if ( $primary_applicant_profiles > 0 )
721 {
722 update_post_meta( $primary_contact_id, '_applicant_profiles', $primary_applicant_profiles );
723 }
724
725 // Merge dismissed properties
726 $primary_dismissed_properties = get_post_meta( $primary_contact_id, '_dismissed_properties', true );
727 if ( empty($primary_dismissed_properties) )
728 {
729 $primary_dismissed_properties = array();
730 }
731 foreach ( $contacts_to_merge as $child_contact_id )
732 {
733 $child_dismissed_properties = get_post_meta( $child_contact_id, '_dismissed_properties', true );
734 if ( is_array($child_dismissed_properties) && !empty($child_dismissed_properties) )
735 {
736 $primary_dismissed_properties = array_merge($primary_dismissed_properties, $child_dismissed_properties);
737 }
738 }
739 if ( !empty($primary_dismissed_properties) )
740 {
741 $primary_dismissed_properties = array_unique($primary_dismissed_properties);
742 update_post_meta( $primary_contact_id, '_dismissed_properties', $primary_dismissed_properties );
743 }
744
745 // Move appraisal records to primary
746 $args = array(
747 'post_type' => 'appraisal',
748 'nopaging' => true,
749 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact merge must find every record linked through contact metadata so summaries and reassignment do not omit relationships.
750 'meta_query' => array(
751 array(
752 'key' => '_property_owner_contact_id',
753 'value' => $contacts_to_merge,
754 'compare' => 'IN'
755 ),
756 )
757 );
758
759 $appraisal_query = new WP_Query($args);
760
761 if ($appraisal_query->have_posts())
762 {
763 while ($appraisal_query->have_posts())
764 {
765 $appraisal_query->the_post();
766 foreach ( $contacts_to_merge as $child_contact_id )
767 {
768 update_post_meta( get_the_id(), '_property_owner_contact_id', $primary_contact_id, $child_contact_id );
769 }
770 }
771 }
772 wp_reset_postdata();
773
774 foreach ( $contacts_to_merge as $child_contact_id )
775 {
776 // Move property owner records to primary
777 $args = array(
778 'post_type' => 'property',
779 'nopaging' => true,
780 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact merge must find every record linked through contact metadata so summaries and reassignment do not omit relationships.
781 'meta_query' => array(
782 'relation' => 'OR',
783 array(
784 'key' => '_owner_contact_id',
785 'value' => $child_contact_id,
786 'compare' => '='
787 ),
788 array(
789 'key' => '_owner_contact_id',
790 'value' => ':"' . $child_contact_id . '"',
791 'compare' => 'LIKE'
792 ),
793 array(
794 'key' => '_owner_contact_id',
795 'value' => ':' . $child_contact_id . ';',
796 'compare' => 'LIKE'
797 )
798 )
799 );
800 $property_query = new WP_Query($args);
801
802 if ($property_query->have_posts())
803 {
804 while ($property_query->have_posts())
805 {
806 $property_query->the_post();
807
808 $property_owner_ids = get_post_meta( get_the_id(), '_owner_contact_id', true );
809
810 if ( !is_array( $property_owner_ids ) )
811 {
812 update_post_meta( get_the_id(), '_owner_contact_id', $primary_contact_id, $child_contact_id );
813 }
814 else
815 {
816 unset($property_owner_ids[array_search($child_contact_id, $property_owner_ids)]);
817 $property_owner_ids[] = $primary_contact_id;
818 update_post_meta( get_the_id(), '_owner_contact_id', $property_owner_ids );
819 }
820 }
821 }
822 wp_reset_postdata();
823 }
824
825 // Move enquiries to primary
826 $args = array(
827 'post_type' => 'enquiry',
828 'nopaging' => true,
829 'fields' => 'ids',
830 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact merge must find every record linked through contact metadata so summaries and reassignment do not omit relationships.
831 'meta_query' => array(
832 array(
833 'key' => '_contact_id',
834 'value' => $contacts_to_merge,
835 'compare' => 'IN'
836 ),
837 ),
838 );
839 $enquiries_query = new WP_Query( $args );
840
841 if ($enquiries_query->have_posts())
842 {
843 while ($enquiries_query->have_posts())
844 {
845 $enquiries_query->the_post();
846 foreach ( $contacts_to_merge as $child_contact_id )
847 {
848 update_post_meta( get_the_id(), '_contact_id', $primary_contact_id, $child_contact_id );
849 }
850 }
851 }
852 wp_reset_postdata();
853
854 // Move viewings to primary
855 $args = array(
856 'post_type' => 'viewing',
857 'nopaging' => true,
858 'fields' => 'ids',
859 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact merge must find every record linked through contact metadata so summaries and reassignment do not omit relationships.
860 'meta_query' => array(
861 array(
862 'key' => '_applicant_contact_id',
863 'value' => $contacts_to_merge,
864 'compare' => 'IN'
865 ),
866 ),
867 );
868 $viewings_query = new WP_Query( $args );
869
870 if ($viewings_query->have_posts())
871 {
872 while ($viewings_query->have_posts())
873 {
874 $viewings_query->the_post();
875 foreach ( $contacts_to_merge as $child_contact_id )
876 {
877 update_post_meta( get_the_id(), '_applicant_contact_id', $primary_contact_id, $child_contact_id );
878 }
879 }
880 }
881 wp_reset_postdata();
882
883 // Move offers to primary
884 $args = array(
885 'post_type' => 'offer',
886 'nopaging' => true,
887 'fields' => 'ids',
888 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact merge must find every record linked through contact metadata so summaries and reassignment do not omit relationships.
889 'meta_query' => array(
890 array(
891 'key' => '_applicant_contact_id',
892 'value' => $contacts_to_merge,
893 'compare' => 'IN'
894 ),
895 ),
896 );
897 $offers_query = new WP_Query( $args );
898
899 if ($offers_query->have_posts())
900 {
901 while ($offers_query->have_posts())
902 {
903 $offers_query->the_post();
904 foreach ( $contacts_to_merge as $child_contact_id )
905 {
906 update_post_meta( get_the_id(), '_applicant_contact_id', $primary_contact_id, $child_contact_id );
907 }
908 }
909 }
910 wp_reset_postdata();
911
912 // Move sales to primary
913 $args = array(
914 'post_type' => 'sale',
915 'nopaging' => true,
916 'fields' => 'ids',
917 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact merge must find every record linked through contact metadata so summaries and reassignment do not omit relationships.
918 'meta_query' => array(
919 array(
920 'key' => '_applicant_contact_id',
921 'value' => $contacts_to_merge,
922 'compare' => 'IN'
923 ),
924 ),
925 );
926 $sales_query = new WP_Query( $args );
927
928 if ($sales_query->have_posts())
929 {
930 while ($sales_query->have_posts())
931 {
932 $sales_query->the_post();
933 foreach ( $contacts_to_merge as $child_contact_id )
934 {
935 update_post_meta( get_the_id(), '_applicant_contact_id', $primary_contact_id, $child_contact_id );
936 }
937 }
938 }
939 wp_reset_postdata();
940
941 // Move tenancies to primary
942 $args = array(
943 'post_type' => 'tenancy',
944 'nopaging' => true,
945 'fields' => 'ids',
946 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact merge must find every record linked through contact metadata so summaries and reassignment do not omit relationships.
947 'meta_query' => array(
948 array(
949 'key' => '_applicant_contact_id',
950 'value' => $contacts_to_merge,
951 'compare' => 'IN'
952 ),
953 ),
954 );
955 $tenancies_query = new WP_Query( $args );
956
957 if ($tenancies_query->have_posts())
958 {
959 while ($tenancies_query->have_posts())
960 {
961 $tenancies_query->the_post();
962 foreach ( $contacts_to_merge as $child_contact_id )
963 {
964 update_post_meta( get_the_id(), '_applicant_contact_id', $primary_contact_id, $child_contact_id );
965 }
966 }
967 }
968 wp_reset_postdata();
969
970 // Copy notes to primary
971 foreach ( $contacts_to_merge as $child_contact_id )
972 {
973 $post = get_post((int)$child_contact_id);
974
975 $args = array(
976 'post_id' => (int)$child_contact_id,
977 'type' => 'propertyhive_note',
978 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact merge must find every record linked through contact metadata so summaries and reassignment do not omit relationships.
979 'meta_query' => array(
980 array(
981 'key' => 'related_to',
982 'value' => '"' . (int)$child_contact_id . '"',
983 'compare' => 'LIKE',
984 ),
985 )
986 );
987 $notes = get_comments( $args );
988
989 foreach ( $notes as $note )
990 {
991 // Want to ignore note if it's an existing 'contact_merged' note. This would be confusing
992 $comment_content = @unserialize($note->comment_content, ['allowed_classes' => false]);
993 if (
994 $comment_content !== false &&
995 isset($comment_content['note_type']) && $comment_content['note_type'] == 'action' &&
996 isset($comment_content['action']) && $comment_content['action'] == 'contact_merged'
997 )
998 {
999 continue;
1000 }
1001
1002 // This note could've been assigned to this contact, or this contact could just be related to it
1003 // Need to check both
1004 if ( $note->comment_post_ID == $child_contact_id )
1005 {
1006 $data = array(
1007 'comment_ID' => $note->comment_ID,
1008 'comment_post_ID' => $primary_contact_id
1009 );
1010 $updated = wp_update_comment( $data );
1011 }
1012
1013 $related_tos = get_comment_meta( $note->comment_ID, 'related_to', true );
1014 if ( !empty($related_tos) )
1015 {
1016 $new_related_to = array();
1017 foreach ( $related_tos as $related_to )
1018 {
1019 if ( $related_to == $child_contact_id )
1020 {
1021 $new_related_to[] = (string)$primary_contact_id; // convert to string to LIKE lookups work
1022 }
1023 else
1024 {
1025 $new_related_to[] = (string)$related_to; // convert to string to LIKE lookups work
1026 }
1027 }
1028 $new_related_to = array_unique($new_related_to);
1029 update_comment_meta( $note->comment_ID, 'related_to', $new_related_to );
1030 }
1031 }
1032 }
1033
1034 // Write a note to all contacts highlighting what has happened
1035 $current_user = wp_get_current_user();
1036 foreach ( $contacts_to_merge as $child_contact_id )
1037 {
1038 $comment = array(
1039 'note_type' => 'action',
1040 'action' => 'contact_merged',
1041 'merged_into' => (int)$primary_contact_id,
1042 );
1043
1044 $data = array(
1045 'comment_post_ID' => (int)$child_contact_id,
1046 'comment_author' => $current_user->display_name,
1047 'comment_author_email' => 'propertyhive@noreply.com',
1048 'comment_author_url' => '',
1049 'comment_date' => gmdate("Y-m-d H:i:s"),
1050 'comment_content' => serialize($comment),
1051 'comment_approved' => 1,
1052 'comment_type' => 'propertyhive_note',
1053 );
1054 $comment_id = wp_insert_comment( $data );
1055
1056 // Move contact being merged to the bin
1057 wp_trash_post( (int)$child_contact_id );
1058 }
1059
1060 // Update email log
1061 foreach ( $contacts_to_merge as $child_contact_id )
1062 {
1063 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This writes the plugin-owned email log relationship during a verified merge; no cached read result is used.
1064 $wpdb->update(
1065 $wpdb->prefix . 'ph_email_log',
1066 array( 'contact_id' => (int) $primary_contact_id ),
1067 array( 'contact_id' => (int) $child_contact_id ),
1068 array( '%d' ),
1069 array( '%d' )
1070 );
1071 }
1072
1073 do_action( 'propertyhive_contacts_merged', $primary_contact_id, $contacts_to_merge );
1074 }
1075 }
1076
1077 endif;
1078