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 +162 -115 2.2.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.
@@ -92,26 +95,34 @@
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 }
@@ -146,9 +157,10 @@
146 157 {
147 158 continue;
148 159 }
149 160
150 - $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 );
151 163 $values = array();
152 164 if ( !empty($value) && taxonomy_exists($key) )
153 165 {
154 166 if ( !is_array($value) ) { $value = array($value); }
@@ -187,62 +199,71 @@
187 199 global $wpdb;
188 200
189 201 $lock_id = uniqid( "", true );
190 202
191 - $wpdb->query("
192 - UPDATE " . $wpdb->prefix . "ph_email_log
193 - SET
194 - status = 'fail2',
195 - lock_id = ''
196 - WHERE
197 - status = 'fail1'
198 - AND
199 - lock_id <> ''
200 - AND
201 - locked_at <= '" . date("Y-m-d H:i:s", strtotime('24 hours ago')) . "'
202 - ");
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 + ) );
203 214
204 - $wpdb->query("
205 - UPDATE " . $wpdb->prefix . "ph_email_log
206 - SET
207 - status = 'fail1',
208 - lock_id = ''
209 - WHERE
210 - status = ''
211 - AND
212 - lock_id <> ''
213 - AND
214 - locked_at <= '" . date("Y-m-d H:i:s", strtotime('24 hours ago')) . "'
215 - ");
216 -
217 - // Lock/reserve all emails in log that are status blank or 'fail1' and lock_id blank and send_at in the past
218 - // Only grab 25 at a time to prevent hanging/being seen as spamming
219 - $wpdb->query("
220 - UPDATE " . $wpdb->prefix . "ph_email_log
221 - SET
222 - lock_id = '" . $lock_id . "',
223 - locked_at = '" . date("Y-m-d H:i:s") . "'
224 - WHERE
225 - (status = '' OR status = 'fail1')
226 - AND
227 - lock_id = ''
228 - AND
229 - send_at <= '" . date("Y-m-d H:i:s") . "'
230 - LIMIT " . apply_filters( 'propertyhive_email_process_limit', 25 ) . "
231 - ");
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 + ) );
232 228
233 - // We now have up to 25 emails locked. Get this 25 and attempt to send
234 - $emails_to_send = $wpdb->get_results("
235 - SELECT *
236 - FROM " . $wpdb->prefix . "ph_email_log
237 - WHERE
238 - lock_id = '" . $lock_id . "'
239 - ");
240 -
241 229 foreach ( $emails_to_send as $email_to_send )
242 230 {
243 231 $email_id = $email_to_send->email_id;
244 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 +
245 266 $headers = array();
246 267 $headers[] = 'From: ' . html_entity_decode($email_to_send->from_name) . ' <' . $email_to_send->from_email_address . '>';
247 268 if ( $email_to_send->cc_email_address != '' )
248 269 {
@@ -288,16 +309,16 @@
288 309 {
289 310 $new_status = 'fail2';
290 311 }
291 312 }
292 - $wpdb->query("
293 - UPDATE " . $wpdb->prefix . "ph_email_log
294 - SET
295 - status = '" . $new_status . "',
296 - lock_id = ''
297 - WHERE
298 - email_id = '" . $email_id . "'
299 - ");
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 + );
300 321 }
301 322
302 323 // Delete old logs
303 324 $keep_logs_days = (string)apply_filters( 'propertyhive_keep_email_logs_days', '3650' ); // 10 years
@@ -308,9 +329,13 @@
308 329 {
309 330 $keep_logs_days = '3650';
310 331 }
311 332
312 - $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 + ) );
313 338 }
314 339
315 340 /*
316 341 * Automatically send new properties to registered applicants
@@ -318,9 +343,11 @@
318 343 public function ph_auto_email_match()
319 344 {
320 345 global $post;
321 346
322 - $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'] );
323 350
324 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"; }
325 352
326 353 // Auto emails enabled in settings
@@ -331,9 +358,9 @@
331 358 // 'Do not email' not selected
332 359
333 360 $auto_property_match_enabled = get_option( 'propertyhive_auto_property_match', '' );
334 361
335 - 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"; }
336 363
337 364 if ( $auto_property_match_enabled == '' )
338 365 {
339 366 return false;
@@ -340,9 +367,9 @@
340 367 }
341 368
342 369 $auto_property_match_enabled_date = get_option( 'propertyhive_auto_property_match_enabled_date', '' );
343 370
344 - 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"; }
345 372
346 373 if ( $auto_property_match_enabled_date == '' )
347 374 {
348 375 return false;
@@ -379,8 +406,9 @@
379 406 $negotiator_email_addresses = array();
380 407
381 408 $args = array(
382 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.
383 411 'role__not_in' => apply_filters( 'property_negotiator_exclude_roles', array('property_hive_contact', 'subscriber') ),
384 412 'fields' => array( 'ID', 'display_name', 'user_email' )
385 413 );
386 414
@@ -424,18 +452,20 @@
424 452
425 453 // Get all contacts that have a type of applicant
426 454 $args = array(
427 455 'post_type' => 'contact',
456 + 'post_status' => 'publish',
428 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.
429 459 'meta_query' => $meta_query,
430 460 'fields' => 'ids'
431 461 );
432 462
433 - 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"; }
434 464
435 465 $contact_query = new WP_Query( $args );
436 466
437 - 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"; }
438 468
439 469 if ( $contact_query->have_posts() )
440 470 {
441 471 $default_subject = get_option( 'propertyhive_property_match_default_email_subject', '' );
@@ -460,9 +490,9 @@
460 490 ),
461 491 );
462 492 $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
463 493
464 - $default_body = wp_kses($default_body, $allowedposttags);
494 + $default_body = wp_kses( $default_body, $allowed_tags );
465 495
466 496 while ( $contact_query->have_posts() )
467 497 {
468 498 $contact_query->the_post();
@@ -468,14 +498,14 @@
468 498 $contact_query->the_post();
469 499
470 500 $contact_id = get_the_ID();
471 501
472 - 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"; }
473 503
474 504 // invalid email address
475 505 if ( strpos( get_post_meta( $contact_id, '_email_address', TRUE ), '@' ) === FALSE )
476 506 {
477 - 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"; }
478 508
479 509 continue;
480 510 }
481 511
@@ -482,9 +512,9 @@
482 512 // email in the list of forbidden contact methods
483 513 $forbidden_contact_methods = get_post_meta( $contact_id, '_forbidden_contact_methods', TRUE );
484 514 if ( is_array($forbidden_contact_methods) && in_array('email', $forbidden_contact_methods) )
485 515 {
486 - 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"; }
487 517
488 518 continue;
489 519 }
490 520
@@ -497,9 +527,9 @@
497 527 {
498 528 $dismissed_properties = array();
499 529 }
500 530
501 - 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"; } }
502 532
503 533 for ( $i = 0; $i < $applicant_profiles; ++$i )
504 534 {
505 535 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
@@ -505,21 +535,21 @@
505 535 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
506 536
507 537 if ( $applicant_profile == '' || !is_array($applicant_profile) || !isset($applicant_profile['department']) )
508 538 {
509 - 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"; }
510 540 continue;
511 541 }
512 542
513 543 if ( !isset($applicant_profile['send_matching_properties']) || ( isset($applicant_profile['send_matching_properties']) && $applicant_profile['send_matching_properties'] != 'yes' ) )
514 544 {
515 - 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"; }
516 546 continue;
517 547 }
518 548
519 549 if ( isset($applicant_profile['auto_match_disabled']) && $applicant_profile['auto_match_disabled'] == 'yes' )
520 550 {
521 - if ( $dry_run === true ) { echo 'Auto match disabled' . "<br>\n"; }
551 + if ( $dry_run === true ) { echo esc_html('Auto match disabled') . "<br>\n"; }
522 552 continue;
523 553 }
524 554
525 555 if ( $dry_run === true ) { echo 'Getting matching properties' . "<br>\n"; }
@@ -525,9 +555,9 @@
525 555 if ( $dry_run === true ) { echo 'Getting matching properties' . "<br>\n"; }
526 556
527 557 $matching_properties = $ph_admin_matching_properties->get_matching_properties( $contact_id, $i, $auto_property_match_enabled_date );
528 558
529 - 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"; }
530 560
531 561 if ( !empty($matching_properties) )
532 562 {
533 563 $already_sent_properties = get_post_meta( $contact_id, '_applicant_profile_' . $i . '_match_history', TRUE );
@@ -534,9 +564,9 @@
534 564
535 565 // Remove from this array if on market changed or price changed
536 566 if ( is_array($already_sent_properties) )
537 567 {
538 - 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"; }
539 569
540 570 foreach ( $already_sent_properties as $already_sent_property_id => $sends )
541 571 {
542 572 $highest_send = $sends[count($sends) - 1]['date'];
@@ -542,21 +572,21 @@
542 572 $highest_send = $sends[count($sends) - 1]['date'];
543 573
544 574 if ( $highest_send != '' )
545 575 {
546 - 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"; }
547 577
548 578 $on_market_change_date = get_post_meta( $already_sent_property_id, '_on_market_change_date', TRUE );
549 579
550 - 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"; }
551 581
552 582 $price_change_date = get_post_meta( $already_sent_property_id, '_price_change_date', TRUE );
553 583
554 - 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"; }
555 585
556 586 if ( $on_market_change_date > $highest_send )
557 587 {
558 - 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"; }
559 589
560 590 // This property has changed since it was last sent. Remove from already sent list so it gets sent again
561 591 unset($already_sent_properties[$already_sent_property_id]);
562 592 }
@@ -561,9 +591,9 @@
561 591 unset($already_sent_properties[$already_sent_property_id]);
562 592 }
563 593 elseif ( $price_change_date > $highest_send )
564 594 {
565 - 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"; }
566 596
567 597 // This property has changed since it was last sent. Remove from already sent list so it gets sent again
568 598 unset($already_sent_properties[$already_sent_property_id]);
569 599 }
@@ -569,12 +599,12 @@
569 599 }
570 600 }
571 601 }
572 602
573 - 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"; }
574 604 }
575 605
576 - 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"; }
577 607
578 608 // Check properties haven't already been sent and not marked as 'not interested'
579 609 $new_matching_properties = array();
580 610 foreach ($matching_properties as $matching_property)
@@ -584,9 +614,9 @@
584 614 $new_matching_properties[] = $matching_property->id;
585 615 }
586 616 }
587 617
588 - 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"; }
589 619
590 620 $max_results = apply_filters( 'propertyhive_auto_match_maximum_results', FALSE);
591 621 if ( $max_results !== FALSE )
592 622 {
@@ -592,9 +622,9 @@
592 622 {
593 623 $new_matching_properties = array_slice($new_matching_properties, 0, (int)$max_results);
594 624 }
595 625
596 - 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"; }
597 627
598 628 if ( !empty($new_matching_properties) )
599 629 {
600 630 $subject = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $default_subject);
@@ -599,10 +629,10 @@
599 629 {
600 630 $subject = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $default_subject);
601 631
602 632 $contact = new PH_Contact($contact_id);
603 - $body = str_replace("[contact_name]", $contact->post_title, $default_body);
604 - $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 );
605 635 $body = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $body);
606 636
607 637 $office_counts = array();
608 638 $negotiator_counts = array();
@@ -651,10 +681,10 @@
651 681 // fallback to admin email address
652 682 $highest_office_email_address = get_option('admin_email');
653 683 }
654 684
655 - $body = str_replace("[office_name]", $highest_office_name, $body);
656 - $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 );
657 687
658 688 $highest_negotiator_name = '';
659 689 $highest_negotiator_email_address = '';
660 690 if ( !empty($negotiator_counts) )
@@ -665,10 +695,10 @@
665 695 $highest_negotiator_name = ( isset($negotiator_names[$highest_negotiator_id]) ? $negotiator_names[$highest_negotiator_id] : '' );
666 696 $highest_negotiator_email_address = ( isset($negotiator_email_addresses[$highest_negotiator_id]) ? $negotiator_email_addresses[$highest_negotiator_id] : '' );
667 697 }
668 698
669 - $body = str_replace("[negotiator_name]", $highest_negotiator_name, $body);
670 - $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 );
671 701
672 702 $highest_office_email_address = apply_filters( 'propertyhive_auto_match_from_email_address', $highest_office_email_address );
673 703
674 704 if ( !$dry_run )
@@ -684,9 +714,9 @@
684 714 );
685 715 }
686 716 else
687 717 {
688 - 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";
689 719 }
690 720 }
691 721 }
692 722 }
@@ -692,14 +722,14 @@
692 722 }
693 723 }
694 724 else
695 725 {
696 - 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"; }
697 727 }
698 728 }
699 729 }
700 730
701 - 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(); }
702 732
703 733 wp_reset_postdata();
704 734 }
705 735
@@ -723,15 +753,15 @@
723 753 ph_get_template( 'emails/email-styles.php' );
724 754 $css = apply_filters( 'propertyhive_email_styles', ob_get_clean() );
725 755
726 756 // include css inliner
727 - if ( ! class_exists( 'Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
757 + if ( ! class_exists( 'PropertyHive_Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
728 758 include_once( dirname( __FILE__ ) . '/libraries/class-emogrifier.php' );
729 759 }
730 760
731 761 // apply CSS styles inline for picky email clients
732 762 try {
733 - $emogrifier = new Emogrifier( $content, $css );
763 + $emogrifier = new PropertyHive_Emogrifier( $content, $css );
734 764 $content = $emogrifier->emogrify();
735 765 } catch ( Exception $e ) {
736 766 die(esc_html("Error converting CSS styles to be inline. Error as follows: " . $e->getMessage()));
737 767 }
@@ -752,9 +782,9 @@
752 782 public function email_footer( $contact_id = '' ) {
753 783 $unsubscribe_link = '';
754 784 if ($contact_id != '')
755 785 {
756 - $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 );
757 787 }
758 788
759 789 ph_get_template( 'emails/email-footer.php', array( 'unsubscribe_link' => $unsubscribe_link ) );
760 790 }
@@ -771,8 +801,9 @@
771 801 ob_start();
772 802
773 803 do_action( 'propertyhive_email_header', $contact_id );
774 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.
775 806 echo wpautop( wptexturize( $message ) );
776 807
777 808 do_action( 'propertyhive_email_footer', $contact_id );
778 809
@@ -783,14 +814,26 @@
783 814 }
784 815
785 816 public function send_enquiry_auto_responder( $data = array() )
786 817 {
787 - 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'] )
788 824 {
789 - $property_ids = ph_clean(explode("|", $data['property_id']));
790 - 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 + }
791 834
792 - $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'] ) : '';
793 836 $subject = get_option( 'propertyhive_enquiry_auto_responder_email_subject', '' );
794 837 $body = get_option( 'propertyhive_enquiry_auto_responder_email_body', '' );
795 838
796 839 if ( $to != '' && $subject != '' && $body != '' )
@@ -798,14 +841,15 @@
798 841 $headers = array();
799 842 $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . get_option( 'propertyhive_email_from_address', get_option( 'admin_email' ) ) . '>';
800 843 $headers[] = 'Content-Type: text/html; charset=UTF-8';
801 844
802 - $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 );
803 847
804 848 $property_address_hyperlinked = array();
805 849 foreach ( $property_ids as $property_id )
806 850 {
807 - $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>';
808 852 }
809 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 );
810 854
811 855 if ( strpos( $body, '[similar_properties]' ) !== FALSE )
@@ -821,8 +865,9 @@
821 865 'post_type' => 'property',
822 866 'post_status' => 'publish',
823 867 'posts_per_page' => 3,
824 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.
825 870 'post__not_in' => array($property_id),
826 871 );
827 872
828 873 $meta_query = array();
@@ -886,13 +931,15 @@
886 931 );
887 932 }
888 933 }
889 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.
890 936 $args['meta_query'] = $meta_query;
891 937
892 938 $property_match_statuses = get_option( 'propertyhive_property_match_statuses', '' );
893 939 if ( $property_match_statuses != '' && is_array($property_match_statuses) && !empty($property_match_statuses) )
894 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.
895 942 $args['tax_query'] = array(
896 943 array(
897 944 'taxonomy' => 'availability',
898 945 'field' => 'term_id',
@@ -933,5 +980,5 @@
933 980 wp_mail( $to, $subject, $body, $headers );
934 981 }
935 982 }
936 983 }
937 -}
984 +}