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-matching-properties.php +411 -82 1.4.59 → 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 Matching Properties Class.
4 7 *
5 8 * @author PropertyHive
@@ -14,35 +17,47 @@
14 17
15 18 /**
16 19 * PH_Admin_Matching_Properties
17 20 */
21 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_Admin_Matching_Properties; preserving the existing PH_* class name is required for plugin and extension compatibility.
18 22 class PH_Admin_Matching_Properties {
19 23
20 24 public function output()
21 25 {
22 - if ( !isset($_GET['contact_id']) || (isset($_GET['contact_id']) && get_post_type((int)$_GET['contact_id']) != 'contact') )
23 - {
24 - die('Invalid contact_id passed');
25 - }
26 - if ( !isset($_GET['applicant_profile']) )
27 - {
28 - die('Invalid applicant_profile passed');
29 - }
26 + // The initial matching screen is read-only. The POST branch below verifies the
27 + // matching nonce before it performs any state-changing action.
28 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This request is used to render the read-only matching screen; POST mutations verify the matching nonce below.
29 + $request_get = wp_unslash( $_GET );
30 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Only the presence of the action selector is checked here; request values are normalized after the nonce check below.
31 + $has_step = isset( $_POST['step'] );
30 32
31 - $contact_id = (int)$_GET['contact_id'];
33 + $contact_id = ( isset( $request_get['contact_id'] ) && is_scalar( $request_get['contact_id'] ) ) ? absint( $request_get['contact_id'] ) : 0;
34 + $applicant_profile_id = ( isset( $request_get['applicant_profile'] ) && is_scalar( $request_get['applicant_profile'] ) ) ? absint( $request_get['applicant_profile'] ) : 0;
32 35
33 - $email_address = get_post_meta( $contact_id, '_email_address', TRUE );
36 + if ( ! $contact_id || get_post_type( $contact_id ) !== 'contact' )
37 + {
38 + die('Invalid contact_id passed');
39 + }
40 + if ( ! isset( $request_get['applicant_profile'] ) || ! is_scalar( $request_get['applicant_profile'] ) )
41 + {
42 + die('Invalid applicant_profile passed');
43 + }
34 44
35 - $applicant_profile_id = (int)$_GET['applicant_profile'];
45 + $email_address = get_post_meta( $contact_id, '_email_address', TRUE );
36 46
37 47 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $applicant_profile_id, TRUE );
38 48
39 - if ( isset($_POST['step']) )
49 + if ( $has_step )
40 50 {
41 - if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'propertyhive-matching-properties' ) )
42 - die( __( 'Action failed. Please refresh the page and retry.', 'propertyhive' ) );
51 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Only the nonce value is read before verification; all other POST values are normalized after the check below.
52 + $request_request = wp_unslash( $_REQUEST );
53 + if ( empty( $request_request['_wpnonce'] ) || ! wp_verify_nonce( ( isset( $request_request['_wpnonce'] ) && is_string( $request_request['_wpnonce'] ) ) ? sanitize_text_field( $request_request['_wpnonce'] ) : '', 'propertyhive-matching-properties' ) )
54 + die( esc_html(__( 'Action failed. Please refresh the page and retry.', 'propertyhive' )) );
43 55
44 - switch ( $_POST['step'] )
56 + $request_post = wp_unslash( $_POST );
57 + $step = is_string( $request_post['step'] ) ? sanitize_key( $request_post['step'] ) : '';
58 +
59 + switch ( $step )
45 60 {
46 61 case "one":
47 62 {
48 63 // Properties have been selected to email or dismiss
@@ -47,19 +62,30 @@
47 62 {
48 63 // Properties have been selected to email or dismiss
49 64
50 65 // Handle dismissed properties
51 - $this->dismiss_properties();
66 + $this->dismiss_properties();
52 67
53 - $nothing_to_send = true;
68 + $nothing_to_send = true;
54 69
55 - // Handle properties to email
56 - if ( isset($_POST['email_property_id']) && !empty($_POST['email_property_id']) )
70 + // Handle properties to email
71 + if ( isset( $request_post['email_property_id'] ) && ! empty( $request_post['email_property_id'] ) )
57 72 {
58 73 $nothing_to_send = false;
59 74
60 75 $subject = get_option( 'propertyhive_property_match_default_email_subject', '' );
61 76 $body = get_option( 'propertyhive_property_match_default_email_body', '' );
77 +
78 + $from_email_option = get_option( 'propertyhive_property_match_default_from', '' );
79 + if( $from_email_option == 'default_from_email' )
80 + {
81 + $from_email_address = get_option('propertyhive_email_from_address', '');
82 + }
83 + else
84 + {
85 + $current_user = wp_get_current_user();
86 + $from_email_address = $current_user->user_email;
87 + }
62 88 }
63 89
64 90 $nothing_to_send = apply_filters( 'propertyhive_property_match_nothing_to_send', $nothing_to_send );
65 91
@@ -71,9 +97,9 @@
71 97 <div id="poststuff">
72 98
73 99 <form method="post" id="mainform" action="" enctype="multipart/form-data">
74 100 <?php
75 - if ( isset($_POST['email_property_id']) && !empty($_POST['email_property_id']) )
101 + if ( isset( $request_post['email_property_id'] ) && ! empty( $request_post['email_property_id'] ) )
76 102 {
77 103 // We've got emails to send
78 104 include 'views/html-admin-matching-properties-email.php';
79 105 }
@@ -81,15 +107,25 @@
81 107 do_action( 'propertyhive_property_match_step_two', $contact_id, $applicant_profile_id );
82 108 ?>
83 109 <p class="submit">
84 110
85 - <input name="save" class="button-primary" type="submit" value="<?php echo __( 'Send Matches', 'propertyhive' ); ?>" />
86 - <?php if ( isset($_POST['email_property_id']) && !empty($_POST['email_property_id']) ) { ?>
87 - <input name="preview" id="preview_email" class="button" type="button" value="<?php echo __( 'Preview Email', 'propertyhive' ); ?>" />
111 + <input name="save" class="button-primary" type="submit" value="<?php echo esc_attr(__( 'Send Matches', 'propertyhive' )); ?>" />
112 + <?php if ( isset( $request_post['email_property_id'] ) && ! empty( $request_post['email_property_id'] ) ) { ?>
113 + <input name="preview" id="preview_email" class="button" type="button" value="<?php echo esc_attr(__( 'Preview Email', 'propertyhive' )); ?>" />
88 114 <?php } ?>
89 115
90 116 <input type="hidden" name="step" value="two" />
91 - <input type="hidden" name="email_property_id" value="<?php echo ( isset($_POST['email_property_id']) && is_array($_POST['email_property_id']) && !empty($_POST['email_property_id']) ) ? implode(",", ph_clean($_POST['email_property_id'])) : ''; ?>" />
117 + <input type="hidden" name="email_property_id" value="<?php
118 + $selected_property_ids = array();
119 + if ( isset( $request_post['email_property_id'] ) && is_array( $request_post['email_property_id'] ) ) {
120 + foreach ( $request_post['email_property_id'] as $selected_property_id ) {
121 + if ( is_scalar( $selected_property_id ) ) {
122 + $selected_property_ids[] = absint( $selected_property_id );
123 + }
124 + }
125 + }
126 + echo esc_attr( implode( ',', $selected_property_ids ) );
127 + ?>" />
92 128 <?php do_action( 'propertyhive_property_match_step_two_hidden_fields' ); ?>
93 129 <?php wp_nonce_field( 'propertyhive-matching-properties' ); ?>
94 130
95 131 </p>
@@ -94,9 +130,9 @@
94 130
95 131 </p>
96 132
97 133 <p>
98 - <?php echo __( 'When sending out lots of emails we recommend using <a href="https://en-gb.wordpress.org/plugins/tags/smtp" target="_blank">a plugin</a> to send them out using SMTP. Your web developer or hosting company should be able to advise on this.', 'propertyhive' );
134 + <?php echo wp_kses_post( __( 'When sending out lots of emails we recommend using <a href="https://en-gb.wordpress.org/plugins/tags/smtp" target="_blank">a plugin</a> to send them out using SMTP. Your web developer or hosting company should be able to advise on this.', 'propertyhive' ) );
99 135 ?>
100 136 </p>
101 137
102 138 </form>
@@ -119,9 +155,9 @@
119 155
120 156 function showPreview()
121 157 {
122 158 jQuery('#mainform').attr('target', '_blank');
123 - jQuery('#mainform').attr('action', '<?php echo admin_url( '?preview_propertyhive_email=true&contact_id=' . (int)$_GET['contact_id'] . '&applicant_profile=' . (int)$_GET['applicant_profile'] ); ?>');
159 + jQuery('#mainform').attr('action', <?php echo wp_json_encode( admin_url( '?preview_propertyhive_email=true&contact_id=' . $contact_id . '&applicant_profile=' . $applicant_profile_id ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); ?>);
124 160
125 161 jQuery('#mainform').submit();
126 162 jQuery('#mainform').attr('target', '_self');
127 163 jQuery('#mainform').attr('action', '');
@@ -131,10 +167,10 @@
131 167 <?php
132 168 }
133 169
134 170 if ( $nothing_to_send == true )
135 - {
136 - echo '<script>window.location.href = "' . get_edit_post_link( $contact_id, 'url' ) . '&ph_message=2";</script>';
171 + {
172 + echo '<script>window.location.href = ' . wp_json_encode( get_edit_post_link( $contact_id, 'url' ) . '&ph_message=2', JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ) . ';</script>';
137 173
138 174 //header("Location: " . get_edit_post_link( $contact_id, 'url' ) . '&ph_message=2' ); // properties marked as not interested
139 175 //die();
140 176 }
@@ -142,27 +178,73 @@
142 178 break;
143 179 }
144 180 case "two":
145 181 {
146 - if ( isset($_POST['email_property_id']) && !empty($_POST['email_property_id']) )
147 - {
148 - $to_email_addresses = explode(",", $_POST['to_email_address']);
182 + if ( isset( $request_post['email_property_id'] ) && ! empty( $request_post['email_property_id'] ) )
183 + {
184 + $to_email_address_input = ( isset( $request_post['to_email_address'] ) && is_string( $request_post['to_email_address'] ) ) ? $request_post['to_email_address'] : '';
185 + $to_email_addresses = explode( ',', $to_email_address_input );
149 186 $new_to_email_addresses = array();
150 - foreach ( $to_email_addresses as $to_email_address)
187 + foreach ( $to_email_addresses as $to_email_address )
151 188 {
152 189 $new_to_email_addresses[] = sanitize_email($to_email_address);
153 190 }
154 191
192 + $cc_email_address_input = ( isset( $request_post['cc_email_address'] ) && is_string( $request_post['cc_email_address'] ) ) ? $request_post['cc_email_address'] : '';
193 + $cc_email_addresses = explode( ',', $cc_email_address_input );
194 + $new_cc_email_addresses = array();
195 + foreach ( $cc_email_addresses as $cc_email_address )
196 + {
197 + $new_cc_email_addresses[] = sanitize_email($cc_email_address);
198 + }
199 +
200 + $bcc_email_address_input = ( isset( $request_post['bcc_email_address'] ) && is_string( $request_post['bcc_email_address'] ) ) ? $request_post['bcc_email_address'] : '';
201 + $bcc_email_addresses = explode( ',', $bcc_email_address_input );
202 + $new_bcc_email_addresses = array();
203 + foreach ( $bcc_email_addresses as $bcc_email_address )
204 + {
205 + $new_bcc_email_addresses[] = sanitize_email($bcc_email_address);
206 + }
207 +
208 + $allowed_tags = array(
209 + 'strong' => array(),
210 + 'span' => array(),
211 + 'em' => array(),
212 + 'h1' => array(),
213 + 'h2' => array(),
214 + 'h3' => array(),
215 + 'h4' => array(),
216 + 'h5' => array(),
217 + 'h6' => array(),
218 + 'i' => array(),
219 + 'u' => array(),
220 + 'b' => array(),
221 + 'a' => array(
222 + 'href' => array(),
223 + 'target' => array(),
224 + ),
225 + );
226 + $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
227 +
228 + $body_input = ( isset( $request_post['body'] ) && is_string( $request_post['body'] ) ) ? $request_post['body'] : '';
229 + $body = wp_kses( $body_input, $allowed_tags );
230 + $email_property_id_input = ( isset( $request_post['email_property_id'] ) && is_scalar( $request_post['email_property_id'] ) ) ? $request_post['email_property_id'] : '';
231 + $from_name_input = ( isset( $request_post['from_name'] ) && is_string( $request_post['from_name'] ) ) ? $request_post['from_name'] : '';
232 + $from_email_address_input = ( isset( $request_post['from_email_address'] ) && is_string( $request_post['from_email_address'] ) ) ? $request_post['from_email_address'] : '';
233 + $subject_input = ( isset( $request_post['subject'] ) && is_string( $request_post['subject'] ) ) ? $request_post['subject'] : '';
234 +
155 235 // Email info entered. Time to send emails
156 236 $this->send_emails(
157 - (int)$_GET['contact_id'],
158 - (int)$_GET['applicant_profile'],
159 - explode(",", ph_clean($_POST['email_property_id'])),
160 - ph_clean($_POST['from_name']),
161 - sanitize_email($_POST['from_email_address']),
162 - ph_clean($_POST['subject']),
163 - sanitize_textarea_field($_POST['body']),
164 - implode(",", $new_to_email_addresses)
237 + $contact_id,
238 + $applicant_profile_id,
239 + array_values( array_filter( array_map( 'absint', explode( ',', sanitize_text_field( $email_property_id_input ) ) ) ) ),
240 + ph_clean( $from_name_input ),
241 + sanitize_email( $from_email_address_input ),
242 + ph_clean( $subject_input ),
243 + $body,
244 + implode(",", $new_to_email_addresses),
245 + implode(",", $new_cc_email_addresses),
246 + implode(",", $new_bcc_email_addresses)
165 247 );
166 248
167 249 //header("Location: " . get_edit_post_link( $contact_id, 'url' ) . '&ph_message=1' ); // email sent
168 250 //die();
@@ -169,9 +251,9 @@
169 251 }
170 252
171 253 do_action( 'propertyhive_property_match_step_send', $contact_id, $applicant_profile_id );
172 254
173 - echo '<script>window.location.href = "' . get_edit_post_link( $contact_id, 'url' ) . '&ph_message=1";</script>';
255 + echo '<script>window.location.href = ' . wp_json_encode( get_edit_post_link( $contact_id, 'url' ) . '&ph_message=1', JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ) . ';</script>';
174 256 }
175 257 }
176 258 }
177 259 else
@@ -177,12 +259,12 @@
177 259 else
178 260 {
179 261 $applicant_profile_match_history = get_post_meta( $contact_id, '_applicant_profile_' . $applicant_profile_id . '_match_history', TRUE );
180 262
181 - $properties = $this->get_matching_properties( (int)$_GET['contact_id'], (int)$_GET['applicant_profile'] );
263 + $properties = $this->get_matching_properties( $contact_id, $applicant_profile_id );
182 264
183 265 $do_not_email = false;
184 - $forbidden_contact_methods = get_post_meta( (int)$_GET['contact_id'], '_forbidden_contact_methods', TRUE );
266 + $forbidden_contact_methods = get_post_meta( $contact_id, '_forbidden_contact_methods', TRUE );
185 267 if ( is_array($forbidden_contact_methods) && in_array('email', $forbidden_contact_methods) )
186 268 {
187 269 $do_not_email = true;
188 270 }
@@ -190,15 +272,19 @@
190 272 include 'views/html-admin-matching-properties.php';
191 273 }
192 274 }
193 275
194 - private function dismiss_properties()
195 - {
196 - $contact_id = (int)$_GET['contact_id'];
197 - $applicant_profile_id = (int)$_GET['applicant_profile'];
276 + private function dismiss_properties()
277 + {
278 + // output() verifies the matching nonce before calling this private mutator.
279 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This private helper is only called from output() after the matching nonce has been verified.
280 + $request_post = wp_unslash( $_POST );
281 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This private helper receives the read-only contact identifier from the already-authorized matching screen.
282 + $request_get = wp_unslash( $_GET );
283 + $contact_id = ( isset( $request_get['contact_id'] ) && is_scalar( $request_get['contact_id'] ) ) ? absint( $request_get['contact_id'] ) : 0;
198 284
199 - // Get currently dismissed properties for this contact to decide if we need to add or remove it
200 - $dismissed_properties = get_post_meta( $contact_id, '_dismissed_properties', TRUE );
285 + // Get currently dismissed properties for this contact to decide if we need to add or remove it
286 + $dismissed_properties = get_post_meta( $contact_id, '_dismissed_properties', TRUE );
201 287
202 288 if ( !is_array($dismissed_properties) )
203 289 {
204 290 $dismissed_properties = array();
@@ -203,12 +289,15 @@
203 289 {
204 290 $dismissed_properties = array();
205 291 }
206 292
207 - if ( isset($_POST['not_interested_property_id']) && !empty($_POST['not_interested_property_id']) )
208 - {
209 - foreach ( $_POST['not_interested_property_id'] as $property_id )
293 + if ( isset( $request_post['not_interested_property_id'] ) && is_array( $request_post['not_interested_property_id'] ) && ! empty( $request_post['not_interested_property_id'] ) )
210 294 {
295 + foreach ( $request_post['not_interested_property_id'] as $property_id )
296 + {
297 + if ( ! is_scalar( $property_id ) ) {
298 + continue;
299 + }
211 300 if ( in_array((int)$property_id, $dismissed_properties) )
212 301 {
213 302 // Already dismissed. Need to remove from array
214 303 if( ($key = array_search((int)$property_id, $dismissed_properties)) !== false )
@@ -252,28 +341,58 @@
252 341
253 342 $args = array(
254 343 'post_type' => 'property',
255 344 'nopaging' => true,
345 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Exclude this contact's explicitly dismissed properties before applying existing matching and extension query conditions.
256 346 'post__not_in' => $dismissed_properties
257 347 );
258 348
349 + // Meta query
350 + $meta_query = array('relation' => 'AND');
351 +
259 352 if ( $date_added_from != '' )
260 353 {
261 - $args['date_query'] = array(
262 - array(
263 - 'after' => $date_added_from,
264 - 'inclusive' => true,
265 - )
354 + $datetime = new DateTimeImmutable(
355 + $date_added_from,
356 + new DateTimeZone( 'UTC' )
266 357 );
358 +
359 + if ( apply_filters( 'propertyhive_matching_properties_use_on_market_change_date', false ) === true )
360 + {
361 + // _on_market_change_date is currently stored using date(),
362 + // which will normally be UTC in WordPress.
363 + $meta_query[] = array(
364 + 'key' => '_on_market_change_date',
365 + 'value' => $datetime->format( 'Y-m-d H:i:s' ),
366 + 'compare' => '>=',
367 + 'type' => 'DATETIME',
368 + );
369 + }
370 + else
371 + {
372 + // post_date is stored in the site's local timezone.
373 + $local_datetime = $datetime->setTimezone( wp_timezone() );
374 +
375 + $args['date_query'] = array(
376 + array(
377 + 'after' => $local_datetime->format( 'Y-m-d H:i:s' ),
378 + 'inclusive' => true,
379 + ),
380 + );
381 + }
267 382 }
268 383
269 - // Meta query
270 - $meta_query = array('relation' => 'AND');
271 384 $meta_query[] = array(
272 385 'key' => '_on_market',
273 386 'value' => 'yes'
274 387 );
275 - if ( isset($applicant_profile['department']) && $applicant_profile['department'] == 'residential-sales' )
388 + if (
389 + isset($applicant_profile['department']) &&
390 + (
391 + $applicant_profile['department'] == 'residential-sales' ||
392 + ph_get_custom_department_based_on($applicant_profile['department']) == 'residential-sales'
393 + )
394 + )
276 395 {
277 396 $meta_query[] = array(
278 397 'key' => '_department',
279 398 'value' => $applicant_profile['department']
@@ -325,9 +444,9 @@
325 444 'type' => 'NUMERIC'
326 445 );
327 446 }
328 447 }
329 - else
448 + elseif ( isset($applicant_profile['max_price_actual']) && !empty($applicant_profile['max_price_actual']) )
330 449 {
331 450 $meta_query[] = array(
332 451 'key' => '_price_actual',
333 452 'value' => $applicant_profile['max_price_actual'],
@@ -335,9 +454,15 @@
335 454 'type' => 'NUMERIC'
336 455 );
337 456 }
338 457 }
339 - elseif ( isset($applicant_profile['department']) && $applicant_profile['department'] == 'residential-lettings' )
458 + elseif (
459 + isset($applicant_profile['department']) &&
460 + (
461 + $applicant_profile['department'] == 'residential-lettings' ||
462 + ph_get_custom_department_based_on($applicant_profile['department']) == 'residential-lettings'
463 + )
464 + )
340 465 {
341 466 $meta_query[] = array(
342 467 'key' => '_department',
343 468 'value' => $applicant_profile['department']
@@ -352,9 +477,17 @@
352 477 );
353 478 }
354 479 }
355 480
356 - if ( isset($applicant_profile['department']) && ( $applicant_profile['department'] == 'residential-sales' || $applicant_profile['department'] == 'residential-lettings' ) )
481 + if (
482 + isset($applicant_profile['department']) &&
483 + (
484 + $applicant_profile['department'] == 'residential-sales' ||
485 + $applicant_profile['department'] == 'residential-lettings' ||
486 + ph_get_custom_department_based_on($applicant_profile['department']) == 'residential-sales' ||
487 + ph_get_custom_department_based_on($applicant_profile['department']) == 'residential-lettings'
488 + )
489 + )
357 490 {
358 491 if ( isset($applicant_profile['min_beds']) && $applicant_profile['min_beds'] != '' && $applicant_profile['min_beds'] != 0 )
359 492 {
360 493 $meta_query[] = array(
@@ -364,9 +497,15 @@
364 497 'type' => 'NUMERIC'
365 498 );
366 499 }
367 500 }
368 - if ( isset($applicant_profile['department']) && $applicant_profile['department'] == 'commercial' )
501 + if (
502 + isset($applicant_profile['department']) &&
503 + (
504 + $applicant_profile['department'] == 'commercial' ||
505 + ph_get_custom_department_based_on($applicant_profile['department']) == 'commercial'
506 + )
507 + )
369 508 {
370 509 if ( isset($applicant_profile['available_as']) && is_array($applicant_profile['available_as']) && !empty($applicant_profile['available_as']) )
371 510 {
372 511 if ( in_array('sale', $applicant_profile['available_as']) && !in_array('rent', $applicant_profile['available_as']) )
@@ -415,13 +554,136 @@
415 554 'compare' => '>=',
416 555 'type' => 'NUMERIC'
417 556 );
418 557 }
558 +
559 + if ( get_option('propertyhive_applicant_locations_type') == 'text' )
560 + {
561 + if ( isset($applicant_profile['location_text']) && $applicant_profile['location_text'] != '' )
562 + {
563 + $address_keywords = array( $applicant_profile['location_text'] );
564 + if ( strpos( $applicant_profile['location_text'], ' ' ) !== FALSE )
565 + {
566 + $address_keywords[] = str_replace(" ", "-", ph_clean($applicant_profile['location_text']));
567 + }
568 + if ( strpos( $applicant_profile['location_text'], '-' ) !== FALSE )
569 + {
570 + $address_keywords[] = str_replace("-", " ", ph_clean($applicant_profile['location_text']));
571 + }
572 +
573 + if ( strpos( $applicant_profile['location_text'], '.' ) !== FALSE )
574 + {
575 + $address_keywords[] = str_replace(".", "", ph_clean($applicant_profile['location_text']));
576 + }
577 + if ( stripos( $applicant_profile['location_text'], 'st ' ) !== FALSE )
578 + {
579 + $address_keywords[] = str_ireplace("st ", "st. ", ph_clean($applicant_profile['location_text']));
580 + }
581 +
582 + $location_query = array('relation' => 'OR');
583 +
584 + $address_fields_to_query = array(
585 + '_address_street',
586 + '_address_two',
587 + '_address_three',
588 + '_address_four',
589 + '_address_postcode'
590 + );
591 +
592 + $address_fields_to_query = apply_filters( 'propertyhive_address_fields_to_query', $address_fields_to_query );
593 +
594 + $address_keyword_compare = get_option( 'propertyhive_address_keyword_compare', '=' );
595 + if ( $address_keyword_compare == 'polygon' )
596 + {
597 + $address_keyword_compare = apply_filters('propertyhive_property_match_address_keyword_compare', '=');
598 + }
599 +
600 + foreach ( $address_keywords as $address_keyword )
601 + {
602 + foreach ( $address_fields_to_query as $address_field )
603 + {
604 + if ( $address_field == '_address_postcode' ) { continue; } // ignore postcode as that is handled differently afterwards
605 +
606 + $location_query[] = array(
607 + 'key' => $address_field,
608 + 'value' => $address_keyword,
609 + 'compare' => $address_keyword_compare
610 + );
611 + }
612 + }
613 + if ( in_array('_address_postcode', $address_fields_to_query) )
614 + {
615 + if ( strlen($applicant_profile['location_text']) <= 4 )
616 + {
617 + $location_query[] = array(
618 + 'key' => '_address_postcode',
619 + 'value' => ph_clean( $applicant_profile['location_text'] ),
620 + 'compare' => '='
621 + );
622 + // Run regex match where given keyword is at the start of the postcode ^
623 + // followed by one or zero letters (for WC2E-style postcodes) [a-zA-Z]?
624 + // then a single space [ ]
625 + $location_query[] = array(
626 + 'key' => '_address_postcode',
627 + 'value' => '^' . ph_clean( $applicant_profile['location_text'] ) . '[a-zA-Z]?[ ]',
628 + 'compare' => 'RLIKE'
629 + );
630 + }
631 + else
632 + {
633 + $postcode = ph_clean( $applicant_profile['location_text'] );
634 +
635 + if ( preg_match('#^(GIR ?0AA|[A-PR-UWYZ]([0-9]{1,2}|([A-HK-Y][0-9]([0-9ABEHMNPRV-Y])?)|[0-9][A-HJKPS-UW])[0-9][ABD-HJLNP-UW-Z]{2})$#i', $postcode) )
636 + {
637 + // UK postcode found with no space
638 +
639 + if ( strlen($postcode) == 5 )
640 + {
641 + $first_part = substr($postcode, 0, 2);
642 + $last_part = substr($postcode, 2, 3);
643 +
644 + $postcode = $first_part . ' ' . $last_part;
645 + }
646 + elseif ( strlen($postcode) == 6 )
647 + {
648 + $first_part = substr($postcode, 0, 3);
649 + $last_part = substr($postcode, 3, 3);
650 +
651 + $postcode = $first_part . ' ' . $last_part;
652 + }
653 + elseif ( strlen($postcode) == 7 )
654 + {
655 + $first_part = substr($postcode, 0, 4);
656 + $last_part = substr($postcode, 4, 3);
657 +
658 + $postcode = $first_part . ' ' . $last_part;
659 + }
660 + }
661 +
662 + $location_query[] = array(
663 + 'key' => '_address_postcode',
664 + 'value' => ph_clean( $postcode ),
665 + 'compare' => 'LIKE'
666 + );
667 + }
668 + }
669 + $meta_query[] = $location_query;
670 + }
671 + }
672 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required property matching criteria (department, price, market state and area) use the plugin's existing metadata schema.
419 673 $args['meta_query'] = $meta_query;
420 674
421 675 // Term query
422 676 $tax_query = array('relation' => 'AND');
423 - if ( isset($applicant_profile['department']) && ( $applicant_profile['department'] == 'residential-sales' || $applicant_profile['department'] == 'residential-lettings' ) )
677 + if (
678 + isset($applicant_profile['department']) &&
679 + (
680 + $applicant_profile['department'] == 'residential-sales' ||
681 + $applicant_profile['department'] == 'residential-lettings' ||
682 + ph_get_custom_department_based_on($applicant_profile['department']) == 'residential-sales' ||
683 + ph_get_custom_department_based_on($applicant_profile['department']) == 'residential-lettings'
684 + )
685 + )
424 686 {
425 687 if ( isset($applicant_profile['property_types']) && is_array($applicant_profile['property_types']) && !empty($applicant_profile['property_types']) )
426 688 {
427 689 $tax_query[] = array(
@@ -431,9 +693,15 @@
431 693 'operator' => 'IN',
432 694 );
433 695 }
434 696 }
435 - if ( isset($applicant_profile['department']) && $applicant_profile['department'] == 'commercial' )
697 + if (
698 + isset($applicant_profile['department']) &&
699 + (
700 + $applicant_profile['department'] == 'commercial' ||
701 + ph_get_custom_department_based_on($applicant_profile['department']) == 'commercial'
702 + )
703 + )
436 704 {
437 705 if ( isset($applicant_profile['commercial_property_types']) && is_array($applicant_profile['commercial_property_types']) && !empty($applicant_profile['commercial_property_types']) )
438 706 {
439 707 $tax_query[] = array(
@@ -443,16 +711,19 @@
443 711 'operator' => 'IN',
444 712 );
445 713 }
446 714 }
447 - if ( isset($applicant_profile['locations']) && is_array($applicant_profile['locations']) && !empty($applicant_profile['locations']) )
715 + if ( get_option('propertyhive_applicant_locations_type') != 'text' )
448 716 {
449 - $tax_query[] = array(
450 - 'taxonomy' => 'location',
451 - 'field' => 'term_id',
452 - 'terms' => $applicant_profile['locations'],
453 - 'operator' => 'IN',
454 - );
717 + if ( isset($applicant_profile['locations']) && is_array($applicant_profile['locations']) && !empty($applicant_profile['locations']) )
718 + {
719 + $tax_query[] = array(
720 + 'taxonomy' => 'location',
721 + 'field' => 'term_id',
722 + 'terms' => $applicant_profile['locations'],
723 + 'operator' => 'IN',
724 + );
725 + }
455 726 }
456 727 $property_match_statuses = get_option( 'propertyhive_property_match_statuses', '' );
457 728 if ( $property_match_statuses != '' && is_array($property_match_statuses) && !empty($property_match_statuses) )
458 729 {
@@ -462,8 +733,9 @@
462 733 'terms' => $property_match_statuses,
463 734 'operator' => 'IN',
464 735 );
465 736 }
737 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Applicant property-type/location and availability constraints require the existing taxonomies; preserve extension query semantics.
466 738 $args['tax_query'] = $tax_query;
467 739
468 740 $args = apply_filters( 'propertyhive_matching_properties_args', $args, $contact_id, $applicant_profile );
469 741
@@ -485,24 +757,30 @@
485 757
486 758 return $properties;
487 759 }
488 760
489 - public function send_emails( $contact_id, $applicant_profile, $email_property_ids, $from_name, $from_email_address, $subject, $body, $to_email_address = '' )
761 + public function send_emails( $contact_id, $applicant_profile, $email_property_ids, $from_name, $from_email_address, $subject, $body, $to_email_address = '', $cc_email_address = '', $bcc_email_address = '' )
490 762 {
491 763 global $wpdb;
492 764
493 765 $current_user = wp_get_current_user();
494 766
767 + $applicant_profile_details = get_post_meta( $contact_id, '_applicant_profile_' . $applicant_profile, TRUE );
768 +
769 + $contact = new PH_Contact($contact_id);
495 770 if ( $to_email_address == '' )
496 771 {
497 - $to_email_address = get_post_meta( $contact_id, '_email_address', TRUE );
772 + $to_email_address = $contact->email_address;
498 773 }
499 774
500 775 $subject = str_replace("[property_count]", count($email_property_ids) . ' propert' . ( ( count($email_property_ids) != 1 ) ? 'ies' : 'y' ), $subject);
501 776
502 - $body = str_replace("[contact_name]", get_the_title($contact_id), $body);
777 + $body = str_replace( '[contact_name]', esc_html( $contact->post_title ), $body );
778 + $body = str_replace( '[contact_dear]', esc_html( $contact->dear() ), $body );
503 779 $body = str_replace("[property_count]", count($email_property_ids) . ' propert' . ( ( count($email_property_ids) != 1 ) ? 'ies' : 'y' ), $body);
504 780
781 + $office_counts = array();
782 +
505 783 if ( strpos($body, '[properties]') !== FALSE )
506 784 {
507 785 ob_start();
508 786 if ( !empty($email_property_ids) )
@@ -510,8 +788,15 @@
510 788 foreach ( $email_property_ids as $email_property_id )
511 789 {
512 790
513 791 $property = new PH_Property((int)$email_property_id);
792 +
793 + if ( $property->office_id != '' && $property->office_id != 0 )
794 + {
795 + if ( !isset($office_counts[$property->office_id]) ) { $office_counts[$property->office_id] = 0; }
796 + ++$office_counts[$property->office_id];
797 + }
798 +
514 799 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
515 800 }
516 801 }
517 802 $body = str_replace("[properties]", ob_get_clean(), $body);
@@ -516,9 +801,49 @@
516 801 }
517 802 $body = str_replace("[properties]", ob_get_clean(), $body);
518 803 }
519 804
805 + // Get email address of office with most properties
806 + $office_name = '';
807 + $office_email_address = '';
808 +
809 + $office_id = get_user_meta($current_user->ID, 'office_id', TRUE);
810 + if ($office_id == '')
811 + {
812 + // No office against user. Use email address of office with most properties
813 + if ( !empty($office_counts) )
814 + {
815 + arsort($office_counts);
816 + reset($office_counts);
817 + $office_id = key($office_counts);
818 + }
819 + }
820 +
821 + if ( !empty($office_id) )
822 + {
823 + $office_name = get_the_title($office_id);
824 + $office_email_address = get_post_meta( $office_id, '_office_email_address_' . str_replace("residential-", "", $applicant_profile_details['department']), TRUE );
825 + }
826 +
827 + $body = str_replace( '[office_name]', esc_html( $office_name ), $body );
828 + $body = str_replace( '[office_email_address]', esc_html( $office_email_address ), $body );
829 +
830 + $body = str_replace( '[negotiator_name]', esc_html( $current_user->display_name ), $body );
831 + $body = str_replace( '[negotiator_email_address]', esc_html( $current_user->user_email ), $body );
832 +
833 + $body = stripslashes($body);
834 +
835 + if (extension_loaded('zlib'))
836 + {
837 + $compressed_body = @gzcompress($body);
838 + if ( $compressed_body !== false )
839 + {
840 + $body = $compressed_body;
841 + }
842 + }
843 +
520 844 // Insert into email log
845 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Typed insert into the plugin-owned email queue table; no WordPress object API represents these queued messages.
521 846 $insert = $wpdb->insert(
522 847 $wpdb->prefix . 'ph_email_log',
523 848 array(
524 849 'contact_id' => $contact_id,
@@ -524,14 +849,16 @@
524 849 'contact_id' => $contact_id,
525 850 'property_ids' => serialize($email_property_ids),
526 851 'applicant_profile_id' => $applicant_profile,
527 852 'to_email_address' => $to_email_address,
853 + 'cc_email_address' => $cc_email_address,
854 + 'bcc_email_address' => $bcc_email_address,
528 855 'from_name' => $from_name,
529 856 'from_email_address' => $from_email_address,
530 857 'subject' => stripslashes($subject),
531 - 'body' => stripslashes($body),
858 + 'body' => $body,
532 859 'status' => '',
533 - 'send_at' => date("Y-m-d H:i:s"),
860 + 'send_at' => gmdate("Y-m-d H:i:s"),
534 861 'sent_by' => $current_user->ID,
535 862 ),
536 863 array(
537 864 '%d',
@@ -543,8 +870,10 @@
543 870 '%s',
544 871 '%s',
545 872 '%s',
546 873 '%s',
874 + '%s',
875 + '%s',
547 876 '%d',
548 877 )
549 878 );
550 879
@@ -568,9 +897,9 @@
568 897 $applicant_profile_match_history[$email_property_id] = array();
569 898 }
570 899
571 900 $applicant_profile_match_history[$email_property_id][] = array(
572 - 'date' => date("Y-m-d H:i:s"),
901 + 'date' => gmdate("Y-m-d H:i:s"),
573 902 'method' => 'email',
574 903 'email_log_id' => $email_log_id,
575 904 );
576 905
@@ -585,9 +914,9 @@
585 914 'comment_post_ID' => $email_property_id,
586 915 'comment_author' => $current_user->display_name,
587 916 'comment_author_email' => '[email protected]',
588 917 'comment_author_url' => '',
589 - 'comment_date' => date("Y-m-d H:i:s"),
918 + 'comment_date' => gmdate("Y-m-d H:i:s"),
590 919 'comment_content' => serialize($comment),
591 920 'comment_approved' => 1,
592 921 'comment_type' => 'propertyhive_note',
593 922 );
@@ -608,9 +937,9 @@
608 937 'comment_post_ID' => $contact_id,
609 938 'comment_author' => $current_user->display_name,
610 939 'comment_author_email' => '[email protected]',
611 940 'comment_author_url' => '',
612 - 'comment_date' => date("Y-m-d H:i:s"),
941 + 'comment_date' => gmdate("Y-m-d H:i:s"),
613 942 'comment_content' => serialize($comment),
614 943 'comment_approved' => 1,
615 944 'comment_type' => 'propertyhive_note',
616 945 );
@@ -620,5 +949,5 @@
620 949 }
621 950
622 951 }
623 952
624 -endif;
953 +endif;