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

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

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