PluginProbe
Property Hive / 2.4.0
Property Hive v2.4.0
2.4.0 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 All 262 releases
← All changes | includes/admin/class-ph-admin-merge-contacts.php +55 -19 2.2.5 → 2.4.0 View file →
@@ -1,5 +1,8 @@
1 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 +
2 5 /**
3 6 * PropertyHive Admin Merge Duplicate Contacts Class.
4 7 *
5 8 * @author PropertyHive
@@ -14,8 +17,9 @@
14 17
15 18 /**
16 19 * PH_Admin_Merge_Contacts
17 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.
18 22 class PH_Admin_Merge_Contacts {
19 23
20 24 /**
21 25 * Handles the display of contacts to merge, to allow the user to choose a primary contact
@@ -24,8 +28,25 @@
24 28 * @return void
25 29 */
26 30 public function output()
27 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 +
28 49 ?>
29 50 <div class="wrap propertyhive">
30 51
31 52 <h1><?php echo esc_html(__('Merge Contacts', 'propertyhive')); ?></h1>
@@ -34,14 +55,8 @@
34 55
35 56 <p><strong>Note:</strong> This action is irreversible.</p>
36 57
37 58 <?php
38 -
39 - if ( isset( $_GET['merge_ids'] ) && $_GET['merge_ids'] != '' )
40 - {
41 - $ids_to_merge = explode( '|', ph_clean( $_GET['merge_ids']) );
42 - }
43 -
44 59 if ( isset( $ids_to_merge ) && count( $ids_to_merge ) > 1 )
45 60 {
46 61 foreach ( $ids_to_merge as $i => $contact_id )
47 62 {
@@ -120,10 +135,13 @@
120 135 $contact_parts = $this->get_tenancy_records( $contact, $contact_parts );
121 136
122 137 $contact_parts = $this->get_note_records( $contact, $contact_parts );
123 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 );
124 141 $contact_parts = apply_filters( 'propertyhive_merge_contact_parts', $contact_parts );
125 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.
126 144 echo implode( '<br>', $contact_parts );
127 145 ?>
128 146 <label style="position:absolute; right:25px; top:25px;">
129 147 <?php echo esc_html(__( 'Use as Primary Contact', 'propertyhive' )); ?>
@@ -160,11 +178,11 @@
160 178 jQuery('#merge_contacts_button').attr('disabled', 'disabled');
161 179
162 180 var data = {
163 181 action: 'propertyhive_merge_contact_records',
164 - contact_ids : '<?php echo esc_js(ph_clean($_GET['merge_ids'])); ?>',
182 + contact_ids : '<?php echo esc_js( $merge_ids_for_request ); ?>',
165 183 primary_contact_id: selected_primary,
166 - nonce: '<?php echo wp_create_nonce( 'propertyhive_merge_contact' ); ?>',
184 + nonce: '<?php echo esc_js( wp_create_nonce( 'propertyhive_merge_contact' ) ); ?>',
167 185 };
168 186
169 187 jQuery.post( '<?php echo esc_url(admin_url('admin-ajax.php')); ?>', data, function(response) {
170 188
@@ -177,9 +195,9 @@
177 195 }
178 196 if (response.success)
179 197 {
180 198 // Redirect to referrer, adding message in admin_notices
181 - window.location.href = '<?php echo admin_url('edit.php?post_type=contact&propertyhive_contacts_merged=1'); ?>';
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 ); ?>;
182 200 }
183 201 });
184 202 }
185 203 });
@@ -211,8 +229,9 @@
211 229 // get properties where this is the owner
212 230 $args = array(
213 231 'post_type' => 'property',
214 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.
215 234 'meta_query' => array(
216 235 'relation' => 'OR',
217 236 array(
218 237 'key' => '_owner_contact_id',
@@ -261,8 +280,9 @@
261 280 // get appraisals where this is the owner and where not instructed
262 281 $args = array(
263 282 'post_type' => 'appraisal',
264 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.
265 285 'meta_query' => array(
266 286 array(
267 287 'key' => '_property_owner_contact_id',
268 288 'value' => $contact->id,
@@ -434,10 +454,12 @@
434 454 }
435 455
436 456 $args = array(
437 457 'post_type' => 'enquiry',
438 - 'nopaging' => true,
458 + 'posts_per_page' => 1,
459 + 'no_found_rows' => false,
439 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.
440 462 'meta_query' => $meta_query,
441 463 );
442 464 $enquiries_query = new WP_Query( $args );
443 465 $enquiries_count = $enquiries_query->found_posts;
@@ -461,8 +483,9 @@
461 483 $args = array(
462 484 'post_type' => 'viewing',
463 485 'posts_per_page' => 1,
464 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.
465 488 'meta_query' => array(
466 489 array(
467 490 'key' => '_applicant_contact_id',
468 491 'value' => $contact->id,
@@ -491,8 +514,9 @@
491 514 $args = array(
492 515 'post_type' => 'offer',
493 516 'posts_per_page' => 1,
494 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.
495 519 'meta_query' => array(
496 520 array(
497 521 'key' => '_applicant_contact_id',
498 522 'value' => $contact->id,
@@ -521,8 +545,9 @@
521 545 $args = array(
522 546 'post_type' => 'sale',
523 547 'posts_per_page' => 1,
524 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.
525 550 'meta_query' => array(
526 551 array(
527 552 'key' => '_applicant_contact_id',
528 553 'value' => $contact->id,
@@ -551,8 +576,9 @@
551 576 $args = array(
552 577 'post_type' => 'tenancy',
553 578 'posts_per_page' => 1,
554 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.
555 581 'meta_query' => array(
556 582 array(
557 583 'key' => '_applicant_contact_id',
558 584 'value' => $contact->id
@@ -584,8 +610,9 @@
584 610
585 611 $args = array(
586 612 'post_id' => (int)$contact->id,
587 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.
588 615 'meta_query' => array(
589 616 array(
590 617 'key' => 'related_to',
591 618 'value' => '"' . (int)$contact->id . '"',
@@ -718,8 +745,9 @@
718 745 // Move appraisal records to primary
719 746 $args = array(
720 747 'post_type' => 'appraisal',
721 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.
722 750 'meta_query' => array(
723 751 array(
724 752 'key' => '_property_owner_contact_id',
725 753 'value' => $contacts_to_merge,
@@ -748,8 +776,9 @@
748 776 // Move property owner records to primary
749 777 $args = array(
750 778 'post_type' => 'property',
751 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.
752 781 'meta_query' => array(
753 782 'relation' => 'OR',
754 783 array(
755 784 'key' => '_owner_contact_id',
@@ -797,8 +826,9 @@
797 826 $args = array(
798 827 'post_type' => 'enquiry',
799 828 'nopaging' => true,
800 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.
801 831 'meta_query' => array(
802 832 array(
803 833 'key' => '_contact_id',
804 834 'value' => $contacts_to_merge,
@@ -825,8 +855,9 @@
825 855 $args = array(
826 856 'post_type' => 'viewing',
827 857 'nopaging' => true,
828 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.
829 860 'meta_query' => array(
830 861 array(
831 862 'key' => '_applicant_contact_id',
832 863 'value' => $contacts_to_merge,
@@ -853,8 +884,9 @@
853 884 $args = array(
854 885 'post_type' => 'offer',
855 886 'nopaging' => true,
856 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.
857 889 'meta_query' => array(
858 890 array(
859 891 'key' => '_applicant_contact_id',
860 892 'value' => $contacts_to_merge,
@@ -881,8 +913,9 @@
881 913 $args = array(
882 914 'post_type' => 'sale',
883 915 'nopaging' => true,
884 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.
885 918 'meta_query' => array(
886 919 array(
887 920 'key' => '_applicant_contact_id',
888 921 'value' => $contacts_to_merge,
@@ -909,8 +942,9 @@
909 942 $args = array(
910 943 'post_type' => 'tenancy',
911 944 'nopaging' => true,
912 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.
913 947 'meta_query' => array(
914 948 array(
915 949 'key' => '_applicant_contact_id',
916 950 'value' => $contacts_to_merge,
@@ -940,8 +974,9 @@
940 974
941 975 $args = array(
942 976 'post_id' => (int)$child_contact_id,
943 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.
944 979 'meta_query' => array(
945 980 array(
946 981 'key' => 'related_to',
947 982 'value' => '"' . (int)$child_contact_id . '"',
@@ -1010,9 +1045,9 @@
1010 1045 'comment_post_ID' => (int)$child_contact_id,
1011 1046 'comment_author' => $current_user->display_name,
1012 1047 'comment_author_email' => '[email protected]',
1013 1048 'comment_author_url' => '',
1014 - 'comment_date' => date("Y-m-d H:i:s"),
1049 + 'comment_date' => gmdate("Y-m-d H:i:s"),
1015 1050 'comment_content' => serialize($comment),
1016 1051 'comment_approved' => 1,
1017 1052 'comment_type' => 'propertyhive_note',
1018 1053 );
@@ -1024,15 +1059,16 @@
1024 1059
1025 1060 // Update email log
1026 1061 foreach ( $contacts_to_merge as $child_contact_id )
1027 1062 {
1028 - $wpdb->query("
1029 - UPDATE " . $wpdb->prefix . "ph_email_log
1030 - SET
1031 - contact_id = '" . (int)$primary_contact_id . "'
1032 - WHERE
1033 - contact_id = '" . (int)$child_contact_id . "'
1034 - ");
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 + );
1035 1071 }
1036 1072
1037 1073 do_action( 'propertyhive_contacts_merged', $primary_contact_id, $contacts_to_merge );
1038 1074 }
@@ -1037,5 +1073,5 @@
1037 1073 do_action( 'propertyhive_contacts_merged', $primary_contact_id, $contacts_to_merge );
1038 1074 }
1039 1075 }
1040 1076
1041 -endif;
1077 +endif;