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
propertyhive / includes / class-ph-emails.php

class-ph-emails.php in Property Hive 2.3.1, at includes/class-ph-emails.php

985 lines 37.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 // ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
4
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit; // Exit if accessed directly
8 }
9
10 /**
11 * Transactional Emails Controller
12 *
13 * Property Hive Emails Class which handles the sending on transactional emails and email templates. This class loads in available emails.
14 *
15 * @class PH_Emails
16 * @version 1.0.0
17 * @package PropertyHive/Classes/Emails
18 * @category Class
19 * @author PropertyHive
20 */
21 class PH_Emails {
22
23 /** @var PH_Emails The single instance of the class */
24 protected static $_instance = null;
25
26 /**
27 * Main PH_Emails Instance.
28 *
29 * Ensures only one instance of PH_Emails is loaded or can be loaded.
30 *
31 * @since 1.0.0
32 * @static
33 * @return PH_Emails Main instance
34 */
35 public static function instance() {
36 if ( is_null( self::$_instance ) ) {
37 self::$_instance = new self();
38 }
39 return self::$_instance;
40 }
41
42 /**
43 * Cloning is forbidden.
44 *
45 * @since 1.0.0
46 */
47 public function __clone() {
48 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'propertyhive' ), '1.0.0' );
49 }
50
51 /**
52 * Unserializing instances of this class is forbidden.
53 *
54 * @since 1.0.0
55 */
56 public function __wakeup() {
57 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'propertyhive' ), '1.0.0' );
58 }
59
60 /**
61 * Constructor for the email class hooks in all emails that can be sent.
62 *
63 */
64 public function __construct() {
65 $this->init();
66
67 // Email Header, Footer
68 add_action( 'propertyhive_email_header', array( $this, 'email_header' ), 10, 1 );
69 add_action( 'propertyhive_email_footer', array( $this, 'email_footer' ), 10, 1 );
70
71 add_action( 'propertyhive_process_email_log', array( $this, 'ph_process_email_log' ) );
72 add_action( 'propertyhive_auto_email_match', array( $this, 'ph_auto_email_match' ) );
73
74 // Send applicant registration email
75 add_action( 'propertyhive_applicant_registered', array( $this, 'send_applicant_registration_alert' ), 10, 2 );
76
77 add_action( 'admin_init', array( $this, 'run_custom_email_cron' ), 10, 1 );
78
79 add_action( 'init', array( $this, 'check_email_queue_is_scheduled'), 99 );
80 }
81
82 public function check_email_queue_is_scheduled()
83 {
84 $schedule = wp_get_schedule( 'propertyhive_process_email_log' );
85
86 if ( $schedule === FALSE )
87 {
88 // Hmm... cron job not found. Let's set it up
89 $timestamp = wp_next_scheduled( 'propertyhive_process_email_log' );
90 wp_unschedule_event($timestamp, 'propertyhive_process_email_log' );
91 wp_clear_scheduled_hook('propertyhive_process_email_log');
92
93 wp_schedule_event( time(), 'every_fifteen_minutes', 'propertyhive_process_email_log' );
94 }
95 }
96
97 public function run_custom_email_cron()
98 {
99 if ( isset( $_GET['custom_email_log_cron'] ) )
100 {
101 if ( ! current_user_can( 'manage_propertyhive' ) ) {
102 wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
103 }
104 check_admin_referer( 'propertyhive-run-email-job' );
105 $job = is_string( $_GET['custom_email_log_cron'] ) ? sanitize_key( wp_unslash( $_GET['custom_email_log_cron'] ) ) : '';
106 if ( ! in_array( $job, array( 'propertyhive_process_email_log', 'propertyhive_auto_email_match' ), true ) ) {
107 wp_die( esc_html__( 'Invalid email job.', 'propertyhive' ), '', array( 'response' => 400 ) );
108 }
109 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Dynamic email cron dispatch is a bounded internal false positive: the value is sanitized, restricted to two propertyhive_* jobs, and reached only after capability and nonce checks.
110 do_action( $job );
111 }
112 }
113
114 public function send_applicant_registration_alert( $contact_post_id, $user_id )
115 {
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'] ) : '';
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
125 if ( $to == '' )
126 {
127 $to = get_option( 'admin_email', '' );
128 }
129
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 );
135
136 $message = __( "A new applicant has registered through your website. Please find details of the applicant below:", 'propertyhive' ) . "\n\n";
137
138 $message = apply_filters( 'propertyhive_applicant_registration_email_pre_body', $message, $contact_post_id );
139
140 $form_controls = ph_get_user_details_form_fields();
141
142 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
143
144 $form_controls_2 = ph_get_applicant_requirements_form_fields();
145
146 $form_controls_2 = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls_2, get_post_meta( $contact_post_id, '_applicant_profile_0', true ) );
147
148 $form_controls = array_merge( $form_controls, $form_controls_2 );
149
150 $form_controls = apply_filters( 'propertyhive_applicant_registration_form_fields', $form_controls );
151
152 unset($form_controls['office_id']);
153
154 foreach ($form_controls as $key => $control)
155 {
156 if ( isset($control['type']) && $control['type'] == 'password' )
157 {
158 continue;
159 }
160
161 $raw_value = isset( $request_post[ $key ] ) && ( is_string( $request_post[ $key ] ) || is_array( $request_post[ $key ] ) ) ? $request_post[ $key ] : '';
162 $value = ph_clean( $raw_value );
163 $values = array();
164 if ( !empty($value) && taxonomy_exists($key) )
165 {
166 if ( !is_array($value) ) { $value = array($value); }
167
168 foreach ( $value as $value_term )
169 {
170 $term = get_term( ph_clean($value_term), $key );
171
172 if ( isset($term->name) )
173 {
174 $values[] = $term->name;
175 }
176 }
177
178 $value = implode(", ", $values);
179 }
180
181 if ( ph_clean($value) == '' )
182 {
183 continue;
184 }
185
186 $label = ( isset($control['label']) ) ? $control['label'] : $key;
187 $message .= $label . ": " . $value . "\n";
188 }
189
190 wp_mail($to, $subject, $message);
191 }
192 }
193
194 /**
195 * Process ph_email_log table. Handle failed, hung and pending emails
196 */
197 public function ph_process_email_log()
198 {
199 global $wpdb;
200
201 $lock_id = uniqid( "", true );
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 ) );
214
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
229 foreach ( $emails_to_send as $email_to_send )
230 {
231 $email_id = $email_to_send->email_id;
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
266 $headers = array();
267 $headers[] = 'From: ' . html_entity_decode($email_to_send->from_name) . ' <' . $email_to_send->from_email_address . '>';
268 if ( $email_to_send->cc_email_address != '' )
269 {
270 $headers[] = 'Cc: ' . $email_to_send->cc_email_address;
271 }
272 if ( $email_to_send->bcc_email_address != '' )
273 {
274 $headers[] = 'Bcc: ' . $email_to_send->bcc_email_address;
275 }
276 $headers[] = 'Content-Type: text/html; charset=UTF-8';
277
278 $body = $email_to_send->body;
279
280 if ( extension_loaded('zlib') && @gzuncompress($body) !== false )
281 {
282 $body = gzuncompress($body);
283 }
284
285 $body = apply_filters( 'propertyhive_mail_content', $this->style_inline( $this->wrap_message( $body, $email_to_send->contact_id ) ) );
286
287 $sent = wp_mail(
288 $email_to_send->to_email_address,
289 $email_to_send->subject,
290 $body,
291 $headers/*,
292 string|array $attachments = array() */
293 );
294
295 $new_status = '';
296 if ( $sent )
297 {
298 // Sent successfully
299 $new_status = 'sent';
300 }
301 else
302 {
303 // Failed to send
304 if ($email_to_send->status == '')
305 {
306 $new_status = 'fail1';
307 }
308 else
309 {
310 $new_status = 'fail2';
311 }
312 }
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 );
321 }
322
323 // Delete old logs
324 $keep_logs_days = (string)apply_filters( 'propertyhive_keep_email_logs_days', '3650' ); // 10 years
325
326 // Revert back to 3650 days if anything other than numbers has been passed
327 // This prevent SQL injection and errors
328 if ( !preg_match("/^\d+$/", $keep_logs_days) )
329 {
330 $keep_logs_days = '3650';
331 }
332
333 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The email queue is a custom plugin table and this retention cleanup has no WordPress API equivalent.
334 $wpdb->query( $wpdb->prepare(
335 "DELETE FROM {$wpdb->prefix}ph_email_log WHERE send_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
336 (int) $keep_logs_days
337 ) );
338 }
339
340 /*
341 * Automatically send new properties to registered applicants
342 */
343 public function ph_auto_email_match()
344 {
345 global $post;
346
347 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- dry_run only selects diagnostic output and never changes persisted data; the real email action is capability and nonce protected in run_custom_email_cron().
348 $request_get = wp_unslash( $_GET );
349 $dry_run = isset( $request_get['dry_run'] );
350
351 if ( $dry_run === true ) { echo 'Running auto-match in dry run mode. Logging will be output and no emails will be sent.' . "<br>\n"; }
352
353 // Auto emails enabled in settings
354 // Auto emails not disabled in applicant record
355 // Property added more recently that setting enabled in settings
356 // Property not already previously sent
357 // Valid email address
358 // 'Do not email' not selected
359
360 $auto_property_match_enabled = get_option( 'propertyhive_auto_property_match', '' );
361
362 if ( $dry_run === true ) { echo 'Auto-match setting enabled: ' . esc_html( $auto_property_match_enabled ) . "<br>\n"; }
363
364 if ( $auto_property_match_enabled == '' )
365 {
366 return false;
367 }
368
369 $auto_property_match_enabled_date = get_option( 'propertyhive_auto_property_match_enabled_date', '' );
370
371 if ( $dry_run === true ) { echo 'Auto-match setting enabled date: ' . esc_html( $auto_property_match_enabled_date ) . "<br>\n"; }
372
373 if ( $auto_property_match_enabled_date == '' )
374 {
375 return false;
376 }
377
378 // Get all of the offices and store them in an array to save having to query them for every email
379 $args = array(
380 'post_type' => 'office',
381 'nopaging' => true
382 );
383
384 $office_names = array();
385 $office_email_addresses = array();
386 $office_query = new WP_Query( $args );
387
388 if ( $office_query->have_posts() )
389 {
390 while ( $office_query->have_posts() )
391 {
392 $office_query->the_post();
393
394 $office_names[get_the_ID()] = get_the_title( get_the_ID() );
395
396 $office_email_addresses['residential-sales'][get_the_ID()] = get_post_meta( get_the_ID(), '_office_email_address_sales', TRUE );
397 $office_email_addresses['residential-lettings'][get_the_ID()] = get_post_meta( get_the_ID(), '_office_email_address_lettings', TRUE );
398 $office_email_addresses['commercial'][get_the_ID()] = get_post_meta( get_the_ID(), '_office_email_address_commercial', TRUE );
399 }
400 }
401
402 wp_reset_postdata();
403
404 // Get all of the negotiators and store them in an array to save having to query them for every email
405 $negotiator_names = array();
406 $negotiator_email_addresses = array();
407
408 $args = array(
409 'number' => 9999,
410 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy Property Negotiator compatibility filter; existing role filters depend on this exact public hook name.
411 'role__not_in' => apply_filters( 'property_negotiator_exclude_roles', array('property_hive_contact', 'subscriber') ),
412 'fields' => array( 'ID', 'display_name', 'user_email' )
413 );
414
415 $args = apply_filters( 'propertyhive_negotiators_query', $args );
416
417 $user_query = new WP_User_Query( $args );
418
419 $negotiators = array();
420
421 if ( ! empty( $user_query->results ) )
422 {
423 foreach ( $user_query->results as $user )
424 {
425 $negotiator_names[$user->ID] = $user->display_name;
426
427 $negotiator_email_addresses[$user->ID] = $user->user_email;
428 }
429 }
430
431 include_once( dirname(__FILE__) . '/admin/class-ph-admin-matching-properties.php' );
432 $ph_admin_matching_properties = new PH_Admin_Matching_Properties();
433
434 $meta_query = array(
435 array(
436 'key' => '_contact_types',
437 'value' => 'applicant',
438 'compare' => 'LIKE'
439 )
440 );
441
442 if ( version_compare( get_bloginfo( 'version' ), '5.1', '>=' ) )
443 {
444 // If WP version contains compare_key, check contact has at least one applicant profile with Send Matching Properties checked
445 $meta_query[] = array(
446 'key' => '_applicant_profile_',
447 'compare_key' => 'LIKE',
448 'value' => 'send_matching_properties";s:3:"yes"',
449 'compare' => 'LIKE',
450 );
451 }
452
453 // Get all contacts that have a type of applicant
454 $args = array(
455 'post_type' => 'contact',
456 'post_status' => 'publish',
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.
459 'meta_query' => $meta_query,
460 'fields' => 'ids'
461 );
462
463 if ( $dry_run === true ) { echo 'Running query to get contacts with args: ' . esc_html( wp_json_encode( $args, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
464
465 $contact_query = new WP_Query( $args );
466
467 if ( $dry_run === true ) { echo 'Found ' . esc_html( $contact_query->found_posts ) . ' contacts' . "<br>\n"; }
468
469 if ( $contact_query->have_posts() )
470 {
471 $default_subject = get_option( 'propertyhive_property_match_default_email_subject', '' );
472 $default_body = get_option( 'propertyhive_property_match_default_email_body', '' );
473
474 $allowed_tags = array(
475 'strong' => array(),
476 'span' => array(),
477 'em' => array(),
478 'h1' => array(),
479 'h2' => array(),
480 'h3' => array(),
481 'h4' => array(),
482 'h5' => array(),
483 'h6' => array(),
484 'i' => array(),
485 'u' => array(),
486 'b' => array(),
487 'a' => array(
488 'href' => array(),
489 'target' => array(),
490 ),
491 );
492 $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
493
494 $default_body = wp_kses( $default_body, $allowed_tags );
495
496 while ( $contact_query->have_posts() )
497 {
498 $contact_query->the_post();
499
500 $contact_id = get_the_ID();
501
502 if ( $dry_run === true ) { echo 'Doing contact: ' . esc_html( get_the_title() ) . "<br>\n"; }
503
504 // invalid email address
505 if ( strpos( get_post_meta( $contact_id, '_email_address', TRUE ), '@' ) === FALSE )
506 {
507 if ( $dry_run === true ) { echo esc_html('Invalid email address. Skipping') . "<br>\n"; }
508
509 continue;
510 }
511
512 // email in the list of forbidden contact methods
513 $forbidden_contact_methods = get_post_meta( $contact_id, '_forbidden_contact_methods', TRUE );
514 if ( is_array($forbidden_contact_methods) && in_array('email', $forbidden_contact_methods) )
515 {
516 if ( $dry_run === true ) { echo esc_html('Email communication forbidden in contact preferences. Skipping') . "<br>\n"; }
517
518 continue;
519 }
520
521 $applicant_profiles = get_post_meta( $contact_id, '_applicant_profiles', TRUE );
522
523 if ( $applicant_profiles != '' && $applicant_profiles > 0 )
524 {
525 $dismissed_properties = get_post_meta( $contact_id, '_dismissed_properties', TRUE );
526 if ( $dismissed_properties == '' )
527 {
528 $dismissed_properties = array();
529 }
530
531 if ( $dry_run === true ) { if ( !empty($dismissed_properties) ) { echo 'Dismissed properties: ' . esc_html( wp_json_encode( $dismissed_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; } }
532
533 for ( $i = 0; $i < $applicant_profiles; ++$i )
534 {
535 $applicant_profile = get_post_meta( $contact_id, '_applicant_profile_' . $i, TRUE );
536
537 if ( $applicant_profile == '' || !is_array($applicant_profile) || !isset($applicant_profile['department']) )
538 {
539 if ( $dry_run === true ) { echo esc_html('Applicant relationship empty or no department set') . "<br>\n"; }
540 continue;
541 }
542
543 if ( !isset($applicant_profile['send_matching_properties']) || ( isset($applicant_profile['send_matching_properties']) && $applicant_profile['send_matching_properties'] != 'yes' ) )
544 {
545 if ( $dry_run === true ) { echo esc_html('Send matching properties disabled') . "<br>\n"; }
546 continue;
547 }
548
549 if ( isset($applicant_profile['auto_match_disabled']) && $applicant_profile['auto_match_disabled'] == 'yes' )
550 {
551 if ( $dry_run === true ) { echo esc_html('Auto match disabled') . "<br>\n"; }
552 continue;
553 }
554
555 if ( $dry_run === true ) { echo 'Getting matching properties' . "<br>\n"; }
556
557 $matching_properties = $ph_admin_matching_properties->get_matching_properties( $contact_id, $i, $auto_property_match_enabled_date );
558
559 if ( $dry_run === true ) { echo esc_html('Found ' . count($matching_properties) . ' matching properties') . "<br>\n"; }
560
561 if ( !empty($matching_properties) )
562 {
563 $already_sent_properties = get_post_meta( $contact_id, '_applicant_profile_' . $i . '_match_history', TRUE );
564
565 // Remove from this array if on market changed or price changed
566 if ( is_array($already_sent_properties) )
567 {
568 if ( $dry_run === true ) { echo 'Already sent properties before: ' . esc_html( wp_json_encode( $already_sent_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
569
570 foreach ( $already_sent_properties as $already_sent_property_id => $sends )
571 {
572 $highest_send = $sends[count($sends) - 1]['date'];
573
574 if ( $highest_send != '' )
575 {
576 if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last sent: ' . esc_html( $highest_send ) . "<br>\n"; }
577
578 $on_market_change_date = get_post_meta( $already_sent_property_id, '_on_market_change_date', TRUE );
579
580 if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last on market change: ' . esc_html( $on_market_change_date ) . "<br>\n"; }
581
582 $price_change_date = get_post_meta( $already_sent_property_id, '_price_change_date', TRUE );
583
584 if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' last price change: ' . esc_html( $price_change_date ) . "<br>\n"; }
585
586 if ( $on_market_change_date > $highest_send )
587 {
588 if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' has changed on market since last sent' . "<br>\n"; }
589
590 // This property has changed since it was last sent. Remove from already sent list so it gets sent again
591 unset($already_sent_properties[$already_sent_property_id]);
592 }
593 elseif ( $price_change_date > $highest_send )
594 {
595 if ( $dry_run === true ) { echo 'Property: ' . esc_html( $already_sent_property_id ) . ' has changed price since last sent' . "<br>\n"; }
596
597 // This property has changed since it was last sent. Remove from already sent list so it gets sent again
598 unset($already_sent_properties[$already_sent_property_id]);
599 }
600 }
601 }
602
603 if ( $dry_run === true ) { echo 'Already sent properties after: ' . esc_html( wp_json_encode( $already_sent_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
604 }
605
606 if ( $dry_run === true ) { echo 'Matching properties before removing already sent: ' . esc_html( wp_json_encode( $matching_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
607
608 // Check properties haven't already been sent and not marked as 'not interested'
609 $new_matching_properties = array();
610 foreach ($matching_properties as $matching_property)
611 {
612 if ( !isset($already_sent_properties[$matching_property->id]) && !in_array($matching_property->id, $dismissed_properties) )
613 {
614 $new_matching_properties[] = $matching_property->id;
615 }
616 }
617
618 if ( $dry_run === true ) { echo 'Matching properties after removing already sent: ' . esc_html( wp_json_encode( $new_matching_properties, JSON_PRETTY_PRINT ) ) . "<br>\n"; }
619
620 $max_results = apply_filters( 'propertyhive_auto_match_maximum_results', FALSE);
621 if ( $max_results !== FALSE )
622 {
623 $new_matching_properties = array_slice($new_matching_properties, 0, (int)$max_results);
624 }
625
626 if ( $dry_run === true ) { echo esc_html('Found ' . count($new_matching_properties) . ' matching properties after removing already sent and dismissed') . "<br>\n"; }
627
628 if ( !empty($new_matching_properties) )
629 {
630 $subject = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $default_subject);
631
632 $contact = new PH_Contact($contact_id);
633 $body = str_replace( '[contact_name]', esc_html( $contact->post_title ), $default_body );
634 $body = str_replace( '[contact_dear]', esc_html( $contact->dear() ), $body );
635 $body = str_replace("[property_count]", count($new_matching_properties) . ' propert' . ( ( count($new_matching_properties) != 1 ) ? 'ies' : 'y' ), $body);
636
637 $office_counts = array();
638 $negotiator_counts = array();
639
640 if ( strpos($body, '[properties]') !== FALSE )
641 {
642 ob_start();
643
644 if ( !empty($new_matching_properties) )
645 {
646 foreach ( $new_matching_properties as $email_property_id )
647 {
648 $property = new PH_Property((int)$email_property_id);
649
650 if ( $property->office_id != '' && $property->office_id != 0 )
651 {
652 if ( !isset($office_counts[$property->office_id]) ) { $office_counts[$property->office_id] = 0; }
653 ++$office_counts[$property->office_id];
654 }
655
656 if ( $property->negotiator_id != '' && $property->negotiator_id != 0 )
657 {
658 if ( !isset($negotiator_counts[$property->negotiator_id]) ) { $negotiator_counts[$property->negotiator_id] = 0; }
659 ++$negotiator_counts[$property->negotiator_id];
660 }
661
662 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
663 }
664 }
665 $body = str_replace("[properties]", ob_get_clean(), $body);
666 }
667
668 // Get email address of office with most properties
669 $highest_office_name = '';
670 $highest_office_email_address = '';
671 if ( !empty($office_counts) )
672 {
673 arsort($office_counts);
674 reset($office_counts);
675 $highest_office_id = key($office_counts);
676 $highest_office_name = ( isset($office_names[$highest_office_id]) ? $office_names[$highest_office_id] : '' );
677 $highest_office_email_address = ( isset($office_email_addresses[$applicant_profile['department']][$highest_office_id]) ? $office_email_addresses[$applicant_profile['department']][$highest_office_id] : '' );
678 }
679 if ( $highest_office_email_address == '' )
680 {
681 // fallback to admin email address
682 $highest_office_email_address = get_option('admin_email');
683 }
684
685 $body = str_replace( '[office_name]', esc_html( $highest_office_name ), $body );
686 $body = str_replace( '[office_email_address]', esc_html( $highest_office_email_address ), $body );
687
688 $highest_negotiator_name = '';
689 $highest_negotiator_email_address = '';
690 if ( !empty($negotiator_counts) )
691 {
692 arsort($negotiator_counts);
693 reset($negotiator_counts);
694 $highest_negotiator_id = key($negotiator_counts);
695 $highest_negotiator_name = ( isset($negotiator_names[$highest_negotiator_id]) ? $negotiator_names[$highest_negotiator_id] : '' );
696 $highest_negotiator_email_address = ( isset($negotiator_email_addresses[$highest_negotiator_id]) ? $negotiator_email_addresses[$highest_negotiator_id] : '' );
697 }
698
699 $body = str_replace( '[negotiator_name]', esc_html( $highest_negotiator_name ), $body );
700 $body = str_replace( '[negotiator_email_address]', esc_html( $highest_negotiator_email_address ), $body );
701
702 $highest_office_email_address = apply_filters( 'propertyhive_auto_match_from_email_address', $highest_office_email_address );
703
704 if ( !$dry_run )
705 {
706 $ph_admin_matching_properties->send_emails(
707 $contact_id,
708 $i,
709 $new_matching_properties,
710 get_bloginfo('name'),
711 $highest_office_email_address,
712 $subject,
713 $body
714 );
715 }
716 else
717 {
718 echo esc_html('Would\'ve sent email. Not sending due to being ran in dry run mode') . "<br>\n";
719 }
720 }
721 }
722 }
723 }
724 else
725 {
726 if ( $dry_run === true ) { echo esc_html('No applicant profiles found. Skipping') . "<br>\n"; }
727 }
728 }
729 }
730
731 if ( $dry_run === true ) { echo esc_html('Finished auto-match process') . "<br>\n"; die(); }
732
733 wp_reset_postdata();
734 }
735
736 /**
737 * Init email classes.
738 */
739 public function init() {
740
741 }
742
743 /**
744 * Apply inline styles to dynamic content.
745 *
746 * @param string|null $content
747 * @return string
748 */
749 public function style_inline( $content ) {
750 // make sure we only inline CSS for html emails
751 if ( class_exists( 'DOMDocument' ) ) {
752 ob_start();
753 ph_get_template( 'emails/email-styles.php' );
754 $css = apply_filters( 'propertyhive_email_styles', ob_get_clean() );
755
756 // include css inliner
757 if ( ! class_exists( 'PropertyHive_Emogrifier' ) && class_exists( 'DOMDocument' ) ) {
758 include_once( dirname( __FILE__ ) . '/libraries/class-emogrifier.php' );
759 }
760
761 // apply CSS styles inline for picky email clients
762 try {
763 $emogrifier = new PropertyHive_Emogrifier( $content, $css );
764 $content = $emogrifier->emogrify();
765 } catch ( Exception $e ) {
766 die(esc_html("Error converting CSS styles to be inline. Error as follows: " . $e->getMessage()));
767 }
768 }
769 return $content;
770 }
771
772 /**
773 * Get the email header.
774 */
775 public function email_header( $contact_id = '' ) {
776 ph_get_template( 'emails/email-header.php' );
777 }
778
779 /**
780 * Get the email footer.
781 */
782 public function email_footer( $contact_id = '' ) {
783 $unsubscribe_link = '';
784 if ($contact_id != '')
785 {
786 $unsubscribe_link = PH()->get_contact_unsubscribe_url( $contact_id );
787 }
788
789 ph_get_template( 'emails/email-footer.php', array( 'unsubscribe_link' => $unsubscribe_link ) );
790 }
791
792 /**
793 * Wraps a message in the Property Hive mail template.
794 *
795 * @param mixed $email_heading
796 * @param string $message
797 * @return string
798 */
799 public function wrap_message( $message, $contact_id = '', $plain_text = false ) {
800 // Buffer
801 ob_start();
802
803 do_action( 'propertyhive_email_header', $contact_id );
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.
806 echo wpautop( wptexturize( $message ) );
807
808 do_action( 'propertyhive_email_footer', $contact_id );
809
810 // Get contents
811 $message = ob_get_clean();
812
813 return $message;
814 }
815
816 public function send_enquiry_auto_responder( $data = array() )
817 {
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'] )
824 {
825 $property_ids = array_filter( array_map( 'absint', explode( '|', $request_data['property_id'] ) ) );
826 if ( empty( $property_ids ) || count( $property_ids ) > 100 ) {
827 return;
828 }
829 foreach ( $property_ids as $property_id ) {
830 if ( 'property' !== get_post_type( $property_id ) || ! propertyhive_is_post_publicly_viewable( $property_id ) ) {
831 return;
832 }
833 }
834
835 $to = isset( $request_data['email_address'] ) && is_string( $request_data['email_address'] ) ? sanitize_email( $request_data['email_address'] ) : '';
836 $subject = get_option( 'propertyhive_enquiry_auto_responder_email_subject', '' );
837 $body = get_option( 'propertyhive_enquiry_auto_responder_email_body', '' );
838
839 if ( $to != '' && $subject != '' && $body != '' )
840 {
841 $headers = array();
842 $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . get_option( 'propertyhive_email_from_address', get_option( 'admin_email' ) ) . '>';
843 $headers[] = 'Content-Type: text/html; charset=UTF-8';
844
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 );
847
848 $property_address_hyperlinked = array();
849 foreach ( $property_ids as $property_id )
850 {
851 $property_address_hyperlinked[] = '<a href="' . esc_url( get_permalink( $property_id ) ) . '">' . esc_html( get_the_title( $property_id ) ) . '</a>';
852 }
853 $body = str_replace( "[property_address_hyperlinked]", implode(' and ', array_filter(array_merge(array(join(', ', array_slice($property_address_hyperlinked, 0, -1))), array_slice($property_address_hyperlinked, -1)), 'strlen')), $body );
854
855 if ( strpos( $body, '[similar_properties]' ) !== FALSE )
856 {
857 $similar_html = '';
858
859 foreach ( $property_ids as $property_id )
860 {
861 $department = get_post_meta( $property_id, '_department', TRUE );
862
863 // Get three similar properties
864 $args = array(
865 'post_type' => 'property',
866 'post_status' => 'publish',
867 'posts_per_page' => 3,
868 'orderby' => 'rand',
869 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Each email’s similar-property query returns three properties and excludes the current property. posts_per_page=3; post__not_in is one property ID; metadata/taxonomy match the recommendation rules.
870 'post__not_in' => array($property_id),
871 );
872
873 $meta_query = array();
874
875 $meta_query[] = array(
876 'key' => '_department',
877 'value' => $department,
878 );
879
880 $meta_query[] = array(
881 'key' => '_on_market',
882 'value' => 'yes',
883 );
884
885 if ( $department != 'commercial' && ph_get_custom_department_based_on( $department ) != 'commercial' )
886 {
887 $bedrooms = get_post_meta( $property_id, '_bedrooms', TRUE );
888
889 $meta_query[] = array(
890 'key' => '_bedrooms',
891 'value' => $bedrooms,
892 'type' => 'NUMERIC'
893 );
894
895
896 $price = get_post_meta( $property_id, '_price_actual', true );
897 $lower_price = $price - ($price / 10);
898 $higher_price = $price + ($price / 10);
899
900 $meta_query[] = array(
901 'key' => '_price_actual',
902 'value' => $lower_price,
903 'compare' => '>=',
904 'type' => 'NUMERIC'
905 );
906
907 $meta_query[] = array(
908 'key' => '_price_actual',
909 'value' => $higher_price,
910 'compare' => '<=',
911 'type' => 'NUMERIC'
912 );
913 }
914 else
915 {
916 $for_sale = get_post_meta( $property_id, '_for_sale', true );
917 $to_rent = get_post_meta( $property_id, '_to_rent', true );
918
919 if ( $for_sale == 'yes' )
920 {
921 $meta_query[] = array(
922 'key' => '_for_sale',
923 'value' => 'yes',
924 );
925 }
926 elseif ( $to_rent == 'yes' )
927 {
928 $meta_query[] = array(
929 'key' => '_to_rent',
930 'value' => 'yes',
931 );
932 }
933 }
934
935 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Each email’s similar-property query returns three properties and excludes the current property. posts_per_page=3; post__not_in is one property ID; metadata/taxonomy match the recommendation rules.
936 $args['meta_query'] = $meta_query;
937
938 $property_match_statuses = get_option( 'propertyhive_property_match_statuses', '' );
939 if ( $property_match_statuses != '' && is_array($property_match_statuses) && !empty($property_match_statuses) )
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.
942 $args['tax_query'] = array(
943 array(
944 'taxonomy' => 'availability',
945 'field' => 'term_id',
946 'terms' => $property_match_statuses,
947 'operator' => 'IN',
948 )
949 );
950 }
951
952 $properties_query = new WP_Query( apply_filters( 'propertyhive_auto_responder_similar_properties_query', $args, $property_id ) );
953
954 if ( $properties_query->have_posts() )
955 {
956 while ( $properties_query->have_posts() )
957 {
958 $properties_query->the_post();
959
960 $property = new PH_Property( get_the_ID() );
961
962 ob_start();
963
964 ph_get_template( 'emails/enquiry-autoresponder-similar-property.php', array( 'property' => $property ) );
965
966 $similar_html .= ob_get_clean();
967 }
968 }
969 }
970 if ( !empty($similar_html) )
971 {
972 $similar_html = '<br><hr><br><h3>Similar Properties You Might Like:</h3>' . $similar_html;
973 }
974 $body = str_replace( "[similar_properties]", $similar_html, $body );
975 }
976
977 $body = apply_filters( 'propertyhive_enquiry_auto_responder_body', $body, $property_ids[0] );
978 $body = apply_filters( 'propertyhive_mail_content', $this->style_inline( $this->wrap_message( $body ) ) );
979
980 wp_mail( $to, $subject, $body, $headers );
981 }
982 }
983 }
984 }
985