PluginProbe
Property Hive / 2.3.0
Property Hive v2.3.0
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 1.4.62 All 260 releases
← All changes | includes/class-ph-emails.php +128 -115 2.2.32.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.
@@ -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,58 +199,34 @@
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
@@ -288,16 +276,16 @@
288 276 {
289 277 $new_status = 'fail2';
290 278 }
291 279 }
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 - ");
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 + );
300 288 }
301 289
302 290 // Delete old logs
303 291 $keep_logs_days = (string)apply_filters( 'propertyhive_keep_email_logs_days', '3650' ); // 10 years
@@ -308,9 +296,13 @@
308 296 {
309 297 $keep_logs_days = '3650';
310 298 }
311 299
312 - $wpdb->query( "DELETE FROM " . $wpdb->prefix . "ph_email_log WHERE send_at < DATE_SUB(NOW(), INTERVAL " . $keep_logs_days . " DAY)" );
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 + ) );
313 305 }
314 306
315 307 /*
316 308 * Automatically send new properties to registered applicants
@@ -318,9 +310,11 @@
318 310 public function ph_auto_email_match()
319 311 {
320 312 global $post;
321 313
322 - $dry_run = isset($_GET['dry_run']) ? true : false;
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'] );
323 317
324 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"; }
325 319
326 320 // Auto emails enabled in settings
@@ -331,9 +325,9 @@
331 325 // 'Do not email' not selected
332 326
333 327 $auto_property_match_enabled = get_option( 'propertyhive_auto_property_match', '' );
334 328
335 - if ( $dry_run === true ) { echo 'Auto-match setting enabled: ' . $auto_property_match_enabled . "<br>\n"; }
329 + if ( $dry_run === true ) { echo 'Auto-match setting enabled: ' . esc_html( $auto_property_match_enabled ) . "<br>\n"; }
336 330
337 331 if ( $auto_property_match_enabled == '' )
338 332 {
339 333 return false;
@@ -340,9 +334,9 @@
340 334 }
341 335
342 336 $auto_property_match_enabled_date = get_option( 'propertyhive_auto_property_match_enabled_date', '' );
343 337
344 - if ( $dry_run === true ) { echo 'Auto-match setting enabled date: ' . $auto_property_match_enabled_date . "<br>\n"; }
338 + if ( $dry_run === true ) { echo 'Auto-match setting enabled date: ' . esc_html( $auto_property_match_enabled_date ) . "<br>\n"; }
345 339
346 340 if ( $auto_property_match_enabled_date == '' )
347 341 {
348 342 return false;
@@ -379,8 +373,9 @@
379 373 $negotiator_email_addresses = array();
380 374
381 375 $args = array(
382 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.
383 378 'role__not_in' => apply_filters( 'property_negotiator_exclude_roles', array('property_hive_contact', 'subscriber') ),
384 379 'fields' => array( 'ID', 'display_name', 'user_email' )
385 380 );
386 381
@@ -425,17 +420,18 @@
425 420 // Get all contacts that have a type of applicant
426 421 $args = array(
427 422 'post_type' => 'contact',
428 423 'nopaging' => true,
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.
429 425 'meta_query' => $meta_query,
430 426 'fields' => 'ids'
431 427 );
432 428
433 - if ( $dry_run === true ) { echo 'Running query to get contacts with args: ' . print_r($args, true) . "<br>\n"; }
429 + if ( $dry_run === true ) { echo 'Running query to get contacts with args: ' . esc_html( wp_json_encode( $args, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
434 430
435 431 $contact_query = new WP_Query( $args );
436 432
437 - if ( $dry_run === true ) { echo 'Found ' . $contact_query->found_posts . ' contacts' . "<br>\n"; }
433 + if ( $dry_run === true ) { echo 'Found ' . esc_html( $contact_query->found_posts ) . ' contacts' . "<br>\n"; }
438 434
439 435 if ( $contact_query->have_posts() )
440 436 {
441 437 $default_subject = get_option( 'propertyhive_property_match_default_email_subject', '' );
@@ -460,9 +456,9 @@
460 456 ),
461 457 );
462 458 $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
463 459
464 - $default_body = wp_kses($default_body, $allowedposttags);
460 + $default_body = wp_kses( $default_body, $allowed_tags );
465 461
466 462 while ( $contact_query->have_posts() )
467 463 {
468 464 $contact_query->the_post();
@@ -468,14 +464,14 @@
468 464 $contact_query->the_post();
469 465
470 466 $contact_id = get_the_ID();
471 467
472 - if ( $dry_run === true ) { echo 'Doing contact: ' . get_the_title() . "<br>\n"; }
468 + if ( $dry_run === true ) { echo 'Doing contact: ' . esc_html( get_the_title() ) . "<br>\n"; }
473 469
474 470 // invalid email address
475 471 if ( strpos( get_post_meta( $contact_id, '_email_address', TRUE ), '@' ) === FALSE )
476 472 {
477 - if ( $dry_run === true ) { echo 'Invalid email address. Skipping' . "<br>\n"; }
473 + if ( $dry_run === true ) { echo esc_html('Invalid email address. Skipping') . "<br>\n"; }
478 474
479 475 continue;
480 476 }
481 477
@@ -482,9 +478,9 @@
482 478 // email in the list of forbidden contact methods
483 479 $forbidden_contact_methods = get_post_meta( $contact_id, '_forbidden_contact_methods', TRUE );
484 480 if ( is_array($forbidden_contact_methods) && in_array('email', $forbidden_contact_methods) )
485 481 {
486 - if ( $dry_run === true ) { echo 'Email communication forbidden in contact preferences. Skipping' . "<br>\n"; }
482 + if ( $dry_run === true ) { echo esc_html('Email communication forbidden in contact preferences. Skipping') . "<br>\n"; }
487 483
488 484 continue;
489 485 }
490 486
@@ -497,9 +493,9 @@
497 493 {
498 494 $dismissed_properties = array();
499 495 }
500 496
501 - if ( $dry_run === true ) { if ( !empty($dismissed_properties) ) { echo 'Dismissed properties: ' . print_r($dismissed_properties, true) . "<br>\n"; } }
497 + if ( $dry_run === true ) { if ( !empty($dismissed_properties) ) { echo 'Dismissed properties: ' . esc_html( wp_json_encode( $dismissed_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; } }
502 498
503 499 for ( $i = 0; $i < $applicant_profiles; ++$i )
504 500 {
505 501 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
@@ -505,21 +501,21 @@
505 501 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
506 502
507 503 if ( $applicant_profile == '' || !is_array($applicant_profile) || !isset($applicant_profile['department']) )
508 504 {
509 - if ( $dry_run === true ) { echo 'Applicant relationship empty or no department set' . "<br>\n"; }
505 + if ( $dry_run === true ) { echo esc_html('Applicant relationship empty or no department set') . "<br>\n"; }
510 506 continue;
511 507 }
512 508
513 509 if ( !isset($applicant_profile['send_matching_properties']) || ( isset($applicant_profile['send_matching_properties']) && $applicant_profile['send_matching_properties'] != 'yes' ) )
514 510 {
515 - if ( $dry_run === true ) { echo 'Send matching properties disabled' . "<br>\n"; }
511 + if ( $dry_run === true ) { echo esc_html('Send matching properties disabled') . "<br>\n"; }
516 512 continue;
517 513 }
518 514
519 515 if ( isset($applicant_profile['auto_match_disabled']) && $applicant_profile['auto_match_disabled'] == 'yes' )
520 516 {
521 - if ( $dry_run === true ) { echo 'Auto match disabled' . "<br>\n"; }
517 + if ( $dry_run === true ) { echo esc_html('Auto match disabled') . "<br>\n"; }
522 518 continue;
523 519 }
524 520
525 521 if ( $dry_run === true ) { echo 'Getting matching properties' . "<br>\n"; }
@@ -525,9 +521,9 @@
525 521 if ( $dry_run === true ) { echo 'Getting matching properties' . "<br>\n"; }
526 522
527 523 $matching_properties = $ph_admin_matching_properties->get_matching_properties( $contact_id, $i, $auto_property_match_enabled_date );
528 524
529 - if ( $dry_run === true ) { echo 'Found ' . count($matching_properties) . ' matching properties' . "<br>\n"; }
525 + if ( $dry_run === true ) { echo esc_html('Found ' . count($matching_properties) . ' matching properties') . "<br>\n"; }
530 526
531 527 if ( !empty($matching_properties) )
532 528 {
533 529 $already_sent_properties = get_post_meta( $contact_id, '_applicant_profile_' . $i . '_match_history', TRUE );
@@ -534,9 +530,9 @@
534 530
535 531 // Remove from this array if on market changed or price changed
536 532 if ( is_array($already_sent_properties) )
537 533 {
538 - if ( $dry_run === true ) { echo 'Already sent properties before: ' . print_r($already_sent_properties, true) . "<br>\n"; }
534 + if ( $dry_run === true ) { echo 'Already sent properties before: ' . esc_html( wp_json_encode( $already_sent_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
539 535
540 536 foreach ( $already_sent_properties as $already_sent_property_id => $sends )
541 537 {
542 538 $highest_send = $sends[count($sends) - 1]['date'];
@@ -542,21 +538,21 @@
542 538 $highest_send = $sends[count($sends) - 1]['date'];
543 539
544 540 if ( $highest_send != '' )
545 541 {
546 - if ( $dry_run === true ) { echo 'Property: ' . $already_sent_property_id . ' last sent: ' . $highest_send . "<br>\n"; }
542 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last sent: ' . esc_html( $highest_send ) . "<br>\n"; }
547 543
548 544 $on_market_change_date = get_post_meta( $already_sent_property_id, '_on_market_change_date', TRUE );
549 545
550 - if ( $dry_run === true ) { echo 'Property: ' . $already_sent_property_id . ' last on market change: ' . $on_market_change_date . "<br>\n"; }
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"; }
551 547
552 548 $price_change_date = get_post_meta( $already_sent_property_id, '_price_change_date', TRUE );
553 549
554 - if ( $dry_run === true ) { echo 'Property: ' . $already_sent_property_id . ' last price change: ' . $price_change_date . "<br>\n"; }
550 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last price change: ' . esc_html( $price_change_date ) . "<br>\n"; }
555 551
556 552 if ( $on_market_change_date > $highest_send )
557 553 {
558 - if ( $dry_run === true ) { echo 'Property: ' . $already_sent_property_id . ' has changed on market since last sent' . "<br>\n"; }
554 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' has changed on market since last sent' . "<br>\n"; }
559 555
560 556 // This property has changed since it was last sent. Remove from already sent list so it gets sent again
561 557 unset($already_sent_properties[$already_sent_property_id]);
562 558 }
@@ -561,9 +557,9 @@
561 557 unset($already_sent_properties[$already_sent_property_id]);
562 558 }
563 559 elseif ( $price_change_date > $highest_send )
564 560 {
565 - if ( $dry_run === true ) { echo 'Property: ' . $already_sent_property_id . ' has changed price since last sent' . "<br>\n"; }
561 + if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' has changed price since last sent' . "<br>\n"; }
566 562
567 563 // This property has changed since it was last sent. Remove from already sent list so it gets sent again
568 564 unset($already_sent_properties[$already_sent_property_id]);
569 565 }
@@ -569,12 +565,12 @@
569 565 }
570 566 }
571 567 }
572 568
573 - if ( $dry_run === true ) { echo 'Already sent properties after: ' . print_r($already_sent_properties, true) . "<br>\n"; }
569 + if ( $dry_run === true ) { echo 'Already sent properties after: ' . esc_html( wp_json_encode( $already_sent_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
574 570 }
575 571
576 - if ( $dry_run === true ) { echo 'Matching properties before removing already sent: ' . print_r($matching_properties, true) . "<br>\n"; }
572 + if ( $dry_run === true ) { echo 'Matching properties before removing already sent: ' . esc_html( wp_json_encode( $matching_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
577 573
578 574 // Check properties haven't already been sent and not marked as 'not interested'
579 575 $new_matching_properties = array();
580 576 foreach ($matching_properties as $matching_property)
@@ -584,9 +580,9 @@
584 580 $new_matching_properties[] = $matching_property->id;
585 581 }
586 582 }
587 583
588 - if ( $dry_run === true ) { echo 'Matching properties after removing already sent: ' . print_r($new_matching_properties, true) . "<br>\n"; }
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"; }
589 585
590 586 $max_results = apply_filters( 'propertyhive_auto_match_maximum_results', FALSE);
591 587 if ( $max_results !== FALSE )
592 588 {
@@ -592,9 +588,9 @@
592 588 {
593 589 $new_matching_properties = array_slice($new_matching_properties, 0, (int)$max_results);
594 590 }
595 591
596 - if ( $dry_run === true ) { echo 'Found ' . count($new_matching_properties) . ' matching properties after removing already sent and dismissed' . "<br>\n"; }
592 + if ( $dry_run === true ) { echo esc_html('Found ' . count($new_matching_properties) . ' matching properties after removing already sent and dismissed') . "<br>\n"; }
597 593
598 594 if ( !empty($new_matching_properties) )
599 595 {
600 596 $subject = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $default_subject);
@@ -599,10 +595,10 @@
599 595 {
600 596 $subject = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $default_subject);
601 597
602 598 $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);
599 + $body = str_replace( '[contact_name]', esc_html( $contact->post_title ), $default_body );
600 + $body = str_replace( '[contact_dear]', esc_html( $contact->dear() ), $body );
605 601 $body = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $body);
606 602
607 603 $office_counts = array();
608 604 $negotiator_counts = array();
@@ -651,10 +647,10 @@
651 647 // fallback to admin email address
652 648 $highest_office_email_address = get_option('admin_email');
653 649 }
654 650
655 - $body = str_replace("[office_name]", $highest_office_name, $body);
656 - $body = str_replace("[office_email_address]", $highest_office_email_address, $body);
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 );
657 653
658 654 $highest_negotiator_name = '';
659 655 $highest_negotiator_email_address = '';
660 656 if ( !empty($negotiator_counts) )
@@ -665,10 +661,10 @@
665 661 $highest_negotiator_name = ( isset($negotiator_names[$highest_negotiator_id]) ? $negotiator_names[$highest_negotiator_id] : '' );
666 662 $highest_negotiator_email_address = ( isset($negotiator_email_addresses[$highest_negotiator_id]) ? $negotiator_email_addresses[$highest_negotiator_id] : '' );
667 663 }
668 664
669 - $body = str_replace("[negotiator_name]", $highest_negotiator_name, $body);
670 - $body = str_replace("[negotiator_email_address]", $highest_negotiator_email_address, $body);
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 );
671 667
672 668 $highest_office_email_address = apply_filters( 'propertyhive_auto_match_from_email_address', $highest_office_email_address );
673 669
674 670 if ( !$dry_run )
@@ -684,9 +680,9 @@
684 680 );
685 681 }
686 682 else
687 683 {
688 - echo 'Would\'ve sent email. Not sending due to being ran in dry run mode' . "<br>\n";
684 + echo esc_html('Would\'ve sent email. Not sending due to being ran in dry run mode') . "<br>\n";
689 685 }
690 686 }
691 687 }
692 688 }
@@ -692,14 +688,14 @@
692 688 }
693 689 }
694 690 else
695 691 {
696 - if ( $dry_run === true ) { echo 'No applicant profiles found. Skipping' . "<br>\n"; }
692 + if ( $dry_run === true ) { echo esc_html('No applicant profiles found. Skipping') . "<br>\n"; }
697 693 }
698 694 }
699 695 }
700 696
701 - if ( $dry_run === true ) { echo 'Finished auto-match process' . "<br>\n"; die(); }
697 + if ( $dry_run === true ) { echo esc_html('Finished auto-match process') . "<br>\n"; die(); }
702 698
703 699 wp_reset_postdata();
704 700 }
705 701
@@ -723,15 +719,15 @@
723 719 ph_get_template( 'emails/email-styles.php' );
724 720 $css = apply_filters( 'propertyhive_email_styles', ob_get_clean() );
725 721
726 722 // include css inliner
727 - if ( ! class_exists( 'Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
723 + if ( ! class_exists( 'PropertyHive_Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
728 724 include_once( dirname( __FILE__ ) . '/libraries/class-emogrifier.php' );
729 725 }
730 726
731 727 // apply CSS styles inline for picky email clients
732 728 try {
733 - $emogrifier = new Emogrifier( $content, $css );
729 + $emogrifier = new PropertyHive_Emogrifier( $content, $css );
734 730 $content = $emogrifier->emogrify();
735 731 } catch ( Exception $e ) {
736 732 die(esc_html("Error converting CSS styles to be inline. Error as follows: " . $e->getMessage()));
737 733 }
@@ -752,9 +748,9 @@
752 748 public function email_footer( $contact_id = '' ) {
753 749 $unsubscribe_link = '';
754 750 if ($contact_id != '')
755 751 {
756 - $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 );
757 753 }
758 754
759 755 ph_get_template( 'emails/email-footer.php', array( 'unsubscribe_link' => $unsubscribe_link ) );
760 756 }
@@ -771,8 +767,9 @@
771 767 ob_start();
772 768
773 769 do_action( 'propertyhive_email_header', $contact_id );
774 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.
775 772 echo wpautop( wptexturize( $message ) );
776 773
777 774 do_action( 'propertyhive_email_footer', $contact_id );
778 775
@@ -783,14 +780,26 @@
783 780 }
784 781
785 782 public function send_enquiry_auto_responder( $data = array() )
786 783 {
787 - if ( isset($data['property_id']) && !empty(ph_clean($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'] )
788 790 {
789 - $property_ids = ph_clean(explode("|", $data['property_id']));
790 - if ( !is_array($property_ids) ) { $property_ids = array($property_ids); }
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 + }
791 800
792 - $to = sanitize_email( $_POST['email_address'] );
801 + $to = isset( $request_data['email_address'] ) && is_string( $request_data['email_address'] ) ? sanitize_email( $request_data['email_address'] ) : '';
793 802 $subject = get_option( 'propertyhive_enquiry_auto_responder_email_subject', '' );
794 803 $body = get_option( 'propertyhive_enquiry_auto_responder_email_body', '' );
795 804
796 805 if ( $to != '' && $subject != '' && $body != '' )
@@ -798,14 +807,15 @@
798 807 $headers = array();
799 808 $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . get_option( 'propertyhive_email_from_address', get_option( 'admin_email' ) ) . '>';
800 809 $headers[] = 'Content-Type: text/html; charset=UTF-8';
801 810
802 - $body = str_replace( "[name]", ( isset($_POST['name']) ? ph_clean($_POST['name']) : '' ), $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 );
803 813
804 814 $property_address_hyperlinked = array();
805 815 foreach ( $property_ids as $property_id )
806 816 {
807 - $property_address_hyperlinked[] = '<a href="' . get_permalink($property_id) . '">' . get_the_title($property_id) . '</a>';
817 + $property_address_hyperlinked[] = '<a href="' . esc_url( get_permalink( $property_id ) ) . '">' . esc_html( get_the_title( $property_id ) ) . '</a>';
808 818 }
809 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 );
810 820
811 821 if ( strpos( $body, '[similar_properties]' ) !== FALSE )
@@ -821,8 +831,9 @@
821 831 'post_type' => 'property',
822 832 'post_status' => 'publish',
823 833 'posts_per_page' => 3,
824 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.
825 836 'post__not_in' => array($property_id),
826 837 );
827 838
828 839 $meta_query = array();
@@ -886,13 +897,15 @@
886 897 );
887 898 }
888 899 }
889 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.
890 902 $args['meta_query'] = $meta_query;
891 903
892 904 $property_match_statuses = get_option( 'propertyhive_property_match_statuses', '' );
893 905 if ( $property_match_statuses != '' && is_array($property_match_statuses) && !empty($property_match_statuses) )
894 906 {
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.
895 908 $args['tax_query'] = array(
896 909 array(
897 910 'taxonomy' => 'availability',
898 911 'field' => 'term_id',
@@ -933,5 +946,5 @@
933 946 wp_mail( $to, $subject, $body, $headers );
934 947 }
935 948 }
936 949 }
937 -}
950 +}