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
← All changes | includes/admin/class-ph-admin-matching-properties.php +413 -82 1.4.492.3.1 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,10 +733,13 @@
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
740 + $args = apply_filters( 'propertyhive_matching_properties_args', $args, $contact_id, $applicant_profile );
741 +
468 742 $properties_query = new WP_Query( $args );
469 743
470 744 if ( $properties_query->have_posts() )
471 745 {
@@ -483,24 +757,30 @@
483 757
484 758 return $properties;
485 759 }
486 760
487 - 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 = '' )
488 762 {
489 763 global $wpdb;
490 764
491 765 $current_user = wp_get_current_user();
492 766
767 + $applicant_profile_details = get_post_meta( $contact_id, '_applicant_profile_' . $applicant_profile, TRUE );
768 +
769 + $contact = new PH_Contact($contact_id);
493 770 if ( $to_email_address == '' )
494 771 {
495 - $to_email_address = get_post_meta( $contact_id, '_email_address', TRUE );
772 + $to_email_address = $contact->email_address;
496 773 }
497 774
498 775 $subject = str_replace("[property_count]", count($email_property_ids) . ' propert' . ( ( count($email_property_ids) != 1 ) ? 'ies' : 'y' ), $subject);
499 776
500 - $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 );
501 779 $body = str_replace("[property_count]", count($email_property_ids) . ' propert' . ( ( count($email_property_ids) != 1 ) ? 'ies' : 'y' ), $body);
502 780
781 + $office_counts = array();
782 +
503 783 if ( strpos($body, '[properties]') !== FALSE )
504 784 {
505 785 ob_start();
506 786 if ( !empty($email_property_ids) )
@@ -508,8 +788,15 @@
508 788 foreach ( $email_property_ids as $email_property_id )
509 789 {
510 790
511 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 +
512 799 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
513 800 }
514 801 }
515 802 $body = str_replace("[properties]", ob_get_clean(), $body);
@@ -514,9 +801,49 @@
514 801 }
515 802 $body = str_replace("[properties]", ob_get_clean(), $body);
516 803 }
517 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 +
518 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.
519 846 $insert = $wpdb->insert(
520 847 $wpdb->prefix . 'ph_email_log',
521 848 array(
522 849 'contact_id' => $contact_id,
@@ -522,14 +849,16 @@
522 849 'contact_id' => $contact_id,
523 850 'property_ids' => serialize($email_property_ids),
524 851 'applicant_profile_id' => $applicant_profile,
525 852 'to_email_address' => $to_email_address,
853 + 'cc_email_address' => $cc_email_address,
854 + 'bcc_email_address' => $bcc_email_address,
526 855 'from_name' => $from_name,
527 856 'from_email_address' => $from_email_address,
528 857 'subject' => stripslashes($subject),
529 - 'body' => stripslashes($body),
858 + 'body' => $body,
530 859 'status' => '',
531 - 'send_at' => date("Y-m-d H:i:s"),
860 + 'send_at' => gmdate("Y-m-d H:i:s"),
532 861 'sent_by' => $current_user->ID,
533 862 ),
534 863 array(
535 864 '%d',
@@ -541,8 +870,10 @@
541 870 '%s',
542 871 '%s',
543 872 '%s',
544 873 '%s',
874 + '%s',
875 + '%s',
545 876 '%d',
546 877 )
547 878 );
548 879
@@ -566,9 +897,9 @@
566 897 $applicant_profile_match_history[$email_property_id] = array();
567 898 }
568 899
569 900 $applicant_profile_match_history[$email_property_id][] = array(
570 - 'date' => date("Y-m-d H:i:s"),
901 + 'date' => gmdate("Y-m-d H:i:s"),
571 902 'method' => 'email',
572 903 'email_log_id' => $email_log_id,
573 904 );
574 905
@@ -583,9 +914,9 @@
583 914 'comment_post_ID' => $email_property_id,
584 915 'comment_author' => $current_user->display_name,
585 916 'comment_author_email' => 'propertyhive@noreply.com',
586 917 'comment_author_url' => '',
587 - 'comment_date' => date("Y-m-d H:i:s"),
918 + 'comment_date' => gmdate("Y-m-d H:i:s"),
588 919 'comment_content' => serialize($comment),
589 920 'comment_approved' => 1,
590 921 'comment_type' => 'propertyhive_note',
591 922 );
@@ -606,9 +937,9 @@
606 937 'comment_post_ID' => $contact_id,
607 938 'comment_author' => $current_user->display_name,
608 939 'comment_author_email' => 'propertyhive@noreply.com',
609 940 'comment_author_url' => '',
610 - 'comment_date' => date("Y-m-d H:i:s"),
941 + 'comment_date' => gmdate("Y-m-d H:i:s"),
611 942 'comment_content' => serialize($comment),
612 943 'comment_approved' => 1,
613 944 'comment_type' => 'propertyhive_note',
614 945 );
@@ -618,5 +949,5 @@
618 949 }
619 950
620 951 }
621 952
622 -endif;
953 +endif;