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 +574 -174 1.4.62.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.
@@ -70,37 +73,66 @@
70 73
71 74 // Send applicant registration email
72 75 add_action( 'propertyhive_applicant_registered', array( $this, 'send_applicant_registration_alert' ), 10, 2 );
73 76
74 - add_action( 'admin_init', array( $this, 'run_custom_email_log_cron' ), 10, 1 );
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
77 - public function run_custom_email_log_cron()
82 + public function check_email_queue_is_scheduled()
78 83 {
79 - if( isset($_GET['custom_email_log_cron']) )
84 + $schedule = wp_get_schedule( 'propertyhive_process_email_log' );
85 +
86 + if ( $schedule === FALSE )
80 87 {
81 - do_action($_GET['custom_email_log_cron']);
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' );
82 94 }
83 95 }
84 96
85 - public function send_applicant_registration_alert( $contact_post_id, $user_id )
97 + public function run_custom_email_cron()
86 98 {
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 - )
99 + if ( isset( $_GET['custom_email_log_cron'] ) )
100 + {
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 );
111 + }
112 + }
113 +
114 + public function send_applicant_registration_alert( $contact_post_id, $user_id )
94 115 {
95 - $to = get_post_meta( $_POST['office_id'], '_office_email_address_' . str_replace("residential-", "", $_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,12 +142,14 @@
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
150 + $form_controls = apply_filters( 'propertyhive_applicant_registration_form_fields', $form_controls );
151 +
118 152 unset($form_controls['office_id']);
119 153
120 154 foreach ($form_controls as $key => $control)
121 155 {
@@ -123,10 +157,35 @@
123 157 {
124 158 continue;
125 159 }
126 160
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) )
165 + {
166 + if ( !is_array($value) ) { $value = array($value); }
167 +
168 + foreach ( $value as $value_term )
169 + {
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);
179 + }
180 +
181 + if ( ph_clean($value) == '' )
182 + {
183 + continue;
184 + }
185 +
127 186 $label = ( isset($control['label']) ) ? $control['label'] : $key;
128 - $message .= $label . ": " . $_POST[$key] . "\n";
187 + $message .= $label . ": " . $value . "\n";
129 188 }
130 189
131 190 wp_mail($to, $subject, $message);
132 191 }
@@ -140,67 +199,91 @@
140 199 global $wpdb;
141 200
142 201 $lock_id = uniqid( "", true );
143 202
144 - $wpdb->query("
145 - UPDATE " . $wpdb->prefix . "ph_email_log
146 - SET
147 - status = 'fail2',
148 - lock_id = ''
149 - WHERE
150 - status = 'fail1'
151 - AND
152 - lock_id <> ''
153 - AND
154 - locked_at <= '" . date("Y-m-d H:i:s", strtotime('24 hours ago')) . "'
155 - ");
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 + ) );
156 214
157 - $wpdb->query("
158 - UPDATE " . $wpdb->prefix . "ph_email_log
159 - SET
160 - status = 'fail1',
161 - lock_id = ''
162 - WHERE
163 - status = ''
164 - AND
165 - lock_id <> ''
166 - AND
167 - locked_at <= '" . date("Y-m-d H:i:s", strtotime('24 hours ago')) . "'
168 - ");
169 -
170 - // Lock/reserve all emails in log that are status blank or 'fail1' and lock_id blank and send_at in the past
171 - // Only grab 5 at a time to prevent hanging/being seen as spamming
172 - $wpdb->query("
173 - UPDATE " . $wpdb->prefix . "ph_email_log
174 - SET
175 - lock_id = '" . $lock_id . "',
176 - locked_at = '" . date("Y-m-d H:i:s") . "'
177 - WHERE
178 - (status = '' OR status = 'fail1')
179 - AND
180 - lock_id = ''
181 - AND
182 - send_at <= '" . date("Y-m-d H:i:s") . "'
183 - LIMIT 5
184 - ");
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 + ) );
185 228
186 - // We now have up to 5 emails locked. Get this 5 and attempt to send
187 - $emails_to_send = $wpdb->get_results("
188 - SELECT *
189 - FROM " . $wpdb->prefix . "ph_email_log
190 - WHERE
191 - lock_id = '" . $lock_id . "'
192 - ");
193 -
194 229 foreach ( $emails_to_send as $email_to_send )
195 230 {
196 231 $email_id = $email_to_send->email_id;
197 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 +
198 266 $headers = array();
199 - $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 + }
200 276 $headers[] = 'Content-Type: text/html; charset=UTF-8';
201 277
202 - $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 ) ) );
203 286
204 287 $sent = wp_mail(
205 288 $email_to_send->to_email_address,
206 289 $email_to_send->subject,
@@ -226,17 +309,33 @@
226 309 {
227 310 $new_status = 'fail2';
228 311 }
229 312 }
230 - $wpdb->query("
231 - UPDATE " . $wpdb->prefix . "ph_email_log
232 - SET
233 - status = '" . $new_status . "',
234 - lock_id = ''
235 - WHERE
236 - email_id = '" . $email_id . "'
237 - ");
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 + );
238 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 + ) );
239 338 }
240 339
241 340 /*
242 341 * Automatically send new properties to registered applicants
@@ -244,8 +343,14 @@
244 343 public function ph_auto_email_match()
245 344 {
246 345 global $post;
247 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 +
248 353 // Auto emails enabled in settings
249 354 // Auto emails not disabled in applicant record
250 355 // Property added more recently that setting enabled in settings
251 356 // Property not already previously sent
@@ -253,8 +358,10 @@
253 358 // 'Do not email' not selected
254 359
255 360 $auto_property_match_enabled = get_option( 'propertyhive_auto_property_match', '' );
256 361
362 + if ( $dry_run === true ) { echo 'Auto-match setting enabled: ' . esc_html( $auto_property_match_enabled ) . "<br>\n"; }
363 +
257 364 if ( $auto_property_match_enabled == '' )
258 365 {
259 366 return false;
260 367 }
@@ -260,37 +367,133 @@
260 367 }
261 368
262 369 $auto_property_match_enabled_date = get_option( 'propertyhive_auto_property_match_enabled_date', '' );
263 370
371 + if ( $dry_run === true ) { echo 'Auto-match setting enabled date: ' . esc_html( $auto_property_match_enabled_date ) . "<br>\n"; }
372 +
264 373 if ( $auto_property_match_enabled_date == '' )
265 374 {
266 375 return false;
267 376 }
268 377
378 + // Get all of the offices and store them in an array to save having to query them for every email
379 + $args = array(
380 + 'post_type' => 'office',
381 + 'nopaging' => true
382 + );
383 +
384 + $office_names = array();
385 + $office_email_addresses = array();
386 + $office_query = new WP_Query( $args );
387 +
388 + if ( $office_query->have_posts() )
389 + {
390 + while ( $office_query->have_posts() )
391 + {
392 + $office_query->the_post();
393 +
394 + $office_names[get_the_ID()] = get_the_title( get_the_ID() );
395 +
396 + $office_email_addresses['residential-sales'][get_the_ID()] = get_post_meta( get_the_ID(), '_office_email_address_sales', TRUE );
397 + $office_email_addresses['residential-lettings'][get_the_ID()] = get_post_meta( get_the_ID(), '_office_email_address_lettings', TRUE );
398 + $office_email_addresses['commercial'][get_the_ID()] = get_post_meta( get_the_ID(), '_office_email_address_commercial', TRUE );
399 + }
400 + }
401 +
402 + wp_reset_postdata();
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 +
269 431 include_once( dirname(__FILE__) . '/admin/class-ph-admin-matching-properties.php' );
270 432 $ph_admin_matching_properties = new PH_Admin_Matching_Properties();
271 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 +
272 453 // Get all contacts that have a type of applicant
273 454 $args = array(
274 455 'post_type' => 'contact',
456 + 'post_status' => 'publish',
275 457 'nopaging' => true,
276 - 'meta_query' => array(
277 - array(
278 - 'key' => '_contact_types',
279 - 'value' => 'applicant',
280 - 'compare' => 'LIKE'
281 - )
282 - ),
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,
283 460 'fields' => 'ids'
284 461 );
285 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 +
286 465 $contact_query = new WP_Query( $args );
287 466
467 + if ( $dry_run === true ) { echo 'Found ' . esc_html( $contact_query->found_posts ) . ' contacts' . "<br>\n"; }
468 +
288 469 if ( $contact_query->have_posts() )
289 470 {
290 471 $default_subject = get_option( 'propertyhive_property_match_default_email_subject', '' );
291 472 $default_body = get_option( 'propertyhive_property_match_default_email_body', '' );
292 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 +
293 496 while ( $contact_query->have_posts() )
294 497 {
295 498 $contact_query->the_post();
296 499
@@ -295,11 +498,15 @@
295 498 $contact_query->the_post();
296 499
297 500 $contact_id = get_the_ID();
298 501
502 + if ( $dry_run === true ) { echo 'Doing contact: ' . esc_html( get_the_title() ) . "<br>\n"; }
503 +
299 504 // invalid email address
300 505 if ( strpos( get_post_meta( $contact_id, '_email_address', TRUE ), '@' ) === FALSE )
301 506 {
507 + if ( $dry_run === true ) { echo esc_html('Invalid email address. Skipping') . "<br>\n"; }
508 +
302 509 continue;
303 510 }
304 511
305 512 // email in the list of forbidden contact methods
@@ -305,8 +512,10 @@
305 512 // email in the list of forbidden contact methods
306 513 $forbidden_contact_methods = get_post_meta( $contact_id, '_forbidden_contact_methods', TRUE );
307 514 if ( is_array($forbidden_contact_methods) && in_array('email', $forbidden_contact_methods) )
308 515 {
516 + if ( $dry_run === true ) { echo esc_html('Email communication forbidden in contact preferences. Skipping') . "<br>\n"; }
517 +
309 518 continue;
310 519 }
311 520
312 521 $applicant_profiles = get_post_meta( $contact_id, '_applicant_profiles', TRUE );
@@ -313,9 +522,15 @@
313 522
314 523 if ( $applicant_profiles != '' && $applicant_profiles > 0 )
315 524 {
316 525 $dismissed_properties = get_post_meta( $contact_id, '_dismissed_properties', TRUE );
526 + if ( $dismissed_properties == '' )
527 + {
528 + $dismissed_properties = array();
529 + }
317 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 +
318 533 for ( $i = 0; $i < $applicant_profiles; ++$i )
319 534 {
320 535 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
321 536
@@ -320,27 +535,77 @@
320 535 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
321 536
322 537 if ( $applicant_profile == '' || !is_array($applicant_profile) || !isset($applicant_profile['department']) )
323 538 {
539 + if ( $dry_run === true ) { echo esc_html('Applicant relationship empty or no department set') . "<br>\n"; }
324 540 continue;
325 541 }
326 542
327 - 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' ) )
328 544 {
545 + if ( $dry_run === true ) { echo esc_html('Send matching properties disabled') . "<br>\n"; }
329 546 continue;
330 547 }
331 548
332 549 if ( isset($applicant_profile['auto_match_disabled']) && $applicant_profile['auto_match_disabled'] == 'yes' )
333 550 {
551 + if ( $dry_run === true ) { echo esc_html('Auto match disabled') . "<br>\n"; }
334 552 continue;
335 553 }
336 554
555 + if ( $dry_run === true ) { echo 'Getting matching properties' . "<br>\n"; }
556 +
337 557 $matching_properties = $ph_admin_matching_properties->get_matching_properties( $contact_id, $i, $auto_property_match_enabled_date );
338 558
559 + if ( $dry_run === true ) { echo esc_html('Found ' . count($matching_properties) . ' matching properties') . "<br>\n"; }
560 +
339 561 if ( !empty($matching_properties) )
340 562 {
341 563 $already_sent_properties = get_post_meta( $contact_id, '_applicant_profile_' . $i . '_match_history', TRUE );
342 564
565 + // Remove from this array if on market changed or price changed
566 + if ( is_array($already_sent_properties) )
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 +
570 + foreach ( $already_sent_properties as $already_sent_property_id => $sends )
571 + {
572 + $highest_send = $sends[count($sends) - 1]['date'];
573 +
574 + if ( $highest_send != '' )
575 + {
576 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last sent: ' . esc_html( $highest_send ) . "<br>\n"; }
577 +
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 +
582 + $price_change_date = get_post_meta( $already_sent_property_id, '_price_change_date', TRUE );
583 +
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 )
587 + {
588 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' has changed on market since last sent' . "<br>\n"; }
589 +
590 + // This property has changed since it was last sent. Remove from already sent list so it gets sent again
591 + unset($already_sent_properties[$already_sent_property_id]);
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 + }
600 + }
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"; }
604 + }
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 +
343 608 // Check properties haven't already been sent and not marked as 'not interested'
344 609 $new_matching_properties = array();
345 610 foreach ($matching_properties as $matching_property)
346 611 {
@@ -349,23 +614,52 @@
349 614 $new_matching_properties[] = $matching_property->id;
350 615 }
351 616 }
352 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 +
353 628 if ( !empty($new_matching_properties) )
354 629 {
355 630 $subject = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $default_subject);
356 631
357 - $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 );
358 635 $body = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $body);
359 636
637 + $office_counts = array();
638 + $negotiator_counts = array();
639 +
360 640 if ( strpos($body, '[properties]') !== FALSE )
361 641 {
362 642 ob_start();
643 +
363 644 if ( !empty($new_matching_properties) )
364 645 {
365 646 foreach ( $new_matching_properties as $email_property_id )
366 647 {
367 648 $property = new PH_Property((int)$email_property_id);
649 +
650 + if ( $property->office_id != '' && $property->office_id != 0 )
651 + {
652 + if ( !isset($office_counts[$property->office_id]) ) { $office_counts[$property->office_id] = 0; }
653 + ++$office_counts[$property->office_id];
654 + }
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 +
368 662 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
369 663 }
370 664 }
371 665 $body = str_replace("[properties]", ob_get_clean(), $body);
@@ -370,24 +664,73 @@
370 664 }
371 665 $body = str_replace("[properties]", ob_get_clean(), $body);
372 666 }
373 667
374 - $ph_admin_matching_properties->send_emails(
375 - $contact_id,
376 - $i,
377 - $new_matching_properties,
378 - get_bloginfo('name'),
379 - get_option('admin_email'),
380 - $subject,
381 - $body
382 - );
668 + // Get email address of office with most properties
669 + $highest_office_name = '';
670 + $highest_office_email_address = '';
671 + if ( !empty($office_counts) )
672 + {
673 + arsort($office_counts);
674 + reset($office_counts);
675 + $highest_office_id = key($office_counts);
676 + $highest_office_name = ( isset($office_names[$highest_office_id]) ? $office_names[$highest_office_id] : '' );
677 + $highest_office_email_address = ( isset($office_email_addresses[$applicant_profile['department']][$highest_office_id]) ? $office_email_addresses[$applicant_profile['department']][$highest_office_id] : '' );
678 + }
679 + if ( $highest_office_email_address == '' )
680 + {
681 + // fallback to admin email address
682 + $highest_office_email_address = get_option('admin_email');
683 + }
684 +
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 + }
383 720 }
384 721 }
385 722 }
386 723 }
724 + else
725 + {
726 + if ( $dry_run === true ) { echo esc_html('No applicant profiles found. Skipping') . "<br>\n"; }
727 + }
387 728 }
388 729 }
389 730
731 + if ( $dry_run === true ) { echo esc_html('Finished auto-match process') . "<br>\n"; die(); }
732 +
390 733 wp_reset_postdata();
391 734 }
392 735
393 736 /**
@@ -393,12 +736,9 @@
393 736 /**
394 737 * Init email classes.
395 738 */
396 739 public function init() {
397 - // include css inliner
398 - if ( ! class_exists( 'Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
399 - include_once( dirname( __FILE__ ) . '/libraries/class-emogrifier.php' );
400 - }
740 +
401 741 }
402 742
403 743 /**
404 744 * Apply inline styles to dynamic content.
@@ -411,14 +751,20 @@
411 751 if ( class_exists( 'DOMDocument' ) ) {
412 752 ob_start();
413 753 ph_get_template( 'emails/email-styles.php' );
414 754 $css = apply_filters( 'propertyhive_email_styles', ob_get_clean() );
755 +
756 + // include css inliner
757 + if ( ! class_exists( 'PropertyHive_Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
758 + include_once( dirname( __FILE__ ) . '/libraries/class-emogrifier.php' );
759 + }
760 +
415 761 // apply CSS styles inline for picky email clients
416 762 try {
417 - $emogrifier = new Emogrifier( $content, $css );
763 + $emogrifier = new PropertyHive_Emogrifier( $content, $css );
418 764 $content = $emogrifier->emogrify();
419 765 } catch ( Exception $e ) {
420 - 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()));
421 767 }
422 768 }
423 769 return $content;
424 770 }
@@ -436,9 +782,9 @@
436 782 public function email_footer( $contact_id = '' ) {
437 783 $unsubscribe_link = '';
438 784 if ($contact_id != '')
439 785 {
440 - $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 );
441 787 }
442 788
443 789 ph_get_template( 'emails/email-footer.php', array( 'unsubscribe_link' => $unsubscribe_link ) );
444 790 }
@@ -455,8 +801,9 @@
455 801 ob_start();
456 802
457 803 do_action( 'propertyhive_email_header', $contact_id );
458 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.
459 806 echo wpautop( wptexturize( $message ) );
460 807
461 808 do_action( 'propertyhive_email_footer', $contact_id );
462 809
@@ -467,12 +814,26 @@
467 814 }
468 815
469 816 public function send_enquiry_auto_responder( $data = array() )
470 817 {
471 - 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'] )
472 824 {
473 - $property_id = $data['property_id'];
474 - $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'] ) : '';
475 836 $subject = get_option( 'propertyhive_enquiry_auto_responder_email_subject', '' );
476 837 $body = get_option( 'propertyhive_enquiry_auto_responder_email_body', '' );
477 838
478 839 if ( $to != '' && $subject != '' && $body != '' )
@@ -477,105 +838,144 @@
477 838
478 839 if ( $to != '' && $subject != '' && $body != '' )
479 840 {
480 841 $headers = array();
481 - $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' ) ) . '>';
482 843 $headers[] = 'Content-Type: text/html; charset=UTF-8';
483 844
484 - $body = str_replace( "[property_address_hyperlinked]", '<a href="' . get_permalink($property_id) . '">' . get_the_title($property_id) . '</a>', $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 );
485 847
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 );
854 +
486 855 if ( strpos( $body, '[similar_properties]' ) !== FALSE )
487 856 {
488 857 $similar_html = '';
489 858
490 - $department = get_post_meta( $property_id, '_department', TRUE );
491 - $bedrooms = get_post_meta( $property_id, '_bedrooms', TRUE );
492 - $price = get_post_meta( $property_id, '_price_actual', true );
493 - $lower_price = $price - ($price / 10);
494 - $higher_price = $price + ($price / 10);
859 + foreach ( $property_ids as $property_id )
860 + {
861 + $department = get_post_meta( $property_id, '_department', TRUE );
495 862
496 - // Get two similar properties
497 - $args = array(
498 - 'post_type' => 'property',
499 - 'post_status' => 'publish',
500 - 'posts_per_page' => 3,
501 - 'orderby' => 'rand',
502 - 'post__not_in' => array($property_id),
503 - );
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 + );
504 872
505 - $meta_query = array();
873 + $meta_query = array();
506 874
507 - $meta_query[] = array(
508 - 'key' => '_department',
509 - 'value' => $department,
510 - );
875 + $meta_query[] = array(
876 + 'key' => '_department',
877 + 'value' => $department,
878 + );
511 879
512 - $meta_query[] = array(
513 - 'key' => '_on_market',
514 - 'value' => 'yes',
515 - );
880 + $meta_query[] = array(
881 + 'key' => '_on_market',
882 + 'value' => 'yes',
883 + );
516 884
517 - $meta_query[] = array(
518 - 'key' => '_bedrooms',
519 - 'value' => $bedrooms,
520 - 'type' => 'NUMERIC'
521 - );
885 + if ( $department != 'commercial' && ph_get_custom_department_based_on( $department ) != 'commercial' )
886 + {
887 + $bedrooms = get_post_meta( $property_id, '_bedrooms', TRUE );
522 888
523 - $meta_query[] = array(
524 - 'key' => '_price_actual',
525 - 'value' => $lower_price,
526 - 'compare' => '>=',
527 - 'type' => 'NUMERIC'
528 - );
889 + $meta_query[] = array(
890 + 'key' => '_bedrooms',
891 + 'value' => $bedrooms,
892 + 'type' => 'NUMERIC'
893 + );
529 894
530 - $meta_query[] = array(
531 - 'key' => '_price_actual',
532 - 'value' => $higher_price,
533 - 'compare' => '<=',
534 - 'type' => 'NUMERIC'
535 - );
536 895
537 - $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);
538 899
539 - $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 + );
540 906
541 - if ( $properties_query->have_posts() )
542 - {
543 - $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 );
544 918
545 - 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) )
546 940 {
547 - $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 + }
548 951
549 - $property = new PH_Property( get_the_ID() );
952 + $properties_query = new WP_Query( apply_filters( 'propertyhive_auto_responder_similar_properties_query', $args, $property_id ) );
550 953
551 - $similar_html .= '<table width="100%" cellpadding="5" cellspacing="0">';
552 - $similar_html .= '<tr>';
553 - $similar_html .= '<td width="20%" valign="top">';
554 - $image = $property->get_main_photo_src();
555 - if ( $image !== false )
954 + if ( $properties_query->have_posts() )
955 + {
956 + while ( $properties_query->have_posts() )
556 957 {
557 - $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();
558 967 }
559 - $similar_html .= '</td>';
560 - $similar_html .= '<td valign="top" class="text">';
561 - $similar_html .= '<p style="margin-bottom:8px !important;"><strong><a href="' . get_permalink() . '">' . get_the_title() . '</a></strong></p>';
562 - $similar_html .= '<p style="margin-bottom:8px !important; font-size:14px;"><strong>' . $property->get_formatted_price() . '</strong></p>';
563 - $similar_html .= '<p style="margin-bottom:8px !important; font-size:14px;">' . $property->bedrooms . ' bed ' . $property->property_type . ' ' . $property->availability . '</p>';
564 - if ( strip_tags($property->post_excerpt) != '' )
565 - {
566 - $similar_html .= '<p style="margin-bottom:0 !important; font-size:14px;">' . substr(strip_tags($property->post_excerpt), 0, 300);
567 - if ( strlen(strip_tags($property->post_excerpt)) > 300 ) { $similar_html .= '...'; }
568 - $similar_html .= '</p>';
569 - }
570 - $similar_html .= '</td>';
571 - $similar_html .= '</tr>';
572 - $similar_html .= '</table><br>';
573 968 }
574 969 }
970 + if ( !empty($similar_html) )
971 + {
972 + $similar_html = '<br><hr><br><h3>Similar Properties You Might Like:</h3>' . $similar_html;
973 + }
575 974 $body = str_replace( "[similar_properties]", $similar_html, $body );
576 975 }
577 976
977 + $body = apply_filters( 'propertyhive_enquiry_auto_responder_body', $body, $property_ids[0] );
578 978 $body = apply_filters( 'propertyhive_mail_content', $this->style_inline( $this->wrap_message( $body ) ) );
579 979
580 980 wp_mail( $to, $subject, $body, $headers );
581 981 }
@@ -580,5 +980,5 @@
580 980 wp_mail( $to, $subject, $body, $headers );
581 981 }
582 982 }
583 983 }
584 -}
984 +}