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/class-ph-emails.php +484 -180 1.4.482.3.1 View file →
@@ -1,6 +1,9 @@
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.
2 4
5 +
3 6 if ( ! defined( 'ABSPATH' ) ) {
4 7 exit; // Exit if accessed directly
5 8 }
6 9
@@ -41,9 +44,9 @@
41 44 *
42 45 * @since 1.0.0
43 46 */
44 47 public function __clone() {
45 - _doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; huh?', 'propertyhive' ), '1.0.0' );
48 + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'propertyhive' ), '1.0.0' );
46 49 }
47 50
48 51 /**
49 52 * Unserializing instances of this class is forbidden.
@@ -50,9 +53,9 @@
50 53 *
51 54 * @since 1.0.0
52 55 */
53 56 public function __wakeup() {
54 - _doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; huh?', 'propertyhive' ), '1.0.0' );
57 + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'propertyhive' ), '1.0.0' );
55 58 }
56 59
57 60 /**
58 61 * Constructor for the email class hooks in all emails that can be sent.
@@ -71,36 +74,65 @@
71 74 // Send applicant registration email
72 75 add_action( 'propertyhive_applicant_registered', array( $this, 'send_applicant_registration_alert' ), 10, 2 );
73 76
74 77 add_action( 'admin_init', array( $this, 'run_custom_email_cron' ), 10, 1 );
78 +
79 + add_action( 'init', array( $this, 'check_email_queue_is_scheduled'), 99 );
75 80 }
76 81
82 + public function check_email_queue_is_scheduled()
83 + {
84 + $schedule = wp_get_schedule( 'propertyhive_process_email_log' );
85 +
86 + if ( $schedule === FALSE )
87 + {
88 + // Hmm... cron job not found. Let's set it up
89 + $timestamp = wp_next_scheduled( 'propertyhive_process_email_log' );
90 + wp_unschedule_event($timestamp, 'propertyhive_process_email_log' );
91 + wp_clear_scheduled_hook('propertyhive_process_email_log');
92 +
93 + wp_schedule_event( time(), 'every_fifteen_minutes', 'propertyhive_process_email_log' );
94 + }
95 + }
96 +
77 97 public function run_custom_email_cron()
78 98 {
79 - if (isset($_GET['custom_email_log_cron']) && in_array($_GET['custom_email_log_cron'], array('propertyhive_process_email_log', 'propertyhive_auto_email_match')) )
99 + if ( isset( $_GET['custom_email_log_cron'] ) )
80 100 {
81 - do_action($_GET['custom_email_log_cron']);
101 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
102 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
103 + }
104 + check_admin_referer( 'propertyhive-run-email-job' );
105 + $job = is_string( $_GET['custom_email_log_cron'] ) ? sanitize_key( wp_unslash( $_GET['custom_email_log_cron'] ) ) : '';
106 + if ( ! in_array( $job, array( 'propertyhive_process_email_log', 'propertyhive_auto_email_match' ), true ) ) {
107 + wp_die( esc_html__( 'Invalid email job.', 'propertyhive' ), '', array( 'response' => 400 ) );
108 + }
109 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Dynamic email cron dispatch is a bounded internal false positive: the value is sanitized, restricted to two propertyhive_* jobs, and reached only after capability and nonce checks.
110 + do_action( $job );
82 111 }
83 112 }
84 113
85 - public function send_applicant_registration_alert( $contact_post_id, $user_id )
86 - {
87 - if (
88 - get_option( 'propertyhive_new_registration_alert', '' ) == 'yes' &&
89 - isset($_POST['office_id']) && // in the future we should have office stored against contact and use that
90 - $_POST['office_id'] != '' &&
91 - isset($_POST['department']) && // Should really take department from contacts requirements
92 - $_POST['department'] != ''
93 - )
114 + public function send_applicant_registration_alert( $contact_post_id, $user_id )
94 115 {
95 - $to = get_post_meta( (int)$_POST['office_id'], '_office_email_address_' . str_replace("residential-", "", ph_clean($_POST['department'])), TRUE );
116 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This internal action callback is invoked by the nonce-checked applicant registration AJAX handler; it only formats that request's notification email.
117 + $request_post = wp_unslash( $_POST );
118 + $office_id = isset( $request_post['office_id'] ) && is_scalar( $request_post['office_id'] ) ? absint( $request_post['office_id'] ) : 0;
119 + $department = isset( $request_post['department'] ) && is_string( $request_post['department'] ) ? sanitize_key( $request_post['department'] ) : '';
96 120
121 + if ( get_option( 'propertyhive_new_registration_alert', '' ) == 'yes' && $office_id > 0 && '' !== $department )
122 + {
123 + $to = get_post_meta( $office_id, '_office_email_address_' . str_replace( 'residential-', '', $department ), TRUE );
124 +
97 125 if ( $to == '' )
98 126 {
99 127 $to = get_option( 'admin_email', '' );
100 128 }
101 129
102 - $subject = sprintf( __( 'A new applicant, %s, has registered through your website', 'propertyhive' ), get_the_title($contact_post_id) );
130 + $subject = sprintf(
131 + /* translators: %s: person name */
132 + __( 'A new applicant, %s, has registered through your website', 'propertyhive' ),
133 + get_the_title($contact_post_id)
134 + );
103 135
104 136 $message = __( "A new applicant has registered through your website. Please find details of the applicant below:", 'propertyhive' ) . "\n\n";
105 137
106 138 $message = apply_filters( 'propertyhive_applicant_registration_email_pre_body', $message, $contact_post_id );
@@ -110,9 +142,9 @@
110 142 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
111 143
112 144 $form_controls_2 = ph_get_applicant_requirements_form_fields();
113 145
114 - $form_controls_2 = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls_2 );
146 + $form_controls_2 = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls_2, get_post_meta( $contact_post_id, '_applicant_profile_0', true ) );
115 147
116 148 $form_controls = array_merge( $form_controls, $form_controls_2 );
117 149
118 150 $form_controls = apply_filters( 'propertyhive_applicant_registration_form_fields', $form_controls );
@@ -125,17 +157,26 @@
125 157 {
126 158 continue;
127 159 }
128 160
129 - $value = ph_clean($_POST[$key]);
130 - if ( taxonomy_exists($key) )
161 + $raw_value = isset( $request_post[ $key ] ) && ( is_string( $request_post[ $key ] ) || is_array( $request_post[ $key ] ) ) ? $request_post[ $key ] : '';
162 + $value = ph_clean( $raw_value );
163 + $values = array();
164 + if ( !empty($value) && taxonomy_exists($key) )
131 165 {
132 - $term = get_term( ph_clean($_POST[$key]), $key );
166 + if ( !is_array($value) ) { $value = array($value); }
133 167
134 - if ( isset($term->name) )
168 + foreach ( $value as $value_term )
135 169 {
136 - $value = $term->name;
137 - }
170 + $term = get_term( ph_clean($value_term), $key );
171 +
172 + if ( isset($term->name) )
173 + {
174 + $values[] = $term->name;
175 + }
176 + }
177 +
178 + $value = implode(", ", $values);
138 179 }
139 180
140 181 if ( ph_clean($value) == '' )
141 182 {
@@ -158,67 +199,91 @@
158 199 global $wpdb;
159 200
160 201 $lock_id = uniqid( "", true );
161 202
162 - $wpdb->query("
163 - UPDATE " . $wpdb->prefix . "ph_email_log
164 - SET
165 - status = 'fail2',
166 - lock_id = ''
167 - WHERE
168 - status = 'fail1'
169 - AND
170 - lock_id <> ''
171 - AND
172 - locked_at <= '" . date("Y-m-d H:i:s", strtotime('24 hours ago')) . "'
173 - ");
203 + $expired_lock = gmdate( 'Y-m-d H:i:s', strtotime( '24 hours ago' ) );
204 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The email queue is a custom plugin table and these bounded maintenance updates have no WordPress API equivalent.
205 + $wpdb->query( $wpdb->prepare(
206 + "UPDATE {$wpdb->prefix}ph_email_log SET status = 'fail2', lock_id = '' WHERE status = 'fail1' AND lock_id <> '' AND locked_at <= %s",
207 + $expired_lock
208 + ) );
209 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The email queue is a custom plugin table and these bounded maintenance updates have no WordPress API equivalent.
210 + $wpdb->query( $wpdb->prepare(
211 + "UPDATE {$wpdb->prefix}ph_email_log SET status = 'fail1', lock_id = '' WHERE status = '' AND lock_id <> '' AND locked_at <= %s",
212 + $expired_lock
213 + ) );
174 214
175 - $wpdb->query("
176 - UPDATE " . $wpdb->prefix . "ph_email_log
177 - SET
178 - status = 'fail1',
179 - lock_id = ''
180 - WHERE
181 - status = ''
182 - AND
183 - lock_id <> ''
184 - AND
185 - locked_at <= '" . date("Y-m-d H:i:s", strtotime('24 hours ago')) . "'
186 - ");
187 -
188 - // Lock/reserve all emails in log that are status blank or 'fail1' and lock_id blank and send_at in the past
189 - // Only grab 5 at a time to prevent hanging/being seen as spamming
190 - $wpdb->query("
191 - UPDATE " . $wpdb->prefix . "ph_email_log
192 - SET
193 - lock_id = '" . $lock_id . "',
194 - locked_at = '" . date("Y-m-d H:i:s") . "'
195 - WHERE
196 - (status = '' OR status = 'fail1')
197 - AND
198 - lock_id = ''
199 - AND
200 - send_at <= '" . date("Y-m-d H:i:s") . "'
201 - LIMIT 5
202 - ");
215 + // Reserve a bounded batch before sending, so concurrent workers cannot send it twice.
216 + $process_limit = max( 1, min( 1000, (int) apply_filters( 'propertyhive_email_process_limit', 25 ) ) );
217 + $now = gmdate( 'Y-m-d H:i:s' );
218 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The email queue is a custom plugin table and this bounded reservation update has no WordPress API equivalent.
219 + $wpdb->query( $wpdb->prepare(
220 + "UPDATE {$wpdb->prefix}ph_email_log SET lock_id = %s, locked_at = %s WHERE (status = '' OR status = 'fail1') AND lock_id = '' AND send_at <= %s LIMIT %d",
221 + $lock_id, $now, $now, $process_limit
222 + ) );
223 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The email queue is a custom plugin table and this lock-scoped read has no WordPress API equivalent.
224 + $emails_to_send = $wpdb->get_results( $wpdb->prepare(
225 + "SELECT * FROM {$wpdb->prefix}ph_email_log WHERE lock_id = %s",
226 + $lock_id
227 + ) );
203 228
204 - // We now have up to 5 emails locked. Get this 5 and attempt to send
205 - $emails_to_send = $wpdb->get_results("
206 - SELECT *
207 - FROM " . $wpdb->prefix . "ph_email_log
208 - WHERE
209 - lock_id = '" . $lock_id . "'
210 - ");
211 -
212 229 foreach ( $emails_to_send as $email_to_send )
213 230 {
214 231 $email_id = $email_to_send->email_id;
215 232
233 + $contact_id = (int)$email_to_send->contact_id;
234 +
235 + // Ensure client is still publised, hasn't unsubscribed from emails and that send matching properties is yes
236 + // To combat being added to the queue but then the queue only getting processed at a later date
237 + $forbidden_contact_methods = get_post_meta( $contact_id, '_forbidden_contact_methods', true );
238 +
239 + $email_forbidden = is_array( $forbidden_contact_methods ) && in_array( 'email', $forbidden_contact_methods, true );
240 +
241 + $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . (int)$email_to_send->applicant_profile_id, true );
242 +
243 + $matching_enabled = is_array( $applicant_profile )
244 + && isset( $applicant_profile['send_matching_properties'] )
245 + && 'yes' === $applicant_profile['send_matching_properties'];
246 +
247 + if (
248 + 'publish' !== get_post_status( $contact_id )
249 + || $email_forbidden
250 + || !$matching_enabled
251 + ) {
252 + $wpdb->update(
253 + $wpdb->prefix . 'ph_email_log',
254 + array(
255 + 'status' => 'fail2',
256 + 'lock_id' => '',
257 + ),
258 + array( 'email_id' => (int)$email_id ),
259 + array( '%s', '%s' ),
260 + array( '%d' )
261 + );
262 +
263 + continue;
264 + }
265 +
216 266 $headers = array();
217 - $headers[] = 'From: ' . $email_to_send->from_name . ' <' . $email_to_send->from_email_address . '>';
267 + $headers[] = 'From: ' . html_entity_decode($email_to_send->from_name) . ' <' . $email_to_send->from_email_address . '>';
268 + if ( $email_to_send->cc_email_address != '' )
269 + {
270 + $headers[] = 'Cc: ' . $email_to_send->cc_email_address;
271 + }
272 + if ( $email_to_send->bcc_email_address != '' )
273 + {
274 + $headers[] = 'Bcc: ' . $email_to_send->bcc_email_address;
275 + }
218 276 $headers[] = 'Content-Type: text/html; charset=UTF-8';
219 277
220 - $body = apply_filters( 'propertyhive_mail_content', $this->style_inline( $this->wrap_message( $email_to_send->body, $email_to_send->contact_id ) ) );
278 + $body = $email_to_send->body;
279 +
280 + if ( extension_loaded('zlib') && @gzuncompress($body) !== false )
281 + {
282 + $body = gzuncompress($body);
283 + }
284 +
285 + $body = apply_filters( 'propertyhive_mail_content', $this->style_inline( $this->wrap_message( $body, $email_to_send->contact_id ) ) );
221 286
222 287 $sent = wp_mail(
223 288 $email_to_send->to_email_address,
224 289 $email_to_send->subject,
@@ -244,17 +309,33 @@
244 309 {
245 310 $new_status = 'fail2';
246 311 }
247 312 }
248 - $wpdb->query("
249 - UPDATE " . $wpdb->prefix . "ph_email_log
250 - SET
251 - status = '" . $new_status . "',
252 - lock_id = ''
253 - WHERE
254 - email_id = '" . $email_id . "'
255 - ");
313 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The email queue is a custom plugin table; wpdb is the required API for updating its status.
314 + $wpdb->update(
315 + $wpdb->prefix . 'ph_email_log',
316 + array( 'status' => $new_status, 'lock_id' => '' ),
317 + array( 'email_id' => (int) $email_id ),
318 + array( '%s', '%s' ),
319 + array( '%d' )
320 + );
256 321 }
322 +
323 + // Delete old logs
324 + $keep_logs_days = (string)apply_filters( 'propertyhive_keep_email_logs_days', '3650' ); // 10 years
325 +
326 + // Revert back to 3650 days if anything other than numbers has been passed
327 + // This prevent SQL injection and errors
328 + if ( !preg_match("/^\d+$/", $keep_logs_days) )
329 + {
330 + $keep_logs_days = '3650';
331 + }
332 +
333 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The email queue is a custom plugin table and this retention cleanup has no WordPress API equivalent.
334 + $wpdb->query( $wpdb->prepare(
335 + "DELETE FROM {$wpdb->prefix}ph_email_log WHERE send_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
336 + (int) $keep_logs_days
337 + ) );
257 338 }
258 339
259 340 /*
260 341 * Automatically send new properties to registered applicants
@@ -262,8 +343,14 @@
262 343 public function ph_auto_email_match()
263 344 {
264 345 global $post;
265 346
347 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- dry_run only selects diagnostic output and never changes persisted data; the real email action is capability and nonce protected in run_custom_email_cron().
348 + $request_get = wp_unslash( $_GET );
349 + $dry_run = isset( $request_get['dry_run'] );
350 +
351 + if ( $dry_run === true ) { echo 'Running auto-match in dry run mode. Logging will be output and no emails will be sent.' . "<br>\n"; }
352 +
266 353 // Auto emails enabled in settings
267 354 // Auto emails not disabled in applicant record
268 355 // Property added more recently that setting enabled in settings
269 356 // Property not already previously sent
@@ -271,8 +358,10 @@
271 358 // 'Do not email' not selected
272 359
273 360 $auto_property_match_enabled = get_option( 'propertyhive_auto_property_match', '' );
274 361
362 + if ( $dry_run === true ) { echo 'Auto-match setting enabled: ' . esc_html( $auto_property_match_enabled ) . "<br>\n"; }
363 +
275 364 if ( $auto_property_match_enabled == '' )
276 365 {
277 366 return false;
278 367 }
@@ -278,20 +367,22 @@
278 367 }
279 368
280 369 $auto_property_match_enabled_date = get_option( 'propertyhive_auto_property_match_enabled_date', '' );
281 370
371 + if ( $dry_run === true ) { echo 'Auto-match setting enabled date: ' . esc_html( $auto_property_match_enabled_date ) . "<br>\n"; }
372 +
282 373 if ( $auto_property_match_enabled_date == '' )
283 374 {
284 375 return false;
285 376 }
286 377
287 - // Get all of the office email addresses and store them in an array to save having to query them for every email
288 - // Get all contacts that have a type of applicant
378 + // Get all of the offices and store them in an array to save having to query them for every email
289 379 $args = array(
290 380 'post_type' => 'office',
291 381 'nopaging' => true
292 382 );
293 383
384 + $office_names = array();
294 385 $office_email_addresses = array();
295 386 $office_query = new WP_Query( $args );
296 387
297 388 if ( $office_query->have_posts() )
@@ -299,8 +390,10 @@
299 390 while ( $office_query->have_posts() )
300 391 {
301 392 $office_query->the_post();
302 393
394 + $office_names[get_the_ID()] = get_the_title( get_the_ID() );
395 +
303 396 $office_email_addresses['residential-sales'][get_the_ID()] = get_post_meta( get_the_ID(), '_office_email_address_sales', TRUE );
304 397 $office_email_addresses['residential-lettings'][get_the_ID()] = get_post_meta( get_the_ID(), '_office_email_address_lettings', TRUE );
305 398 $office_email_addresses['commercial'][get_the_ID()] = get_post_meta( get_the_ID(), '_office_email_address_commercial', TRUE );
306 399 }
@@ -307,32 +400,100 @@
307 400 }
308 401
309 402 wp_reset_postdata();
310 403
404 + // Get all of the negotiators and store them in an array to save having to query them for every email
405 + $negotiator_names = array();
406 + $negotiator_email_addresses = array();
407 +
408 + $args = array(
409 + 'number' => 9999,
410 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy Property Negotiator compatibility filter; existing role filters depend on this exact public hook name.
411 + 'role__not_in' => apply_filters( 'property_negotiator_exclude_roles', array('property_hive_contact', 'subscriber') ),
412 + 'fields' => array( 'ID', 'display_name', 'user_email' )
413 + );
414 +
415 + $args = apply_filters( 'propertyhive_negotiators_query', $args );
416 +
417 + $user_query = new WP_User_Query( $args );
418 +
419 + $negotiators = array();
420 +
421 + if ( ! empty( $user_query->results ) )
422 + {
423 + foreach ( $user_query->results as $user )
424 + {
425 + $negotiator_names[$user->ID] = $user->display_name;
426 +
427 + $negotiator_email_addresses[$user->ID] = $user->user_email;
428 + }
429 + }
430 +
311 431 include_once( dirname(__FILE__) . '/admin/class-ph-admin-matching-properties.php' );
312 432 $ph_admin_matching_properties = new PH_Admin_Matching_Properties();
313 433
434 + $meta_query = array(
435 + array(
436 + 'key' => '_contact_types',
437 + 'value' => 'applicant',
438 + 'compare' => 'LIKE'
439 + )
440 + );
441 +
442 + if ( version_compare( get_bloginfo( 'version' ), '5.1', '>=' ) )
443 + {
444 + // If WP version contains compare_key, check contact has at least one applicant profile with Send Matching Properties checked
445 + $meta_query[] = array(
446 + 'key' => '_applicant_profile_',
447 + 'compare_key' => 'LIKE',
448 + 'value' => 'send_matching_properties";s:3:"yes"',
449 + 'compare' => 'LIKE',
450 + );
451 + }
452 +
314 453 // Get all contacts that have a type of applicant
315 454 $args = array(
316 455 'post_type' => 'contact',
456 + 'post_status' => 'publish',
317 457 'nopaging' => true,
318 - 'meta_query' => array(
319 - array(
320 - 'key' => '_contact_types',
321 - 'value' => 'applicant',
322 - 'compare' => 'LIKE'
323 - )
324 - ),
458 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Matching mail must include all eligible applicant profiles; membership and opt-in eligibility are stored in contact metadata.
459 + 'meta_query' => $meta_query,
325 460 'fields' => 'ids'
326 461 );
327 462
463 + if ( $dry_run === true ) { echo 'Running query to get contacts with args: ' . esc_html( wp_json_encode( $args, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
464 +
328 465 $contact_query = new WP_Query( $args );
329 466
467 + if ( $dry_run === true ) { echo 'Found ' . esc_html( $contact_query->found_posts ) . ' contacts' . "<br>\n"; }
468 +
330 469 if ( $contact_query->have_posts() )
331 470 {
332 471 $default_subject = get_option( 'propertyhive_property_match_default_email_subject', '' );
333 472 $default_body = get_option( 'propertyhive_property_match_default_email_body', '' );
334 473
474 + $allowed_tags = array(
475 + 'strong' => array(),
476 + 'span' => array(),
477 + 'em' => array(),
478 + 'h1' => array(),
479 + 'h2' => array(),
480 + 'h3' => array(),
481 + 'h4' => array(),
482 + 'h5' => array(),
483 + 'h6' => array(),
484 + 'i' => array(),
485 + 'u' => array(),
486 + 'b' => array(),
487 + 'a' => array(
488 + 'href' => array(),
489 + 'target' => array(),
490 + ),
491 + );
492 + $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
493 +
494 + $default_body = wp_kses( $default_body, $allowed_tags );
495 +
335 496 while ( $contact_query->have_posts() )
336 497 {
337 498 $contact_query->the_post();
338 499
@@ -337,11 +498,15 @@
337 498 $contact_query->the_post();
338 499
339 500 $contact_id = get_the_ID();
340 501
502 + if ( $dry_run === true ) { echo 'Doing contact: ' . esc_html( get_the_title() ) . "<br>\n"; }
503 +
341 504 // invalid email address
342 505 if ( strpos( get_post_meta( $contact_id, '_email_address', TRUE ), '@' ) === FALSE )
343 506 {
507 + if ( $dry_run === true ) { echo esc_html('Invalid email address. Skipping') . "<br>\n"; }
508 +
344 509 continue;
345 510 }
346 511
347 512 // email in the list of forbidden contact methods
@@ -347,8 +512,10 @@
347 512 // email in the list of forbidden contact methods
348 513 $forbidden_contact_methods = get_post_meta( $contact_id, '_forbidden_contact_methods', TRUE );
349 514 if ( is_array($forbidden_contact_methods) && in_array('email', $forbidden_contact_methods) )
350 515 {
516 + if ( $dry_run === true ) { echo esc_html('Email communication forbidden in contact preferences. Skipping') . "<br>\n"; }
517 +
351 518 continue;
352 519 }
353 520
354 521 $applicant_profiles = get_post_meta( $contact_id, '_applicant_profiles', TRUE );
@@ -360,8 +527,10 @@
360 527 {
361 528 $dismissed_properties = array();
362 529 }
363 530
531 + if ( $dry_run === true ) { if ( !empty($dismissed_properties) ) { echo 'Dismissed properties: ' . esc_html( wp_json_encode( $dismissed_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; } }
532 +
364 533 for ( $i = 0; $i < $applicant_profiles; ++$i )
365 534 {
366 535 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
367 536
@@ -366,23 +535,30 @@
366 535 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
367 536
368 537 if ( $applicant_profile == '' || !is_array($applicant_profile) || !isset($applicant_profile['department']) )
369 538 {
539 + if ( $dry_run === true ) { echo esc_html('Applicant relationship empty or no department set') . "<br>\n"; }
370 540 continue;
371 541 }
372 542
373 - if ( isset($applicant_profile['send_matching_properties']) && $applicant_profile['send_matching_properties'] == '' )
543 + if ( !isset($applicant_profile['send_matching_properties']) || ( isset($applicant_profile['send_matching_properties']) && $applicant_profile['send_matching_properties'] != 'yes' ) )
374 544 {
545 + if ( $dry_run === true ) { echo esc_html('Send matching properties disabled') . "<br>\n"; }
375 546 continue;
376 547 }
377 548
378 549 if ( isset($applicant_profile['auto_match_disabled']) && $applicant_profile['auto_match_disabled'] == 'yes' )
379 550 {
551 + if ( $dry_run === true ) { echo esc_html('Auto match disabled') . "<br>\n"; }
380 552 continue;
381 553 }
382 554
555 + if ( $dry_run === true ) { echo 'Getting matching properties' . "<br>\n"; }
556 +
383 557 $matching_properties = $ph_admin_matching_properties->get_matching_properties( $contact_id, $i, $auto_property_match_enabled_date );
384 558
559 + if ( $dry_run === true ) { echo esc_html('Found ' . count($matching_properties) . ' matching properties') . "<br>\n"; }
560 +
385 561 if ( !empty($matching_properties) )
386 562 {
387 563 $already_sent_properties = get_post_meta( $contact_id, '_applicant_profile_' . $i . '_match_history', TRUE );
388 564
@@ -388,8 +564,10 @@
388 564
389 565 // Remove from this array if on market changed or price changed
390 566 if ( is_array($already_sent_properties) )
391 567 {
568 + if ( $dry_run === true ) { echo 'Already sent properties before: ' . esc_html( wp_json_encode( $already_sent_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
569 +
392 570 foreach ( $already_sent_properties as $already_sent_property_id => $sends )
393 571 {
394 572 $highest_send = $sends[count($sends) - 1]['date'];
395 573
@@ -394,20 +572,40 @@
394 572 $highest_send = $sends[count($sends) - 1]['date'];
395 573
396 574 if ( $highest_send != '' )
397 575 {
576 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last sent: ' . esc_html( $highest_send ) . "<br>\n"; }
577 +
398 578 $on_market_change_date = get_post_meta( $already_sent_property_id, '_on_market_change_date', TRUE );
579 +
580 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last on market change: ' . esc_html( $on_market_change_date ) . "<br>\n"; }
581 +
399 582 $price_change_date = get_post_meta( $already_sent_property_id, '_price_change_date', TRUE );
400 583
401 - if ( $on_market_change_date > $highest_send || $price_change_date > $highest_send )
584 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last price change: ' . esc_html( $price_change_date ) . "<br>\n"; }
585 +
586 + if ( $on_market_change_date > $highest_send )
402 587 {
588 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' has changed on market since last sent' . "<br>\n"; }
589 +
403 590 // This property has changed since it was last sent. Remove from already sent list so it gets sent again
404 591 unset($already_sent_properties[$already_sent_property_id]);
405 592 }
593 + elseif ( $price_change_date > $highest_send )
594 + {
595 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' has changed price since last sent' . "<br>\n"; }
596 +
597 + // This property has changed since it was last sent. Remove from already sent list so it gets sent again
598 + unset($already_sent_properties[$already_sent_property_id]);
599 + }
406 600 }
407 601 }
602 +
603 + if ( $dry_run === true ) { echo 'Already sent properties after: ' . esc_html( wp_json_encode( $already_sent_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
408 604 }
409 605
606 + if ( $dry_run === true ) { echo 'Matching properties before removing already sent: ' . esc_html( wp_json_encode( $matching_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
607 +
410 608 // Check properties haven't already been sent and not marked as 'not interested'
411 609 $new_matching_properties = array();
412 610 foreach ($matching_properties as $matching_property)
413 611 {
@@ -416,20 +614,34 @@
416 614 $new_matching_properties[] = $matching_property->id;
417 615 }
418 616 }
419 617
618 + if ( $dry_run === true ) { echo 'Matching properties after removing already sent: ' . esc_html( wp_json_encode( $new_matching_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
619 +
620 + $max_results = apply_filters( 'propertyhive_auto_match_maximum_results', FALSE);
621 + if ( $max_results !== FALSE )
622 + {
623 + $new_matching_properties = array_slice($new_matching_properties, 0, (int)$max_results);
624 + }
625 +
626 + if ( $dry_run === true ) { echo esc_html('Found ' . count($new_matching_properties) . ' matching properties after removing already sent and dismissed') . "<br>\n"; }
627 +
420 628 if ( !empty($new_matching_properties) )
421 629 {
422 630 $subject = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $default_subject);
423 631
424 - $body = str_replace("[contact_name]", get_the_title($contact_id), $default_body);
632 + $contact = new PH_Contact($contact_id);
633 + $body = str_replace( '[contact_name]', esc_html( $contact->post_title ), $default_body );
634 + $body = str_replace( '[contact_dear]', esc_html( $contact->dear() ), $body );
425 635 $body = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $body);
426 636
637 + $office_counts = array();
638 + $negotiator_counts = array();
639 +
427 640 if ( strpos($body, '[properties]') !== FALSE )
428 641 {
429 642 ob_start();
430 643
431 - $office_counts = array();
432 644 if ( !empty($new_matching_properties) )
433 645 {
434 646 foreach ( $new_matching_properties as $email_property_id )
435 647 {
@@ -440,8 +652,14 @@
440 652 if ( !isset($office_counts[$property->office_id]) ) { $office_counts[$property->office_id] = 0; }
441 653 ++$office_counts[$property->office_id];
442 654 }
443 655
656 + if ( $property->negotiator_id != '' && $property->negotiator_id != 0 )
657 + {
658 + if ( !isset($negotiator_counts[$property->negotiator_id]) ) { $negotiator_counts[$property->negotiator_id] = 0; }
659 + ++$negotiator_counts[$property->negotiator_id];
660 + }
661 +
444 662 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
445 663 }
446 664 }
447 665 $body = str_replace("[properties]", ob_get_clean(), $body);
@@ -447,8 +665,9 @@
447 665 $body = str_replace("[properties]", ob_get_clean(), $body);
448 666 }
449 667
450 668 // Get email address of office with most properties
669 + $highest_office_name = '';
451 670 $highest_office_email_address = '';
452 671 if ( !empty($office_counts) )
453 672 {
454 673 arsort($office_counts);
@@ -453,8 +672,9 @@
453 672 {
454 673 arsort($office_counts);
455 674 reset($office_counts);
456 675 $highest_office_id = key($office_counts);
676 + $highest_office_name = ( isset($office_names[$highest_office_id]) ? $office_names[$highest_office_id] : '' );
457 677 $highest_office_email_address = ( isset($office_email_addresses[$applicant_profile['department']][$highest_office_id]) ? $office_email_addresses[$applicant_profile['department']][$highest_office_id] : '' );
458 678 }
459 679 if ( $highest_office_email_address == '' )
460 680 {
@@ -461,24 +681,56 @@
461 681 // fallback to admin email address
462 682 $highest_office_email_address = get_option('admin_email');
463 683 }
464 684
465 - $ph_admin_matching_properties->send_emails(
466 - $contact_id,
467 - $i,
468 - $new_matching_properties,
469 - get_bloginfo('name'),
470 - $highest_office_email_address,
471 - $subject,
472 - $body
473 - );
685 + $body = str_replace( '[office_name]', esc_html( $highest_office_name ), $body );
686 + $body = str_replace( '[office_email_address]', esc_html( $highest_office_email_address ), $body );
687 +
688 + $highest_negotiator_name = '';
689 + $highest_negotiator_email_address = '';
690 + if ( !empty($negotiator_counts) )
691 + {
692 + arsort($negotiator_counts);
693 + reset($negotiator_counts);
694 + $highest_negotiator_id = key($negotiator_counts);
695 + $highest_negotiator_name = ( isset($negotiator_names[$highest_negotiator_id]) ? $negotiator_names[$highest_negotiator_id] : '' );
696 + $highest_negotiator_email_address = ( isset($negotiator_email_addresses[$highest_negotiator_id]) ? $negotiator_email_addresses[$highest_negotiator_id] : '' );
697 + }
698 +
699 + $body = str_replace( '[negotiator_name]', esc_html( $highest_negotiator_name ), $body );
700 + $body = str_replace( '[negotiator_email_address]', esc_html( $highest_negotiator_email_address ), $body );
701 +
702 + $highest_office_email_address = apply_filters( 'propertyhive_auto_match_from_email_address', $highest_office_email_address );
703 +
704 + if ( !$dry_run )
705 + {
706 + $ph_admin_matching_properties->send_emails(
707 + $contact_id,
708 + $i,
709 + $new_matching_properties,
710 + get_bloginfo('name'),
711 + $highest_office_email_address,
712 + $subject,
713 + $body
714 + );
715 + }
716 + else
717 + {
718 + echo esc_html('Would\'ve sent email. Not sending due to being ran in dry run mode') . "<br>\n";
719 + }
474 720 }
475 721 }
476 722 }
477 723 }
724 + else
725 + {
726 + if ( $dry_run === true ) { echo esc_html('No applicant profiles found. Skipping') . "<br>\n"; }
727 + }
478 728 }
479 729 }
480 730
731 + if ( $dry_run === true ) { echo esc_html('Finished auto-match process') . "<br>\n"; die(); }
732 +
481 733 wp_reset_postdata();
482 734 }
483 735
484 736 /**
@@ -501,18 +753,18 @@
501 753 ph_get_template( 'emails/email-styles.php' );
502 754 $css = apply_filters( 'propertyhive_email_styles', ob_get_clean() );
503 755
504 756 // include css inliner
505 - if ( ! class_exists( 'Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
757 + if ( ! class_exists( 'PropertyHive_Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
506 758 include_once( dirname( __FILE__ ) . '/libraries/class-emogrifier.php' );
507 759 }
508 760
509 761 // apply CSS styles inline for picky email clients
510 762 try {
511 - $emogrifier = new Emogrifier( $content, $css );
763 + $emogrifier = new PropertyHive_Emogrifier( $content, $css );
512 764 $content = $emogrifier->emogrify();
513 765 } catch ( Exception $e ) {
514 - die("Error converting CSS styles to be inline. Error as follows: " . $e->getMessage());
766 + die(esc_html("Error converting CSS styles to be inline. Error as follows: " . $e->getMessage()));
515 767 }
516 768 }
517 769 return $content;
518 770 }
@@ -530,9 +782,9 @@
530 782 public function email_footer( $contact_id = '' ) {
531 783 $unsubscribe_link = '';
532 784 if ($contact_id != '')
533 785 {
534 - $unsubscribe_link = site_url() .'?ph_unsubscribe=' . base64_encode($contact_id . '|' . md5( get_post_meta( $contact_id, '_email_address', TRUE ) ) );
786 + $unsubscribe_link = PH()->get_contact_unsubscribe_url( $contact_id );
535 787 }
536 788
537 789 ph_get_template( 'emails/email-footer.php', array( 'unsubscribe_link' => $unsubscribe_link ) );
538 790 }
@@ -549,8 +801,9 @@
549 801 ob_start();
550 802
551 803 do_action( 'propertyhive_email_header', $contact_id );
552 804
805 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Email bodies are deliberately HTML. The callers sanitize stored/request template content before this shared wrapper, and email header/footer plus propertyhive_mail_content are trusted extension points; escaping here would break supported markup.
553 806 echo wpautop( wptexturize( $message ) );
554 807
555 808 do_action( 'propertyhive_email_footer', $contact_id );
556 809
@@ -561,12 +814,26 @@
561 814 }
562 815
563 816 public function send_enquiry_auto_responder( $data = array() )
564 817 {
565 - if ( isset($data['property_id']) && sanitize_text_field($data['property_id']) != '' )
818 + if ( ! is_array( $data ) ) {
819 + return;
820 + }
821 +
822 + $request_data = wp_unslash( $data );
823 + if ( isset( $request_data['property_id'] ) && is_string( $request_data['property_id'] ) && '' !== $request_data['property_id'] )
566 824 {
567 - $property_id = $data['property_id'];
568 - $to = sanitize_email( $_POST['email_address'] );
825 + $property_ids = array_filter( array_map( 'absint', explode( '|', $request_data['property_id'] ) ) );
826 + if ( empty( $property_ids ) || count( $property_ids ) > 100 ) {
827 + return;
828 + }
829 + foreach ( $property_ids as $property_id ) {
830 + if ( 'property' !== get_post_type( $property_id ) || ! propertyhive_is_post_publicly_viewable( $property_id ) ) {
831 + return;
832 + }
833 + }
834 +
835 + $to = isset( $request_data['email_address'] ) && is_string( $request_data['email_address'] ) ? sanitize_email( $request_data['email_address'] ) : '';
569 836 $subject = get_option( 'propertyhive_enquiry_auto_responder_email_subject', '' );
570 837 $body = get_option( 'propertyhive_enquiry_auto_responder_email_body', '' );
571 838
572 839 if ( $to != '' && $subject != '' && $body != '' )
@@ -571,107 +838,144 @@
571 838
572 839 if ( $to != '' && $subject != '' && $body != '' )
573 840 {
574 841 $headers = array();
575 - $headers[] = 'From: ' . get_bloginfo('name') . ' <' . get_option( 'propertyhive_email_from_address', get_option( 'admin_email' ) ) . '>';
842 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . get_option( 'propertyhive_email_from_address', get_option( 'admin_email' ) ) . '>';
576 843 $headers[] = 'Content-Type: text/html; charset=UTF-8';
577 844
578 - $body = str_replace( "[name]", ( isset($_POST['name']) ? ph_clean($_POST['name']) : '' ), $body );
845 + $name = isset( $request_data['name'] ) && is_string( $request_data['name'] ) ? ph_clean( $request_data['name'] ) : '';
846 + $body = str_replace( '[name]', esc_html( $name ), $body );
579 847
580 - $body = str_replace( "[property_address_hyperlinked]", '<a href="' . get_permalink($property_id) . '">' . get_the_title($property_id) . '</a>', $body );
848 + $property_address_hyperlinked = array();
849 + foreach ( $property_ids as $property_id )
850 + {
851 + $property_address_hyperlinked[] = '<a href="' . esc_url( get_permalink( $property_id ) ) . '">' . esc_html( get_the_title( $property_id ) ) . '</a>';
852 + }
853 + $body = str_replace( "[property_address_hyperlinked]", implode(' and ', array_filter(array_merge(array(join(', ', array_slice($property_address_hyperlinked, 0, -1))), array_slice($property_address_hyperlinked, -1)), 'strlen')), $body );
581 854
582 855 if ( strpos( $body, '[similar_properties]' ) !== FALSE )
583 856 {
584 857 $similar_html = '';
585 858
586 - $department = get_post_meta( $property_id, '_department', TRUE );
587 - $bedrooms = get_post_meta( $property_id, '_bedrooms', TRUE );
588 - $price = get_post_meta( $property_id, '_price_actual', true );
589 - $lower_price = $price - ($price / 10);
590 - $higher_price = $price + ($price / 10);
859 + foreach ( $property_ids as $property_id )
860 + {
861 + $department = get_post_meta( $property_id, '_department', TRUE );
591 862
592 - // Get two similar properties
593 - $args = array(
594 - 'post_type' => 'property',
595 - 'post_status' => 'publish',
596 - 'posts_per_page' => 3,
597 - 'orderby' => 'rand',
598 - 'post__not_in' => array($property_id),
599 - );
863 + // Get three similar properties
864 + $args = array(
865 + 'post_type' => 'property',
866 + 'post_status' => 'publish',
867 + 'posts_per_page' => 3,
868 + 'orderby' => 'rand',
869 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Each email’s similar-property query returns three properties and excludes the current property. posts_per_page=3; post__not_in is one property ID; metadata/taxonomy match the recommendation rules.
870 + 'post__not_in' => array($property_id),
871 + );
600 872
601 - $meta_query = array();
873 + $meta_query = array();
602 874
603 - $meta_query[] = array(
604 - 'key' => '_department',
605 - 'value' => $department,
606 - );
875 + $meta_query[] = array(
876 + 'key' => '_department',
877 + 'value' => $department,
878 + );
607 879
608 - $meta_query[] = array(
609 - 'key' => '_on_market',
610 - 'value' => 'yes',
611 - );
880 + $meta_query[] = array(
881 + 'key' => '_on_market',
882 + 'value' => 'yes',
883 + );
612 884
613 - $meta_query[] = array(
614 - 'key' => '_bedrooms',
615 - 'value' => $bedrooms,
616 - 'type' => 'NUMERIC'
617 - );
885 + if ( $department != 'commercial' && ph_get_custom_department_based_on( $department ) != 'commercial' )
886 + {
887 + $bedrooms = get_post_meta( $property_id, '_bedrooms', TRUE );
618 888
619 - $meta_query[] = array(
620 - 'key' => '_price_actual',
621 - 'value' => $lower_price,
622 - 'compare' => '>=',
623 - 'type' => 'NUMERIC'
624 - );
889 + $meta_query[] = array(
890 + 'key' => '_bedrooms',
891 + 'value' => $bedrooms,
892 + 'type' => 'NUMERIC'
893 + );
625 894
626 - $meta_query[] = array(
627 - 'key' => '_price_actual',
628 - 'value' => $higher_price,
629 - 'compare' => '<=',
630 - 'type' => 'NUMERIC'
631 - );
632 895
633 - $args['meta_query'] = $meta_query;
896 + $price = get_post_meta( $property_id, '_price_actual', true );
897 + $lower_price = $price - ($price / 10);
898 + $higher_price = $price + ($price / 10);
634 899
635 - $properties_query = new WP_Query( apply_filters( 'propertyhive_auto_responder_similar_properties_query', $args, $property_id ) );
900 + $meta_query[] = array(
901 + 'key' => '_price_actual',
902 + 'value' => $lower_price,
903 + 'compare' => '>=',
904 + 'type' => 'NUMERIC'
905 + );
636 906
637 - if ( $properties_query->have_posts() )
638 - {
639 - $similar_html = '<br><hr><br><h3>Similar Properties You Might Like:</h3>';
907 + $meta_query[] = array(
908 + 'key' => '_price_actual',
909 + 'value' => $higher_price,
910 + 'compare' => '<=',
911 + 'type' => 'NUMERIC'
912 + );
913 + }
914 + else
915 + {
916 + $for_sale = get_post_meta( $property_id, '_for_sale', true );
917 + $to_rent = get_post_meta( $property_id, '_to_rent', true );
640 918
641 - while ( $properties_query->have_posts() )
919 + if ( $for_sale == 'yes' )
920 + {
921 + $meta_query[] = array(
922 + 'key' => '_for_sale',
923 + 'value' => 'yes',
924 + );
925 + }
926 + elseif ( $to_rent == 'yes' )
927 + {
928 + $meta_query[] = array(
929 + 'key' => '_to_rent',
930 + 'value' => 'yes',
931 + );
932 + }
933 + }
934 +
935 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Each email’s similar-property query returns three properties and excludes the current property. posts_per_page=3; post__not_in is one property ID; metadata/taxonomy match the recommendation rules.
936 + $args['meta_query'] = $meta_query;
937 +
938 + $property_match_statuses = get_option( 'propertyhive_property_match_statuses', '' );
939 + if ( $property_match_statuses != '' && is_array($property_match_statuses) && !empty($property_match_statuses) )
642 940 {
643 - $properties_query->the_post();
941 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Each email’s similar-property query returns three properties and excludes the current property. posts_per_page=3; post__not_in is one property ID; metadata/taxonomy match the recommendation rules.
942 + $args['tax_query'] = array(
943 + array(
944 + 'taxonomy' => 'availability',
945 + 'field' => 'term_id',
946 + 'terms' => $property_match_statuses,
947 + 'operator' => 'IN',
948 + )
949 + );
950 + }
644 951
645 - $property = new PH_Property( get_the_ID() );
952 + $properties_query = new WP_Query( apply_filters( 'propertyhive_auto_responder_similar_properties_query', $args, $property_id ) );
646 953
647 - $similar_html .= '<table width="100%" cellpadding="5" cellspacing="0">';
648 - $similar_html .= '<tr>';
649 - $similar_html .= '<td width="20%" valign="top">';
650 - $image = $property->get_main_photo_src();
651 - if ( $image !== false )
954 + if ( $properties_query->have_posts() )
955 + {
956 + while ( $properties_query->have_posts() )
652 957 {
653 - $similar_html .= '<a href="' . get_permalink() . '"><img src="' . $image . '" alt="' . get_the_title() . '"></a>';
958 + $properties_query->the_post();
959 +
960 + $property = new PH_Property( get_the_ID() );
961 +
962 + ob_start();
963 +
964 + ph_get_template( 'emails/enquiry-autoresponder-similar-property.php', array( 'property' => $property ) );
965 +
966 + $similar_html .= ob_get_clean();
654 967 }
655 - $similar_html .= '</td>';
656 - $similar_html .= '<td valign="top" class="text">';
657 - $similar_html .= '<p style="margin-bottom:8px !important;"><strong><a href="' . get_permalink() . '">' . get_the_title() . '</a></strong></p>';
658 - $similar_html .= '<p style="margin-bottom:8px !important; font-size:14px;"><strong>' . $property->get_formatted_price() . '</strong></p>';
659 - $similar_html .= '<p style="margin-bottom:8px !important; font-size:14px;">' . $property->bedrooms . ' bed ' . $property->property_type . ' ' . $property->availability . '</p>';
660 - if ( strip_tags($property->post_excerpt) != '' )
661 - {
662 - $similar_html .= '<p style="margin-bottom:0 !important; font-size:14px;">' . substr(strip_tags($property->post_excerpt), 0, 300);
663 - if ( strlen(strip_tags($property->post_excerpt)) > 300 ) { $similar_html .= '...'; }
664 - $similar_html .= '</p>';
665 - }
666 - $similar_html .= '</td>';
667 - $similar_html .= '</tr>';
668 - $similar_html .= '</table><br>';
669 968 }
670 969 }
970 + if ( !empty($similar_html) )
971 + {
972 + $similar_html = '<br><hr><br><h3>Similar Properties You Might Like:</h3>' . $similar_html;
973 + }
671 974 $body = str_replace( "[similar_properties]", $similar_html, $body );
672 975 }
673 976
977 + $body = apply_filters( 'propertyhive_enquiry_auto_responder_body', $body, $property_ids[0] );
674 978 $body = apply_filters( 'propertyhive_mail_content', $this->style_inline( $this->wrap_message( $body ) ) );
675 979
676 980 wp_mail( $to, $subject, $body, $headers );
677 981 }
@@ -676,5 +980,5 @@
676 980 wp_mail( $to, $subject, $body, $headers );
677 981 }
678 982 }
679 983 }
680 -}
984 +}