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 +575 -77 1.4.6 → 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($_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 = $_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 = $_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,23 +62,116 @@
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 - // Handle properties to email
54 - if ( isset($_POST['email_property_id']) && !empty($_POST['email_property_id']) )
68 + $nothing_to_send = true;
69 +
70 + // Handle properties to email
71 + if ( isset( $request_post['email_property_id'] ) && ! empty( $request_post['email_property_id'] ) )
55 72 {
73 + $nothing_to_send = false;
74 +
56 75 $subject = get_option( 'propertyhive_property_match_default_email_subject', '' );
57 76 $body = get_option( 'propertyhive_property_match_default_email_body', '' );
58 77
59 - // We've got emails to send
60 - include 'views/html-admin-matching-properties-email.php';
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 + }
61 88 }
62 - else
63 - {
64 - echo '<script>window.location.href = "' . get_edit_post_link( $contact_id, 'url' ) . '&ph_message=2";</script>';
65 89
90 + $nothing_to_send = apply_filters( 'propertyhive_property_match_nothing_to_send', $nothing_to_send );
91 +
92 + if ( $nothing_to_send != true )
93 + {
94 +?>
95 +<div class="wrap propertyhive">
96 +
97 + <div id="poststuff">
98 +
99 + <form method="post" id="mainform" action="" enctype="multipart/form-data">
100 +<?php
101 + if ( isset( $request_post['email_property_id'] ) && ! empty( $request_post['email_property_id'] ) )
102 + {
103 + // We've got emails to send
104 + include 'views/html-admin-matching-properties-email.php';
105 + }
106 +
107 + do_action( 'propertyhive_property_match_step_two', $contact_id, $applicant_profile_id );
108 +?>
109 + <p class="submit">
110 +
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' )); ?>" />
114 + <?php } ?>
115 +
116 + <input type="hidden" name="step" value="two" />
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 + ?>" />
128 + <?php do_action( 'propertyhive_property_match_step_two_hidden_fields' ); ?>
129 + <?php wp_nonce_field( 'propertyhive-matching-properties' ); ?>
130 +
131 + </p>
132 +
133 + <p>
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' ) );
135 + ?>
136 + </p>
137 +
138 + </form>
139 +
140 + </div>
141 +
142 +</div>
143 +
144 +<script>
145 +
146 + jQuery(document).ready(function()
147 + {
148 + jQuery('#preview_email').click(function(e)
149 + {
150 + e.preventDefault();
151 +
152 + showPreview();
153 + });
154 + });
155 +
156 + function showPreview()
157 + {
158 + jQuery('#mainform').attr('target', '_blank');
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 ); ?>);
160 +
161 + jQuery('#mainform').submit();
162 + jQuery('#mainform').attr('target', '_self');
163 + jQuery('#mainform').attr('action', '');
164 + }
165 +
166 +</script>
167 +<?php
168 + }
169 +
170 + if ( $nothing_to_send == true )
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>';
173 +
66 174 //header("Location: " . get_edit_post_link( $contact_id, 'url' ) . '&ph_message=2' ); // properties marked as not interested
67 175 //die();
68 176 }
69 177
@@ -70,27 +178,82 @@
70 178 break;
71 179 }
72 180 case "two":
73 181 {
74 - if ( isset($_POST['email_property_id']) && !empty($_POST['email_property_id']) )
75 - {
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 );
186 + $new_to_email_addresses = array();
187 + foreach ( $to_email_addresses as $to_email_address )
188 + {
189 + $new_to_email_addresses[] = sanitize_email($to_email_address);
190 + }
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 +
76 235 // Email info entered. Time to send emails
77 236 $this->send_emails(
78 - $_GET['contact_id'],
79 - $_GET['applicant_profile'],
80 - explode(",", $_POST['email_property_id']),
81 - $_POST['from_name'],
82 - $_POST['from_email_address'],
83 - $_POST['subject'],
84 - $_POST['body'],
85 - $_POST['to_email_address']
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)
86 247 );
87 248
88 - echo '<script>window.location.href = "' . get_edit_post_link( $contact_id, 'url' ) . '&ph_message=1";</script>';
89 -
90 249 //header("Location: " . get_edit_post_link( $contact_id, 'url' ) . '&ph_message=1' ); // email sent
91 250 //die();
92 251 }
252 +
253 + do_action( 'propertyhive_property_match_step_send', $contact_id, $applicant_profile_id );
254 +
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>';
93 256 }
94 257 }
95 258 }
96 259 else
@@ -96,12 +259,12 @@
96 259 else
97 260 {
98 261 $applicant_profile_match_history = get_post_meta( $contact_id, '_applicant_profile_' . $applicant_profile_id . '_match_history', TRUE );
99 262
100 - $properties = $this->get_matching_properties( $_GET['contact_id'], $_GET['applicant_profile'] );
263 + $properties = $this->get_matching_properties( $contact_id, $applicant_profile_id );
101 264
102 265 $do_not_email = false;
103 - $forbidden_contact_methods = get_post_meta( $_GET['contact_id'], '_forbidden_contact_methods', TRUE );
266 + $forbidden_contact_methods = get_post_meta( $contact_id, '_forbidden_contact_methods', TRUE );
104 267 if ( is_array($forbidden_contact_methods) && in_array('email', $forbidden_contact_methods) )
105 268 {
106 269 $do_not_email = true;
107 270 }
@@ -109,15 +272,19 @@
109 272 include 'views/html-admin-matching-properties.php';
110 273 }
111 274 }
112 275
113 - private function dismiss_properties()
114 - {
115 - $contact_id = $_GET['contact_id'];
116 - $applicant_profile_id = $_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;
117 284
118 - // Get currently dismissed properties for this contact to decide if we need to add or remove it
119 - $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 );
120 287
121 288 if ( !is_array($dismissed_properties) )
122 289 {
123 290 $dismissed_properties = array();
@@ -122,16 +289,19 @@
122 289 {
123 290 $dismissed_properties = array();
124 291 }
125 292
126 - if ( isset($_POST['not_interested_property_id']) && !empty($_POST['not_interested_property_id']) )
127 - {
128 - 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'] ) )
129 294 {
130 - if ( in_array($property_id, $dismissed_properties) )
295 + foreach ( $request_post['not_interested_property_id'] as $property_id )
296 + {
297 + if ( ! is_scalar( $property_id ) ) {
298 + continue;
299 + }
300 + if ( in_array((int)$property_id, $dismissed_properties) )
131 301 {
132 302 // Already dismissed. Need to remove from array
133 - if( ($key = array_search($property_id, $dismissed_properties)) !== false )
303 + if( ($key = array_search((int)$property_id, $dismissed_properties)) !== false )
134 304 {
135 305 unset($dismissed_properties[$key]);
136 306 }
137 307 }
@@ -137,9 +307,9 @@
137 307 }
138 308 else
139 309 {
140 310 // Not dismissed. Add to array
141 - $dismissed_properties[] = $property_id;
311 + $dismissed_properties[] = (int)$property_id;
142 312 }
143 313 }
144 314
145 315 $dismissed_properties = array_unique($dismissed_properties);
@@ -157,8 +327,11 @@
157 327 $contact = get_post($contact_id);
158 328
159 329 if ( !is_null( $contact ) )
160 330 {
331 + $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
332 + $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
333 +
161 334 $dismissed_properties = get_post_meta( $contact_id, '_dismissed_properties', TRUE );
162 335 if ( !is_array($dismissed_properties) )
163 336 {
164 337 $dismissed_properties = array();
@@ -168,35 +341,113 @@
168 341
169 342 $args = array(
170 343 'post_type' => 'property',
171 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.
172 346 'post__not_in' => $dismissed_properties
173 347 );
174 348
349 + // Meta query
350 + $meta_query = array('relation' => 'AND');
351 +
175 352 if ( $date_added_from != '' )
176 353 {
177 - $args['date_query'] = array(
178 - array(
179 - 'after' => $date_added_from,
180 - 'inclusive' => true,
181 - )
354 + $datetime = new DateTimeImmutable(
355 + $date_added_from,
356 + new DateTimeZone( 'UTC' )
182 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 + }
183 382 }
184 383
185 - // Meta query
186 - $meta_query = array('relation' => 'AND');
187 384 $meta_query[] = array(
188 385 'key' => '_on_market',
189 386 'value' => 'yes'
190 387 );
191 - 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 + )
192 395 {
193 396 $meta_query[] = array(
194 397 'key' => '_department',
195 398 'value' => $applicant_profile['department']
196 399 );
197 - if ( isset($applicant_profile['max_price_actual']) && $applicant_profile['max_price_actual'] != '' && $applicant_profile['max_price_actual'] != 0 )
400 +
401 + if (
402 + get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' ) != '' &&
403 + get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' ) != ''
404 + )
198 405 {
406 + $match_price_range_lower = '';
407 + if ( !isset($applicant_profile['match_price_range_lower_actual']) || ( isset($applicant_profile['match_price_range_lower_actual']) && $applicant_profile['match_price_range_lower_actual'] == '' ) )
408 + {
409 + if ( isset($applicant_profile['max_price_actual']) && $applicant_profile['max_price_actual'] != '' )
410 + {
411 + if ( $percentage_lower != '' )
412 + {
413 + $match_price_range_lower = $applicant_profile['max_price_actual'] - ( $applicant_profile['max_price_actual'] * ( $percentage_lower / 100 ) );
414 + }
415 + }
416 + }
417 + else
418 + {
419 + $match_price_range_lower = $applicant_profile['match_price_range_lower_actual'];
420 + }
421 +
422 + $match_price_range_higher = '';
423 + if ( !isset($applicant_profile['match_price_range_higher_actual']) || ( isset($applicant_profile['match_price_range_higher_actual']) && $applicant_profile['match_price_range_higher_actual'] == '' ) )
424 + {
425 + if ( isset($applicant_profile['max_price_actual']) && $applicant_profile['max_price_actual'] != '' )
426 + {
427 + if ( $percentage_higher != '' )
428 + {
429 + $match_price_range_higher = $applicant_profile['max_price_actual'] + ( $applicant_profile['max_price_actual'] * ( $percentage_higher / 100 ) );
430 + }
431 + }
432 + }
433 + else
434 + {
435 + $match_price_range_higher = $applicant_profile['match_price_range_higher_actual'];
436 + }
437 +
438 + if ( $match_price_range_lower != '' && $match_price_range_higher != '' )
439 + {
440 + $meta_query[] = array(
441 + 'key' => '_price_actual',
442 + 'value' => array($match_price_range_lower, $match_price_range_higher),
443 + 'compare' => 'BETWEEN',
444 + 'type' => 'NUMERIC'
445 + );
446 + }
447 + }
448 + elseif ( isset($applicant_profile['max_price_actual']) && !empty($applicant_profile['max_price_actual']) )
449 + {
199 450 $meta_query[] = array(
200 451 'key' => '_price_actual',
201 452 'value' => $applicant_profile['max_price_actual'],
202 453 'compare' => '<=',
@@ -203,9 +454,15 @@
203 454 'type' => 'NUMERIC'
204 455 );
205 456 }
206 457 }
207 - 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 + )
208 465 {
209 466 $meta_query[] = array(
210 467 'key' => '_department',
211 468 'value' => $applicant_profile['department']
@@ -220,9 +477,17 @@
220 477 );
221 478 }
222 479 }
223 480
224 - 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 + )
225 490 {
226 491 if ( isset($applicant_profile['min_beds']) && $applicant_profile['min_beds'] != '' && $applicant_profile['min_beds'] != 0 )
227 492 {
228 493 $meta_query[] = array(
@@ -232,9 +497,15 @@
232 497 'type' => 'NUMERIC'
233 498 );
234 499 }
235 500 }
236 - 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 + )
237 508 {
238 509 if ( isset($applicant_profile['available_as']) && is_array($applicant_profile['available_as']) && !empty($applicant_profile['available_as']) )
239 510 {
240 511 if ( in_array('sale', $applicant_profile['available_as']) && !in_array('rent', $applicant_profile['available_as']) )
@@ -255,14 +526,164 @@
255 526 {
256 527 // Do nothing as both are ticked
257 528 }
258 529 }
530 +
531 + if (
532 + !isset($applicant_profile['min_floor_area_actual']) ||
533 + ( isset($applicant_profile['min_floor_area_actual']) && $applicant_profile['min_floor_area_actual'] === '')
534 + )
535 + {
536 + $applicant_profile['min_floor_area_actual'] = 0;
537 + }
538 + if (
539 + !isset($applicant_profile['max_floor_area_actual']) ||
540 + ( isset($applicant_profile['max_floor_area_actual']) && $applicant_profile['max_floor_area_actual'] === '')
541 + )
542 + {
543 + $applicant_profile['max_floor_area_actual'] = 99999999999;
544 + }
545 + $meta_query[] = array(
546 + 'key' => '_floor_area_from_sqft',
547 + 'value' => $applicant_profile['max_floor_area_actual'],
548 + 'compare' => '<=',
549 + 'type' => 'NUMERIC'
550 + );
551 + $meta_query[] = array(
552 + 'key' => '_floor_area_to_sqft',
553 + 'value' => $applicant_profile['min_floor_area_actual'],
554 + 'compare' => '>=',
555 + 'type' => 'NUMERIC'
556 + );
259 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.
260 673 $args['meta_query'] = $meta_query;
261 674
262 675 // Term query
263 676 $tax_query = array('relation' => 'AND');
264 - 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 + )
265 686 {
266 687 if ( isset($applicant_profile['property_types']) && is_array($applicant_profile['property_types']) && !empty($applicant_profile['property_types']) )
267 688 {
268 689 $tax_query[] = array(
@@ -272,9 +693,15 @@
272 693 'operator' => 'IN',
273 694 );
274 695 }
275 696 }
276 - 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 + )
277 704 {
278 705 if ( isset($applicant_profile['commercial_property_types']) && is_array($applicant_profile['commercial_property_types']) && !empty($applicant_profile['commercial_property_types']) )
279 706 {
280 707 $tax_query[] = array(
@@ -284,25 +711,39 @@
284 711 'operator' => 'IN',
285 712 );
286 713 }
287 714 }
288 - if ( isset($applicant_profile['locations']) && is_array($applicant_profile['locations']) && !empty($applicant_profile['locations']) )
715 + if ( get_option('propertyhive_applicant_locations_type') != 'text' )
289 716 {
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 + }
726 + }
727 + $property_match_statuses = get_option( 'propertyhive_property_match_statuses', '' );
728 + if ( $property_match_statuses != '' && is_array($property_match_statuses) && !empty($property_match_statuses) )
729 + {
290 730 $tax_query[] = array(
291 - 'taxonomy' => 'location',
731 + 'taxonomy' => 'availability',
292 732 'field' => 'term_id',
293 - 'terms' => $applicant_profile['locations'],
733 + 'terms' => $property_match_statuses,
294 734 'operator' => 'IN',
295 735 );
296 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.
297 738 $args['tax_query'] = $tax_query;
298 739
740 + $args = apply_filters( 'propertyhive_matching_properties_args', $args, $contact_id, $applicant_profile );
741 +
299 742 $properties_query = new WP_Query( $args );
300 743
301 744 if ( $properties_query->have_posts() )
302 745 {
303 - //echo '<h2>' . $properties_query->found_posts . ' matching propert' . ( ( $properties_query->found_posts != 1 ) ? 'ies' : 'y') . ' found</h2>';
304 -
305 746 while ( $properties_query->have_posts() )
306 747 {
307 748 $properties_query->the_post();
308 749
@@ -316,24 +757,30 @@
316 757
317 758 return $properties;
318 759 }
319 760
320 - 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 = '' )
321 762 {
322 763 global $wpdb;
323 764
324 765 $current_user = wp_get_current_user();
325 766
767 + $applicant_profile_details = get_post_meta( $contact_id, '_applicant_profile_' . $applicant_profile, TRUE );
768 +
769 + $contact = new PH_Contact($contact_id);
326 770 if ( $to_email_address == '' )
327 771 {
328 - $to_email_address = get_post_meta( $contact_id, '_email_address', TRUE );
772 + $to_email_address = $contact->email_address;
329 773 }
330 774
331 775 $subject = str_replace("[property_count]", count($email_property_ids) . ' propert' . ( ( count($email_property_ids) != 1 ) ? 'ies' : 'y' ), $subject);
332 776
333 - $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 );
334 779 $body = str_replace("[property_count]", count($email_property_ids) . ' propert' . ( ( count($email_property_ids) != 1 ) ? 'ies' : 'y' ), $body);
335 780
781 + $office_counts = array();
782 +
336 783 if ( strpos($body, '[properties]') !== FALSE )
337 784 {
338 785 ob_start();
339 786 if ( !empty($email_property_ids) )
@@ -341,8 +788,15 @@
341 788 foreach ( $email_property_ids as $email_property_id )
342 789 {
343 790
344 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 +
345 799 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
346 800 }
347 801 }
348 802 $body = str_replace("[properties]", ob_get_clean(), $body);
@@ -347,9 +801,49 @@
347 801 }
348 802 $body = str_replace("[properties]", ob_get_clean(), $body);
349 803 }
350 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 +
351 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.
352 846 $insert = $wpdb->insert(
353 847 $wpdb->prefix . 'ph_email_log',
354 848 array(
355 849 'contact_id' => $contact_id,
@@ -355,14 +849,16 @@
355 849 'contact_id' => $contact_id,
356 850 'property_ids' => serialize($email_property_ids),
357 851 'applicant_profile_id' => $applicant_profile,
358 852 'to_email_address' => $to_email_address,
853 + 'cc_email_address' => $cc_email_address,
854 + 'bcc_email_address' => $bcc_email_address,
359 855 'from_name' => $from_name,
360 856 'from_email_address' => $from_email_address,
361 857 'subject' => stripslashes($subject),
362 - 'body' => stripslashes($body),
858 + 'body' => $body,
363 859 'status' => '',
364 - 'send_at' => date("Y-m-d H:i:s"),
860 + 'send_at' => gmdate("Y-m-d H:i:s"),
365 861 'sent_by' => $current_user->ID,
366 862 ),
367 863 array(
368 864 '%d',
@@ -374,8 +870,10 @@
374 870 '%s',
375 871 '%s',
376 872 '%s',
377 873 '%s',
874 + '%s',
875 + '%s',
378 876 '%d',
379 877 )
380 878 );
381 879
@@ -399,9 +897,9 @@
399 897 $applicant_profile_match_history[$email_property_id] = array();
400 898 }
401 899
402 900 $applicant_profile_match_history[$email_property_id][] = array(
403 - 'date' => date("Y-m-d H:i:s"),
901 + 'date' => gmdate("Y-m-d H:i:s"),
404 902 'method' => 'email',
405 903 'email_log_id' => $email_log_id,
406 904 );
407 905
@@ -416,9 +914,9 @@
416 914 'comment_post_ID' => $email_property_id,
417 915 'comment_author' => $current_user->display_name,
418 916 'comment_author_email' => '[email protected]',
419 917 'comment_author_url' => '',
420 - 'comment_date' => date("Y-m-d H:i:s"),
918 + 'comment_date' => gmdate("Y-m-d H:i:s"),
421 919 'comment_content' => serialize($comment),
422 920 'comment_approved' => 1,
423 921 'comment_type' => 'propertyhive_note',
424 922 );
@@ -439,9 +937,9 @@
439 937 'comment_post_ID' => $contact_id,
440 938 'comment_author' => $current_user->display_name,
441 939 'comment_author_email' => '[email protected]',
442 940 'comment_author_url' => '',
443 - 'comment_date' => date("Y-m-d H:i:s"),
941 + 'comment_date' => gmdate("Y-m-d H:i:s"),
444 942 'comment_content' => serialize($comment),
445 943 'comment_approved' => 1,
446 944 'comment_type' => 'propertyhive_note',
447 945 );
@@ -451,5 +949,5 @@
451 949 }
452 950
453 951 }
454 952
455 -endif;
953 +endif;