PluginProbe
Property Hive / 2.3.0
Property Hive v2.3.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 1.4.61 All 261 releases
← All changes | includes/class-ph-emails.php +540 -174 1.4.52.3.0 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,58 @@
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
198 233 $headers = array();
199 - $headers[] = 'From: ' . $email_to_send->from_name . ' <' . $email_to_send->from_email_address . '>';
234 + $headers[] = 'From: ' . html_entity_decode($email_to_send->from_name) . ' <' . $email_to_send->from_email_address . '>';
235 + if ( $email_to_send->cc_email_address != '' )
236 + {
237 + $headers[] = 'Cc: ' . $email_to_send->cc_email_address;
238 + }
239 + if ( $email_to_send->bcc_email_address != '' )
240 + {
241 + $headers[] = 'Bcc: ' . $email_to_send->bcc_email_address;
242 + }
200 243 $headers[] = 'Content-Type: text/html; charset=UTF-8';
201 244
202 - $body = apply_filters( 'propertyhive_mail_content', $this->style_inline( $this->wrap_message( $email_to_send->body, $email_to_send->contact_id ) ) );
245 + $body = $email_to_send->body;
246 +
247 + if ( extension_loaded('zlib') && @gzuncompress($body) !== false )
248 + {
249 + $body = gzuncompress($body);
250 + }
251 +
252 + $body = apply_filters( 'propertyhive_mail_content', $this->style_inline( $this->wrap_message( $body, $email_to_send->contact_id ) ) );
203 253
204 254 $sent = wp_mail(
205 255 $email_to_send->to_email_address,
206 256 $email_to_send->subject,
@@ -226,17 +276,33 @@
226 276 {
227 277 $new_status = 'fail2';
228 278 }
229 279 }
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 - ");
280 + // 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.
281 + $wpdb->update(
282 + $wpdb->prefix . 'ph_email_log',
283 + array( 'status' => $new_status, 'lock_id' => '' ),
284 + array( 'email_id' => (int) $email_id ),
285 + array( '%s', '%s' ),
286 + array( '%d' )
287 + );
238 288 }
289 +
290 + // Delete old logs
291 + $keep_logs_days = (string)apply_filters( 'propertyhive_keep_email_logs_days', '3650' ); // 10 years
292 +
293 + // Revert back to 3650 days if anything other than numbers has been passed
294 + // This prevent SQL injection and errors
295 + if ( !preg_match("/^\d+$/", $keep_logs_days) )
296 + {
297 + $keep_logs_days = '3650';
298 + }
299 +
300 + // 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.
301 + $wpdb->query( $wpdb->prepare(
302 + "DELETE FROM {$wpdb->prefix}ph_email_log WHERE send_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
303 + (int) $keep_logs_days
304 + ) );
239 305 }
240 306
241 307 /*
242 308 * Automatically send new properties to registered applicants
@@ -244,8 +310,14 @@
244 310 public function ph_auto_email_match()
245 311 {
246 312 global $post;
247 313
314 + // 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().
315 + $request_get = wp_unslash( $_GET );
316 + $dry_run = isset( $request_get['dry_run'] );
317 +
318 + if ( $dry_run === true ) { echo 'Running auto-match in dry run mode. Logging will be output and no emails will be sent.' . "<br>\n"; }
319 +
248 320 // Auto emails enabled in settings
249 321 // Auto emails not disabled in applicant record
250 322 // Property added more recently that setting enabled in settings
251 323 // Property not already previously sent
@@ -253,8 +325,10 @@
253 325 // 'Do not email' not selected
254 326
255 327 $auto_property_match_enabled = get_option( 'propertyhive_auto_property_match', '' );
256 328
329 + if ( $dry_run === true ) { echo 'Auto-match setting enabled: ' . esc_html( $auto_property_match_enabled ) . "<br>\n"; }
330 +
257 331 if ( $auto_property_match_enabled == '' )
258 332 {
259 333 return false;
260 334 }
@@ -260,37 +334,132 @@
260 334 }
261 335
262 336 $auto_property_match_enabled_date = get_option( 'propertyhive_auto_property_match_enabled_date', '' );
263 337
338 + if ( $dry_run === true ) { echo 'Auto-match setting enabled date: ' . esc_html( $auto_property_match_enabled_date ) . "<br>\n"; }
339 +
264 340 if ( $auto_property_match_enabled_date == '' )
265 341 {
266 342 return false;
267 343 }
268 344
345 + // Get all of the offices and store them in an array to save having to query them for every email
346 + $args = array(
347 + 'post_type' => 'office',
348 + 'nopaging' => true
349 + );
350 +
351 + $office_names = array();
352 + $office_email_addresses = array();
353 + $office_query = new WP_Query( $args );
354 +
355 + if ( $office_query->have_posts() )
356 + {
357 + while ( $office_query->have_posts() )
358 + {
359 + $office_query->the_post();
360 +
361 + $office_names[get_the_ID()] = get_the_title( get_the_ID() );
362 +
363 + $office_email_addresses['residential-sales'][get_the_ID()] = get_post_meta( get_the_ID(), '_office_email_address_sales', TRUE );
364 + $office_email_addresses['residential-lettings'][get_the_ID()] = get_post_meta( get_the_ID(), '_office_email_address_lettings', TRUE );
365 + $office_email_addresses['commercial'][get_the_ID()] = get_post_meta( get_the_ID(), '_office_email_address_commercial', TRUE );
366 + }
367 + }
368 +
369 + wp_reset_postdata();
370 +
371 + // Get all of the negotiators and store them in an array to save having to query them for every email
372 + $negotiator_names = array();
373 + $negotiator_email_addresses = array();
374 +
375 + $args = array(
376 + 'number' => 9999,
377 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy Property Negotiator compatibility filter; existing role filters depend on this exact public hook name.
378 + 'role__not_in' => apply_filters( 'property_negotiator_exclude_roles', array('property_hive_contact', 'subscriber') ),
379 + 'fields' => array( 'ID', 'display_name', 'user_email' )
380 + );
381 +
382 + $args = apply_filters( 'propertyhive_negotiators_query', $args );
383 +
384 + $user_query = new WP_User_Query( $args );
385 +
386 + $negotiators = array();
387 +
388 + if ( ! empty( $user_query->results ) )
389 + {
390 + foreach ( $user_query->results as $user )
391 + {
392 + $negotiator_names[$user->ID] = $user->display_name;
393 +
394 + $negotiator_email_addresses[$user->ID] = $user->user_email;
395 + }
396 + }
397 +
269 398 include_once( dirname(__FILE__) . '/admin/class-ph-admin-matching-properties.php' );
270 399 $ph_admin_matching_properties = new PH_Admin_Matching_Properties();
271 400
401 + $meta_query = array(
402 + array(
403 + 'key' => '_contact_types',
404 + 'value' => 'applicant',
405 + 'compare' => 'LIKE'
406 + )
407 + );
408 +
409 + if ( version_compare( get_bloginfo( 'version' ), '5.1', '>=' ) )
410 + {
411 + // If WP version contains compare_key, check contact has at least one applicant profile with Send Matching Properties checked
412 + $meta_query[] = array(
413 + 'key' => '_applicant_profile_',
414 + 'compare_key' => 'LIKE',
415 + 'value' => 'send_matching_properties";s:3:"yes"',
416 + 'compare' => 'LIKE',
417 + );
418 + }
419 +
272 420 // Get all contacts that have a type of applicant
273 421 $args = array(
274 422 'post_type' => 'contact',
275 423 'nopaging' => true,
276 - 'meta_query' => array(
277 - array(
278 - 'key' => '_contact_types',
279 - 'value' => 'applicant',
280 - 'compare' => 'LIKE'
281 - )
282 - ),
424 + // 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.
425 + 'meta_query' => $meta_query,
283 426 'fields' => 'ids'
284 427 );
285 428
429 + if ( $dry_run === true ) { echo 'Running query to get contacts with args: ' . esc_html( wp_json_encode( $args, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
430 +
286 431 $contact_query = new WP_Query( $args );
287 432
433 + if ( $dry_run === true ) { echo 'Found ' . esc_html( $contact_query->found_posts ) . ' contacts' . "<br>\n"; }
434 +
288 435 if ( $contact_query->have_posts() )
289 436 {
290 437 $default_subject = get_option( 'propertyhive_property_match_default_email_subject', '' );
291 438 $default_body = get_option( 'propertyhive_property_match_default_email_body', '' );
292 439
440 + $allowed_tags = array(
441 + 'strong' => array(),
442 + 'span' => array(),
443 + 'em' => array(),
444 + 'h1' => array(),
445 + 'h2' => array(),
446 + 'h3' => array(),
447 + 'h4' => array(),
448 + 'h5' => array(),
449 + 'h6' => array(),
450 + 'i' => array(),
451 + 'u' => array(),
452 + 'b' => array(),
453 + 'a' => array(
454 + 'href' => array(),
455 + 'target' => array(),
456 + ),
457 + );
458 + $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
459 +
460 + $default_body = wp_kses( $default_body, $allowed_tags );
461 +
293 462 while ( $contact_query->have_posts() )
294 463 {
295 464 $contact_query->the_post();
296 465
@@ -295,11 +464,15 @@
295 464 $contact_query->the_post();
296 465
297 466 $contact_id = get_the_ID();
298 467
468 + if ( $dry_run === true ) { echo 'Doing contact: ' . esc_html( get_the_title() ) . "<br>\n"; }
469 +
299 470 // invalid email address
300 471 if ( strpos( get_post_meta( $contact_id, '_email_address', TRUE ), '@' ) === FALSE )
301 472 {
473 + if ( $dry_run === true ) { echo esc_html('Invalid email address. Skipping') . "<br>\n"; }
474 +
302 475 continue;
303 476 }
304 477
305 478 // email in the list of forbidden contact methods
@@ -305,8 +478,10 @@
305 478 // email in the list of forbidden contact methods
306 479 $forbidden_contact_methods = get_post_meta( $contact_id, '_forbidden_contact_methods', TRUE );
307 480 if ( is_array($forbidden_contact_methods) && in_array('email', $forbidden_contact_methods) )
308 481 {
482 + if ( $dry_run === true ) { echo esc_html('Email communication forbidden in contact preferences. Skipping') . "<br>\n"; }
483 +
309 484 continue;
310 485 }
311 486
312 487 $applicant_profiles = get_post_meta( $contact_id, '_applicant_profiles', TRUE );
@@ -313,9 +488,15 @@
313 488
314 489 if ( $applicant_profiles != '' && $applicant_profiles > 0 )
315 490 {
316 491 $dismissed_properties = get_post_meta( $contact_id, '_dismissed_properties', TRUE );
492 + if ( $dismissed_properties == '' )
493 + {
494 + $dismissed_properties = array();
495 + }
317 496
497 + if ( $dry_run === true ) { if ( !empty($dismissed_properties) ) { echo 'Dismissed properties: ' . esc_html( wp_json_encode( $dismissed_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; } }
498 +
318 499 for ( $i = 0; $i < $applicant_profiles; ++$i )
319 500 {
320 501 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
321 502
@@ -320,27 +501,77 @@
320 501 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
321 502
322 503 if ( $applicant_profile == '' || !is_array($applicant_profile) || !isset($applicant_profile['department']) )
323 504 {
505 + if ( $dry_run === true ) { echo esc_html('Applicant relationship empty or no department set') . "<br>\n"; }
324 506 continue;
325 507 }
326 508
327 - if ( isset($applicant_profile['send_matching_properties']) && $applicant_profile['send_matching_properties'] == '' )
509 + if ( !isset($applicant_profile['send_matching_properties']) || ( isset($applicant_profile['send_matching_properties']) && $applicant_profile['send_matching_properties'] != 'yes' ) )
328 510 {
511 + if ( $dry_run === true ) { echo esc_html('Send matching properties disabled') . "<br>\n"; }
329 512 continue;
330 513 }
331 514
332 515 if ( isset($applicant_profile['auto_match_disabled']) && $applicant_profile['auto_match_disabled'] == 'yes' )
333 516 {
517 + if ( $dry_run === true ) { echo esc_html('Auto match disabled') . "<br>\n"; }
334 518 continue;
335 519 }
336 520
521 + if ( $dry_run === true ) { echo 'Getting matching properties' . "<br>\n"; }
522 +
337 523 $matching_properties = $ph_admin_matching_properties->get_matching_properties( $contact_id, $i, $auto_property_match_enabled_date );
338 524
525 + if ( $dry_run === true ) { echo esc_html('Found ' . count($matching_properties) . ' matching properties') . "<br>\n"; }
526 +
339 527 if ( !empty($matching_properties) )
340 528 {
341 529 $already_sent_properties = get_post_meta( $contact_id, '_applicant_profile_' . $i . '_match_history', TRUE );
342 530
531 + // Remove from this array if on market changed or price changed
532 + if ( is_array($already_sent_properties) )
533 + {
534 + if ( $dry_run === true ) { echo 'Already sent properties before: ' . esc_html( wp_json_encode( $already_sent_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
535 +
536 + foreach ( $already_sent_properties as $already_sent_property_id => $sends )
537 + {
538 + $highest_send = $sends[count($sends) - 1]['date'];
539 +
540 + if ( $highest_send != '' )
541 + {
542 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last sent: ' . esc_html( $highest_send ) . "<br>\n"; }
543 +
544 + $on_market_change_date = get_post_meta( $already_sent_property_id, '_on_market_change_date', TRUE );
545 +
546 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last on market change: ' . esc_html( $on_market_change_date ) . "<br>\n"; }
547 +
548 + $price_change_date = get_post_meta( $already_sent_property_id, '_price_change_date', TRUE );
549 +
550 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last price change: ' . esc_html( $price_change_date ) . "<br>\n"; }
551 +
552 + if ( $on_market_change_date > $highest_send )
553 + {
554 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' has changed on market since last sent' . "<br>\n"; }
555 +
556 + // This property has changed since it was last sent. Remove from already sent list so it gets sent again
557 + unset($already_sent_properties[$already_sent_property_id]);
558 + }
559 + elseif ( $price_change_date > $highest_send )
560 + {
561 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' has changed price since last sent' . "<br>\n"; }
562 +
563 + // This property has changed since it was last sent. Remove from already sent list so it gets sent again
564 + unset($already_sent_properties[$already_sent_property_id]);
565 + }
566 + }
567 + }
568 +
569 + if ( $dry_run === true ) { echo 'Already sent properties after: ' . esc_html( wp_json_encode( $already_sent_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
570 + }
571 +
572 + if ( $dry_run === true ) { echo 'Matching properties before removing already sent: ' . esc_html( wp_json_encode( $matching_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
573 +
343 574 // Check properties haven't already been sent and not marked as 'not interested'
344 575 $new_matching_properties = array();
345 576 foreach ($matching_properties as $matching_property)
346 577 {
@@ -349,23 +580,52 @@
349 580 $new_matching_properties[] = $matching_property->id;
350 581 }
351 582 }
352 583
584 + if ( $dry_run === true ) { echo 'Matching properties after removing already sent: ' . esc_html( wp_json_encode( $new_matching_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
585 +
586 + $max_results = apply_filters( 'propertyhive_auto_match_maximum_results', FALSE);
587 + if ( $max_results !== FALSE )
588 + {
589 + $new_matching_properties = array_slice($new_matching_properties, 0, (int)$max_results);
590 + }
591 +
592 + if ( $dry_run === true ) { echo esc_html('Found ' . count($new_matching_properties) . ' matching properties after removing already sent and dismissed') . "<br>\n"; }
593 +
353 594 if ( !empty($new_matching_properties) )
354 595 {
355 596 $subject = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $default_subject);
356 597
357 - $body = str_replace("[contact_name]", get_the_title($contact_id), $default_body);
598 + $contact = new PH_Contact($contact_id);
599 + $body = str_replace( '[contact_name]', esc_html( $contact->post_title ), $default_body );
600 + $body = str_replace( '[contact_dear]', esc_html( $contact->dear() ), $body );
358 601 $body = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $body);
359 602
603 + $office_counts = array();
604 + $negotiator_counts = array();
605 +
360 606 if ( strpos($body, '[properties]') !== FALSE )
361 607 {
362 608 ob_start();
609 +
363 610 if ( !empty($new_matching_properties) )
364 611 {
365 612 foreach ( $new_matching_properties as $email_property_id )
366 613 {
367 614 $property = new PH_Property((int)$email_property_id);
615 +
616 + if ( $property->office_id != '' && $property->office_id != 0 )
617 + {
618 + if ( !isset($office_counts[$property->office_id]) ) { $office_counts[$property->office_id] = 0; }
619 + ++$office_counts[$property->office_id];
620 + }
621 +
622 + if ( $property->negotiator_id != '' && $property->negotiator_id != 0 )
623 + {
624 + if ( !isset($negotiator_counts[$property->negotiator_id]) ) { $negotiator_counts[$property->negotiator_id] = 0; }
625 + ++$negotiator_counts[$property->negotiator_id];
626 + }
627 +
368 628 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
369 629 }
370 630 }
371 631 $body = str_replace("[properties]", ob_get_clean(), $body);
@@ -370,24 +630,73 @@
370 630 }
371 631 $body = str_replace("[properties]", ob_get_clean(), $body);
372 632 }
373 633
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 - );
634 + // Get email address of office with most properties
635 + $highest_office_name = '';
636 + $highest_office_email_address = '';
637 + if ( !empty($office_counts) )
638 + {
639 + arsort($office_counts);
640 + reset($office_counts);
641 + $highest_office_id = key($office_counts);
642 + $highest_office_name = ( isset($office_names[$highest_office_id]) ? $office_names[$highest_office_id] : '' );
643 + $highest_office_email_address = ( isset($office_email_addresses[$applicant_profile['department']][$highest_office_id]) ? $office_email_addresses[$applicant_profile['department']][$highest_office_id] : '' );
644 + }
645 + if ( $highest_office_email_address == '' )
646 + {
647 + // fallback to admin email address
648 + $highest_office_email_address = get_option('admin_email');
649 + }
650 +
651 + $body = str_replace( '[office_name]', esc_html( $highest_office_name ), $body );
652 + $body = str_replace( '[office_email_address]', esc_html( $highest_office_email_address ), $body );
653 +
654 + $highest_negotiator_name = '';
655 + $highest_negotiator_email_address = '';
656 + if ( !empty($negotiator_counts) )
657 + {
658 + arsort($negotiator_counts);
659 + reset($negotiator_counts);
660 + $highest_negotiator_id = key($negotiator_counts);
661 + $highest_negotiator_name = ( isset($negotiator_names[$highest_negotiator_id]) ? $negotiator_names[$highest_negotiator_id] : '' );
662 + $highest_negotiator_email_address = ( isset($negotiator_email_addresses[$highest_negotiator_id]) ? $negotiator_email_addresses[$highest_negotiator_id] : '' );
663 + }
664 +
665 + $body = str_replace( '[negotiator_name]', esc_html( $highest_negotiator_name ), $body );
666 + $body = str_replace( '[negotiator_email_address]', esc_html( $highest_negotiator_email_address ), $body );
667 +
668 + $highest_office_email_address = apply_filters( 'propertyhive_auto_match_from_email_address', $highest_office_email_address );
669 +
670 + if ( !$dry_run )
671 + {
672 + $ph_admin_matching_properties->send_emails(
673 + $contact_id,
674 + $i,
675 + $new_matching_properties,
676 + get_bloginfo('name'),
677 + $highest_office_email_address,
678 + $subject,
679 + $body
680 + );
681 + }
682 + else
683 + {
684 + echo esc_html('Would\'ve sent email. Not sending due to being ran in dry run mode') . "<br>\n";
685 + }
383 686 }
384 687 }
385 688 }
386 689 }
690 + else
691 + {
692 + if ( $dry_run === true ) { echo esc_html('No applicant profiles found. Skipping') . "<br>\n"; }
693 + }
387 694 }
388 695 }
389 696
697 + if ( $dry_run === true ) { echo esc_html('Finished auto-match process') . "<br>\n"; die(); }
698 +
390 699 wp_reset_postdata();
391 700 }
392 701
393 702 /**
@@ -393,12 +702,9 @@
393 702 /**
394 703 * Init email classes.
395 704 */
396 705 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 - }
706 +
401 707 }
402 708
403 709 /**
404 710 * Apply inline styles to dynamic content.
@@ -411,14 +717,20 @@
411 717 if ( class_exists( 'DOMDocument' ) ) {
412 718 ob_start();
413 719 ph_get_template( 'emails/email-styles.php' );
414 720 $css = apply_filters( 'propertyhive_email_styles', ob_get_clean() );
721 +
722 + // include css inliner
723 + if ( ! class_exists( 'PropertyHive_Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
724 + include_once( dirname( __FILE__ ) . '/libraries/class-emogrifier.php' );
725 + }
726 +
415 727 // apply CSS styles inline for picky email clients
416 728 try {
417 - $emogrifier = new Emogrifier( $content, $css );
729 + $emogrifier = new PropertyHive_Emogrifier( $content, $css );
418 730 $content = $emogrifier->emogrify();
419 731 } catch ( Exception $e ) {
420 - die("Error converting CSS styles to be inline. Error as follows: " . $e->getMessage());
732 + die(esc_html("Error converting CSS styles to be inline. Error as follows: " . $e->getMessage()));
421 733 }
422 734 }
423 735 return $content;
424 736 }
@@ -436,9 +748,9 @@
436 748 public function email_footer( $contact_id = '' ) {
437 749 $unsubscribe_link = '';
438 750 if ($contact_id != '')
439 751 {
440 - $unsubscribe_link = site_url() .'?ph_unsubscribe=' . base64_encode($contact_id . '|' . md5( get_post_meta( $contact_id, '_email_address', TRUE ) ) );
752 + $unsubscribe_link = PH()->get_contact_unsubscribe_url( $contact_id );
441 753 }
442 754
443 755 ph_get_template( 'emails/email-footer.php', array( 'unsubscribe_link' => $unsubscribe_link ) );
444 756 }
@@ -455,8 +767,9 @@
455 767 ob_start();
456 768
457 769 do_action( 'propertyhive_email_header', $contact_id );
458 770
771 + // 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 772 echo wpautop( wptexturize( $message ) );
460 773
461 774 do_action( 'propertyhive_email_footer', $contact_id );
462 775
@@ -467,12 +780,26 @@
467 780 }
468 781
469 782 public function send_enquiry_auto_responder( $data = array() )
470 783 {
471 - if ( isset($data['property_id']) && sanitize_text_field($data['property_id']) != '' )
784 + if ( ! is_array( $data ) ) {
785 + return;
786 + }
787 +
788 + $request_data = wp_unslash( $data );
789 + if ( isset( $request_data['property_id'] ) && is_string( $request_data['property_id'] ) && '' !== $request_data['property_id'] )
472 790 {
473 - $property_id = $data['property_id'];
474 - $to = sanitize_email( $_POST['email_address'] );
791 + $property_ids = array_filter( array_map( 'absint', explode( '|', $request_data['property_id'] ) ) );
792 + if ( empty( $property_ids ) || count( $property_ids ) > 100 ) {
793 + return;
794 + }
795 + foreach ( $property_ids as $property_id ) {
796 + if ( 'property' !== get_post_type( $property_id ) || ! propertyhive_is_post_publicly_viewable( $property_id ) ) {
797 + return;
798 + }
799 + }
800 +
801 + $to = isset( $request_data['email_address'] ) && is_string( $request_data['email_address'] ) ? sanitize_email( $request_data['email_address'] ) : '';
475 802 $subject = get_option( 'propertyhive_enquiry_auto_responder_email_subject', '' );
476 803 $body = get_option( 'propertyhive_enquiry_auto_responder_email_body', '' );
477 804
478 805 if ( $to != '' && $subject != '' && $body != '' )
@@ -477,105 +804,144 @@
477 804
478 805 if ( $to != '' && $subject != '' && $body != '' )
479 806 {
480 807 $headers = array();
481 - $headers[] = 'From: ' . get_bloginfo('name') . ' <' . get_option( 'propertyhive_email_from_address', get_option( 'admin_email' ) ) . '>';
808 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . get_option( 'propertyhive_email_from_address', get_option( 'admin_email' ) ) . '>';
482 809 $headers[] = 'Content-Type: text/html; charset=UTF-8';
483 810
484 - $body = str_replace( "[property_address_hyperlinked]", '<a href="' . get_permalink($property_id) . '">' . get_the_title($property_id) . '</a>', $body );
811 + $name = isset( $request_data['name'] ) && is_string( $request_data['name'] ) ? ph_clean( $request_data['name'] ) : '';
812 + $body = str_replace( '[name]', esc_html( $name ), $body );
485 813
814 + $property_address_hyperlinked = array();
815 + foreach ( $property_ids as $property_id )
816 + {
817 + $property_address_hyperlinked[] = '<a href="' . esc_url( get_permalink( $property_id ) ) . '">' . esc_html( get_the_title( $property_id ) ) . '</a>';
818 + }
819 + $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 );
820 +
486 821 if ( strpos( $body, '[similar_properties]' ) !== FALSE )
487 822 {
488 823 $similar_html = '';
489 824
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);
825 + foreach ( $property_ids as $property_id )
826 + {
827 + $department = get_post_meta( $property_id, '_department', TRUE );
495 828
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 - );
829 + // Get three similar properties
830 + $args = array(
831 + 'post_type' => 'property',
832 + 'post_status' => 'publish',
833 + 'posts_per_page' => 3,
834 + 'orderby' => 'rand',
835 + // 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.
836 + 'post__not_in' => array($property_id),
837 + );
504 838
505 - $meta_query = array();
839 + $meta_query = array();
506 840
507 - $meta_query[] = array(
508 - 'key' => '_department',
509 - 'value' => $department,
510 - );
841 + $meta_query[] = array(
842 + 'key' => '_department',
843 + 'value' => $department,
844 + );
511 845
512 - $meta_query[] = array(
513 - 'key' => '_on_market',
514 - 'value' => 'yes',
515 - );
846 + $meta_query[] = array(
847 + 'key' => '_on_market',
848 + 'value' => 'yes',
849 + );
516 850
517 - $meta_query[] = array(
518 - 'key' => '_bedrooms',
519 - 'value' => $bedrooms,
520 - 'type' => 'NUMERIC'
521 - );
851 + if ( $department != 'commercial' && ph_get_custom_department_based_on( $department ) != 'commercial' )
852 + {
853 + $bedrooms = get_post_meta( $property_id, '_bedrooms', TRUE );
522 854
523 - $meta_query[] = array(
524 - 'key' => '_price_actual',
525 - 'value' => $lower_price,
526 - 'compare' => '>=',
527 - 'type' => 'NUMERIC'
528 - );
855 + $meta_query[] = array(
856 + 'key' => '_bedrooms',
857 + 'value' => $bedrooms,
858 + 'type' => 'NUMERIC'
859 + );
529 860
530 - $meta_query[] = array(
531 - 'key' => '_price_actual',
532 - 'value' => $higher_price,
533 - 'compare' => '<=',
534 - 'type' => 'NUMERIC'
535 - );
536 861
537 - $args['meta_query'] = $meta_query;
862 + $price = get_post_meta( $property_id, '_price_actual', true );
863 + $lower_price = $price - ($price / 10);
864 + $higher_price = $price + ($price / 10);
538 865
539 - $properties_query = new WP_Query( apply_filters( 'propertyhive_auto_responder_similar_properties_query', $args, $property_id ) );
866 + $meta_query[] = array(
867 + 'key' => '_price_actual',
868 + 'value' => $lower_price,
869 + 'compare' => '>=',
870 + 'type' => 'NUMERIC'
871 + );
540 872
541 - if ( $properties_query->have_posts() )
542 - {
543 - $similar_html = '<br><hr><br><h3>Similar Properties You Might Like:</h3>';
873 + $meta_query[] = array(
874 + 'key' => '_price_actual',
875 + 'value' => $higher_price,
876 + 'compare' => '<=',
877 + 'type' => 'NUMERIC'
878 + );
879 + }
880 + else
881 + {
882 + $for_sale = get_post_meta( $property_id, '_for_sale', true );
883 + $to_rent = get_post_meta( $property_id, '_to_rent', true );
544 884
545 - while ( $properties_query->have_posts() )
885 + if ( $for_sale == 'yes' )
886 + {
887 + $meta_query[] = array(
888 + 'key' => '_for_sale',
889 + 'value' => 'yes',
890 + );
891 + }
892 + elseif ( $to_rent == 'yes' )
893 + {
894 + $meta_query[] = array(
895 + 'key' => '_to_rent',
896 + 'value' => 'yes',
897 + );
898 + }
899 + }
900 +
901 + // 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.
902 + $args['meta_query'] = $meta_query;
903 +
904 + $property_match_statuses = get_option( 'propertyhive_property_match_statuses', '' );
905 + if ( $property_match_statuses != '' && is_array($property_match_statuses) && !empty($property_match_statuses) )
546 906 {
547 - $properties_query->the_post();
907 + // 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.
908 + $args['tax_query'] = array(
909 + array(
910 + 'taxonomy' => 'availability',
911 + 'field' => 'term_id',
912 + 'terms' => $property_match_statuses,
913 + 'operator' => 'IN',
914 + )
915 + );
916 + }
548 917
549 - $property = new PH_Property( get_the_ID() );
918 + $properties_query = new WP_Query( apply_filters( 'propertyhive_auto_responder_similar_properties_query', $args, $property_id ) );
550 919
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 )
920 + if ( $properties_query->have_posts() )
921 + {
922 + while ( $properties_query->have_posts() )
556 923 {
557 - $similar_html .= '<a href="' . get_permalink() . '"><img src="' . $image . '" alt="' . get_the_title() . '"></a>';
924 + $properties_query->the_post();
925 +
926 + $property = new PH_Property( get_the_ID() );
927 +
928 + ob_start();
929 +
930 + ph_get_template( 'emails/enquiry-autoresponder-similar-property.php', array( 'property' => $property ) );
931 +
932 + $similar_html .= ob_get_clean();
558 933 }
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 934 }
574 935 }
936 + if ( !empty($similar_html) )
937 + {
938 + $similar_html = '<br><hr><br><h3>Similar Properties You Might Like:</h3>' . $similar_html;
939 + }
575 940 $body = str_replace( "[similar_properties]", $similar_html, $body );
576 941 }
577 942
943 + $body = apply_filters( 'propertyhive_enquiry_auto_responder_body', $body, $property_ids[0] );
578 944 $body = apply_filters( 'propertyhive_mail_content', $this->style_inline( $this->wrap_message( $body ) ) );
579 945
580 946 wp_mail( $to, $subject, $body, $headers );
581 947 }
@@ -580,5 +946,5 @@
580 946 wp_mail( $to, $subject, $body, $headers );
581 947 }
582 948 }
583 949 }
584 -}
950 +}