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-ajax.php

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

8,704 lines 375.6 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' ) ) exit; // Exit if accessed directly
7
8 /**
9 * PropertyHive PH_AJAX
10 *
11 * AJAX Event Handler
12 *
13 * @class PH_AJAX
14 * @version 1.0.0
15 * @package PropertyHive/Classes
16 * @category Class
17 * @author PropertyHive
18 */
19 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_AJAX; preserving the existing PH_* class name is required for plugin and extension compatibility.
20 class PH_AJAX {
21
22 /**
23 * Hook into ajax events
24 */
25 public function __construct() {
26
27 // propertyhive_EVENT => nopriv
28 $ajax_events = array(
29 'add_note' => false,
30 'delete_note' => false,
31 'toggle_note_pinned' => false,
32 'get_notes_grid' => false,
33 'get_pinned_notes_grid' => false,
34 'fetch_note_mentions' => false,
35 'search_contacts' => false,
36 'search_properties' => false,
37 'search_negotiators' => false,
38 'load_existing_owner_contact' => false,
39 'load_existing_features' => false,
40 'make_property_enquiry' => true,
41 'create_contact_from_enquiry' => false,
42 'merge_contact_records' => false,
43
44 // Dashboard components
45 'get_news' => false,
46 'get_viewings_awaiting_applicant_feedback' => false,
47 'get_my_upcoming_appointments' => false,
48 'get_upcoming_overdue_key_dates' => false,
49
50 // Property actions
51 'check_duplicate_reference_number' => false,
52 'osm_geocoding_request' => false,
53 'get_property_marketing_statistics_meta_box' => false,
54 'get_property_tenancies_grid' => false,
55
56 // Contact actions
57 'create_contact_login' => false,
58 'get_contact_tenancies_grid' => false,
59 'get_contact_solicitor' => false,
60
61 // Appraisal actions
62 'get_appraisal_details_meta_box' => false,
63 'get_appraisal_actions' => false,
64 'appraisal_carried_out' => false,
65 'appraisal_cancelled' => false,
66 'appraisal_won' => false,
67 'appraisal_lost_reason' => false,
68 'appraisal_instructed' => false,
69 'appraisal_email_owner_booking_confirmation' => false,
70 'appraisal_revert_pending' => false,
71 'appraisal_revert_carried_out' => false,
72 'appraisal_revert_won' => false,
73
74 // Viewing actions
75 'book_viewing_property' => false,
76 'book_viewing_contact' => false,
77 'get_viewing_details_meta_box' => false,
78 'get_viewing_actions' => false,
79 'get_viewing_lightbox' => false,
80 'viewing_carried_out' => false,
81 'viewing_cancelled' => false,
82 'viewing_no_show' => false,
83 'viewing_email_applicant_booking_confirmation' => false,
84 'viewing_email_owner_booking_confirmation' => false,
85 'viewing_email_attending_negotiator_booking_confirmation' => false,
86 'viewing_email_applicant_cancellation_notification' => false,
87 'viewing_email_owner_cancellation_notification' => false,
88 'viewing_email_attending_negotiator_cancellation_notification' => false,
89 'viewing_interested_feedback' => false,
90 'viewing_not_interested_feedback' => false,
91 'viewing_feedback_not_required' => false,
92 'viewing_revert_feedback_pending' => false,
93 'viewing_revert_pending' => false,
94 'viewing_feedback_passed_on' => false,
95 'get_property_viewings_meta_box' => false,
96 'get_contact_viewings_meta_box' => false,
97
98 // Offer actions
99 'record_offer_property' => false,
100 'record_offer_contact' => false,
101 'get_offer_details_meta_box' => false,
102 'get_offer_actions' => false,
103 'get_property_offers_meta_box' => false,
104 'offer_accepted' => false,
105 'offer_declined' => false,
106 'offer_withdrawn' => false,
107 'offer_revert_pending' => false,
108 'get_contact_offers_meta_box' => false,
109
110 // Sale actions
111 'get_sale_details_meta_box' => false,
112 'get_sale_actions' => false,
113 'get_sale_details_meta_box' => false,
114 'sale_exchanged' => false,
115 'sale_completed' => false,
116 'sale_fallen_through' => false,
117 'offer_declined' => false,
118 'get_property_sales_meta_box' => false,
119 'get_contact_sales_meta_box' => false,
120
121 // Enquiry actions
122 'get_property_enquiries_meta_box' => false,
123 'get_contact_enquiries_meta_box' => false,
124
125 // Tenancy actions
126 'add_key_date' => false,
127 'get_management_dates_grid' => false,
128 'get_key_dates_quick_edit_row' => false,
129 'check_key_date_recurrence' => false,
130 'save_key_date' => false,
131 'delete_key_date' => false,
132
133 'validate_save_contact' => false,
134 'applicant_registration' => true,
135 'login' => true,
136 'lost_password' => true,
137 'reset_password' => true,
138 'save_account_details' => true,
139 'save_account_requirements' => true,
140
141 // Dismissing notices
142 'dismiss_notice_leave_review' => false,
143 'dismiss_notice_retired_template_assistant' => false,
144 'dismiss_notice_demo_data' => false,
145 'dismiss_notice_epl' => false,
146 'dismiss_notice_missing_search_results' => false,
147 'dismiss_notice_missing_google_maps_api_key' => false,
148 'dismiss_notice_invalid_expired_license_key' => false,
149 'dismiss_notice_email_cron_not_running' => false,
150
151 // Settings
152 'save_term_order' => false,
153
154 // PRO features activate/deactivate
155 'activate_pro_feature' => false,
156 'deactivate_pro_feature' => false,
157
158 'deactivate_survey' => false,
159 );
160
161 foreach ( $ajax_events as $ajax_event => $nopriv )
162 {
163 if ( ! $nopriv ) {
164 add_action( 'wp_ajax_propertyhive_' . $ajax_event, array( $this, 'authorize_admin_ajax' ), 0 );
165 }
166 add_action( 'wp_ajax_propertyhive_' . $ajax_event, array( $this, $ajax_event ) );
167
168 if ( $nopriv ) {
169 add_action( 'wp_ajax_nopriv_propertyhive_' . $ajax_event, array( $this, $ajax_event ) );
170 }
171 }
172 }
173
174 /**
175 * Require CRM access before dispatching an administrative AJAX action.
176 * Individual callbacks still enforce their nonces and record permissions.
177 */
178 public function authorize_admin_ajax()
179 {
180 if ( ! current_user_can( 'manage_propertyhive' ) ) {
181 wp_send_json_error( esc_html__( 'Insufficient permissions', 'propertyhive' ), 403 );
182 }
183 }
184
185 /** Validate a CRM action's target before rendering or changing a record. */
186 private function get_authorized_record_id( $field, $post_type )
187 {
188 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Shared record guard: mutating callers verify their own action nonce; read-only callers are CRM-only through authorize_admin_ajax. This helper performs no writes.
189 $post_id = isset( $_POST[$field] ) && is_scalar( $_POST[$field] ) ? absint( $_POST[$field] ) : 0;
190 if ( !is_array($post_type) ) { $post_type = array($post_type); }
191 if (
192 $post_id < 1 ||
193 ! in_array( get_post_type( $post_id ), $post_type, true ) ||
194 ! current_user_can( 'manage_propertyhive' ) ||
195 ! current_user_can( 'edit_post', $post_id ) )
196 {
197 wp_send_json_error( __( 'Invalid record or insufficient permissions.', 'propertyhive' ), 403 );
198 }
199 return $post_id;
200 }
201
202 /** Normalize viewing booking fields before creating any records. */
203 private function get_viewing_booking_input()
204 {
205 $input = array();
206 foreach ( array( 'start_date', 'start_time', 'applicant_name', 'applicant_email_address', 'applicant_telephone_number', 'applicant_address' ) as $field ) {
207 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Both booking callbacks verify book-viewing before calling this input-only helper.
208 if ( isset( $_POST[$field] ) && ! is_string( $_POST[$field] ) ) {
209 wp_send_json_error( __( 'Invalid booking details.', 'propertyhive' ), 400 );
210 }
211 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Both booking callbacks verify book-viewing before calling this input-only helper.
212 $input[$field] = isset( $_POST[$field] ) ? ( 'applicant_address' === $field ? sanitize_textarea_field( wp_unslash( $_POST[$field] ) ) : sanitize_text_field( wp_unslash( $_POST[$field] ) ) ) : '';
213 }
214 if ( '' === $input['start_date'] || '' === $input['start_time'] || false === strtotime( $input['start_date'] . ' ' . $input['start_time'] ) ) {
215 wp_send_json_error( __( 'Invalid viewing date or time.', 'propertyhive' ), 400 );
216 }
217 foreach ( array( 'applicant_ids', 'property_ids', 'negotiator_ids' ) as $field ) {
218 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Inspect scalar/list shape first; each accepted ID is validated as a positive decimal string and converted with absint below.
219 $values = isset( $_POST[$field] ) ? $_POST[$field] : array();
220 $values = is_array( $values ) ? $values : ( '' === $values ? array() : array( $values ) );
221 $input[$field] = array();
222 foreach ( $values as $value ) {
223 if ( ! is_scalar( $value ) || ! ctype_digit( (string) $value ) || (int) $value < 1 ) {
224 wp_send_json_error( __( 'Invalid booking selection.', 'propertyhive' ), 400 );
225 }
226 $input[$field][] = absint( $value );
227 }
228 }
229 $viewing_type = get_post_type_object( 'viewing' );
230 if ( ! current_user_can( 'manage_propertyhive' ) || ! $viewing_type || ! current_user_can( $viewing_type->cap->create_posts ) ) {
231 wp_send_json_error( __( 'Insufficient permissions.', 'propertyhive' ), 403 );
232 }
233 return $input;
234 }
235
236 /** Normalize offer recording fields before creating any records. */
237 private function get_offer_input()
238 {
239 $input = array();
240 foreach ( array( 'offer_date', 'offer_time', 'amount', 'applicant_name', 'applicant_email_address', 'applicant_telephone_number', 'applicant_address' ) as $field ) {
241 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Both offer callbacks verify record-offer before calling this input-only helper.
242 if ( isset( $_POST[$field] ) && ! is_string( $_POST[$field] ) ) {
243 wp_send_json_error( __( 'Invalid offer details.', 'propertyhive' ), 400 );
244 }
245 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Both offer callbacks verify record-offer before calling this input-only helper.
246 $input[$field] = isset( $_POST[$field] ) ? ( 'applicant_address' === $field ? sanitize_textarea_field( wp_unslash( $_POST[$field] ) ) : sanitize_text_field( wp_unslash( $_POST[$field] ) ) ) : '';
247 }
248 if ( '' === $input['offer_date'] || '' === $input['offer_time'] || false === strtotime( $input['offer_date'] . ' ' . $input['offer_time'] ) ) {
249 wp_send_json_error( __( 'Invalid offer date or time.', 'propertyhive' ), 400 );
250 }
251 foreach ( array( 'applicant_ids', 'property_ids' ) as $field ) {
252 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Inspect scalar/list shape first; each accepted ID is validated as a positive decimal string and converted with absint below.
253 $values = isset( $_POST[$field] ) ? $_POST[$field] : array();
254 $values = is_array( $values ) ? $values : ( '' === $values ? array() : array( $values ) );
255 $input[$field] = array();
256 foreach ( $values as $value ) {
257 if ( ! is_scalar( $value ) || ! ctype_digit( (string) $value ) || (int) $value < 1 ) {
258 wp_send_json_error( __( 'Invalid offer selection.', 'propertyhive' ), 400 );
259 }
260 $input[$field][] = absint( $value );
261 }
262 }
263 $offer_type = get_post_type_object( 'offer' );
264 if ( ! current_user_can( 'manage_propertyhive' ) || ! $offer_type || ! current_user_can( $offer_type->cap->create_posts ) ) {
265 wp_send_json_error( __( 'Insufficient permissions.', 'propertyhive' ), 403 );
266 }
267 $input['amount'] = preg_replace( '/[^0-9.]/', '', $input['amount'] );
268 if ( '' === $input['amount'] || ! is_numeric( $input['amount'] ) ) {
269 wp_send_json_error( __( 'Invalid offer amount.', 'propertyhive' ), 400 );
270 }
271 return $input;
272 }
273
274 /** Preserve PHP upload metadata for WordPress's upload validator. */
275 private function get_viewing_email_uploads()
276 {
277 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing -- Calling email callbacks verify viewing-actions first. File metadata must reach wp_handle_upload unchanged; shape is checked below, and core verifies uploaded-file provenance, MIME/extension, size and safe destination filename.
278 $files = isset( $_FILES['attachments'] ) ? $_FILES['attachments'] : array();
279 foreach ( array( 'name', 'type', 'tmp_name', 'error', 'size' ) as $key ) {
280 if ( ! isset( $files[$key] ) || ! is_array( $files[$key] ) ) {
281 wp_send_json_error( __( 'Invalid attachment data.', 'propertyhive' ), 400 );
282 }
283 }
284 foreach ( $files['name'] as $index => $name ) {
285 foreach ( array( 'name', 'type', 'tmp_name' ) as $key ) {
286 if ( ! isset( $files[$key][$index] ) || ! is_string( $files[$key][$index] ) ) {
287 wp_send_json_error( __( 'Invalid attachment data.', 'propertyhive' ), 400 );
288 }
289 }
290 foreach ( array( 'error', 'size' ) as $key ) {
291 if ( ! isset( $files[$key][$index] ) || ! is_scalar( $files[$key][$index] ) || ! ctype_digit( (string) $files[$key][$index] ) ) {
292 wp_send_json_error( __( 'Invalid attachment data.', 'propertyhive' ), 400 );
293 }
294 }
295 }
296 return $files;
297 }
298
299 public function deactivate_survey()
300 {
301 // Verify the nonce
302 if ( !isset($_POST['nonce']) || !wp_verify_nonce( ( isset( $_POST['nonce'] ) && is_string( $_POST['nonce'] ) ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'deactivate-survey') )
303 {
304 wp_send_json_error('Invalid nonce', 403);
305 die();
306 }
307
308 if ( !isset($_POST['reason']) || !is_string($_POST['reason']) || empty($_POST['reason']) )
309 {
310 wp_send_json_error('Reason is required', 400);
311 die();
312 }
313
314 $reason = sanitize_text_field( wp_unslash( $_POST['reason'] ) );
315 $comments = ( isset($_POST['comments']) && is_string($_POST['comments']) ) ? sanitize_textarea_field( wp_unslash( $_POST['comments'] ) ) : '';
316 $anonymous = isset($_POST['anonymous']) && $_POST['anonymous'] === 'yes';
317
318 $license_type = get_option('propertyhive_license_type');
319 if ( $license_type == 'pro' )
320 {
321 $license_key = get_option('propertyhive_pro_license_key');
322 }
323 else
324 {
325 $license_key = get_option('propertyhive_license_key');
326 }
327 $propertyhive_install_timestamp = get_option('propertyhive_install_timestamp');
328 $active_plugins = get_option('active_plugins');
329 $all_plugins = get_plugins(); // Fetch detailed data for all plugins
330
331 $active_plugins_with_versions = array();
332
333 foreach ( $active_plugins as $plugin )
334 {
335 if ( isset($all_plugins[$plugin]) )
336 {
337 $active_plugins_with_versions[] = array(
338 'name' => $all_plugins[$plugin]['Name'],
339 'version' => $all_plugins[$plugin]['Version'],
340 'path' => $plugin,
341 );
342 }
343 }
344 $server_software = ( isset( $_SERVER['SERVER_SOFTWARE'] ) && is_string( $_SERVER['SERVER_SOFTWARE'] ) ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'Unknown';
345
346 // Prepare data for third-party POST
347 $third_party_data = array(
348 'reason' => $reason,
349 'comments' => $comments,
350 'anonymous' => $anonymous ? 'yes' : 'no',
351 );
352
353 if (!$anonymous)
354 {
355 $third_party_data['site_url'] = get_site_url();
356 $third_party_data['admin_email'] = get_option('admin_email');
357 $third_party_data['license_type'] = $license_type;
358 $third_party_data['license_key'] = $license_key;
359 $third_party_data['active_plugins'] = $active_plugins_with_versions;
360 $third_party_data['active_theme'] = wp_get_theme()->get('Name');
361 $third_party_data['wordpress_version'] = get_bloginfo('version');
362 $third_party_data['php_version'] = phpversion();
363 $third_party_data['server_software'] = $server_software;
364 }
365
366 //wp_send_json_success(json_encode($third_party_data, true));
367
368 // Make the remote POST request
369 $response = wp_remote_post('https://wp-property-hive.com/deactivate-survey.php', array(
370 'method' => 'POST',
371 'body' => $third_party_data
372 ));
373
374 if ( is_wp_error($response) )
375 {
376 wp_send_json_error($response->get_error_message(), 500);
377 die();
378 }
379
380 $response_body = wp_remote_retrieve_body($response);
381 wp_send_json_success(json_decode($response_body, true));
382
383 die();
384 }
385
386 public function save_term_order()
387 {
388 check_ajax_referer( 'updates', 'security' );
389
390 if ( ! isset( $_POST['taxonomy'], $_POST['term'] ) || ! is_string( $_POST['taxonomy'] ) || ! is_array( $_POST['term'] ) || empty( $_POST['term'] ) ) {
391 die();
392 }
393 $taxonomy_name = sanitize_key( wp_unslash( $_POST['taxonomy'] ) );
394 $taxonomy = get_taxonomy( $taxonomy_name );
395 if ( ! $taxonomy || ! current_user_can( $taxonomy->cap->manage_terms ) ) {
396 wp_send_json_error( esc_html__( 'Insufficient permissions', 'propertyhive' ), 403 );
397 }
398 $term_ids = array();
399 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Validate raw term ID types before accepting only positive decimal integers below; no text is stored.
400 foreach ( $_POST['term'] as $term_id ) {
401 if ( ! is_string( $term_id ) || ! ctype_digit( $term_id ) || 0 === absint( $term_id ) ) {
402 die();
403 }
404 $term_ids[] = absint( $term_id );
405 }
406 update_option( 'propertyhive_taxonomy_terms_order_' . $taxonomy_name, implode( '|', $term_ids ) );
407 die();
408 }
409
410 public function dismiss_notice_leave_review()
411 {
412 update_option( 'propertyhive_review_prompt_due_timestamp', 0 );
413
414 // Quit out
415 die();
416 }
417
418 public function dismiss_notice_retired_template_assistant()
419 {
420 if ( is_multisite() )
421 {
422 if ( ! is_super_admin() ) return;
423 delete_site_option( 'propertyhive_template_assistant_retired_notice' );
424 }
425 else
426 {
427 if ( ! current_user_can( 'activate_plugins' ) ) return;
428 delete_option( 'propertyhive_template_assistant_retired_notice' );
429 }
430
431 // Quit out
432 die();
433 }
434
435 public function dismiss_notice_demo_data()
436 {
437 update_option( 'propertyhive_hide_demo_data_tab', 'yes' );
438
439 // Quit out
440 die();
441 }
442
443 public function dismiss_notice_epl()
444 {
445 update_option( 'epl_notice_dismissed', 'yes' );
446
447 // Quit out
448 die();
449 }
450
451 public function dismiss_notice_missing_search_results()
452 {
453 update_option( 'missing_search_results_notice_dismissed', 'yes' );
454
455 // Quit out
456 die();
457 }
458
459 public function dismiss_notice_missing_google_maps_api_key()
460 {
461 update_option( 'missing_google_maps_api_key_notice_dismissed', 'yes' );
462
463 // Quit out
464 die();
465 }
466
467 public function dismiss_notice_invalid_expired_license_key()
468 {
469 update_option( 'missing_invalid_expired_license_key_notice_dismissed', 'yes' );
470
471 // Quit out
472 die();
473 }
474
475 public function dismiss_notice_email_cron_not_running()
476 {
477 update_option( 'email_cron_not_running_dismissed', 'yes' );
478 }
479
480 /**
481 * Output headers for JSON requests
482 */
483 private function json_headers() {
484 header( 'Content-Type: application/json; charset=utf-8' );
485 }
486
487 /**
488 * Return a list string, comma delimited with an ampersand(&) before the final item
489 */
490 private function get_list_string( $list_items )
491 {
492 $list_string = '';
493 if ( count($list_items) == 1 )
494 {
495 $list_string = $list_items[0];
496 }
497 elseif ( count($list_items) > 1 )
498 {
499 $last_item = array_pop($list_items);
500 $list_string = implode(', ', $list_items) . ' & ' . $last_item;
501 }
502 return $list_string;
503 }
504
505 private function check_recaptcha_form_response($errors, $key, $control)
506 {
507 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
508 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reads a CAPTCHA response token and performs remote validation; the helper does not write state. It is called from nonce-protected applicant_registration and from the separately assessed public enquiry endpoint. This line alone is not a CSRF sink.
509 $response = ( isset( $_POST['g-recaptcha-response'] ) && is_string( $_POST['g-recaptcha-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['g-recaptcha-response'] ) ) : '';
510
511 $response = wp_remote_post(
512 'https://www.google.com/recaptcha/api/siteverify',
513 array(
514 'method' => 'POST',
515 'body' => array( 'secret' => $secret, 'response' => $response ),
516 )
517 );
518 if ( is_wp_error( $response ) )
519 {
520 $errors[] = $response->get_error_message();
521 }
522 else
523 {
524 $response = json_decode($response['body'], TRUE);
525
526 if ( $response === FALSE )
527 {
528 $errors[] = __( 'Error decoding response from reCAPTCHA check', 'propertyhive' );
529 }
530 else
531 {
532 if ( isset($response['success']) && $response['success'] == true )
533 {
534 if ( $key == 'recaptcha' )
535 {
536
537 }
538 elseif ( $key == 'recaptcha-v3' )
539 {
540 $score_threshold = round((float)get_option('propertyhive_captcha_score_threshold', 0.5), 1);
541 if ( !is_numeric($score_threshold) || $score_threshold < 0 || $score_threshold > 1 )
542 {
543 $score_threshold = 0.5;
544 }
545 if ( isset($response['score']) && $response['score'] >= $score_threshold )
546 {
547
548 }
549 else
550 {
551 $errors[] = __('Failed reCAPTCHA validation due to high spam score', 'propertyhive' ) . ': ' . $response['score'];
552 }
553 }
554 }
555 else
556 {
557 $error_message = __( 'Failed reCAPTCHA validation', 'propertyhive' );
558
559 // Check if Google returned error codes
560 if ( isset($response['error-codes']) && is_array($response['error-codes']) )
561 {
562 $error_message .= ' (' . implode(', ', $response['error-codes']) . ')';
563 }
564
565 $errors[] = $error_message;
566 }
567 }
568 }
569 return $errors;
570 }
571
572 public function create_contact_login()
573 {
574 check_ajax_referer( 'create-login', 'security' );
575
576 $contact_id = isset( $_POST['contact_id'] ) && is_scalar( $_POST['contact_id'] ) ? absint( $_POST['contact_id'] ) : 0;
577 if ( ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'edit_post', $contact_id ) ) {
578 wp_send_json_error( __( 'Insufficient permissions', 'propertyhive' ), 403 );
579 }
580 if ( 'contact' !== get_post_type( $contact_id ) ) {
581 wp_send_json_error( __( 'Invalid contact.', 'propertyhive' ), 400 );
582 }
583 if ( get_post_meta( $contact_id, '_user_id', true ) ) {
584 wp_send_json_error( __( 'This contact already has a login.', 'propertyhive' ), 409 );
585 }
586
587 if ( empty( $_POST['password'] ) || ! is_string( $_POST['password'] ) )
588 {
589 $return = array('error' => 'No password entered');
590 wp_send_json( $return );
591 }
592
593 $contact = new PH_Contact($contact_id);
594
595 $display_name = get_the_title($contact_id);
596
597 // Create user
598 $userdata = array(
599 'display_name' => $display_name,
600 'user_login' => sanitize_email($contact->email_address),
601 'user_email' => sanitize_email($contact->email_address),
602 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Opaque password is type checked above, unslashed once and passed directly to WordPress hashing; text sanitization would change the credential.
603 'user_pass' => wp_unslash( $_POST['password'] ),
604 'role' => 'property_hive_contact',
605 'show_admin_bar_front' => 'false',
606 );
607
608 if ( !empty($display_name) )
609 {
610 $name_parts = explode( ' ', $display_name );
611
612 if ( count($name_parts) > 1 )
613 {
614 $userdata['last_name'] = array_pop($name_parts);
615 $userdata['first_name'] = implode(' ', $name_parts);
616 }
617 else
618 {
619 $userdata['last_name'] = $display_name;
620 }
621 }
622
623 $user_id = wp_insert_user( $userdata );
624
625 // On success
626 if ( ! is_wp_error( $user_id ) )
627 {
628 // Assign user ID to CPT
629 add_post_meta( $contact_id, '_user_id', $user_id );
630
631 $return = array('success' => true);
632 }
633 else
634 {
635 $return = array('error' => 'Failed to create user login');
636 }
637
638 wp_send_json( $return );
639 }
640
641 /**
642 * Login user
643 */
644 public function login()
645 {
646 $return = array(
647 'success' => false,
648 'errors' => array(),
649 );
650
651 if ( check_ajax_referer( 'ph_login', 'security', false ) === FALSE )
652 {
653 $return['errors'][] = 'Invalid nonce';
654
655 wp_send_json( $return );
656 }
657
658 if ( ! isset( $_POST['email_address'], $_POST['password'] ) || ! is_string( $_POST['email_address'] ) || ! is_string( $_POST['password'] ) ) {
659 $return['errors'][] = __( 'Enter your login details.', 'propertyhive' );
660 wp_send_json( $return );
661 }
662 $creds = array(
663 'user_login' => sanitize_text_field( wp_unslash( $_POST['email_address'] ) ),
664 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Authentication requires the exact password, without text or HTML sanitization.
665 'user_password' => wp_unslash( $_POST['password'] ),
666 );
667
668 $user = wp_signon( apply_filters( 'propertyhive_login_credentials', $creds ), is_ssl() );
669
670 if ( is_wp_error( $user ) )
671 {
672 //$return['errors'][] = $user->get_error_message();
673 }
674 else
675 {
676 // Check has associated contact CPT and is published
677 $args = array(
678 'post_type' => apply_filters( 'propertyhive_allowed_login_post_type', array( 'contact' ) ),
679 'fields' => 'ids',
680 'posts_per_page' => 1,
681 'post_status' => array( 'publish' ),
682 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Login/contact duplicate/address lookups use a fixed meta relation and return a small result set (1 row for identity checks, 10 for the address autocomplete). posts_per_page=1; posts_per_page=10; fields=ids on all four; values are the authenticated user, submitted email, search text, or current contact.
683 'meta_query' => array(
684 array(
685 'key' => '_user_id',
686 'value' => $user->ID
687 )
688 )
689 );
690
691 $contact_query = new WP_Query( $args );
692
693 if ( $contact_query->have_posts() )
694 {
695 while ( $contact_query->have_posts() )
696 {
697 $contact_query->the_post();
698
699 // Has associated published contact CPT
700 $return['success'] = true;
701
702 do_action('propertyhive_user_logged_in', get_the_ID(), $user->ID);
703 }
704 }
705
706 wp_reset_postdata();
707 }
708
709 wp_send_json( $return );
710 }
711
712 /**
713 * Lost password
714 */
715 public function lost_password()
716 {
717 $return = array(
718 'success' => false,
719 'errors' => array(),
720 );
721
722 if ( check_ajax_referer( 'ph_lost_password', 'security', false ) === FALSE )
723 {
724 $return['errors'][] = 'Invalid nonce';
725
726 wp_send_json( $return );
727 }
728
729 $email_address = isset( $_POST['email_address'] ) && is_string( $_POST['email_address'] ) ? sanitize_email( wp_unslash( $_POST['email_address'] ) ) : '';
730
731 $user_data = get_user_by( 'email', $email_address );
732
733 // check email address exists
734 if ( !$user_data )
735 {
736 $return['errors'][] = 'Email address not found';
737
738 wp_send_json( $return );
739 }
740
741 // Send reset email
742 $to = $email_address;
743 $subject = __( 'Password Reset Request for', 'propertyhive' ) . ' ' . get_bloginfo('name');
744 $body = __( 'Someone has requested a new password for an account on', 'propertyhive' ) . ' ' . get_bloginfo('name') . ".\n\n";
745 $body .= __( 'If you didn\'t make this request you can ignore this email. If you\'d like to proceed please follow the link below', 'propertyhive' ) . ":\n\n";
746 $body .= add_query_arg( array(
747 'key' => get_password_reset_key( $user_data ),
748 'id' => $user_data->ID,
749 ), get_permalink( get_option( 'propertyhive_applicant_reset_password_page_id', '' ) ) );
750
751
752 $from = get_option('propertyhive_email_from_address', '');
753 if ( $from == '' )
754 {
755 $from = get_bloginfo('admin_email');
756 }
757
758 $headers = array();
759 $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
760 $headers[] = 'Reply-To: ' . sanitize_email($from);
761 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
762
763 $headers = apply_filters( 'propertyhive_lost_password_email_headers', $headers );
764
765 wp_mail( $to, $subject, $body, $headers );
766
767 $return['success'] = true;
768
769 wp_send_json( $return );
770 }
771
772 /**
773 * Reset password
774 */
775 public function reset_password()
776 {
777 $return = array(
778 'success' => false,
779 'errors' => array(),
780 );
781
782 if ( check_ajax_referer( 'ph_reset_password', 'security', false ) === FALSE )
783 {
784 $return['errors'][] = 'Invalid nonce';
785
786 wp_send_json( $return );
787 }
788
789 // check key and user login again
790 if ( ! isset( $_POST['reset_key'], $_POST['reset_login'], $_POST['password_1'], $_POST['password_2'] ) || ! is_string( $_POST['reset_key'] ) || ! is_string( $_POST['reset_login'] ) || ! is_string( $_POST['password_1'] ) || ! is_string( $_POST['password_2'] ) ) {
791 $return['errors'][] = __( 'Please enter valid password reset details.', 'propertyhive' );
792 wp_send_json( $return );
793 }
794 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Core validates the exact opaque reset token and login; text sanitization would change credentials.
795 $user = check_password_reset_key( wp_unslash( $_POST['reset_key'] ), wp_unslash( $_POST['reset_login'] ) );
796
797 // check passwords match and are strong enough
798 if ( $user instanceof WP_User )
799 {
800 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Preserve the exact password; authentication secrets must not be text-sanitized.
801 $password_1 = wp_unslash( $_POST['password_1'] );
802 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Preserve the exact password; authentication secrets must not be text-sanitized.
803 $password_2 = wp_unslash( $_POST['password_2'] );
804
805 if ( empty( $password_1 ) )
806 {
807 $return['errors'][] = __( 'Please enter your password.', 'propertyhive' );
808 }
809
810 if ( $password_1 !== $password_2 )
811 {
812 $return['errors'][] = __( 'Passwords do not match.', 'propertyhive' );
813 }
814
815 // Check password strength?
816 }
817 else
818 {
819 $return['errors'][] = __( 'This key is invalid or has already been used. Please reset your password again if needed..', 'propertyhive' );
820 }
821
822 if ( !empty($return['errors']) )
823 {
824 wp_send_json( $return );
825 }
826
827 // do actual reset
828 $errors = new WP_Error();
829 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core hook validate_password_reset; renaming it would break the core hook contract.
830 do_action( 'validate_password_reset', $errors, $user );
831
832 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core hook password_reset; renaming it would break the core hook contract.
833 do_action( 'password_reset', $user, $password_1 );
834
835 wp_set_password( $password_1, $user->ID );
836
837 $return['success'] = true;
838
839 wp_send_json( $return );
840 }
841
842 /**
843 * Register applicant
844 */
845 public function applicant_registration()
846 {
847 // Validate contact
848 global $post;
849
850 $return = array(
851 'success' => false,
852 'errors' => array(),
853 );
854
855 if ( check_ajax_referer( 'ph_register', 'security', false ) === FALSE )
856 {
857 $return['errors'][] = 'Invalid nonce';
858
859 $this->json_headers();
860 echo json_encode( $return );
861
862 // Quit out
863 die();
864 }
865
866 // Validate
867 $errors = array();
868
869 $registration_input = array();
870 foreach ( array( 'name', 'email_address', 'telephone_number', 'department', 'maximum_price', 'maximum_rent', 'minimum_bedrooms', 'available_as_sale', 'available_as_rent', 'minimum_floor_area', 'maximum_floor_area', 'location_text', 'additional_requirements' ) as $input_key ) {
871 if ( isset( $_POST[$input_key] ) && ! is_string( $_POST[$input_key] ) ) {
872 $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
873 $registration_input[$input_key] = '';
874 continue;
875 }
876 if ( 'additional_requirements' === $input_key ) {
877 $registration_input[$input_key] = isset( $_POST[$input_key] ) ? sanitize_textarea_field( wp_unslash( $_POST[$input_key] ) ) : '';
878 } else {
879 $registration_input[$input_key] = isset( $_POST[$input_key] ) ? sanitize_text_field( wp_unslash( $_POST[$input_key] ) ) : '';
880 }
881 }
882 foreach ( array( 'property_type', 'commercial_property_type', 'location' ) as $input_key ) {
883 $registration_input[$input_key] = array();
884 if ( isset( $_POST[$input_key] ) ) {
885 if ( ! is_string( $_POST[$input_key] ) && ! is_array( $_POST[$input_key] ) ) {
886 $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
887 continue;
888 }
889 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Validate element types before unslashing and sanitizing each accepted selection below.
890 foreach ( (array) $_POST[$input_key] as $selection ) {
891 if ( ! is_string( $selection ) ) {
892 $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
893 continue;
894 }
895 $registration_input[$input_key][] = sanitize_text_field( wp_unslash( $selection ) );
896 }
897 }
898 }
899 foreach ( array( 'password', 'password2' ) as $input_key ) {
900 if ( isset( $_POST[$input_key] ) && ! is_string( $_POST[$input_key] ) ) {
901 $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
902 $registration_input[$input_key] = '';
903 } else {
904 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Passwords are type-checked opaque strings, unslashed once and passed unchanged to WordPress hashing.
905 $registration_input[$input_key] = isset( $_POST[$input_key] ) ? wp_unslash( $_POST[$input_key] ) : '';
906 }
907 }
908
909 $form_controls = ph_get_user_details_form_fields();
910
911 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
912
913 $form_controls_2 = ph_get_applicant_requirements_form_fields();
914
915 $form_controls_2 = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls_2, false );
916
917 $form_controls = array_merge( $form_controls, $form_controls_2 );
918
919 // need to improve this as duplicated in ph-shortcodes.php
920 if ( get_option( 'propertyhive_applicant_registration_form_disclaimer', '' ) != '' )
921 {
922 $disclaimer = get_option( 'propertyhive_applicant_registration_form_disclaimer', '' );
923
924 $form_controls['disclaimer'] = array(
925 'type' => 'checkbox',
926 'label' => $disclaimer,
927 'label_style' => 'width:100%;',
928 'required' => true
929 );
930 }
931
932 $form_controls = apply_filters( 'propertyhive_applicant_registration_form_fields', $form_controls );
933
934 $contact_post_id = false;
935
936 foreach ( $form_controls as $key => $control )
937 {
938 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
939 {
940 // This field is mandatory. Lets check we received it in the post
941 if ( ! isset( $_POST[$key] ) || ( isset( $_POST[$key] ) && empty( $_POST[$key] ) ) )
942 {
943 $errors[] = __( 'Missing required field', 'propertyhive' ) . ': ' . $key;
944 }
945 }
946 if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) )
947 {
948 if ( ! is_string( $_POST[$key] ) || ! is_email( wp_unslash( $_POST[$key] ) ) )
949 {
950 $errors[] = __( 'Invalid email address provided', 'propertyhive' );
951 }
952 else
953 {
954 // Make sure this email address doesn't exist already
955 $args = array(
956 'post_type' => 'contact',
957 'posts_per_page' => 1,
958 'fields' => 'ids',
959 'post_status' => array( 'publish' ),
960 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Login/contact duplicate/address lookups use a fixed meta relation and return a small result set (1 row for identity checks, 10 for the address autocomplete). posts_per_page=1; posts_per_page=10; fields=ids on all four; values are the authenticated user, submitted email, search text, or current contact.
961 'meta_query' => array(
962 array(
963 'key' => '_email_address',
964 'value' => sanitize_email( wp_unslash( $_POST[$key] ) )
965 )
966 )
967 );
968
969 $contacts_query = new WP_Query( $args );
970
971 if ( $contacts_query->have_posts() )
972 {
973 // Public registration does not prove ownership of an existing CRM contact.
974 $errors[] = __( 'This email address is already registered to a user. Please sign in or contact the agency.', 'propertyhive' );
975 }
976 else
977 {
978 if ( email_exists( sanitize_email( wp_unslash( $_POST[$key] ) ) ) )
979 {
980 $errors[] = __( 'This email address is already registered to a user', 'propertyhive' );
981 }
982 }
983 wp_reset_postdata();
984 }
985 }
986 if ( in_array( $key, array('recaptcha', 'recaptcha-v3') ) )
987 {
988 $errors = $this->check_recaptcha_form_response($errors, $key, $control);
989 }
990
991 if ( $key == 'hCaptcha' )
992 {
993 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
994 $response = ( isset( $_POST['h-captcha-response'] ) && is_string( $_POST['h-captcha-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['h-captcha-response'] ) ) : '';
995
996 $response = wp_remote_post(
997 'https://hcaptcha.com/siteverify',
998 array(
999 'method' => 'POST',
1000 'body' => array( 'secret' => $secret, 'response' => $response ),
1001 )
1002 );
1003
1004 if ( is_wp_error( $response ) )
1005 {
1006 $errors[] = $response->get_error_message();
1007 }
1008 else
1009 {
1010 $response = json_decode($response['body'], TRUE);
1011 if ( $response === FALSE )
1012 {
1013 $errors[] = 'Error decoding response from hCaptcha check';
1014 }
1015 else
1016 {
1017 if ( isset($response['success']) && $response['success'] == true )
1018 {
1019
1020 }
1021 else
1022 {
1023 $errors[] = 'Failed hCaptcha validation';
1024 }
1025 }
1026 }
1027 }
1028
1029 if ( $key == 'turnstile' )
1030 {
1031 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
1032 $response = ( isset( $_POST['cf-turnstile-response'] ) && is_string( $_POST['cf-turnstile-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) ) : '';
1033
1034 $response = wp_remote_post(
1035 'https://challenges.cloudflare.com/turnstile/v0/siteverify', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Server-side CAPTCHA token verification API.
1036 array(
1037 'method' => 'POST',
1038 'headers' => array(
1039 'Content-Type' => 'application/x-www-form-urlencoded',
1040 ),
1041 'body' => array( 'secret' => $secret, 'response' => $response ),
1042 )
1043 );
1044
1045 if ( is_wp_error( $response ) )
1046 {
1047 $errors[] = $response->get_error_message();
1048 }
1049 else
1050 {
1051 $response = json_decode($response['body'], TRUE);
1052 if ( $response === FALSE )
1053 {
1054 $errors[] = 'Error decoding response from turnstile check';
1055 }
1056 else
1057 {
1058 if ( isset($response['success']) && $response['success'] == true )
1059 {
1060
1061 }
1062 else
1063 {
1064 $errors[] = 'Failed turnstile validation';
1065 }
1066 }
1067 }
1068 }
1069 }
1070
1071 // Check password and password2 match
1072 if ( isset( $_POST['password'] ) && isset( $_POST['password2'] ) && $registration_input['password'] !== $registration_input['password2'] )
1073 {
1074 $errors[] = __( 'The passwords entered do not match', 'propertyhive' );
1075 }
1076
1077 if ( !empty($errors) )
1078 {
1079 // Failed validation
1080
1081 $return['success'] = false;
1082 $return['reason'] = 'validation';
1083 $return['errors'] = $errors;
1084 }
1085 else
1086 {
1087 if ( $contact_post_id === FALSE )
1088 {
1089 // create CPT
1090 $contact_post = array(
1091 'post_title' => wp_slash( $registration_input['name'] ),
1092 'post_content' => '',
1093 'post_type' => 'contact',
1094 'post_status' => 'publish',
1095 'comment_status'=> 'closed',
1096 'ping_status' => 'closed',
1097 );
1098
1099 // Insert the post into the database
1100 $contact_post_id = wp_insert_post( $contact_post );
1101 }
1102 else
1103 {
1104 // update CPT
1105 $contact_post = array(
1106 'ID' => $contact_post_id,
1107 'post_title' => wp_slash( $registration_input['name'] ),
1108 'post_status' => 'publish',
1109 );
1110
1111 // Insert the post into the database
1112 wp_update_post( $contact_post );
1113 }
1114
1115 $forbidden_contact_methods = get_post_meta( $contact_post_id, '_forbidden_contact_methods', TRUE );
1116 if ( !is_array($forbidden_contact_methods) )
1117 {
1118 $forbidden_contact_methods = array();
1119 }
1120 if ( ( $key = array_search('email', $forbidden_contact_methods) ) !== false ) {
1121 unset($forbidden_contact_methods[$key]);
1122 }
1123 update_post_meta( $contact_post_id, '_forbidden_contact_methods', array_unique($forbidden_contact_methods) );
1124
1125 // Add post meta (contact details, requirements etc)
1126 update_post_meta( $contact_post_id, '_email_address', sanitize_email( $registration_input['email_address'] ) );
1127
1128 $telephone_number = get_post_meta( $contact_post_id, '_telephone_number', TRUE );
1129 if ( isset($_POST['telephone_number']) && $_POST['telephone_number'] != '' )
1130 {
1131 $telephone_number = $registration_input['telephone_number'];
1132 }
1133 update_post_meta( $contact_post_id, '_telephone_number', wp_slash( ph_clean($telephone_number) ) );
1134 update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean( ph_clean_telephone_number($telephone_number) ) );
1135
1136 $contact_types = get_post_meta( $contact_post_id, '_contact_types', TRUE );
1137 if ( !is_array($contact_types) )
1138 {
1139 $contact_types = array();
1140 }
1141 if ( !in_array('applicant', $contact_types) )
1142 {
1143 $contact_types[] = 'applicant';
1144 }
1145 update_post_meta( $contact_post_id, '_contact_types', array_unique($contact_types) );
1146
1147 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
1148
1149 $applicant_profile = array();
1150 $applicant_profile['department'] = $registration_input['department'];
1151
1152 $base_department = $registration_input['department'];
1153 if ( !in_array( $base_department, array('residential-sales', 'residential-lettings', 'commercial') ) )
1154 {
1155 $base_department = ph_get_custom_department_based_on($base_department);
1156 }
1157
1158 if ( $base_department == 'residential-sales' )
1159 {
1160 $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_price']);
1161
1162 $applicant_profile['max_price'] = $price;
1163
1164 // Not used yet but could be if introducing currencies in the future.
1165 $applicant_profile['max_price_actual'] = $price;
1166
1167 $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
1168 $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
1169
1170 if ( $percentage_lower != '' && $percentage_higher != '' && $registration_input['maximum_price'] != '' && $registration_input['maximum_price'] != 0 )
1171 {
1172 $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_price']);
1173 $applicant_profile['match_price_range_lower'] = $price - ( $price * ( $percentage_lower / 100 ) );
1174 $applicant_profile['match_price_range_lower_actual'] = $price - ( $price * ( $percentage_lower / 100 ) );
1175
1176 $applicant_profile['match_price_range_higher'] = $price + ( $price * ( $percentage_higher / 100 ) );
1177 $applicant_profile['match_price_range_higher_actual'] = $price + ( $price * ( $percentage_higher / 100 ) );
1178 }
1179 }
1180 elseif ( $base_department == 'residential-lettings' )
1181 {
1182 $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_rent']);
1183
1184 $applicant_profile['max_rent'] = $price;
1185 $applicant_profile['rent_frequency'] = 'pcm';
1186 $price_actual = $price; // Stored in pcm
1187 $applicant_profile['max_price_actual'] = $price_actual;
1188 }
1189
1190 if ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' )
1191 {
1192 $beds = preg_replace("/[^0-9.]/", '', $registration_input['minimum_bedrooms']);
1193 $applicant_profile['min_beds'] = $beds;
1194
1195 if ( isset($_POST['property_type']) && !empty($_POST['property_type']) )
1196 {
1197 $applicant_profile['property_types'] = $registration_input['property_type'];
1198 }
1199 }
1200
1201 if ( $base_department == 'commercial' )
1202 {
1203 $available_as = array();
1204 if ( isset($_POST['available_as_sale']) && $registration_input['available_as_sale'] == 'yes' )
1205 {
1206 $available_as[] = 'sale';
1207 }
1208 if ( isset($_POST['available_as_rent']) && $registration_input['available_as_rent'] == 'yes' )
1209 {
1210 $available_as[] = 'rent';
1211 }
1212 $applicant_profile['available_as'] = $available_as;
1213
1214 $floor_area = preg_replace("/[^0-9.]/", '', $registration_input['minimum_floor_area']);
1215 $applicant_profile['min_floor_area'] = $floor_area;
1216 $applicant_profile['min_floor_area_actual'] = $floor_area;
1217
1218 $floor_area = preg_replace("/[^0-9.]/", '', $registration_input['maximum_floor_area']);
1219 $applicant_profile['max_floor_area'] = $floor_area;
1220 $applicant_profile['max_floor_area_actual'] = $floor_area;
1221
1222 if ( isset($_POST['commercial_property_type']) && !empty($_POST['commercial_property_type']) )
1223 {
1224 $applicant_profile['commercial_property_types'] = $registration_input['commercial_property_type'];
1225 }
1226 }
1227
1228 if ( isset($_POST['location']) && !empty($_POST['location']) )
1229 {
1230 $applicant_profile['locations'] = $registration_input['location'];
1231 }
1232
1233 if ( isset($_POST['location_text']) && !empty($_POST['location_text']) )
1234 {
1235 $applicant_profile['location_text'] = $registration_input['location_text'];
1236 }
1237
1238 $applicant_profile['notes'] = $registration_input['additional_requirements'];
1239
1240 $applicant_profile['send_matching_properties'] = 'yes';
1241 //$applicant_profile['auto_match_disabled'] = ''; // don't know what to do about this yet. Should probably look at global setting and reflect that
1242
1243 update_post_meta( $contact_post_id, '_applicant_profile_0', wp_slash( $applicant_profile ) );
1244
1245 if ( get_option( 'propertyhive_applicant_users', '' ) == 'yes' )
1246 {
1247 $display_name = wp_slash( $registration_input['name'] );
1248
1249 // Create user
1250 $userdata = array(
1251 'display_name' => $display_name,
1252 'user_login' => sanitize_email( $registration_input['email_address'] ),
1253 'user_email' => sanitize_email( $registration_input['email_address'] ),
1254 'user_pass' => $registration_input['password'],
1255 'role' => 'property_hive_contact',
1256 'show_admin_bar_front' => 'false',
1257 );
1258
1259 if ( !empty($display_name) )
1260 {
1261 $name_parts = explode( ' ', $display_name );
1262
1263 if ( count($name_parts) > 1 )
1264 {
1265 $userdata['last_name'] = array_pop($name_parts);
1266 $userdata['first_name'] = implode(' ', $name_parts);
1267 }
1268 else
1269 {
1270 $userdata['last_name'] = $display_name;
1271 }
1272 }
1273
1274 $user_id = wp_insert_user( $userdata );
1275
1276 //On success
1277 if ( ! is_wp_error( $user_id ) )
1278 {
1279 // Assign user ID to CPT
1280 add_post_meta( $contact_post_id, '_user_id', $user_id );
1281
1282 $return['success'] = true;
1283
1284 wp_set_auth_cookie( $user_id, true );
1285
1286 do_action( 'propertyhive_applicant_registered', $contact_post_id, $user_id );
1287 }
1288 else
1289 {
1290 $return['success'] = false;
1291 $return['reason'] = 'validation';
1292 $return['errors'] = array('Failed to create user. You might experience issues with logging in');
1293 }
1294 }
1295 else
1296 {
1297 $return['success'] = true;
1298
1299 do_action( 'propertyhive_applicant_registered', $contact_post_id, 0 );
1300 }
1301 }
1302
1303 $this->json_headers();
1304 echo json_encode( $return );
1305
1306 // Quit out
1307 die();
1308 }
1309
1310 /**
1311 * Save account details
1312 */
1313 public function save_account_details()
1314 {
1315 global $wpdb, $current_user;
1316
1317 add_filter( 'send_email_change_email', '__return_false' );
1318
1319 $return = array(
1320 'success' => false,
1321 'errors' => array(),
1322 'new_details_nonce' => wp_create_nonce( "ph_userdetails" ),
1323 );
1324
1325 // Got an issue with nonce being declined on second submission.
1326 // Need to sort before putting this back in
1327 if ( check_ajax_referer( 'ph_userdetails', 'ph_account_details_security', false ) === FALSE )
1328 {
1329 $return['errors'][] = 'Invalid nonce';
1330
1331 $this->json_headers();
1332 echo json_encode( $return );
1333
1334 // Quit out
1335 die();
1336 }
1337
1338 // Validate
1339 $errors = array();
1340
1341 $user_id = (int) get_current_user_id();
1342 $current_user = get_user_by( 'id', $user_id );
1343 if ( $user_id <= 0 )
1344 {
1345 $return['success'] = false;
1346 $return['reason'] = 'notloggedin';
1347 $return['errors'] = array('It doesn\'t appear that you\'re logged in');
1348
1349 $this->json_headers();
1350 echo json_encode( $return );
1351
1352 // Quit out
1353 die();
1354 }
1355
1356 $account_input = array();
1357 foreach ( array( 'name', 'email_address', 'telephone_number', 'password', 'password2' ) as $input_key ) {
1358 if ( isset( $_POST[$input_key] ) && ! is_string( $_POST[$input_key] ) ) {
1359 $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
1360 $account_input[$input_key] = '';
1361 continue;
1362 }
1363 if ( in_array( $input_key, array( 'password', 'password2' ), true ) ) {
1364 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Passwords are opaque strings: type checked above and unslashed exactly once, never text-sanitized or modified before WordPress hashes them.
1365 $account_input[$input_key] = isset( $_POST[$input_key] ) ? wp_unslash( $_POST[$input_key] ) : '';
1366 } else {
1367 $account_input[$input_key] = isset( $_POST[$input_key] ) ? sanitize_text_field( wp_unslash( $_POST[$input_key] ) ) : '';
1368 }
1369 }
1370 $form_controls = ph_get_user_details_form_fields();
1371
1372 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
1373
1374 foreach ( $form_controls as $key => $control )
1375 {
1376 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
1377 {
1378 // This field is mandatory. Lets check we received it in the post
1379 if ( ! isset( $_POST[$key] ) || ( isset( $_POST[$key] ) && empty( $_POST[$key] ) ) && $control['type'] != 'password' )
1380 {
1381 $errors[] = __( 'Missing required field', 'propertyhive' ) . ': ' . $key;
1382 }
1383 }
1384 if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) )
1385 {
1386 if ( ! is_string( $_POST[$key] ) || ! is_email( sanitize_email( wp_unslash( $_POST[$key] ) ) ) )
1387 {
1388 $errors[] = __( 'Invalid email address provided', 'propertyhive' );
1389 }
1390
1391 // need to see if email address is being changed and, if so, check new email address doesn't exist already
1392 }
1393 }
1394
1395 // Check password and password2 match
1396 if ( isset( $_POST['password'] ) && isset( $_POST['password2'] ) && $account_input['password'] !== '' && $account_input['password'] !== $account_input['password2'] )
1397 {
1398 $errors[] = __( 'The passwords entered do not match', 'propertyhive' );
1399 }
1400
1401 $user_roles = $current_user->roles;
1402 $user_role = array_shift( $user_roles );
1403 if ( 'property_hive_contact' === $user_role ) {
1404 $existing_login_user = username_exists( sanitize_email( $account_input['email_address'] ) );
1405 if ( $existing_login_user && (int) $existing_login_user !== $user_id ) {
1406 $errors[] = __( 'This email address is already used as a login.', 'propertyhive' );
1407 }
1408 }
1409
1410 $existing_email_user = email_exists( sanitize_email( $account_input['email_address'] ) );
1411 if ( $existing_email_user && (int) $existing_email_user !== $user_id ) {
1412 $errors[] = __( 'This email address is already registered to a user', 'propertyhive' );
1413 }
1414
1415 if ( !empty($errors) )
1416 {
1417 // Failed validation
1418
1419 $return['success'] = false;
1420 $return['reason'] = 'validation';
1421 $return['errors'] = $errors;
1422 }
1423 else
1424 {
1425 $contact = new PH_Contact( '', $user_id );
1426 if ( empty( $contact->id ) || 'contact' !== get_post_type( $contact->id ) ) {
1427 $return['reason'] = 'validation';
1428 $return['errors'] = array( __( 'Unable to find your contact record. Please contact the agency.', 'propertyhive' ) );
1429 wp_send_json( $return );
1430 }
1431
1432 // create CPT
1433 $contact_post = array(
1434 'ID' => $contact->id,
1435 'post_title' => wp_slash( $account_input['name'] ),
1436 );
1437
1438 // Update the post in the database
1439 $contact_post_id = wp_update_post( $contact_post );
1440
1441 update_post_meta( $contact_post_id, '_email_address', sanitize_email( $account_input['email_address'] ) );
1442 if (isset($_POST['telephone_number']))
1443 {
1444 update_post_meta( $contact_post_id, '_telephone_number', wp_slash( $account_input['telephone_number'] ) );
1445 update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean_telephone_number( $account_input['telephone_number'] ) );
1446 }
1447
1448 // Update user
1449 $userdata = array(
1450 'ID' => $user_id,
1451 'display_name' => wp_slash( $account_input['name'] ),
1452 'user_email' => sanitize_email( $account_input['email_address'] ),
1453 );
1454
1455 if ( isset($_POST['password']) && !empty($_POST['password']) )
1456 {
1457 $userdata['user_pass'] = $account_input['password'];
1458 }
1459
1460 $user_id = wp_update_user( $userdata );
1461
1462 if ( ! is_wp_error( $user_id ) && $user_role === 'property_hive_contact' )
1463 {
1464 // Have to update login via SQL as wp_update_user won't allow altering
1465 // Only do it for property hive contacts though as admin or editor might be viewing this page
1466 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- WordPress cannot rename a login via wp_update_user; uniqueness is validated above, and old/new user caches are cleared immediately below.
1467 $wpdb->update( $wpdb->users, array( 'user_login' => sanitize_email( $account_input['email_address'] ) ), array( 'ID' => $user_id ), array( '%s' ), array( '%d' ) );
1468 clean_user_cache( $current_user );
1469 clean_user_cache( $user_id );
1470 }
1471
1472 //On success
1473 if ( ! is_wp_error( $user_id ) )
1474 {
1475 $return['success'] = true;
1476
1477 wp_set_auth_cookie( $user_id, true );
1478
1479 do_action( 'propertyhive_account_details_updated', $contact_post_id, $user_id );
1480 }
1481 else
1482 {
1483 $return['success'] = false;
1484 $return['reason'] = 'validation';
1485 $return['errors'] = array('Failed to update user. Please try again');
1486 }
1487 }
1488
1489 $this->json_headers();
1490 echo json_encode( $return );
1491
1492 // Quit out
1493 die();
1494 }
1495
1496 /**
1497 * Save account requirements
1498 */
1499 public function save_account_requirements()
1500 {
1501 // Validate contact
1502 global $post;
1503
1504 $return = array(
1505 'success' => false,
1506 'errors' => array(),
1507 'new_requirements_nonce' => wp_create_nonce( "ph_requirements" ),
1508 );
1509
1510 // Got an issue with nonce being declined on second submission.
1511 // Need to sort before putting this back in
1512 if ( check_ajax_referer( 'ph_requirements', 'ph_account_requirements_security', false ) === FALSE )
1513 {
1514 $return['errors'][] = 'Invalid nonce';
1515
1516 $this->json_headers();
1517 echo json_encode( $return );
1518
1519 // Quit out
1520 die();
1521 }
1522
1523 // Validate
1524 $errors = array();
1525
1526 $user_id = (int) get_current_user_id();
1527 $current_user = get_user_by( 'id', $user_id );
1528 if ( $user_id <= 0 )
1529 {
1530 $return['success'] = false;
1531 $return['reason'] = 'notloggedin';
1532 $return['errors'] = array('It doesn\'t appear that you\'re logged in');
1533
1534 $this->json_headers();
1535 echo json_encode( $return );
1536
1537 // Quit out
1538 die();
1539 }
1540
1541 $contact = new PH_Contact( '', $user_id );
1542
1543 $contact_post_id = $contact->id;
1544
1545 if ( empty( $contact_post_id ) ) {
1546 $errors[] = __( 'Unable to find your contact record. Please contact the agency.', 'propertyhive' );
1547 }
1548 $requirements_input = array();
1549 foreach ( array( 'profile_id', 'department', 'maximum_price', 'maximum_rent', 'minimum_bedrooms', 'available_as_sale', 'available_as_rent', 'minimum_floor_area', 'maximum_floor_area', 'location_text', 'additional_requirements' ) as $input_key ) {
1550 if ( isset( $_POST[$input_key] ) && ! is_string( $_POST[$input_key] ) ) {
1551 $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
1552 $requirements_input[$input_key] = '';
1553 continue;
1554 }
1555 if ( 'additional_requirements' === $input_key ) {
1556 $requirements_input[$input_key] = isset( $_POST[$input_key] ) ? sanitize_textarea_field( wp_unslash( $_POST[$input_key] ) ) : '';
1557 } else {
1558 $requirements_input[$input_key] = isset( $_POST[$input_key] ) ? sanitize_text_field( wp_unslash( $_POST[$input_key] ) ) : '';
1559 }
1560 }
1561 foreach ( array( 'property_type', 'commercial_property_type', 'location' ) as $input_key ) {
1562 $requirements_input[$input_key] = array();
1563 if ( isset( $_POST[$input_key] ) ) {
1564 if ( ! is_string( $_POST[$input_key] ) && ! is_array( $_POST[$input_key] ) ) {
1565 $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
1566 continue;
1567 }
1568 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Validate element types before unslashing and sanitizing each accepted selection below.
1569 foreach ( (array) $_POST[$input_key] as $selection ) {
1570 if ( ! is_string( $selection ) ) {
1571 $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
1572 continue;
1573 }
1574 $requirements_input[$input_key][] = sanitize_text_field( wp_unslash( $selection ) );
1575 }
1576 }
1577 }
1578 if ( '' !== $requirements_input['profile_id'] && ! ctype_digit( $requirements_input['profile_id'] ) ) {
1579 $errors[] = __( 'Invalid applicant profile', 'propertyhive' );
1580 }
1581 $profile_id = absint( $requirements_input['profile_id'] );
1582 $form_controls = ph_get_applicant_requirements_form_fields();
1583
1584 $form_controls = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls, get_post_meta( $contact_post_id, '_applicant_profile_' . $profile_id, true ) );
1585
1586 foreach ( $form_controls as $key => $control )
1587 {
1588 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
1589 {
1590 // This field is mandatory. Lets check we received it in the post
1591 if ( ! isset( $_POST[$key] ) || ( isset( $_POST[$key] ) && empty( $_POST[$key] ) ) )
1592 {
1593 $errors[] = __( 'Missing required field', 'propertyhive' ) . ': ' . $key;
1594 }
1595 }
1596 }
1597
1598 if ( !empty($errors) )
1599 {
1600 // Failed validation
1601
1602 $return['success'] = false;
1603 $return['reason'] = 'validation';
1604 $return['errors'] = $errors;
1605 }
1606 else
1607 {
1608 $applicant_profile = array();
1609 $applicant_profile['department'] = $requirements_input['department'];
1610
1611 $base_department = $requirements_input['department'];
1612 if ( !in_array( $base_department, array('residential-sales', 'residential-lettings', 'commercial') ) )
1613 {
1614 $base_department = ph_get_custom_department_based_on($base_department);
1615 }
1616
1617 if ( $base_department == 'residential-sales' )
1618 {
1619 $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_price']);
1620
1621 $applicant_profile['max_price'] = $price;
1622
1623 // Not used yet but could be if introducing currencies in the future.
1624 $applicant_profile['max_price_actual'] = $price;
1625
1626 $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
1627 $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
1628
1629 if ( $percentage_lower != '' && $percentage_higher != '' && $requirements_input['maximum_price'] != '' && $requirements_input['maximum_price'] != 0 )
1630 {
1631 $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_price']);
1632 $applicant_profile['match_price_range_lower'] = $price - ( $price * ( $percentage_lower / 100 ) );
1633 $applicant_profile['match_price_range_lower_actual'] = $price - ( $price * ( $percentage_lower / 100 ) );
1634
1635 $applicant_profile['match_price_range_higher'] = $price + ( $price * ( $percentage_higher / 100 ) );
1636 $applicant_profile['match_price_range_higher_actual'] = $price + ( $price * ( $percentage_higher / 100 ) );
1637 }
1638 }
1639 elseif ( $base_department == 'residential-lettings' )
1640 {
1641 $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_rent']);
1642
1643 $applicant_profile['max_rent'] = $price;
1644 $applicant_profile['rent_frequency'] = 'pcm';
1645 $price_actual = $price; // Stored in pcm
1646 $applicant_profile['max_price_actual'] = $price_actual;
1647 }
1648
1649 if ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' )
1650 {
1651 $beds = preg_replace("/[^0-9]/", '', $requirements_input['minimum_bedrooms']);
1652 $applicant_profile['min_beds'] = $beds;
1653
1654 if ( isset($_POST['property_type']) && !empty($_POST['property_type']) )
1655 {
1656 $applicant_profile['property_types'] = $requirements_input['property_type'];
1657 }
1658 }
1659
1660 if ( $base_department == 'commercial' )
1661 {
1662 $available_as = array();
1663 if ( isset($_POST['available_as_sale']) && $requirements_input['available_as_sale'] == 'yes' )
1664 {
1665 $available_as[] = 'sale';
1666 }
1667 if ( isset($_POST['available_as_rent']) && $requirements_input['available_as_rent'] == 'yes' )
1668 {
1669 $available_as[] = 'rent';
1670 }
1671 $applicant_profile['available_as'] = $available_as;
1672
1673 $floor_area = preg_replace("/[^0-9.]/", '', $requirements_input['minimum_floor_area']);
1674 $applicant_profile['min_floor_area'] = $floor_area;
1675 $applicant_profile['min_floor_area_actual'] = $floor_area;
1676
1677 $floor_area = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_floor_area']);
1678 $applicant_profile['max_floor_area'] = $floor_area;
1679 $applicant_profile['max_floor_area_actual'] = $floor_area;
1680
1681 if ( isset($_POST['commercial_property_type']) && !empty($_POST['commercial_property_type']) )
1682 {
1683 $applicant_profile['commercial_property_types'] = $requirements_input['commercial_property_type'];
1684 }
1685 }
1686
1687 if ( isset($_POST['location']) && !empty($_POST['location']) )
1688 {
1689 $applicant_profile['locations'] = $requirements_input['location'];
1690 }
1691
1692 if ( isset($_POST['location_text']) && !empty($_POST['location_text']) )
1693 {
1694 $applicant_profile['location_text'] = $requirements_input['location_text'];
1695 }
1696
1697 $applicant_profile['notes'] = $requirements_input['additional_requirements'];
1698
1699 $applicant_profile['send_matching_properties'] = 'yes';
1700 //$applicant_profile['auto_match_disabled'] = ''; // don't know what to do about this yet. Should probably look at global setting and reflect that
1701
1702 update_post_meta( $contact_post_id, '_applicant_profile_' . $profile_id, wp_slash( $applicant_profile ) );
1703
1704 $return['success'] = true;
1705
1706 do_action( 'propertyhive_account_requirements_updated', $contact_post_id, $user_id );
1707 }
1708
1709 $this->json_headers();
1710 echo json_encode( $return );
1711
1712 // Quit out
1713 die();
1714 }
1715
1716 /**
1717 * Load existing features
1718 */
1719 public function load_existing_features() {
1720
1721 global $post, $wpdb;
1722
1723 $this->json_headers();
1724
1725 $return = array();
1726
1727 $property_query = new WP_Query(array(
1728 'post_type' => 'property',
1729 'post_status' => 'publish',
1730 'nopaging' => true,
1731 'fields' => 'ids',
1732 ));
1733
1734 if ($property_query->have_posts())
1735 {
1736 while ($property_query->have_posts())
1737 {
1738 $property_query->the_post();
1739
1740 $num_property_features = get_post_meta(get_the_ID(), '_features', TRUE);
1741 if ($num_property_features == '') { $num_property_features = 0; }
1742
1743 for ($i = 0; $i < $num_property_features; ++$i)
1744 {
1745 $feature = get_post_meta(get_the_ID(), '_feature_' . $i, TRUE);
1746 if (!in_array($feature, $return) && trim($feature) != '')
1747 {
1748 $return[] = $feature;
1749 }
1750 }
1751 }
1752 }
1753
1754 echo json_encode($return);
1755
1756 // Quit out
1757 die();
1758 }
1759
1760 /**
1761 * Load existing owner on property record
1762 */
1763 public function load_existing_owner_contact() {
1764
1765 check_ajax_referer( 'load-existing-owner-contact', 'security' );
1766
1767 $contact_id = isset( $_POST['contact_id'] ) && is_scalar( $_POST['contact_id'] ) ? absint( $_POST['contact_id'] ) : 0;
1768
1769 $contact = $contact_id > 0 && 'contact' === get_post_type( $contact_id ) ? get_post( $contact_id ) : null;
1770
1771 echo '<div id="existing-owner-details-' . esc_attr($contact_id) . '">';
1772
1773 if ( !is_null( $contact ) )
1774 {
1775 echo '<p class="form-field">';
1776 echo '<label>' . esc_html(__('Name', 'propertyhive')) . '</label>';
1777 echo '<a href="' . esc_url(get_edit_post_link( $contact_id )) . '">' . esc_html(get_the_title($contact_id)) . '</a>';
1778 echo '</p>';
1779
1780 $address = array();
1781 $address_elements = array( '_address_name_number', '_address_street', '_address_two', '_address_three', '_address_four', '_address_postcode' );
1782 foreach ( $address_elements as $address_element )
1783 {
1784 if ( get_post_meta($contact_id, $address_element, TRUE) != '' )
1785 {
1786 $address[] = get_post_meta($contact_id, $address_element, TRUE);
1787 }
1788 }
1789
1790 echo '<p class="form-field">';
1791 echo '<label>' . esc_html(__('Address', 'propertyhive')) . '</label>';
1792 echo ( ( !empty($address) ) ? esc_html(implode(", ", $address)) : '-' );
1793 echo '</p>';
1794
1795 echo '<p class="form-field">';
1796 echo '<label>' . esc_html(__('Telephone Number', 'propertyhive')) . '</label>';
1797 echo ( ( get_post_meta($contact_id, '_telephone_number', TRUE) != '' ) ? esc_html(get_post_meta($contact_id, '_telephone_number', TRUE)) : '-' );
1798 echo '</p>';
1799
1800 echo '<p class="form-field">';
1801 echo '<label>' . esc_html(__('Email Address', 'propertyhive')) . '</label>';
1802 echo ( ( get_post_meta($contact_id, '_email_address', TRUE) != '' ) ? esc_html(get_post_meta($contact_id, '_email_address', TRUE)) : '-' );
1803 echo '</p>';
1804
1805 $contact_solicitor_contact_id = get_post_meta( $contact_id, '_contact_solicitor_contact_id', true );
1806
1807 if ( !empty($contact_solicitor_contact_id) )
1808 {
1809 $solicitor_contact = new PH_Contact($contact_solicitor_contact_id);
1810
1811 echo '<p class="form-field">';
1812 echo '<label>' . esc_html(__('Solicitor', 'propertyhive')) . '</label>';
1813 echo '<a href="' . esc_url(get_edit_post_link($contact_solicitor_contact_id, '')) . '">' . esc_html(get_the_title($contact_solicitor_contact_id) . ( $solicitor_contact->company_name != '' && $solicitor_contact->company_name != get_the_title($contact_solicitor_contact_id) ? ' (' . $solicitor_contact->company_name . ')' : '' )) . '</a>';
1814 echo '</p>';
1815 }
1816 }
1817 else
1818 {
1819 echo esc_html(__( 'Invalid contact record', 'propertyhive' ));
1820 }
1821
1822 echo '<p class="form-field">';
1823 echo '<label></label>';
1824 echo '<a href="" class="button" id="remove-owner-contact-' . esc_attr($contact_id) . '">Remove Owner</a> ';
1825 echo '<a href="" class="button add-additional-owner-contact">Add Additional Owner</a>';
1826 echo '</p>';
1827
1828 echo '</div>';
1829
1830 // Quit out
1831 die();
1832
1833 }
1834
1835 /**
1836 * Search contacts via ajax
1837 */
1838 public function search_contacts() {
1839
1840 global $post;
1841
1842 check_ajax_referer( 'search-contacts', 'security' );
1843
1844 $return = array();
1845
1846 $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
1847 $contact_type = isset( $_POST['contact_type'] ) && is_string( $_POST['contact_type'] ) ? sanitize_text_field( wp_unslash( $_POST['contact_type'] ) ) : '';
1848 $exclude_ids = isset( $_POST['exclude_ids'] ) && is_string( $_POST['exclude_ids'] ) ? sanitize_text_field( wp_unslash( $_POST['exclude_ids'] ) ) : '';
1849
1850 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1851 {
1852 // Get all contacts that match the name
1853 $args = array(
1854 'post_type' => 'contact',
1855 'propertyhive_contact_search_keyword' => $keyword,
1856 'nopaging' => true,
1857 'post_status' => array( 'publish', 'private' ),
1858 'fields' => 'ids'
1859 );
1860 if ( '' !== $contact_type )
1861 {
1862 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact roles are stored in legacy contact metadata; preserve complete keyword-matched ID results and caller exclusions.
1863 $args['meta_query'] = array(
1864 array(
1865 'key' => '_contact_types',
1866 'value' => $contact_type,
1867 'compare' => 'LIKE',
1868 )
1869 );
1870 }
1871 if ( '' !== $exclude_ids )
1872 {
1873 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Contact roles are stored in legacy contact metadata; preserve complete keyword-matched ID results and caller exclusions.
1874 $args['post__not_in'] = array_map( 'absint', explode( '|', $exclude_ids ) );
1875 }
1876
1877 add_filter( 'posts_where', array( $this, 'search_contacts_where' ), 10, 2 );
1878
1879 $contact_query = new WP_Query( $args );
1880
1881 remove_filter( 'posts_where', array( $this, 'search_contacts_where' ) );
1882
1883 if ( $contact_query->have_posts() )
1884 {
1885 while ( $contact_query->have_posts() )
1886 {
1887 $contact_query->the_post();
1888
1889 $contact = new PH_Contact( get_the_ID() );
1890
1891 $return[] = array(
1892 'ID' => get_the_ID(),
1893 'post_title' => get_the_title(get_the_ID()) . ( $contact_type == 'thirdparty' && $contact->company_name != '' && $contact->company_name != get_the_title(get_the_ID()) ? ' (' . $contact->company_name . ')' : '' ) ,
1894 'address_name_number' => $contact->_address_name_number,
1895 'address_street' => $contact->_address_street,
1896 'address_two' => $contact->_address_two,
1897 'address_three' => $contact->_address_three,
1898 'address_four' => $contact->_address_four,
1899 'address_postcode' => $contact->_address_postcode,
1900 'address_country' => $contact->_address_country,
1901 'address_full_formatted' => $contact->get_formatted_full_address(', '),
1902 'telephone_number' => $contact->_telephone_number,
1903 'email_address' => $contact->_email_address,
1904 );
1905 }
1906 }
1907
1908 wp_reset_postdata();
1909 }
1910
1911 $this->json_headers();
1912 echo json_encode( $return );
1913
1914 // Quit out
1915 die();
1916 }
1917
1918 public function search_contacts_where( $where, $wp_query )
1919 {
1920 global $wpdb;
1921
1922 $keyword = $wp_query->get( 'propertyhive_contact_search_keyword', '' );
1923 if ( ! is_string( $keyword ) || '' === $keyword ) {
1924 return $where;
1925 }
1926 $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_title LIKE %s", '%' . $wpdb->esc_like( $keyword ) . '%' );
1927
1928 return $where;
1929 }
1930
1931 /**
1932 * Search propertie via ajax
1933 */
1934 public function search_properties() {
1935
1936 global $post;
1937
1938 check_ajax_referer( 'search-properties', 'security' );
1939
1940 $return = array();
1941
1942 $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
1943
1944 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1945 {
1946 // Get all contacts that match the name
1947 $args = array(
1948 'post_type' => 'property',
1949 'nopaging' => true,
1950 'post_status' => array( 'publish', 'draft', 'private' ),
1951 'fields' => 'ids'
1952 );
1953
1954 $meta_query = array(
1955 array(
1956 'relation' => 'OR',
1957 array(
1958 'key' => '_address_concatenated',
1959 'value' => $keyword,
1960 'compare' => 'LIKE'
1961 ),
1962 array(
1963 'key' => '_reference_number',
1964 'value' => $keyword,
1965 'compare' => '='
1966 ),
1967 ),
1968 );
1969
1970 $department_input = isset( $_POST['department'] ) && is_string( $_POST['department'] ) ? sanitize_text_field( wp_unslash( $_POST['department'] ) ) : '';
1971 if ( '' !== $department_input )
1972 {
1973 $departments_query = array(
1974 'relation' => 'OR',
1975 );
1976
1977 $explode_departments = explode("|", $department_input);
1978 $new_departments = array();
1979 foreach ( $explode_departments as $department )
1980 {
1981 $explode_department = explode("~", $department);
1982
1983 $new_departments[] = $explode_department[0];
1984
1985 $departments_sub_query = array();
1986
1987 $departments_sub_query[] = array(
1988 'key' => '_department',
1989 'value' => $explode_department[0],
1990 );
1991
1992 if ( $explode_department[0] == 'commercial' && isset($explode_department[1]) )
1993 {
1994 switch ($explode_department[1])
1995 {
1996 case "forsale":
1997 {
1998 $departments_sub_query[] = array(
1999 'key' => '_for_sale',
2000 'value' => 'yes',
2001 );
2002 break;
2003 }
2004 }
2005 }
2006
2007 $departments_query[] = $departments_sub_query;
2008 }
2009 $meta_query[] = $departments_query;
2010 }
2011
2012 if ( !empty($meta_query) )
2013 {
2014 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Department/market filters use existing property metadata; preserve the established property-search result set.
2015 $args['meta_query'] = $meta_query;
2016 }
2017
2018 $property_query = new WP_Query( $args );
2019
2020 if ( $property_query->have_posts() )
2021 {
2022 while ( $property_query->have_posts() )
2023 {
2024 $property_query->the_post();
2025
2026 $property = new PH_Property(get_the_ID());
2027
2028 $owner_id = $property->_owner_contact_id;
2029 $owner_name = '';
2030 if ( ( is_array($owner_id) && !empty($owner_id) ) || ( !is_array($owner_id) && $owner_id != '' ) )
2031 {
2032 if ( is_array($owner_id) )
2033 {
2034 $owner_id = reset($owner_id);
2035 }
2036 $owner_name = get_the_title($owner_id);
2037 }
2038
2039 $post_title = $property->get_formatted_full_address();
2040 if ( get_post_status() == 'draft' )
2041 {
2042 $post_title .= ' - Draft';
2043 }
2044
2045 $return[] = array(
2046 'ID' => get_the_ID(),
2047 'post_title' => $post_title,
2048 'owner_id' => $owner_id,
2049 'owner_name' => $owner_name
2050 );
2051 }
2052 }
2053
2054 wp_reset_postdata();
2055 }
2056
2057 $this->json_headers();
2058 echo json_encode( $return );
2059
2060 // Quit out
2061 die();
2062 }
2063
2064 /**
2065 * Search users/negotiators via ajax
2066 */
2067 public function search_negotiators() {
2068
2069 global $post;
2070
2071 check_ajax_referer( 'search-negotiators', 'security' );
2072
2073 $return = array();
2074
2075 $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
2076
2077 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
2078 {
2079 // Get all contacts that match the name
2080 $args = array(
2081 'number' => 9999,
2082 'search' => $keyword . '*',
2083 'orderby' => 'display_name',
2084 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy Property Negotiator compatibility filter; existing role filters depend on this exact public hook name.
2085 'role__not_in' => apply_filters( 'property_negotiator_exclude_roles', array('property_hive_contact', 'subscriber') )
2086 );
2087
2088 $args = apply_filters( 'propertyhive_negotiators_query', $args );
2089
2090 $user_query = new WP_User_Query( $args );
2091
2092 // Get the results
2093 $users = $user_query->get_results();
2094
2095 if ( !empty($users) )
2096 {
2097 foreach ($users as $user)
2098 {
2099 $user_data = get_userdata($user->ID);
2100
2101 $return[] = array(
2102 'ID' => $user->ID,
2103 'post_title' => $user_data->display_name
2104 );
2105 }
2106 }
2107 }
2108
2109 $this->json_headers();
2110 echo json_encode( $return );
2111
2112 // Quit out
2113 die();
2114 }
2115
2116 /**
2117 * Add note via ajax
2118 */
2119 public function add_note() {
2120
2121 check_ajax_referer( 'add-note', 'security' );
2122
2123 if ( ! current_user_can( 'manage_propertyhive' ) )
2124 wp_die( esc_html(__( 'You do not have permission to manage notes', 'propertyhive' )), 403 );
2125
2126 $post_id = isset( $_POST['post_id'] ) && is_scalar( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
2127 if ( $post_id < 1 || ! get_post( $post_id ) || ! current_user_can( 'edit_post', $post_id ) || ! isset( $_POST['note'] ) || ! is_string( $_POST['note'] ) ) {
2128 wp_send_json_error( __( 'Invalid note or insufficient permissions.', 'propertyhive' ), 403 );
2129 }
2130
2131 if ( $post_id > 0 ) {
2132
2133 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Rich mention spans are converted to the established text token below, then all HTML is stripped before storage.
2134 $note = trim( wp_unslash( $_POST['note'] ) );
2135
2136 $pattern = '/<span [^>]*data-post-id="(\d+)"[^>]*>([^<]*)<\/span>/i';
2137 $replacement = function($matches) {
2138 $post_id = $matches[1];
2139 $text = $matches[2];
2140 return '{{mention-' . $post_id . '|' . $text . '}}';
2141 };
2142 $note = preg_replace_callback($pattern, $replacement, $note);
2143
2144 $note = str_replace( array('<br>', '<br />'), "\n", $note );
2145
2146 $note = wp_strip_all_tags( $note );
2147
2148 // Add note/comment to property
2149 $comment = array(
2150 'note_type' => 'note',
2151 'note' => $note
2152 );
2153
2154 if ( isset($_POST['pinned']) )
2155 {
2156 $comment['pinned'] = '1';
2157 }
2158
2159 $comment_id = PH_Comments::insert_note( $post_id, $comment );
2160
2161 if ($comment_id !== FALSE)
2162 {
2163 $comment = get_comment($comment_id);
2164 ?>
2165 <li rel="<?php echo absint( $comment_id ) ; ?>" class="note">
2166 <div class="note_content">
2167 <?php echo wp_kses_post( wpautop( wptexturize( wp_kses_post( $note ) ) ) ); ?>
2168 </div>
2169 <p class="meta">
2170 <abbr class="exact-date" title="<?php echo esc_attr($comment->comment_date_gmt); ?> GMT"><?php /* translators: %s: Elapsed time. */ printf( esc_html__( '%s ago', 'propertyhive' ), esc_html( human_time_diff( strtotime( $comment->comment_date_gmt ), current_time( 'timestamp', 1 ) ) ) ); ?></abbr>
2171 <?php if ( $comment->comment_author !== esc_html__( 'Property Hive', 'propertyhive' ) ) /* translators: %s: Note author. */ printf( ' ' . esc_html__( 'by %s', 'propertyhive' ), esc_html( $comment->comment_author ) ); ?>
2172 <a href="#" class="delete_note"><?php echo esc_html(__( 'Delete', 'propertyhive' )); ?></a>
2173 </p>
2174 </li>
2175 <?php
2176 }
2177 }
2178
2179 // Quit out
2180 die();
2181 }
2182
2183 /**
2184 * Delete order note via ajax
2185 */
2186 public function delete_note() {
2187
2188 check_ajax_referer( 'delete-note', 'security' );
2189
2190 if ( ! current_user_can( 'manage_propertyhive' ) )
2191 wp_send_json_error( __( 'You do not have permission to manage notes', 'propertyhive' ), 403 );
2192
2193 $note_id = isset( $_POST['note_id'] ) && is_scalar( $_POST['note_id'] ) ? absint( $_POST['note_id'] ) : 0;
2194 $note_comment = get_comment( $note_id );
2195 if ( $note_id < 1 || ! $note_comment || 'propertyhive_note' !== $note_comment->comment_type || ! current_user_can( 'edit_post', $note_comment->comment_post_ID ) ) {
2196 wp_send_json_error( __( 'Invalid note or insufficient permissions.', 'propertyhive' ), 403 );
2197 }
2198
2199 if ( $note_id > 0 ) {
2200 wp_delete_comment( $note_id );
2201
2202 wp_send_json_success();
2203 }
2204
2205 wp_send_json_error();
2206 }
2207
2208 /**
2209 * Change existing note entry to be pinned
2210 */
2211 public function toggle_note_pinned() {
2212
2213 check_ajax_referer( 'pin-note', 'security' );
2214
2215 if ( ! current_user_can( 'manage_propertyhive' ) )
2216 wp_send_json_error( __( 'You do not have permission to manage notes', 'propertyhive' ), 403 );
2217
2218 $note_id = isset( $_POST['note_id'] ) && is_scalar( $_POST['note_id'] ) ? absint( $_POST['note_id'] ) : 0;
2219 $note_comment = get_comment( $note_id );
2220 if ( $note_id < 1 || ! $note_comment || 'propertyhive_note' !== $note_comment->comment_type || ! current_user_can( 'edit_post', $note_comment->comment_post_ID ) ) {
2221 wp_send_json_error( __( 'Invalid note or insufficient permissions.', 'propertyhive' ), 403 );
2222 }
2223
2224 if ( $note_id > 0 ) {
2225
2226 $comment = get_comment($note_id);
2227 $comment_content = @unserialize($comment->comment_content, ['allowed_classes' => false]);
2228
2229 if ( is_array( $comment_content ) )
2230 {
2231 if ( isset($comment_content['pinned']))
2232 {
2233 unset($comment_content['pinned']);
2234 }
2235 else
2236 {
2237 $comment_content['pinned'] = '1';
2238 }
2239 }
2240
2241 else {
2242 wp_send_json_error( __( 'Invalid note data.', 'propertyhive' ), 400 );
2243 }
2244 wp_update_comment( wp_slash( array( 'comment_ID' => $note_id, 'comment_content' => serialize( $comment_content ) ) ) );
2245
2246 wp_send_json_success();
2247 }
2248
2249 wp_send_json_error();
2250 }
2251
2252 public function get_notes_grid() {
2253
2254 global $wpdb, $post;
2255
2256 check_ajax_referer( 'get-notes', 'security' );
2257
2258 if ( ! current_user_can( 'manage_propertyhive' ) )
2259 wp_die( esc_html(__( 'You do not have permission to manage notes', 'propertyhive' )), 403 );
2260
2261 $post_id = isset( $_POST['post_id'] ) && is_scalar( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
2262 $post = get_post( $post_id );
2263 if ( $post_id < 1 || ! $post || ! current_user_can( 'edit_post', $post_id ) ) {
2264 wp_send_json_error( __( 'Invalid record or insufficient permissions.', 'propertyhive' ), 403 );
2265 }
2266
2267 $section = isset( $_POST['section'] ) && is_string( $_POST['section'] ) ? sanitize_text_field( wp_unslash( $_POST['section'] ) ) : '';
2268 include( PH()->plugin_path() . '/includes/admin/views/html-display-notes.php' );
2269
2270 // Quit out
2271 die();
2272 }
2273
2274 public function get_pinned_notes_grid() {
2275
2276 global $wpdb, $post;
2277
2278 check_ajax_referer( 'get-notes', 'security' );
2279
2280 if ( ! current_user_can( 'manage_propertyhive' ) )
2281 wp_die( esc_html(__( 'You do not have permission to manage notes', 'propertyhive' )), 403 );
2282
2283 $post_id = isset( $_POST['post_id'] ) && is_scalar( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
2284 $post = get_post( $post_id );
2285 if ( $post_id < 1 || ! $post || ! current_user_can( 'edit_post', $post_id ) ) {
2286 wp_send_json_error( __( 'Invalid record or insufficient permissions.', 'propertyhive' ), 403 );
2287 }
2288
2289 $section = isset( $_POST['section'] ) && is_string( $_POST['section'] ) ? sanitize_text_field( wp_unslash( $_POST['section'] ) ) : '';
2290 include( PH()->plugin_path() . '/includes/admin/views/html-display-notes.php' );
2291
2292 // Quit out
2293 die();
2294 }
2295
2296 public function fetch_note_mentions() {
2297
2298 global $wpdb;
2299
2300 check_ajax_referer( 'get-notes', 'security' );
2301
2302 if ( ! current_user_can( 'manage_propertyhive' ) )
2303 wp_die( esc_html(__( 'You do not have permission to manage notes', 'propertyhive' )), 403 );
2304
2305 $query = isset( $_POST['query'] ) && is_string( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
2306
2307 $mentions = array();
2308
2309 // Get contacts
2310 $args = array(
2311 'post_type' => 'contact',
2312 'posts_per_page' => 10,
2313 'post_status' => array( 'publish' ),
2314 's' => $query
2315 );
2316
2317 $contacts_query = new WP_Query( $args );
2318
2319 if ( $contacts_query->have_posts() )
2320 {
2321 while ( $contacts_query->have_posts() )
2322 {
2323 $contacts_query->the_post();
2324
2325 $contact = new PH_Contact(get_the_ID());
2326
2327 $details = array();
2328 if ( $contact->get_formatted_full_address() != '' )
2329 {
2330 $details[] = $contact->get_formatted_full_address();
2331 }
2332 if ( $contact->email_address != '' || $contact->telephone_number != '' )
2333 {
2334 $sub_details = array();
2335 if ( $contact->email_address != '' )
2336 {
2337 $sub_details[] = 'E: ' . $contact->email_address;
2338 }
2339 if ( $contact->telephone_number != '' )
2340 {
2341 $sub_details[] = 'T: ' . $contact->telephone_number;
2342 }
2343 $details[] = implode(" | ", $sub_details);
2344 }
2345
2346 $mentions[] = array(
2347 'type' => 'contact',
2348 'id' => get_the_ID(),
2349 'name' => get_the_title(),
2350 'details' => implode("<br>", $details),
2351 );
2352 }
2353 }
2354 wp_reset_postdata();
2355
2356 // Get properties
2357 $args = array(
2358 'post_type' => 'property',
2359 'posts_per_page' => 10,
2360 'post_status' => array( 'publish' ),
2361 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Login/contact duplicate/address lookups use a fixed meta relation and return a small result set (1 row for identity checks, 10 for the address autocomplete). posts_per_page=1; posts_per_page=10; fields=ids on all four; values are the authenticated user, submitted email, search text, or current contact.
2362 'meta_query' => array(
2363 'relation' => 'OR',
2364 array(
2365 'key' => '_address_concatenated',
2366 'value' => $query,
2367 'compare' => 'LIKE'
2368 ),
2369 array(
2370 'key' => '_reference_number',
2371 'value' => $query,
2372 'compare' => '='
2373 )
2374 )
2375 );
2376
2377 $properties_query = new WP_Query( $args );
2378
2379 if ( $properties_query->have_posts() )
2380 {
2381 while ( $properties_query->have_posts() )
2382 {
2383 $properties_query->the_post();
2384
2385 $property = new PH_Property(get_the_ID());
2386
2387 $details = array();
2388 if ( $property->get_formatted_price() != '' )
2389 {
2390 $details[] = $property->get_formatted_price();
2391 }
2392 if ( $property->property_type != '' )
2393 {
2394 $details[] = $property->property_type;
2395 }
2396
2397 $mentions[] = array(
2398 'type' => 'property',
2399 'id' => get_the_ID(),
2400 'name' => $property->get_formatted_full_address(),
2401 'details' => implode(" | ", $details),
2402 );
2403 }
2404 }
2405 wp_reset_postdata();
2406
2407 wp_send_json($mentions);
2408 }
2409
2410 /**
2411 * Delete order note via ajax
2412 */
2413 public function make_property_enquiry() {
2414
2415 global $post;
2416
2417 $return = array();
2418
2419 // Validate
2420 $errors = array();
2421 $form_controls = array();
2422
2423 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2424 if ( ! isset( $_POST['property_id'] ) || ! is_string( $_POST['property_id'] ) || empty( $_POST['property_id'] ) )
2425 {
2426 $errors[] = __( 'Property ID is a required field and must be supplied when making an enquiry', 'propertyhive' );
2427 }
2428 else
2429 {
2430 //$post = get_post((int)$_POST['property_id']);
2431
2432 $form_controls = ph_get_property_enquiry_form_fields();
2433
2434 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2435 $form_controls = apply_filters( 'propertyhive_property_enquiry_form_fields', $form_controls, sanitize_text_field( wp_unslash( $_POST['property_id'] ) ) );
2436 }
2437
2438 foreach ( $form_controls as $key => $control )
2439 {
2440 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
2441 {
2442 // This field is mandatory. Lets check we received it in the post
2443 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2444 if ( ! isset( $_POST[$key] ) || ( isset( $_POST[$key] ) && empty( $_POST[$key] ) ) )
2445 {
2446 $errors[] = __( 'Missing required field', 'propertyhive' ) . ': ' . $key;
2447 }
2448 }
2449 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2450 if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) && ( ! is_string( $_POST[$key] ) || ! is_email( wp_unslash( $_POST[$key] ) ) ) )
2451 {
2452 $errors[] = __( 'Invalid email address provided', 'propertyhive' ) . ': ' . $key;
2453 }
2454 if ( in_array( $key, array('recaptcha', 'recaptcha-v3') ) )
2455 {
2456 $errors = $this->check_recaptcha_form_response($errors, $key, $control);
2457 }
2458 if ( $key == 'hCaptcha' )
2459 {
2460 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
2461 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2462 $response = ( isset( $_POST['h-captcha-response'] ) && is_string( $_POST['h-captcha-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['h-captcha-response'] ) ) : '';
2463
2464 $response = wp_remote_post(
2465 'https://hcaptcha.com/siteverify',
2466 array(
2467 'method' => 'POST',
2468 'body' => array( 'secret' => $secret, 'response' => $response ),
2469 )
2470 );
2471
2472 if ( is_wp_error( $response ) )
2473 {
2474 $errors[] = $response->get_error_message();
2475 }
2476 else
2477 {
2478 $response = json_decode($response['body'], TRUE);
2479 if ( $response === FALSE )
2480 {
2481 $errors[] = __( 'Error decoding response from hCaptcha check', 'propertyhive' );
2482 }
2483 else
2484 {
2485 if ( isset($response['success']) && $response['success'] == true )
2486 {
2487
2488 }
2489 else
2490 {
2491 $errors[] = __( 'Failed hCaptcha validation', 'propertyhive' );
2492 }
2493 }
2494 }
2495 }
2496 if ( $key == 'turnstile' )
2497 {
2498 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
2499 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2500 $response = ( isset( $_POST['cf-turnstile-response'] ) && is_string( $_POST['cf-turnstile-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) ) : '';
2501
2502 $response = wp_remote_post(
2503 'https://challenges.cloudflare.com/turnstile/v0/siteverify', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Server-side CAPTCHA token verification API.
2504 array(
2505 'method' => 'POST',
2506 'headers' => array(
2507 'Content-Type' => 'application/x-www-form-urlencoded',
2508 ),
2509 'body' => array( 'secret' => $secret, 'response' => $response ),
2510 )
2511 );
2512
2513 if ( is_wp_error( $response ) )
2514 {
2515 $errors[] = $response->get_error_message();
2516 }
2517 else
2518 {
2519 $response = json_decode($response['body'], TRUE);
2520 if ( $response === FALSE )
2521 {
2522 $errors[] = 'Error decoding response from turnstile check';
2523 }
2524 else
2525 {
2526 if ( isset($response['success']) && $response['success'] == true )
2527 {
2528
2529 }
2530 else
2531 {
2532 $errors[] = 'Failed turnstile validation';
2533 }
2534 }
2535 }
2536 }
2537 }
2538
2539 if (
2540 get_option( 'propertyhive_property_enquiry_form_disclaimer', '' ) != '' &&
2541 (
2542 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2543 !isset( $_POST['disclaimer'] ) ||
2544 (
2545 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2546 isset( $_POST['disclaimer'] ) && empty( $_POST['disclaimer'] )
2547 )
2548 )
2549 )
2550 {
2551 $errors[] = __( 'Missing required field', 'propertyhive' ) . ': disclaimer';
2552 }
2553
2554 // Check only expected fields are received
2555 /*$allowed_keys = array_keys($form_controls);
2556 $allowed_keys[] = 'action';
2557 $allowed_keys[] = 'utm_source';
2558 $allowed_keys[] = 'utm_medium';
2559 $allowed_keys[] = 'utm_term';
2560 $allowed_keys[] = 'utm_content';
2561 $allowed_keys[] = 'utm_campaign';
2562 $allowed_keys[] = 'gclid';
2563 $allowed_keys[] = 'fbclid';
2564 $allowed_keys[] = 'property_id';
2565 $allowed_keys[] = 'disclaimer';
2566 $allowed_keys[] = 'g-recaptcha-response';
2567 $allowed_keys[] = 'h-captcha-response';
2568 $allowed_keys[] = 'cf-turnstile-response';
2569
2570 $allowed_keys = apply_filters(
2571 'propertyhive_property_enquiry_allowed_keys',
2572 $allowed_keys
2573 );
2574
2575 foreach ( $_POST as $key => $value )
2576 {
2577 if ( !in_array($key, $allowed_keys) )
2578 {
2579 // Unexpected field
2580 $errors[] = sprintf(
2581 esc_html__( 'Unexpected field %s received', 'propertyhive' ),
2582 esc_html( $key )
2583 );
2584 break;
2585 }
2586 }*/
2587
2588 // Passed validation
2589 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2590 $property_ids = isset( $_POST['property_id'] ) && is_string( $_POST['property_id'] ) ? array_values( array_filter( array_map( 'absint', explode( '|', sanitize_text_field( wp_unslash( $_POST['property_id'] ) ) ) ) ) ) : array();
2591 if ( empty( $property_ids ) ) {
2592 $errors[] = __( 'Invalid property supplied', 'propertyhive' );
2593 }
2594 if ( count( $property_ids ) > 100 ) {
2595 $errors[] = __( 'Too many properties supplied.', 'propertyhive' );
2596 }
2597 foreach ( $property_ids as $property_id )
2598 {
2599 if ( get_post_type( $property_id ) !== 'property' || ! propertyhive_is_post_publicly_viewable( $property_id ) )
2600 {
2601 $errors[] = __( 'Invalid property supplied', 'propertyhive' );
2602 break;
2603 }
2604 }
2605
2606 if ( !empty($errors) )
2607 {
2608 // Failed validation
2609
2610 $return['success'] = false;
2611 $return['reason'] = 'validation';
2612 $return['errors'] = $errors;
2613 }
2614 else
2615 {
2616 // Get recipient email address
2617 $to = '';
2618
2619 // Try and get office's email address first, else fallback to admin email
2620 $office_id = get_post_meta((int)$property_ids[0], '_office_id', TRUE);
2621 if ( $office_id != '' )
2622 {
2623 if ( get_post_type( $office_id ) == 'office' )
2624 {
2625 $property_department = get_post_meta((int)$property_ids[0], '_department', TRUE);
2626
2627 $fields_to_check = array();
2628 switch ( $property_department )
2629 {
2630 case "residential-sales":
2631 {
2632 $fields_to_check[] = '_office_email_address_sales';
2633 $fields_to_check[] = '_office_email_address_lettings';
2634 $fields_to_check[] = '_office_email_address_commercial';
2635 break;
2636 }
2637 case "residential-lettings":
2638 {
2639 $fields_to_check[] = '_office_email_address_lettings';
2640 $fields_to_check[] = '_office_email_address_sales';
2641 $fields_to_check[] = '_office_email_address_commercial';
2642 break;
2643 }
2644 case "commercial":
2645 {
2646 $fields_to_check[] = '_office_email_address_commercial';
2647 $fields_to_check[] = '_office_email_address_lettings';
2648 $fields_to_check[] = '_office_email_address_sales';
2649 break;
2650 }
2651 default:
2652 {
2653 $fields_to_check[] = '_office_email_address_' . str_replace("residential-", "", $property_department);
2654 $fields_to_check[] = '_office_email_address_sales';
2655 $fields_to_check[] = '_office_email_address_lettings';
2656 $fields_to_check[] = '_office_email_address_commercial';
2657 break;
2658 }
2659 }
2660
2661 foreach ( $fields_to_check as $field_to_check )
2662 {
2663 $to = get_post_meta($office_id, $field_to_check, TRUE);
2664 if ( $to != '' )
2665 {
2666 break;
2667 }
2668 }
2669 }
2670 }
2671 if ( $to == '' )
2672 {
2673 $to = get_option( 'admin_email' );
2674 }
2675
2676 if ( count($property_ids) == 1 )
2677 {
2678 $subject = __( 'New Property Enquiry', 'propertyhive' ) . ': ' . get_the_title( (int)$property_ids[0] );
2679 }
2680 else
2681 {
2682 $subject = __( 'Multiple Property Enquiry', 'propertyhive' ) . ': ' . count($property_ids) . ' Properties';
2683 }
2684 $message = __( "You have received a property enquiry via your website. Please find details of the enquiry below", 'propertyhive' ) . "\n\n";
2685
2686 $message = apply_filters( 'propertyhive_property_enquiry_pre_body', $message, $property_ids );
2687
2688 $message .= ( count($property_ids) > 1 ? __( 'Properties', 'propertyhive' ) : __( 'Property', 'propertyhive' ) ) . ":\n";
2689 foreach ( $property_ids as $property_id )
2690 {
2691 $property = new PH_Property((int)$property_id);
2692 $message .= apply_filters( 'propertyhive_property_enquiry_property_output', $property->get_formatted_full_address() . "\n" . html_entity_decode(wp_strip_all_tags($property->get_formatted_price())) . "\n" . get_permalink( (int)$property_id ), (int)$property_id ) . "\n\n";
2693 }
2694
2695 unset($form_controls['action']);
2696 unset($_POST['action']);
2697 unset($form_controls['property_id']); // Unset so the field doesn't get shown in the enquiry details
2698
2699 $form_controls = apply_filters( 'propertyhive_property_enquiry_body_form_fields', $form_controls );
2700
2701 foreach ($form_controls as $key => $control)
2702 {
2703 if ( isset($control['type']) && in_array($control['type'], array('html', 'recaptcha', 'recaptcha-v3', 'hCaptcha', 'turnstile')) ) { continue; }
2704
2705 $label = ( isset($control['label']) ) ? $control['label'] : $key;
2706 $label = ( isset($control['email_label']) ) ? $control['email_label'] : $label;
2707 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2708 $value = ( isset($_POST[$key]) && is_string($_POST[$key]) ) ? sanitize_textarea_field( wp_unslash( $_POST[$key] ) ) : '';
2709
2710 $message .= wp_strip_all_tags($label) . ": " . wp_strip_all_tags($value) . "\n";
2711 }
2712
2713 if (
2714 apply_filters('propertyhive_enquiry_email_show_manage_link', true) &&
2715 count($property_ids) == 1 &&
2716 get_option( 'propertyhive_module_disabled_enquiries', '' ) != 'yes' &&
2717 get_option( 'propertyhive_store_property_enquiries', 'yes' ) == 'yes'
2718 )
2719 {
2720 $post_type_object = get_post_type_object( 'property' );
2721 $property_enquiries_url = admin_url( sprintf( $post_type_object->_edit_link . '&action=edit', (int)$property_ids[0] ) ) . '#propertyhive-property-enquiries';
2722 $message .= "\n" . __( "To manage this enquiry please visit the following URL", 'propertyhive' ) . ':' . "\n\n";
2723 $message .= $property_enquiries_url;
2724 }
2725
2726 $message = apply_filters( 'propertyhive_property_enquiry_post_body', $message, $property_ids );
2727
2728 $from_email_address = get_option('propertyhive_email_from_address', '');
2729 if ( $from_email_address == '' )
2730 {
2731 $from_email_address = get_option('admin_email');
2732 }
2733 if ( $from_email_address == '' )
2734 {
2735 // Should never get here
2736 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2737 $from_email_address = ( isset( $_POST['email_address'] ) && is_string( $_POST['email_address'] ) ) ? sanitize_email( wp_unslash( $_POST['email_address'] ) ) : '';
2738 }
2739
2740 $headers = array();
2741 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2742 $name = isset( $_POST['name'] )
2743 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2744 ? sanitize_text_field( wp_unslash( $_POST['name'] ) )
2745 : '';
2746
2747 $name = str_replace( array( "\r", "\n" ), '', $name );
2748
2749 $from_email_address = sanitize_email( $from_email_address );
2750
2751 if ( $name !== '' )
2752 {
2753 $headers[] = sprintf( 'From: %s <%s>', $name, $from_email_address );
2754 }
2755 else
2756 {
2757 $headers[] = sprintf( 'From: <%s>', $from_email_address );
2758 }
2759
2760 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2761 if ( isset($_POST['email_address']) )
2762 {
2763 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2764 $reply_to = sanitize_email(wp_unslash($_POST['email_address']));
2765
2766 if ( is_email($reply_to) )
2767 {
2768 $headers[] = 'Reply-To: ' . $reply_to;
2769 }
2770 }
2771
2772 $to = apply_filters( 'propertyhive_property_enquiry_to', $to, $property_ids );
2773 $subject = apply_filters( 'propertyhive_property_enquiry_subject', $subject, $property_ids );
2774 $headers = apply_filters( 'propertyhive_property_enquiry_headers', $headers, $property_ids );
2775 $message = apply_filters( 'propertyhive_property_enquiry_body', $message, $property_ids );
2776
2777 do_action( 'propertyhive_before_property_enquiry_sent' );
2778
2779 $sent = wp_mail( $to, $subject, $message, $headers );
2780
2781 do_action( 'propertyhive_after_property_enquiry_sent' );
2782
2783 if ( ! $sent )
2784 {
2785 $return['success'] = false;
2786 $return['reason'] = 'nosend';
2787 $return['errors'] = $errors;
2788 }
2789 else
2790 {
2791 $return['success'] = true;
2792
2793 $enquiry_post_id = '';
2794
2795 if ( get_option( 'propertyhive_store_property_enquiries', 'yes' ) == 'yes' )
2796 {
2797 // Now insert into enquiries section of WordPress
2798 if ( count($property_ids) == 1 )
2799 {
2800 $title = __( 'Property Enquiry', 'propertyhive' ) . ': ' . get_the_title( (int)$property_ids[0] );
2801 }
2802 else
2803 {
2804 $title = __( 'Multiple Property Enquiry', 'propertyhive' );
2805 }
2806 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2807 if ( isset($_POST['name']) && ! empty($_POST['name']) )
2808 {
2809 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2810 $title .= ' ' . __( 'from', 'propertyhive' ) . ' ' . ph_clean(wp_unslash($_POST['name']));
2811 }
2812
2813 $enquiry_post = array(
2814 'post_title' => $title,
2815 'post_content' => '',
2816 'post_type' => 'enquiry',
2817 'post_status' => 'publish',
2818 'comment_status' => 'closed',
2819 'ping_status' => 'closed',
2820 );
2821
2822 // Insert the post into the database
2823 $enquiry_post_id = wp_insert_post( $enquiry_post );
2824
2825 add_post_meta( $enquiry_post_id, '_status', 'open' );
2826 add_post_meta( $enquiry_post_id, '_source', 'website' );
2827 add_post_meta( $enquiry_post_id, '_negotiator_id', '' );
2828 add_post_meta( $enquiry_post_id, '_office_id', $office_id );
2829
2830 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2831 foreach ($_POST as $key => $value)
2832 {
2833 $meta_key = is_string( $key ) ? $key : '';
2834
2835 // Only store non-empty keys containing characters safe for use as post meta.
2836 if ( $meta_key === '' || ! preg_match( '/\A[A-Za-z0-9_-]+\z/', $meta_key ) )
2837 {
2838 continue;
2839 }
2840
2841 if ( $meta_key == 'property_id' )
2842 {
2843 foreach ( $property_ids as $property_id )
2844 {
2845 add_post_meta( $enquiry_post_id, $meta_key, (int)$property_id );
2846 }
2847 }
2848 else
2849 {
2850 add_post_meta( $enquiry_post_id, $meta_key, sanitize_textarea_field(wp_unslash($value)) );
2851 }
2852 }
2853 }
2854
2855 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2856 do_action('propertyhive_property_enquiry_sent', $_POST, $to, $enquiry_post_id);
2857
2858 // Send auto-responder
2859 if ( get_option( 'propertyhive_enquiry_auto_responder', '' ) == 'yes' )
2860 {
2861 // Auto-responder enabled
2862 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2863 PH()->email->send_enquiry_auto_responder( $_POST );
2864 }
2865 }
2866 }
2867
2868 $this->json_headers();
2869 echo json_encode( $return );
2870
2871 // Quit out
2872 die();
2873 }
2874
2875 /**
2876 * Create contact from enquiry
2877 */
2878 public function create_contact_from_enquiry()
2879 {
2880 global $post;
2881
2882 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- create_contact_from_enquiry reads post_id to construct the action-specific nonce name and reads security as the nonce value; wp_verify_nonce occurs immediately. The event is admin-only and authorize_admin_ajax enforces manage_propertyhive before the callback. These are nonce inputs, not unguarded business mutations.
2883 $enquiry_post_id = isset( $_POST['post_id'] ) && is_scalar( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
2884 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- create_contact_from_enquiry reads post_id to construct the action-specific nonce name and reads security as the nonce value; wp_verify_nonce occurs immediately. The event is admin-only and authorize_admin_ajax enforces manage_propertyhive before the callback. These are nonce inputs, not unguarded business mutations.
2885 $nonce = isset( $_POST['security'] ) && is_string( $_POST['security'] ) ? sanitize_text_field( wp_unslash( $_POST['security'] ) ) : '';
2886
2887 if ( ! wp_verify_nonce( $nonce, 'create-contact-from-enquiry-nonce-' . $enquiry_post_id ) )
2888 {
2889 // This nonce is not valid.
2890 die( json_encode( array('error' => 'Invalid nonce. Please refresh and try again') ) );
2891 }
2892
2893 $enquiry_meta = get_metadata( 'post', $enquiry_post_id );
2894
2895 $name = false;
2896 $email = false;
2897 $telephone = false;
2898 $address = false;
2899 $postcode = false;
2900 $property_id = false;
2901
2902 foreach ($enquiry_meta as $key => $value)
2903 {
2904 if ( strpos(strtolower($key), 'name') !== false && strpos(strtolower($key), 'property') === false && $value[0] != '' )
2905 {
2906 if ( $name === false )
2907 {
2908 $name = $value[0];
2909 }
2910 else
2911 {
2912 $name .= ' ' . $value[0];
2913 }
2914 }
2915 elseif ( strpos(strtolower($key), 'email') !== false && $value[0] != '' )
2916 {
2917 if ( $email === false )
2918 {
2919 $email = $value[0];
2920 }
2921 else
2922 {
2923 $email .= ',' . $value[0];
2924 }
2925 }
2926 elseif ( strpos(strtolower($key), 'phone') !== false && $value[0] != '' )
2927 {
2928 if ( $telephone === false )
2929 {
2930 $telephone = $value[0];
2931 }
2932 else
2933 {
2934 $telephone .= ',' . $value[0];
2935 }
2936 }
2937 elseif ( strtolower($key) == 'address' && $value[0] != '' )
2938 {
2939 $address = $value[0];
2940 }
2941 elseif ( strtolower($key) == 'postcode' && $value[0] != '' )
2942 {
2943 $postcode = $value[0];
2944 }
2945 elseif ( !$property_id && strpos(strtolower($key), 'property_id') !== false && !empty($value[0]) )
2946 {
2947 $property_id = (int)$value[0];
2948 }
2949 }
2950
2951 if ( $name === false && $email === false )
2952 {
2953 die( json_encode( array('error' => 'Name and email address not found') ) );
2954 }
2955
2956 $postdata = array(
2957 'post_excerpt' => '',
2958 'post_content' => '',
2959 'post_title' => wp_strip_all_tags( $name ),
2960 'post_status' => 'publish',
2961 'post_type' => 'contact',
2962 'ping_status' => 'closed',
2963 'comment_status' => 'closed',
2964 );
2965
2966 $contact_post_id = wp_insert_post( $postdata, true );
2967
2968 if ( is_wp_error( $contact_post_id ) )
2969 {
2970 die( json_encode( array('error' => 'Error creating contact') ) );
2971 }
2972 elseif ( $contact_post_id == 0 )
2973 {
2974 die( json_encode( array('error' => 'Error creating contact') ) );
2975 }
2976
2977 update_post_meta( $enquiry_post_id, '_contact_id', $contact_post_id );
2978
2979 if ( $telephone !== FALSE ) {
2980 update_post_meta( $contact_post_id, '_telephone_number', ph_clean( ph_clean_telephone_number( $telephone ) ) );
2981 update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean( ph_clean_telephone_number($telephone) ) );
2982 }
2983
2984 if ( $email !== FALSE ) { update_post_meta( $contact_post_id, '_email_address', ph_clean( $email ) ); }
2985
2986 if ( $address !== FALSE )
2987 {
2988 if ( strpos(strtolower($address), ',') !== false )
2989 {
2990 // Split name/number and street by the first comma
2991 $address_parts = explode(',', $address, 2);
2992 update_post_meta( $contact_post_id, '_address_name_number', ph_clean( trim($address_parts[0]) ) );
2993 update_post_meta( $contact_post_id, '_address_street', ph_clean( trim($address_parts[1]) ) );
2994 }
2995 else
2996 {
2997 $address_parts = explode(' ', $address, 2);
2998 // If first "word" starts with a number (123, 1A etc), put it in name/number
2999 if ( is_numeric(substr($address_parts[0], 0, 1)) )
3000 {
3001 update_post_meta( $contact_post_id, '_address_name_number', ph_clean( trim($address_parts[0]) ) );
3002 update_post_meta( $contact_post_id, '_address_street', ph_clean( trim($address_parts[1]) ) );
3003 }
3004 else
3005 {
3006 update_post_meta( $contact_post_id, '_address_name_number', ph_clean( $address ) );
3007 }
3008 }
3009 }
3010
3011 if ( $postcode !== FALSE ) { update_post_meta( $contact_post_id, '_address_postcode', ph_clean( $postcode ) ); }
3012
3013 // Enquiry is related to a property, so create an applicant record for the contact
3014 if ( !empty( $property_id ) && get_post_type( $property_id ) == 'property' )
3015 {
3016 update_post_meta( $contact_post_id, '_applicant_profiles', '1' );
3017
3018 $applicant_profile = array();
3019 $applicant_profile['department'] = get_post_meta( $property_id, '_department', TRUE );
3020
3021 $base_department = $applicant_profile['department'];
3022 if ( !in_array( $base_department, array('residential-sales', 'residential-lettings', 'commercial') ) )
3023 {
3024 $base_department = ph_get_custom_department_based_on($base_department);
3025 }
3026
3027 if ( $base_department == 'residential-sales' )
3028 {
3029 $property_price = preg_replace("/[^0-9.]/", '', ph_clean(get_post_meta( $property_id, '_price', TRUE )));
3030
3031 if ( !empty($property_price) )
3032 {
3033 $applicant_profile['max_price'] = $property_price;
3034
3035 // Not used yet but could be if introducing currencies in the future.
3036 $applicant_profile['max_price_actual'] = $property_price;
3037
3038 $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
3039 $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
3040
3041 if ( $percentage_lower != '' && $percentage_higher != '' )
3042 {
3043 $applicant_profile['match_price_range_lower'] = $property_price - ( $property_price * ( $percentage_lower / 100 ) );
3044 $applicant_profile['match_price_range_lower_actual'] = $property_price - ( $property_price * ( $percentage_lower / 100 ) );
3045
3046 $applicant_profile['match_price_range_higher'] = $property_price + ( $property_price * ( $percentage_higher / 100 ) );
3047 $applicant_profile['match_price_range_higher_actual'] = $property_price + ( $property_price * ( $percentage_higher / 100 ) );
3048 }
3049 }
3050 }
3051 elseif ( $base_department == 'residential-lettings' )
3052 {
3053 $property_rent = preg_replace("/[^0-9.]/", '', ph_clean(get_post_meta( $property_id, '_rent', TRUE )));
3054 $property_rent_freq = get_post_meta( $property_id, '_rent_frequency', TRUE );
3055
3056 $applicant_profile['max_rent'] = $property_rent;
3057 $applicant_profile['rent_frequency'] = $property_rent_freq;
3058
3059 $price_actual = $property_rent; // Used for ordering properties. Stored in pcm
3060 switch ( $property_rent_freq )
3061 {
3062 case "pw": { $price_actual = ($property_rent * 52) / 12; break; }
3063 case "pcm": { $price_actual = $property_rent; break; }
3064 case "pq": { $price_actual = ($property_rent * 4) / 52; break; }
3065 case "pa": { $price_actual = ($property_rent / 52); break; }
3066 }
3067 $applicant_profile['max_price_actual'] = $price_actual;
3068 }
3069
3070 if ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' )
3071 {
3072 $beds = preg_replace("/[^0-9]/", '', ph_clean(get_post_meta( $property_id, '_bedrooms', TRUE )));
3073 $applicant_profile['min_beds'] = $beds;
3074 }
3075
3076 if ( $base_department == 'commercial' )
3077 {
3078 $property_for_sale = get_post_meta( $property_id, '_for_sale', TRUE );
3079 $property_to_rent = get_post_meta( $property_id, '_to_rent', TRUE );
3080
3081 $available_as = array();
3082 if ( $property_for_sale == 'yes' )
3083 {
3084 $available_as[] = 'sale';
3085 }
3086 if ( $property_to_rent == 'yes' )
3087 {
3088 $available_as[] = 'rent';
3089 }
3090 $applicant_profile['available_as'] = $available_as;
3091 }
3092
3093 $applicant_profile['send_matching_properties'] = apply_filters( 'propertyhive_default_applicant_send_matching_properties', false ) === true ? 'yes' : '';
3094 $applicant_profile['auto_match_disabled'] = 'yes';
3095
3096 $applicant_profile['added_from_enquiry'] = 'yes';
3097
3098 update_post_meta( $contact_post_id, '_applicant_profile_0', $applicant_profile );
3099
3100 update_post_meta( $contact_post_id, '_contact_types', array( 'applicant' ) );
3101 }
3102
3103 do_action('propertyhive_create_contact_from_enquiry', $enquiry_post_id, $contact_post_id);
3104
3105 die( json_encode( array('success' => get_edit_post_link($contact_post_id, '')) ) );
3106 }
3107
3108 public function validate_save_contact()
3109 {
3110 global $post;
3111
3112 check_ajax_referer( 'contact-save-validation', 'security' );
3113
3114 $this->json_headers();
3115
3116 $form_data = array();
3117 if ( isset( $_POST['form_data'] ) && is_string( $_POST['form_data'] ) ) {
3118 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Decode serialized form input first; only the typed and sanitized email address and numeric contact ID below are consumed.
3119 parse_str( wp_unslash( $_POST['form_data'] ), $form_data );
3120 }
3121 $email_address_input = isset( $form_data['_email_address'] ) && is_string( $form_data['_email_address'] ) ? sanitize_text_field( $form_data['_email_address'] ) : '';
3122 $contact_id = isset( $form_data['post_ID'] ) && is_scalar( $form_data['post_ID'] ) ? absint( $form_data['post_ID'] ) : 0;
3123
3124 $return = array('errors' => array());
3125
3126 if ( '' !== $email_address_input )
3127 {
3128 $email_addresses = explode( ",", $email_address_input );
3129
3130 foreach ( $email_addresses as $email_address )
3131 {
3132 $email_address = trim( $email_address );
3133
3134 if ( !is_email($email_address) )
3135 {
3136 $return['errors'][] = __( 'Email address is invalid', 'propertyhive' ) . ' - ' . $email_address;
3137 }
3138
3139 // Check it doesn't exist already
3140 $args = array(
3141 'post_type' => 'contact',
3142 'post_status' => 'any',
3143 'posts_per_page' => 1,
3144 'fields' => 'ids',
3145 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Login/contact duplicate/address lookups use a fixed meta relation and return a small result set (1 row for identity checks, 10 for the address autocomplete). posts_per_page=1; posts_per_page=10; fields=ids on all four; values are the authenticated user, submitted email, search text, or current contact.
3146 'meta_query' => array(
3147 'relation' => 'OR',
3148 array(
3149 'key' => '_email_address',
3150 'value' => $email_address,
3151 'compare' => '='
3152 ),
3153 array(
3154 'key' => '_email_address',
3155 'value' => ',' . $email_address,
3156 'compare' => 'LIKE'
3157 )
3158 ,
3159 array(
3160 'key' => '_email_address',
3161 'value' => $email_address . ',',
3162 'compare' => 'LIKE'
3163 )
3164 )
3165 );
3166 if ( $contact_id )
3167 {
3168 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Login/contact duplicate/address lookups use a fixed meta relation and return a small result set (1 row for identity checks, 10 for the address autocomplete). posts_per_page=1; posts_per_page=10; fields=ids on all four; values are the authenticated user, submitted email, search text, or current contact.
3169 $args['post__not_in'] = array( $contact_id );
3170 }
3171
3172 $contact_query = new WP_Query( $args );
3173
3174 if ( $contact_query->have_posts() )
3175 {
3176 while ( $contact_query->have_posts() )
3177 {
3178 $contact_query->the_post();
3179
3180 /* translators: 1: Contact name, 2: Email address. */
3181 $return['errors'][] = sprintf( __( 'A contact, %1$s, already exists with email address %2$s', 'propertyhive' ), get_the_title(), $email_address );
3182 }
3183 }
3184 }
3185 }
3186
3187 echo json_encode($return);
3188
3189 die();
3190 }
3191
3192 public function merge_contact_records()
3193 {
3194 $this->json_headers();
3195
3196 if ( ! isset( $_POST['nonce'] ) || ! check_ajax_referer( 'propertyhive_merge_contact', 'nonce', false ) )
3197 {
3198 $return = array('error' => 'Invalid nonce');
3199 echo json_encode( $return );
3200 die();
3201 }
3202
3203 if ( !isset( $_POST['contact_ids'] ) || !is_string( $_POST['contact_ids'] ) || empty( $_POST['contact_ids'] ) || !isset( $_POST['primary_contact_id'] ) || !is_string( $_POST['primary_contact_id'] ) || empty( $_POST['primary_contact_id'] ) )
3204 {
3205 $return = array('error' => 'Invalid parameters received');
3206 echo json_encode( $return );
3207 die();
3208 }
3209
3210 $contacts_to_merge = array_values( array_unique( array_filter( array_map( 'absint', explode( '|', sanitize_text_field( wp_unslash( $_POST['contact_ids'] ) ) ) ) ) ) );
3211
3212 $primary_contact_id = absint( wp_unslash( $_POST['primary_contact_id'] ) );
3213
3214 if ( count( $contacts_to_merge ) < 2 || !in_array( $primary_contact_id, $contacts_to_merge, true ) )
3215 {
3216 $return = array('error' => 'Invalid Contact IDs received');
3217 echo json_encode( $return );
3218 die();
3219 }
3220
3221 if ( get_post_type( $primary_contact_id ) !== 'contact' )
3222 {
3223 $return = array('error' => 'Primary contact ' . $primary_contact_id . ' is not a contact');
3224 echo json_encode( $return );
3225 die();
3226 }
3227
3228 if ( !current_user_can( 'manage_propertyhive' ) || !current_user_can( 'edit_post', $primary_contact_id ) )
3229 {
3230 $return = array('error' => 'Insufficient permissions for primary contact');
3231 echo json_encode( $return );
3232 die();
3233 }
3234
3235 // Check each post ID passed through is in fact of post type 'contact'
3236 foreach ( $contacts_to_merge as $child_contact_id )
3237 {
3238 if ( get_post_type((int)$child_contact_id) !== 'contact' )
3239 {
3240 $return = array('error' => 'Contact ID ' . $child_contact_id . ' is not a contact');
3241 echo json_encode( $return );
3242 die();
3243 }
3244
3245 if ( !current_user_can( 'edit_post', $child_contact_id ) )
3246 {
3247 $return = array('error' => 'Insufficient permissions for contact ID ' . $child_contact_id );
3248 echo json_encode( $return );
3249 die();
3250 }
3251 }
3252
3253 // Remove primary from list
3254 unset($contacts_to_merge[array_search($primary_contact_id, $contacts_to_merge)]);
3255
3256 include_once PH()->plugin_path() . '/includes/admin/class-ph-admin-merge-contacts.php';
3257 $ph_admin_merge_contacts = new PH_Admin_Merge_Contacts();
3258 $ph_admin_merge_contacts->do_merge( $primary_contact_id, $contacts_to_merge );
3259
3260 echo json_encode( array('success' => true) );
3261 die();
3262 }
3263
3264 // Dashboard related functions
3265 public function get_news()
3266 {
3267 $this->json_headers();
3268
3269 include_once( ABSPATH . WPINC . '/feed.php' );
3270
3271 $return = array();
3272
3273 // Get a SimplePie feed object from the specified feed source.
3274 $rss = fetch_feed( 'https://wp-property-hive.com/category/property-hive-news/feed/' );
3275
3276 $maxitems = 0;
3277
3278 if ( ! is_wp_error( $rss ) ) : // Checks that the object is created correctly
3279
3280 // Figure out how many total items there are, but limit it to 5.
3281 $maxitems = $rss->get_item_quantity( 5 );
3282
3283 // Build an array of all the items, starting with element 0 (first element).
3284 $rss_items = $rss->get_items( 0, $maxitems );
3285
3286 foreach ( $rss_items as $item )
3287 {
3288 $return[] = array(
3289 'title' => esc_html( $item->get_title() ),
3290 'permalink' => esc_url( $item->get_permalink() ) . '?src=dashboard',
3291 'date' => $item->get_date('F d, Y')
3292 );
3293 }
3294
3295 endif;
3296
3297 echo json_encode($return);
3298
3299 die();
3300 }
3301
3302 public function get_viewings_awaiting_applicant_feedback()
3303 {
3304 global $post;
3305
3306 $this->json_headers();
3307
3308 $return = array();
3309
3310 $args = array(
3311 'post_type' => 'viewing',
3312 'fields' => 'ids',
3313 'post_status' => 'publish',
3314 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Dashboard selects viewing status/feedback from existing metadata with WordPress's default page limit; extension query filters remain supported.
3315 'meta_query' => array(
3316 array(
3317 'key' => '_status',
3318 'value' => 'carried_out'
3319 ),
3320 array(
3321 'key' => '_feedback_status',
3322 'value' => ''
3323 )
3324 )
3325 );
3326
3327 $args = apply_filters( 'propertyhive_admin_dashboard_viewings_awaiting_applicant_feedback_args', $args );
3328
3329 $viewings_query = new WP_Query( $args );
3330
3331 if ( $viewings_query->have_posts() )
3332 {
3333 while ( $viewings_query->have_posts() )
3334 {
3335 $viewings_query->the_post();
3336
3337 $property_id = get_post_meta( get_the_ID(), '_property_id', TRUE );
3338 $property = new PH_Property((int)$property_id);
3339
3340 $applicant_contact_ids = get_post_meta( get_the_ID(), '_applicant_contact_id' );
3341
3342 $return[] = array(
3343 'ID' => get_the_ID(),
3344 'edit_link' => get_edit_post_link( get_the_ID() ),
3345 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
3346 'start_date_time_formatted_Hi_jSFY' => gmdate("H:i jS F Y", strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE ))),
3347 'property_id' => $property_id,
3348 'property_address' => $property->get_formatted_full_address(),
3349 'applicant_contact_id' => $applicant_contact_ids[0],
3350 'applicant_name' => get_the_title( $applicant_contact_ids[0] ),
3351 );
3352 }
3353 }
3354
3355 wp_reset_postdata();
3356
3357 echo json_encode($return);
3358
3359 die();
3360 }
3361
3362 public function get_my_upcoming_appointments()
3363 {
3364 global $post;
3365
3366 $this->json_headers();
3367
3368 $return = array();
3369
3370 $args = array(
3371 'post_type' => 'viewing',
3372 'fields' => 'ids',
3373 'post_status' => 'publish',
3374 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Dashboard scopes upcoming events by status, time and current negotiator metadata with WordPress's default page limit.
3375 'meta_query' => array(
3376 array(
3377 'key' => '_status',
3378 'value' => 'pending'
3379 ),
3380 array(
3381 'key' => '_start_date_time',
3382 'value' => gmdate("Y-m-d H:i:s"),
3383 'compare' => '>='
3384 ),
3385 array(
3386 'key' => '_negotiator_id',
3387 'value' => get_current_user_id(),
3388 ),
3389 )
3390 );
3391
3392 $args = apply_filters( 'propertyhive_admin_dashboard_my_upcoming_appointments_viewing_args', $args );
3393 $args = apply_filters( 'propertyhive_admin_dashboard_my_upcoming_appointments_args', $args );
3394
3395 $viewings_query = new WP_Query( $args );
3396
3397 if ( $viewings_query->have_posts() )
3398 {
3399 while ( $viewings_query->have_posts() )
3400 {
3401 $viewings_query->the_post();
3402
3403 $property_id = get_post_meta( get_the_ID(), '_property_id', TRUE );
3404 $property = new PH_Property((int)$property_id);
3405
3406 $return[] = array(
3407 'ID' => get_the_ID(),
3408 'edit_link' => get_edit_post_link( get_the_ID() ),
3409 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
3410 'start_date_time_formatted_Hi_jSFY' => gmdate("H:i jS F Y", strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE ))),
3411 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
3412 'title' => 'Viewing at ' . $property->get_formatted_full_address(),
3413 );
3414 }
3415 }
3416
3417 wp_reset_postdata();
3418
3419 $args = array(
3420 'post_type' => 'appraisal',
3421 'fields' => 'ids',
3422 'post_status' => 'publish',
3423 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Dashboard scopes upcoming events by status, time and current negotiator metadata with WordPress's default page limit.
3424 'meta_query' => array(
3425 array(
3426 'key' => '_status',
3427 'value' => 'pending'
3428 ),
3429 array(
3430 'key' => '_start_date_time',
3431 'value' => gmdate("Y-m-d H:i:s"),
3432 'compare' => '>='
3433 ),
3434 array(
3435 'key' => '_negotiator_id',
3436 'value' => get_current_user_id(),
3437 ),
3438 )
3439 );
3440
3441 $args = apply_filters( 'propertyhive_admin_dashboard_my_upcoming_appointments_appraisal_args', $args );
3442 $args = apply_filters( 'propertyhive_admin_dashboard_my_upcoming_appointments_args', $args );
3443
3444 $appraisals_query = new WP_Query( $args );
3445
3446 if ( $appraisals_query->have_posts() )
3447 {
3448 while ( $appraisals_query->have_posts() )
3449 {
3450 $appraisals_query->the_post();
3451
3452 $appraisal = new PH_Appraisal(get_the_ID());
3453
3454 $return[] = array(
3455 'ID' => get_the_ID(),
3456 'edit_link' => get_edit_post_link( get_the_ID() ),
3457 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
3458 'start_date_time_formatted_Hi_jSFY' => gmdate("H:i jS F Y", strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE ))),
3459 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
3460 'title' => 'Appraisal at ' . $appraisal->get_formatted_full_address(),
3461 );
3462 }
3463 }
3464
3465 wp_reset_postdata();
3466
3467 $return = apply_filters( 'propertyhive_dashboard_my_upcoming_appointments', $return );
3468
3469 if ( !empty($return) )
3470 {
3471 $sort = array();
3472 foreach ($return as $key => $part) {
3473 $sort[$key] = strtotime($part['start_date_time']);
3474 }
3475 array_multisort($sort, SORT_ASC, $return);
3476
3477 $return = array_slice($return, 0, 10);
3478 }
3479
3480 echo json_encode($return);
3481
3482 die();
3483 }
3484
3485 public function get_upcoming_overdue_key_dates()
3486 {
3487 global $post;
3488
3489 $this->json_headers();
3490
3491 $return = array();
3492
3493 $meta_query = array(
3494 array(
3495 'key' => '_key_date_status',
3496 'value' => 'pending',
3497 ),
3498 );
3499
3500 $upcoming_threshold = new DateTime('+ ' . apply_filters( 'propertyhive_key_date_upcoming_days', 7 ) . ' DAYS');
3501 $meta_query[] = array(
3502 'key' => '_date_due',
3503 'value' => $upcoming_threshold->format('Y-m-d'),
3504 'type' => 'date',
3505 'compare' => '<=',
3506 );
3507
3508 $args = array(
3509 'post_type' => 'key_date',
3510 'fields' => 'ids',
3511 'post_status' => 'publish',
3512 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Dashboard filters and orders due dates stored in key-date metadata with WordPress's default page limit.
3513 'meta_query' => $meta_query,
3514 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Dashboard filters and orders due dates stored in key-date metadata with WordPress's default page limit.
3515 'meta_key' => '_date_due',
3516 'orderby' => 'meta_value',
3517 'order' => 'ASC',
3518 );
3519
3520 $args = apply_filters( 'propertyhive_admin_dashboard_upcoming_overdue_key_dates_args', $args );
3521
3522 $key_dates_query = new WP_Query( $args );
3523
3524 if ( $key_dates_query->have_posts() )
3525 {
3526 while ( $key_dates_query->have_posts() )
3527 {
3528 $key_dates_query->the_post();
3529
3530 $key_date = new PH_Key_Date( get_post( get_the_ID() ) );
3531
3532 $property_id = get_post_meta( get_the_ID(), '_property_id', TRUE );
3533 $property_edit_link = '';
3534 $property_address = '';
3535 if ( !empty($property_id) )
3536 {
3537 $property = new PH_Property((int)$property_id);
3538 $property_edit_link = get_edit_post_link( $property_id );
3539 $property_address = $property->get_formatted_full_address();
3540 }
3541
3542 $tenancy_id = get_post_meta( get_the_ID(), '_tenancy_id', TRUE );
3543 if ( !empty($tenancy_id) )
3544 {
3545 $key_date_edit_link = get_edit_post_link( $tenancy_id ) . '#propertyhive-tenancy-management%7Cpropertyhive-management-dates';
3546 }
3547 else
3548 {
3549 $key_date_edit_link = $property_edit_link . '#propertyhive-property-tenancies%7Cpropertyhive-management-dates';
3550 }
3551
3552 $due_date = $key_date->date_due();
3553 $date_format = 'jS F Y';
3554 if ( $due_date->format('H:i') != '00:00' )
3555 {
3556 $date_format = 'H:i ' . $date_format;
3557 }
3558
3559 $return[] = array(
3560 'ID' => get_the_ID(),
3561 'key_date_edit_link' => $key_date_edit_link,
3562 'description' => $key_date->description(),
3563 'upcoming_overdue_status' => $key_date->status(),
3564 'property_edit_link' => $property_edit_link,
3565 'property_address' => $property_address,
3566 'due_date_time_formatted' => $due_date->format($date_format),
3567 );
3568 }
3569 }
3570
3571 wp_reset_postdata();
3572
3573 echo json_encode($return);
3574
3575 die();
3576 }
3577
3578 public function check_duplicate_reference_number()
3579 {
3580 check_ajax_referer( 'check-duplicate-reference-number', 'security' );
3581
3582 if ( !isset($_POST['reference_number']) || empty($_POST['reference_number']) )
3583 {
3584 echo '';
3585 die();
3586 }
3587
3588 $args = array(
3589 'post_type' => 'property',
3590 'post_status' => 'publish',
3591 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Duplicate detection must match on-market/reference metadata and exclude the current integer record ID; query retains the default page limit.
3592 'meta_query' => array(
3593 array(
3594 'key' => '_on_market',
3595 'value' => 'yes'
3596 ),
3597 array(
3598 'key' => '_reference_number',
3599 'value' => sanitize_text_field( wp_unslash( $_POST['reference_number'] ) )
3600 ),
3601 ),
3602 );
3603
3604 if ( isset($_POST['post_id']) && !empty($_POST['post_id']) )
3605 {
3606 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Duplicate detection must match on-market/reference metadata and exclude the current integer record ID; query retains the default page limit.
3607 $args['post__not_in'] = array((int)$_POST['post_id']);
3608 }
3609
3610 $property_query = new WP_Query($args);
3611
3612 if ( $property_query->have_posts() )
3613 {
3614 echo '1';
3615 die();
3616 }
3617
3618 echo '';
3619 die();
3620 }
3621
3622 public function osm_geocoding_request()
3623 {
3624 check_ajax_referer( 'osm_geocoding_request', 'security' );
3625
3626 if ( ! isset( $_POST['country'], $_POST['address'] ) || ! is_string( $_POST['country'] ) || ! is_string( $_POST['address'] ) ) {
3627 wp_send_json( array( 'error' => 'Invalid geocoding address.', 'lat' => '', 'lng' => '' ) );
3628 }
3629 $country = sanitize_text_field( wp_unslash( $_POST['country'] ) );
3630 $address = sanitize_text_field( wp_unslash( $_POST['address'] ) );
3631
3632 $lat = '';
3633 $lng = '';
3634 $error = '';
3635
3636 // Rate limit: 1 request/second
3637 $rate_key = 'ph_osm_geo_last_ts';
3638 $last_ts = (int)get_transient( $rate_key );
3639 $now = time();
3640
3641 if ( $last_ts && ($now - $last_ts) < 1 )
3642 {
3643 // Too soon: tell client to retry shortly
3644 $error = 'Too many geocoding requests. Please wait a second and try again.';
3645 wp_send_json( array( 'error' => $error ) );
3646 }
3647
3648 // Set timestamp immediately to prevent stampedes
3649 set_transient( $rate_key, $now );
3650
3651 $request_url = add_query_arg( array(
3652 'format' => 'json',
3653 'limit' => 1,
3654 'countrycodes' => rawurlencode( strtolower( $country ) ),
3655 'addressdetails' => 1,
3656 'q' => rawurlencode( $address ),
3657 ), 'https://nominatim.openstreetmap.org/search' );
3658
3659 $response = wp_remote_get(
3660 $request_url,
3661 array(
3662 'headers' => array(
3663 'Referer' => home_url(),
3664 'User-Agent' => 'Property-Hive/' . PH_VERSION . ' (+https://wp-property-hive.com)',
3665 ),
3666 )
3667 );
3668
3669 if ( is_wp_error( $response ))
3670 {
3671 $error = $response->get_error_message();
3672 wp_send_json( array( 'error' => $error, 'lat' => $lat, 'lng' => $lng ) );
3673 }
3674
3675 if ( wp_remote_retrieve_response_code($response) !== 200 )
3676 {
3677 $error = wp_remote_retrieve_response_code($response) . ' response received when geocoding address ' . $address . '. Error message: ' . wp_remote_retrieve_response_message($response);
3678 wp_send_json( array( 'error' => $error, 'lat' => $lat, 'lng' => $lng ) );
3679 }
3680
3681 if ( is_array( $response ) )
3682 {
3683 $body = wp_remote_retrieve_body( $response );
3684 $json = json_decode($body, true);
3685
3686 if ( !empty($json) && isset($json[0]['lat']) && isset($json[0]['lon']) )
3687 {
3688 $lat = $json[0]['lat'];
3689 $lng = $json[0]['lon'];
3690 }
3691 else
3692 {
3693 $error = 'No co-ordinates returned for the address provided ' . $address . ': ' . $body;
3694 }
3695 }
3696 else
3697 {
3698 $error = 'Failed to parse JSON response from OSM Geocoding service: ' . wp_json_encode( $response );
3699 }
3700
3701 wp_send_json( array( 'error' => $error, 'lat' => $lat, 'lng' => $lng ) );
3702 }
3703
3704 public function get_property_marketing_statistics_meta_box()
3705 {
3706 check_ajax_referer( 'get_property_marketing_statistics_meta_box', 'security' );
3707
3708 global $post;
3709 $post_id = isset( $_POST['post_id'] ) && is_scalar( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
3710 if ( $post_id < 1 || 'property' !== get_post_type( $post_id ) || ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'edit_post', $post_id ) ) {
3711 wp_send_json_error( __( 'Invalid property or insufficient permissions.', 'propertyhive' ), 403 );
3712 }
3713
3714
3715
3716
3717 $view_statistics = get_post_meta( $post_id, '_view_statistics', TRUE );
3718 if ( !is_array($view_statistics) )
3719 {
3720 $view_statistics = array();
3721 }
3722
3723 $date_from = isset( $_POST['statistics_date_from'] ) && is_string( $_POST['statistics_date_from'] ) ? sanitize_text_field( wp_unslash( $_POST['statistics_date_from'] ) ) : gmdate("Y-m-d", strtotime('7 days ago'));
3724 $date_from = strtotime($date_from);
3725
3726 $date_to = isset( $_POST['statistics_date_to'] ) && is_string( $_POST['statistics_date_to'] ) ? sanitize_text_field( wp_unslash( $_POST['statistics_date_to'] ) ) : gmdate("Y-m-d");
3727 $date_to = strtotime($date_to);
3728 if ( false === $date_from || false === $date_to ) {
3729 wp_send_json_error( __( 'Invalid statistics dates.', 'propertyhive' ), 400 );
3730 }
3731
3732 echo '<div class="propertyhive_meta_box"><div class="options_group">';
3733 $view_statistics_output = array();
3734 $total_views = 0;
3735
3736 for ($i = $date_from; $i <= $date_to; $i += 86400)
3737 {
3738 if ( isset($view_statistics[gmdate("Y-m-d", $i)]) )
3739 {
3740 $view_statistics_output[] = array( $i * 1000, $view_statistics[gmdate("Y-m-d", $i)] );
3741 $total_views += $view_statistics[gmdate("Y-m-d", $i)];
3742 }
3743 else
3744 {
3745 $view_statistics_output[] = array( $i * 1000, 0 );
3746 }
3747 }
3748
3749 echo '<h3>' . esc_html(__( 'Views On Website', 'propertyhive' )) . ' (' . esc_html(number_format($total_views, 0)) . ')</h3>';
3750
3751 echo '<div id="marketing_statistics_website_view_graph" style="height:400px; width:100%;"></div>';
3752
3753 echo '</div>';
3754
3755 echo '</div>';
3756
3757 echo '<input type="hidden" name="marketing_statistics" id="marketing_statistics" value="' . esc_attr(json_encode($view_statistics_output)) . '">';
3758
3759 die();
3760 }
3761
3762 public function get_appraisal_details_meta_box()
3763 {
3764 global $post;
3765
3766 check_ajax_referer( 'appraisal-details-meta-box', 'security' );
3767
3768 $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
3769 $post = get_post( $post_id );
3770
3771 $appraisal = new PH_Appraisal( $post_id );
3772
3773 echo '<div class="propertyhive_meta_box">';
3774
3775 echo '<div class="options_group">';
3776
3777 echo '<p class="form-field">
3778
3779 <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
3780
3781 ' . esc_html(ucwords(str_replace("_", " ", $appraisal->status)));
3782
3783 echo '</p>';
3784
3785 if ( $appraisal->status == 'cancelled' )
3786 {
3787 $args = array(
3788 'id' => '_cancelled_reason',
3789 'label' => __( 'Reason Cancelled', 'propertyhive' ),
3790 'desc_tip' => false,
3791 'class' => '',
3792 'value' => $appraisal->cancelled_reason,
3793 'custom_attributes' => array(
3794 'style' => 'width:95%; max-width:500px;'
3795 )
3796 );
3797 propertyhive_wp_textarea_input( $args );
3798 }
3799
3800 if ( $appraisal->status == 'carried_out' || $appraisal->status == 'won' || $appraisal->status == 'instructed' )
3801 {
3802 $ph_countries = new PH_Countries();
3803
3804 $currency = 'GBP';
3805 $currency_symbol = '&pound;';
3806
3807 $default_country = get_option( 'propertyhive_default_country', 'GB' );
3808 $countries = get_option( 'propertyhive_countries', array( $default_country ) );
3809 if ( count($countries) == 1 )
3810 {
3811 foreach ( $countries as $country )
3812 {
3813 $country = $ph_countries->get_country( $country );
3814
3815 $currency = $country['currency_code'];
3816 }
3817 }
3818
3819 $currency = $ph_countries->get_currency( $currency );
3820 if ( isset($currency['currency_symbol']) )
3821 {
3822 $currency_symbol = $currency['currency_symbol'];
3823 }
3824
3825 if ( $appraisal->department == 'residential-sales' )
3826 {
3827 $args = array(
3828 'id' => '_valued_price',
3829 'label' => __( 'Valued Price', 'propertyhive' ) . ' (' . $currency_symbol . ')',
3830 'desc_tip' => false,
3831 'class' => 'short',
3832 'value' => ph_display_price_field( $appraisal->valued_price ),
3833 );
3834 propertyhive_wp_text_input( $args );
3835 }
3836 elseif ( $appraisal->department == 'residential-lettings' )
3837 {
3838 $rent_frequency = $appraisal->valued_rent_frequency;
3839
3840 echo '<p class="form-field">
3841
3842 <label for="">' . esc_html(__('Valued Rent', 'propertyhive')) . ' (' . esc_html($currency_symbol) . ')</label>
3843
3844 <input type="text" class="" name="_valued_rent" id="_valued_rent" value="' . esc_attr(ph_display_price_field( $appraisal->valued_rent )) . '" placeholder="" style="width:10%; min-width:100px;">
3845
3846 <select id="_valued_rent_frequency" name="_valued_rent_frequency" class="select" style="width:auto">
3847 <option value="pd"' . ( ($rent_frequency == 'pd') ? ' selected' : '') . '>' . esc_html(__('Per Day', 'propertyhive')) . '</option>
3848 <option value="pppw"' . ( ($rent_frequency == 'pppw') ? ' selected' : '') . '>' . esc_html(__('Per Person Per Week', 'propertyhive')) . '</option>
3849 <option value="pw"' . ( ($rent_frequency == 'pw') ? ' selected' : '') . '>' . esc_html(__('Per Week', 'propertyhive')) . '</option>
3850 <option value="pcm"' . ( ($rent_frequency == 'pcm' || $rent_frequency == '') ? ' selected' : '') . '>' . esc_html(__('Per Calendar Month', 'propertyhive')) . '</option>
3851 <option value="pq"' . ( ($rent_frequency == 'pq') ? ' selected' : '') . '>' . esc_html(__('Per Quarter', 'propertyhive')) . '</option>
3852 <option value="pa"' . ( ($rent_frequency == 'pa') ? ' selected' : '') . '>' . esc_html(__('Per Annum', 'propertyhive')) . '</option>
3853 </select>
3854
3855 </p>';
3856 }
3857 }
3858
3859 if ( $appraisal->status == 'lost' )
3860 {
3861 $args = array(
3862 'id' => '_lost_reason',
3863 'label' => __( 'Reason Lost', 'propertyhive' ),
3864 'desc_tip' => false,
3865 'class' => '',
3866 'value' => $appraisal->lost_reason,
3867 'custom_attributes' => array(
3868 'style' => 'width:95%; max-width:500px;'
3869 )
3870 );
3871 propertyhive_wp_textarea_input( $args );
3872 }
3873
3874 do_action('propertyhive_appraisal_details_fields');
3875
3876 echo '</div>';
3877
3878 echo '</div>';
3879
3880 die();
3881 }
3882
3883 public function get_appraisal_actions()
3884 {
3885 check_ajax_referer( 'appraisal-actions', 'security' );
3886
3887 $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
3888
3889 $status = get_post_meta( $post_id, '_status', TRUE );
3890 $department = get_post_meta( $post_id, '_department', TRUE );
3891
3892 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_appraisal_actions_meta_box">
3893
3894 <div class="options_group" style="padding-top:8px;">';
3895
3896 $show_cancelled_meta_boxes = false;
3897 $show_carried_out_meta_boxes = false;
3898 $show_instructed_meta_boxes = false;
3899 $show_lost_meta_boxes = false;
3900 $show_customise_confirmation_meta_boxes = false;
3901
3902 $actions = array();
3903
3904 if ( $status == 'pending' )
3905 {
3906 $owner_booking_confirmation_sent_at = get_post_meta( $post_id, '_owner_booking_confirmation_sent_at', TRUE );
3907
3908 $appraisal_department = get_post_meta( $post_id, '_department', TRUE );
3909 $owner_contact_id = get_post_meta( $post_id, '_property_owner_contact_id', TRUE );
3910 $owner_or_landlord = ( $appraisal_department == 'residential-lettings' ? 'Landlord' : 'Owner' );
3911
3912 if ( !empty($owner_contact_id) )
3913 {
3914 if ( get_option( 'propertyhive_customise_confirmation_emails', '' ) == 'yes' )
3915 {
3916 $actions[] = '<a
3917 href="#action_panel_appraisal_email_owner_booking_confirmation_customise"
3918 class="button appraisal-action"
3919 style="width:100%; margin-bottom:7px; text-align:center"
3920 >' . ( ( $owner_booking_confirmation_sent_at == '' ) ? esc_html(( $owner_or_landlord === 'Landlord' ? esc_html__( 'Email Landlord Booking Confirmation', 'propertyhive' ) : esc_html__( 'Email Owner Booking Confirmation', 'propertyhive' ) )) : esc_html(( $owner_or_landlord === 'Landlord' ? esc_html__( 'Re-Email Landlord Booking Confirmation', 'propertyhive' ) : esc_html__( 'Re-Email Owner Booking Confirmation', 'propertyhive' ) ) ) ) . '</a>';
3921
3922 $show_customise_confirmation_meta_boxes = true;
3923 }
3924 else
3925 {
3926 $actions[] = '<a
3927 href="#action_panel_appraisal_email_owner_booking_confirmation"
3928 class="button appraisal-action"
3929 style="width:100%; margin-bottom:7px; text-align:center"
3930 >' . ( ( $owner_booking_confirmation_sent_at == '' ) ? esc_html(( $owner_or_landlord === 'Landlord' ? esc_html__( 'Email Landlord Booking Confirmation', 'propertyhive' ) : esc_html__( 'Email Owner Booking Confirmation', 'propertyhive' ) )) : esc_html(( $owner_or_landlord === 'Landlord' ? esc_html__( 'Re-Email Landlord Booking Confirmation', 'propertyhive' ) : esc_html__( 'Re-Email Owner Booking Confirmation', 'propertyhive' ) ) )) . '</a>';
3931 }
3932
3933 $actions[] = '<div id="appraisal_owner_confirmation_date" style="text-align:center; font-size:12px; color:#999; margin-bottom:7px;' . ( ( $owner_booking_confirmation_sent_at == '' ) ? 'display:none' : '' ) . '">' . ( ( $owner_booking_confirmation_sent_at != '' ) ? 'Previously sent to ' . esc_html(strtolower($owner_or_landlord)) . ' on <span title="' . esc_attr($owner_booking_confirmation_sent_at) . '">' . esc_html(gmdate("jS F", strtotime($owner_booking_confirmation_sent_at))) . '</span>' : '' ) . '</div>';
3934
3935 $actions[] = '<hr>';
3936 }
3937
3938 /*$actions[] = '<a
3939 href=""
3940 class="button"
3941 style="width:100%; margin-bottom:7px; text-align:center"
3942 >' . __('Print Market Appraisal Sheet', 'propertyhive') . '</a>';
3943 if ( get_option('propertyhive_module_disabled_contacts', '') != 'yes' )
3944 {
3945 $actions[] = '<a
3946 href=""
3947 class="button"
3948 style="width:100%; margin-bottom:7px; text-align:center"
3949 >' . __('Run Potential Applicant Match', 'propertyhive') . '</a>';
3950 }*/
3951 $actions[] = '<a
3952 href="#action_panel_appraisal_carried_out"
3953 class="button button-success appraisal-action"
3954 style="width:100%; margin-bottom:7px; text-align:center"
3955 >' . esc_html(__('Appraisal Carried Out', 'propertyhive')) . '</a>';
3956 $actions[] = '<a
3957 href="#action_panel_appraisal_cancelled"
3958 class="button appraisal-action"
3959 style="width:100%; margin-bottom:7px; text-align:center"
3960 >' . esc_html(__('Appraisal Cancelled', 'propertyhive')) . '</a>';
3961
3962 $show_cancelled_meta_boxes = true;
3963 $show_carried_out_meta_boxes = true;
3964 }
3965
3966 if ( $status == 'carried_out' )
3967 {
3968 $actions[] = '<a
3969 href="#action_panel_appraisal_won"
3970 class="button button-success appraisal-action"
3971 style="width:100%; margin-bottom:7px; text-align:center"
3972 >' . esc_html(__('Appraisal Won', 'propertyhive')) . '</a>';
3973
3974 $actions[] = '<a
3975 href="#action_panel_appraisal_lost"
3976 class="button button-danger appraisal-action"
3977 style="width:100%; margin-bottom:7px; text-align:center"
3978 >' . esc_html(__('Appraisal Lost', 'propertyhive')) . '</a>';
3979
3980 $show_lost_meta_boxes = true;
3981 }
3982
3983 if ( $status == 'won' )
3984 {
3985 $actions[] = '<a
3986 href="#action_panel_appraisal_instruct"
3987 class="button button-success appraisal-action"
3988 style="width:100%; margin-bottom:7px; text-align:center"
3989 >' . esc_html(__('Instruct Property', 'propertyhive')) . '</a>';
3990
3991 $show_instructed_meta_boxes = true;
3992 }
3993
3994 if ( $status == 'won' || $status == 'lost' )
3995 {
3996 $actions[] = '<a
3997 href="#action_panel_appraisal_revert_carried_out"
3998 class="button appraisal-action"
3999 style="width:100%; margin-bottom:7px; text-align:center"
4000 >' . esc_html(__('Revert To Carried Out', 'propertyhive')) . '</a>';
4001 }
4002
4003 if ( $status == 'instructed' )
4004 {
4005 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
4006
4007 $actions[] = '<a
4008 href="' . esc_url(get_edit_post_link($property_id)) . '"
4009 class="button"
4010 style="width:100%; margin-bottom:7px; text-align:center"
4011 >' . esc_html(__('View Instructed Property', 'propertyhive')) . '</a>';
4012
4013 /*$actions[] = '<a
4014 href="#action_panel_appraisal_revert_won"
4015 class="button appraisal-action"
4016 style="width:100%; margin-bottom:7px; text-align:center"
4017 >' . __('Revert To Won', 'propertyhive') . '</a>';*/
4018 }
4019
4020 if ( $status == 'carried_out' || $status == 'cancelled' )
4021 {
4022 $actions[] = '<a
4023 href="#action_panel_appraisal_revert_pending"
4024 class="button appraisal-action"
4025 style="width:100%; margin-bottom:7px; text-align:center"
4026 >' . esc_html(__('Revert To Pending', 'propertyhive')) . '</a>';
4027 }
4028
4029 $actions = apply_filters( 'propertyhive_admin_appraisal_actions', $actions, $post_id );
4030 $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
4031
4032 if ( !empty($actions) )
4033 {
4034 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in action URLs and labels are escaped during assembly; preserve trusted PHP action filters and the fixed button handlers.
4035 echo implode("", $actions);
4036 }
4037 else
4038 {
4039 echo '<div style="text-align:center">' . esc_html(__( 'No actions to display', 'propertyhive' )) . '</div>';
4040 }
4041
4042 echo '</div>
4043
4044 </div>';
4045
4046 // Success action panel
4047 echo '<div id="action_panel_success" class="propertyhive_meta_box propertyhive_meta_box_actions" style="display:none;">
4048
4049 <div class="options_group" style="padding-top:8px;">
4050
4051 <div id="success_actions"></div>
4052
4053 <a class="button action-cancel" style="width:100%;" href="#">' . esc_html(__( 'Back To Actions', 'propertyhive' )) . '</a>
4054
4055 </div>
4056
4057 </div>';
4058
4059 do_action( 'propertyhive_admin_appraisal_action_options', $post_id );
4060 do_action( 'propertyhive_admin_post_action_options', $post_id );
4061
4062 if ( $show_customise_confirmation_meta_boxes )
4063 {
4064 $subject = get_option( 'propertyhive_appraisal_owner_booking_confirmation_email_subject', '' );
4065 $body = get_option( 'propertyhive_appraisal_owner_booking_confirmation_email_body', '' );
4066
4067 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_email_owner_booking_confirmation_customise" style="display:none;">
4068
4069 <div class="options_group" style="padding-top:8px;">
4070
4071 <div class="form-field">
4072
4073 <label for="_owner_confirmation_email_subject">' . esc_html(__( 'Subject', 'propertyhive' )) . '</label>
4074
4075 <input id="_owner_confirmation_email_subject" name="_owner_confirmation_email_subject" style="width:100%;" value="' . esc_attr($subject) . '">
4076
4077 </div>
4078
4079 <div class="form-field">
4080
4081 <label for="_owner_confirmation_email_body">' . esc_html(__( 'Body', 'propertyhive' )) . '</label>
4082
4083 <textarea id="_owner_confirmation_email_body" name="_owner_confirmation_email_body" style="width:100%; height:100px;">' . esc_html($body) . '</textarea>
4084
4085 </div>
4086
4087 <a class="button action-cancel" href="#">' . esc_html(__( 'Cancel', 'propertyhive' )) . '</a>
4088 <a class="button button-primary owner-booking-confirmation-action-submit" href="#">' . esc_html(__( 'Send', 'propertyhive' )) . '</a>
4089
4090 </div>
4091
4092 </div>';
4093 }
4094
4095 if ( $show_cancelled_meta_boxes )
4096 {
4097 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_cancelled" style="display:none;">
4098
4099 <div class="options_group" style="padding-top:8px;">
4100
4101 <div class="form-field">
4102
4103 <label for="_appraisal_cancelled_reason">' . esc_html(__( 'Reason Cancelled', 'propertyhive' )) . '</label>
4104
4105 <textarea id="_cancelled_reason" name="_cancelled_reason" style="width:100%;">' . esc_html(get_post_meta( $post_id, '_cancelled_reason', TRUE )) . '</textarea>
4106
4107 </div>
4108
4109 <a class="button action-cancel" href="#">' . esc_html(__( 'Cancel', 'propertyhive' )) . '</a>
4110 <a class="button button-primary cancelled-reason-action-submit" href="#">' . esc_html(__( 'Save', 'propertyhive' )) . '</a>
4111
4112 </div>
4113
4114 </div>';
4115 }
4116
4117 if ( $show_carried_out_meta_boxes )
4118 {
4119 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_carried_out" style="display:none;">
4120
4121 <div class="options_group" style="padding-top:8px;">';
4122
4123 $ph_countries = new PH_Countries();
4124
4125 $currency = 'GBP';
4126 $currency_symbol = '&pound;';
4127
4128 $default_country = get_option( 'propertyhive_default_country', 'GB' );
4129 $countries = get_option( 'propertyhive_countries', array( $default_country ) );
4130 if ( count($countries) == 1 )
4131 {
4132 foreach ( $countries as $country )
4133 {
4134 $country = $ph_countries->get_country( $country );
4135
4136 $currency = $country['currency_code'];
4137 }
4138 }
4139
4140 $currency = $ph_countries->get_currency( $currency );
4141 if ( isset($currency['currency_symbol']) )
4142 {
4143 $currency_symbol = $currency['currency_symbol'];
4144 }
4145
4146 if ( $department == 'residential-sales' )
4147 {
4148 echo '<div class="form-field">
4149
4150 <label for="_price">' . esc_html(/* translators: %s: Currency symbol. */ sprintf( __( 'Valued Price (%s)', 'propertyhive' ), $currency_symbol )) . '</label>
4151
4152 <input type="text" id="_price" name="_price" style="width:100%;" value="' . esc_attr(get_post_meta( $post_id, '_valued_price', TRUE )) . '">
4153
4154 </div>';
4155 }
4156 else
4157 {
4158 $rent_frequency = get_post_meta( $post_id, '_valued_rent_frequency', TRUE );
4159 echo '<div class="form-field">
4160
4161 <label for="_price">' . esc_html(/* translators: %s: Currency symbol. */ sprintf( __( 'Valued Rent (%s)', 'propertyhive' ), $currency_symbol )) . '</label>
4162
4163 <input type="text" id="_price" name="_price" style="width:100%;" value="' . esc_attr(get_post_meta( $post_id, '_valued_rent', TRUE )) . '">
4164
4165 <select id="_rent_frequency" name="_rent_frequency" class="select" style="width:100%">
4166 <option value="pd"' . ( ($rent_frequency == 'pd') ? ' selected' : '') . '>' . esc_html(__('Per Day', 'propertyhive')) . '</option>
4167 <option value="pppw"' . ( ($rent_frequency == 'pppw') ? ' selected' : '') . '>' . esc_html(__('Per Person Per Week', 'propertyhive')) . '</option>
4168 <option value="pw"' . ( ($rent_frequency == 'pw') ? ' selected' : '') . '>' . esc_html(__('Per Week', 'propertyhive')) . '</option>
4169 <option value="pcm"' . ( ($rent_frequency == 'pcm' || $rent_frequency == '') ? ' selected' : '') . '>' . esc_html(__('Per Calendar Month', 'propertyhive')) . '</option>
4170 <option value="pq"' . ( ($rent_frequency == 'pq') ? ' selected' : '') . '>' . esc_html(__('Per Quarter', 'propertyhive')) . '</option>
4171 <option value="pa"' . ( ($rent_frequency == 'pa') ? ' selected' : '') . '>' . esc_html(__('Per Annum', 'propertyhive')) . '</option>
4172 </select>
4173
4174 </div>';
4175 }
4176
4177 echo '<a class="button action-cancel" href="#">' . esc_html(__( 'Cancel', 'propertyhive' )) . '</a>
4178 <a class="button button-primary carried-out-action-submit" href="#">' . esc_html(__( 'Save', 'propertyhive' )) . '</a>
4179
4180 </div>
4181
4182 </div>';
4183 }
4184
4185 if ( $show_instructed_meta_boxes )
4186 {
4187 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_instruct" style="display:none;">
4188
4189 <div class="options_group" style="padding-top:8px;">';
4190
4191 echo '<div style="margin-bottom:13px;">' . esc_html(__( 'Upon instruction a new property record will be created within the \'Properties\' area.', 'propertyhive' )) . '</div>';
4192
4193 echo '<a class="button action-cancel" href="#">' . esc_html(__( 'Cancel', 'propertyhive' )) . '</a>
4194 <a class="button button-primary instructed-action-submit" href="#">' . esc_html(__( 'OK', 'propertyhive' )) . '</a>
4195
4196 </div>
4197
4198 </div>';
4199 }
4200
4201 if ( $show_lost_meta_boxes )
4202 {
4203 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_lost" style="display:none;">
4204
4205 <div class="options_group" style="padding-top:8px;">
4206
4207 <div class="form-field">
4208
4209 <label for="_lost_reason">' . esc_html(__( 'Reason Lost', 'propertyhive' )) . '</label>
4210
4211 <textarea id="_lost_reason" name="_lost_reason" style="width:100%;">' . esc_html(get_post_meta( $post_id, '_lost_reason', TRUE )) . '</textarea>
4212
4213 </div>
4214
4215 <a class="button action-cancel" href="#">' . esc_html(__( 'Cancel', 'propertyhive' )) . '</a>
4216 <a class="button button-primary lost-reason-action-submit" href="#">' . esc_html( __( 'Save Reason Lost', 'propertyhive' ) ) . '</a>
4217
4218 </div>
4219
4220 </div>';
4221 }
4222
4223 die();
4224 }
4225
4226 public function appraisal_carried_out()
4227 {
4228 check_ajax_referer( 'appraisal-actions', 'security' );
4229
4230 $post_id = isset( $_POST['appraisal_id'] ) && is_scalar( $_POST['appraisal_id'] ) ? absint( $_POST['appraisal_id'] ) : 0;
4231 if ( $post_id < 1 || ! current_user_can( 'manage_propertyhive' ) || 'appraisal' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
4232 wp_send_json_error( __( 'Invalid appraisal or insufficient permissions.', 'propertyhive' ), 403 );
4233 }
4234
4235 $status = get_post_meta( $post_id, '_status', TRUE );
4236
4237 if ( $status == 'pending' )
4238 {
4239 $department = get_post_meta( $post_id, '_department', true );
4240 $valuation_input = array();
4241 $fields = 'residential-sales' === $department ? array( 'price' ) : ( 'residential-lettings' === $department ? array( 'rent', 'rent_frequency' ) : array() );
4242 foreach ( $fields as $field ) {
4243 if ( ! isset( $_POST[$field] ) || ! is_string( $_POST[$field] ) ) {
4244 wp_send_json_error( __( 'Invalid valuation details.', 'propertyhive' ), 400 );
4245 }
4246 $valuation_input[$field] = sanitize_text_field( wp_unslash( $_POST[$field] ) );
4247 }
4248 if ( 'residential-lettings' === $department && ! in_array( $valuation_input['rent_frequency'], array( 'pd', 'pppw', 'pw', 'pcm', 'pq', 'pa' ), true ) ) {
4249 wp_send_json_error( __( 'Invalid rent frequency.', 'propertyhive' ), 400 );
4250 }
4251 if ( 'residential-lettings' === $department ) {
4252 $rent_number = preg_replace( '/[^0-9.]/', '', $valuation_input['rent'] );
4253 if ( '' !== $rent_number && ! is_numeric( $rent_number ) ) {
4254 wp_send_json_error( __( 'Invalid rent amount.', 'propertyhive' ), 400 );
4255 }
4256 $valuation_input['rent'] = '' === $rent_number ? '0' : $rent_number;
4257 }
4258 update_post_meta( $post_id, '_status', 'carried_out' );
4259
4260 if ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-sales' )
4261 {
4262 $price = preg_replace("/[^0-9.]/", '', $valuation_input['price']);
4263 update_post_meta( $post_id, '_valued_price', $price );
4264 update_post_meta( $post_id, '_valued_price_actual', $price );
4265 }
4266 elseif ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-lettings' )
4267 {
4268 $rent = preg_replace("/[^0-9.]/", '', $valuation_input['rent']);
4269 update_post_meta( $post_id, '_valued_rent', $rent );
4270
4271 update_post_meta( $post_id, '_valued_rent_frequency', $valuation_input['rent_frequency'] );
4272
4273 switch ($valuation_input['rent_frequency'])
4274 {
4275 case "pd": { $price = ($rent * 365) / 12; break; }
4276 case "pppw":
4277 {
4278 $bedrooms = get_post_meta( $post_id, '_bedrooms', true );
4279 if ( ( $bedrooms !== FALSE && $bedrooms != 0 && $bedrooms != '' ) && apply_filters( 'propertyhive_pppw_to_consider_bedrooms', true ) == true )
4280 {
4281 $price = (($rent * 52) / 12) * $bedrooms;
4282 }
4283 else
4284 {
4285 $price = ($rent * 52) / 12;
4286 }
4287 break;
4288 }
4289 case "pw": { $price = ($rent * 52) / 12; break; }
4290 case "pcm": { $price = $rent; break; }
4291 case "pq": { $price = ($rent * 4) / 12; break; }
4292 case "pa": { $price = ($rent / 12); break; }
4293 }
4294 update_post_meta( $post_id, '_valued_price_actual', $price );
4295 }
4296
4297 // Add note/comment to appraisal
4298 $comment = array(
4299 'note_type' => 'action',
4300 'action' => 'appraisal_carried_out',
4301 );
4302
4303 PH_Comments::insert_note( $post_id, $comment );
4304
4305 wp_send_json_success();
4306 }
4307
4308 wp_send_json_success();
4309 }
4310
4311 public function appraisal_cancelled()
4312 {
4313 check_ajax_referer( 'appraisal-actions', 'security' );
4314
4315 $post_id = isset( $_POST['appraisal_id'] ) && is_scalar( $_POST['appraisal_id'] ) ? absint( $_POST['appraisal_id'] ) : 0;
4316 if ( $post_id < 1 || ! current_user_can( 'manage_propertyhive' ) || 'appraisal' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
4317 wp_send_json_error( __( 'Invalid appraisal or insufficient permissions.', 'propertyhive' ), 403 );
4318 }
4319
4320 if ( ! isset( $_POST['cancelled_reason'] ) || ! is_string( $_POST['cancelled_reason'] ) ) {
4321 wp_send_json_error( __( 'Invalid appraisal reason.', 'propertyhive' ), 400 );
4322 }
4323 $reason = sanitize_textarea_field( wp_unslash( $_POST['cancelled_reason'] ) );
4324
4325 $status = get_post_meta( $post_id, '_status', TRUE );
4326
4327 if ( $status == 'pending' )
4328 {
4329 update_post_meta( $post_id, '_status', 'cancelled' );
4330 update_post_meta( $post_id, '_cancelled_reason', wp_slash( $reason ) );
4331
4332 // Add note/comment to appraisal
4333 $comment = array(
4334 'note_type' => 'action',
4335 'action' => 'appraisal_cancelled',
4336 );
4337
4338 PH_Comments::insert_note( $post_id, $comment );
4339
4340 wp_send_json_success();
4341 }
4342
4343 wp_send_json_error();
4344 }
4345
4346 public function appraisal_won()
4347 {
4348 check_ajax_referer( 'appraisal-actions', 'security' );
4349
4350 $post_id = isset( $_POST['appraisal_id'] ) && is_scalar( $_POST['appraisal_id'] ) ? absint( $_POST['appraisal_id'] ) : 0;
4351 if ( $post_id < 1 || ! current_user_can( 'manage_propertyhive' ) || 'appraisal' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
4352 wp_send_json_error( __( 'Invalid appraisal or insufficient permissions.', 'propertyhive' ), 403 );
4353 }
4354
4355 $status = get_post_meta( $post_id, '_status', TRUE );
4356
4357 if ( $status == 'carried_out' )
4358 {
4359 update_post_meta( $post_id, '_status', 'won' );
4360
4361 // Add note/comment to appraisal
4362 $comment = array(
4363 'note_type' => 'action',
4364 'action' => 'appraisal_won',
4365 );
4366
4367 PH_Comments::insert_note( $post_id, $comment );
4368
4369 wp_send_json_success();
4370 }
4371
4372 wp_send_json_error();
4373 }
4374
4375 public function appraisal_lost_reason()
4376 {
4377 check_ajax_referer( 'appraisal-actions', 'security' );
4378
4379 $post_id = isset( $_POST['appraisal_id'] ) && is_scalar( $_POST['appraisal_id'] ) ? absint( $_POST['appraisal_id'] ) : 0;
4380 if ( $post_id < 1 || ! current_user_can( 'manage_propertyhive' ) || 'appraisal' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
4381 wp_send_json_error( __( 'Invalid appraisal or insufficient permissions.', 'propertyhive' ), 403 );
4382 }
4383
4384 if ( ! isset( $_POST['lost_reason'] ) || ! is_string( $_POST['lost_reason'] ) ) {
4385 wp_send_json_error( __( 'Invalid appraisal reason.', 'propertyhive' ), 400 );
4386 }
4387 $reason = sanitize_textarea_field( wp_unslash( $_POST['lost_reason'] ) );
4388
4389 $status = get_post_meta( $post_id, '_status', TRUE );
4390
4391 if ( $status == 'carried_out' )
4392 {
4393 update_post_meta( $post_id, '_status', 'lost' );
4394 update_post_meta( $post_id, '_lost_reason', wp_slash( $reason ) );
4395
4396 // Add note/comment to appraisal
4397 $comment = array(
4398 'note_type' => 'action',
4399 'action' => 'appraisal_lost',
4400 );
4401
4402 PH_Comments::insert_note( $post_id, $comment );
4403
4404 wp_send_json_success();
4405 }
4406
4407 wp_send_json_error();
4408 }
4409
4410 public function appraisal_instructed()
4411 {
4412 check_ajax_referer( 'appraisal-actions', 'security' );
4413
4414 $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4415
4416 $status = get_post_meta( $post_id, '_status', TRUE );
4417
4418 if ( $status == 'won' )
4419 {
4420 // Create property record and copy everything over
4421 $display_address = array();
4422 if ( get_post_meta( $post_id, '_address_street', TRUE ) != '' )
4423 {
4424 $display_address[] = get_post_meta( $post_id, '_address_street', TRUE );
4425 }
4426 if ( get_post_meta( $post_id, '_address_two', TRUE ) != '' )
4427 {
4428 $display_address[] = get_post_meta( $post_id, '_address_two', TRUE );
4429 }
4430 if ( get_post_meta( $post_id, '_address_three', TRUE ) != '' )
4431 {
4432 $display_address[] = get_post_meta( $post_id, '_address_three', TRUE );
4433 }
4434 else
4435 {
4436 if ( get_post_meta( $post_id, '_address_four', TRUE ) != '' )
4437 {
4438 $display_address[] = get_post_meta( $post_id, '_address_four', TRUE );
4439 }
4440 }
4441 $display_address = implode(", ", $display_address);
4442
4443 $property_post = array(
4444 'post_title' => ph_clean($display_address),
4445 'post_content' => '',
4446 'post_type' => 'property',
4447 'post_status' => 'publish',
4448 'comment_status' => 'closed',
4449 'ping_status' => 'closed',
4450 );
4451
4452 // Insert the post into the database
4453 $property_post_id = wp_insert_post( $property_post );
4454
4455 if ( is_wp_error($property_post_id) || $property_post_id == 0 )
4456 {
4457 // Failed. Don't really know at the moment how to handle this
4458
4459 $return = array('error' => 'Failed to create property post. Please try again');
4460 echo json_encode( $return );
4461 die();
4462 }
4463 else
4464 {
4465 // Successfully added property post
4466
4467 $department = get_post_meta( $post_id, '_department', TRUE );
4468
4469 $reference_number = '';
4470 if ( get_option( 'propertyhive_auto_incremental_reference_numbers' ) == 'yes' )
4471 {
4472 $next = get_option( 'propertyhive_auto_incremental_next', '' );
4473 if ( $next == '' || (int)$next == 0 )
4474 {
4475 $next = 1;
4476 }
4477 $reference_number = $next;
4478
4479 $next_auto_increment = $next + 1;
4480
4481 update_option( 'propertyhive_auto_incremental_next', $next_auto_increment );
4482 }
4483 update_post_meta( $property_post_id, '_reference_number', $reference_number );
4484
4485 update_post_meta( $property_post_id, '_address_name_number', get_post_meta( $post_id, '_address_name_number', TRUE ) );
4486 update_post_meta( $property_post_id, '_address_street', get_post_meta( $post_id, '_address_street', TRUE ) );
4487 update_post_meta( $property_post_id, '_address_two', get_post_meta( $post_id, '_address_two', TRUE ) );
4488 update_post_meta( $property_post_id, '_address_three', get_post_meta( $post_id, '_address_three', TRUE ) );
4489 update_post_meta( $property_post_id, '_address_four', get_post_meta( $post_id, '_address_four', TRUE ) );
4490 update_post_meta( $property_post_id, '_address_postcode', get_post_meta( $post_id, '_address_postcode', TRUE ) );
4491 update_post_meta( $property_post_id, '_address_country', get_post_meta( $post_id, '_address_country', TRUE ) );
4492
4493 if ( ini_get('allow_url_fopen') )
4494 {
4495 // No lat lng. Let's get it
4496 $address_to_geocode = array();
4497 if ( get_post_meta( $post_id, '_address_name_number', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_name_number', TRUE ); }
4498 if ( get_post_meta( $post_id, '_address_street', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_street', TRUE ); }
4499 if ( get_post_meta( $post_id, '_address_two', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_two', TRUE ); }
4500 if ( get_post_meta( $post_id, '_address_three', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_three', TRUE ); }
4501 if ( get_post_meta( $post_id, '_address_four', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_four', TRUE ); }
4502 if ( get_post_meta( $post_id, '_address_postcode', TRUE ) ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_postcode', TRUE ); }
4503
4504 $country = get_option( 'propertyhive_default_country', 'GB' );
4505
4506 if ( get_option('propertyhive_geocoding_provider') == 'osm' )
4507 {
4508 $request_url = "https://nominatim.openstreetmap.org/search?format=json&limit=1&countrycodes=" . strtolower($country) . "&addressdetails=1&q=" . urlencode(implode( ", ", $address_to_geocode ));
4509 $response = wp_remote_get(
4510 $request_url,
4511 array(
4512 'headers' => array(
4513 'Referer' => home_url(),
4514 'User-Agent' => 'Property-Hive/' . PH_VERSION . ' (+https://wp-property-hive.com)',
4515 ),
4516 )
4517 );
4518 if ( is_array( $response ) )
4519 {
4520 $body = wp_remote_retrieve_body( $response );
4521 $json = json_decode($body, true);
4522
4523 if ( !empty($json) && isset($json[0]['lat']) && isset($json[0]['lon']) )
4524 {
4525 $lat = $json[0]['lat'];
4526 $lng = $json[0]['lon'];
4527
4528 if ($lat != '' && $lng != '')
4529 {
4530 update_post_meta( $property_post_id, '_latitude', $lat );
4531 update_post_meta( $property_post_id, '_longitude', $lng );
4532 }
4533 }
4534 }
4535 }
4536 else
4537 {
4538 $request_url = "https://maps.googleapis.com/maps/api/geocode/xml?address=" . urlencode( implode( ", ", $address_to_geocode ) ) . "&sensor=false&region=" . strtolower($country); // the request URL you'll send to google to get back your XML feed
4539
4540 $api_key = get_option('propertyhive_google_maps_api_key', '');
4541 if ( $api_key != '' ) { $request_url .= "&key=" . $api_key; }
4542
4543 $response = wp_remote_get($request_url);
4544
4545 if ( is_array( $response ) && !is_wp_error( $response ) )
4546 {
4547 $header = $response['headers']; // array of http header lines
4548 $body = $response['body']; // use the content
4549
4550 $xml = simplexml_load_string($body);
4551
4552 if ( $xml !== FALSE )
4553 {
4554 $status = $xml->status; // Get the request status as google's api can return several responses
4555
4556 if ($status == "OK")
4557 {
4558 //request returned completed time to get lat / lng for storage
4559 $lat = (string)$xml->result->geometry->location->lat;
4560 $lng = (string)$xml->result->geometry->location->lng;
4561
4562 if ($lat != '' && $lng != '')
4563 {
4564 update_post_meta( $property_post_id, '_latitude', $lat );
4565 update_post_meta( $property_post_id, '_longitude', $lng );
4566 }
4567 }
4568 }
4569 }
4570 }
4571 }
4572
4573 update_post_meta( $property_post_id, '_department', $department );
4574
4575 switch ( $department )
4576 {
4577 case "residential-sales":
4578 {
4579 update_post_meta( $property_post_id, '_currency', 'GBP' );
4580
4581 $price = preg_replace("/[^0-9.]/", '', get_post_meta( $post_id, '_valued_price', TRUE ));
4582 update_post_meta( $property_post_id, '_price', $price );
4583
4584 break;
4585 }
4586 case "residential-lettings":
4587 {
4588 update_post_meta( $property_post_id, '_currency', 'GBP' );
4589
4590 $rent = preg_replace("/[^0-9.]/", '', get_post_meta( $post_id, '_valued_rent', TRUE ));
4591 update_post_meta( $property_post_id, '_rent', $rent );
4592 update_post_meta( $property_post_id, '_rent_frequency', get_post_meta( $post_id, '_valued_rent_frequency', TRUE ) );
4593
4594 break;
4595 }
4596 }
4597
4598 // Store price in common currency (GBP) used for ordering
4599 $ph_countries = new PH_Countries();
4600 $ph_countries->update_property_price_actual( $property_post_id );
4601
4602 update_post_meta( $property_post_id, '_bedrooms', get_post_meta( $post_id, '_bedrooms', TRUE ) );
4603 update_post_meta( $property_post_id, '_bathrooms', get_post_meta( $post_id, '_bathrooms', TRUE ) );
4604 update_post_meta( $property_post_id, '_reception_rooms', get_post_meta( $post_id, '_reception_rooms', TRUE ) );
4605
4606 update_post_meta( $property_post_id, '_on_market', '' );
4607 update_post_meta( $property_post_id, '_featured', '' );
4608
4609 // Taxonomies
4610 wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'property_type', array("fields" => "ids") ), 'property_type' );
4611 wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'parking', array("fields" => "ids") ), 'parking' );
4612 wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'outside_space', array("fields" => "ids") ), 'outside_space' );
4613
4614 update_post_meta( $property_post_id, '_council_tax_band', get_post_meta( $post_id, '_council_tax_band', TRUE ) );
4615
4616 $owner_contact_ids = get_post_meta( $post_id, '_property_owner_contact_id', TRUE );
4617 if ( !is_array($owner_contact_ids) )
4618 {
4619 $owner_contact_ids = array($owner_contact_ids);
4620 }
4621 update_post_meta( $property_post_id, '_owner_contact_id', $owner_contact_ids );
4622
4623 // Make updates to appraisal
4624 update_post_meta( $post_id, '_status', 'instructed' );
4625 update_post_meta( $post_id, '_property_id', $property_post_id );
4626
4627 //Update owner(s)
4628 foreach ( $owner_contact_ids as $owner_contact_id )
4629 {
4630 $contact_types = get_post_meta( $owner_contact_id, '_contact_types', TRUE );
4631
4632 if ( !in_array('owner', $contact_types) )
4633 {
4634 $contact_types[] = 'owner';
4635 }
4636
4637 // get appraisals where this is the owner and where not instructed
4638 $args = array(
4639 'post_type' => 'appraisal',
4640 'nopaging' => true,
4641 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Instruction must link every non-instructed appraisal for this owner; those relationships/statuses use the existing metadata schema.
4642 'meta_query' => array(
4643 array(
4644 'key' => '_property_owner_contact_id',
4645 'value' => $owner_contact_id,
4646 'compare' => '='
4647 ),
4648 array(
4649 'key' => '_status',
4650 'value' => 'instructed',
4651 'compare' => '!='
4652 )
4653 )
4654 );
4655
4656 $appraisal_query = new WP_Query($args);
4657
4658 if (!$appraisal_query->have_posts())
4659 {
4660 // no longer a potential owner.
4661 if (($key = array_search('potentialowner', $contact_types)) !== false)
4662 {
4663 unset($contact_types[$key]);
4664 }
4665 }
4666 wp_reset_postdata();
4667
4668 update_post_meta( $owner_contact_id, '_contact_types', $contact_types );
4669 }
4670
4671 // Add note/comment to appraisal
4672 $comment = array(
4673 'note_type' => 'action',
4674 'action' => 'appraisal_instructed',
4675 );
4676
4677 PH_Comments::insert_note( $post_id, $comment );
4678
4679 wp_send_json_success();
4680 }
4681 }
4682
4683 wp_send_json_error();
4684 }
4685
4686 public function appraisal_email_owner_booking_confirmation()
4687 {
4688 check_ajax_referer( 'appraisal-actions', 'security' );
4689
4690 $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4691
4692 $appraisal = new PH_Appraisal($post_id);
4693
4694 $owner_contact_id = $appraisal->property_owner_contact_id;
4695
4696 if ( !is_array($owner_contact_id) ) { $owner_contact_id = array($owner_contact_id); }
4697
4698 if ( !empty($owner_contact_id) )
4699 {
4700 $owner_emails = array();
4701 $owner_names = array();
4702 $owner_dears = array();
4703
4704 foreach ($owner_contact_id as $owner_id)
4705 {
4706 $owner_contact = new PH_Contact($owner_id);
4707
4708 $owner_email = sanitize_email( $owner_contact->email_address );
4709 $owner_name = $owner_contact->post_title;
4710 $owner_dear = $owner_contact->dear();
4711
4712 if( ! empty($owner_email) ) array_push($owner_emails, $owner_email);
4713 if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
4714 if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
4715 }
4716
4717 $owner_names_string = $this->get_list_string($owner_names);
4718 $owner_dears_string = $this->get_list_string($owner_dears);
4719
4720 $negotiator_names = array();
4721 $negotiator_names_string = '';
4722
4723 $negotiator_email_addresses = array();
4724 $negotiator_email_addresses_string = '';
4725
4726 $negotiator_telephone_numbers = array();
4727 $negotiator_telephone_numbers_string = '';
4728
4729 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
4730 if ( !empty($negotiator_ids) )
4731 {
4732 foreach ( $negotiator_ids as $negotiator_id )
4733 {
4734 $negotiator = get_user_by( 'id', $negotiator_id );
4735 if ( $negotiator !== false )
4736 {
4737 if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
4738 {
4739 $negotiator_names[] = $negotiator->display_name;
4740 }
4741
4742 if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
4743 {
4744 $negotiator_email_addresses[] = $negotiator->user_email;
4745 }
4746
4747 $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
4748 if ( !empty($telephone_number) )
4749 {
4750 $negotiator_telephone_numbers[] = $telephone_number;
4751 }
4752 }
4753 }
4754 }
4755 if ( !empty($negotiator_names) )
4756 {
4757 $last = array_slice($negotiator_names, -1);
4758 $first = join(', ', array_slice($negotiator_names, 0, -1));
4759 $both = array_filter(array_merge(array($first), $last), 'strlen');
4760 $negotiator_names_string = join(' and ', $both);
4761 }
4762 if ( !empty($negotiator_email_addresses) )
4763 {
4764 $last = array_slice($negotiator_email_addresses, -1);
4765 $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
4766 $both = array_filter(array_merge(array($first), $last), 'strlen');
4767 $negotiator_email_addresses_string = join(' and ', $both);
4768 }
4769 if ( !empty($negotiator_telephone_numbers) )
4770 {
4771 $last = array_slice($negotiator_telephone_numbers, -1);
4772 $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
4773 $both = array_filter(array_merge(array($first), $last), 'strlen');
4774 $negotiator_telephone_numbers_string = join(' and ', $both);
4775 }
4776
4777 $to = implode(",", $owner_emails);
4778
4779 $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_appraisal_owner_booking_confirmation_email_subject', '' );
4780 $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_appraisal_owner_booking_confirmation_email_body', '' );
4781
4782 $appraisal_date_timestamp = strtotime($appraisal->start_date_time);
4783
4784 $subject = str_replace('[property_address]', $appraisal->get_formatted_full_address(), $subject);
4785 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
4786 $subject = str_replace('[appraisal_time]', gmdate("H:i", $appraisal_date_timestamp), $subject);
4787 $subject = str_replace('[appraisal_date]', gmdate("l jS F Y", $appraisal_date_timestamp), $subject);
4788 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
4789 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
4790 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
4791
4792 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook appraisal_owner_booking_confirmation_email_subject; third-party email integrations depend on the established name.
4793 $subject = apply_filters( 'appraisal_owner_booking_confirmation_email_subject', $subject, $post_id );
4794
4795 $body = str_replace('[property_address]', $appraisal->get_formatted_full_address(), $body);
4796 $body = str_replace('[owner_name]', $owner_names_string, $body);
4797 $body = str_replace('[owner_dear]', $owner_dears_string, $body);
4798 $body = str_replace('[appraisal_time]', gmdate("H:i", $appraisal_date_timestamp), $body);
4799 $body = str_replace('[appraisal_date]', gmdate("l jS F Y", $appraisal_date_timestamp), $body);
4800 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
4801 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
4802 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
4803
4804 $body = html_entity_decode($body);
4805
4806 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook appraisal_owner_booking_confirmation_email_body; third-party email integrations depend on the established name.
4807 $body = apply_filters( 'appraisal_owner_booking_confirmation_email_body', $body, $post_id );
4808
4809 $from = '';
4810 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
4811 if ( $from_setting == 'user' )
4812 {
4813 $current_user = wp_get_current_user();
4814 $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
4815 }
4816 if ( $from == '' )
4817 {
4818 $from = get_option('propertyhive_email_from_address', '');
4819 }
4820 if ( $from == '' )
4821 {
4822 $from = get_bloginfo('admin_email');
4823 }
4824
4825 $headers = array();
4826 $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
4827 $headers[] = 'Reply-To: ' . sanitize_email($from);
4828 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
4829
4830 $headers = apply_filters( 'propertyhive_appraisal_owner_booking_confirmation_email_headers', $headers );
4831
4832 $sent = wp_mail($to, $subject, $body, $headers);
4833
4834 if ( !$sent )
4835 {
4836 wp_send_json_error('Failed to send email');
4837 }
4838
4839 if ( apply_filters( 'propertyhive_log_booking_confirmation_emails', false ) === true )
4840 {
4841 // Add note/comment to appraisal
4842 $comment = array(
4843 'note_type' => 'action',
4844 'action' => 'appraisal_owner_booking_confirmation_email',
4845 );
4846
4847 PH_Comments::insert_note( $post_id, $comment );
4848 }
4849
4850 update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
4851
4852 wp_send_json_success();
4853 }
4854 else
4855 {
4856 wp_send_json_error('No owner recipients found');
4857 }
4858
4859 wp_die();
4860 }
4861
4862 public function appraisal_revert_pending()
4863 {
4864 check_ajax_referer( 'appraisal-actions', 'security' );
4865
4866 $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4867
4868 $status = get_post_meta( $post_id, '_status', TRUE );
4869
4870 if ( $status == 'carried_out' || $status == 'cancelled' )
4871 {
4872 update_post_meta( $post_id, '_status', 'pending' );
4873
4874 // Add note/comment to appraisal
4875 $comment = array(
4876 'note_type' => 'action',
4877 'action' => 'appraisal_revert_pending',
4878 );
4879
4880 PH_Comments::insert_note( $post_id, $comment );
4881
4882 wp_send_json_success();
4883 }
4884
4885 wp_send_json_error();
4886 }
4887
4888 public function appraisal_revert_carried_out()
4889 {
4890 check_ajax_referer( 'appraisal-actions', 'security' );
4891
4892 $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4893
4894 $status = get_post_meta( $post_id, '_status', TRUE );
4895
4896 if ( $status == 'won' || $status == 'lost' )
4897 {
4898 update_post_meta( $post_id, '_status', 'carried_out' );
4899
4900 // Add note/comment to appraisal
4901 $comment = array(
4902 'note_type' => 'action',
4903 'action' => 'appraisal_revert_carried_out',
4904 );
4905
4906 PH_Comments::insert_note( $post_id, $comment );
4907
4908 wp_send_json_success();
4909 }
4910
4911 wp_send_json_error();
4912 }
4913
4914 public function appraisal_revert_won()
4915 {
4916 check_ajax_referer( 'appraisal-actions', 'security' );
4917
4918 $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4919
4920 $status = get_post_meta( $post_id, '_status', TRUE );
4921
4922 if ( $status == 'instructed' )
4923 {
4924 update_post_meta( $post_id, '_status', 'won' );
4925
4926 // Add note/comment to appraisal
4927 $comment = array(
4928 'note_type' => 'action',
4929 'action' => 'appraisal_revert_won',
4930 );
4931
4932 PH_Comments::insert_note( $post_id, $comment );
4933
4934 wp_send_json_success();
4935 }
4936
4937 wp_send_json_error();
4938 }
4939
4940 // Viewing related functions
4941 public function book_viewing_property()
4942 {
4943 check_ajax_referer( 'book-viewing', 'security' );
4944
4945 $this->json_headers();
4946
4947 $booking = $this->get_viewing_booking_input();
4948 $property_id = $this->get_authorized_record_id( 'property_id', 'property' );
4949 if ($property_id < 1)
4950 {
4951 $return = array('error' => 'No property selected');
4952 echo json_encode( $return );
4953 die();
4954 }
4955
4956 $property = new PH_Property( $property_id );
4957
4958 foreach ( $booking['applicant_ids'] as $applicant_id ) {
4959 if ( 'contact' !== get_post_type( $applicant_id ) || ! current_user_can( 'edit_post', $applicant_id ) ) {
4960 wp_send_json_error( __( 'Invalid applicant or insufficient permissions.', 'propertyhive' ), 403 );
4961 }
4962 }
4963 if ( empty( $booking['applicant_ids'] ) && '' !== $booking['applicant_name'] && ! current_user_can( get_post_type_object( 'contact' )->cap->create_posts ) ) {
4964 wp_send_json_error( __( 'Insufficient permissions to create contacts.', 'propertyhive' ), 403 );
4965 }
4966 $applicant_contact_ids = array();
4967
4968 // Create applicant record if required
4969 if (empty($booking['applicant_ids']) && !empty($booking['applicant_name']))
4970 {
4971 // Need to create contact/applicant
4972 $contact_post = array(
4973 'post_title' => $booking['applicant_name'],
4974 'post_content' => '',
4975 'post_type' => 'contact',
4976 'post_status' => 'publish',
4977 'comment_status' => 'closed',
4978 'ping_status' => 'closed',
4979 );
4980
4981 // Insert the post into the database
4982 $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
4983
4984 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
4985 {
4986 $return = array('error' => 'Failed to create contact post. Please try again');
4987 echo json_encode( $return );
4988 die();
4989 }
4990
4991 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
4992
4993 $email_address = sanitize_email( $booking['applicant_email_address'] );
4994 $telephone_number = $booking['applicant_telephone_number'];
4995 update_post_meta( $contact_post_id, '_email_address', $email_address );
4996 update_post_meta( $contact_post_id, '_telephone_number', wp_slash( $telephone_number ) );
4997 update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean( ph_clean_telephone_number($telephone_number) ) );
4998
4999 if ( '' !== $booking['applicant_address'] )
5000 {
5001 $address = ph_split_address_into_fields( $booking['applicant_address'] );
5002
5003 update_post_meta( $contact_post_id, '_address_name_number', wp_slash( $address['address_name_number'] ) );
5004 update_post_meta( $contact_post_id, '_address_street', wp_slash( $address['address_street'] ) );
5005 update_post_meta( $contact_post_id, '_address_two', wp_slash( $address['address_two'] ) );
5006 update_post_meta( $contact_post_id, '_address_three', wp_slash( $address['address_three'] ) );
5007 update_post_meta( $contact_post_id, '_address_four', wp_slash( $address['address_four'] ) );
5008 update_post_meta( $contact_post_id, '_address_postcode', wp_slash( $address['address_postcode'] ) );
5009 update_post_meta( $contact_post_id, '_address_country', get_option( 'propertyhive_default_country', 'GB' ) );
5010 }
5011
5012 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
5013 update_post_meta( $contact_post_id, '_applicant_profile_0', array( 'department' => $property->department, 'send_matching_properties' => '' ) );
5014
5015 $applicant_contact_ids[] = $contact_post_id;
5016 }
5017
5018 if (!empty($booking['applicant_ids']) && empty($booking['applicant_name']))
5019 {
5020 // This is an existing contact
5021 $applicant_contact_ids = $booking['applicant_ids'];
5022 }
5023
5024 $applicant_contact_ids = array_unique($applicant_contact_ids);
5025
5026 if ( empty($applicant_contact_ids) )
5027 {
5028 $return = array('error' => 'No applicant selected, or unable to create applicant record');
5029 echo json_encode( $return );
5030 die();
5031 }
5032
5033 // Make sure each of the contacts has an applicant profile of the correct department
5034 /*foreach ( $applicant_contact_ids as $applicant_contact_id )
5035 {
5036 $has_correct_profile = false;
5037
5038 // Get all existing profiles
5039 $existing_contact_types = get_post_meta( $applicant_contact_id, '_contact_types', TRUE );
5040 if ( $existing_contact_types == '' || !is_array($existing_contact_types) )
5041 {
5042 $existing_contact_types = array();
5043 }
5044 if ( in_array( 'applicant', $existing_contact_types ) )
5045 {
5046 $num_applicant_profiles = get_post_meta( $applicant_contact_id, '_applicant_profiles', TRUE );
5047 if ( $num_applicant_profiles == '' )
5048 {
5049 $num_applicant_profiles = 0;
5050 }
5051
5052 if ( $num_applicant_profiles > 0 )
5053 {
5054 for ( $i = 0; $i < $num_applicant_profiles; ++$i )
5055 {
5056 $applicant_profile = get_post_meta( $applicant_contact_id, '_applicant_profile_' . $i, TRUE );
5057 if ( $applicant_profile['department'] == $property->department )
5058 {
5059 $has_correct_profile = true;
5060 }
5061 }
5062 }
5063 }
5064
5065 if ( !$has_correct_profile )
5066 {
5067 if ( in_array( 'applicant', $existing_contact_types ) )
5068 {
5069 // Already an applicant. Just need to add profile
5070 }
5071 else
5072 {
5073 $existing_contact_types[] = 'applicant';
5074 update_post_meta( $applicant_contact_id, '_contact_types', $existing_contact_types );
5075 }
5076
5077 $num_applicant_profiles = get_post_meta( $applicant_contact_id, '_applicant_profiles', TRUE );
5078 if ( $num_applicant_profiles == '' )
5079 {
5080 $num_applicant_profiles = 0;
5081 }
5082
5083 update_post_meta( $applicant_contact_id, '_applicant_profiles', $num_applicant_profiles + 1 );
5084 update_post_meta( $applicant_contact_id, '_applicant_profile_' . $num_applicant_profiles, array( 'department' => $property->department ) );
5085 }
5086 }*/
5087
5088 // Insert viewing record
5089 $viewing_post = array(
5090 'post_title' => '',
5091 'post_content' => '',
5092 'post_type' => 'viewing',
5093 'post_status' => 'publish',
5094 'comment_status' => 'closed',
5095 'ping_status' => 'closed',
5096 );
5097
5098 // Insert the post into the database
5099 $viewing_post_id = wp_insert_post( $viewing_post );
5100
5101 if ( is_wp_error($viewing_post_id) || $viewing_post_id == 0 )
5102 {
5103 $return = array('error' => 'Failed to create viewing post. Please try again');
5104 echo json_encode( $return );
5105 die();
5106 }
5107
5108 add_post_meta( $viewing_post_id, '_start_date_time', $booking['start_date'] . ' ' . $booking['start_time'] );
5109 add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
5110 add_post_meta( $viewing_post_id, '_property_id', $property_id );
5111
5112 $applicant_contacts = array();
5113 foreach ($applicant_contact_ids as $applicant_contact_id)
5114 {
5115 add_post_meta( $viewing_post_id, '_applicant_contact_id', $applicant_contact_id );
5116
5117 $applicant_contacts[] = array(
5118 'ID' => $applicant_contact_id,
5119 'post_title' => get_the_title($applicant_contact_id),
5120 'edit_link' => get_edit_post_link( $applicant_contact_id, '' ),
5121 );
5122 }
5123
5124 add_post_meta( $viewing_post_id, '_status', 'pending' );
5125 add_post_meta( $viewing_post_id, '_feedback_status', '' );
5126 add_post_meta( $viewing_post_id, '_feedback', '' );
5127 add_post_meta( $viewing_post_id, '_feedback_passed_on', '' );
5128
5129 if ( !empty($booking['negotiator_ids']) )
5130 {
5131 foreach ( $booking['negotiator_ids'] as $negotiator_id )
5132 {
5133 add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
5134 }
5135 }
5136
5137 $return = array('success' => array(
5138 'viewing' => array(
5139 'ID' => $viewing_post_id,
5140 'edit_link' => get_edit_post_link( $viewing_post_id, '' ),
5141 ),
5142 'applicant_contacts' => $applicant_contacts,
5143 ));
5144
5145 echo json_encode( $return );
5146
5147 die();
5148 }
5149
5150 public function book_viewing_contact()
5151 {
5152 check_ajax_referer( 'book-viewing', 'security' );
5153
5154 $this->json_headers();
5155
5156 $booking = $this->get_viewing_booking_input();
5157 $contact_id = $this->get_authorized_record_id( 'contact_id', 'contact' );
5158 foreach ( $booking['property_ids'] as $property_id ) {
5159 if ( 'property' !== get_post_type( $property_id ) || ! current_user_can( 'edit_post', $property_id ) ) {
5160 wp_send_json_error( __( 'Invalid property or insufficient permissions.', 'propertyhive' ), 403 );
5161 }
5162 }
5163 if ($contact_id < 1)
5164 {
5165 $return = array('error' => 'No contact selected');
5166 echo json_encode( $return );
5167 die();
5168 }
5169
5170 if (empty($booking['property_ids']))
5171 {
5172 $return = array('error' => 'No property selected');
5173 echo json_encode( $return );
5174 die();
5175 }
5176
5177 // Loop through contacts and create one viewing each
5178 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
5179 foreach ( $booking['property_ids'] as $property_id )
5180 {
5181 // Insert viewing record
5182 $viewing_post = array(
5183 'post_title' => '',
5184 'post_content' => '',
5185 'post_type' => 'viewing',
5186 'post_status' => 'publish',
5187 'comment_status' => 'closed',
5188 'ping_status' => 'closed',
5189 );
5190
5191 // Insert the post into the database
5192 $viewing_post_id = wp_insert_post( $viewing_post );
5193
5194 if ( is_wp_error($viewing_post_id) || $viewing_post_id == 0 )
5195 {
5196 $return = array('error' => 'Failed to create viewing post. Please try again');
5197 echo json_encode( $return );
5198 die();
5199 }
5200
5201 add_post_meta( $viewing_post_id, '_start_date_time', $booking['start_date'] . ' ' . $booking['start_time'] );
5202 add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
5203 add_post_meta( $viewing_post_id, '_property_id', (int)$property_id );
5204 add_post_meta( $viewing_post_id, '_applicant_contact_id', $contact_id );
5205 add_post_meta( $viewing_post_id, '_status', 'pending' );
5206 add_post_meta( $viewing_post_id, '_feedback_status', '' );
5207 add_post_meta( $viewing_post_id, '_feedback', '' );
5208 add_post_meta( $viewing_post_id, '_feedback_passed_on', '' );
5209
5210 if ( !empty($booking['negotiator_ids']) )
5211 {
5212 foreach ( $booking['negotiator_ids'] as $negotiator_id )
5213 {
5214 add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
5215 }
5216 }
5217 }
5218
5219 $properties = array();
5220 foreach ( $booking['property_ids'] as $property_id )
5221 {
5222 $properties[] = array(
5223 'ID' => (int)$property_id,
5224 'post_title' => get_the_title((int)$property_id),
5225 'edit_link' => get_edit_post_link( (int)$property_id, '' ),
5226 );
5227 }
5228
5229 $return = array('success' => array(
5230 'viewing' => array(
5231 'ID' => $viewing_post_id,
5232 'edit_link' => get_edit_post_link( $viewing_post_id, '' ),
5233 ),
5234 'properties' => $properties,
5235 ));
5236
5237 echo json_encode( $return );
5238
5239 die();
5240 }
5241
5242 public function get_viewing_details_meta_box()
5243 {
5244 global $post;
5245
5246 check_ajax_referer( 'viewing-details-meta-box', 'security' );
5247
5248 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5249
5250 $post = get_post( $post_id );
5251
5252 $viewing = new PH_Viewing( $post_id );
5253
5254 $readonly = isset( $_POST['readonly'] ) && is_scalar( $_POST['readonly'] ) ? filter_var( wp_unslash( $_POST['readonly'] ), FILTER_VALIDATE_BOOLEAN ) : false;
5255
5256 include( PH()->plugin_path() . '/includes/admin/views/html-viewing-details-meta-box.php' );
5257
5258 die();
5259 }
5260
5261 public function get_viewing_actions()
5262 {
5263 check_ajax_referer( 'viewing-actions', 'security' );
5264
5265 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5266
5267 include( PH()->plugin_path() . '/includes/admin/views/html-viewing-actions.php' );
5268
5269 die();
5270 }
5271
5272 public function get_viewing_lightbox()
5273 {
5274 global $post;
5275
5276 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- get_viewing_lightbox is an admin-only event (event map false), so authorize_admin_ajax enforces manage_propertyhive before this callback. The callback loads a viewing and includes a lightbox template; it performs no write. A local nonce is a defense-in-depth recommendation for this read-only GET, not an independent mutation vulnerability.
5277 $post_id = isset( $_GET['post_id'] ) && is_scalar( $_GET['post_id'] ) ? absint( $_GET['post_id'] ) : 0;
5278 if ( $post_id < 1 || 'viewing' !== get_post_type( $post_id ) || ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'edit_post', $post_id ) ) {
5279 wp_send_json_error( __( 'Invalid record or insufficient permissions.', 'propertyhive' ), 403 );
5280 }
5281
5282 $post = get_post((int)$post_id);
5283
5284 $viewing = new PH_Viewing($post_id);
5285
5286 include( PH()->plugin_path() . '/includes/admin/views/html-viewing-details-lightbox.php' );
5287
5288 die();
5289 }
5290
5291 public function viewing_carried_out()
5292 {
5293 check_ajax_referer( 'viewing-actions', 'security' );
5294
5295 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5296
5297 $status = get_post_meta( $post_id, '_status', TRUE );
5298
5299 if ( $status == 'pending' )
5300 {
5301 update_post_meta( $post_id, '_status', 'carried_out' );
5302
5303 // Add note/comment to viewing
5304 $comment = array(
5305 'note_type' => 'action',
5306 'action' => 'viewing_carried_out',
5307 );
5308
5309 PH_Comments::insert_note( $post_id, $comment );
5310
5311 wp_send_json_success();
5312 }
5313
5314 wp_send_json_error();
5315 }
5316
5317 public function viewing_no_show()
5318 {
5319 check_ajax_referer( 'viewing-actions', 'security' );
5320
5321 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5322
5323 $status = get_post_meta( $post_id, '_status', TRUE );
5324
5325 if ( $status == 'pending' )
5326 {
5327 update_post_meta( $post_id, '_status', 'no_show' );
5328
5329 // Add note/comment to viewing
5330 $comment = array(
5331 'note_type' => 'action',
5332 'action' => 'viewing_applicant_no_show',
5333 );
5334
5335 PH_Comments::insert_note( $post_id, $comment );
5336
5337 wp_send_json_success();
5338 }
5339
5340 wp_send_json_error();
5341 }
5342
5343 public function viewing_cancelled()
5344 {
5345 check_ajax_referer( 'viewing-actions', 'security' );
5346
5347 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5348
5349 $text = isset( $_POST['cancelled_reason'] ) && is_string( $_POST['cancelled_reason'] ) ? sanitize_textarea_field( wp_unslash( $_POST['cancelled_reason'] ) ) : '';
5350
5351 $status = get_post_meta( $post_id, '_status', TRUE );
5352
5353 if ( $status == 'pending' )
5354 {
5355 update_post_meta( $post_id, '_status', 'cancelled' );
5356 update_post_meta( $post_id, '_cancelled_reason', wp_slash( $text ) );
5357 update_post_meta( $post_id, '_cancelled_reason_public', isset($_POST['cancelled_reason_public']) && $_POST['cancelled_reason_public'] == 'yes' ? 'yes' : '' );
5358
5359 // Add note/comment to viewing
5360 $comment = array(
5361 'note_type' => 'action',
5362 'action' => 'viewing_cancelled',
5363 );
5364
5365 PH_Comments::insert_note( $post_id, $comment );
5366
5367 wp_send_json_success();
5368 }
5369
5370 wp_send_json_error();
5371 }
5372
5373 public function viewing_email_applicant_booking_confirmation()
5374 {
5375 check_ajax_referer( 'viewing-actions', 'security' );
5376
5377 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5378
5379 $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
5380 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5381
5382 if ( !is_array($applicant_contact_ids) || (int)$property_id == '' || count($applicant_contact_ids) == 0 || (int)$property_id == 0 )
5383 {
5384 wp_send_json_error('Missing contact or property');
5385 }
5386
5387 $property = new PH_Property((int)$property_id);
5388
5389 $to = array();
5390 foreach ($applicant_contact_ids as $applicant_contact_id)
5391 {
5392 $applicant_email_address = get_post_meta( $applicant_contact_id, '_email_address', TRUE );
5393 $explode_applicant_email_address = explode( ",", $applicant_email_address );
5394 foreach ( $explode_applicant_email_address as $email_address )
5395 {
5396 $to[] = sanitize_email($email_address);
5397 }
5398 }
5399
5400 $to = array_filter($to);
5401
5402 if ( !empty(implode($to)) )
5403 {
5404 $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_viewing_applicant_booking_confirmation_email_subject', '' );
5405 $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_viewing_applicant_booking_confirmation_email_body', '' );
5406
5407 $applicant_names = array();
5408 $applicant_dears = array();
5409 foreach ($applicant_contact_ids as $applicant_contact_id)
5410 {
5411 $applicant_contact = new PH_Contact($applicant_contact_id);
5412 $applicant_names[] = $applicant_contact->post_title;
5413 $applicant_dears[] = $applicant_contact->dear();
5414 }
5415 $applicant_names = array_filter($applicant_names);
5416 $applicant_dears = array_filter($applicant_dears);
5417
5418 $applicant_names_string = $this->get_list_string($applicant_names);
5419 $applicant_dears_string = $this->get_list_string($applicant_dears);
5420
5421 $negotiator_names = array();
5422 $negotiator_names_string = '';
5423
5424 $negotiator_email_addresses = array();
5425 $negotiator_email_addresses_string = '';
5426
5427 $negotiator_telephone_numbers = array();
5428 $negotiator_telephone_numbers_string = '';
5429
5430 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
5431 if ( !empty($negotiator_ids) )
5432 {
5433 foreach ( $negotiator_ids as $negotiator_id )
5434 {
5435 $negotiator = get_user_by( 'id', $negotiator_id );
5436 if ( $negotiator !== false )
5437 {
5438 if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
5439 {
5440 $negotiator_names[] = $negotiator->display_name;
5441 }
5442
5443 if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
5444 {
5445 $negotiator_email_addresses[] = $negotiator->user_email;
5446 }
5447
5448 $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
5449 if ( !empty($telephone_number) )
5450 {
5451 $negotiator_telephone_numbers[] = $telephone_number;
5452 }
5453 }
5454 }
5455 }
5456 if ( !empty($negotiator_names) )
5457 {
5458 $last = array_slice($negotiator_names, -1);
5459 $first = join(', ', array_slice($negotiator_names, 0, -1));
5460 $both = array_filter(array_merge(array($first), $last), 'strlen');
5461 $negotiator_names_string = join(' and ', $both);
5462 }
5463 if ( !empty($negotiator_email_addresses) )
5464 {
5465 $last = array_slice($negotiator_email_addresses, -1);
5466 $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
5467 $both = array_filter(array_merge(array($first), $last), 'strlen');
5468 $negotiator_email_addresses_string = join(' and ', $both);
5469 }
5470 if ( !empty($negotiator_telephone_numbers) )
5471 {
5472 $last = array_slice($negotiator_telephone_numbers, -1);
5473 $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
5474 $both = array_filter(array_merge(array($first), $last), 'strlen');
5475 $negotiator_telephone_numbers_string = join(' and ', $both);
5476 }
5477
5478 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
5479 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
5480 $subject = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5481 $subject = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5482 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
5483 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
5484 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
5485
5486 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_applicant_booking_confirmation_email_subject; third-party email integrations depend on the established name.
5487 $subject = apply_filters( 'viewing_applicant_booking_confirmation_email_subject', $subject, $post_id, $property_id );
5488
5489 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
5490 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
5491 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
5492 $body = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5493 $body = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5494 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
5495 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
5496 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5497
5498 $body = html_entity_decode($body);
5499
5500 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_applicant_booking_confirmation_email_body; third-party email integrations depend on the established name.
5501 $body = apply_filters( 'viewing_applicant_booking_confirmation_email_body', $body, $post_id, $property_id );
5502
5503 $from = '';
5504 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
5505 if ( $from_setting == 'user' )
5506 {
5507 $current_user = wp_get_current_user();
5508 $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
5509
5510 if ( $from == '' )
5511 {
5512 $from = $property->office_email_address;
5513 }
5514 }
5515 if ( $from_setting == 'office' )
5516 {
5517 $from = $property->office_email_address;
5518 }
5519 if ( $from == '' )
5520 {
5521 $from = get_option('propertyhive_email_from_address', '');
5522 }
5523 if ( $from == '' )
5524 {
5525 $from = get_bloginfo('admin_email');
5526 }
5527
5528 $attachments = array();
5529 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5530 {
5531 $uploaded_files = $this->get_viewing_email_uploads();
5532
5533 // Handle each file upload
5534 foreach ($uploaded_files['name'] as $key => $value)
5535 {
5536 if ($uploaded_files['name'][$key])
5537 {
5538 $file = array(
5539 'name' => $uploaded_files['name'][$key],
5540 'type' => $uploaded_files['type'][$key],
5541 'tmp_name' => $uploaded_files['tmp_name'][$key],
5542 'error' => $uploaded_files['error'][$key],
5543 'size' => $uploaded_files['size'][$key]
5544 );
5545
5546 // Move the file to a temporary location
5547 $upload_overrides = array('test_form' => false);
5548 $movefile = wp_handle_upload($file, $upload_overrides);
5549
5550 if ($movefile && !isset($movefile['error']))
5551 {
5552 // Add the file path to attachments array
5553 $attachments[] = $movefile['file'];
5554 }
5555 else
5556 {
5557 // Handle error in file upload
5558 wp_send_json_error($movefile['error']);
5559 }
5560 }
5561 }
5562 }
5563
5564 $headers = array();
5565 $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
5566 $headers[] = 'Reply-To: ' . sanitize_email($from);
5567 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
5568
5569 $headers = apply_filters( 'propertyhive_viewing_applicant_booking_confirmation_email_headers', $headers );
5570
5571 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
5572
5573 foreach ($attachments as $temp_file)
5574 {
5575 @wp_delete_file($temp_file);
5576 }
5577
5578 if ( !$sent )
5579 {
5580 wp_send_json_error('Failed to send email');
5581 }
5582
5583 update_post_meta( $post_id, '_applicant_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
5584
5585 if ( apply_filters( 'propertyhive_log_booking_confirmation_emails', false ) === true )
5586 {
5587 // Add note/comment to viewing
5588 $comment = array(
5589 'note_type' => 'action',
5590 'action' => 'viewing_applicant_booking_confirmation_email',
5591 );
5592
5593 PH_Comments::insert_note( $post_id, $comment );
5594 }
5595
5596 wp_send_json_success();
5597 }
5598 else
5599 {
5600 wp_send_json_error('No valid recipient email addresses');
5601 }
5602
5603 wp_die();
5604 }
5605
5606 public function viewing_email_owner_booking_confirmation()
5607 {
5608 check_ajax_referer( 'viewing-actions', 'security' );
5609
5610 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5611
5612 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5613 $property_department = get_post_meta( $property_id, '_department' );
5614
5615 $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
5616 $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
5617
5618 if ( $owner_contact_ids > 0 ) {
5619
5620 $owner_emails = array();
5621 $owner_names = array();
5622 $owner_dears = array();
5623
5624 foreach ($owner_contact_ids as $owner_id)
5625 {
5626 $owner_contact = new PH_Contact($owner_id);
5627
5628 $owner_name = $owner_contact->post_title;
5629 $owner_dear = $owner_contact->dear();
5630
5631 if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
5632 if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
5633
5634 $owner_email = $owner_contact->email_address;
5635 $explode_owner_email = explode( ",", $owner_email );
5636 foreach ( $explode_owner_email as $email_address )
5637 {
5638 $owner_emails[] = sanitize_email($email_address);
5639 }
5640 }
5641
5642 $owner_names_string = $this->get_list_string($owner_names);
5643 $owner_dears_string = $this->get_list_string($owner_dears);
5644
5645 if ( !empty($applicant_contact_ids) )
5646 {
5647 $applicant_names = array();
5648 $applicant_dears = array();
5649 foreach ($applicant_contact_ids as $applicant_contact_id)
5650 {
5651 $applicant_contact = new PH_Contact($applicant_contact_id);
5652 $applicant_names[] = $applicant_contact->post_title;
5653 $applicant_dears[] = $applicant_contact->dear();
5654 }
5655 $applicant_names = array_filter($applicant_names);
5656 $applicant_dears = array_filter($applicant_dears);
5657 }
5658
5659 $applicant_names_string = $this->get_list_string($applicant_names);
5660 $applicant_dears_string = $this->get_list_string($applicant_dears);
5661
5662 $negotiator_names = array();
5663 $negotiator_names_string = '';
5664
5665 $negotiator_email_addresses = array();
5666 $negotiator_email_addresses_string = '';
5667
5668 $negotiator_telephone_numbers = array();
5669 $negotiator_telephone_numbers_string = '';
5670
5671 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
5672 if ( !empty($negotiator_ids) )
5673 {
5674 foreach ( $negotiator_ids as $negotiator_id )
5675 {
5676 $negotiator = get_user_by( 'id', $negotiator_id );
5677 if ( $negotiator !== false )
5678 {
5679 if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
5680 {
5681 $negotiator_names[] = $negotiator->display_name;
5682 }
5683
5684 if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
5685 {
5686 $negotiator_email_addresses[] = $negotiator->user_email;
5687 }
5688
5689 $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
5690 if ( !empty($telephone_number) )
5691 {
5692 $negotiator_telephone_numbers[] = $telephone_number;
5693 }
5694 }
5695 }
5696 }
5697 if ( !empty($negotiator_names) )
5698 {
5699 $last = array_slice($negotiator_names, -1);
5700 $first = join(', ', array_slice($negotiator_names, 0, -1));
5701 $both = array_filter(array_merge(array($first), $last), 'strlen');
5702 $negotiator_names_string = join(' and ', $both);
5703 }
5704 if ( !empty($negotiator_email_addresses) )
5705 {
5706 $last = array_slice($negotiator_email_addresses, -1);
5707 $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
5708 $both = array_filter(array_merge(array($first), $last), 'strlen');
5709 $negotiator_email_addresses_string = join(' and ', $both);
5710 }
5711 if ( !empty($negotiator_telephone_numbers) )
5712 {
5713 $last = array_slice($negotiator_telephone_numbers, -1);
5714 $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
5715 $both = array_filter(array_merge(array($first), $last), 'strlen');
5716 $negotiator_telephone_numbers_string = join(' and ', $both);
5717 }
5718
5719 $property = new PH_Property((int)$property_id);
5720
5721 $to = implode(",", $owner_emails);
5722
5723 $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_viewing_owner_booking_confirmation_email_subject', '' );
5724 $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_viewing_owner_booking_confirmation_email_body', '' );
5725
5726 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
5727 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
5728 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
5729 $subject = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5730 $subject = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5731 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
5732 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
5733 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
5734
5735 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_owner_booking_confirmation_email_subject; third-party email integrations depend on the established name.
5736 $subject = apply_filters( 'viewing_owner_booking_confirmation_email_subject', $subject, $post_id, $property_id );
5737
5738 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
5739 $body = str_replace('[owner_name]', $owner_names_string, $body);
5740 $body = str_replace('[owner_dear]', $owner_dears_string, $body);
5741 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
5742 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
5743 $body = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5744 $body = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5745 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
5746 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
5747 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5748
5749 $body = html_entity_decode($body);
5750
5751 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_owner_booking_confirmation_email_body; third-party email integrations depend on the established name.
5752 $body = apply_filters( 'viewing_owner_booking_confirmation_email_body', $body, $post_id, $property_id );
5753
5754 $from = '';
5755 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
5756 if ( $from_setting == 'user' )
5757 {
5758 $current_user = wp_get_current_user();
5759 $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
5760
5761 if ( $from == '' )
5762 {
5763 $from = $property->office_email_address;
5764 }
5765 }
5766 if ( $from_setting == 'office' )
5767 {
5768 $from = $property->office_email_address;
5769 }
5770 if ( $from == '' )
5771 {
5772 $from = get_option('propertyhive_email_from_address', '');
5773 }
5774 if ( $from == '' )
5775 {
5776 $from = get_bloginfo('admin_email');
5777 }
5778
5779 $attachments = array();
5780 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5781 {
5782 $uploaded_files = $this->get_viewing_email_uploads();
5783
5784 // Handle each file upload
5785 foreach ($uploaded_files['name'] as $key => $value)
5786 {
5787 if ($uploaded_files['name'][$key])
5788 {
5789 $file = array(
5790 'name' => $uploaded_files['name'][$key],
5791 'type' => $uploaded_files['type'][$key],
5792 'tmp_name' => $uploaded_files['tmp_name'][$key],
5793 'error' => $uploaded_files['error'][$key],
5794 'size' => $uploaded_files['size'][$key]
5795 );
5796
5797 // Move the file to a temporary location
5798 $upload_overrides = array('test_form' => false);
5799 $movefile = wp_handle_upload($file, $upload_overrides);
5800
5801 if ($movefile && !isset($movefile['error']))
5802 {
5803 // Add the file path to attachments array
5804 $attachments[] = $movefile['file'];
5805 }
5806 else
5807 {
5808 // Handle error in file upload
5809 wp_send_json_error($movefile['error']);
5810 }
5811 }
5812 }
5813 }
5814
5815 $headers = array();
5816 $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
5817 $headers[] = 'Reply-To: ' . sanitize_email($from);
5818 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
5819
5820 $headers = apply_filters( 'propertyhive_viewing_owner_booking_confirmation_email_headers', $headers );
5821
5822 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
5823
5824 foreach ($attachments as $temp_file)
5825 {
5826 @wp_delete_file($temp_file);
5827 }
5828
5829 if ( !$sent )
5830 {
5831 wp_send_json_error('Failed to send email');
5832 }
5833
5834 if ( apply_filters( 'propertyhive_log_booking_confirmation_emails', false ) === true )
5835 {
5836 // Add note/comment to viewing
5837 $comment = array(
5838 'note_type' => 'action',
5839 'action' => 'viewing_owner_booking_confirmation_email',
5840 );
5841
5842 PH_Comments::insert_note( $post_id, $comment );
5843 }
5844
5845 update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
5846
5847 wp_send_json_success();
5848 }
5849 else
5850 {
5851 wp_send_json_error('No owner recipients');
5852 }
5853
5854 wp_die();
5855 }
5856
5857 public function viewing_email_attending_negotiator_booking_confirmation()
5858 {
5859 check_ajax_referer( 'viewing-actions', 'security' );
5860
5861 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5862 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5863
5864 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
5865
5866 $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
5867 $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
5868
5869 if ( !empty($negotiator_ids) ) {
5870
5871 $tos = array();
5872 foreach ($negotiator_ids as $negotiator_id)
5873 {
5874 $user_info = get_userdata((int)$negotiator_id);
5875 $tos[] = sanitize_email($user_info->user_email);
5876 }
5877 $to = implode(",", $tos);
5878
5879 $owner_emails = array();
5880 $owner_names = array();
5881 $owner_dears = array();
5882 $owner_details = array();
5883
5884 if ( !empty($owner_contact_ids) )
5885 {
5886 foreach ($owner_contact_ids as $owner_id)
5887 {
5888 $owner_contact = new PH_Contact($owner_id);
5889
5890 $owner_name = $owner_contact->post_title;
5891 $owner_dear = $owner_contact->dear();
5892
5893 if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
5894 if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
5895
5896 $owner_email = $owner_contact->email_address;
5897 $explode_owner_email = explode( ",", $owner_email );
5898 foreach ( $explode_owner_email as $email_address )
5899 {
5900 $owner_emails[] = sanitize_email($email_address);
5901 }
5902
5903 $owner_details[] = $owner_contact->post_title . "\nT: " . $owner_contact->telephone_number . "\nE: " . $owner_contact->email_address;
5904 }
5905 }
5906
5907 $owner_details = implode("\n\n", $owner_details);
5908
5909 $owner_names_string = $this->get_list_string($owner_names);
5910 $owner_dears_string = $this->get_list_string($owner_dears);
5911
5912 $applicant_names = array();
5913 $applicant_dears = array();
5914 $applicant_details = array();
5915
5916 if ( !empty($applicant_contact_ids) )
5917 {
5918 foreach ($applicant_contact_ids as $applicant_contact_id)
5919 {
5920 $applicant_contact = new PH_Contact($applicant_contact_id);
5921 $applicant_names[] = $applicant_contact->post_title;
5922 $applicant_dears[] = $applicant_contact->dear();
5923
5924 $applicant_details[] = $applicant_contact->post_title . "\nT: " . $applicant_contact->telephone_number . "\nE: " . $applicant_contact->email_address;
5925 }
5926 }
5927
5928 $applicant_details = implode("\n\n", $applicant_details);
5929
5930 $applicant_names = array_filter($applicant_names);
5931 $applicant_dears = array_filter($applicant_dears);
5932
5933 $applicant_names_string = $this->get_list_string($applicant_names);
5934 $applicant_dears_string = $this->get_list_string($applicant_dears);
5935
5936 $negotiator_names = array();
5937 $negotiator_names_string = '';
5938
5939 $negotiator_email_addresses = array();
5940 $negotiator_email_addresses_string = '';
5941
5942 $negotiator_telephone_numbers = array();
5943 $negotiator_telephone_numbers_string = '';
5944
5945 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
5946 if ( !empty($negotiator_ids) )
5947 {
5948 foreach ( $negotiator_ids as $negotiator_id )
5949 {
5950 $negotiator = get_user_by( 'id', $negotiator_id );
5951 if ( $negotiator !== false )
5952 {
5953 if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
5954 {
5955 $negotiator_names[] = $negotiator->display_name;
5956 }
5957
5958 if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
5959 {
5960 $negotiator_email_addresses[] = $negotiator->user_email;
5961 }
5962
5963 $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
5964 if ( !empty($telephone_number) )
5965 {
5966 $negotiator_telephone_numbers[] = $telephone_number;
5967 }
5968 }
5969 }
5970 }
5971 if ( !empty($negotiator_names) )
5972 {
5973 $last = array_slice($negotiator_names, -1);
5974 $first = join(', ', array_slice($negotiator_names, 0, -1));
5975 $both = array_filter(array_merge(array($first), $last), 'strlen');
5976 $negotiator_names_string = join(' and ', $both);
5977 }
5978 if ( !empty($negotiator_email_addresses) )
5979 {
5980 $last = array_slice($negotiator_email_addresses, -1);
5981 $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
5982 $both = array_filter(array_merge(array($first), $last), 'strlen');
5983 $negotiator_email_addresses_string = join(' and ', $both);
5984 }
5985 if ( !empty($negotiator_telephone_numbers) )
5986 {
5987 $last = array_slice($negotiator_telephone_numbers, -1);
5988 $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
5989 $both = array_filter(array_merge(array($first), $last), 'strlen');
5990 $negotiator_telephone_numbers_string = join(' and ', $both);
5991 }
5992
5993 $property = new PH_Property((int)$property_id);
5994
5995 $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_viewing_attending_negotiator_booking_confirmation_email_subject', '' );
5996 $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_viewing_attending_negotiator_booking_confirmation_email_body', '' );
5997
5998 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
5999 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
6000 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
6001 $subject = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6002 $subject = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6003 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
6004 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
6005 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
6006
6007 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_attending_negotiator_booking_confirmation_email_subject; third-party email integrations depend on the established name.
6008 $subject = apply_filters( 'viewing_attending_negotiator_booking_confirmation_email_subject', $subject, $post_id, $property_id );
6009
6010 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
6011 $body = str_replace('[owner_name]', $owner_names_string, $body);
6012 $body = str_replace('[owner_dear]', $owner_dears_string, $body);
6013 $body = str_replace('[owner_details]', $owner_details, $body);
6014 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
6015 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
6016 $body = str_replace('[applicant_details]', $applicant_details, $body);
6017 $body = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6018 $body = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6019 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
6020 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
6021 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
6022
6023 $body = html_entity_decode($body);
6024
6025 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_attending_negotiator_booking_confirmation_email_body; third-party email integrations depend on the established name.
6026 $body = apply_filters( 'viewing_attending_negotiator_booking_confirmation_email_body', $body, $post_id, $property_id );
6027
6028 $from = '';
6029 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
6030 if ( $from_setting == 'user' )
6031 {
6032 $current_user = wp_get_current_user();
6033 $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
6034
6035 if ( $from == '' )
6036 {
6037 $from = $property->office_email_address;
6038 }
6039 }
6040 if ( $from_setting == 'office' )
6041 {
6042 $from = $property->office_email_address;
6043 }
6044 if ( $from == '' )
6045 {
6046 $from = get_option('propertyhive_email_from_address', '');
6047 }
6048 if ( $from == '' )
6049 {
6050 $from = get_bloginfo('admin_email');
6051 }
6052
6053 $attachments = array();
6054 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6055 {
6056 $uploaded_files = $this->get_viewing_email_uploads();
6057
6058 // Handle each file upload
6059 foreach ($uploaded_files['name'] as $key => $value)
6060 {
6061 if ($uploaded_files['name'][$key])
6062 {
6063 $file = array(
6064 'name' => $uploaded_files['name'][$key],
6065 'type' => $uploaded_files['type'][$key],
6066 'tmp_name' => $uploaded_files['tmp_name'][$key],
6067 'error' => $uploaded_files['error'][$key],
6068 'size' => $uploaded_files['size'][$key]
6069 );
6070
6071 // Move the file to a temporary location
6072 $upload_overrides = array('test_form' => false);
6073 $movefile = wp_handle_upload($file, $upload_overrides);
6074
6075 if ($movefile && !isset($movefile['error']))
6076 {
6077 // Add the file path to attachments array
6078 $attachments[] = $movefile['file'];
6079 }
6080 else
6081 {
6082 // Handle error in file upload
6083 wp_send_json_error($movefile['error']);
6084 }
6085 }
6086 }
6087 }
6088
6089 $headers = array();
6090 $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
6091 $headers[] = 'Reply-To: ' . sanitize_email($from);
6092 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
6093
6094 $headers = apply_filters( 'propertyhive_viewing_attending_negotiator_booking_confirmation_email_headers', $headers );
6095
6096 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
6097
6098 foreach ($attachments as $temp_file)
6099 {
6100 @wp_delete_file($temp_file);
6101 }
6102
6103 if ( !$sent )
6104 {
6105 wp_send_json_error('Failed to send email');
6106 }
6107
6108 // Add note/comment to viewing
6109 if ( apply_filters( 'propertyhive_log_booking_confirmation_emails', false ) === true )
6110 {
6111 $comment = array(
6112 'note_type' => 'action',
6113 'action' => 'viewing_attending_negotiator_booking_confirmation_email',
6114 );
6115
6116 PH_Comments::insert_note( $post_id, $comment );
6117 }
6118
6119 update_post_meta( $post_id, '_attending_negotiator_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
6120
6121 wp_send_json_success();
6122 }
6123 else
6124 {
6125 wp_send_json_error('No attending negotiator recipients');
6126 }
6127
6128 wp_die();
6129 }
6130
6131 public function viewing_email_applicant_cancellation_notification()
6132 {
6133 check_ajax_referer( 'viewing-actions', 'security' );
6134
6135 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6136
6137 $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
6138 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
6139
6140 if ( !is_array($applicant_contact_ids) || (int)$property_id == '' || count($applicant_contact_ids) == 0 || (int)$property_id == 0 )
6141 {
6142 wp_send_json_error('Missing contact or property');
6143 }
6144
6145 $property = new PH_Property((int)$property_id);
6146
6147 $to = array();
6148 foreach ($applicant_contact_ids as $applicant_contact_id)
6149 {
6150 $applicant_email_address = get_post_meta( $applicant_contact_id, '_email_address', TRUE );
6151 $explode_applicant_email_address = explode( ",", $applicant_email_address );
6152 foreach ( $explode_applicant_email_address as $email_address )
6153 {
6154 $to[] = sanitize_email($email_address);
6155 }
6156 }
6157
6158 $to = array_filter($to);
6159
6160 if ( !empty(implode($to)) )
6161 {
6162 $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_viewing_applicant_cancellation_notification_email_subject', '' );
6163 $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_viewing_applicant_cancellation_notification_email_body', '' );
6164
6165 $applicant_names = array();
6166 $applicant_dears = array();
6167 foreach ($applicant_contact_ids as $applicant_contact_id)
6168 {
6169 $applicant_contact = new PH_Contact($applicant_contact_id);
6170 $applicant_names[] = $applicant_contact->post_title;
6171 $applicant_dears[] = $applicant_contact->dear();
6172 }
6173 $applicant_names = array_filter($applicant_names);
6174 $applicant_dears = array_filter($applicant_dears);
6175
6176 $applicant_names_string = $this->get_list_string($applicant_names);
6177 $applicant_dears_string = $this->get_list_string($applicant_dears);
6178
6179 $negotiator_names = array();
6180 $negotiator_names_string = '';
6181
6182 $negotiator_email_addresses = array();
6183 $negotiator_email_addresses_string = '';
6184
6185 $negotiator_telephone_numbers = array();
6186 $negotiator_telephone_numbers_string = '';
6187
6188 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
6189 if ( !empty($negotiator_ids) )
6190 {
6191 foreach ( $negotiator_ids as $negotiator_id )
6192 {
6193 $negotiator = get_user_by( 'id', $negotiator_id );
6194 if ( $negotiator !== false )
6195 {
6196 if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
6197 {
6198 $negotiator_names[] = $negotiator->display_name;
6199 }
6200
6201 if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
6202 {
6203 $negotiator_email_addresses[] = $negotiator->user_email;
6204 }
6205
6206 $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
6207 if ( !empty($telephone_number) )
6208 {
6209 $negotiator_telephone_numbers[] = $telephone_number;
6210 }
6211 }
6212 }
6213 }
6214 if ( !empty($negotiator_names) )
6215 {
6216 $last = array_slice($negotiator_names, -1);
6217 $first = join(', ', array_slice($negotiator_names, 0, -1));
6218 $both = array_filter(array_merge(array($first), $last), 'strlen');
6219 $negotiator_names_string = join(' and ', $both);
6220 }
6221 if ( !empty($negotiator_email_addresses) )
6222 {
6223 $last = array_slice($negotiator_email_addresses, -1);
6224 $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
6225 $both = array_filter(array_merge(array($first), $last), 'strlen');
6226 $negotiator_email_addresses_string = join(' and ', $both);
6227 }
6228 if ( !empty($negotiator_telephone_numbers) )
6229 {
6230 $last = array_slice($negotiator_telephone_numbers, -1);
6231 $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
6232 $both = array_filter(array_merge(array($first), $last), 'strlen');
6233 $negotiator_telephone_numbers_string = join(' and ', $both);
6234 }
6235
6236 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
6237 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
6238 $subject = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6239 $subject = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6240 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
6241 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
6242 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
6243
6244 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_applicant_cancellation_notification_email_subject; third-party email integrations depend on the established name.
6245 $subject = apply_filters( 'viewing_applicant_cancellation_notification_email_subject', $subject, $post_id, $property_id );
6246
6247 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
6248 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
6249 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
6250 $body = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6251 $body = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6252 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
6253 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
6254 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
6255
6256 $cancelled_reason = '';
6257 if (
6258 get_post_meta( $post_id, '_cancelled_reason_public', true ) == 'yes' &&
6259 get_post_meta( $post_id, '_cancelled_reason', true ) != ''
6260 )
6261 {
6262 $cancelled_reason .= "\n\nReason: " . get_post_meta( $post_id, '_cancelled_reason', true );
6263 }
6264 $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
6265
6266 $body = html_entity_decode($body);
6267
6268 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_applicant_cancellation_notification_email_body; third-party email integrations depend on the established name.
6269 $body = apply_filters( 'viewing_applicant_cancellation_notification_email_body', $body, $post_id, $property_id );
6270
6271 $from = '';
6272 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
6273 if ( $from_setting == 'user' )
6274 {
6275 $current_user = wp_get_current_user();
6276 $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
6277
6278 if ( $from == '' )
6279 {
6280 $from = $property->office_email_address;
6281 }
6282 }
6283 if ( $from_setting == 'office' )
6284 {
6285 $from = $property->office_email_address;
6286 }
6287 if ( $from == '' )
6288 {
6289 $from = get_option('propertyhive_email_from_address', '');
6290 }
6291 if ( $from == '' )
6292 {
6293 $from = get_bloginfo('admin_email');
6294 }
6295
6296 $attachments = array();
6297 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6298 {
6299 $uploaded_files = $this->get_viewing_email_uploads();
6300
6301 // Handle each file upload
6302 foreach ($uploaded_files['name'] as $key => $value)
6303 {
6304 if ($uploaded_files['name'][$key])
6305 {
6306 $file = array(
6307 'name' => $uploaded_files['name'][$key],
6308 'type' => $uploaded_files['type'][$key],
6309 'tmp_name' => $uploaded_files['tmp_name'][$key],
6310 'error' => $uploaded_files['error'][$key],
6311 'size' => $uploaded_files['size'][$key]
6312 );
6313
6314 // Move the file to a temporary location
6315 $upload_overrides = array('test_form' => false);
6316 $movefile = wp_handle_upload($file, $upload_overrides);
6317
6318 if ($movefile && !isset($movefile['error']))
6319 {
6320 // Add the file path to attachments array
6321 $attachments[] = $movefile['file'];
6322 }
6323 else
6324 {
6325 // Handle error in file upload
6326 wp_send_json_error($movefile['error']);
6327 }
6328 }
6329 }
6330 }
6331
6332 $headers = array();
6333 $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
6334 $headers[] = 'Reply-To: ' . sanitize_email($from);
6335 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
6336
6337 $headers = apply_filters( 'propertyhive_viewing_applicant_cancellation_notification_email_headers', $headers );
6338
6339 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
6340
6341 foreach ($attachments as $temp_file)
6342 {
6343 @wp_delete_file($temp_file);
6344 }
6345
6346 if ( !$sent )
6347 {
6348 wp_send_json_error('Failed to send email');
6349 }
6350
6351 update_post_meta( $post_id, '_applicant_cancellation_notification_sent_at', gmdate("Y-m-d H:i:s") );
6352
6353 if ( apply_filters( 'propertyhive_log_cancellation_notification_emails', false ) === true )
6354 {
6355 // Add note/comment to viewing
6356 $comment = array(
6357 'note_type' => 'action',
6358 'action' => 'viewing_applicant_cancellation_notification_email',
6359 );
6360
6361 PH_Comments::insert_note( $post_id, $comment );
6362 }
6363
6364 wp_send_json_success();
6365 }
6366 else
6367 {
6368 wp_send_json_error('No valid recipient email addresses');
6369 }
6370
6371 wp_die();
6372 }
6373
6374 public function viewing_email_owner_cancellation_notification()
6375 {
6376 check_ajax_referer( 'viewing-actions', 'security' );
6377
6378 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6379
6380 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
6381 $property_department = get_post_meta( $property_id, '_department' );
6382
6383 $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
6384 $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
6385
6386 if ( $owner_contact_ids > 0 ) {
6387
6388 $owner_emails = array();
6389 $owner_names = array();
6390 $owner_dears = array();
6391
6392 foreach ($owner_contact_ids as $owner_id)
6393 {
6394 $owner_contact = new PH_Contact($owner_id);
6395
6396 $owner_name = $owner_contact->post_title;
6397 $owner_dear = $owner_contact->dear();
6398
6399 if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
6400 if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
6401
6402 $owner_email = $owner_contact->email_address;
6403 $explode_owner_email = explode( ",", $owner_email );
6404 foreach ( $explode_owner_email as $email_address )
6405 {
6406 $owner_emails[] = sanitize_email($email_address);
6407 }
6408 }
6409
6410 $owner_names_string = $this->get_list_string($owner_names);
6411 $owner_dears_string = $this->get_list_string($owner_dears);
6412
6413 if ( !empty($applicant_contact_ids) )
6414 {
6415 $applicant_names = array();
6416 $applicant_dears = array();
6417 foreach ($applicant_contact_ids as $applicant_contact_id)
6418 {
6419 $applicant_contact = new PH_Contact($applicant_contact_id);
6420 $applicant_names[] = $applicant_contact->post_title;
6421 $applicant_dears[] = $applicant_contact->dear();
6422 }
6423 $applicant_names = array_filter($applicant_names);
6424 $applicant_dears = array_filter($applicant_dears);
6425 }
6426
6427 $applicant_names_string = $this->get_list_string($applicant_names);
6428 $applicant_dears_string = $this->get_list_string($applicant_dears);
6429
6430 $negotiator_names = array();
6431 $negotiator_names_string = '';
6432
6433 $negotiator_email_addresses = array();
6434 $negotiator_email_addresses_string = '';
6435
6436 $negotiator_telephone_numbers = array();
6437 $negotiator_telephone_numbers_string = '';
6438
6439 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
6440 if ( !empty($negotiator_ids) )
6441 {
6442 foreach ( $negotiator_ids as $negotiator_id )
6443 {
6444 $negotiator = get_user_by( 'id', $negotiator_id );
6445 if ( $negotiator !== false )
6446 {
6447 if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
6448 {
6449 $negotiator_names[] = $negotiator->display_name;
6450 }
6451
6452 if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
6453 {
6454 $negotiator_email_addresses[] = $negotiator->user_email;
6455 }
6456
6457 $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
6458 if ( !empty($telephone_number) )
6459 {
6460 $negotiator_telephone_numbers[] = $telephone_number;
6461 }
6462 }
6463 }
6464 }
6465 if ( !empty($negotiator_names) )
6466 {
6467 $last = array_slice($negotiator_names, -1);
6468 $first = join(', ', array_slice($negotiator_names, 0, -1));
6469 $both = array_filter(array_merge(array($first), $last), 'strlen');
6470 $negotiator_names_string = join(' and ', $both);
6471 }
6472 if ( !empty($negotiator_email_addresses) )
6473 {
6474 $last = array_slice($negotiator_email_addresses, -1);
6475 $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
6476 $both = array_filter(array_merge(array($first), $last), 'strlen');
6477 $negotiator_email_addresses_string = join(' and ', $both);
6478 }
6479 if ( !empty($negotiator_telephone_numbers) )
6480 {
6481 $last = array_slice($negotiator_telephone_numbers, -1);
6482 $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
6483 $both = array_filter(array_merge(array($first), $last), 'strlen');
6484 $negotiator_telephone_numbers_string = join(' and ', $both);
6485 }
6486
6487 $property = new PH_Property((int)$property_id);
6488
6489 $to = implode(",", $owner_emails);
6490
6491 $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_viewing_owner_cancellation_notification_email_subject', '' );
6492 $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_viewing_owner_cancellation_notification_email_body', '' );
6493
6494 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
6495 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
6496 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
6497 $subject = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6498 $subject = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6499 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
6500 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
6501 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
6502
6503 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_owner_cancellation_notification_email_subject; third-party email integrations depend on the established name.
6504 $subject = apply_filters( 'viewing_owner_cancellation_notification_email_subject', $subject, $post_id, $property_id );
6505
6506 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
6507 $body = str_replace('[owner_name]', $owner_names_string, $body);
6508 $body = str_replace('[owner_dear]', $owner_dears_string, $body);
6509 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
6510 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
6511 $body = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6512 $body = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6513 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
6514 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
6515 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
6516
6517 $cancelled_reason = '';
6518 if (
6519 get_post_meta( $post_id, '_cancelled_reason_public', true ) == 'yes' &&
6520 get_post_meta( $post_id, '_cancelled_reason', true ) != ''
6521 )
6522 {
6523 $cancelled_reason .= "\n\nReason: " . get_post_meta( $post_id, '_cancelled_reason', true );
6524 }
6525 $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
6526
6527 $body = html_entity_decode($body);
6528
6529 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_owner_cancellation_notification_email_body; third-party email integrations depend on the established name.
6530 $body = apply_filters( 'viewing_owner_cancellation_notification_email_body', $body, $post_id, $property_id );
6531
6532 $from = '';
6533 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
6534 if ( $from_setting == 'user' )
6535 {
6536 $current_user = wp_get_current_user();
6537 $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
6538
6539 if ( $from == '' )
6540 {
6541 $from = $property->office_email_address;
6542 }
6543 }
6544 if ( $from_setting == 'office' )
6545 {
6546 $from = $property->office_email_address;
6547 }
6548 if ( $from == '' )
6549 {
6550 $from = get_option('propertyhive_email_from_address', '');
6551 }
6552 if ( $from == '' )
6553 {
6554 $from = get_bloginfo('admin_email');
6555 }
6556
6557 $attachments = array();
6558 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6559 {
6560 $uploaded_files = $this->get_viewing_email_uploads();
6561
6562 // Handle each file upload
6563 foreach ($uploaded_files['name'] as $key => $value)
6564 {
6565 if ($uploaded_files['name'][$key])
6566 {
6567 $file = array(
6568 'name' => $uploaded_files['name'][$key],
6569 'type' => $uploaded_files['type'][$key],
6570 'tmp_name' => $uploaded_files['tmp_name'][$key],
6571 'error' => $uploaded_files['error'][$key],
6572 'size' => $uploaded_files['size'][$key]
6573 );
6574
6575 // Move the file to a temporary location
6576 $upload_overrides = array('test_form' => false);
6577 $movefile = wp_handle_upload($file, $upload_overrides);
6578
6579 if ($movefile && !isset($movefile['error']))
6580 {
6581 // Add the file path to attachments array
6582 $attachments[] = $movefile['file'];
6583 }
6584 else
6585 {
6586 // Handle error in file upload
6587 wp_send_json_error($movefile['error']);
6588 }
6589 }
6590 }
6591 }
6592
6593 $headers = array();
6594 $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
6595 $headers[] = 'Reply-To: ' . sanitize_email($from);
6596 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
6597
6598 $headers = apply_filters( 'propertyhive_viewing_owner_cancellation_notification_email_headers', $headers );
6599
6600 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
6601
6602 foreach ($attachments as $temp_file)
6603 {
6604 @wp_delete_file($temp_file);
6605 }
6606
6607 if ( !$sent )
6608 {
6609 wp_send_json_error('Failed to send email');
6610 }
6611
6612 if ( apply_filters( 'propertyhive_log_cancellation_notification_emails', false ) === true )
6613 {
6614 // Add note/comment to viewing
6615 $comment = array(
6616 'note_type' => 'action',
6617 'action' => 'viewing_owner_cancellation_notification_email',
6618 );
6619
6620 PH_Comments::insert_note( $post_id, $comment );
6621 }
6622
6623 update_post_meta( $post_id, '_owner_cancellation_notification_sent_at', gmdate("Y-m-d H:i:s") );
6624
6625 wp_send_json_success();
6626 }
6627 else
6628 {
6629 wp_send_json_error('No owner recipients');
6630 }
6631
6632 wp_die();
6633 }
6634
6635 public function viewing_email_attending_negotiator_cancellation_notification()
6636 {
6637 check_ajax_referer( 'viewing-actions', 'security' );
6638
6639 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6640 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
6641
6642 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
6643
6644 $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
6645 $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
6646
6647 if ( !empty($negotiator_ids) ) {
6648
6649 $tos = array();
6650 foreach ($negotiator_ids as $negotiator_id)
6651 {
6652 $user_info = get_userdata((int)$negotiator_id);
6653 $tos[] = sanitize_email($user_info->user_email);
6654 }
6655 $to = implode(",", $tos);
6656
6657 $owner_emails = array();
6658 $owner_names = array();
6659 $owner_dears = array();
6660 $owner_details = array();
6661
6662 if ( !empty($owner_contact_ids) )
6663 {
6664 foreach ($owner_contact_ids as $owner_id)
6665 {
6666 $owner_contact = new PH_Contact($owner_id);
6667
6668 $owner_name = $owner_contact->post_title;
6669 $owner_dear = $owner_contact->dear();
6670
6671 if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
6672 if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
6673
6674 $owner_email = $owner_contact->email_address;
6675 $explode_owner_email = explode( ",", $owner_email );
6676 foreach ( $explode_owner_email as $email_address )
6677 {
6678 $owner_emails[] = sanitize_email($email_address);
6679 }
6680
6681 $owner_details[] = $owner_contact->post_title . "\nT: " . $owner_contact->telephone_number . "\nE: " . $owner_contact->email_address;
6682 }
6683 }
6684
6685 $owner_details = implode("\n\n", $owner_details);
6686
6687 $owner_names_string = $this->get_list_string($owner_names);
6688 $owner_dears_string = $this->get_list_string($owner_dears);
6689
6690 $applicant_names = array();
6691 $applicant_dears = array();
6692 $applicant_details = array();
6693
6694 if ( !empty($applicant_contact_ids) )
6695 {
6696 foreach ($applicant_contact_ids as $applicant_contact_id)
6697 {
6698 $applicant_contact = new PH_Contact($applicant_contact_id);
6699 $applicant_names[] = $applicant_contact->post_title;
6700 $applicant_dears[] = $applicant_contact->dear();
6701
6702 $applicant_details[] = $applicant_contact->post_title . "\nT: " . $applicant_contact->telephone_number . "\nE: " . $applicant_contact->email_address;
6703 }
6704 }
6705
6706 $applicant_details = implode("\n\n", $applicant_details);
6707
6708 $applicant_names = array_filter($applicant_names);
6709 $applicant_dears = array_filter($applicant_dears);
6710
6711 $applicant_names_string = $this->get_list_string($applicant_names);
6712 $applicant_dears_string = $this->get_list_string($applicant_dears);
6713
6714 $negotiator_names = array();
6715 $negotiator_names_string = '';
6716
6717 $negotiator_email_addresses = array();
6718 $negotiator_email_addresses_string = '';
6719
6720 $negotiator_telephone_numbers = array();
6721 $negotiator_telephone_numbers_string = '';
6722
6723 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
6724 if ( !empty($negotiator_ids) )
6725 {
6726 foreach ( $negotiator_ids as $negotiator_id )
6727 {
6728 $negotiator = get_user_by( 'id', $negotiator_id );
6729 if ( $negotiator !== false )
6730 {
6731 if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
6732 {
6733 $negotiator_names[] = $negotiator->display_name;
6734 }
6735
6736 if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
6737 {
6738 $negotiator_email_addresses[] = $negotiator->user_email;
6739 }
6740
6741 $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
6742 if ( !empty($telephone_number) )
6743 {
6744 $negotiator_telephone_numbers[] = $telephone_number;
6745 }
6746 }
6747 }
6748 }
6749 if ( !empty($negotiator_names) )
6750 {
6751 $last = array_slice($negotiator_names, -1);
6752 $first = join(', ', array_slice($negotiator_names, 0, -1));
6753 $both = array_filter(array_merge(array($first), $last), 'strlen');
6754 $negotiator_names_string = join(' and ', $both);
6755 }
6756 if ( !empty($negotiator_email_addresses) )
6757 {
6758 $last = array_slice($negotiator_email_addresses, -1);
6759 $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
6760 $both = array_filter(array_merge(array($first), $last), 'strlen');
6761 $negotiator_email_addresses_string = join(' and ', $both);
6762 }
6763 if ( !empty($negotiator_telephone_numbers) )
6764 {
6765 $last = array_slice($negotiator_telephone_numbers, -1);
6766 $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
6767 $both = array_filter(array_merge(array($first), $last), 'strlen');
6768 $negotiator_telephone_numbers_string = join(' and ', $both);
6769 }
6770
6771 $property = new PH_Property((int)$property_id);
6772
6773 $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_viewing_attending_negotiator_cancellation_notification_email_subject', '' );
6774 $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_viewing_attending_negotiator_cancellation_notification_email_body', '' );
6775
6776 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
6777 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
6778 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
6779 $subject = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6780 $subject = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6781 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
6782 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
6783 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
6784
6785 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_attending_negotiator_cancellation_notification_email_subject; third-party email integrations depend on the established name.
6786 $subject = apply_filters( 'viewing_attending_negotiator_cancellation_notification_email_subject', $subject, $post_id, $property_id );
6787
6788 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
6789 $body = str_replace('[owner_name]', $owner_names_string, $body);
6790 $body = str_replace('[owner_dear]', $owner_dears_string, $body);
6791 $body = str_replace('[owner_details]', $owner_details, $body);
6792 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
6793 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
6794 $body = str_replace('[applicant_details]', $applicant_details, $body);
6795 $body = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6796 $body = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6797 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
6798 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
6799 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
6800
6801 $cancelled_reason = '';
6802 if (
6803 get_post_meta( $post_id, '_cancelled_reason_public', true ) == 'yes' &&
6804 get_post_meta( $post_id, '_cancelled_reason', true ) != ''
6805 )
6806 {
6807 $cancelled_reason .= "\n\nReason: " . get_post_meta( $post_id, '_cancelled_reason', true );
6808 }
6809 $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
6810
6811 $body = html_entity_decode($body);
6812
6813 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_attending_negotiator_cancellation_notification_email_body; third-party email integrations depend on the established name.
6814 $body = apply_filters( 'viewing_attending_negotiator_cancellation_notification_email_body', $body, $post_id, $property_id );
6815
6816 $from = '';
6817 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
6818 if ( $from_setting == 'user' )
6819 {
6820 $current_user = wp_get_current_user();
6821 $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
6822
6823 if ( $from == '' )
6824 {
6825 $from = $property->office_email_address;
6826 }
6827 }
6828 if ( $from_setting == 'office' )
6829 {
6830 $from = $property->office_email_address;
6831 }
6832 if ( $from == '' )
6833 {
6834 $from = get_option('propertyhive_email_from_address', '');
6835 }
6836 if ( $from == '' )
6837 {
6838 $from = get_bloginfo('admin_email');
6839 }
6840
6841 $attachments = array();
6842 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6843 {
6844 $uploaded_files = $this->get_viewing_email_uploads();
6845
6846 // Handle each file upload
6847 foreach ($uploaded_files['name'] as $key => $value)
6848 {
6849 if ($uploaded_files['name'][$key])
6850 {
6851 $file = array(
6852 'name' => $uploaded_files['name'][$key],
6853 'type' => $uploaded_files['type'][$key],
6854 'tmp_name' => $uploaded_files['tmp_name'][$key],
6855 'error' => $uploaded_files['error'][$key],
6856 'size' => $uploaded_files['size'][$key]
6857 );
6858
6859 // Move the file to a temporary location
6860 $upload_overrides = array('test_form' => false);
6861 $movefile = wp_handle_upload($file, $upload_overrides);
6862
6863 if ($movefile && !isset($movefile['error']))
6864 {
6865 // Add the file path to attachments array
6866 $attachments[] = $movefile['file'];
6867 }
6868 else
6869 {
6870 // Handle error in file upload
6871 wp_send_json_error($movefile['error']);
6872 }
6873 }
6874 }
6875 }
6876
6877 $headers = array();
6878 $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
6879 $headers[] = 'Reply-To: ' . sanitize_email($from);
6880 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
6881
6882 $headers = apply_filters( 'propertyhive_viewing_attending_negotiator_cancellation_notification_email_headers', $headers );
6883
6884 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
6885
6886 foreach ($attachments as $temp_file)
6887 {
6888 @wp_delete_file($temp_file);
6889 }
6890
6891 if ( !$sent )
6892 {
6893 wp_send_json_error('Failed to send email');
6894 }
6895
6896 // Add note/comment to viewing
6897 if ( apply_filters( 'propertyhive_log_cancellation_notification_emails', false ) === true )
6898 {
6899 $comment = array(
6900 'note_type' => 'action',
6901 'action' => 'viewing_attending_negotiator_cancellation_notification_email',
6902 );
6903
6904 PH_Comments::insert_note( $post_id, $comment );
6905 }
6906
6907 update_post_meta( $post_id, '_attending_negotiator_cancellation_notification_sent_at', gmdate("Y-m-d H:i:s") );
6908
6909 wp_send_json_success();
6910 }
6911 else
6912 {
6913 wp_send_json_error('No attending negotiator recipients');
6914 }
6915
6916 wp_die();
6917 }
6918
6919 public function viewing_interested_feedback()
6920 {
6921 check_ajax_referer( 'viewing-actions', 'security' );
6922
6923 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6924
6925 $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6926
6927 $status = get_post_meta( $post_id, '_status', TRUE );
6928
6929 if ( $status == 'carried_out' )
6930 {
6931 update_post_meta( $post_id, '_feedback_status', 'interested' );
6932 update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
6933
6934 // Add note/comment to viewing
6935 $comment = array(
6936 'note_type' => 'action',
6937 'action' => 'viewing_applicant_interested',
6938 );
6939
6940 PH_Comments::insert_note( $post_id, $comment );
6941
6942 wp_send_json_success();
6943 }
6944
6945 wp_send_json_error();
6946 }
6947
6948 public function viewing_not_interested_feedback()
6949 {
6950 check_ajax_referer( 'viewing-actions', 'security' );
6951
6952 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6953
6954 $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6955
6956 $status = get_post_meta( $post_id, '_status', TRUE );
6957
6958 if ( $status == 'carried_out' )
6959 {
6960 update_post_meta( $post_id, '_feedback_status', 'not_interested' );
6961 update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
6962
6963 // Add note/comment to viewing
6964 $comment = array(
6965 'note_type' => 'action',
6966 'action' => 'viewing_applicant_not_interested',
6967 );
6968
6969 PH_Comments::insert_note( $post_id, $comment );
6970
6971 wp_send_json_success();
6972 }
6973
6974 wp_send_json_error();
6975 }
6976
6977 public function viewing_feedback_not_required()
6978 {
6979 check_ajax_referer( 'viewing-actions', 'security' );
6980
6981 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6982
6983 $status = get_post_meta( $post_id, '_status', TRUE );
6984
6985 if ( $status == 'carried_out' )
6986 {
6987 update_post_meta( $post_id, '_feedback_status', 'not_required' );
6988
6989 // Add note/comment to viewing
6990 $comment = array(
6991 'note_type' => 'action',
6992 'action' => 'viewing_feedback_not_required',
6993 );
6994
6995 PH_Comments::insert_note( $post_id, $comment );
6996
6997 wp_send_json_success();
6998 }
6999
7000 wp_send_json_error();
7001 }
7002
7003 public function viewing_revert_feedback_pending()
7004 {
7005 check_ajax_referer( 'viewing-actions', 'security' );
7006
7007 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
7008
7009 $status = get_post_meta( $post_id, '_status', TRUE );
7010
7011 if ( $status == 'carried_out' )
7012 {
7013 update_post_meta( $post_id, '_feedback_status', '' );
7014 update_post_meta( $post_id, '_feedback_passed_on', '' );
7015 delete_post_meta( $post_id, '_feedback_received_date' );
7016
7017 // Add note/comment to viewing
7018 $comment = array(
7019 'note_type' => 'action',
7020 'action' => 'viewing_revert_feedback_pending',
7021 );
7022
7023 PH_Comments::insert_note( $post_id, $comment );
7024
7025 wp_send_json_success();
7026 }
7027
7028 wp_send_json_error();
7029 }
7030
7031 public function viewing_revert_pending()
7032 {
7033 check_ajax_referer( 'viewing-actions', 'security' );
7034
7035 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
7036
7037 $status = get_post_meta( $post_id, '_status', TRUE );
7038
7039 if ( in_array( $status, array('carried_out', 'cancelled', 'no_show') ) )
7040 {
7041 update_post_meta( $post_id, '_status', 'pending' );
7042 update_post_meta( $post_id, '_feedback_status', '' );
7043 delete_post_meta( $post_id, '_feedback_received_date' );
7044
7045 // Add note/comment to viewing
7046 $comment = array(
7047 'note_type' => 'action',
7048 'action' => 'viewing_revert_pending',
7049 );
7050
7051 PH_Comments::insert_note( $post_id, $comment );
7052
7053 wp_send_json_success();
7054 }
7055
7056 wp_send_json_error();
7057 }
7058
7059 public function viewing_feedback_passed_on()
7060 {
7061 check_ajax_referer( 'viewing-actions', 'security' );
7062
7063 $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
7064
7065 $status = get_post_meta( $post_id, '_status', TRUE );
7066
7067 if ( $status == 'carried_out' )
7068 {
7069 update_post_meta( $post_id, '_feedback_passed_on', 'yes' );
7070
7071 // Add note/comment to viewing
7072 $comment = array(
7073 'note_type' => 'action',
7074 'action' => 'viewing_feedback_passed_on',
7075 );
7076
7077 PH_Comments::insert_note( $post_id, $comment );
7078
7079 wp_send_json_success();
7080 }
7081
7082 wp_send_json_error();
7083 }
7084
7085 public function get_property_viewings_meta_box()
7086 {
7087 $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
7088
7089 $selected_status = '';
7090 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
7091 if ( isset( $_POST['selected_status'] ) && is_string( $_POST['selected_status'] ) )
7092 {
7093 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
7094 $selected_status = ph_clean( wp_unslash( $_POST['selected_status'] ) );
7095 }
7096
7097 include( PH()->plugin_path() . '/includes/admin/views/html-property-viewings-meta-box.php' );
7098
7099 do_action('propertyhive_property_viewings_fields');
7100
7101 // Quit out
7102 die();
7103 }
7104
7105 public function get_contact_viewings_meta_box()
7106 {
7107 $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
7108
7109 $selected_status = '';
7110 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
7111 if ( isset( $_POST['selected_status'] ) && is_string( $_POST['selected_status'] ) )
7112 {
7113 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
7114 $selected_status = ph_clean( wp_unslash( $_POST['selected_status'] ) );
7115 }
7116
7117 include( PH()->plugin_path() . '/includes/admin/views/html-contact-viewings-meta-box.php' );
7118
7119 do_action('propertyhive_contact_viewings_fields');
7120
7121 // Quit out
7122 die();
7123 }
7124
7125 // Offer related functions
7126 public function record_offer_property()
7127 {
7128 check_ajax_referer( 'record-offer', 'security' );
7129
7130 $this->json_headers();
7131
7132 $input = $this->get_offer_input();
7133 $property_id = $this->get_authorized_record_id( 'property_id', 'property' );
7134 foreach ( $input['applicant_ids'] as $applicant_id ) {
7135 if ( 'contact' !== get_post_type( $applicant_id ) || ! current_user_can( 'edit_post', $applicant_id ) ) {
7136 wp_send_json_error( __( 'Invalid applicant or insufficient permissions.', 'propertyhive' ), 403 );
7137 }
7138 }
7139 if ( empty( $input['applicant_ids'] ) && '' !== $input['applicant_name'] && ! current_user_can( get_post_type_object( 'contact' )->cap->create_posts ) ) {
7140 wp_send_json_error( __( 'Insufficient permissions to create contacts.', 'propertyhive' ), 403 );
7141 }
7142 if ($property_id < 1)
7143 {
7144 $return = array('error' => 'No property selected');
7145 echo json_encode( $return );
7146 die();
7147 }
7148
7149 $property = new PH_Property($property_id);
7150
7151 $applicant_contact_ids = array();
7152
7153 // Create applicant record if required
7154 if (empty($input['applicant_ids']) && !empty($input['applicant_name']))
7155 {
7156 // Need to create contact/applicant
7157 $contact_post = array(
7158 'post_title' => $input['applicant_name'],
7159 'post_content' => '',
7160 'post_type' => 'contact',
7161 'post_status' => 'publish',
7162 'comment_status' => 'closed',
7163 'ping_status' => 'closed',
7164 );
7165
7166 // Insert the post into the database
7167 $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
7168
7169 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
7170 {
7171 $return = array('error' => 'Failed to create contact post. Please try again');
7172 echo json_encode( $return );
7173 die();
7174 }
7175
7176 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
7177
7178 $email_address = sanitize_email( $input['applicant_email_address'] );
7179 $telephone_number = $input['applicant_telephone_number'];
7180 update_post_meta( $contact_post_id, '_email_address', wp_slash( $email_address ) );
7181 update_post_meta( $contact_post_id, '_telephone_number', wp_slash( $telephone_number ) );
7182 update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean( ph_clean_telephone_number($telephone_number) ) );
7183
7184 if ( '' !== $input['applicant_address'] )
7185 {
7186 $address = ph_split_address_into_fields( $input['applicant_address'] );
7187
7188 update_post_meta( $contact_post_id, '_address_name_number', wp_slash( $address['address_name_number'] ) );
7189 update_post_meta( $contact_post_id, '_address_street', wp_slash( $address['address_street'] ) );
7190 update_post_meta( $contact_post_id, '_address_two', wp_slash( $address['address_two'] ) );
7191 update_post_meta( $contact_post_id, '_address_three', wp_slash( $address['address_three'] ) );
7192 update_post_meta( $contact_post_id, '_address_four', wp_slash( $address['address_four'] ) );
7193 update_post_meta( $contact_post_id, '_address_postcode', wp_slash( $address['address_postcode'] ) );
7194 update_post_meta( $contact_post_id, '_address_country', get_option( 'propertyhive_default_country', 'GB' ) );
7195 }
7196
7197 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
7198 update_post_meta( $contact_post_id, '_applicant_profile_0', array( 'department' => $property->department, 'send_matching_properties' => '' ) );
7199
7200 $applicant_contact_ids[] = $contact_post_id;
7201 }
7202
7203 if (!empty($input['applicant_ids']) && empty($input['applicant_name']))
7204 {
7205 // This is an existing contact
7206 foreach ( $input['applicant_ids'] as $applicant_id )
7207 {
7208 $applicant_contact_ids[] = (int)$applicant_id;
7209 }
7210 }
7211
7212 $applicant_contact_ids = array_unique($applicant_contact_ids);
7213
7214 if ( empty($applicant_contact_ids) )
7215 {
7216 $return = array('error' => 'No applicant selected, or unable to create applicant record');
7217 echo json_encode( $return );
7218 die();
7219 }
7220
7221 // Loop through contacts and create one offer each
7222 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
7223 foreach ( $applicant_contact_ids as $applicant_contact_id )
7224 {
7225 // Insert offer record
7226 $offer_post = array(
7227 'post_title' => '',
7228 'post_content' => '',
7229 'post_type' => 'offer',
7230 'post_status' => 'publish',
7231 'comment_status' => 'closed',
7232 'ping_status' => 'closed',
7233 );
7234
7235 // Insert the post into the database
7236 $offer_post_id = wp_insert_post( $offer_post );
7237
7238 if ( is_wp_error($offer_post_id) || $offer_post_id == 0 )
7239 {
7240 $return = array('error' => 'Failed to create offer post. Please try again');
7241 echo json_encode( $return );
7242 die();
7243 }
7244
7245 $amount = $input['amount'];
7246
7247 add_post_meta( $offer_post_id, '_offer_date_time', $input['offer_date'] . ' ' . $input['offer_time'] );
7248 add_post_meta( $offer_post_id, '_property_id', $property_id );
7249 add_post_meta( $offer_post_id, '_applicant_contact_id', $applicant_contact_id );
7250 add_post_meta( $offer_post_id, '_amount', $amount );
7251 add_post_meta( $offer_post_id, '_status', 'pending' );
7252
7253 $applicant_solicitor_contact_id = get_post_meta( $applicant_contact_id, '_contact_solicitor_contact_id', TRUE );
7254 if ( !empty($applicant_solicitor_contact_id) )
7255 {
7256 add_post_meta( $offer_post_id, '_applicant_solicitor_contact_id', (int)$applicant_solicitor_contact_id );
7257 }
7258
7259 $owner_contact_ids = get_post_meta($property_id, '_owner_contact_id', TRUE);
7260 if ( !empty($owner_contact_ids) )
7261 {
7262 $owner_contact_ids = is_array( $owner_contact_ids ) ? $owner_contact_ids : array( $owner_contact_ids );
7263 foreach ( $owner_contact_ids as $owner_contact_id )
7264 {
7265 $property_owner_solicitor_contact_id = get_post_meta( (int)$owner_contact_id, '_contact_solicitor_contact_id', TRUE );
7266 if ( !empty($property_owner_solicitor_contact_id) )
7267 {
7268 add_post_meta( $offer_post_id, '_property_owner_solicitor_contact_id', (int)$property_owner_solicitor_contact_id );
7269 }
7270 }
7271 }
7272 }
7273
7274 $applicant_contacts = array();
7275 foreach ( $applicant_contact_ids as $applicant_contact_id )
7276 {
7277 $applicant_contacts[] = array(
7278 'ID' => $applicant_contact_id,
7279 'post_title' => get_the_title($applicant_contact_id),
7280 'edit_link' => get_edit_post_link( $applicant_contact_id, '' ),
7281 );
7282 }
7283
7284 $return = array('success' => array(
7285 'offer' => array(
7286 'ID' => $offer_post_id,
7287 'edit_link' => get_edit_post_link( $offer_post_id, '' ),
7288 ),
7289 'applicant_contacts' => $applicant_contacts,
7290 ));
7291
7292 echo json_encode( $return );
7293
7294 die();
7295 }
7296
7297 public function record_offer_contact()
7298 {
7299 check_ajax_referer( 'record-offer', 'security' );
7300
7301 $this->json_headers();
7302
7303 $input = $this->get_offer_input();
7304 $contact_id = $this->get_authorized_record_id( 'contact_id', 'contact' );
7305 foreach ( $input['property_ids'] as $property_id ) {
7306 if ( 'property' !== get_post_type( $property_id ) || ! current_user_can( 'edit_post', $property_id ) ) {
7307 wp_send_json_error( __( 'Invalid property or insufficient permissions.', 'propertyhive' ), 403 );
7308 }
7309 }
7310 if ($contact_id < 1)
7311 {
7312 $return = array('error' => 'No contact selected');
7313 echo json_encode( $return );
7314 die();
7315 }
7316
7317 if (empty($input['property_ids']))
7318 {
7319 $return = array('error' => 'No property selected');
7320 echo json_encode( $return );
7321 die();
7322 }
7323
7324 // Loop through contacts and create one offer each
7325 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
7326 foreach ( $input['property_ids'] as $property_id )
7327 {
7328 // Insert offer record
7329 $offer_post = array(
7330 'post_title' => '',
7331 'post_content' => '',
7332 'post_type' => 'offer',
7333 'post_status' => 'publish',
7334 'comment_status' => 'closed',
7335 'ping_status' => 'closed',
7336 );
7337
7338 // Insert the post into the database
7339 $offer_post_id = wp_insert_post( $offer_post );
7340
7341 if ( is_wp_error($offer_post_id) || $offer_post_id == 0 )
7342 {
7343 $return = array('error' => 'Failed to create offer post. Please try again');
7344 echo json_encode( $return );
7345 die();
7346 }
7347
7348 $amount = $input['amount'];
7349
7350 add_post_meta( $offer_post_id, '_offer_date_time', $input['offer_date'] . ' ' . $input['offer_time'] );
7351 add_post_meta( $offer_post_id, '_property_id', (int)$property_id );
7352 add_post_meta( $offer_post_id, '_applicant_contact_id', $contact_id );
7353 add_post_meta( $offer_post_id, '_amount', $amount );
7354 add_post_meta( $offer_post_id, '_status', 'pending' );
7355
7356 $applicant_solicitor_contact_id = get_post_meta( $contact_id, '_contact_solicitor_contact_id', TRUE );
7357 if ( !empty($applicant_solicitor_contact_id) )
7358 {
7359 add_post_meta( $offer_post_id, '_applicant_solicitor_contact_id', (int)$applicant_solicitor_contact_id );
7360 }
7361
7362 $owner_contact_ids = get_post_meta($property_id, '_owner_contact_id', TRUE);
7363 if ( !empty($owner_contact_ids) )
7364 {
7365 $owner_contact_ids = is_array( $owner_contact_ids ) ? $owner_contact_ids : array( $owner_contact_ids );
7366 foreach ( $owner_contact_ids as $owner_contact_id )
7367 {
7368 $property_owner_solicitor_contact_id = get_post_meta( (int)$owner_contact_id, '_contact_solicitor_contact_id', TRUE );
7369 if ( !empty($property_owner_solicitor_contact_id) )
7370 {
7371 add_post_meta( $offer_post_id, '_property_owner_solicitor_contact_id', (int)$property_owner_solicitor_contact_id );
7372 }
7373 }
7374 }
7375 }
7376
7377 $properties = array();
7378 foreach ( $input['property_ids'] as $property_id )
7379 {
7380 $properties[] = array(
7381 'ID' => (int)$property_id,
7382 'post_title' => get_the_title((int)$property_id),
7383 'edit_link' => get_edit_post_link( (int)$property_id, '' ),
7384 );
7385 }
7386
7387 $return = array('success' => array(
7388 'offer' => array(
7389 'ID' => $offer_post_id,
7390 'edit_link' => get_edit_post_link( $offer_post_id, '' ),
7391 ),
7392 'properties' => $properties,
7393 ));
7394
7395 echo json_encode( $return );
7396
7397 die();
7398 }
7399
7400 public function get_offer_details_meta_box()
7401 {
7402 global $post;
7403
7404 check_ajax_referer( 'offer-details-meta-box', 'security' );
7405
7406 $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7407
7408 $post = get_post( $post_id );
7409
7410 $offer = new PH_Offer( $post_id );
7411
7412 echo '<div class="propertyhive_meta_box">';
7413
7414 echo '<div class="options_group">';
7415
7416 if ( $offer->status != '' )
7417 {
7418 echo '<p class="form-field">
7419
7420 <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
7421
7422 ' . esc_html(propertyhive_get_status_label( $offer->status )) . '
7423
7424 </p>';
7425 }
7426
7427 $offer_date_time = $offer->offer_date_time;
7428 if ( empty($offer_date_time) )
7429 {
7430 $offer_date_time = gmdate("Y-m-d H:i:s");
7431 }
7432
7433 echo '<p class="form-field offer_date_time_field">
7434
7435 <label for="_offer_date">' . esc_html(__('Offer Date / Time', 'propertyhive')) . '</label>
7436
7437 <input type="date" class="small" name="_offer_date" id="_offer_date" value="' . esc_attr(gmdate("Y-m-d", strtotime($offer_date_time))) . '" placeholder="">
7438 <select id="_offer_time_hours" name="_offer_time_hours" class="select short" style="width:55px">';
7439
7440 if ( empty($offer_date_time) )
7441 {
7442 $value = gmdate("H");
7443 }
7444 else
7445 {
7446 $value = gmdate( "H", strtotime( $offer_date_time ) );
7447 }
7448 for ( $i = 0; $i < 23; ++$i )
7449 {
7450 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
7451 echo '<option value="' . esc_attr($j) . '"';
7452 if ($i == $value) { echo ' selected'; }
7453 echo '>' . esc_html($j) . '</option>';
7454 }
7455
7456 echo '</select>
7457 :
7458 <select id="_offer_time_minutes" name="_offer_time_minutes" class="select short" style="width:55px">';
7459
7460 if ( empty($offer_date_time) )
7461 {
7462 $value = '';
7463 }
7464 else
7465 {
7466 $value = gmdate( "i", strtotime( $offer_date_time ) );
7467 }
7468 for ( $i = 0; $i < 60; $i+=5 )
7469 {
7470 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
7471 echo '<option value="' . esc_attr($j) . '"';
7472 if ($i == $value) { echo ' selected'; }
7473 echo '>' . esc_html($j) . '</option>';
7474 }
7475
7476 echo '</select>
7477
7478 </p>';
7479
7480 $args = array(
7481 'id' => '_amount',
7482 'label' => __( 'Offer Amount', 'propertyhive' ) . ' (&pound;)',
7483 'desc_tip' => false,
7484 'class' => 'short',
7485 'value' => ( is_numeric($offer->amount) ? ph_display_price_field( $offer->amount ) : '' ),
7486 'custom_attributes' => array(
7487 //'style' => 'width:95%; max-width:500px;'
7488 )
7489 );
7490 propertyhive_wp_text_input( $args );
7491
7492 do_action('propertyhive_offer_details_fields');
7493
7494 echo '</div>';
7495
7496 echo '</div>';
7497
7498 die();
7499 }
7500
7501 public function get_offer_actions()
7502 {
7503 check_ajax_referer( 'offer-actions', 'security' );
7504
7505 $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7506
7507 $status = get_post_meta( $post_id, '_status', TRUE );
7508
7509 // Success action panel
7510 echo '<div id="action_panel_success" class="propertyhive_meta_box propertyhive_meta_box_actions" style="display:none;">
7511
7512 <div class="options_group" style="padding-top:8px;">
7513
7514 <div id="success_actions"></div>
7515
7516 <a class="button action-cancel" style="width:100%;" href="#">' . esc_html(__( 'Back To Actions', 'propertyhive' )) . '</a>
7517
7518 </div>
7519
7520 </div>';
7521
7522 do_action( 'propertyhive_admin_offer_action_options', $post_id );
7523 do_action( 'propertyhive_admin_post_action_options', $post_id );
7524
7525 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_offer_actions_meta_box">
7526
7527 <div class="options_group" style="padding-top:8px;">';
7528
7529 $actions = array();
7530
7531 if ( $status == 'pending' )
7532 {
7533 $actions[] = '<a
7534 href="#action_panel_offer_accepted"
7535 class="button button-success offer-action"
7536 style="width:100%; margin-bottom:7px; text-align:center"
7537 >' . wp_kses_post( __('Accept Offer', 'propertyhive') ) . '</a>';
7538 $actions[] = '<a
7539 href="#action_panel_offer_declined"
7540 class="button button-danger offer-action"
7541 style="width:100%; margin-bottom:7px; text-align:center"
7542 >' . wp_kses_post( __('Decline Offer', 'propertyhive') ) . '</a>';
7543 $actions[] = '<a
7544 href="#action_panel_offer_withdrawn"
7545 class="button offer-action"
7546 style="width:100%; margin-bottom:7px; text-align:center"
7547 >' . wp_kses_post( __('Withdraw Offer', 'propertyhive') ) . '</a>';
7548 }
7549
7550 if ( $status == 'accepted' )
7551 {
7552 // See if a sale has this offer id associated with it
7553 $sale_id = get_post_meta( $post_id, '_sale_id', TRUE );
7554 if ( $sale_id != '' && get_post_status($sale_id) != 'publish' )
7555 {
7556 $sale_id = '';
7557 }
7558
7559 if ( $sale_id != '' )
7560 {
7561 $actions[] = '<a
7562 href="' . esc_url(get_edit_post_link( $sale_id, '' )) . '"
7563 class="button"
7564 style="width:100%; margin-bottom:7px; text-align:center"
7565 >' . wp_kses_post( __('View Sale', 'propertyhive') ) . '</a>';
7566 }
7567 else
7568 {
7569 $actions[] = '<a
7570 href="' . esc_url(wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), 'propertyhive-create_sale-' . $post_id, 'create_sale' )) . '"
7571 class="button button-success button-create-sale"
7572 style="width:100%; margin-bottom:7px; text-align:center"
7573 onclick="setTimeout(function() { jQuery(\'.button-create-sale\').attr(\'href\', \'#\'); jQuery(\'.button-create-sale\').attr(\'disabled\', \'disabled\'); jQuery(\'.button-create-sale\').html(\'Creating...\'); }, 50);"
7574 >' . wp_kses_post( __('Create Sale', 'propertyhive') ) . '</a>';
7575 $actions[] = '<a
7576 href="#action_panel_offer_withdrawn"
7577 class="button offer-action"
7578 style="width:100%; margin-bottom:7px; text-align:center"
7579 >' . wp_kses_post( __('Withdraw Offer', 'propertyhive') ) . '</a>';
7580 }
7581 }
7582
7583 if ( $status == 'accepted' || $status == 'declined' || $status == 'withdrawn' )
7584 {
7585 $actions[] = '<a
7586 href="#action_panel_offer_revert_pending"
7587 class="button offer-action"
7588 style="width:100%; margin-bottom:7px; text-align:center"
7589 >' . wp_kses_post( __('Revert To Pending', 'propertyhive') ) . '</a>';
7590 }
7591
7592 $actions = apply_filters( 'propertyhive_admin_offer_actions', $actions, $post_id );
7593 $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
7594
7595 if ( !empty($actions) )
7596 {
7597 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in action URLs and labels are escaped during assembly; preserve trusted PHP action filters and the fixed button handlers.
7598 echo implode("", $actions);
7599 }
7600 else
7601 {
7602 echo '<div style="text-align:center">' . esc_html(__( 'No actions to display', 'propertyhive' )) . '</div>';
7603 }
7604
7605 echo '</div>
7606
7607 </div>';
7608
7609 die();
7610 }
7611
7612 public function offer_accepted()
7613 {
7614 check_ajax_referer( 'offer-actions', 'security' );
7615
7616 $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7617
7618 $status = get_post_meta( $post_id, '_status', TRUE );
7619
7620 if ( $status == 'pending' )
7621 {
7622 update_post_meta( $post_id, '_status', 'accepted' );
7623
7624 // Add note/comment to offer
7625 $comment = array(
7626 'note_type' => 'action',
7627 'action' => 'offer_accepted',
7628 );
7629
7630 PH_Comments::insert_note( $post_id, $comment );
7631
7632 wp_send_json_success();
7633 }
7634
7635 wp_send_json_error();
7636 }
7637
7638 public function offer_declined()
7639 {
7640 check_ajax_referer( 'offer-actions', 'security' );
7641
7642 $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7643
7644 $status = get_post_meta( $post_id, '_status', TRUE );
7645
7646 if ( $status == 'pending' )
7647 {
7648 update_post_meta( $post_id, '_status', 'declined' );
7649
7650 // Add note/comment to offer
7651 $comment = array(
7652 'note_type' => 'action',
7653 'action' => 'offer_declined',
7654 );
7655
7656 PH_Comments::insert_note( $post_id, $comment );
7657
7658 wp_send_json_success();
7659 }
7660
7661 wp_send_json_error();
7662 }
7663
7664 public function offer_withdrawn()
7665 {
7666 check_ajax_referer( 'offer-actions', 'security' );
7667
7668 $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7669
7670 $status = get_post_meta( $post_id, '_status', TRUE );
7671
7672 if ( $status == 'pending' || $status == 'accepted' )
7673 {
7674 update_post_meta( $post_id, '_status', 'withdrawn' );
7675
7676 // Add note/comment to offer
7677 $comment = array(
7678 'note_type' => 'action',
7679 'action' => 'offer_withdrawn',
7680 );
7681
7682 PH_Comments::insert_note( $post_id, $comment );
7683
7684 wp_send_json_success();
7685 }
7686
7687 wp_send_json_error();
7688 }
7689
7690 public function offer_revert_pending()
7691 {
7692 check_ajax_referer( 'offer-actions', 'security' );
7693
7694 $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7695
7696 $status = get_post_meta( $post_id, '_status', TRUE );
7697
7698 if ( $status == 'accepted' || $status == 'declined' || $status == 'withdrawn' )
7699 {
7700 update_post_meta( $post_id, '_status', 'pending' );
7701
7702 // Add note/comment to offer
7703 $comment = array(
7704 'note_type' => 'action',
7705 'action' => 'offer_revert_pending',
7706 );
7707
7708 PH_Comments::insert_note( $post_id, $comment );
7709
7710 wp_send_json_success();
7711 }
7712
7713 wp_send_json_error();
7714 }
7715
7716 public function get_property_offers_meta_box()
7717 {
7718 $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
7719
7720 $selected_status = '';
7721 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
7722 if ( isset( $_POST['selected_status'] ) && is_string( $_POST['selected_status'] ) )
7723 {
7724 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
7725 $selected_status = ph_clean( wp_unslash( $_POST['selected_status'] ) );
7726 }
7727
7728 include( PH()->plugin_path() . '/includes/admin/views/html-property-offers-meta-box.php' );
7729
7730 do_action('propertyhive_property_offers_fields');
7731
7732 // Quit out
7733 die();
7734 }
7735
7736 public function get_contact_offers_meta_box()
7737 {
7738 $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
7739
7740 $selected_status = '';
7741 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
7742 if ( isset( $_POST['selected_status'] ) && is_string( $_POST['selected_status'] ) )
7743 {
7744 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
7745 $selected_status = ph_clean( wp_unslash( $_POST['selected_status'] ) );
7746 }
7747
7748 include( PH()->plugin_path() . '/includes/admin/views/html-contact-offers-meta-box.php' );
7749
7750 do_action('propertyhive_contact_offers_fields');
7751
7752 // Quit out
7753 die();
7754 }
7755
7756 // Sale related functions
7757 public function get_sale_details_meta_box()
7758 {
7759 global $post;
7760
7761 check_ajax_referer( 'sale-details-meta-box', 'security' );
7762
7763 $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7764
7765 $post = get_post( $post_id );
7766
7767 $sale = new PH_Offer( $post_id );
7768
7769 echo '<div class="propertyhive_meta_box">';
7770
7771 echo '<div class="options_group">';
7772
7773 if ( $sale->status != '' )
7774 {
7775 echo '<p class="form-field">
7776
7777 <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
7778
7779 ' . esc_html(propertyhive_get_status_label( $sale->status )) . '
7780
7781 </p>';
7782 }
7783
7784 $sale_date_time = $sale->sale_date_time;
7785 if ( empty($sale_date_time) )
7786 {
7787 $sale_date_time = gmdate("Y-m-d H:i:s");
7788 }
7789
7790 echo '<p class="form-field sale_date_field">
7791
7792 <label for="_sale_date">' . esc_html(__('Sale Date', 'propertyhive')) . '</label>
7793
7794 <input type="date" class="small" name="_sale_date" id="_sale_date" value="' . esc_attr(gmdate("Y-m-d", strtotime($sale_date_time))) . '" placeholder="">
7795
7796 </p>';
7797
7798 $args = array(
7799 'id' => '_amount',
7800 'label' => __( 'Sale Amount', 'propertyhive' ) . ' (&pound;)',
7801 'desc_tip' => false,
7802 'class' => 'short',
7803 'value' => ( is_numeric($sale->amount) ? ph_display_price_field( $sale->amount ) : '' ),
7804 'custom_attributes' => array(
7805 //'style' => 'width:95%; max-width:500px;'
7806 )
7807 );
7808 propertyhive_wp_text_input( $args );
7809
7810 do_action('propertyhive_sale_details_fields');
7811
7812 echo '</div>';
7813
7814 echo '</div>';
7815
7816 die();
7817 }
7818
7819 public function get_sale_actions()
7820 {
7821 check_ajax_referer( 'sale-actions', 'security' );
7822
7823 $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7824
7825 $status = get_post_meta( $post_id, '_status', TRUE );
7826
7827 // Success action panel
7828 echo '<div id="action_panel_success" class="propertyhive_meta_box propertyhive_meta_box_actions" style="display:none;">
7829
7830 <div class="options_group" style="padding-top:8px;">
7831
7832 <div id="success_actions"></div>
7833
7834 <a class="button action-cancel" style="width:100%;" href="#">' . esc_html__( 'Back To Actions', 'propertyhive' ) . '</a>
7835
7836 </div>
7837
7838 </div>';
7839
7840 do_action( 'propertyhive_admin_sale_action_options', $post_id );
7841 do_action( 'propertyhive_admin_post_action_options', $post_id );
7842
7843 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_sale_actions_meta_box">
7844
7845 <div class="options_group" style="padding-top:8px;">';
7846
7847 $actions = array();
7848
7849 if ( $status == 'current' )
7850 {
7851 $actions[] = '<a
7852 href="#action_panel_sale_exchanged"
7853 class="button button-success sale-action"
7854 style="width:100%; margin-bottom:7px; text-align:center"
7855 >' . esc_html(__('Sale Exchanged', 'propertyhive')) . '</a>';
7856
7857 }
7858
7859 if ( $status == 'exchanged' )
7860 {
7861 $actions[] = '<a
7862 href="#action_panel_sale_completed"
7863 class="button button-success sale-action"
7864 style="width:100%; margin-bottom:7px; text-align:center"
7865 >' . esc_html(__('Sale Completed', 'propertyhive')) . '</a>';
7866 }
7867
7868 if ( $status == 'completed' )
7869 {
7870
7871 }
7872
7873 if ( $status == 'current' || $status == 'exchanged' )
7874 {
7875 $actions[] = '<a
7876 href="#action_panel_sale_fallen_through"
7877 class="button sale-action"
7878 style="width:100%; margin-bottom:7px; text-align:center"
7879 >' . esc_html(__('Sale Fallen Through', 'propertyhive')) . '</a>';
7880 }
7881
7882 $actions = apply_filters( 'propertyhive_admin_sale_actions', $actions, $post_id );
7883 $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
7884
7885 if ( !empty($actions) )
7886 {
7887 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in action URLs and labels are escaped during assembly; preserve trusted PHP action filters and the fixed button handlers.
7888 echo implode("", $actions);
7889 }
7890 else
7891 {
7892 echo '<div style="text-align:center">' . esc_html(__( 'No actions to display', 'propertyhive' )) . '</div>';
7893 }
7894
7895 echo '</div>
7896
7897 </div>';
7898
7899 die();
7900 }
7901
7902 public function sale_exchanged()
7903 {
7904 check_ajax_referer( 'sale-actions', 'security' );
7905
7906 $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7907
7908 $status = get_post_meta( $post_id, '_status', TRUE );
7909
7910 if ( $status == 'current' )
7911 {
7912 update_post_meta( $post_id, '_status', 'exchanged' );
7913
7914 // Add note/comment to sale
7915 $comment = array(
7916 'note_type' => 'action',
7917 'action' => 'sale_exchanged',
7918 );
7919
7920 PH_Comments::insert_note( $post_id, $comment );
7921
7922 wp_send_json_success();
7923 }
7924
7925 wp_send_json_error();
7926 }
7927
7928 public function sale_completed()
7929 {
7930 check_ajax_referer( 'sale-actions', 'security' );
7931
7932 $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7933
7934 $status = get_post_meta( $post_id, '_status', TRUE );
7935
7936 if ( $status == 'exchanged' )
7937 {
7938 update_post_meta( $post_id, '_status', 'completed' );
7939
7940 // Add note/comment to sale
7941 $comment = array(
7942 'note_type' => 'action',
7943 'action' => 'sale_completed',
7944 );
7945
7946 PH_Comments::insert_note( $post_id, $comment );
7947
7948 wp_send_json_success();
7949 }
7950
7951 wp_send_json_error();
7952 }
7953
7954 public function sale_fallen_through()
7955 {
7956 check_ajax_referer( 'sale-actions', 'security' );
7957
7958 $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7959
7960 $status = get_post_meta( $post_id, '_status', TRUE );
7961
7962 if ( $status == 'current' || $status == 'exchanged' )
7963 {
7964 update_post_meta( $post_id, '_status', 'fallen_through' );
7965
7966 // Add note/comment to sale
7967 $comment = array(
7968 'note_type' => 'action',
7969 'action' => 'sale_fallen_through',
7970 );
7971
7972 PH_Comments::insert_note( $post_id, $comment );
7973
7974 wp_send_json_success();
7975 }
7976
7977 wp_send_json_error();
7978 }
7979
7980 public function get_property_sales_meta_box()
7981 {
7982 $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
7983
7984 $selected_status = '';
7985 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
7986 if ( isset( $_POST['selected_status'] ) && is_string( $_POST['selected_status'] ) )
7987 {
7988 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
7989 $selected_status = ph_clean( wp_unslash( $_POST['selected_status'] ) );
7990 }
7991
7992 include( PH()->plugin_path() . '/includes/admin/views/html-property-sales-meta-box.php' );
7993
7994 do_action('propertyhive_property_sales_fields');
7995
7996 // Quit out
7997 die();
7998 }
7999
8000 public function get_contact_sales_meta_box()
8001 {
8002 $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
8003
8004 $selected_status = '';
8005 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8006 if ( isset( $_POST['selected_status'] ) && is_string( $_POST['selected_status'] ) )
8007 {
8008 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8009 $selected_status = ph_clean( wp_unslash( $_POST['selected_status'] ) );
8010 }
8011
8012 include( PH()->plugin_path() . '/includes/admin/views/html-contact-sales-meta-box.php' );
8013
8014 do_action('propertyhive_contact_sales_fields');
8015
8016 // Quit out
8017 die();
8018 }
8019
8020 public function get_property_enquiries_meta_box()
8021 {
8022 $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
8023
8024 $selected_status = '';
8025 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8026 if ( isset( $_POST['selected_status'] ) && is_string( $_POST['selected_status'] ) )
8027 {
8028 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8029 $selected_status = ph_clean( wp_unslash( $_POST['selected_status'] ) );
8030 }
8031
8032 include( PH()->plugin_path() . '/includes/admin/views/html-property-enquiries-meta-box.php' );
8033
8034 do_action('propertyhive_property_enquiries_fields');
8035
8036 // Quit out
8037 die();
8038 }
8039
8040 public function get_contact_enquiries_meta_box()
8041 {
8042 $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
8043
8044 $selected_status = '';
8045 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8046 if ( isset( $_POST['selected_status'] ) && is_string( $_POST['selected_status'] ) )
8047 {
8048 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8049 $selected_status = ph_clean( wp_unslash( $_POST['selected_status'] ) );
8050 }
8051
8052 include( PH()->plugin_path() . '/includes/admin/views/html-contact-enquiries-meta-box.php' );
8053
8054 do_action('propertyhive_contact_enquiries_fields');
8055
8056 // Quit out
8057 die();
8058 }
8059
8060 /**
8061 * Add new management key date via ajax
8062 */
8063 public function add_key_date() {
8064 check_ajax_referer( 'propertyhive-add-key-date', 'security' );
8065 $parent_post_id = isset( $_POST['post_id'] ) && is_scalar( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
8066 if ( ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'edit_post', $parent_post_id ) ) {
8067 wp_send_json_error( __( 'Insufficient permissions', 'propertyhive' ), 403 );
8068 }
8069 $parent_post_type = get_post_type( $parent_post_id );
8070 if ( ! in_array( $parent_post_type, array( 'property', 'tenancy' ), true ) ) {
8071 wp_send_json_error( __( 'Invalid parent record.', 'propertyhive' ), 400 );
8072 }
8073 $details = array();
8074 foreach ( array( 'key_date_description', 'key_date_type', 'key_date_due', 'key_date_hours', 'key_date_minutes' ) as $field ) {
8075 if ( ! isset( $_POST[$field] ) || ! is_string( $_POST[$field] ) ) {
8076 wp_send_json_error( __( 'Missing or invalid key date details.', 'propertyhive' ), 400 );
8077 }
8078 $details[$field] = sanitize_text_field( wp_unslash( $_POST[$field] ) );
8079 }
8080 $date_description = $details['key_date_description'];
8081 $date_type_id = absint( $details['key_date_type'] );
8082 $date_due = $details['key_date_due'] . ' ' . $details['key_date_hours'] . ':' . $details['key_date_minutes'];
8083 $parsed_date = DateTime::createFromFormat( '!Y-m-d H:i', $date_due );
8084 $date_type = get_term( $date_type_id, 'management_key_date_type' );
8085 if ( '' === $date_description || ! $parsed_date || $parsed_date->format( 'Y-m-d H:i' ) !== $date_due || ! $date_type || is_wp_error( $date_type ) ) {
8086 wp_send_json_error( __( 'Invalid key date details.', 'propertyhive' ), 400 );
8087 }
8088 $date_notes = isset( $_POST['key_date_notes'] ) && is_string( $_POST['key_date_notes'] ) ? sanitize_textarea_field( wp_unslash( $_POST['key_date_notes'] ) ) : '';
8089 $key_date_post_id = wp_insert_post( wp_slash( array(
8090 'post_title' => $date_description,
8091 'post_content' => '',
8092 'post_type' => 'key_date',
8093 'post_status' => 'publish',
8094 'comment_status'=> 'closed',
8095 'ping_status' => 'closed',
8096 ) ), true );
8097 if ( is_wp_error( $key_date_post_id ) ) {
8098 wp_send_json_error( __( 'Failed to create the key date. Please try again.', 'propertyhive' ), 500 );
8099 }
8100 add_post_meta( $key_date_post_id, '_date_due', $date_due );
8101 add_post_meta( $key_date_post_id, '_key_date_status', 'pending' );
8102 add_post_meta( $key_date_post_id, '_key_date_type_id', $date_type_id );
8103 add_post_meta( $key_date_post_id, '_key_date_notes', wp_slash( $date_notes ) );
8104 if ( 'tenancy' === $parent_post_type ) {
8105 add_post_meta( $key_date_post_id, '_tenancy_id', $parent_post_id );
8106 add_post_meta( $key_date_post_id, '_property_id', absint( get_post_meta( $parent_post_id, '_property_id', true ) ) );
8107 } else {
8108 add_post_meta( $key_date_post_id, '_property_id', $parent_post_id );
8109 }
8110 wp_send_json_success( array( 'id' => $key_date_post_id ) );
8111 }
8112
8113 public function get_management_dates_grid()
8114 {
8115 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8116 $post_id = $this->get_authorized_record_id( 'post_id', array( 'property', 'tenancy' ) );
8117
8118 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- get_management_dates_grid and get_key_dates_quick_edit_row render management-date HTML; check_key_date_recurrence computes and echoes a next date. These callbacks are false events guarded by authorize_admin_ajax and contain no writes. The current add_key_date/save_key_date/delete_key_date mutations are separate methods with local nonce/capability checks.
8119 if ( isset( $_POST['selected_type_id'] ) && is_scalar( $_POST['selected_type_id'] ) )
8120 {
8121 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- get_management_dates_grid and get_key_dates_quick_edit_row render management-date HTML; check_key_date_recurrence computes and echoes a next date. These callbacks are false events guarded by authorize_admin_ajax and contain no writes. The current add_key_date/save_key_date/delete_key_date mutations are separate methods with local nonce/capability checks.
8122 $selected_type_id = (int)$_POST['selected_type_id'];
8123 }
8124
8125 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- get_management_dates_grid and get_key_dates_quick_edit_row render management-date HTML; check_key_date_recurrence computes and echoes a next date. These callbacks are false events guarded by authorize_admin_ajax and contain no writes. The current add_key_date/save_key_date/delete_key_date mutations are separate methods with local nonce/capability checks.
8126 if ( isset( $_POST['selected_status'] ) && is_string( $_POST['selected_status'] ) )
8127 {
8128 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- get_management_dates_grid and get_key_dates_quick_edit_row render management-date HTML; check_key_date_recurrence computes and echoes a next date. These callbacks are false events guarded by authorize_admin_ajax and contain no writes. The current add_key_date/save_key_date/delete_key_date mutations are separate methods with local nonce/capability checks.
8129 $selected_status = ph_clean( wp_unslash( $_POST['selected_status'] ) );
8130 }
8131
8132 include( PH()->plugin_path() . '/includes/admin/views/html-management-dates-meta-box.php' );
8133
8134 // Quit out
8135 die();
8136 }
8137
8138 public function get_key_dates_quick_edit_row()
8139 {
8140 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8141 $post_id = $this->get_authorized_record_id( 'post_id', array( 'tenancy', 'property' ) );
8142
8143 include( PH()->plugin_path() . '/includes/admin/views/html-key-dates-quick-edit.php' );
8144
8145 // Quit out
8146 die();
8147 }
8148
8149 public function check_key_date_recurrence()
8150 {
8151 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8152 $post_id = $this->get_authorized_record_id( 'post_id', 'key_date' );
8153
8154 $next_key_date = '';
8155
8156 $key_date = new PH_Key_Date(get_post($post_id));
8157 $key_date_due = $key_date->date_due();
8158
8159 $key_date_type = $key_date->key_date_type_id();
8160
8161 $recurrence_rules = get_option( 'propertyhive_key_date_type', array() );
8162 $recurrence_rules = is_array( $recurrence_rules ) ? $recurrence_rules : array();
8163
8164 if ( isset($recurrence_rules[$key_date_type]) && isset( $recurrence_rules[$key_date_type]['recurrence_rule'] ) )
8165 {
8166 foreach ( explode(';', $recurrence_rules[$key_date_type]['recurrence_rule']) as $key_value_pair )
8167 {
8168 list($key, $value) = explode('=', $key_value_pair);
8169 $recurrence[strtolower($key)] = $value;
8170 }
8171
8172 if ( isset($recurrence['freq']) && $recurrence['freq'] != 'ONCE' )
8173 {
8174 $interval = isset($recurrence['interval']) ? $recurrence['interval'] : '1';
8175 switch( $recurrence['freq'] )
8176 {
8177 case 'DAILY':
8178 $frequency = 'day';
8179 break;
8180 case 'WEEKLY':
8181 $frequency = 'week';
8182 break;
8183 case 'MONTHLY':
8184 $frequency = 'month';
8185 break;
8186 case 'YEARLY':
8187 $frequency = 'year';
8188 break;
8189 }
8190
8191 if ( isset($frequency) )
8192 {
8193 $next_key_date = date_add($key_date_due, date_interval_create_from_date_string($interval . ' ' . $frequency));
8194 $next_key_date = date_format($next_key_date, 'Y-m-d');
8195 }
8196 }
8197 }
8198
8199 echo esc_html($next_key_date);
8200
8201 // Quit out
8202 die();
8203 }
8204
8205 public function save_key_date()
8206 {
8207 check_ajax_referer( 'save-key-date', 'security' );
8208
8209 $this->json_headers();
8210
8211 if ( ! current_user_can( 'manage_propertyhive' ) )
8212 wp_send_json_error( __( 'You do not have permission to manage key dates', 'propertyhive' ), 403 );
8213
8214 $key_date_post_id = isset( $_POST['post_id'] ) && is_scalar( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
8215 if ( $key_date_post_id < 1 || 'key_date' !== get_post_type( $key_date_post_id ) || ! current_user_can( 'edit_post', $key_date_post_id ) ) {
8216 wp_send_json_error( __( 'Invalid key date or insufficient permissions.', 'propertyhive' ), 403 );
8217 }
8218 $date_input = array();
8219 foreach ( array( 'description', 'due_date_time', 'status', 'type', 'notes' ) as $field ) {
8220 if ( ! isset( $_POST[$field] ) || ! is_string( $_POST[$field] ) ) {
8221 wp_send_json_error( __( 'Missing or invalid key date details.', 'propertyhive' ), 400 );
8222 }
8223 $date_input[$field] = 'notes' === $field ? sanitize_textarea_field( wp_unslash( $_POST[$field] ) ) : sanitize_text_field( wp_unslash( $_POST[$field] ) );
8224 }
8225 $next_key_date = null;
8226 if ( isset( $_POST['next_key_date'] ) ) {
8227 if ( ! is_string( $_POST['next_key_date'] ) ) {
8228 wp_send_json_error( __( 'Invalid next key date.', 'propertyhive' ), 400 );
8229 }
8230 $next_key_date = sanitize_text_field( wp_unslash( $_POST['next_key_date'] ) );
8231 }
8232
8233 $args = array(
8234 'ID' => $key_date_post_id,
8235 'post_title' => $date_input['description'],
8236 );
8237 wp_update_post( wp_slash( $args ) );
8238
8239 update_post_meta( $key_date_post_id, '_date_due', $date_input['due_date_time'] );
8240 update_post_meta( $key_date_post_id, '_key_date_status', $date_input['status'] );
8241 update_post_meta( $key_date_post_id, '_key_date_type_id', absint( $date_input['type'] ) );
8242 update_post_meta( $key_date_post_id, '_key_date_notes', wp_slash( $date_input['notes'] ));
8243
8244 if ( null !== $next_key_date )
8245 {
8246 // Insert next key date record
8247 $next_key_date_post = array(
8248 'post_title' => $date_input['description'],
8249 'post_content' => '',
8250 'post_type' => 'key_date',
8251 'post_status' => 'publish',
8252 'comment_status' => 'closed',
8253 'ping_status' => 'closed',
8254 );
8255
8256 // Insert the post into the database
8257 $next_key_date_post_id = wp_insert_post( wp_slash( $next_key_date_post ) );
8258
8259 if ( is_wp_error($next_key_date_post_id) || $next_key_date_post_id == 0 )
8260 {
8261 $return = array('error' => 'Failed to create next key date post. Please try again');
8262 echo json_encode( $return );
8263 die();
8264 }
8265
8266 add_post_meta( $next_key_date_post_id, '_date_due', $next_key_date );
8267 add_post_meta( $next_key_date_post_id, '_key_date_status', 'pending' );
8268 add_post_meta( $next_key_date_post_id, '_key_date_type_id', absint( $date_input['type'] ) );
8269
8270 if ( metadata_exists('post', $key_date_post_id, '_property_id') ) {
8271 add_post_meta( $next_key_date_post_id, '_property_id', get_post_meta($key_date_post_id, '_property_id', true) );
8272 }
8273
8274 if ( metadata_exists('post', $key_date_post_id, '_tenancy_id') ) {
8275 add_post_meta( $next_key_date_post_id, '_tenancy_id', get_post_meta($key_date_post_id, '_tenancy_id', true) );
8276 }
8277 }
8278
8279 die();
8280 }
8281
8282 public function delete_key_date()
8283 {
8284 check_ajax_referer( 'delete-key-date', 'security' );
8285
8286 $this->json_headers();
8287
8288 if ( ! current_user_can( 'manage_propertyhive' ) )
8289 wp_send_json_error( __( 'You do not have permission to manage key dates', 'propertyhive' ), 403 );
8290
8291 $date_post_id = isset( $_POST['date_post_id'] ) && is_scalar( $_POST['date_post_id'] ) ? absint( $_POST['date_post_id'] ) : 0;
8292 if ( $date_post_id < 1 || 'key_date' !== get_post_type( $date_post_id ) || ! current_user_can( 'delete_post', $date_post_id ) ) {
8293 wp_send_json_error( __( 'Invalid key date or insufficient permissions.', 'propertyhive' ), 403 );
8294 }
8295
8296 wp_delete_post($date_post_id, TRUE);
8297
8298 $return = array('success' => true);
8299 echo json_encode( $return );
8300
8301 die();
8302 }
8303
8304 public function get_property_tenancies_grid()
8305 {
8306 $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
8307
8308 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8309 if ( isset( $_POST['selected_status'] ) && is_string( $_POST['selected_status'] ) )
8310 {
8311 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8312 $selected_status = ph_clean( wp_unslash( $_POST['selected_status'] ) );
8313 }
8314
8315 include( PH()->plugin_path() . '/includes/admin/views/html-property-tenancies-meta-box.php' );
8316
8317 // Quit out
8318 die();
8319 }
8320
8321 public function get_contact_tenancies_grid()
8322 {
8323 $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
8324
8325 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8326 if ( isset( $_POST['selected_status'] ) && is_string( $_POST['selected_status'] ) )
8327 {
8328 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only CRM renderer/calculation; authorize_admin_ajax checks manage_propertyhive before dispatch, and mutations have separate nonce-protected callbacks.
8329 $selected_status = ph_clean( wp_unslash( $_POST['selected_status'] ) );
8330 }
8331
8332 include( PH()->plugin_path() . '/includes/admin/views/html-contact-tenancies-meta-box.php' );
8333
8334 // Quit out
8335 die();
8336 }
8337
8338 public function get_contact_solicitor()
8339 {
8340 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Tenancy grids and get_contact_solicitor only read identifiers/meta and include/echo results. They are false events guarded by authorize_admin_ajax and contain no writes.
8341 $post_id = $this->get_authorized_record_id( 'post_id', array( 'contact', 'property' ) );
8342 switch( get_post_type( $post_id ) )
8343 {
8344 case 'contact':
8345 {
8346 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Tenancy grids and get_contact_solicitor only read identifiers/meta and include/echo results. They are false events guarded by authorize_admin_ajax and contain no writes.
8347 $contact_post_ids = array( $post_id );
8348 break;
8349 }
8350 case 'property':
8351 {
8352 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Tenancy grids and get_contact_solicitor only read identifiers/meta and include/echo results. They are false events guarded by authorize_admin_ajax and contain no writes.
8353 $owner_contact_ids = get_post_meta($post_id, '_owner_contact_id', TRUE);
8354 if ( !empty( $owner_contact_ids ) )
8355 {
8356 if ( !is_array($owner_contact_ids) )
8357 {
8358 $owner_contact_ids = array($owner_contact_ids);
8359 }
8360
8361 $contact_post_ids = $owner_contact_ids;
8362 }
8363 break;
8364 }
8365 }
8366
8367 if ( isset( $contact_post_ids ) )
8368 {
8369 foreach ( $contact_post_ids as $contact_post_id )
8370 {
8371 $solicitor_contact_id = get_post_meta( $contact_post_id, '_contact_solicitor_contact_id', TRUE );
8372 if ( !empty($solicitor_contact_id) )
8373 {
8374 $solicitor_name = get_the_title($solicitor_contact_id);
8375
8376 $solicitor_company_name = get_post_meta( $solicitor_contact_id, '_company_name', TRUE );
8377 if ( !empty($solicitor_company_name) && $solicitor_company_name != $solicitor_name )
8378 {
8379 $solicitor_name .= ' (' . $solicitor_company_name . ')';
8380 }
8381
8382 echo json_encode( array(
8383 'id' => $solicitor_contact_id,
8384 'name' => $solicitor_name,
8385 ) );
8386 break;
8387 }
8388 }
8389 }
8390
8391 // Quit out
8392 die();
8393 }
8394
8395 public function activate_pro_feature()
8396 {
8397 if ( ! isset( $_POST['_ajax_nonce'] ) || ! is_string( $_POST['_ajax_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ajax_nonce'] ) ), 'updates' ) )
8398 {
8399 $return = array(
8400 'errorMessage' => 'Invalid nonce provided'
8401 );
8402 wp_send_json_error($return);
8403 }
8404
8405 if ( ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'install_plugins' ) )
8406 {
8407 $return = array(
8408 'errorMessage' => __( 'Sorry, you are not allowed to manage plugins on this site.', 'propertyhive' )
8409 );
8410 wp_send_json_error( $return );
8411 }
8412
8413 // check plugin status
8414 $slug = isset( $_POST['slug'] ) && is_string( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
8415
8416 $feature = get_ph_pro_feature( $slug );
8417
8418 if ( $feature === false )
8419 {
8420 $return = array(
8421 'errorMessage' => 'Feature not found'
8422 );
8423 wp_send_json_error($return);
8424 }
8425
8426 if ( is_plugin_active( $feature['wordpress_plugin_file'] ) )
8427 {
8428 $return = array(
8429 'errorMessage' => 'Plugin already active'
8430 );
8431 wp_send_json_error($return);
8432 }
8433
8434 $pro = false;
8435 $plans = (isset($feature['plans']) & is_array($feature['plans'])) ? $feature['plans'] : array();
8436 if ( !in_array('free', $plans) )
8437 {
8438 $pro = true;
8439 }
8440
8441 // check it's not a pro feature if they don't have pro enabled
8442 if ( $pro )
8443 {
8444 $valid_license_key = false;
8445
8446 // check it's not a plugin that was installed pre version 2
8447 $pre_pro_add_ons = get_option( 'propertyhive_pre_pro_add_ons', array() );
8448 if ( empty($pre_pro_add_ons) ) { $pre_pro_add_ons = array(); }
8449 foreach ($pre_pro_add_ons as $pre_pro_add_on)
8450 {
8451 if ( $pre_pro_add_on['slug'] == $slug )
8452 {
8453 // Yep. It was installed already and should be allowed to be activated
8454 $valid_license_key = true;
8455 }
8456 }
8457
8458 if ( $valid_license_key === false )
8459 {
8460 // check pro license key valid
8461 if ( PH()->license->is_valid_pro_license_key(true) )
8462 {
8463 $product_id_and_package = PH()->license->get_pro_license_product_id_and_package();
8464
8465 if ( isset($product_id_and_package['success']) && $product_id_and_package['success'] === true )
8466 {
8467 if (
8468 isset($feature['plans']) &&
8469 isset($product_id_and_package['package']) &&
8470 in_array($product_id_and_package['package'], $feature['plans'])
8471 )
8472 {
8473 $valid_license_key = true;
8474 }
8475 else
8476 {
8477 $return = array(
8478 'errorMessage' => 'Trying to activate a feature that\'s not on your chosen plan'
8479 );
8480 wp_send_json_error($return);
8481 }
8482 }
8483 else
8484 {
8485 $return = array(
8486 'errorMessage' => 'License key valid but failed to get package'
8487 );
8488 wp_send_json_error($return);
8489 }
8490 }
8491 else
8492 {
8493 $return = array(
8494 'errorMessage' => 'Trying to activate a PRO feature but no valid PRO license key entered'
8495 );
8496 wp_send_json_error($return);
8497 }
8498 }
8499
8500 if ( $valid_license_key === false )
8501 {
8502 $return = array(
8503 'errorMessage' => 'Trying to activate a PRO feature but no valid PRO license key entered'
8504 );
8505 wp_send_json_error($return);
8506 }
8507 }
8508
8509 if ( !is_dir(WP_PLUGIN_DIR . '/' . $slug) && strpos($feature['download_url'], 'wordpress.org') === false )
8510 {
8511 // not a public WP plugin. Must be hosted privately
8512 if ( !$pro )
8513 {
8514 // It's free, just let them have it
8515 $response = wp_remote_get(
8516 $feature['download_url'],
8517 array(
8518 'timeout' => 60,
8519 'sslverify' => true,
8520 )
8521 );
8522 }
8523 else
8524 {
8525 // Run through server check to ensure only the genuinely lovely humans get this Pro feature
8526 $response = wp_remote_post(
8527 'https://wp-property-hive.com/activate-pro-feature.php',
8528 array(
8529 'timeout' => 60,
8530 'sslverify' => true,
8531 'headers' => array(
8532 'Content-Type' => 'application/json',
8533 'X-PH-License-Key' => get_option( 'propertyhive_pro_license_key', '' ),
8534 'X-PH-License-Type' => PH()->license->get_license_type(),
8535 'X-PH-Instance-Id' => get_option( 'propertyhive_pro_instance_id', '' ),
8536 'X-PH-Plugin-Version' => PH_VERSION,
8537 ),
8538 'body' => wp_json_encode(array(
8539 'wordpress_plugin_file' => $feature['wordpress_plugin_file'],
8540 )),
8541 )
8542 );
8543 }
8544
8545 if ( is_wp_error( $response ) )
8546 {
8547 $return = array(
8548 'errorMessage' => $response->get_error_message()
8549 );
8550 wp_send_json_error($return);
8551 }
8552
8553 if ( !isset($response['body']) )
8554 {
8555 $return = array(
8556 'errorMessage' => 'No response body received'
8557 );
8558 wp_send_json_error($return);
8559 }
8560
8561 $zip_contents = $response['body']; // use the content
8562
8563 if ( empty($zip_contents) )
8564 {
8565 $return = array(
8566 'errorMessage' => 'Failed to obtain plugin'
8567 );
8568 wp_send_json_error($return);
8569 }
8570
8571 if ( ! wp_is_writable( WP_PLUGIN_DIR ) )
8572 {
8573 $return = array(
8574 'errorMessage' => 'Destination directory (' . WP_PLUGIN_DIR . ') for writing plugin temporarily does not exist or is not writable.'
8575 );
8576 wp_send_json_error($return);
8577 }
8578
8579 $tmpfname = wp_tempnam( $slug . '.zip' );
8580 if ( ! $tmpfname ) {
8581 wp_send_json_error( array( 'errorMessage' => __( 'Unable to create a temporary download file.', 'propertyhive' ) ) );
8582 }
8583
8584 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
8585 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
8586 $download_filesystem = new WP_Filesystem_Direct( false );
8587 if ( ! $download_filesystem->put_contents( $tmpfname, $zip_contents, 0600 ) ) {
8588 wp_delete_file( $tmpfname );
8589 wp_send_json_error( array( 'errorMessage' => __( 'The temporary download could not be written completely.', 'propertyhive' ) ) );
8590 }
8591
8592 global $wp_filesystem;
8593 $wp_filesystem = new WP_Filesystem_Direct( false );
8594
8595 if ( !defined( 'FS_CHMOD_FILE' ) ) {
8596 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress Filesystem API constant FS_CHMOD_FILE; it is a core filesystem contract and must retain the framework name.
8597 define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ) );
8598 }
8599 if ( !defined( 'FS_CHMOD_DIR' ) ) {
8600 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress Filesystem API constant FS_CHMOD_DIR; it is a core filesystem contract and must retain the framework name.
8601 define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 | 0755 ) );
8602 }
8603
8604 // file obtained and stored. need to unzip and put into plugins directory
8605 // phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.PluginDirectoryWrite -- Authorized plugin installation: WordPress requires the add-on files in its plugin directory.
8606 $unzipped = unzip_file( $tmpfname, WP_PLUGIN_DIR );
8607 if ( is_wp_error( $unzipped ) )
8608 {
8609 @wp_delete_file($tmpfname);
8610
8611 $return = array(
8612 'errorMessage' => $unzipped->get_error_message()
8613 );
8614 wp_send_json_error($return);
8615 }
8616
8617 @wp_delete_file($tmpfname);
8618
8619 // Need to sort out cache for activate plugin to work
8620 // Taken from WordPress.org docs
8621 $cache_plugins = wp_cache_get( 'plugins', 'plugins' );
8622 if ( !empty( $cache_plugins ) )
8623 {
8624 $new_plugin = array(
8625 'Name' => $slug,
8626 'PluginURI' => '',
8627 'Version' => '',
8628 'Description' => '',
8629 'Author' => '',
8630 'AuthorURI' => '',
8631 'TextDomain' => '',
8632 'DomainPath' => '',
8633 'Network' => '',
8634 'Title' => $slug,
8635 'AuthorName' => '',
8636 );
8637 $cache_plugins[''][$feature['wordpress_plugin_file']] = $new_plugin;
8638 wp_cache_set( 'plugins', $cache_plugins, 'plugins' );
8639 }
8640 }
8641
8642 if ( is_dir(WP_PLUGIN_DIR . '/' . $slug) )
8643 {
8644 // folder already exists. just activate it
8645 $activated = activate_plugin( $feature['wordpress_plugin_file'] );
8646 if ( is_wp_error( $activated ) )
8647 {
8648 $return = array(
8649 'errorMessage' => $activated->get_error_message()
8650 );
8651 wp_send_json_error($return);
8652 }
8653
8654 wp_send_json_success();
8655 }
8656
8657 if ( strpos($feature['download_url'], 'wordpress.org') !== false )
8658 {
8659 // this is a public WP plugin
8660 wp_ajax_install_plugin();
8661 }
8662
8663 wp_send_json_success();
8664 }
8665
8666 public function deactivate_pro_feature()
8667 {
8668 if ( ! isset( $_POST['_ajax_nonce'] ) || ! is_string( $_POST['_ajax_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ajax_nonce'] ) ), 'updates' ) )
8669 {
8670 $return = array(
8671 'errorMessage' => 'Invalid nonce provided'
8672 );
8673 wp_send_json_error($return);
8674 }
8675
8676 if ( ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'install_plugins' ) )
8677 {
8678 $return = array(
8679 'errorMessage' => __( 'Sorry, you are not allowed to manage plugins on this site.', 'propertyhive' )
8680 );
8681 wp_send_json_error( $return );
8682 }
8683
8684 // check plugin is active
8685 $slug = isset( $_POST['slug'] ) && is_string( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
8686
8687 $feature = get_ph_pro_feature( $slug );
8688
8689 if ( false === $feature || ! is_plugin_active( $feature['wordpress_plugin_file'] ) )
8690 {
8691 $return = array(
8692 'errorMessage' => 'Plugin not active'
8693 );
8694 wp_send_json_error($return);
8695 }
8696
8697 deactivate_plugins( array($feature['wordpress_plugin_file']) );
8698
8699 wp_send_json_success();
8700 }
8701 }
8702
8703 new PH_AJAX();
8704