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 +167 -116 2.2.22.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.
@@ -92,32 +95,44 @@
92 95 }
93 96
94 97 public function run_custom_email_cron()
95 98 {
96 - 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'] ) )
97 100 {
98 - 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 );
99 111 }
100 112 }
101 113
102 - public function send_applicant_registration_alert( $contact_post_id, $user_id )
103 - {
104 - if (
105 - get_option( 'propertyhive_new_registration_alert', '' ) == 'yes' &&
106 - isset($_POST['office_id']) && // in the future we should have office stored against contact and use that
107 - $_POST['office_id'] != '' &&
108 - isset($_POST['department']) && // Should really take department from contacts requirements
109 - $_POST['department'] != ''
110 - )
114 + public function send_applicant_registration_alert( $contact_post_id, $user_id )
111 115 {
112 - $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'] ) : '';
113 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 +
114 125 if ( $to == '' )
115 126 {
116 127 $to = get_option( 'admin_email', '' );
117 128 }
118 129
119 - $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 + );
120 135
121 136 $message = __( "A new applicant has registered through your website. Please find details of the applicant below:", 'propertyhive' ) . "\n\n";
122 137
123 138 $message = apply_filters( 'propertyhive_applicant_registration_email_pre_body', $message, $contact_post_id );
@@ -142,9 +157,10 @@
142 157 {
143 158 continue;
144 159 }
145 160
146 - $value = ph_clean($_POST[$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 );
147 163 $values = array();
148 164 if ( !empty($value) && taxonomy_exists($key) )
149 165 {
150 166 if ( !is_array($value) ) { $value = array($value); }
@@ -183,62 +199,71 @@
183 199 global $wpdb;
184 200
185 201 $lock_id = uniqid( "", true );
186 202
187 - $wpdb->query("
188 - UPDATE " . $wpdb->prefix . "ph_email_log
189 - SET
190 - status = 'fail2',
191 - lock_id = ''
192 - WHERE
193 - status = 'fail1'
194 - AND
195 - lock_id <> ''
196 - AND
197 - locked_at <= '" . date("Y-m-d H:i:s", strtotime('24 hours ago')) . "'
198 - ");
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 + ) );
199 214
200 - $wpdb->query("
201 - UPDATE " . $wpdb->prefix . "ph_email_log
202 - SET
203 - status = 'fail1',
204 - lock_id = ''
205 - WHERE
206 - status = ''
207 - AND
208 - lock_id <> ''
209 - AND
210 - locked_at <= '" . date("Y-m-d H:i:s", strtotime('24 hours ago')) . "'
211 - ");
212 -
213 - // Lock/reserve all emails in log that are status blank or 'fail1' and lock_id blank and send_at in the past
214 - // Only grab 25 at a time to prevent hanging/being seen as spamming
215 - $wpdb->query("
216 - UPDATE " . $wpdb->prefix . "ph_email_log
217 - SET
218 - lock_id = '" . $lock_id . "',
219 - locked_at = '" . date("Y-m-d H:i:s") . "'
220 - WHERE
221 - (status = '' OR status = 'fail1')
222 - AND
223 - lock_id = ''
224 - AND
225 - send_at <= '" . date("Y-m-d H:i:s") . "'
226 - LIMIT " . apply_filters( 'propertyhive_email_process_limit', 25 ) . "
227 - ");
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 + ) );
228 228
229 - // We now have up to 25 emails locked. Get this 25 and attempt to send
230 - $emails_to_send = $wpdb->get_results("
231 - SELECT *
232 - FROM " . $wpdb->prefix . "ph_email_log
233 - WHERE
234 - lock_id = '" . $lock_id . "'
235 - ");
236 -
237 229 foreach ( $emails_to_send as $email_to_send )
238 230 {
239 231 $email_id = $email_to_send->email_id;
240 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 +
241 266 $headers = array();
242 267 $headers[] = 'From: ' . html_entity_decode($email_to_send->from_name) . ' <' . $email_to_send->from_email_address . '>';
243 268 if ( $email_to_send->cc_email_address != '' )
244 269 {
@@ -284,16 +309,16 @@
284 309 {
285 310 $new_status = 'fail2';
286 311 }
287 312 }
288 - $wpdb->query("
289 - UPDATE " . $wpdb->prefix . "ph_email_log
290 - SET
291 - status = '" . $new_status . "',
292 - lock_id = ''
293 - WHERE
294 - email_id = '" . $email_id . "'
295 - ");
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 + );
296 321 }
297 322
298 323 // Delete old logs
299 324 $keep_logs_days = (string)apply_filters( 'propertyhive_keep_email_logs_days', '3650' ); // 10 years
@@ -304,9 +329,13 @@
304 329 {
305 330 $keep_logs_days = '3650';
306 331 }
307 332
308 - $wpdb->query( "DELETE FROM " . $wpdb->prefix . "ph_email_log WHERE send_at < DATE_SUB(NOW(), INTERVAL " . $keep_logs_days . " DAY)" );
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 + ) );
309 338 }
310 339
311 340 /*
312 341 * Automatically send new properties to registered applicants
@@ -314,9 +343,11 @@
314 343 public function ph_auto_email_match()
315 344 {
316 345 global $post;
317 346
318 - $dry_run = isset($_GET['dry_run']) ? true : false;
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'] );
319 350
320 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"; }
321 352
322 353 // Auto emails enabled in settings
@@ -327,9 +358,9 @@
327 358 // 'Do not email' not selected
328 359
329 360 $auto_property_match_enabled = get_option( 'propertyhive_auto_property_match', '' );
330 361
331 - if ( $dry_run === true ) { echo 'Auto-match setting enabled: ' . $auto_property_match_enabled . "<br>\n"; }
362 + if ( $dry_run === true ) { echo 'Auto-match setting enabled: ' . esc_html( $auto_property_match_enabled ) . "<br>\n"; }
332 363
333 364 if ( $auto_property_match_enabled == '' )
334 365 {
335 366 return false;
@@ -336,9 +367,9 @@
336 367 }
337 368
338 369 $auto_property_match_enabled_date = get_option( 'propertyhive_auto_property_match_enabled_date', '' );
339 370
340 - if ( $dry_run === true ) { echo 'Auto-match setting enabled date: ' . $auto_property_match_enabled_date . "<br>\n"; }
371 + if ( $dry_run === true ) { echo 'Auto-match setting enabled date: ' . esc_html( $auto_property_match_enabled_date ) . "<br>\n"; }
341 372
342 373 if ( $auto_property_match_enabled_date == '' )
343 374 {
344 375 return false;
@@ -375,8 +406,9 @@
375 406 $negotiator_email_addresses = array();
376 407
377 408 $args = array(
378 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.
379 411 'role__not_in' => apply_filters( 'property_negotiator_exclude_roles', array('property_hive_contact', 'subscriber') ),
380 412 'fields' => array( 'ID', 'display_name', 'user_email' )
381 413 );
382 414
@@ -420,18 +452,20 @@
420 452
421 453 // Get all contacts that have a type of applicant
422 454 $args = array(
423 455 'post_type' => 'contact',
456 + 'post_status' => 'publish',
424 457 'nopaging' => true,
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.
425 459 'meta_query' => $meta_query,
426 460 'fields' => 'ids'
427 461 );
428 462
429 - if ( $dry_run === true ) { echo 'Running query to get contacts with args: ' . print_r($args, true) . "<br>\n"; }
463 + if ( $dry_run === true ) { echo 'Running query to get contacts with args: ' . esc_html( wp_json_encode( $args, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
430 464
431 465 $contact_query = new WP_Query( $args );
432 466
433 - if ( $dry_run === true ) { echo 'Found ' . $contact_query->found_posts . ' contacts' . "<br>\n"; }
467 + if ( $dry_run === true ) { echo 'Found ' . esc_html( $contact_query->found_posts ) . ' contacts' . "<br>\n"; }
434 468
435 469 if ( $contact_query->have_posts() )
436 470 {
437 471 $default_subject = get_option( 'propertyhive_property_match_default_email_subject', '' );
@@ -456,9 +490,9 @@
456 490 ),
457 491 );
458 492 $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
459 493
460 - $default_body = wp_kses($default_body, $allowedposttags);
494 + $default_body = wp_kses( $default_body, $allowed_tags );
461 495
462 496 while ( $contact_query->have_posts() )
463 497 {
464 498 $contact_query->the_post();
@@ -464,14 +498,14 @@
464 498 $contact_query->the_post();
465 499
466 500 $contact_id = get_the_ID();
467 501
468 - if ( $dry_run === true ) { echo 'Doing contact: ' . get_the_title() . "<br>\n"; }
502 + if ( $dry_run === true ) { echo 'Doing contact: ' . esc_html( get_the_title() ) . "<br>\n"; }
469 503
470 504 // invalid email address
471 505 if ( strpos( get_post_meta( $contact_id, '_email_address', TRUE ), '@' ) === FALSE )
472 506 {
473 - if ( $dry_run === true ) { echo 'Invalid email address. Skipping' . "<br>\n"; }
507 + if ( $dry_run === true ) { echo esc_html('Invalid email address. Skipping') . "<br>\n"; }
474 508
475 509 continue;
476 510 }
477 511
@@ -478,9 +512,9 @@
478 512 // email in the list of forbidden contact methods
479 513 $forbidden_contact_methods = get_post_meta( $contact_id, '_forbidden_contact_methods', TRUE );
480 514 if ( is_array($forbidden_contact_methods) && in_array('email', $forbidden_contact_methods) )
481 515 {
482 - if ( $dry_run === true ) { echo 'Email communication forbidden in contact preferences. Skipping' . "<br>\n"; }
516 + if ( $dry_run === true ) { echo esc_html('Email communication forbidden in contact preferences. Skipping') . "<br>\n"; }
483 517
484 518 continue;
485 519 }
486 520
@@ -493,9 +527,9 @@
493 527 {
494 528 $dismissed_properties = array();
495 529 }
496 530
497 - if ( $dry_run === true ) { if ( !empty($dismissed_properties) ) { echo 'Dismissed properties: ' . print_r($dismissed_properties, true) . "<br>\n"; } }
531 + if ( $dry_run === true ) { if ( !empty($dismissed_properties) ) { echo 'Dismissed properties: ' . esc_html( wp_json_encode( $dismissed_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; } }
498 532
499 533 for ( $i = 0; $i < $applicant_profiles; ++$i )
500 534 {
501 535 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
@@ -501,21 +535,21 @@
501 535 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
502 536
503 537 if ( $applicant_profile == '' || !is_array($applicant_profile) || !isset($applicant_profile['department']) )
504 538 {
505 - if ( $dry_run === true ) { echo 'Applicant relationship empty or no department set' . "<br>\n"; }
539 + if ( $dry_run === true ) { echo esc_html('Applicant relationship empty or no department set') . "<br>\n"; }
506 540 continue;
507 541 }
508 542
509 543 if ( !isset($applicant_profile['send_matching_properties']) || ( isset($applicant_profile['send_matching_properties']) && $applicant_profile['send_matching_properties'] != 'yes' ) )
510 544 {
511 - if ( $dry_run === true ) { echo 'Send matching properties disabled' . "<br>\n"; }
545 + if ( $dry_run === true ) { echo esc_html('Send matching properties disabled') . "<br>\n"; }
512 546 continue;
513 547 }
514 548
515 549 if ( isset($applicant_profile['auto_match_disabled']) && $applicant_profile['auto_match_disabled'] == 'yes' )
516 550 {
517 - if ( $dry_run === true ) { echo 'Auto match disabled' . "<br>\n"; }
551 + if ( $dry_run === true ) { echo esc_html('Auto match disabled') . "<br>\n"; }
518 552 continue;
519 553 }
520 554
521 555 if ( $dry_run === true ) { echo 'Getting matching properties' . "<br>\n"; }
@@ -521,9 +555,9 @@
521 555 if ( $dry_run === true ) { echo 'Getting matching properties' . "<br>\n"; }
522 556
523 557 $matching_properties = $ph_admin_matching_properties->get_matching_properties( $contact_id, $i, $auto_property_match_enabled_date );
524 558
525 - if ( $dry_run === true ) { echo 'Found ' . count($matching_properties) . ' matching properties' . "<br>\n"; }
559 + if ( $dry_run === true ) { echo esc_html('Found ' . count($matching_properties) . ' matching properties') . "<br>\n"; }
526 560
527 561 if ( !empty($matching_properties) )
528 562 {
529 563 $already_sent_properties = get_post_meta( $contact_id, '_applicant_profile_' . $i . '_match_history', TRUE );
@@ -530,9 +564,9 @@
530 564
531 565 // Remove from this array if on market changed or price changed
532 566 if ( is_array($already_sent_properties) )
533 567 {
534 - if ( $dry_run === true ) { echo 'Already sent properties before: ' . print_r($already_sent_properties, true) . "<br>\n"; }
568 + if ( $dry_run === true ) { echo 'Already sent properties before: ' . esc_html( wp_json_encode( $already_sent_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
535 569
536 570 foreach ( $already_sent_properties as $already_sent_property_id => $sends )
537 571 {
538 572 $highest_send = $sends[count($sends) - 1]['date'];
@@ -538,21 +572,21 @@
538 572 $highest_send = $sends[count($sends) - 1]['date'];
539 573
540 574 if ( $highest_send != '' )
541 575 {
542 - if ( $dry_run === true ) { echo 'Property: ' . $already_sent_property_id . ' last sent: ' . $highest_send . "<br>\n"; }
576 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last sent: ' . esc_html( $highest_send ) . "<br>\n"; }
543 577
544 578 $on_market_change_date = get_post_meta( $already_sent_property_id, '_on_market_change_date', TRUE );
545 579
546 - if ( $dry_run === true ) { echo 'Property: ' . $already_sent_property_id . ' last on market change: ' . $on_market_change_date . "<br>\n"; }
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"; }
547 581
548 582 $price_change_date = get_post_meta( $already_sent_property_id, '_price_change_date', TRUE );
549 583
550 - if ( $dry_run === true ) { echo 'Property: ' . $already_sent_property_id . ' last price change: ' . $price_change_date . "<br>\n"; }
584 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last price change: ' . esc_html( $price_change_date ) . "<br>\n"; }
551 585
552 586 if ( $on_market_change_date > $highest_send )
553 587 {
554 - if ( $dry_run === true ) { echo 'Property: ' . $already_sent_property_id . ' has changed on market since last sent' . "<br>\n"; }
588 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' has changed on market since last sent' . "<br>\n"; }
555 589
556 590 // This property has changed since it was last sent. Remove from already sent list so it gets sent again
557 591 unset($already_sent_properties[$already_sent_property_id]);
558 592 }
@@ -557,9 +591,9 @@
557 591 unset($already_sent_properties[$already_sent_property_id]);
558 592 }
559 593 elseif ( $price_change_date > $highest_send )
560 594 {
561 - if ( $dry_run === true ) { echo 'Property: ' . $already_sent_property_id . ' has changed price since last sent' . "<br>\n"; }
595 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' has changed price since last sent' . "<br>\n"; }
562 596
563 597 // This property has changed since it was last sent. Remove from already sent list so it gets sent again
564 598 unset($already_sent_properties[$already_sent_property_id]);
565 599 }
@@ -565,12 +599,12 @@
565 599 }
566 600 }
567 601 }
568 602
569 - if ( $dry_run === true ) { echo 'Already sent properties after: ' . print_r($already_sent_properties, true) . "<br>\n"; }
603 + if ( $dry_run === true ) { echo 'Already sent properties after: ' . esc_html( wp_json_encode( $already_sent_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
570 604 }
571 605
572 - if ( $dry_run === true ) { echo 'Matching properties before removing already sent: ' . print_r($matching_properties, true) . "<br>\n"; }
606 + if ( $dry_run === true ) { echo 'Matching properties before removing already sent: ' . esc_html( wp_json_encode( $matching_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
573 607
574 608 // Check properties haven't already been sent and not marked as 'not interested'
575 609 $new_matching_properties = array();
576 610 foreach ($matching_properties as $matching_property)
@@ -580,9 +614,9 @@
580 614 $new_matching_properties[] = $matching_property->id;
581 615 }
582 616 }
583 617
584 - if ( $dry_run === true ) { echo 'Matching properties after removing already sent: ' . print_r($new_matching_properties, true) . "<br>\n"; }
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"; }
585 619
586 620 $max_results = apply_filters( 'propertyhive_auto_match_maximum_results', FALSE);
587 621 if ( $max_results !== FALSE )
588 622 {
@@ -588,9 +622,9 @@
588 622 {
589 623 $new_matching_properties = array_slice($new_matching_properties, 0, (int)$max_results);
590 624 }
591 625
592 - if ( $dry_run === true ) { echo 'Found ' . count($new_matching_properties) . ' matching properties after removing already sent and dismissed' . "<br>\n"; }
626 + if ( $dry_run === true ) { echo esc_html('Found ' . count($new_matching_properties) . ' matching properties after removing already sent and dismissed') . "<br>\n"; }
593 627
594 628 if ( !empty($new_matching_properties) )
595 629 {
596 630 $subject = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $default_subject);
@@ -595,10 +629,10 @@
595 629 {
596 630 $subject = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $default_subject);
597 631
598 632 $contact = new PH_Contact($contact_id);
599 - $body = str_replace("[contact_name]", $contact->post_title, $default_body);
600 - $body = str_replace("[contact_dear]", $contact->dear(), $body);
633 + $body = str_replace( '[contact_name]', esc_html( $contact->post_title ), $default_body );
634 + $body = str_replace( '[contact_dear]', esc_html( $contact->dear() ), $body );
601 635 $body = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $body);
602 636
603 637 $office_counts = array();
604 638 $negotiator_counts = array();
@@ -647,10 +681,10 @@
647 681 // fallback to admin email address
648 682 $highest_office_email_address = get_option('admin_email');
649 683 }
650 684
651 - $body = str_replace("[office_name]", $highest_office_name, $body);
652 - $body = str_replace("[office_email_address]", $highest_office_email_address, $body);
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 );
653 687
654 688 $highest_negotiator_name = '';
655 689 $highest_negotiator_email_address = '';
656 690 if ( !empty($negotiator_counts) )
@@ -661,10 +695,10 @@
661 695 $highest_negotiator_name = ( isset($negotiator_names[$highest_negotiator_id]) ? $negotiator_names[$highest_negotiator_id] : '' );
662 696 $highest_negotiator_email_address = ( isset($negotiator_email_addresses[$highest_negotiator_id]) ? $negotiator_email_addresses[$highest_negotiator_id] : '' );
663 697 }
664 698
665 - $body = str_replace("[negotiator_name]", $highest_negotiator_name, $body);
666 - $body = str_replace("[negotiator_email_address]", $highest_negotiator_email_address, $body);
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 );
667 701
668 702 $highest_office_email_address = apply_filters( 'propertyhive_auto_match_from_email_address', $highest_office_email_address );
669 703
670 704 if ( !$dry_run )
@@ -680,9 +714,9 @@
680 714 );
681 715 }
682 716 else
683 717 {
684 - echo 'Would\'ve sent email. Not sending due to being ran in dry run mode' . "<br>\n";
718 + echo esc_html('Would\'ve sent email. Not sending due to being ran in dry run mode') . "<br>\n";
685 719 }
686 720 }
687 721 }
688 722 }
@@ -688,14 +722,14 @@
688 722 }
689 723 }
690 724 else
691 725 {
692 - if ( $dry_run === true ) { echo 'No applicant profiles found. Skipping' . "<br>\n"; }
726 + if ( $dry_run === true ) { echo esc_html('No applicant profiles found. Skipping') . "<br>\n"; }
693 727 }
694 728 }
695 729 }
696 730
697 - if ( $dry_run === true ) { echo 'Finished auto-match process' . "<br>\n"; die(); }
731 + if ( $dry_run === true ) { echo esc_html('Finished auto-match process') . "<br>\n"; die(); }
698 732
699 733 wp_reset_postdata();
700 734 }
701 735
@@ -719,15 +753,15 @@
719 753 ph_get_template( 'emails/email-styles.php' );
720 754 $css = apply_filters( 'propertyhive_email_styles', ob_get_clean() );
721 755
722 756 // include css inliner
723 - if ( ! class_exists( 'Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
757 + if ( ! class_exists( 'PropertyHive_Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
724 758 include_once( dirname( __FILE__ ) . '/libraries/class-emogrifier.php' );
725 759 }
726 760
727 761 // apply CSS styles inline for picky email clients
728 762 try {
729 - $emogrifier = new Emogrifier( $content, $css );
763 + $emogrifier = new PropertyHive_Emogrifier( $content, $css );
730 764 $content = $emogrifier->emogrify();
731 765 } catch ( Exception $e ) {
732 766 die(esc_html("Error converting CSS styles to be inline. Error as follows: " . $e->getMessage()));
733 767 }
@@ -748,9 +782,9 @@
748 782 public function email_footer( $contact_id = '' ) {
749 783 $unsubscribe_link = '';
750 784 if ($contact_id != '')
751 785 {
752 - $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 );
753 787 }
754 788
755 789 ph_get_template( 'emails/email-footer.php', array( 'unsubscribe_link' => $unsubscribe_link ) );
756 790 }
@@ -767,8 +801,9 @@
767 801 ob_start();
768 802
769 803 do_action( 'propertyhive_email_header', $contact_id );
770 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.
771 806 echo wpautop( wptexturize( $message ) );
772 807
773 808 do_action( 'propertyhive_email_footer', $contact_id );
774 809
@@ -779,14 +814,26 @@
779 814 }
780 815
781 816 public function send_enquiry_auto_responder( $data = array() )
782 817 {
783 - if ( isset($data['property_id']) && !empty(ph_clean($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'] )
784 824 {
785 - $property_ids = ph_clean(explode("|", $data['property_id']));
786 - if ( !is_array($property_ids) ) { $property_ids = array($property_ids); }
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 + }
787 834
788 - $to = sanitize_email( $_POST['email_address'] );
835 + $to = isset( $request_data['email_address'] ) && is_string( $request_data['email_address'] ) ? sanitize_email( $request_data['email_address'] ) : '';
789 836 $subject = get_option( 'propertyhive_enquiry_auto_responder_email_subject', '' );
790 837 $body = get_option( 'propertyhive_enquiry_auto_responder_email_body', '' );
791 838
792 839 if ( $to != '' && $subject != '' && $body != '' )
@@ -794,14 +841,15 @@
794 841 $headers = array();
795 842 $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . get_option( 'propertyhive_email_from_address', get_option( 'admin_email' ) ) . '>';
796 843 $headers[] = 'Content-Type: text/html; charset=UTF-8';
797 844
798 - $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 );
799 847
800 848 $property_address_hyperlinked = array();
801 849 foreach ( $property_ids as $property_id )
802 850 {
803 - $property_address_hyperlinked[] = '<a href="' . get_permalink($property_id) . '">' . get_the_title($property_id) . '</a>';
851 + $property_address_hyperlinked[] = '<a href="' . esc_url( get_permalink( $property_id ) ) . '">' . esc_html( get_the_title( $property_id ) ) . '</a>';
804 852 }
805 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 );
806 854
807 855 if ( strpos( $body, '[similar_properties]' ) !== FALSE )
@@ -817,8 +865,9 @@
817 865 'post_type' => 'property',
818 866 'post_status' => 'publish',
819 867 'posts_per_page' => 3,
820 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.
821 870 'post__not_in' => array($property_id),
822 871 );
823 872
824 873 $meta_query = array();
@@ -882,13 +931,15 @@
882 931 );
883 932 }
884 933 }
885 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.
886 936 $args['meta_query'] = $meta_query;
887 937
888 938 $property_match_statuses = get_option( 'propertyhive_property_match_statuses', '' );
889 939 if ( $property_match_statuses != '' && is_array($property_match_statuses) && !empty($property_match_statuses) )
890 940 {
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.
891 942 $args['tax_query'] = array(
892 943 array(
893 944 'taxonomy' => 'availability',
894 945 'field' => 'term_id',
@@ -929,5 +980,5 @@
929 980 wp_mail( $to, $subject, $body, $headers );
930 981 }
931 982 }
932 983 }
933 -}
984 +}