PluginProbe
Property Hive / 2.3.0
Property Hive v2.3.0
2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 1.4.61 All 261 releases
← All changes | includes/class-ph-ajax.php +5014 -1419 1.4.572.3.0 View file →
@@ -1,6 +1,9 @@
1 1 <?php
2 +// phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 +// ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
2 4
5 +
3 6 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
4 7
5 8 /**
6 9 * PropertyHive PH_AJAX
@@ -12,8 +15,9 @@
12 15 * @package PropertyHive/Classes
13 16 * @category Class
14 17 * @author PropertyHive
15 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.
16 20 class PH_AJAX {
17 21
18 22 /**
19 23 * Hook into ajax events
@@ -23,8 +27,12 @@
23 27 // propertyhive_EVENT => nopriv
24 28 $ajax_events = array(
25 29 'add_note' => false,
26 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,
27 35 'search_contacts' => false,
28 36 'search_properties' => false,
29 37 'search_negotiators' => false,
30 38 'load_existing_owner_contact' => false,
@@ -30,19 +38,26 @@
30 38 'load_existing_owner_contact' => false,
31 39 'load_existing_features' => false,
32 40 'make_property_enquiry' => true,
33 41 'create_contact_from_enquiry' => false,
42 + 'merge_contact_records' => false,
34 43
35 44 // Dashboard components
36 45 'get_news' => false,
37 46 'get_viewings_awaiting_applicant_feedback' => false,
38 47 'get_my_upcoming_appointments' => false,
48 + 'get_upcoming_overdue_key_dates' => false,
39 49
40 50 // Property actions
51 + 'check_duplicate_reference_number' => false,
52 + 'osm_geocoding_request' => false,
41 53 'get_property_marketing_statistics_meta_box' => false,
54 + 'get_property_tenancies_grid' => false,
42 55
43 56 // Contact actions
44 57 'create_contact_login' => false,
58 + 'get_contact_tenancies_grid' => false,
59 + 'get_contact_solicitor' => false,
45 60
46 61 // Appraisal actions
47 62 'get_appraisal_details_meta_box' => false,
48 63 'get_appraisal_actions' => false,
@@ -50,8 +65,9 @@
50 65 'appraisal_cancelled' => false,
51 66 'appraisal_won' => false,
52 67 'appraisal_lost_reason' => false,
53 68 'appraisal_instructed' => false,
69 + 'appraisal_email_owner_booking_confirmation' => false,
54 70 'appraisal_revert_pending' => false,
55 71 'appraisal_revert_carried_out' => false,
56 72 'appraisal_revert_won' => false,
57 73
@@ -59,12 +75,18 @@
59 75 'book_viewing_property' => false,
60 76 'book_viewing_contact' => false,
61 77 'get_viewing_details_meta_box' => false,
62 78 'get_viewing_actions' => false,
79 + 'get_viewing_lightbox' => false,
63 80 'viewing_carried_out' => false,
64 81 'viewing_cancelled' => false,
82 + 'viewing_no_show' => false,
65 83 'viewing_email_applicant_booking_confirmation' => false,
66 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,
67 89 'viewing_interested_feedback' => false,
68 90 'viewing_not_interested_feedback' => false,
69 91 'viewing_feedback_not_required' => false,
70 92 'viewing_revert_feedback_pending' => false,
@@ -80,8 +102,9 @@
80 102 'get_offer_actions' => false,
81 103 'get_property_offers_meta_box' => false,
82 104 'offer_accepted' => false,
83 105 'offer_declined' => false,
106 + 'offer_withdrawn' => false,
84 107 'offer_revert_pending' => false,
85 108 'get_contact_offers_meta_box' => false,
86 109
87 110 // Sale actions
@@ -94,21 +117,53 @@
94 117 'offer_declined' => false,
95 118 'get_property_sales_meta_box' => false,
96 119 'get_contact_sales_meta_box' => false,
97 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 +
98 133 'validate_save_contact' => false,
99 134 'applicant_registration' => true,
100 135 'login' => true,
136 + 'lost_password' => true,
137 + 'reset_password' => true,
101 138 'save_account_details' => true,
102 139 'save_account_requirements' => true,
103 140
141 + // Dismissing notices
104 142 'dismiss_notice_leave_review' => false,
143 + 'dismiss_notice_retired_template_assistant' => false,
144 + 'dismiss_notice_demo_data' => false,
145 + 'dismiss_notice_epl' => false,
105 146 'dismiss_notice_missing_search_results' => false,
106 147 'dismiss_notice_missing_google_maps_api_key' => false,
107 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,
108 159 );
109 160
110 - foreach ( $ajax_events as $ajax_event => $nopriv ) {
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 + }
111 166 add_action( 'wp_ajax_propertyhive_' . $ajax_event, array( $this, $ajax_event ) );
112 167
113 168 if ( $nopriv ) {
114 169 add_action( 'wp_ajax_nopriv_propertyhive_' . $ajax_event, array( $this, $ajax_event ) );
@@ -115,8 +170,244 @@
115 170 }
116 171 }
117 172 }
118 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 +
119 410 public function dismiss_notice_leave_review()
120 411 {
121 412 update_option( 'propertyhive_review_prompt_due_timestamp', 0 );
122 413
@@ -123,8 +414,41 @@
123 414 // Quit out
124 415 die();
125 416 }
126 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 +
127 451 public function dismiss_notice_missing_search_results()
128 452 {
129 453 update_option( 'missing_search_results_notice_dismissed', 'yes' );
130 454
@@ -147,8 +471,13 @@
147 471 // Quit out
148 472 die();
149 473 }
150 474
475 + public function dismiss_notice_email_cron_not_running()
476 + {
477 + update_option( 'email_cron_not_running_dismissed', 'yes' );
478 + }
479 +
151 480 /**
152 481 * Output headers for JSON requests
153 482 */
154 483 private function json_headers() {
@@ -154,40 +483,144 @@
154 483 private function json_headers() {
155 484 header( 'Content-Type: application/json; charset=utf-8' );
156 485 }
157 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 +
158 572 public function create_contact_login()
159 573 {
160 574 check_ajax_referer( 'create-login', 'security' );
161 575
162 - $this->json_headers();
163 -
164 - if (empty($_POST['contact_id']))
165 - {
166 - $return = array('error' => 'No contact selected');
167 - echo json_encode( $return );
168 - die();
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 );
169 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 + }
170 586
171 - if (empty($_POST['password']))
587 + if ( empty( $_POST['password'] ) || ! is_string( $_POST['password'] ) )
172 588 {
173 589 $return = array('error' => 'No password entered');
174 - echo json_encode( $return );
175 - die();
590 + wp_send_json( $return );
176 591 }
177 592
178 - $contact = new PH_Contact((int)$_POST['contact_id']);
593 + $contact = new PH_Contact($contact_id);
179 594
595 + $display_name = get_the_title($contact_id);
596 +
180 597 // Create user
181 598 $userdata = array(
182 - 'display_name' => get_the_title((int)$_POST['contact_id']),
599 + 'display_name' => $display_name,
183 600 'user_login' => sanitize_email($contact->email_address),
184 601 'user_email' => sanitize_email($contact->email_address),
185 - 'user_pass' => $_POST['password'],
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'] ),
186 604 'role' => 'property_hive_contact',
187 605 'show_admin_bar_front' => 'false',
188 606 );
189 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 +
190 623 $user_id = wp_insert_user( $userdata );
191 624
192 625 // On success
193 626 if ( ! is_wp_error( $user_id ) )
@@ -192,9 +625,9 @@
192 625 // On success
193 626 if ( ! is_wp_error( $user_id ) )
194 627 {
195 628 // Assign user ID to CPT
196 - add_post_meta( (int)$_POST['contact_id'], '_user_id', $user_id );
629 + add_post_meta( $contact_id, '_user_id', $user_id );
197 630
198 631 $return = array('success' => true);
199 632 }
200 633 else
@@ -201,10 +634,9 @@
201 634 {
202 635 $return = array('error' => 'Failed to create user login');
203 636 }
204 637
205 - echo json_encode( $return );
206 - die();
638 + wp_send_json( $return );
207 639 }
208 640
209 641 /**
210 642 * Login user
@@ -219,18 +651,19 @@
219 651 if ( check_ajax_referer( 'ph_login', 'security', false ) === FALSE )
220 652 {
221 653 $return['errors'][] = 'Invalid nonce';
222 654
223 - $this->json_headers();
224 - echo json_encode( $return );
225 -
226 - // Quit out
227 - die();
655 + wp_send_json( $return );
228 656 }
229 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 + }
230 662 $creds = array(
231 - 'user_login' => ph_clean($_POST['email_address']),
232 - 'user_password' => ph_clean($_POST['password']),
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'] ),
233 666 );
234 667
235 668 $user = wp_signon( apply_filters( 'propertyhive_login_credentials', $creds ), is_ssl() );
236 669
@@ -241,12 +674,13 @@
241 674 else
242 675 {
243 676 // Check has associated contact CPT and is published
244 677 $args = array(
245 - 'post_type' => 'contact',
678 + 'post_type' => apply_filters( 'propertyhive_allowed_login_post_type', array( 'contact' ) ),
246 679 'fields' => 'ids',
247 680 'posts_per_page' => 1,
248 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.
249 683 'meta_query' => array(
250 684 array(
251 685 'key' => '_user_id',
252 686 'value' => $user->ID
@@ -271,16 +705,142 @@
271 705
272 706 wp_reset_postdata();
273 707 }
274 708
275 - $this->json_headers();
276 - echo json_encode( $return );
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 );
277 766
278 - // Quit out
279 - die();
767 + $return['success'] = true;
768 +
769 + wp_send_json( $return );
280 770 }
281 771
282 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 + /**
283 843 * Register applicant
284 844 */
285 845 public function applicant_registration()
286 846 {
@@ -305,8 +865,48 @@
305 865
306 866 // Validate
307 867 $errors = array();
308 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 +
309 909 $form_controls = ph_get_user_details_form_fields();
310 910
311 911 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
312 912
@@ -311,9 +911,9 @@
311 911 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
312 912
313 913 $form_controls_2 = ph_get_applicant_requirements_form_fields();
314 914
315 - $form_controls_2 = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls_2 );
915 + $form_controls_2 = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls_2, false );
316 916
317 917 $form_controls = array_merge( $form_controls, $form_controls_2 );
318 918
319 919 // need to improve this as duplicated in ph-shortcodes.php
@@ -344,9 +944,9 @@
344 944 }
345 945 }
346 946 if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) )
347 947 {
348 - if ( ! is_email( $_POST[$key] ) )
948 + if ( ! is_string( $_POST[$key] ) || ! is_email( wp_unslash( $_POST[$key] ) ) )
349 949 {
350 950 $errors[] = __( 'Invalid email address provided', 'propertyhive' );
351 951 }
352 952 else
@@ -356,12 +956,13 @@
356 956 'post_type' => 'contact',
357 957 'posts_per_page' => 1,
358 958 'fields' => 'ids',
359 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.
360 961 'meta_query' => array(
361 962 array(
362 963 'key' => '_email_address',
363 - 'value' => $_POST[$key]
964 + 'value' => sanitize_email( wp_unslash( $_POST[$key] ) )
364 965 )
365 966 )
366 967 );
367 968
@@ -368,39 +969,81 @@
368 969 $contacts_query = new WP_Query( $args );
369 970
370 971 if ( $contacts_query->have_posts() )
371 972 {
372 - while ( $contacts_query->have_posts() )
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] ) ) ) )
373 979 {
374 - $contacts_query->the_post();
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 + }
375 990
376 - $contact_post_id = get_the_ID();
377 - }
378 - //$errors[] = __( 'This email address is already registered', 'propertyhive' );
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';
379 1014 }
380 1015 else
381 1016 {
382 - if ( email_exists( $_POST[$key] ) )
1017 + if ( isset($response['success']) && $response['success'] == true )
383 1018 {
384 - $errors[] = __( 'This email address is already registered to a user', 'propertyhive' );
1019 +
385 1020 }
1021 + else
1022 + {
1023 + $errors[] = 'Failed hCaptcha validation';
1024 + }
386 1025 }
387 - wp_reset_postdata();
388 1026 }
389 1027 }
390 - if ( $key == 'recaptcha' )
1028 +
1029 + if ( $key == 'turnstile' )
391 1030 {
392 1031 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
393 - $response = isset( $_POST['g-recaptcha-response'] ) ? ph_clean($_POST['g-recaptcha-response']) : '';
1032 + $response = ( isset( $_POST['cf-turnstile-response'] ) && is_string( $_POST['cf-turnstile-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) ) : '';
394 1033
395 - $response = wp_remote_post(
396 - 'https://www.google.com/recaptcha/api/siteverify',
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.
397 1036 array(
398 1037 'method' => 'POST',
1038 + 'headers' => array(
1039 + 'Content-Type' => 'application/x-www-form-urlencoded',
1040 + ),
399 1041 'body' => array( 'secret' => $secret, 'response' => $response ),
400 1042 )
401 1043 );
402 - if ( is_wp_error( $response ) )
1044 +
1045 + if ( is_wp_error( $response ) )
403 1046 {
404 1047 $errors[] = $response->get_error_message();
405 1048 }
406 1049 else
@@ -407,9 +1050,9 @@
407 1050 {
408 1051 $response = json_decode($response['body'], TRUE);
409 1052 if ( $response === FALSE )
410 1053 {
411 - $errors[] = __( 'Error decoding response from reCAPTCHA check', 'propertyhive' );
1054 + $errors[] = 'Error decoding response from turnstile check';
412 1055 }
413 1056 else
414 1057 {
415 1058 if ( isset($response['success']) && $response['success'] == true )
@@ -417,9 +1060,9 @@
417 1060
418 1061 }
419 1062 else
420 1063 {
421 - $errors[] = __( 'Failed reCAPTCHA validation', 'propertyhive' );
1064 + $errors[] = 'Failed turnstile validation';
422 1065 }
423 1066 }
424 1067 }
425 1068 }
@@ -425,9 +1068,9 @@
425 1068 }
426 1069 }
427 1070
428 1071 // Check password and password2 match
429 - if ( isset( $_POST['password'] ) && isset( $_POST['password2'] ) && $_POST['password'] != $_POST['password2'] )
1072 + if ( isset( $_POST['password'] ) && isset( $_POST['password2'] ) && $registration_input['password'] !== $registration_input['password2'] )
430 1073 {
431 1074 $errors[] = __( 'The passwords entered do not match', 'propertyhive' );
432 1075 }
433 1076
@@ -444,9 +1087,9 @@
444 1087 if ( $contact_post_id === FALSE )
445 1088 {
446 1089 // create CPT
447 1090 $contact_post = array(
448 - 'post_title' => ph_clean($_POST['name']),
1091 + 'post_title' => wp_slash( $registration_input['name'] ),
449 1092 'post_content' => '',
450 1093 'post_type' => 'contact',
451 1094 'post_status' => 'publish',
452 1095 'comment_status'=> 'closed',
@@ -460,9 +1103,9 @@
460 1103 {
461 1104 // update CPT
462 1105 $contact_post = array(
463 1106 'ID' => $contact_post_id,
464 - 'post_title' => ph_clean($_POST['name']),
1107 + 'post_title' => wp_slash( $registration_input['name'] ),
465 1108 'post_status' => 'publish',
466 1109 );
467 1110
468 1111 // Insert the post into the database
@@ -479,16 +1122,16 @@
479 1122 }
480 1123 update_post_meta( $contact_post_id, '_forbidden_contact_methods', array_unique($forbidden_contact_methods) );
481 1124
482 1125 // Add post meta (contact details, requirements etc)
483 - update_post_meta( $contact_post_id, '_email_address', sanitize_email($_POST['email_address']) );
1126 + update_post_meta( $contact_post_id, '_email_address', sanitize_email( $registration_input['email_address'] ) );
484 1127
485 1128 $telephone_number = get_post_meta( $contact_post_id, '_telephone_number', TRUE );
486 1129 if ( isset($_POST['telephone_number']) && $_POST['telephone_number'] != '' )
487 1130 {
488 - $telephone_number = $_POST['telephone_number'];
1131 + $telephone_number = $registration_input['telephone_number'];
489 1132 }
490 - update_post_meta( $contact_post_id, '_telephone_number', ph_clean($telephone_number) );
1133 + update_post_meta( $contact_post_id, '_telephone_number', wp_slash( ph_clean($telephone_number) ) );
491 1134 update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean( ph_clean_telephone_number($telephone_number) ) );
492 1135
493 1136 $contact_types = get_post_meta( $contact_post_id, '_contact_types', TRUE );
494 1137 if ( !is_array($contact_types) )
@@ -503,14 +1146,20 @@
503 1146
504 1147 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
505 1148
506 1149 $applicant_profile = array();
507 - $applicant_profile['department'] = $_POST['department'];
1150 + $applicant_profile['department'] = $registration_input['department'];
508 1151
509 - if ( $_POST['department'] == 'residential-sales' )
1152 + $base_department = $registration_input['department'];
1153 + if ( !in_array( $base_department, array('residential-sales', 'residential-lettings', 'commercial') ) )
510 1154 {
511 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_price']));
1155 + $base_department = ph_get_custom_department_based_on($base_department);
1156 + }
512 1157
1158 + if ( $base_department == 'residential-sales' )
1159 + {
1160 + $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_price']);
1161 +
513 1162 $applicant_profile['max_price'] = $price;
514 1163
515 1164 // Not used yet but could be if introducing currencies in the future.
516 1165 $applicant_profile['max_price_actual'] = $price;
@@ -517,11 +1166,11 @@
517 1166
518 1167 $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
519 1168 $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
520 1169
521 - if ( $percentage_lower != '' && $percentage_higher != '' && $_POST['maximum_price'] != '' && $_POST['maximum_price'] != 0 )
1170 + if ( $percentage_lower != '' && $percentage_higher != '' && $registration_input['maximum_price'] != '' && $registration_input['maximum_price'] != 0 )
522 1171 {
523 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_price']));
1172 + $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_price']);
524 1173 $applicant_profile['match_price_range_lower'] = $price - ( $price * ( $percentage_lower / 100 ) );
525 1174 $applicant_profile['match_price_range_lower_actual'] = $price - ( $price * ( $percentage_lower / 100 ) );
526 1175
527 1176 $applicant_profile['match_price_range_higher'] = $price + ( $price * ( $percentage_higher / 100 ) );
@@ -527,11 +1176,11 @@
527 1176 $applicant_profile['match_price_range_higher'] = $price + ( $price * ( $percentage_higher / 100 ) );
528 1177 $applicant_profile['match_price_range_higher_actual'] = $price + ( $price * ( $percentage_higher / 100 ) );
529 1178 }
530 1179 }
531 - elseif ( $_POST['department'] == 'residential-lettings' )
1180 + elseif ( $base_department == 'residential-lettings' )
532 1181 {
533 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_rent']));
1182 + $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_rent']);
534 1183
535 1184 $applicant_profile['max_rent'] = $price;
536 1185 $applicant_profile['rent_frequency'] = 'pcm';
537 1186 $price_actual = $price; // Stored in pcm
@@ -537,70 +1186,92 @@
537 1186 $price_actual = $price; // Stored in pcm
538 1187 $applicant_profile['max_price_actual'] = $price_actual;
539 1188 }
540 1189
541 - if ( $_POST['department'] == 'residential-sales' || $_POST['department'] == 'residential-lettings' )
1190 + if ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' )
542 1191 {
543 - $beds = preg_replace("/[^0-9]/", '', ph_clean($_POST['minimum_bedrooms']));
1192 + $beds = preg_replace("/[^0-9.]/", '', $registration_input['minimum_bedrooms']);
544 1193 $applicant_profile['min_beds'] = $beds;
545 1194
546 1195 if ( isset($_POST['property_type']) && !empty($_POST['property_type']) )
547 1196 {
548 - $applicant_profile['property_types'] = is_array(ph_clean($_POST['property_type'])) ? ph_clean($_POST['property_type']) : array(ph_clean($_POST['property_type']));
1197 + $applicant_profile['property_types'] = $registration_input['property_type'];
549 1198 }
550 1199 }
551 1200
552 - if ( $_POST['department'] == 'commercial' )
1201 + if ( $base_department == 'commercial' )
553 1202 {
554 1203 $available_as = array();
555 - if ( isset($_POST['available_as_sale']) && $_POST['available_as_sale'] == 'yes' )
1204 + if ( isset($_POST['available_as_sale']) && $registration_input['available_as_sale'] == 'yes' )
556 1205 {
557 1206 $available_as[] = 'sale';
558 1207 }
559 - if ( isset($_POST['available_as_rent']) && $_POST['available_as_rent'] == 'yes' )
1208 + if ( isset($_POST['available_as_rent']) && $registration_input['available_as_rent'] == 'yes' )
560 1209 {
561 1210 $available_as[] = 'rent';
562 1211 }
563 1212 $applicant_profile['available_as'] = $available_as;
564 1213
565 - $floor_area = preg_replace("/[^0-9.]/", '', ph_clean($_POST['minimum_floor_area']));
1214 + $floor_area = preg_replace("/[^0-9.]/", '', $registration_input['minimum_floor_area']);
566 1215 $applicant_profile['min_floor_area'] = $floor_area;
567 1216 $applicant_profile['min_floor_area_actual'] = $floor_area;
568 1217
569 - $floor_area = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_floor_area']));
1218 + $floor_area = preg_replace("/[^0-9.]/", '', $registration_input['maximum_floor_area']);
570 1219 $applicant_profile['max_floor_area'] = $floor_area;
571 1220 $applicant_profile['max_floor_area_actual'] = $floor_area;
572 1221
573 1222 if ( isset($_POST['commercial_property_type']) && !empty($_POST['commercial_property_type']) )
574 1223 {
575 - $applicant_profile['commercial_property_types'] = is_array(ph_clean($_POST['commercial_property_type'])) ? ph_clean($_POST['commercial_property_type']) : array(ph_clean($_POST['commercial_property_type']));
1224 + $applicant_profile['commercial_property_types'] = $registration_input['commercial_property_type'];
576 1225 }
577 1226 }
578 1227
579 1228 if ( isset($_POST['location']) && !empty($_POST['location']) )
580 1229 {
581 - $applicant_profile['locations'] = is_array(ph_clean($_POST['location'])) ? ph_clean($_POST['location']) : array(ph_clean($_POST['location']));
1230 + $applicant_profile['locations'] = $registration_input['location'];
582 1231 }
583 1232
584 - $applicant_profile['notes'] = ( ( isset($_POST['additional_requirements']) ) ? sanitize_textarea_field($_POST['additional_requirements']) : '' );
1233 + if ( isset($_POST['location_text']) && !empty($_POST['location_text']) )
1234 + {
1235 + $applicant_profile['location_text'] = $registration_input['location_text'];
1236 + }
585 1237
1238 + $applicant_profile['notes'] = $registration_input['additional_requirements'];
1239 +
586 1240 $applicant_profile['send_matching_properties'] = 'yes';
587 1241 //$applicant_profile['auto_match_disabled'] = ''; // don't know what to do about this yet. Should probably look at global setting and reflect that
588 1242
589 - update_post_meta( $contact_post_id, '_applicant_profile_0', $applicant_profile );
1243 + update_post_meta( $contact_post_id, '_applicant_profile_0', wp_slash( $applicant_profile ) );
590 1244
591 1245 if ( get_option( 'propertyhive_applicant_users', '' ) == 'yes' )
592 1246 {
1247 + $display_name = wp_slash( $registration_input['name'] );
1248 +
593 1249 // Create user
594 1250 $userdata = array(
595 - 'display_name' => ph_clean($_POST['name']),
596 - 'user_login' => sanitize_email($_POST['email_address']),
597 - 'user_email' => sanitize_email($_POST['email_address']),
598 - 'user_pass' => ph_clean($_POST['password']),
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'],
599 1255 'role' => 'property_hive_contact',
600 1256 'show_admin_bar_front' => 'false',
601 1257 );
602 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 +
603 1274 $user_id = wp_insert_user( $userdata );
604 1275
605 1276 //On success
606 1277 if ( ! is_wp_error( $user_id ) )
@@ -647,13 +1318,14 @@
647 1318
648 1319 $return = array(
649 1320 'success' => false,
650 1321 'errors' => array(),
1322 + 'new_details_nonce' => wp_create_nonce( "ph_userdetails" ),
651 1323 );
652 1324
653 1325 // Got an issue with nonce being declined on second submission.
654 1326 // Need to sort before putting this back in
655 - /*if ( check_ajax_referer( 'ph_details', 'security', false ) === FALSE )
1327 + if ( check_ajax_referer( 'ph_userdetails', 'ph_account_details_security', false ) === FALSE )
656 1328 {
657 1329 $return['errors'][] = 'Invalid nonce';
658 1330
659 1331 $this->json_headers();
@@ -660,9 +1332,9 @@
660 1332 echo json_encode( $return );
661 1333
662 1334 // Quit out
663 1335 die();
664 - }*/
1336 + }
665 1337
666 1338 // Validate
667 1339 $errors = array();
668 1340
@@ -680,8 +1352,22 @@
680 1352 // Quit out
681 1353 die();
682 1354 }
683 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 + }
684 1370 $form_controls = ph_get_user_details_form_fields();
685 1371
686 1372 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
687 1373
@@ -696,9 +1382,9 @@
696 1382 }
697 1383 }
698 1384 if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) )
699 1385 {
700 - if ( ! is_email( $_POST[$key] ) )
1386 + if ( ! is_string( $_POST[$key] ) || ! is_email( sanitize_email( wp_unslash( $_POST[$key] ) ) ) )
701 1387 {
702 1388 $errors[] = __( 'Invalid email address provided', 'propertyhive' );
703 1389 }
704 1390
@@ -706,13 +1392,27 @@
706 1392 }
707 1393 }
708 1394
709 1395 // Check password and password2 match
710 - if ( isset( $_POST['password'] ) && isset( $_POST['password2'] ) && !empty( $_POST['password'] ) && $_POST['password'] != $_POST['password2'] )
1396 + if ( isset( $_POST['password'] ) && isset( $_POST['password2'] ) && $account_input['password'] !== '' && $account_input['password'] !== $account_input['password2'] )
711 1397 {
712 1398 $errors[] = __( 'The passwords entered do not match', 'propertyhive' );
713 1399 }
714 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 +
715 1415 if ( !empty($errors) )
716 1416 {
717 1417 // Failed validation
718 1418
@@ -722,46 +1422,52 @@
722 1422 }
723 1423 else
724 1424 {
725 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 + }
726 1431
727 1432 // create CPT
728 1433 $contact_post = array(
729 1434 'ID' => $contact->id,
730 - 'post_title' => ph_clean($_POST['name']),
1435 + 'post_title' => wp_slash( $account_input['name'] ),
731 1436 );
732 1437
733 1438 // Update the post in the database
734 1439 $contact_post_id = wp_update_post( $contact_post );
735 1440
736 - update_post_meta( $contact_post_id, '_email_address', sanitize_email($_POST['email_address']) );
1441 + update_post_meta( $contact_post_id, '_email_address', sanitize_email( $account_input['email_address'] ) );
737 1442 if (isset($_POST['telephone_number']))
738 1443 {
739 - update_post_meta( $contact_post_id, '_telephone_number', ph_clean($_POST['telephone_number']) );
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'] ) );
740 1446 }
741 1447
742 1448 // Update user
743 1449 $userdata = array(
744 1450 'ID' => $user_id,
745 - 'display_name' => ph_clean($_POST['name']),
746 - 'user_email' => sanitize_email($_POST['email_address']),
1451 + 'display_name' => wp_slash( $account_input['name'] ),
1452 + 'user_email' => sanitize_email( $account_input['email_address'] ),
747 1453 );
748 1454
749 1455 if ( isset($_POST['password']) && !empty($_POST['password']) )
750 1456 {
751 - $userdata['user_pass'] = ph_clean($_POST['password']);
1457 + $userdata['user_pass'] = $account_input['password'];
752 1458 }
753 1459
754 1460 $user_id = wp_update_user( $userdata );
755 1461
756 - $user_roles = $current_user->roles;
757 - $user_role = array_shift($user_roles);
758 -
759 - if ( $user_role === 'property_hive_contact' )
1462 + if ( ! is_wp_error( $user_id ) && $user_role === 'property_hive_contact' )
760 1463 {
761 1464 // Have to update login via SQL as wp_update_user won't allow altering
762 1465 // Only do it for property hive contacts though as admin or editor might be viewing this page
763 - $wpdb->update($wpdb->users, array('user_login' => sanitize_email($_POST['email_address'])), array('ID' => $user_id));
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 );
764 1470 }
765 1471
766 1472 //On success
767 1473 if ( ! is_wp_error( $user_id ) )
@@ -797,13 +1503,14 @@
797 1503
798 1504 $return = array(
799 1505 'success' => false,
800 1506 'errors' => array(),
1507 + 'new_requirements_nonce' => wp_create_nonce( "ph_requirements" ),
801 1508 );
802 1509
803 1510 // Got an issue with nonce being declined on second submission.
804 1511 // Need to sort before putting this back in
805 - /*if ( check_ajax_referer( 'ph_requirements', 'security', false ) === FALSE )
1512 + if ( check_ajax_referer( 'ph_requirements', 'ph_account_requirements_security', false ) === FALSE )
806 1513 {
807 1514 $return['errors'][] = 'Invalid nonce';
808 1515
809 1516 $this->json_headers();
@@ -810,9 +1517,9 @@
810 1517 echo json_encode( $return );
811 1518
812 1519 // Quit out
813 1520 die();
814 - }*/
1521 + }
815 1522
816 1523 // Validate
817 1524 $errors = array();
818 1525
@@ -830,11 +1537,52 @@
830 1537 // Quit out
831 1538 die();
832 1539 }
833 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'] );
834 1582 $form_controls = ph_get_applicant_requirements_form_fields();
835 1583
836 - $form_controls = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls );
1584 + $form_controls = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls, get_post_meta( $contact_post_id, '_applicant_profile_' . $profile_id, true ) );
837 1585
838 1586 foreach ( $form_controls as $key => $control )
839 1587 {
840 1588 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
@@ -856,27 +1604,42 @@
856 1604 $return['errors'] = $errors;
857 1605 }
858 1606 else
859 1607 {
860 - $contact = new PH_Contact( '', $user_id );
1608 + $applicant_profile = array();
1609 + $applicant_profile['department'] = $requirements_input['department'];
861 1610
862 - $contact_post_id = $contact->id;
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 + }
863 1616
864 - $applicant_profile = array();
865 - $applicant_profile['department'] = ph_clean($_POST['department']);
866 -
867 - if ( $_POST['department'] == 'residential-sales' )
1617 + if ( $base_department == 'residential-sales' )
868 1618 {
869 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_price']));
1619 + $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_price']);
870 1620
871 1621 $applicant_profile['max_price'] = $price;
872 1622
873 1623 // Not used yet but could be if introducing currencies in the future.
874 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 + }
875 1638 }
876 - elseif ( $_POST['department'] == 'residential-lettings' )
1639 + elseif ( $base_department == 'residential-lettings' )
877 1640 {
878 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_rent']));
1641 + $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_rent']);
879 1642
880 1643 $applicant_profile['max_rent'] = $price;
881 1644 $applicant_profile['rent_frequency'] = 'pcm';
882 1645 $price_actual = $price; // Stored in pcm
@@ -882,57 +1645,62 @@
882 1645 $price_actual = $price; // Stored in pcm
883 1646 $applicant_profile['max_price_actual'] = $price_actual;
884 1647 }
885 1648
886 - if ( $_POST['department'] == 'residential-sales' || $_POST['department'] == 'residential-lettings' )
1649 + if ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' )
887 1650 {
888 - $beds = preg_replace("/[^0-9]/", '', ph_clean($_POST['minimum_bedrooms']));
1651 + $beds = preg_replace("/[^0-9]/", '', $requirements_input['minimum_bedrooms']);
889 1652 $applicant_profile['min_beds'] = $beds;
890 1653
891 1654 if ( isset($_POST['property_type']) && !empty($_POST['property_type']) )
892 1655 {
893 - $applicant_profile['property_types'] = array(ph_clean($_POST['property_type']));
1656 + $applicant_profile['property_types'] = $requirements_input['property_type'];
894 1657 }
895 1658 }
896 1659
897 - if ( $_POST['department'] == 'commercial' )
1660 + if ( $base_department == 'commercial' )
898 1661 {
899 1662 $available_as = array();
900 - if ( isset($_POST['available_as_sale']) && $_POST['available_as_sale'] == 'yes' )
1663 + if ( isset($_POST['available_as_sale']) && $requirements_input['available_as_sale'] == 'yes' )
901 1664 {
902 1665 $available_as[] = 'sale';
903 1666 }
904 - if ( isset($_POST['available_as_rent']) && $_POST['available_as_rent'] == 'yes' )
1667 + if ( isset($_POST['available_as_rent']) && $requirements_input['available_as_rent'] == 'yes' )
905 1668 {
906 1669 $available_as[] = 'rent';
907 1670 }
908 1671 $applicant_profile['available_as'] = $available_as;
909 1672
910 - $floor_area = preg_replace("/[^0-9.]/", '', ph_clean($_POST['minimum_floor_area']));
1673 + $floor_area = preg_replace("/[^0-9.]/", '', $requirements_input['minimum_floor_area']);
911 1674 $applicant_profile['min_floor_area'] = $floor_area;
912 1675 $applicant_profile['min_floor_area_actual'] = $floor_area;
913 1676
914 - $floor_area = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_floor_area']));
1677 + $floor_area = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_floor_area']);
915 1678 $applicant_profile['max_floor_area'] = $floor_area;
916 1679 $applicant_profile['max_floor_area_actual'] = $floor_area;
917 1680
918 1681 if ( isset($_POST['commercial_property_type']) && !empty($_POST['commercial_property_type']) )
919 1682 {
920 - $applicant_profile['commercial_property_types'] = array(ph_clean($_POST['commercial_property_type']));
1683 + $applicant_profile['commercial_property_types'] = $requirements_input['commercial_property_type'];
921 1684 }
922 1685 }
923 1686
924 1687 if ( isset($_POST['location']) && !empty($_POST['location']) )
925 1688 {
926 - $applicant_profile['locations'] = array(ph_clean($_POST['location']));
1689 + $applicant_profile['locations'] = $requirements_input['location'];
927 1690 }
928 1691
929 - $applicant_profile['notes'] = ( ( isset($_POST['additional_requirements']) ) ? sanitize_textarea_field($_POST['additional_requirements']) : '' );
1692 + if ( isset($_POST['location_text']) && !empty($_POST['location_text']) )
1693 + {
1694 + $applicant_profile['location_text'] = $requirements_input['location_text'];
1695 + }
930 1696
1697 + $applicant_profile['notes'] = $requirements_input['additional_requirements'];
1698 +
931 1699 $applicant_profile['send_matching_properties'] = 'yes';
932 1700 //$applicant_profile['auto_match_disabled'] = ''; // don't know what to do about this yet. Should probably look at global setting and reflect that
933 1701
934 - update_post_meta( $contact_post_id, '_applicant_profile_0', $applicant_profile );
1702 + update_post_meta( $contact_post_id, '_applicant_profile_' . $profile_id, wp_slash( $applicant_profile ) );
935 1703
936 1704 $return['success'] = true;
937 1705
938 1706 do_action( 'propertyhive_account_requirements_updated', $contact_post_id, $user_id );
@@ -957,10 +1725,11 @@
957 1725 $return = array();
958 1726
959 1727 $property_query = new WP_Query(array(
960 1728 'post_type' => 'property',
961 - 'post_status' => 'any',
962 - 'nopaging' => true
1729 + 'post_status' => 'publish',
1730 + 'nopaging' => true,
1731 + 'fields' => 'ids',
963 1732 ));
964 1733
965 1734 if ($property_query->have_posts())
966 1735 {
@@ -967,14 +1736,14 @@
967 1736 while ($property_query->have_posts())
968 1737 {
969 1738 $property_query->the_post();
970 1739
971 - $num_property_features = get_post_meta($post->ID, '_features', TRUE);
1740 + $num_property_features = get_post_meta(get_the_ID(), '_features', TRUE);
972 1741 if ($num_property_features == '') { $num_property_features = 0; }
973 1742
974 1743 for ($i = 0; $i < $num_property_features; ++$i)
975 1744 {
976 - $feature = get_post_meta($post->ID, '_feature_' . $i, TRUE);
1745 + $feature = get_post_meta(get_the_ID(), '_feature_' . $i, TRUE);
977 1746 if (!in_array($feature, $return) && trim($feature) != '')
978 1747 {
979 1748 $return[] = $feature;
980 1749 }
@@ -994,19 +1763,19 @@
994 1763 public function load_existing_owner_contact() {
995 1764
996 1765 check_ajax_referer( 'load-existing-owner-contact', 'security' );
997 1766
998 - $contact_id = (int)$_POST['contact_id'];
1767 + $contact_id = isset( $_POST['contact_id'] ) && is_scalar( $_POST['contact_id'] ) ? absint( $_POST['contact_id'] ) : 0;
999 1768
1000 - $contact = get_post($contact_id);
1769 + $contact = $contact_id > 0 && 'contact' === get_post_type( $contact_id ) ? get_post( $contact_id ) : null;
1001 1770
1002 - echo '<div id="existing-owner-details-' . $contact_id . '">';
1771 + echo '<div id="existing-owner-details-' . esc_attr($contact_id) . '">';
1003 1772
1004 1773 if ( !is_null( $contact ) )
1005 1774 {
1006 1775 echo '<p class="form-field">';
1007 - echo '<label>' . __('Name', 'propertyhive') . '</label>';
1008 - echo '<a href="' . get_edit_post_link( $contact_id ) . '">' . get_the_title($contact_id) . '</a>';
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>';
1009 1778 echo '</p>';
1010 1779
1011 1780 $address = array();
1012 1781 $address_elements = array( '_address_name_number', '_address_street', '_address_two', '_address_three', '_address_four', '_address_postcode' );
@@ -1018,30 +1787,42 @@
1018 1787 }
1019 1788 }
1020 1789
1021 1790 echo '<p class="form-field">';
1022 - echo '<label>' . __('Address', 'propertyhive') . '</label>';
1023 - echo ( ( !empty($address) ) ? implode(", ", $address) : '-' );
1791 + echo '<label>' . esc_html(__('Address', 'propertyhive')) . '</label>';
1792 + echo ( ( !empty($address) ) ? esc_html(implode(", ", $address)) : '-' );
1024 1793 echo '</p>';
1025 1794
1026 1795 echo '<p class="form-field">';
1027 - echo '<label>' . __('Telephone Number', 'propertyhive') . '</label>';
1028 - echo ( ( get_post_meta($contact_id, '_telephone_number', TRUE) != '' ) ? get_post_meta($contact_id, '_telephone_number', TRUE) : '-' );
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)) : '-' );
1029 1798 echo '</p>';
1030 1799
1031 1800 echo '<p class="form-field">';
1032 - echo '<label>' . __('Email Address', 'propertyhive') . '</label>';
1033 - echo ( ( get_post_meta($contact_id, '_email_address', TRUE) != '' ) ? get_post_meta($contact_id, '_email_address', TRUE) : '-' );
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)) : '-' );
1034 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 + }
1035 1816 }
1036 1817 else
1037 1818 {
1038 - echo __( 'Invalid contact record', 'propertyhive' );
1819 + echo esc_html(__( 'Invalid contact record', 'propertyhive' ));
1039 1820 }
1040 1821
1041 1822 echo '<p class="form-field">';
1042 1823 echo '<label></label>';
1043 - echo '<a href="" class="button" id="remove-owner-contact-' . $contact_id . '">Remove Owner</a> ';
1824 + echo '<a href="" class="button" id="remove-owner-contact-' . esc_attr($contact_id) . '">Remove Owner</a> ';
1044 1825 echo '<a href="" class="button add-additional-owner-contact">Add Additional Owner</a>';
1045 1826 echo '</p>';
1046 1827
1047 1828 echo '</div>';
@@ -1061,9 +1842,11 @@
1061 1842 check_ajax_referer( 'search-contacts', 'security' );
1062 1843
1063 1844 $return = array();
1064 1845
1065 - $keyword = ph_clean($_POST['keyword']);
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'] ) ) : '';
1066 1849
1067 1850 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1068 1851 {
1069 1852 // Get all contacts that match the name
@@ -1068,22 +1851,29 @@
1068 1851 {
1069 1852 // Get all contacts that match the name
1070 1853 $args = array(
1071 1854 'post_type' => 'contact',
1855 + 'propertyhive_contact_search_keyword' => $keyword,
1072 1856 'nopaging' => true,
1073 - 'post_status' => array( 'publish' ),
1857 + 'post_status' => array( 'publish', 'private' ),
1074 1858 'fields' => 'ids'
1075 1859 );
1076 - if ( isset($_POST['contact_type']) && $_POST['contact_type'] != '' )
1860 + if ( '' !== $contact_type )
1077 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.
1078 1863 $args['meta_query'] = array(
1079 1864 array(
1080 1865 'key' => '_contact_types',
1081 - 'value' => ph_clean($_POST['contact_type']),
1866 + 'value' => $contact_type,
1082 1867 'compare' => 'LIKE',
1083 1868 )
1084 1869 );
1085 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 + }
1086 1876
1087 1877 add_filter( 'posts_where', array( $this, 'search_contacts_where' ), 10, 2 );
1088 1878
1089 1879 $contact_query = new WP_Query( $args );
@@ -1099,9 +1889,9 @@
1099 1889 $contact = new PH_Contact( get_the_ID() );
1100 1890
1101 1891 $return[] = array(
1102 1892 'ID' => get_the_ID(),
1103 - 'post_title' => get_the_title(get_the_ID()) . ( isset($_POST['contact_type']) && $_POST['contact_type'] == 'thirdparty' && $contact->company_name != '' && $contact->company_name != get_the_title(get_the_ID()) ? ' (' . $contact->company_name . ')' : '' ) ,
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 . ')' : '' ) ,
1104 1894 'address_name_number' => $contact->_address_name_number,
1105 1895 'address_street' => $contact->_address_street,
1106 1896 'address_two' => $contact->_address_two,
1107 1897 'address_three' => $contact->_address_three,
@@ -1107,9 +1897,11 @@
1107 1897 'address_three' => $contact->_address_three,
1108 1898 'address_four' => $contact->_address_four,
1109 1899 'address_postcode' => $contact->_address_postcode,
1110 1900 'address_country' => $contact->_address_country,
1111 - 'address_full_formatted' => $contact->get_formatted_full_address('<br>'),
1901 + 'address_full_formatted' => $contact->get_formatted_full_address(', '),
1902 + 'telephone_number' => $contact->_telephone_number,
1903 + 'email_address' => $contact->_email_address,
1112 1904 );
1113 1905 }
1114 1906 }
1115 1907
@@ -1126,10 +1918,14 @@
1126 1918 public function search_contacts_where( $where, $wp_query )
1127 1919 {
1128 1920 global $wpdb;
1129 1921
1130 - $where .= ' AND ' . $wpdb->posts . '.post_title LIKE \'%' . esc_sql( $wpdb->esc_like( ph_clean($_POST['keyword']) ) ) . '%\'';
1131 -
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 +
1132 1928 return $where;
1133 1929 }
1134 1930
1135 1931 /**
@@ -1142,9 +1938,9 @@
1142 1938 check_ajax_referer( 'search-properties', 'security' );
1143 1939
1144 1940 $return = array();
1145 1941
1146 - $keyword = ph_clean($_POST['keyword']);
1942 + $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
1147 1943
1148 1944 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1149 1945 {
1150 1946 // Get all contacts that match the name
@@ -1150,33 +1946,78 @@
1150 1946 // Get all contacts that match the name
1151 1947 $args = array(
1152 1948 'post_type' => 'property',
1153 1949 'nopaging' => true,
1154 - 'post_status' => array( 'publish' ),
1950 + 'post_status' => array( 'publish', 'draft', 'private' ),
1155 1951 'fields' => 'ids'
1156 1952 );
1157 1953
1158 - $meta_query = array();
1159 - if ( isset($_POST['department']) && $_POST['department'] != '' )
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 )
1160 1972 {
1161 - $meta_query[] = array(
1162 - 'key' => '_department',
1163 - 'value' => ph_clean($_POST['department']),
1973 + $departments_query = array(
1974 + 'relation' => 'OR',
1164 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;
1165 2010 }
2011 +
1166 2012 if ( !empty($meta_query) )
1167 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.
1168 2015 $args['meta_query'] = $meta_query;
1169 2016 }
1170 2017
1171 - add_filter( 'posts_join', array( $this, 'search_properties_join' ), 10, 2 );
1172 - add_filter( 'posts_where', array( $this, 'search_properties_where' ), 10, 2 );
1173 -
1174 2018 $property_query = new WP_Query( $args );
1175 2019
1176 - remove_filter( 'posts_join', array( $this, 'search_properties_join' ) );
1177 - remove_filter( 'posts_where', array( $this, 'search_properties_where' ) );
1178 -
1179 2020 if ( $property_query->have_posts() )
1180 2021 {
1181 2022 while ( $property_query->have_posts() )
1182 2023 {
@@ -1193,12 +2034,18 @@
1193 2034 $owner_id = reset($owner_id);
1194 2035 }
1195 2036 $owner_name = get_the_title($owner_id);
1196 2037 }
2038 +
2039 + $post_title = $property->get_formatted_full_address();
2040 + if ( get_post_status() == 'draft' )
2041 + {
2042 + $post_title .= ' - Draft';
2043 + }
1197 2044
1198 2045 $return[] = array(
1199 2046 'ID' => get_the_ID(),
1200 - 'post_title' => $property->get_formatted_full_address(),
2047 + 'post_title' => $post_title,
1201 2048 'owner_id' => $owner_id,
1202 2049 'owner_name' => $owner_name
1203 2050 );
1204 2051 }
@@ -1213,39 +2060,8 @@
1213 2060 // Quit out
1214 2061 die();
1215 2062 }
1216 2063
1217 - public function search_properties_join( $joins )
1218 - {
1219 - global $wpdb;
1220 -
1221 - $joins .= " INNER JOIN {$wpdb->postmeta} AS mt1 ON {$wpdb->posts}.ID = mt1.post_id ";
1222 -
1223 - return $joins;
1224 - }
1225 -
1226 - public function search_properties_where( $where )
1227 - {
1228 - global $wpdb;
1229 - $where .= " AND (
1230 - (mt1.meta_key='_address_name_number' AND mt1.meta_value LIKE '" . esc_sql( $wpdb->esc_like( ph_clean($_POST['keyword']) ) ) . "%')
1231 - OR
1232 - (mt1.meta_key='_address_street' AND mt1.meta_value LIKE '" . esc_sql( $wpdb->esc_like( ph_clean($_POST['keyword']) ) ) . "%')
1233 - OR
1234 - (mt1.meta_key='_address_2' AND mt1.meta_value LIKE '" . esc_sql( $wpdb->esc_like( ph_clean($_POST['keyword']) ) ) . "%')
1235 - OR
1236 - (mt1.meta_key='_address_3' AND mt1.meta_value LIKE '" . esc_sql( $wpdb->esc_like( ph_clean($_POST['keyword']) ) ) . "%')
1237 - OR
1238 - (mt1.meta_key='_address_4' AND mt1.meta_value LIKE '" . esc_sql( $wpdb->esc_like( ph_clean($_POST['keyword']) ) ) . "%')
1239 - OR
1240 - (mt1.meta_key='_address_postcode' AND mt1.meta_value LIKE '" . esc_sql( $wpdb->esc_like( ph_clean($_POST['keyword']) ) ) . "%')
1241 - OR
1242 - (mt1.meta_key='_reference_number' AND mt1.meta_value = '" . esc_sql(ph_clean($_POST['keyword'])) . "')
1243 - ) ";
1244 -
1245 - return $where;
1246 - }
1247 -
1248 2064 /**
1249 2065 * Search users/negotiators via ajax
1250 2066 */
1251 2067 public function search_negotiators() {
@@ -1255,9 +2071,9 @@
1255 2071 check_ajax_referer( 'search-negotiators', 'security' );
1256 2072
1257 2073 $return = array();
1258 2074
1259 - $keyword = ph_clean($_POST['keyword']);
2075 + $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
1260 2076
1261 2077 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1262 2078 {
1263 2079 // Get all contacts that match the name
@@ -1264,10 +2080,13 @@
1264 2080 $args = array(
1265 2081 'number' => 9999,
1266 2082 'search' => $keyword . '*',
1267 2083 'orderby' => 'display_name',
1268 - 'role__not_in' => array('property_hive_contact')
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') )
1269 2086 );
2087 +
2088 + $args = apply_filters( 'propertyhive_negotiators_query', $args );
1270 2089
1271 2090 $user_query = new WP_User_Query( $args );
1272 2091
1273 2092 // Get the results
@@ -1299,15 +2118,34 @@
1299 2118 */
1300 2119 public function add_note() {
1301 2120
1302 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 );
1303 2125
1304 - $post_id = (int)$_POST['post_id'];
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 + }
1305 2130
1306 2131 if ( $post_id > 0 ) {
1307 2132
1308 - $note = wp_kses_post( trim( stripslashes( $_POST['note'] ) ) );
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'] ) );
1309 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 +
1310 2148 // Add note/comment to property
1311 2149 $comment = array(
1312 2150 'note_type' => 'note',
1313 2151 'note' => $note
@@ -1312,8 +2150,13 @@
1312 2150 'note_type' => 'note',
1313 2151 'note' => $note
1314 2152 );
1315 2153
2154 + if ( isset($_POST['pinned']) )
2155 + {
2156 + $comment['pinned'] = '1';
2157 + }
2158 +
1316 2159 $comment_id = PH_Comments::insert_note( $post_id, $comment );
1317 2160
1318 2161 if ($comment_id !== FALSE)
1319 2162 {
@@ -1320,14 +2163,14 @@
1320 2163 $comment = get_comment($comment_id);
1321 2164 ?>
1322 2165 <li rel="<?php echo absint( $comment_id ) ; ?>" class="note">
1323 2166 <div class="note_content">
1324 - <?php echo wpautop( wptexturize( wp_kses_post( $note ) ) ); ?>
2167 + <?php echo wp_kses_post( wpautop( wptexturize( wp_kses_post( $note ) ) ) ); ?>
1325 2168 </div>
1326 2169 <p class="meta">
1327 - <abbr class="exact-date" title="<?php echo $comment->comment_date_gmt; ?> GMT"><?php printf( __( '%s ago', 'propertyhive' ), human_time_diff( strtotime( $comment->comment_date_gmt ), current_time( 'timestamp', 1 ) ) ); ?></abbr>
1328 - <?php if ( $comment->comment_author !== __( 'Property Hive', 'propertyhive' ) ) printf( ' ' . __( 'by %s', 'propertyhive' ), $comment->comment_author ); ?>
1329 - <a href="#" class="delete_note"><?php _e( 'Delete', 'propertyhive' ); ?></a>
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>
1330 2173 </p>
1331 2174 </li>
1332 2175 <?php
1333 2176 }
@@ -1334,9 +2177,9 @@
1334 2177 }
1335 2178
1336 2179 // Quit out
1337 2180 die();
1338 - }
2181 + }
1339 2182
1340 2183 /**
1341 2184 * Delete order note via ajax
1342 2185 */
@@ -1343,19 +2186,229 @@
1343 2186 public function delete_note() {
1344 2187
1345 2188 check_ajax_referer( 'delete-note', 'security' );
1346 2189
1347 - $note_id = (int)$_POST['note_id'];
2190 + if ( ! current_user_can( 'manage_propertyhive' ) )
2191 + wp_send_json_error( __( 'You do not have permission to manage notes', 'propertyhive' ), 403 );
1348 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 +
1349 2199 if ( $note_id > 0 ) {
1350 2200 wp_delete_comment( $note_id );
2201 +
2202 + wp_send_json_success();
1351 2203 }
1352 2204
1353 - // Quit out
1354 - die();
2205 + wp_send_json_error();
1355 2206 }
1356 -
2207 +
1357 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 + /**
1358 2411 * Delete order note via ajax
1359 2412 */
1360 2413 public function make_property_enquiry() {
1361 2414
@@ -1366,11 +2419,12 @@
1366 2419 // Validate
1367 2420 $errors = array();
1368 2421 $form_controls = array();
1369 2422
1370 - if ( ! isset( $_POST['property_id'] ) || ( isset( $_POST['property_id'] ) && empty( $_POST['property_id'] ) ) )
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'] ) )
1371 2425 {
1372 - $errors[] = __( 'Property ID is a required field and must be supplied when making an enquiry', 'propertyhive' ) . ': ' . $key;
2426 + $errors[] = __( 'Property ID is a required field and must be supplied when making an enquiry', 'propertyhive' );
1373 2427 }
1374 2428 else
1375 2429 {
1376 2430 //$post = get_post((int)$_POST['property_id']);
@@ -1376,9 +2430,10 @@
1376 2430 //$post = get_post((int)$_POST['property_id']);
1377 2431
1378 2432 $form_controls = ph_get_property_enquiry_form_fields();
1379 2433
1380 - $form_controls = apply_filters( 'propertyhive_property_enquiry_form_fields', $form_controls );
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'] ) ) );
1381 2436 }
1382 2437
1383 2438 foreach ( $form_controls as $key => $control )
1384 2439 {
@@ -1384,30 +2439,38 @@
1384 2439 {
1385 2440 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
1386 2441 {
1387 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.
1388 2444 if ( ! isset( $_POST[$key] ) || ( isset( $_POST[$key] ) && empty( $_POST[$key] ) ) )
1389 2445 {
1390 2446 $errors[] = __( 'Missing required field', 'propertyhive' ) . ': ' . $key;
1391 2447 }
1392 2448 }
1393 - if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) && ! is_email( $_POST[$key] ) )
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] ) ) ) )
1394 2451 {
1395 2452 $errors[] = __( 'Invalid email address provided', 'propertyhive' ) . ': ' . $key;
1396 2453 }
1397 - if ( $key == 'recaptcha' )
2454 + if ( in_array( $key, array('recaptcha', 'recaptcha-v3') ) )
1398 2455 {
2456 + $errors = $this->check_recaptcha_form_response($errors, $key, $control);
2457 + }
2458 + if ( $key == 'hCaptcha' )
2459 + {
1399 2460 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
1400 - $response = isset( $_POST['g-recaptcha-response'] ) ? ph_clean($_POST['g-recaptcha-response']) : '';
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'] ) ) : '';
1401 2463
1402 - $response = wp_remote_post(
1403 - 'https://www.google.com/recaptcha/api/siteverify',
2464 + $response = wp_remote_post(
2465 + 'https://hcaptcha.com/siteverify',
1404 2466 array(
1405 2467 'method' => 'POST',
1406 2468 'body' => array( 'secret' => $secret, 'response' => $response ),
1407 2469 )
1408 2470 );
1409 - if ( is_wp_error( $response ) )
2471 +
2472 + if ( is_wp_error( $response ) )
1410 2473 {
1411 2474 $errors[] = $response->get_error_message();
1412 2475 }
1413 2476 else
@@ -1414,9 +2477,9 @@
1414 2477 {
1415 2478 $response = json_decode($response['body'], TRUE);
1416 2479 if ( $response === FALSE )
1417 2480 {
1418 - $errors[] = 'Error decoding response from reCAPTCHA check';
2481 + $errors[] = __( 'Error decoding response from hCaptcha check', 'propertyhive' );
1419 2482 }
1420 2483 else
1421 2484 {
1422 2485 if ( isset($response['success']) && $response['success'] == true )
@@ -1424,15 +2487,123 @@
1424 2487
1425 2488 }
1426 2489 else
1427 2490 {
1428 - $errors[] = 'Failed reCAPTCHA validation';
2491 + $errors[] = __( 'Failed hCaptcha validation', 'propertyhive' );
1429 2492 }
1430 2493 }
1431 2494 }
1432 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 + }
1433 2537 }
1434 -
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 +
1435 2606 if ( !empty($errors) )
1436 2607 {
1437 2608 // Failed validation
1438 2609
@@ -1441,11 +2612,8 @@
1441 2612 $return['errors'] = $errors;
1442 2613 }
1443 2614 else
1444 2615 {
1445 - // Passed validation
1446 - $property_ids = explode("|", ph_clean($_POST['property_id']));
1447 -
1448 2616 // Get recipient email address
1449 2617 $to = '';
1450 2618
1451 2619 // Try and get office's email address first, else fallback to admin email
@@ -1479,8 +2647,16 @@
1479 2647 $fields_to_check[] = '_office_email_address_lettings';
1480 2648 $fields_to_check[] = '_office_email_address_sales';
1481 2649 break;
1482 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 + }
1483 2659 }
1484 2660
1485 2661 foreach ( $fields_to_check as $field_to_check )
1486 2662 {
@@ -1495,9 +2671,9 @@
1495 2671 if ( $to == '' )
1496 2672 {
1497 2673 $to = get_option( 'admin_email' );
1498 2674 }
1499 -
2675 +
1500 2676 if ( count($property_ids) == 1 )
1501 2677 {
1502 2678 $subject = __( 'New Property Enquiry', 'propertyhive' ) . ': ' . get_the_title( (int)$property_ids[0] );
1503 2679 }
@@ -1511,11 +2687,11 @@
1511 2687
1512 2688 $message .= ( count($property_ids) > 1 ? __( 'Properties', 'propertyhive' ) : __( 'Property', 'propertyhive' ) ) . ":\n";
1513 2689 foreach ( $property_ids as $property_id )
1514 2690 {
1515 - $message .= apply_filters( 'propertyhive_property_enquiry_property_output', get_the_title( (int)$property_id ) . " (" . get_permalink( (int)$property_id ) . ")", (int)$property_id ) . "\n";
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";
1516 2693 }
1517 - $message .= "\n";
1518 2694
1519 2695 unset($form_controls['action']);
1520 2696 unset($_POST['action']);
1521 2697 unset($form_controls['property_id']); // Unset so the field doesn't get shown in the enquiry details
@@ -1523,17 +2699,31 @@
1523 2699 $form_controls = apply_filters( 'propertyhive_property_enquiry_body_form_fields', $form_controls );
1524 2700
1525 2701 foreach ($form_controls as $key => $control)
1526 2702 {
1527 - if ( isset($control['type']) && $control['type'] == 'html' ) { continue; }
2703 + if ( isset($control['type']) && in_array($control['type'], array('html', 'recaptcha', 'recaptcha-v3', 'hCaptcha', 'turnstile')) ) { continue; }
1528 2704
1529 2705 $label = ( isset($control['label']) ) ? $control['label'] : $key;
1530 2706 $label = ( isset($control['email_label']) ) ? $control['email_label'] : $label;
1531 - $value = ( isset($_POST[$key]) ) ? sanitize_textarea_field($_POST[$key]) : '';
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] ) ) : '';
1532 2709
1533 - $message .= strip_tags($label) . ": " . strip_tags($value) . "\n";
2710 + $message .= wp_strip_all_tags($label) . ": " . wp_strip_all_tags($value) . "\n";
1534 2711 }
1535 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 +
1536 2726 $message = apply_filters( 'propertyhive_property_enquiry_post_body', $message, $property_ids );
1537 2727
1538 2728 $from_email_address = get_option('propertyhive_email_from_address', '');
1539 2729 if ( $from_email_address == '' )
@@ -1542,31 +2732,54 @@
1542 2732 }
1543 2733 if ( $from_email_address == '' )
1544 2734 {
1545 2735 // Should never get here
1546 - $from_email_address = $_POST['email_address'];
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'] ) ) : '';
1547 2738 }
1548 2739
1549 2740 $headers = array();
1550 - if ( isset($_POST['name']) && ! empty($_POST['name']) )
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 !== '' )
1551 2752 {
1552 - $headers[] = 'From: ' . ph_clean( $_POST['name'] ) . ' <' . sanitize_email( $from_email_address ) . '>';
2753 + $headers[] = sprintf( 'From: %s <%s>', $name, $from_email_address );
1553 2754 }
1554 2755 else
1555 2756 {
1556 - $headers[] = 'From: <' . sanitize_email( $from_email_address ) . '>';
2757 + $headers[] = sprintf( 'From: <%s>', $from_email_address );
1557 2758 }
1558 - if ( isset($_POST['email_address']) && sanitize_email( $_POST['email_address'] ) != '' )
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']) )
1559 2762 {
1560 - $headers[] = 'Reply-To: ' . sanitize_email( $_POST['email_address'] );
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 + }
1561 2770 }
1562 2771
1563 2772 $to = apply_filters( 'propertyhive_property_enquiry_to', $to, $property_ids );
1564 2773 $subject = apply_filters( 'propertyhive_property_enquiry_subject', $subject, $property_ids );
2774 + $headers = apply_filters( 'propertyhive_property_enquiry_headers', $headers, $property_ids );
1565 2775 $message = apply_filters( 'propertyhive_property_enquiry_body', $message, $property_ids );
1566 - $headers = apply_filters( 'propertyhive_property_enquiry_headers', $headers, $property_ids );
2776 +
2777 + do_action( 'propertyhive_before_property_enquiry_sent' );
1567 2778
1568 2779 $sent = wp_mail( $to, $subject, $message, $headers );
2780 +
2781 + do_action( 'propertyhive_after_property_enquiry_sent' );
1569 2782
1570 2783 if ( ! $sent )
1571 2784 {
1572 2785 $return['success'] = false;
@@ -1575,8 +2788,10 @@
1575 2788 }
1576 2789 else
1577 2790 {
1578 2791 $return['success'] = true;
2792 +
2793 + $enquiry_post_id = '';
1579 2794
1580 2795 if ( get_option( 'propertyhive_store_property_enquiries', 'yes' ) == 'yes' )
1581 2796 {
1582 2797 // Now insert into enquiries section of WordPress
@@ -1587,11 +2802,13 @@
1587 2802 else
1588 2803 {
1589 2804 $title = __( 'Multiple Property Enquiry', 'propertyhive' );
1590 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.
1591 2807 if ( isset($_POST['name']) && ! empty($_POST['name']) )
1592 2808 {
1593 - $title .= __( ' from ', 'propertyhive' ) . ph_clean($_POST['name']);
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']));
1594 2811 }
1595 2812
1596 2813 $enquiry_post = array(
1597 2814 'post_title' => $title,
@@ -1609,32 +2826,43 @@
1609 2826 add_post_meta( $enquiry_post_id, '_source', 'website' );
1610 2827 add_post_meta( $enquiry_post_id, '_negotiator_id', '' );
1611 2828 add_post_meta( $enquiry_post_id, '_office_id', $office_id );
1612 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.
1613 2831 foreach ($_POST as $key => $value)
1614 2832 {
1615 - if ( $key == 'property_id' )
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 ) )
1616 2837 {
2838 + continue;
2839 + }
2840 +
2841 + if ( $meta_key == 'property_id' )
2842 + {
1617 2843 foreach ( $property_ids as $property_id )
1618 2844 {
1619 - add_post_meta( $enquiry_post_id, $key, (int)$property_id );
2845 + add_post_meta( $enquiry_post_id, $meta_key, (int)$property_id );
1620 2846 }
1621 2847 }
1622 2848 else
1623 2849 {
1624 - add_post_meta( $enquiry_post_id, $key, sanitize_textarea_field($value) );
2850 + add_post_meta( $enquiry_post_id, $meta_key, sanitize_textarea_field(wp_unslash($value)) );
1625 2851 }
1626 2852 }
1627 2853 }
1628 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 +
1629 2858 // Send auto-responder
1630 2859 if ( get_option( 'propertyhive_enquiry_auto_responder', '' ) == 'yes' )
1631 2860 {
1632 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.
1633 2863 PH()->email->send_enquiry_auto_responder( $_POST );
1634 2864 }
1635 -
1636 - do_action('propertyhive_property_enquiry_sent', $_POST, $to);
1637 2865 }
1638 2866 }
1639 2867
1640 2868 $this->json_headers();
@@ -1650,12 +2878,14 @@
1650 2878 public function create_contact_from_enquiry()
1651 2879 {
1652 2880 global $post;
1653 2881
1654 - $enquiry_post_id = ( (isset($_POST['post_id'])) ? (int)$_POST['post_id'] : '' );
1655 - $nonce = ( (isset($_POST['security'])) ? ph_clean($_POST['security']) : '' );
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'] ) ) : '';
1656 2886
1657 - if ( ! wp_verify_nonce( $nonce, 'create-content-from-enquiry-nonce-' . $enquiry_post_id ) )
2887 + if ( ! wp_verify_nonce( $nonce, 'create-contact-from-enquiry-nonce-' . $enquiry_post_id ) )
1658 2888 {
1659 2889 // This nonce is not valid.
1660 2890 die( json_encode( array('error' => 'Invalid nonce. Please refresh and try again') ) );
1661 2891 }
@@ -1664,36 +2894,70 @@
1664 2894
1665 2895 $name = false;
1666 2896 $email = false;
1667 2897 $telephone = false;
2898 + $address = false;
2899 + $postcode = false;
2900 + $property_id = false;
1668 2901
1669 2902 foreach ($enquiry_meta as $key => $value)
1670 2903 {
1671 - if ( strpos($key, 'name') !== false )
2904 + if ( strpos(strtolower($key), 'name') !== false && strpos(strtolower($key), 'property') === false && $value[0] != '' )
1672 2905 {
1673 - $name = $value[0];
2906 + if ( $name === false )
2907 + {
2908 + $name = $value[0];
2909 + }
2910 + else
2911 + {
2912 + $name .= ' ' . $value[0];
2913 + }
1674 2914 }
1675 - elseif ( strpos($key, 'email') !== false )
2915 + elseif ( strpos(strtolower($key), 'email') !== false && $value[0] != '' )
1676 2916 {
1677 - $email = $value[0];
2917 + if ( $email === false )
2918 + {
2919 + $email = $value[0];
2920 + }
2921 + else
2922 + {
2923 + $email .= ',' . $value[0];
2924 + }
1678 2925 }
1679 - elseif ( strpos($key, 'telephone') !== false )
2926 + elseif ( strpos(strtolower($key), 'phone') !== false && $value[0] != '' )
1680 2927 {
1681 - $telephone = $value[0];
2928 + if ( $telephone === false )
2929 + {
2930 + $telephone = $value[0];
2931 + }
2932 + else
2933 + {
2934 + $telephone .= ',' . $value[0];
2935 + }
1682 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 + }
1683 2949 }
1684 2950
1685 - if ( $name === false || $email === false )
2951 + if ( $name === false && $email === false )
1686 2952 {
1687 - // This nonce is not valid.
1688 - die( json_encode( array('error' => 'Name or email address not found') ) );
2953 + die( json_encode( array('error' => 'Name and email address not found') ) );
1689 2954 }
1690 2955
1691 - // We've not imported this property before
1692 2956 $postdata = array(
1693 2957 'post_excerpt' => '',
1694 2958 'post_content' => '',
1695 - 'post_title' => utf8_encode(wp_strip_all_tags( $name )),
2959 + 'post_title' => wp_strip_all_tags( $name ),
1696 2960 'post_status' => 'publish',
1697 2961 'post_type' => 'contact',
1698 2962 'ping_status' => 'closed',
1699 2963 'comment_status' => 'closed',
@@ -1709,15 +2973,134 @@
1709 2973 {
1710 2974 die( json_encode( array('error' => 'Error creating contact') ) );
1711 2975 }
1712 2976
2977 + update_post_meta( $enquiry_post_id, '_contact_id', $contact_post_id );
2978 +
1713 2979 if ( $telephone !== FALSE ) {
1714 - update_post_meta( $contact_post_id, '_telephone_number', ph_clean( $telephone ) );
1715 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) ) );
1716 2982 }
1717 2983
1718 2984 if ( $email !== FALSE ) { update_post_meta( $contact_post_id, '_email_address', ph_clean( $email ) ); }
1719 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 +
1720 3103 do_action('propertyhive_create_contact_from_enquiry', $enquiry_post_id, $contact_post_id);
1721 3104
1722 3105 die( json_encode( array('success' => get_edit_post_link($contact_post_id, '')) ) );
1723 3106 }
@@ -1729,15 +3112,21 @@
1729 3112 check_ajax_referer( 'contact-save-validation', 'security' );
1730 3113
1731 3114 $this->json_headers();
1732 3115
1733 - parse_str($_POST['form_data']);
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;
1734 3123
1735 3124 $return = array('errors' => array());
1736 3125
1737 - if ( isset($_email_address) && $_email_address != '' )
3126 + if ( '' !== $email_address_input )
1738 3127 {
1739 - $email_addresses = explode( ",", $_email_address );
3128 + $email_addresses = explode( ",", $email_address_input );
1740 3129
1741 3130 foreach ( $email_addresses as $email_address )
1742 3131 {
1743 3132 $email_address = trim( $email_address );
@@ -1752,8 +3141,9 @@
1752 3141 'post_type' => 'contact',
1753 3142 'post_status' => 'any',
1754 3143 'posts_per_page' => 1,
1755 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.
1756 3146 'meta_query' => array(
1757 3147 'relation' => 'OR',
1758 3148 array(
1759 3149 'key' => '_email_address',
@@ -1772,11 +3162,12 @@
1772 3162 'compare' => 'LIKE'
1773 3163 )
1774 3164 )
1775 3165 );
1776 - if ( isset($post_ID) && $post_ID != '' )
3166 + if ( $contact_id )
1777 3167 {
1778 - $args['post__not_in'] = array( $post_ID );
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 );
1779 3170 }
1780 3171
1781 3172 $contact_query = new WP_Query( $args );
1782 3173
@@ -1785,9 +3176,10 @@
1785 3176 while ( $contact_query->have_posts() )
1786 3177 {
1787 3178 $contact_query->the_post();
1788 3179
1789 - $return['errors'][] = __( 'A contact, ' . get_the_title() . ', already exists with email address', 'propertyhive' ) . ' ' . $email_address;
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 );
1790 3182 }
1791 3183 }
1792 3184 }
1793 3185 }
@@ -1796,8 +3188,80 @@
1796 3188
1797 3189 die();
1798 3190 }
1799 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 +
1800 3264 // Dashboard related functions
1801 3265 public function get_news()
1802 3266 {
1803 3267 $this->json_headers();
@@ -1822,9 +3286,9 @@
1822 3286 foreach ( $rss_items as $item )
1823 3287 {
1824 3288 $return[] = array(
1825 3289 'title' => esc_html( $item->get_title() ),
1826 - 'permalink' => esc_url( $item->get_permalink() ),
3290 + 'permalink' => esc_url( $item->get_permalink() ) . '?src=dashboard',
1827 3291 'date' => $item->get_date('F d, Y')
1828 3292 );
1829 3293 }
1830 3294
@@ -1846,8 +3310,9 @@
1846 3310 $args = array(
1847 3311 'post_type' => 'viewing',
1848 3312 'fields' => 'ids',
1849 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.
1850 3315 'meta_query' => array(
1851 3316 array(
1852 3317 'key' => '_status',
1853 3318 'value' => 'carried_out'
@@ -1858,8 +3323,10 @@
1858 3323 )
1859 3324 )
1860 3325 );
1861 3326
3327 + $args = apply_filters( 'propertyhive_admin_dashboard_viewings_awaiting_applicant_feedback_args', $args );
3328 +
1862 3329 $viewings_query = new WP_Query( $args );
1863 3330
1864 3331 if ( $viewings_query->have_posts() )
1865 3332 {
@@ -1869,19 +3336,19 @@
1869 3336
1870 3337 $property_id = get_post_meta( get_the_ID(), '_property_id', TRUE );
1871 3338 $property = new PH_Property((int)$property_id);
1872 3339
1873 - $applicant_contact_id = get_post_meta( get_the_ID(), '_applicant_contact_id', TRUE );
3340 + $applicant_contact_ids = get_post_meta( get_the_ID(), '_applicant_contact_id' );
1874 3341
1875 3342 $return[] = array(
1876 3343 'ID' => get_the_ID(),
1877 3344 'edit_link' => get_edit_post_link( get_the_ID() ),
1878 3345 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
1879 - 'start_date_time_formatted_Hi_jSFY' => date("H:i jS F Y", strtotime(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 ))),
1880 3347 'property_id' => $property_id,
1881 3348 'property_address' => $property->get_formatted_full_address(),
1882 - 'applicant_contact_id' => $applicant_contact_id,
1883 - 'applicant_name' => get_the_title( $applicant_contact_id ),
3349 + 'applicant_contact_id' => $applicant_contact_ids[0],
3350 + 'applicant_name' => get_the_title( $applicant_contact_ids[0] ),
1884 3351 );
1885 3352 }
1886 3353 }
1887 3354
@@ -1903,8 +3370,9 @@
1903 3370 $args = array(
1904 3371 'post_type' => 'viewing',
1905 3372 'fields' => 'ids',
1906 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.
1907 3375 'meta_query' => array(
1908 3376 array(
1909 3377 'key' => '_status',
1910 3378 'value' => 'pending'
@@ -1910,9 +3378,9 @@
1910 3378 'value' => 'pending'
1911 3379 ),
1912 3380 array(
1913 3381 'key' => '_start_date_time',
1914 - 'value' => date("Y-m-d H:i:s"),
3382 + 'value' => gmdate("Y-m-d H:i:s"),
1915 3383 'compare' => '>='
1916 3384 ),
1917 3385 array(
1918 3386 'key' => '_negotiator_id',
@@ -1920,8 +3388,11 @@
1920 3388 ),
1921 3389 )
1922 3390 );
1923 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 +
1924 3395 $viewings_query = new WP_Query( $args );
1925 3396
1926 3397 if ( $viewings_query->have_posts() )
1927 3398 {
@@ -1935,9 +3406,9 @@
1935 3406 $return[] = array(
1936 3407 'ID' => get_the_ID(),
1937 3408 'edit_link' => get_edit_post_link( get_the_ID() ),
1938 3409 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
1939 - 'start_date_time_formatted_Hi_jSFY' => date("H:i jS F Y", strtotime(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 ))),
1940 3411 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
1941 3412 'title' => 'Viewing at ' . $property->get_formatted_full_address(),
1942 3413 );
1943 3414 }
@@ -1948,8 +3419,9 @@
1948 3419 $args = array(
1949 3420 'post_type' => 'appraisal',
1950 3421 'fields' => 'ids',
1951 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.
1952 3424 'meta_query' => array(
1953 3425 array(
1954 3426 'key' => '_status',
1955 3427 'value' => 'pending'
@@ -1955,9 +3427,9 @@
1955 3427 'value' => 'pending'
1956 3428 ),
1957 3429 array(
1958 3430 'key' => '_start_date_time',
1959 - 'value' => date("Y-m-d H:i:s"),
3431 + 'value' => gmdate("Y-m-d H:i:s"),
1960 3432 'compare' => '>='
1961 3433 ),
1962 3434 array(
1963 3435 'key' => '_negotiator_id',
@@ -1965,8 +3437,11 @@
1965 3437 ),
1966 3438 )
1967 3439 );
1968 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 +
1969 3444 $appraisals_query = new WP_Query( $args );
1970 3445
1971 3446 if ( $appraisals_query->have_posts() )
1972 3447 {
@@ -1979,9 +3454,9 @@
1979 3454 $return[] = array(
1980 3455 'ID' => get_the_ID(),
1981 3456 'edit_link' => get_edit_post_link( get_the_ID() ),
1982 3457 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
1983 - 'start_date_time_formatted_Hi_jSFY' => date("H:i jS F Y", strtotime(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 ))),
1984 3459 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
1985 3460 'title' => 'Appraisal at ' . $appraisal->get_formatted_full_address(),
1986 3461 );
1987 3462 }
@@ -2006,39 +3481,265 @@
2006 3481
2007 3482 die();
2008 3483 }
2009 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 +
2010 3704 public function get_property_marketing_statistics_meta_box()
2011 3705 {
2012 3706 check_ajax_referer( 'get_property_marketing_statistics_meta_box', 'security' );
2013 3707
2014 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 + }
2015 3713
2016 - echo '<div class="propertyhive_meta_box">';
2017 -
2018 - echo '<div class="options_group">';
2019 3714
2020 - echo '<h3>' . __( 'Views On Website', 'propertyhive' ) . '</h3>';
2021 3715
2022 - $view_statistics = get_post_meta( (int)$_POST['post_id'], '_view_statistics', TRUE );
3716 +
3717 + $view_statistics = get_post_meta( $post_id, '_view_statistics', TRUE );
2023 3718 if ( !is_array($view_statistics) )
2024 3719 {
2025 3720 $view_statistics = array();
2026 3721 }
2027 3722
2028 - $date_from = isset($_POST['statistics_date_from']) ? $_POST['statistics_date_from'] : date("Y-m-d", strtotime('7 days ago'));
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'));
2029 3724 $date_from = strtotime($date_from);
2030 3725
2031 - $date_to = isset($_POST['statistics_date_to']) ? $_POST['statistics_date_to'] : date("Y-m-d");
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");
2032 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 + }
2033 3731
3732 + echo '<div class="propertyhive_meta_box"><div class="options_group">';
2034 3733 $view_statistics_output = array();
3734 + $total_views = 0;
2035 3735
2036 3736 for ($i = $date_from; $i <= $date_to; $i += 86400)
2037 3737 {
2038 - if ( isset($view_statistics[date("Y-m-d", $i)]) )
3738 + if ( isset($view_statistics[gmdate("Y-m-d", $i)]) )
2039 3739 {
2040 - $view_statistics_output[] = array( $i * 1000, $view_statistics[date("Y-m-d", $i)] );
3740 + $view_statistics_output[] = array( $i * 1000, $view_statistics[gmdate("Y-m-d", $i)] );
3741 + $total_views += $view_statistics[gmdate("Y-m-d", $i)];
2041 3742 }
2042 3743 else
2043 3744 {
2044 3745 $view_statistics_output[] = array( $i * 1000, 0 );
@@ -2044,9 +3745,11 @@
2044 3745 $view_statistics_output[] = array( $i * 1000, 0 );
2045 3746 }
2046 3747 }
2047 3748
2048 - echo '<div id="marketing_statistics_website_view_graph" style="height:400px"></div>';
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>';
2049 3752
2050 3753 echo '</div>';
2051 3754
2052 3755 echo '</div>';
@@ -2061,11 +3764,12 @@
2061 3764 global $post;
2062 3765
2063 3766 check_ajax_referer( 'appraisal-details-meta-box', 'security' );
2064 3767
2065 - $post = get_post((int)$_POST['appraisal_id']);
3768 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
3769 + $post = get_post( $post_id );
2066 3770
2067 - $appraisal = new PH_Appraisal((int)$_POST['appraisal_id']);
3771 + $appraisal = new PH_Appraisal( $post_id );
2068 3772
2069 3773 echo '<div class="propertyhive_meta_box">';
2070 3774
2071 3775 echo '<div class="options_group">';
@@ -2071,11 +3775,11 @@
2071 3775 echo '<div class="options_group">';
2072 3776
2073 3777 echo '<p class="form-field">
2074 3778
2075 - <label for="">' . __('Status', 'propertyhive') . '</label>
3779 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
2076 3780
2077 - ' . ucwords(str_replace("_", " ", $appraisal->status));
3781 + ' . esc_html(ucwords(str_replace("_", " ", $appraisal->status)));
2078 3782
2079 3783 echo '</p>';
2080 3784
2081 3785 if ( $appraisal->status == 'cancelled' )
@@ -2094,16 +3798,39 @@
2094 3798 }
2095 3799
2096 3800 if ( $appraisal->status == 'carried_out' || $appraisal->status == 'won' || $appraisal->status == 'instructed' )
2097 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 +
2098 3825 if ( $appraisal->department == 'residential-sales' )
2099 3826 {
2100 3827 $args = array(
2101 3828 'id' => '_valued_price',
2102 - 'label' => __( 'Valued Price', 'propertyhive' ) . ' (&pound;)',
3829 + 'label' => __( 'Valued Price', 'propertyhive' ) . ' (' . $currency_symbol . ')',
2103 3830 'desc_tip' => false,
2104 3831 'class' => 'short',
2105 - 'value' => $appraisal->valued_price,
3832 + 'value' => ph_display_price_field( $appraisal->valued_price ),
2106 3833 );
2107 3834 propertyhive_wp_text_input( $args );
2108 3835 }
2109 3836 elseif ( $appraisal->department == 'residential-lettings' )
@@ -2111,18 +3838,19 @@
2111 3838 $rent_frequency = $appraisal->valued_rent_frequency;
2112 3839
2113 3840 echo '<p class="form-field">
2114 3841
2115 - <label for="">' . __('Valued Rent', 'propertyhive') . ' (&pound;)</label>
3842 + <label for="">' . esc_html(__('Valued Rent', 'propertyhive')) . ' (' . esc_html($currency_symbol) . ')</label>
2116 3843
2117 - <input type="text" class="" name="_valued_rent" id="_valued_rent" value="' . $appraisal->valued_rent . '" placeholder="" style="width:10%; min-width:100px;">
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;">
2118 3845
2119 3846 <select id="_valued_rent_frequency" name="_valued_rent_frequency" class="select" style="width:auto">
2120 - <option value="pppw"' . ( ($rent_frequency == 'pppw') ? ' selected' : '') . '>' . __('Per Person Per Week', 'propertyhive') . '</option>
2121 - <option value="pw"' . ( ($rent_frequency == 'pw') ? ' selected' : '') . '>' . __('Per Week', 'propertyhive') . '</option>
2122 - <option value="pcm"' . ( ($rent_frequency == 'pcm' || $rent_frequency == '') ? ' selected' : '') . '>' . __('Per Calendar Month', 'propertyhive') . '</option>
2123 - <option value="pq"' . ( ($rent_frequency == 'pq') ? ' selected' : '') . '>' . __('Per Quarter', 'propertyhive') . '</option>
2124 - <option value="pa"' . ( ($rent_frequency == 'pa') ? ' selected' : '') . '>' . __('Per Annum', 'propertyhive') . '</option>
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>
2125 3853 </select>
2126 3854
2127 3855 </p>';
2128 3856 }
@@ -2155,9 +3883,9 @@
2155 3883 public function get_appraisal_actions()
2156 3884 {
2157 3885 check_ajax_referer( 'appraisal-actions', 'security' );
2158 3886
2159 - $post_id = (int)$_POST['appraisal_id'];
3887 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2160 3888
2161 3889 $status = get_post_meta( $post_id, '_status', TRUE );
2162 3890 $department = get_post_meta( $post_id, '_department', TRUE );
2163 3891
@@ -2168,13 +3896,46 @@
2168 3896 $show_cancelled_meta_boxes = false;
2169 3897 $show_carried_out_meta_boxes = false;
2170 3898 $show_instructed_meta_boxes = false;
2171 3899 $show_lost_meta_boxes = false;
3900 + $show_customise_confirmation_meta_boxes = false;
2172 3901
2173 3902 $actions = array();
2174 3903
2175 3904 if ( $status == 'pending' )
2176 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 +
2177 3938 /*$actions[] = '<a
2178 3939 href=""
2179 3940 class="button"
2180 3941 style="width:100%; margin-bottom:7px; text-align:center"
@@ -2190,14 +3951,14 @@
2190 3951 $actions[] = '<a
2191 3952 href="#action_panel_appraisal_carried_out"
2192 3953 class="button button-success appraisal-action"
2193 3954 style="width:100%; margin-bottom:7px; text-align:center"
2194 - >' . __('Appraisal Carried Out', 'propertyhive') . '</a>';
3955 + >' . esc_html(__('Appraisal Carried Out', 'propertyhive')) . '</a>';
2195 3956 $actions[] = '<a
2196 3957 href="#action_panel_appraisal_cancelled"
2197 3958 class="button appraisal-action"
2198 3959 style="width:100%; margin-bottom:7px; text-align:center"
2199 - >' . __('Appraisal Cancelled', 'propertyhive') . '</a>';
3960 + >' . esc_html(__('Appraisal Cancelled', 'propertyhive')) . '</a>';
2200 3961
2201 3962 $show_cancelled_meta_boxes = true;
2202 3963 $show_carried_out_meta_boxes = true;
2203 3964 }
@@ -2207,15 +3968,15 @@
2207 3968 $actions[] = '<a
2208 3969 href="#action_panel_appraisal_won"
2209 3970 class="button button-success appraisal-action"
2210 3971 style="width:100%; margin-bottom:7px; text-align:center"
2211 - >' . __('Appraisal Won', 'propertyhive') . '</a>';
3972 + >' . esc_html(__('Appraisal Won', 'propertyhive')) . '</a>';
2212 3973
2213 3974 $actions[] = '<a
2214 3975 href="#action_panel_appraisal_lost"
2215 3976 class="button button-danger appraisal-action"
2216 3977 style="width:100%; margin-bottom:7px; text-align:center"
2217 - >' . __('Appraisal Lost', 'propertyhive') . '</a>';
3978 + >' . esc_html(__('Appraisal Lost', 'propertyhive')) . '</a>';
2218 3979
2219 3980 $show_lost_meta_boxes = true;
2220 3981 }
2221 3982
@@ -2224,9 +3985,9 @@
2224 3985 $actions[] = '<a
2225 3986 href="#action_panel_appraisal_instruct"
2226 3987 class="button button-success appraisal-action"
2227 3988 style="width:100%; margin-bottom:7px; text-align:center"
2228 - >' . __('Instruct Property', 'propertyhive') . '</a>';
3989 + >' . esc_html(__('Instruct Property', 'propertyhive')) . '</a>';
2229 3990
2230 3991 $show_instructed_meta_boxes = true;
2231 3992 }
2232 3993
@@ -2235,9 +3996,9 @@
2235 3996 $actions[] = '<a
2236 3997 href="#action_panel_appraisal_revert_carried_out"
2237 3998 class="button appraisal-action"
2238 3999 style="width:100%; margin-bottom:7px; text-align:center"
2239 - >' . __('Revert To Carried Out', 'propertyhive') . '</a>';
4000 + >' . esc_html(__('Revert To Carried Out', 'propertyhive')) . '</a>';
2240 4001 }
2241 4002
2242 4003 if ( $status == 'instructed' )
2243 4004 {
@@ -2243,12 +4004,12 @@
2243 4004 {
2244 4005 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
2245 4006
2246 4007 $actions[] = '<a
2247 - href="' . get_edit_post_link($property_id) . '"
4008 + href="' . esc_url(get_edit_post_link($property_id)) . '"
2248 4009 class="button"
2249 4010 style="width:100%; margin-bottom:7px; text-align:center"
2250 - >' . __('View Instructed Property', 'propertyhive') . '</a>';
4011 + >' . esc_html(__('View Instructed Property', 'propertyhive')) . '</a>';
2251 4012
2252 4013 /*$actions[] = '<a
2253 4014 href="#action_panel_appraisal_revert_won"
2254 4015 class="button appraisal-action"
@@ -2261,20 +4022,22 @@
2261 4022 $actions[] = '<a
2262 4023 href="#action_panel_appraisal_revert_pending"
2263 4024 class="button appraisal-action"
2264 4025 style="width:100%; margin-bottom:7px; text-align:center"
2265 - >' . __('Revert To Pending', 'propertyhive') . '</a>';
4026 + >' . esc_html(__('Revert To Pending', 'propertyhive')) . '</a>';
2266 4027 }
2267 4028
2268 4029 $actions = apply_filters( 'propertyhive_admin_appraisal_actions', $actions, $post_id );
4030 + $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
2269 4031
2270 4032 if ( !empty($actions) )
2271 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.
2272 4035 echo implode("", $actions);
2273 4036 }
2274 4037 else
2275 4038 {
2276 - echo '<div style="text-align:center">' . __( 'No actions to display', 'propertyhive' ) . '</div>';
4039 + echo '<div style="text-align:center">' . esc_html(__( 'No actions to display', 'propertyhive' )) . '</div>';
2277 4040 }
2278 4041
2279 4042 echo '</div>
2280 4043
@@ -2279,8 +4042,57 @@
2279 4042 echo '</div>
2280 4043
2281 4044 </div>';
2282 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 +
2283 4095 if ( $show_cancelled_meta_boxes )
2284 4096 {
2285 4097 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_cancelled" style="display:none;">
2286 4098
@@ -2287,16 +4099,16 @@
2287 4099 <div class="options_group" style="padding-top:8px;">
2288 4100
2289 4101 <div class="form-field">
2290 4102
2291 - <label for="_appraisal_cancelled_reason">' . __( 'Reason Cancelled', 'propertyhive' ) . '</label>
4103 + <label for="_appraisal_cancelled_reason">' . esc_html(__( 'Reason Cancelled', 'propertyhive' )) . '</label>
2292 4104
2293 - <textarea id="_cancelled_reason" name="_cancelled_reason" style="width:100%;">' . get_post_meta( $post_id, '_cancelled_reason', TRUE ) . '</textarea>
4105 + <textarea id="_cancelled_reason" name="_cancelled_reason" style="width:100%;">' . esc_html(get_post_meta( $post_id, '_cancelled_reason', TRUE )) . '</textarea>
2294 4106
2295 4107 </div>
2296 4108
2297 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2298 - <a class="button button-primary cancelled-reason-action-submit" href="#">' . __( 'Save', 'propertyhive' ) . '</a>
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>
2299 4111
2300 4112 </div>
2301 4113
2302 4114 </div>';
@@ -2307,15 +4119,38 @@
2307 4119 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_carried_out" style="display:none;">
2308 4120
2309 4121 <div class="options_group" style="padding-top:8px;">';
2310 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 +
2311 4146 if ( $department == 'residential-sales' )
2312 4147 {
2313 4148 echo '<div class="form-field">
2314 4149
2315 - <label for="_price">' . __( 'Valued Price (&pound;)', 'propertyhive' ) . '</label>
4150 + <label for="_price">' . esc_html(/* translators: %s: Currency symbol. */ sprintf( __( 'Valued Price (%s)', 'propertyhive' ), $currency_symbol )) . '</label>
2316 4151
2317 - <input type="text" id="_price" name="_price" style="width:100%;" value="' . get_post_meta( $post_id, '_valued_price', TRUE ) . '">
4152 + <input type="text" id="_price" name="_price" style="width:100%;" value="' . esc_attr(get_post_meta( $post_id, '_valued_price', TRUE )) . '">
2318 4153
2319 4154 </div>';
2320 4155 }
2321 4156 else
@@ -2322,25 +4157,26 @@
2322 4157 {
2323 4158 $rent_frequency = get_post_meta( $post_id, '_valued_rent_frequency', TRUE );
2324 4159 echo '<div class="form-field">
2325 4160
2326 - <label for="_price">' . __( 'Valued Rent (&pound;)', 'propertyhive' ) . '</label>
4161 + <label for="_price">' . esc_html(/* translators: %s: Currency symbol. */ sprintf( __( 'Valued Rent (%s)', 'propertyhive' ), $currency_symbol )) . '</label>
2327 4162
2328 - <input type="text" id="_price" name="_price" style="width:100%;" value="' . get_post_meta( $post_id, '_valued_rent', TRUE ) . '">
4163 + <input type="text" id="_price" name="_price" style="width:100%;" value="' . esc_attr(get_post_meta( $post_id, '_valued_rent', TRUE )) . '">
2329 4164
2330 4165 <select id="_rent_frequency" name="_rent_frequency" class="select" style="width:100%">
2331 - <option value="pppw"' . ( ($rent_frequency == 'pppw') ? ' selected' : '') . '>' . __('Per Person Per Week', 'propertyhive') . '</option>
2332 - <option value="pw"' . ( ($rent_frequency == 'pw') ? ' selected' : '') . '>' . __('Per Week', 'propertyhive') . '</option>
2333 - <option value="pcm"' . ( ($rent_frequency == 'pcm' || $rent_frequency == '') ? ' selected' : '') . '>' . __('Per Calendar Month', 'propertyhive') . '</option>
2334 - <option value="pq"' . ( ($rent_frequency == 'pq') ? ' selected' : '') . '>' . __('Per Quarter', 'propertyhive') . '</option>
2335 - <option value="pa"' . ( ($rent_frequency == 'pa') ? ' selected' : '') . '>' . __('Per Annum', 'propertyhive') . '</option>
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>
2336 4172 </select>
2337 4173
2338 4174 </div>';
2339 4175 }
2340 4176
2341 - echo '<a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2342 - <a class="button button-primary carried-out-action-submit" href="#">' . __( 'Save', 'propertyhive' ) . '</a>
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>
2343 4179
2344 4180 </div>
2345 4181
2346 4182 </div>';
@@ -2351,12 +4187,12 @@
2351 4187 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_instruct" style="display:none;">
2352 4188
2353 4189 <div class="options_group" style="padding-top:8px;">';
2354 4190
2355 - echo '<div style="margin-bottom:13px;">' . __( 'Upon instruction a new property record will be created within the \'Properties\' area.', 'propertyhive' ) . '</div>';
4191 + echo '<div style="margin-bottom:13px;">' . esc_html(__( 'Upon instruction a new property record will be created within the \'Properties\' area.', 'propertyhive' )) . '</div>';
2356 4192
2357 - echo '<a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2358 - <a class="button button-primary instructed-action-submit" href="#">' . __( 'OK', 'propertyhive' ) . '</a>
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>
2359 4195
2360 4196 </div>
2361 4197
2362 4198 </div>';
@@ -2369,16 +4205,16 @@
2369 4205 <div class="options_group" style="padding-top:8px;">
2370 4206
2371 4207 <div class="form-field">
2372 4208
2373 - <label for="_lost_reason">' . __( 'Reason Lost', 'propertyhive' ) . '</label>
4209 + <label for="_lost_reason">' . esc_html(__( 'Reason Lost', 'propertyhive' )) . '</label>
2374 4210
2375 - <textarea id="_lost_reason" name="_lost_reason" style="width:100%;">' . get_post_meta( $post_id, '_lost_reason', TRUE ) . '</textarea>
4211 + <textarea id="_lost_reason" name="_lost_reason" style="width:100%;">' . esc_html(get_post_meta( $post_id, '_lost_reason', TRUE )) . '</textarea>
2376 4212
2377 4213 </div>
2378 4214
2379 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2380 - <a class="button button-primary lost-reason-action-submit" href="#">' . wp_kses_post( __( 'Save Reason Lost', 'propertyhive' ) ) . '</a>
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>
2381 4217
2382 4218 </div>
2383 4219
2384 4220 </div>';
@@ -2390,34 +4226,57 @@
2390 4226 public function appraisal_carried_out()
2391 4227 {
2392 4228 check_ajax_referer( 'appraisal-actions', 'security' );
2393 4229
2394 - $post_id = (int)$_POST['appraisal_id'];
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 + }
2395 4234
2396 4235 $status = get_post_meta( $post_id, '_status', TRUE );
2397 4236
2398 4237 if ( $status == 'pending' )
2399 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 + }
2400 4258 update_post_meta( $post_id, '_status', 'carried_out' );
2401 4259
2402 4260 if ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-sales' )
2403 4261 {
2404 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['price']));
4262 + $price = preg_replace("/[^0-9.]/", '', $valuation_input['price']);
2405 4263 update_post_meta( $post_id, '_valued_price', $price );
2406 4264 update_post_meta( $post_id, '_valued_price_actual', $price );
2407 4265 }
2408 4266 elseif ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-lettings' )
2409 4267 {
2410 - $rent = preg_replace("/[^0-9]/", '', ph_clean($_POST['rent']));
4268 + $rent = preg_replace("/[^0-9.]/", '', $valuation_input['rent']);
2411 4269 update_post_meta( $post_id, '_valued_rent', $rent );
2412 4270
2413 - update_post_meta( $post_id, '_valued_rent_frequency', ph_clean($_POST['rent_frequency']) );
4271 + update_post_meta( $post_id, '_valued_rent_frequency', $valuation_input['rent_frequency'] );
2414 4272
2415 - switch (ph_clean($_POST['rent_frequency']))
4273 + switch ($valuation_input['rent_frequency'])
2416 4274 {
4275 + case "pd": { $price = ($rent * 365) / 12; break; }
2417 4276 case "pppw":
2418 4277 {
2419 - $bedrooms = get_post_meta( $postID, '_bedrooms', true );
4278 + $bedrooms = get_post_meta( $post_id, '_bedrooms', true );
2420 4279 if ( ( $bedrooms !== FALSE && $bedrooms != 0 && $bedrooms != '' ) && apply_filters( 'propertyhive_pppw_to_consider_bedrooms', true ) == true )
2421 4280 {
2422 4281 $price = (($rent * 52) / 12) * $bedrooms;
2423 4282 }
@@ -2441,11 +4300,13 @@
2441 4300 'action' => 'appraisal_carried_out',
2442 4301 );
2443 4302
2444 4303 PH_Comments::insert_note( $post_id, $comment );
4304 +
4305 + wp_send_json_success();
2445 4306 }
2446 4307
2447 - die();
4308 + wp_send_json_success();
2448 4309 }
2449 4310
2450 4311 public function appraisal_cancelled()
2451 4312 {
@@ -2450,16 +4311,24 @@
2450 4311 public function appraisal_cancelled()
2451 4312 {
2452 4313 check_ajax_referer( 'appraisal-actions', 'security' );
2453 4314
2454 - $post_id = (int)$_POST['appraisal_id'];
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 + }
2455 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 +
2456 4325 $status = get_post_meta( $post_id, '_status', TRUE );
2457 4326
2458 4327 if ( $status == 'pending' )
2459 4328 {
2460 4329 update_post_meta( $post_id, '_status', 'cancelled' );
2461 - update_post_meta( $post_id, '_cancelled_reason', sanitize_textarea_field( $_POST['cancelled_reason'] ) );
4330 + update_post_meta( $post_id, '_cancelled_reason', wp_slash( $reason ) );
2462 4331
2463 4332 // Add note/comment to appraisal
2464 4333 $comment = array(
2465 4334 'note_type' => 'action',
@@ -2466,11 +4335,13 @@
2466 4335 'action' => 'appraisal_cancelled',
2467 4336 );
2468 4337
2469 4338 PH_Comments::insert_note( $post_id, $comment );
4339 +
4340 + wp_send_json_success();
2470 4341 }
2471 4342
2472 - die();
4343 + wp_send_json_error();
2473 4344 }
2474 4345
2475 4346 public function appraisal_won()
2476 4347 {
@@ -2475,9 +4346,12 @@
2475 4346 public function appraisal_won()
2476 4347 {
2477 4348 check_ajax_referer( 'appraisal-actions', 'security' );
2478 4349
2479 - $post_id = (int)$_POST['appraisal_id'];
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 + }
2480 4354
2481 4355 $status = get_post_meta( $post_id, '_status', TRUE );
2482 4356
2483 4357 if ( $status == 'carried_out' )
@@ -2490,11 +4364,13 @@
2490 4364 'action' => 'appraisal_won',
2491 4365 );
2492 4366
2493 4367 PH_Comments::insert_note( $post_id, $comment );
4368 +
4369 + wp_send_json_success();
2494 4370 }
2495 4371
2496 - die();
4372 + wp_send_json_error();
2497 4373 }
2498 4374
2499 4375 public function appraisal_lost_reason()
2500 4376 {
@@ -2499,16 +4375,24 @@
2499 4375 public function appraisal_lost_reason()
2500 4376 {
2501 4377 check_ajax_referer( 'appraisal-actions', 'security' );
2502 4378
2503 - $post_id = (int)$_POST['appraisal_id'];
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 + }
2504 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 +
2505 4389 $status = get_post_meta( $post_id, '_status', TRUE );
2506 4390
2507 4391 if ( $status == 'carried_out' )
2508 4392 {
2509 4393 update_post_meta( $post_id, '_status', 'lost' );
2510 - update_post_meta( $post_id, '_lost_reason', sanitize_textarea_field( $_POST['lost_reason'] ) );
4394 + update_post_meta( $post_id, '_lost_reason', wp_slash( $reason ) );
2511 4395
2512 4396 // Add note/comment to appraisal
2513 4397 $comment = array(
2514 4398 'note_type' => 'action',
@@ -2515,11 +4399,13 @@
2515 4399 'action' => 'appraisal_lost',
2516 4400 );
2517 4401
2518 4402 PH_Comments::insert_note( $post_id, $comment );
4403 +
4404 + wp_send_json_success();
2519 4405 }
2520 4406
2521 - die();
4407 + wp_send_json_error();
2522 4408 }
2523 4409
2524 4410 public function appraisal_instructed()
2525 4411 {
@@ -2524,9 +4410,9 @@
2524 4410 public function appraisal_instructed()
2525 4411 {
2526 4412 check_ajax_referer( 'appraisal-actions', 'security' );
2527 4413
2528 - $post_id = (int)$_POST['appraisal_id'];
4414 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2529 4415
2530 4416 $status = get_post_meta( $post_id, '_status', TRUE );
2531 4417
2532 4418 if ( $status == 'won' )
@@ -2570,10 +4456,10 @@
2570 4456 {
2571 4457 // Failed. Don't really know at the moment how to handle this
2572 4458
2573 4459 $return = array('error' => 'Failed to create property post. Please try again');
2574 - //echo json_encode( $return );
2575 - //die();
4460 + echo json_encode( $return );
4461 + die();
2576 4462 }
2577 4463 else
2578 4464 {
2579 4465 // Successfully added property post
@@ -2579,8 +4465,24 @@
2579 4465 // Successfully added property post
2580 4466
2581 4467 $department = get_post_meta( $post_id, '_department', TRUE );
2582 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 +
2583 4485 update_post_meta( $property_post_id, '_address_name_number', get_post_meta( $post_id, '_address_name_number', TRUE ) );
2584 4486 update_post_meta( $property_post_id, '_address_street', get_post_meta( $post_id, '_address_street', TRUE ) );
2585 4487 update_post_meta( $property_post_id, '_address_two', get_post_meta( $post_id, '_address_two', TRUE ) );
2586 4488 update_post_meta( $property_post_id, '_address_three', get_post_meta( $post_id, '_address_three', TRUE ) );
@@ -2599,36 +4501,70 @@
2599 4501 if ( get_post_meta( $post_id, '_address_four', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_four', TRUE ); }
2600 4502 if ( get_post_meta( $post_id, '_address_postcode', TRUE ) ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_postcode', TRUE ); }
2601 4503
2602 4504 $country = get_option( 'propertyhive_default_country', 'GB' );
2603 - $request_url = "https://maps.googleapis.com/maps/api/geocode/xml?address=" . urlencode( implode( ", ", $address_to_geocode ) ) . "&sensor=false&region=gb"; // the request URL you'll send to google to get back your XML feed
2604 -
2605 - $api_key = get_option('propertyhive_google_maps_api_key', '');
2606 - if ( $api_key != '' ) { $request_url .= "&key=" . $api_key; }
2607 4505
2608 - $response = wp_remote_get($request_url);
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);
2609 4522
2610 - if ( is_array( $response ) && !is_wp_error( $response ) )
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
2611 4537 {
2612 - $header = $response['headers']; // array of http header lines
2613 - $body = $response['body']; // use the content
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
2614 4539
2615 - $xml = simplexml_load_string($body);
4540 + $api_key = get_option('propertyhive_google_maps_api_key', '');
4541 + if ( $api_key != '' ) { $request_url .= "&key=" . $api_key; }
2616 4542
2617 - if ( $xml !== FALSE )
4543 + $response = wp_remote_get($request_url);
4544 +
4545 + if ( is_array( $response ) && !is_wp_error( $response ) )
2618 4546 {
2619 - $status = $xml->status; // Get the request status as google's api can return several responses
4547 + $header = $response['headers']; // array of http header lines
4548 + $body = $response['body']; // use the content
2620 4549
2621 - if ($status == "OK")
4550 + $xml = simplexml_load_string($body);
4551 +
4552 + if ( $xml !== FALSE )
2622 4553 {
2623 - //request returned completed time to get lat / lng for storage
2624 - $lat = (string)$xml->result->geometry->location->lat;
2625 - $lng = (string)$xml->result->geometry->location->lng;
2626 -
2627 - if ($lat != '' && $lng != '')
4554 + $status = $xml->status; // Get the request status as google's api can return several responses
4555 +
4556 + if ($status == "OK")
2628 4557 {
2629 - update_post_meta( $post_id, '_latitude', $lat );
2630 - update_post_meta( $post_id, '_longitude', $lng );
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 + }
2631 4567 }
2632 4568 }
2633 4569 }
2634 4570 }
@@ -2641,9 +4577,9 @@
2641 4577 case "residential-sales":
2642 4578 {
2643 4579 update_post_meta( $property_post_id, '_currency', 'GBP' );
2644 4580
2645 - $price = preg_replace("/[^0-9]/", '', get_post_meta( $post_id, '_valued_price', TRUE ));
4581 + $price = preg_replace("/[^0-9.]/", '', get_post_meta( $post_id, '_valued_price', TRUE ));
2646 4582 update_post_meta( $property_post_id, '_price', $price );
2647 4583
2648 4584 break;
2649 4585 }
@@ -2674,8 +4610,10 @@
2674 4610 wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'property_type', array("fields" => "ids") ), 'property_type' );
2675 4611 wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'parking', array("fields" => "ids") ), 'parking' );
2676 4612 wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'outside_space', array("fields" => "ids") ), 'outside_space' );
2677 4613
4614 + update_post_meta( $property_post_id, '_council_tax_band', get_post_meta( $post_id, '_council_tax_band', TRUE ) );
4615 +
2678 4616 $owner_contact_ids = get_post_meta( $post_id, '_property_owner_contact_id', TRUE );
2679 4617 if ( !is_array($owner_contact_ids) )
2680 4618 {
2681 4619 $owner_contact_ids = array($owner_contact_ids);
@@ -2699,12 +4637,13 @@
2699 4637 // get appraisals where this is the owner and where not instructed
2700 4638 $args = array(
2701 4639 'post_type' => 'appraisal',
2702 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.
2703 4642 'meta_query' => array(
2704 4643 array(
2705 4644 'key' => '_property_owner_contact_id',
2706 - 'value' => $post->ID,
4645 + 'value' => $owner_contact_id,
2707 4646 'compare' => '='
2708 4647 ),
2709 4648 array(
2710 4649 'key' => '_status',
@@ -2735,19 +4674,197 @@
2735 4674 'action' => 'appraisal_instructed',
2736 4675 );
2737 4676
2738 4677 PH_Comments::insert_note( $post_id, $comment );
4678 +
4679 + wp_send_json_success();
2739 4680 }
2740 4681 }
2741 4682
2742 - die();
4683 + wp_send_json_error();
2743 4684 }
2744 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 +
2745 4862 public function appraisal_revert_pending()
2746 4863 {
2747 4864 check_ajax_referer( 'appraisal-actions', 'security' );
2748 4865
2749 - $post_id = (int)$_POST['appraisal_id'];
4866 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2750 4867
2751 4868 $status = get_post_meta( $post_id, '_status', TRUE );
2752 4869
2753 4870 if ( $status == 'carried_out' || $status == 'cancelled' )
@@ -2760,11 +4877,13 @@
2760 4877 'action' => 'appraisal_revert_pending',
2761 4878 );
2762 4879
2763 4880 PH_Comments::insert_note( $post_id, $comment );
4881 +
4882 + wp_send_json_success();
2764 4883 }
2765 4884
2766 - die();
4885 + wp_send_json_error();
2767 4886 }
2768 4887
2769 4888 public function appraisal_revert_carried_out()
2770 4889 {
@@ -2769,9 +4888,9 @@
2769 4888 public function appraisal_revert_carried_out()
2770 4889 {
2771 4890 check_ajax_referer( 'appraisal-actions', 'security' );
2772 4891
2773 - $post_id = (int)$_POST['appraisal_id'];
4892 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2774 4893
2775 4894 $status = get_post_meta( $post_id, '_status', TRUE );
2776 4895
2777 4896 if ( $status == 'won' || $status == 'lost' )
@@ -2784,11 +4903,13 @@
2784 4903 'action' => 'appraisal_revert_carried_out',
2785 4904 );
2786 4905
2787 4906 PH_Comments::insert_note( $post_id, $comment );
4907 +
4908 + wp_send_json_success();
2788 4909 }
2789 4910
2790 - die();
4911 + wp_send_json_error();
2791 4912 }
2792 4913
2793 4914 public function appraisal_revert_won()
2794 4915 {
@@ -2793,9 +4914,9 @@
2793 4914 public function appraisal_revert_won()
2794 4915 {
2795 4916 check_ajax_referer( 'appraisal-actions', 'security' );
2796 4917
2797 - $post_id = (int)$_POST['appraisal_id'];
4918 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2798 4919
2799 4920 $status = get_post_meta( $post_id, '_status', TRUE );
2800 4921
2801 4922 if ( $status == 'instructed' )
@@ -2808,11 +4929,13 @@
2808 4929 'action' => 'appraisal_revert_won',
2809 4930 );
2810 4931
2811 4932 PH_Comments::insert_note( $post_id, $comment );
4933 +
4934 + wp_send_json_success();
2812 4935 }
2813 4936
2814 - die();
4937 + wp_send_json_error();
2815 4938 }
2816 4939
2817 4940 // Viewing related functions
2818 4941 public function book_viewing_property()
@@ -2820,10 +4943,11 @@
2820 4943 check_ajax_referer( 'book-viewing', 'security' );
2821 4944
2822 4945 $this->json_headers();
2823 4946
2824 - // TO DO: Should do validation on server side also
2825 - if (empty($_POST['property_id']))
4947 + $booking = $this->get_viewing_booking_input();
4948 + $property_id = $this->get_authorized_record_id( 'property_id', 'property' );
4949 + if ($property_id < 1)
2826 4950 {
2827 4951 $return = array('error' => 'No property selected');
2828 4952 echo json_encode( $return );
2829 4953 die();
@@ -2828,18 +4952,26 @@
2828 4952 echo json_encode( $return );
2829 4953 die();
2830 4954 }
2831 4955
2832 - $property = new PH_Property((int)$_POST['property_id']);
4956 + $property = new PH_Property( $property_id );
2833 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 + }
2834 4966 $applicant_contact_ids = array();
2835 4967
2836 4968 // Create applicant record if required
2837 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
4969 + if (empty($booking['applicant_ids']) && !empty($booking['applicant_name']))
2838 4970 {
2839 4971 // Need to create contact/applicant
2840 4972 $contact_post = array(
2841 - 'post_title' => ph_clean($_POST['applicant_name']),
4973 + 'post_title' => $booking['applicant_name'],
2842 4974 'post_content' => '',
2843 4975 'post_type' => 'contact',
2844 4976 'post_status' => 'publish',
2845 4977 'comment_status' => 'closed',
@@ -2846,9 +4978,9 @@
2846 4978 'ping_status' => 'closed',
2847 4979 );
2848 4980
2849 4981 // Insert the post into the database
2850 - $contact_post_id = wp_insert_post( $contact_post );
4982 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
2851 4983
2852 4984 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
2853 4985 {
2854 4986 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -2857,8 +4989,27 @@
2857 4989 }
2858 4990
2859 4991 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
2860 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 +
2861 5012 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
2862 5013 update_post_meta( $contact_post_id, '_applicant_profile_0', array( 'department' => $property->department, 'send_matching_properties' => '' ) );
2863 5014
2864 5015 $applicant_contact_ids[] = $contact_post_id;
@@ -2863,20 +5014,12 @@
2863 5014
2864 5015 $applicant_contact_ids[] = $contact_post_id;
2865 5016 }
2866 5017
2867 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
5018 + if (!empty($booking['applicant_ids']) && empty($booking['applicant_name']))
2868 5019 {
2869 5020 // This is an existing contact
2870 - if ( !is_array($_POST['applicant_ids']) )
2871 - {
2872 - $_POST['applicant_ids'] = array(ph_clean($_POST['applicant_ids']));
2873 - }
2874 -
2875 - foreach ( $_POST['applicant_ids'] as $applicant_id )
2876 - {
2877 - $applicant_contact_ids[] = (int)$applicant_id;
2878 - }
5021 + $applicant_contact_ids = $booking['applicant_ids'];
2879 5022 }
2880 5023
2881 5024 $applicant_contact_ids = array_unique($applicant_contact_ids);
2882 5025
@@ -2941,53 +5084,37 @@
2941 5084 update_post_meta( $applicant_contact_id, '_applicant_profile_' . $num_applicant_profiles, array( 'department' => $property->department ) );
2942 5085 }
2943 5086 }*/
2944 5087
2945 - // Loop through contacts and create one viewing each
2946 - // At the moment it's a 1-to-1 relationship, but might support multiple in the future
2947 - foreach ( $applicant_contact_ids as $applicant_contact_id )
2948 - {
2949 - // Insert viewing record
2950 - $viewing_post = array(
2951 - 'post_title' => '',
2952 - 'post_content' => '',
2953 - 'post_type' => 'viewing',
2954 - 'post_status' => 'publish',
2955 - 'comment_status' => 'closed',
2956 - 'ping_status' => 'closed',
2957 - );
2958 -
2959 - // Insert the post into the database
2960 - $viewing_post_id = wp_insert_post( $viewing_post );
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 + );
2961 5097
2962 - if ( is_wp_error($viewing_post_id) || $viewing_post_id == 0 )
2963 - {
2964 - $return = array('error' => 'Failed to create viewing post. Please try again');
2965 - echo json_encode( $return );
2966 - die();
2967 - }
2968 -
2969 - add_post_meta( $viewing_post_id, '_start_date_time', ph_clean($_POST['start_date']) . ' ' . ph_clean($_POST['start_time']) );
2970 - add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
2971 - add_post_meta( $viewing_post_id, '_property_id', (int)$_POST['property_id'] );
2972 - add_post_meta( $viewing_post_id, '_applicant_contact_id', $applicant_contact_id );
2973 - add_post_meta( $viewing_post_id, '_status', 'pending' );
2974 - add_post_meta( $viewing_post_id, '_feedback_status', '' );
2975 - add_post_meta( $viewing_post_id, '_feedback', '' );
2976 - add_post_meta( $viewing_post_id, '_feedback_passed_on', '' );
5098 + // Insert the post into the database
5099 + $viewing_post_id = wp_insert_post( $viewing_post );
2977 5100
2978 - if ( !empty($_POST['negotiator_ids']) )
2979 - {
2980 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
2981 - {
2982 - add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
2983 - }
2984 - }
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();
2985 5106 }
2986 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 +
2987 5112 $applicant_contacts = array();
2988 - foreach ( $applicant_contact_ids as $applicant_contact_id )
5113 + foreach ($applicant_contact_ids as $applicant_contact_id)
2989 5114 {
5115 + add_post_meta( $viewing_post_id, '_applicant_contact_id', $applicant_contact_id );
5116 +
2990 5117 $applicant_contacts[] = array(
2991 5118 'ID' => $applicant_contact_id,
2992 5119 'post_title' => get_the_title($applicant_contact_id),
2993 5120 'edit_link' => get_edit_post_link( $applicant_contact_id, '' ),
@@ -2993,8 +5120,21 @@
2993 5120 'edit_link' => get_edit_post_link( $applicant_contact_id, '' ),
2994 5121 );
2995 5122 }
2996 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 +
2997 5137 $return = array('success' => array(
2998 5138 'viewing' => array(
2999 5139 'ID' => $viewing_post_id,
3000 5140 'edit_link' => get_edit_post_link( $viewing_post_id, '' ),
@@ -3012,10 +5152,16 @@
3012 5152 check_ajax_referer( 'book-viewing', 'security' );
3013 5153
3014 5154 $this->json_headers();
3015 5155
3016 - // TO DO: Should do validation on server side also
3017 - if (empty($_POST['contact_id']))
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)
3018 5164 {
3019 5165 $return = array('error' => 'No contact selected');
3020 5166 echo json_encode( $return );
3021 5167 die();
@@ -3020,9 +5166,9 @@
3020 5166 echo json_encode( $return );
3021 5167 die();
3022 5168 }
3023 5169
3024 - if (empty($_POST['property_ids']))
5170 + if (empty($booking['property_ids']))
3025 5171 {
3026 5172 $return = array('error' => 'No property selected');
3027 5173 echo json_encode( $return );
3028 5174 die();
@@ -3029,9 +5175,9 @@
3029 5175 }
3030 5176
3031 5177 // Loop through contacts and create one viewing each
3032 5178 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
3033 - foreach ( $_POST['property_ids'] as $property_id )
5179 + foreach ( $booking['property_ids'] as $property_id )
3034 5180 {
3035 5181 // Insert viewing record
3036 5182 $viewing_post = array(
3037 5183 'post_title' => '',
@@ -3051,20 +5197,20 @@
3051 5197 echo json_encode( $return );
3052 5198 die();
3053 5199 }
3054 5200
3055 - add_post_meta( $viewing_post_id, '_start_date_time', ph_clean($_POST['start_date']) . ' ' . ph_clean($_POST['start_time']) );
5201 + add_post_meta( $viewing_post_id, '_start_date_time', $booking['start_date'] . ' ' . $booking['start_time'] );
3056 5202 add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
3057 5203 add_post_meta( $viewing_post_id, '_property_id', (int)$property_id );
3058 - add_post_meta( $viewing_post_id, '_applicant_contact_id', (int)$_POST['contact_id'] );
5204 + add_post_meta( $viewing_post_id, '_applicant_contact_id', $contact_id );
3059 5205 add_post_meta( $viewing_post_id, '_status', 'pending' );
3060 5206 add_post_meta( $viewing_post_id, '_feedback_status', '' );
3061 5207 add_post_meta( $viewing_post_id, '_feedback', '' );
3062 5208 add_post_meta( $viewing_post_id, '_feedback_passed_on', '' );
3063 5209
3064 - if ( !empty($_POST['negotiator_ids']) )
5210 + if ( !empty($booking['negotiator_ids']) )
3065 5211 {
3066 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
5212 + foreach ( $booking['negotiator_ids'] as $negotiator_id )
3067 5213 {
3068 5214 add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
3069 5215 }
3070 5216 }
@@ -3070,9 +5216,9 @@
3070 5216 }
3071 5217 }
3072 5218
3073 5219 $properties = array();
3074 - foreach ( $_POST['property_ids'] as $property_id )
5220 + foreach ( $booking['property_ids'] as $property_id )
3075 5221 {
3076 5222 $properties[] = array(
3077 5223 'ID' => (int)$property_id,
3078 5224 'post_title' => get_the_title((int)$property_id),
@@ -3098,503 +5244,1144 @@
3098 5244 global $post;
3099 5245
3100 5246 check_ajax_referer( 'viewing-details-meta-box', 'security' );
3101 5247
3102 - $post = get_post((int)$_POST['viewing_id']);
5248 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3103 5249
3104 - $viewing = new PH_Viewing((int)$_POST['viewing_id']);
5250 + $post = get_post( $post_id );
3105 5251
3106 - echo '<div class="propertyhive_meta_box">';
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;
3107 5275
3108 - echo '<div class="options_group">';
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 + }
3109 5281
3110 - echo '<p class="form-field">
3111 -
3112 - <label for="">' . __('Status', 'propertyhive') . '</label>
3113 -
3114 - ' . ucwords(str_replace("_", " ", $viewing->status));
5282 + $post = get_post((int)$post_id);
3115 5283
3116 - if ( $viewing->status == 'offer_made' )
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' )
3117 5300 {
3118 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
3119 - {
3120 - $offer_id = get_post_meta( $viewing->id, '_offer_id', TRUE );
3121 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
3122 - {
3123 - $offer_id = '';
3124 - }
5301 + update_post_meta( $post_id, '_status', 'carried_out' );
3125 5302
3126 - if ( $offer_id != '' )
3127 - {
3128 - echo ' (<a href="' . get_edit_post_link($offer_id) . '">' . __('View Offer', 'propertyhive') . '</a>)';
3129 - }
3130 - }
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();
3131 5312 }
3132 -
3133 - echo '</p>';
3134 5313
3135 - if ( $viewing->status == 'cancelled' )
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' )
3136 5326 {
3137 - $args = array(
3138 - 'id' => '_cancelled_reason',
3139 - 'label' => __( 'Reason Cancelled', 'propertyhive' ),
3140 - 'desc_tip' => false,
3141 - 'class' => '',
3142 - 'value' => $viewing->cancelled_reason,
3143 - 'custom_attributes' => array(
3144 - 'style' => 'width:95%; max-width:500px;'
3145 - )
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',
3146 5333 );
3147 - propertyhive_wp_textarea_input( $args );
5334 +
5335 + PH_Comments::insert_note( $post_id, $comment );
5336 +
5337 + wp_send_json_success();
3148 5338 }
3149 5339
3150 - if ( $viewing->status == 'carried_out' )
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' )
3151 5354 {
3152 - echo '<p class="form-field">
3153 -
3154 - <label for="">' . __('Applicant Feedback', 'propertyhive') . '</label>';
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' : '' );
3155 5358
3156 - switch ( $viewing->feedback_status )
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 )
3157 5395 {
3158 - case "interested":
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 )
3159 5434 {
3160 - echo 'Interested';
3161 - break;
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 + }
3162 5454 }
3163 - case "not_interested":
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 == '' )
3164 5511 {
3165 - echo 'Not Interested';
3166 - break;
5512 + $from = $property->office_email_address;
3167 5513 }
3168 - case "not_required":
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)
3169 5535 {
3170 - echo 'Feedback Not Required';
3171 - break;
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 + }
3172 5561 }
3173 - default:
3174 - {
3175 - echo 'Awaiting Feedback';
3176 - }
3177 5562 }
3178 5563
3179 - echo '</p>';
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';
3180 5568
3181 - if ( $viewing->feedback_status == 'interested' || $viewing->feedback_status == 'not_interested' )
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)
3182 5574 {
3183 - $args = array(
3184 - 'id' => '_feedback',
3185 - 'label' => __( 'Feedback', 'propertyhive' ),
3186 - 'desc_tip' => false,
3187 - 'class' => '',
3188 - 'value' => $viewing->feedback,
3189 - 'custom_attributes' => array(
3190 - 'style' => 'width:95%; max-width:500px;'
3191 - )
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',
3192 5591 );
3193 - propertyhive_wp_textarea_input( $args );
5592 +
5593 + PH_Comments::insert_note( $post_id, $comment );
3194 5594 }
5595 +
5596 + wp_send_json_success();
3195 5597 }
3196 -
3197 - if ( $viewing->status == 'carried_out' && ( $viewing->feedback_status == 'interested' || $viewing->feedback_status == 'not_interested' ) )
5598 + else
3198 5599 {
3199 - echo '<p class="form-field">
3200 -
3201 - <label for="">' . __('Feedback Passed On', 'propertyhive') . '</label>';
3202 -
3203 - echo ( ($viewing->feedback_passed_on == 'yes') ? 'Yes' : 'No' );
3204 -
3205 - echo '</p>';
5600 + wp_send_json_error('No valid recipient email addresses');
3206 5601 }
3207 5602
3208 - do_action('propertyhive_viewing_details_fields');
3209 -
3210 - echo '</div>';
3211 -
3212 - echo '</div>';
3213 -
3214 - die();
5603 + wp_die();
3215 5604 }
3216 5605
3217 - public function get_viewing_actions()
5606 + public function viewing_email_owner_booking_confirmation()
3218 5607 {
3219 5608 check_ajax_referer( 'viewing-actions', 'security' );
3220 5609
3221 - $post_id = (int)$_POST['viewing_id'];
5610 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3222 5611
3223 - $status = get_post_meta( $post_id, '_status', TRUE );
3224 - $feedback_status = get_post_meta( $post_id, '_feedback_status', TRUE );
5612 + $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5613 + $property_department = get_post_meta( $property_id, '_department' );
3225 5614
3226 - echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_viewing_actions_meta_box">
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 ) {
3227 5619
3228 - <div class="options_group" style="padding-top:8px;">';
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);
3229 5627
3230 - $show_cancelled_meta_boxes = false;
3231 - $show_feedback_meta_boxes = false;
5628 + $owner_name = $owner_contact->post_title;
5629 + $owner_dear = $owner_contact->dear();
3232 5630
3233 - $actions = array();
5631 + if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
5632 + if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
3234 5633
3235 - if ( $status == 'pending' )
3236 - {
3237 - $applicant_contact_id = get_post_meta( $post_id, '_applicant_contact_id', TRUE );
3238 - $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3239 - $applicant_email_address = get_post_meta( $applicant_contact_id, '_email_address', TRUE );
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 + }
3240 5641
3241 - if ( (int)$applicant_contact_id == '' || (int)$property_id == '' || (int)$applicant_contact_id == 0 || (int)$property_id == 0 || sanitize_email($applicant_email_address) == '' )
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) )
3242 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);
3243 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 + }
3244 5696 }
3245 - else
5697 + if ( !empty($negotiator_names) )
3246 5698 {
3247 - $applicant_booking_confirmation_sent_at = get_post_meta( $post_id, '_applicant_booking_confirmation_sent_at', TRUE );
3248 - $owner_booking_confirmation_sent_at = get_post_meta( $post_id, '_owner_booking_confirmation_sent_at', TRUE );
3249 -
3250 - //Applicant
3251 - $actions[] = '<a
3252 - href="#action_panel_viewing_email_applicant_booking_confirmation"
3253 - class="button viewing-action"
3254 - style="width:100%; margin-bottom:7px; text-align:center"
3255 - >' . ( ( $applicant_booking_confirmation_sent_at == '' ) ? __('Email Applicant Booking Confirmation', 'propertyhive') : __('Re-Email Applicant Booking Confirmation', 'propertyhive') ) . '</a>';
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 + }
3256 5718
3257 - $actions[] = '<div id="viewing_applicant_confirmation_date" style="text-align:center; font-size:12px; color:#999; margin-bottom:7px;' . ( ( $applicant_booking_confirmation_sent_at == '' ) ? 'display:none' : '' ) . '">' . ( ( $applicant_booking_confirmation_sent_at != '' ) ? 'Previously sent to applicant on <span title="' . $applicant_booking_confirmation_sent_at . '">' . date("jS F", strtotime($applicant_booking_confirmation_sent_at)) : '' ) . '</span></div>';
5719 + $property = new PH_Property((int)$property_id);
3258 5720
3259 - // Owner/Landlord
3260 - $property_department = get_post_meta( $property_id, '_department', TRUE );
3261 - $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
3262 - $owner_or_landlord = ( $property_department == 'residential-lettings' ? 'Landlord' : 'Owner' );
5721 + $to = implode(",", $owner_emails);
3263 5722
3264 - if ( count($owner_contact_ids) > 0) {
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', '' );
3265 5725
3266 - $actions[] = '<a
3267 - href="#action_panel_viewing_email_owner_booking_confirmation"
3268 - class="button viewing-action"
3269 - style="width:100%; margin-bottom:7px; text-align:center"
3270 - >' . ( ( $owner_booking_confirmation_sent_at == '' ) ? __('Email ' . $owner_or_landlord . ' Booking Confirmation', 'propertyhive') : __('Re-Email ' . $owner_or_landlord . ' Booking Confirmation', 'propertyhive') ) . '</a>';
3271 -
3272 - $actions[] = '<div id="viewing_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 ' . strtolower($owner_or_landlord) . ' on <span title="' . $owner_booking_confirmation_sent_at . '">' . date("jS F", strtotime($owner_booking_confirmation_sent_at)) : '' ) . '</span></div>';
3273 - }
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);
3274 5734
3275 - $actions[] = '<hr>';
3276 - }
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 );
3277 5737
3278 - $actions[] = '<a
3279 - href="#action_panel_viewing_carried_out"
3280 - class="button button-success viewing-action"
3281 - style="width:100%; margin-bottom:7px; text-align:center"
3282 - >' . __('Viewing Carried Out', 'propertyhive') . '</a>';
3283 - $actions[] = '<a
3284 - href="#action_panel_viewing_cancelled"
3285 - class="button viewing-action"
3286 - style="width:100%; margin-bottom:7px; text-align:center"
3287 - >' . __('Viewing Cancelled', 'propertyhive') . '</a>';
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);
3288 5748
3289 - $show_cancelled_meta_boxes = true;
3290 - }
5749 + $body = html_entity_decode($body);
3291 5750
3292 - if ( $status == 'carried_out' )
3293 - {
3294 - if ( $feedback_status == '' )
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' )
3295 5757 {
3296 - $actions[] = '<a
3297 - href="#action_panel_viewing_interested"
3298 - class="button button-success viewing-action"
3299 - style="width:100%; margin-bottom:7px; text-align:center"
3300 - >' . wp_kses_post( __('Applicant Interested', 'propertyhive') ) . '</a>';
5758 + $current_user = wp_get_current_user();
5759 + $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
3301 5760
3302 - $actions[] = '<a
3303 - href="#action_panel_viewing_not_interested"
3304 - class="button button-danger viewing-action"
3305 - style="width:100%; margin-bottom:7px; text-align:center"
3306 - >' . wp_kses_post( __('Applicant Not Interested', 'propertyhive') ) . '</a>';
3307 -
3308 - $actions[] = '<a
3309 - href="#action_panel_viewing_feedback_not_required"
3310 - class="button viewing-action"
3311 - style="width:100%; margin-bottom:7px; text-align:center"
3312 - >' . wp_kses_post( __('Feedback Not Required', 'propertyhive') ) . '</a>';
3313 -
3314 - $show_feedback_meta_boxes = true;
5761 + if ( $from == '' )
5762 + {
5763 + $from = $property->office_email_address;
5764 + }
3315 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 + }
3316 5778
3317 - if ( $feedback_status == 'interested' )
5779 + $attachments = array();
5780 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
3318 5781 {
3319 - $actions[] = '<a
3320 - href="' . trim(admin_url(), '/') . '/post-new.php?post_type=viewing&applicant_contact_id=' . get_post_meta( $post_id, '_applicant_contact_id', TRUE ) . '&property_id=' . get_post_meta( $post_id, '_property_id', TRUE ) . '&viewing_id=' . $post_id .'"
3321 - class="button button-success"
3322 - style="width:100%; margin-bottom:7px; text-align:center"
3323 - >' . wp_kses_post( __('Book Second Viewing', 'propertyhive') ) . '</a>';
5782 + $uploaded_files = $this->get_viewing_email_uploads();
3324 5783
3325 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
5784 + // Handle each file upload
5785 + foreach ($uploaded_files['name'] as $key => $value)
3326 5786 {
3327 - $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3328 - if ( get_post_meta( $property_id, '_department', TRUE ) == 'residential-sales' )
5787 + if ($uploaded_files['name'][$key])
3329 5788 {
3330 - // See if an offer has this viewing id associated with it
3331 - $offer_id = get_post_meta( $post_id, '_offer_id', TRUE );
3332 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
3333 - {
3334 - $offer_id = '';
3335 - }
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 + );
3336 5796
3337 - if ( $offer_id != '' )
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']))
3338 5802 {
3339 - $actions[] = '<a
3340 - href="' . get_edit_post_link( $offer_id, '' ) . '"
3341 - class="button"
3342 - style="width:100%; margin-bottom:7px; text-align:center"
3343 - >' . wp_kses_post( __('View Offer', 'propertyhive') ) . '</a>';
3344 - }
5803 + // Add the file path to attachments array
5804 + $attachments[] = $movefile['file'];
5805 + }
3345 5806 else
3346 5807 {
3347 - $actions[] = '<a
3348 - href="' . wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), '1', 'create_offer' ) . '"
3349 - class="button button-success"
3350 - style="width:100%; margin-bottom:7px; text-align:center"
3351 - >' . wp_kses_post( __('Record Offer', 'propertyhive') ) . '</a>';
5808 + // Handle error in file upload
5809 + wp_send_json_error($movefile['error']);
3352 5810 }
3353 5811 }
3354 5812 }
3355 5813 }
3356 5814
3357 - if ( get_post_meta( $post_id, '_feedback_passed_on', TRUE ) != 'yes' && ( $feedback_status == 'interested' || $feedback_status == 'not_interested' ) )
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)
3358 5825 {
3359 - $actions[] = '<a
3360 - href="#action_panel_viewing_revert_feedback_passed_on"
3361 - class="button viewing-action"
3362 - style="width:100%; margin-bottom:7px; text-align:center"
3363 - >' . wp_kses_post( __('Feedback Passed On To Owner', 'propertyhive') ) . '</a>';
5826 + @wp_delete_file($temp_file);
3364 5827 }
3365 5828
3366 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' || $feedback_status == 'not_required' )
5829 + if ( !$sent )
3367 5830 {
3368 - $actions[] = '<a
3369 - href="#action_panel_viewing_revert_feedback_pending"
3370 - class="button viewing-action"
3371 - style="width:100%; margin-bottom:7px; text-align:center"
3372 - >' . wp_kses_post( __('Revert To Feedback Pending', 'propertyhive') ) . '</a>';
5831 + wp_send_json_error('Failed to send email');
3373 5832 }
3374 - }
3375 5833
3376 - if ( $status == 'offer_made' )
3377 - {
3378 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
5834 + if ( apply_filters( 'propertyhive_log_booking_confirmation_emails', false ) === true )
3379 5835 {
3380 - $offer_id = get_post_meta( $post_id, '_offer_id', TRUE );
3381 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
3382 - {
3383 - $offer_id = '';
3384 - }
5836 + // Add note/comment to viewing
5837 + $comment = array(
5838 + 'note_type' => 'action',
5839 + 'action' => 'viewing_owner_booking_confirmation_email',
5840 + );
3385 5841
3386 - if ( $offer_id != '' )
3387 - {
3388 - $actions[] = '<a
3389 - href="' . get_edit_post_link( $offer_id, '' ) . '"
3390 - class="button"
3391 - style="width:100%; margin-bottom:7px; text-align:center"
3392 - >' . wp_kses_post( __('View Offer', 'propertyhive') ) . '</a>';
3393 - }
5842 + PH_Comments::insert_note( $post_id, $comment );
3394 5843 }
3395 - }
3396 5844
3397 - if ( ( $status == 'carried_out' && $feedback_status == '' ) || $status == 'cancelled' )
3398 - {
3399 - $actions[] = '<a
3400 - href="#action_panel_viewing_revert_pending"
3401 - class="button viewing-action"
3402 - style="width:100%; margin-bottom:7px; text-align:center"
3403 - >' . wp_kses_post( __('Revert To Pending', 'propertyhive') ) . '</a>';
3404 - }
5845 + update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
3405 5846
3406 - $actions = apply_filters( 'propertyhive_admin_viewing_actions', $actions, $post_id );
3407 -
3408 - if ( !empty($actions) )
3409 - {
3410 - echo implode("", $actions);
5847 + wp_send_json_success();
3411 5848 }
3412 5849 else
3413 5850 {
3414 - echo '<div style="text-align:center">' . wp_kses_post( __( 'No actions to display', 'propertyhive' ) ) . '</div>';
5851 + wp_send_json_error('No owner recipients');
3415 5852 }
3416 5853
3417 - echo '</div>
5854 + wp_die();
5855 + }
3418 5856
3419 - </div>';
5857 + public function viewing_email_attending_negotiator_booking_confirmation()
5858 + {
5859 + check_ajax_referer( 'viewing-actions', 'security' );
3420 5860
3421 - if ( $show_cancelled_meta_boxes )
3422 - {
3423 - echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_viewing_cancelled" style="display:none;">
5861 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5862 + $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3424 5863
3425 - <div class="options_group" style="padding-top:8px;">
5864 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
3426 5865
3427 - <div class="form-field">
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) ) {
3428 5870
3429 - <label for="_viewing_cancelled_reason">' . __( 'Reason Cancelled', 'propertyhive' ) . '</label>
3430 -
3431 - <textarea id="_cancelled_reason" name="_cancelled_reason" style="width:100%;">' . get_post_meta( $post_id, '_cancelled_reason', TRUE ) . '</textarea>
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);
3432 5878
3433 - </div>
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);
3434 5889
3435 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
3436 - <a class="button button-primary cancelled-reason-action-submit" href="#">' . __( 'Save', 'propertyhive' ) . '</a>
5890 + $owner_name = $owner_contact->post_title;
5891 + $owner_dear = $owner_contact->dear();
3437 5892
3438 - </div>
5893 + if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
5894 + if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
3439 5895
3440 - </div>';
3441 - }
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 + }
3442 5902
3443 - if ( $show_feedback_meta_boxes )
3444 - {
3445 - echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_viewing_interested" style="display:none;">
5903 + $owner_details[] = $owner_contact->post_title . "\nT: " . $owner_contact->telephone_number . "\nE: " . $owner_contact->email_address;
5904 + }
5905 + }
3446 5906
3447 - <div class="options_group" style="padding-top:8px;">
5907 + $owner_details = implode("\n\n", $owner_details);
3448 5908
3449 - <div class="form-field">
5909 + $owner_names_string = $this->get_list_string($owner_names);
5910 + $owner_dears_string = $this->get_list_string($owner_dears);
3450 5911
3451 - <label for="_viewing_interested_feedback">' . __( 'Applicant Feedback', 'propertyhive' ) . '</label>
3452 -
3453 - <textarea id="_interested_feedback" name="_interested_feedback" style="width:100%;">' . get_post_meta( $post_id, '_feedback', TRUE ) . '</textarea>
5912 + $applicant_names = array();
5913 + $applicant_dears = array();
5914 + $applicant_details = array();
3454 5915
3455 - </div>
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();
3456 5923
3457 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
3458 - <a class="button button-primary interested-feedback-action-submit" href="#">' . wp_kses_post( __( 'Save Feedback', 'propertyhive' ) ) . '</a>
5924 + $applicant_details[] = $applicant_contact->post_title . "\nT: " . $applicant_contact->telephone_number . "\nE: " . $applicant_contact->email_address;
5925 + }
5926 + }
3459 5927
3460 - </div>
5928 + $applicant_details = implode("\n\n", $applicant_details);
3461 5929
3462 - </div>';
5930 + $applicant_names = array_filter($applicant_names);
5931 + $applicant_dears = array_filter($applicant_dears);
3463 5932
3464 - echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_viewing_not_interested" style="display:none;">
5933 + $applicant_names_string = $this->get_list_string($applicant_names);
5934 + $applicant_dears_string = $this->get_list_string($applicant_dears);
3465 5935
3466 - <div class="options_group" style="padding-top:8px;">
5936 + $negotiator_names = array();
5937 + $negotiator_names_string = '';
3467 5938
3468 - <div class="form-field">
5939 + $negotiator_email_addresses = array();
5940 + $negotiator_email_addresses_string = '';
3469 5941
3470 - <label for="_viewing_not_interested_feedback">' . __( 'Applicant Feedback', 'propertyhive' ) . '</label>
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 + }
3471 5957
3472 - <textarea id="_not_interested_feedback" name="_not_interested_feedback" style="width:100%;">' . get_post_meta( $post_id, '_feedback', TRUE ) . '</textarea>
5958 + if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
5959 + {
5960 + $negotiator_email_addresses[] = $negotiator->user_email;
5961 + }
3473 5962
3474 - </div>
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 + }
3475 5992
3476 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
3477 - <a class="button button-primary not-interested-feedback-action-submit" href="#">' . wp_kses_post( __( 'Save Feedback', 'propertyhive' ) ) . '</a>
5993 + $property = new PH_Property((int)$property_id);
3478 5994
3479 - </div>
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', '' );
3480 5997
3481 - </div>';
3482 - }
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);
3483 6006
3484 - die();
3485 - }
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 );
3486 6009
3487 - public function viewing_carried_out()
3488 - {
3489 - check_ajax_referer( 'viewing-actions', 'security' );
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);
3490 6022
3491 - $post_id = (int)$_POST['viewing_id'];
6023 + $body = html_entity_decode($body);
3492 6024
3493 - $status = get_post_meta( $post_id, '_status', TRUE );
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 );
3494 6027
3495 - if ( $status == 'pending' )
3496 - {
3497 - update_post_meta( $post_id, '_status', 'carried_out' );
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 : '' );
3498 6034
3499 - // Add note/comment to viewing
3500 - $comment = array(
3501 - 'note_type' => 'action',
3502 - 'action' => 'viewing_carried_out',
3503 - );
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 + }
3504 6052
3505 - PH_Comments::insert_note( $post_id, $comment );
3506 - }
6053 + $attachments = array();
6054 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6055 + {
6056 + $uploaded_files = $this->get_viewing_email_uploads();
3507 6057
3508 - die();
3509 - }
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 + );
3510 6070
3511 - public function viewing_cancelled()
3512 - {
3513 - check_ajax_referer( 'viewing-actions', 'security' );
6071 + // Move the file to a temporary location
6072 + $upload_overrides = array('test_form' => false);
6073 + $movefile = wp_handle_upload($file, $upload_overrides);
3514 6074
3515 - $post_id = (int)$_POST['viewing_id'];
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 + }
3516 6088
3517 - $status = get_post_meta( $post_id, '_status', TRUE );
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';
3518 6093
3519 - if ( $status == 'pending' )
3520 - {
3521 - update_post_meta( $post_id, '_status', 'cancelled' );
3522 - update_post_meta( $post_id, '_cancelled_reason', sanitize_textarea_field( $_POST['cancelled_reason'] ) );
6094 + $headers = apply_filters( 'propertyhive_viewing_attending_negotiator_booking_confirmation_email_headers', $headers );
3523 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 +
3524 6108 // Add note/comment to viewing
3525 - $comment = array(
3526 - 'note_type' => 'action',
3527 - 'action' => 'viewing_cancelled',
3528 - );
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 + );
3529 6115
3530 - PH_Comments::insert_note( $post_id, $comment );
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();
3531 6122 }
6123 + else
6124 + {
6125 + wp_send_json_error('No attending negotiator recipients');
6126 + }
3532 6127
3533 - die();
6128 + wp_die();
3534 6129 }
3535 6130
3536 - public function viewing_email_applicant_booking_confirmation()
6131 + public function viewing_email_applicant_cancellation_notification()
3537 6132 {
3538 6133 check_ajax_referer( 'viewing-actions', 'security' );
3539 6134
3540 - $post_id = (int)$_POST['viewing_id'];
6135 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3541 6136
3542 - $applicant_contact_id = get_post_meta( $post_id, '_applicant_contact_id', TRUE );
6137 + $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
3543 6138 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3544 6139
3545 - if ( (int)$applicant_contact_id == '' || (int)$property_id == '' || (int)$applicant_contact_id == 0 || (int)$property_id == 0 )
6140 + if ( !is_array($applicant_contact_ids) || (int)$property_id == '' || count($applicant_contact_ids) == 0 || (int)$property_id == 0 )
3546 6141 {
3547 - die();
6142 + wp_send_json_error('Missing contact or property');
3548 6143 }
3549 6144
3550 6145 $property = new PH_Property((int)$property_id);
3551 6146
3552 - $to = get_post_meta( $applicant_contact_id, '_email_address', TRUE );
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 + }
3553 6157
3554 - if ( sanitize_email($to) != '' )
6158 + $to = array_filter($to);
6159 +
6160 + if ( !empty(implode($to)) )
3555 6161 {
3556 - $subject = get_option( 'propertyhive_viewing_applicant_booking_confirmation_email_subject', '' );
3557 - $body = get_option( 'propertyhive_viewing_applicant_booking_confirmation_email_body', '' );
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', '' );
3558 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 +
3559 6236 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
3560 - $subject = str_replace('[applicant_name]', get_the_title($applicant_contact_id), $subject);
3561 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
3562 - $subject = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
3563 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 +
3564 6247 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
3565 - $body = str_replace('[applicant_name]', get_the_title($applicant_contact_id), $body);
3566 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
3567 - $body = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
3568 6255
3569 - $from = $property->office_email_address;
3570 - if ( sanitize_email($from) == '' )
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 + )
3571 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 + {
3572 6293 $from = get_bloginfo('admin_email');
3573 6294 }
3574 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 +
3575 6332 $headers = array();
3576 - $headers[] = 'From: ' . get_bloginfo('name') . ' <' . $from . '>';
6333 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
6334 + $headers[] = 'Reply-To: ' . sanitize_email($from);
3577 6335 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
3578 6336
3579 - wp_mail($to, $subject, $body, $headers);
6337 + $headers = apply_filters( 'propertyhive_viewing_applicant_cancellation_notification_email_headers', $headers );
3580 6338
3581 - update_post_meta( $post_id, '_applicant_booking_confirmation_sent_at', date("Y-m-d H:i:s") );
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();
3582 6365 }
6366 + else
6367 + {
6368 + wp_send_json_error('No valid recipient email addresses');
6369 + }
3583 6370
3584 - die();
6371 + wp_die();
3585 6372 }
3586 6373
3587 - public function viewing_email_owner_booking_confirmation()
6374 + public function viewing_email_owner_cancellation_notification()
3588 6375 {
3589 6376 check_ajax_referer( 'viewing-actions', 'security' );
3590 6377
3591 - $post_id = (int)$_POST['viewing_id'];
6378 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3592 6379
3593 6380 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3594 6381 $property_department = get_post_meta( $property_id, '_department' );
3595 6382
3596 - $applicant_contact_id = get_post_meta( $post_id, '_applicant_contact_id', TRUE );
6383 + $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
3597 6384 $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
3598 6385
3599 6386 if ( $owner_contact_ids > 0 ) {
3600 6387
@@ -3599,66 +6386,551 @@
3599 6386 if ( $owner_contact_ids > 0 ) {
3600 6387
3601 6388 $owner_emails = array();
3602 6389 $owner_names = array();
6390 + $owner_dears = array();
3603 6391
3604 6392 foreach ($owner_contact_ids as $owner_id)
3605 6393 {
3606 - $owner_email = sanitize_email( get_post_meta($owner_id, '_email_address', TRUE) );
3607 - $owner_name = get_the_title($owner_id);
6394 + $owner_contact = new PH_Contact($owner_id);
3608 6395
3609 - if( ! empty($owner_email) ) array_push($owner_emails, $owner_email);
6396 + $owner_name = $owner_contact->post_title;
6397 + $owner_dear = $owner_contact->dear();
6398 +
3610 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 + }
3611 6408 }
3612 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 +
3613 6487 $property = new PH_Property((int)$property_id);
3614 6488
3615 6489 $to = implode(",", $owner_emails);
3616 6490
3617 - $subject = get_option( 'propertyhive_viewing_owner_booking_confirmation_email_subject', '' );
3618 - $body = get_option( 'propertyhive_viewing_owner_booking_confirmation_email_body', '' );
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', '' );
3619 6493
3620 6494 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
3621 - $subject = str_replace('[owner_name]', implode(", ", $owner_names), $subject);
3622 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
3623 - $subject = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
3624 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 +
3625 6506 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
3626 - $body = str_replace('[owner_name]', implode(", ", $owner_names), $body);
3627 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
3628 - $body = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
3629 6516
3630 - $from = $property->office_email_address;
3631 - if ( sanitize_email($from) == '' )
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 + )
3632 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 + {
3633 6554 $from = get_bloginfo('admin_email');
3634 6555 }
3635 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 +
3636 6593 $headers = array();
3637 - $headers[] = 'From: ' . get_bloginfo('name') . ' <' . $from . '>';
6594 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
6595 + $headers[] = 'Reply-To: ' . sanitize_email($from);
3638 6596 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
3639 6597
3640 - wp_mail($to, $subject, $body, $headers);
6598 + $headers = apply_filters( 'propertyhive_viewing_owner_cancellation_notification_email_headers', $headers );
3641 6599
3642 - update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', date("Y-m-d H:i:s") );
6600 + $sent = wp_mail($to, $subject, $body, $headers, $attachments);
3643 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();
3644 6626 }
6627 + else
6628 + {
6629 + wp_send_json_error('No owner recipients');
6630 + }
3645 6631
3646 - die();
6632 + wp_die();
3647 6633 }
3648 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 +
3649 6919 public function viewing_interested_feedback()
3650 6920 {
3651 6921 check_ajax_referer( 'viewing-actions', 'security' );
3652 6922
3653 - $post_id = (int)$_POST['viewing_id'];
6923 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3654 6924
6925 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6926 +
3655 6927 $status = get_post_meta( $post_id, '_status', TRUE );
3656 6928
3657 6929 if ( $status == 'carried_out' )
3658 6930 {
3659 6931 update_post_meta( $post_id, '_feedback_status', 'interested' );
3660 - update_post_meta( $post_id, '_feedback', sanitize_textarea_field( $_POST['feedback'] ) );
6932 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
3661 6933
3662 6934 // Add note/comment to viewing
3663 6935 $comment = array(
3664 6936 'note_type' => 'action',
@@ -3665,11 +6937,13 @@
3665 6937 'action' => 'viewing_applicant_interested',
3666 6938 );
3667 6939
3668 6940 PH_Comments::insert_note( $post_id, $comment );
6941 +
6942 + wp_send_json_success();
3669 6943 }
3670 6944
3671 - die();
6945 + wp_send_json_error();
3672 6946 }
3673 6947
3674 6948 public function viewing_not_interested_feedback()
3675 6949 {
@@ -3674,16 +6948,18 @@
3674 6948 public function viewing_not_interested_feedback()
3675 6949 {
3676 6950 check_ajax_referer( 'viewing-actions', 'security' );
3677 6951
3678 - $post_id = (int)$_POST['viewing_id'];
6952 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3679 6953
6954 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6955 +
3680 6956 $status = get_post_meta( $post_id, '_status', TRUE );
3681 6957
3682 6958 if ( $status == 'carried_out' )
3683 6959 {
3684 6960 update_post_meta( $post_id, '_feedback_status', 'not_interested' );
3685 - update_post_meta( $post_id, '_feedback', sanitize_textarea_field( $_POST['feedback'] ) );
6961 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
3686 6962
3687 6963 // Add note/comment to viewing
3688 6964 $comment = array(
3689 6965 'note_type' => 'action',
@@ -3690,11 +6966,13 @@
3690 6966 'action' => 'viewing_applicant_not_interested',
3691 6967 );
3692 6968
3693 6969 PH_Comments::insert_note( $post_id, $comment );
6970 +
6971 + wp_send_json_success();
3694 6972 }
3695 6973
3696 - die();
6974 + wp_send_json_error();
3697 6975 }
3698 6976
3699 6977 public function viewing_feedback_not_required()
3700 6978 {
@@ -3699,9 +6977,9 @@
3699 6977 public function viewing_feedback_not_required()
3700 6978 {
3701 6979 check_ajax_referer( 'viewing-actions', 'security' );
3702 6980
3703 - $post_id = (int)$_POST['viewing_id'];
6981 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3704 6982
3705 6983 $status = get_post_meta( $post_id, '_status', TRUE );
3706 6984
3707 6985 if ( $status == 'carried_out' )
@@ -3714,11 +6992,13 @@
3714 6992 'action' => 'viewing_feedback_not_required',
3715 6993 );
3716 6994
3717 6995 PH_Comments::insert_note( $post_id, $comment );
6996 +
6997 + wp_send_json_success();
3718 6998 }
3719 6999
3720 - die();
7000 + wp_send_json_error();
3721 7001 }
3722 7002
3723 7003 public function viewing_revert_feedback_pending()
3724 7004 {
@@ -3723,9 +7003,9 @@
3723 7003 public function viewing_revert_feedback_pending()
3724 7004 {
3725 7005 check_ajax_referer( 'viewing-actions', 'security' );
3726 7006
3727 - $post_id = (int)$_POST['viewing_id'];
7007 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3728 7008
3729 7009 $status = get_post_meta( $post_id, '_status', TRUE );
3730 7010
3731 7011 if ( $status == 'carried_out' )
@@ -3731,8 +7011,9 @@
3731 7011 if ( $status == 'carried_out' )
3732 7012 {
3733 7013 update_post_meta( $post_id, '_feedback_status', '' );
3734 7014 update_post_meta( $post_id, '_feedback_passed_on', '' );
7015 + delete_post_meta( $post_id, '_feedback_received_date' );
3735 7016
3736 7017 // Add note/comment to viewing
3737 7018 $comment = array(
3738 7019 'note_type' => 'action',
@@ -3739,11 +7020,13 @@
3739 7020 'action' => 'viewing_revert_feedback_pending',
3740 7021 );
3741 7022
3742 7023 PH_Comments::insert_note( $post_id, $comment );
7024 +
7025 + wp_send_json_success();
3743 7026 }
3744 7027
3745 - die();
7028 + wp_send_json_error();
3746 7029 }
3747 7030
3748 7031 public function viewing_revert_pending()
3749 7032 {
@@ -3748,16 +7031,17 @@
3748 7031 public function viewing_revert_pending()
3749 7032 {
3750 7033 check_ajax_referer( 'viewing-actions', 'security' );
3751 7034
3752 - $post_id = (int)$_POST['viewing_id'];
7035 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3753 7036
3754 7037 $status = get_post_meta( $post_id, '_status', TRUE );
3755 7038
3756 - if ( $status == 'carried_out' || $status == 'cancelled' )
7039 + if ( in_array( $status, array('carried_out', 'cancelled', 'no_show') ) )
3757 7040 {
3758 7041 update_post_meta( $post_id, '_status', 'pending' );
3759 7042 update_post_meta( $post_id, '_feedback_status', '' );
7043 + delete_post_meta( $post_id, '_feedback_received_date' );
3760 7044
3761 7045 // Add note/comment to viewing
3762 7046 $comment = array(
3763 7047 'note_type' => 'action',
@@ -3764,11 +7048,13 @@
3764 7048 'action' => 'viewing_revert_pending',
3765 7049 );
3766 7050
3767 7051 PH_Comments::insert_note( $post_id, $comment );
7052 +
7053 + wp_send_json_success();
3768 7054 }
3769 7055
3770 - die();
7056 + wp_send_json_error();
3771 7057 }
3772 7058
3773 7059 public function viewing_feedback_passed_on()
3774 7060 {
@@ -3773,9 +7059,9 @@
3773 7059 public function viewing_feedback_passed_on()
3774 7060 {
3775 7061 check_ajax_referer( 'viewing-actions', 'security' );
3776 7062
3777 - $post_id = (int)$_POST['viewing_id'];
7063 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3778 7064
3779 7065 $status = get_post_meta( $post_id, '_status', TRUE );
3780 7066
3781 7067 if ( $status == 'carried_out' )
@@ -3788,299 +7074,52 @@
3788 7074 'action' => 'viewing_feedback_passed_on',
3789 7075 );
3790 7076
3791 7077 PH_Comments::insert_note( $post_id, $comment );
7078 +
7079 + wp_send_json_success();
3792 7080 }
3793 7081
3794 - die();
7082 + wp_send_json_error();
3795 7083 }
3796 7084
3797 7085 public function get_property_viewings_meta_box()
3798 7086 {
3799 - check_ajax_referer( 'get_property_viewings_meta_box', 'security' );
7087 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
3800 7088
3801 - global $post;
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 + }
3802 7096
3803 - echo '<div class="propertyhive_meta_box">';
3804 -
3805 - echo '<div class="options_group">';
7097 + include( PH()->plugin_path() . '/includes/admin/views/html-property-viewings-meta-box.php' );
3806 7098
3807 - $args = array(
3808 - 'post_type' => 'viewing',
3809 - 'nopaging' => true,
3810 - 'orderby' => 'meta_value',
3811 - 'order' => 'DESC',
3812 - 'meta_key' => '_start_date_time',
3813 - 'post_status' => 'publish',
3814 - 'meta_query' => array(
3815 - array(
3816 - 'key' => '_property_id',
3817 - 'value' => (int)$_POST['post_id']
3818 - )
3819 - )
3820 - );
3821 - $viewings_query = new WP_Query( $args );
3822 -
3823 - if ( $viewings_query->have_posts() )
3824 - {
3825 - echo '<table style="width:100%">
3826 - <thead>
3827 - <tr>
3828 - <th style="text-align:left;">' . __( 'Date', 'propertyhive' ) . ' / ' . __( 'Time', 'propertyhive' ) . '</th>
3829 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
3830 - <th style="text-align:left;">' . __( 'Attending Negotiator(s)', 'propertyhive' ) . '</th>
3831 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3832 - </tr>
3833 - </thead>
3834 - <tbody>';
3835 -
3836 - while ( $viewings_query->have_posts() )
3837 - {
3838 - $viewings_query->the_post();
3839 -
3840 - echo '<tr>';
3841 - echo '<td style="text-align:left;"><a href="' . get_edit_post_link( get_the_ID(), '' ) . '">' . date("H:i jS F Y", strtotime(get_post_meta(get_the_ID(), '_start_date_time', TRUE))) . '</a></td>';
3842 - echo '<td style="text-align:left;">';
3843 - if ( get_post_meta(get_the_ID(), '_applicant_contact_id', TRUE) != '' )
3844 - {
3845 - echo '<a href="' . get_edit_post_link( get_post_meta(get_the_ID(), '_applicant_contact_id', TRUE), '' ) . '">' . get_the_title(get_post_meta(get_the_ID(), '_applicant_contact_id', TRUE)) . '</a>';
3846 - }
3847 - else
3848 - {
3849 - echo '-';
3850 - }
3851 - echo '</td>';
3852 - echo '<td style="text-align:left;">';
3853 -
3854 - $negotiator_ids = get_post_meta(get_the_ID(), '_negotiator_id');
3855 -
3856 - if (!empty($negotiator_ids))
3857 - {
3858 - $i = 0;
3859 - foreach ($negotiator_ids as $negotiator_id)
3860 - {
3861 - if ( $i > 0 ) { echo ', '; }
3862 -
3863 - $userdata = get_userdata( $negotiator_id );
3864 - if ( $userdata !== FALSE )
3865 - {
3866 - echo $userdata->display_name;
3867 - }
3868 - else
3869 - {
3870 - echo '<em>Unknown user</em>';
3871 - }
3872 - ++$i;
3873 - }
3874 - }
3875 - else
3876 - {
3877 - echo 'Unattended';
3878 - }
3879 -
3880 - echo '</td>';
3881 - echo '<td style="text-align:left;">';
3882 -
3883 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
3884 - echo ucwords(str_replace("_", " ", $status));
3885 - if ( $status == 'pending' )
3886 - {
3887 - echo '<br>';
3888 - // confirmation status
3889 - if ( get_post_meta(get_the_ID(), '_all_confirmed', TRUE) == 'yes' )
3890 - {
3891 - echo __( 'All Parties Confirmed', 'propertyhive' );
3892 - }
3893 - else
3894 - {
3895 - echo __( 'Awaiting Confirmation', 'propertyhive' );
3896 - }
3897 - }
3898 - if ( $status == 'carried_out' )
3899 - {
3900 - echo '<br>';
3901 - $feedback_status = get_post_meta(get_the_ID(), '_feedback_status', TRUE);
3902 - switch ( $feedback_status )
3903 - {
3904 - case "interested": { echo 'Applicant Interested'; break; }
3905 - case "not_interested": { echo 'Applicant Not Interested'; break; }
3906 - case "not_required": { echo 'Feedback Not Required'; break; }
3907 - default: { echo 'Awaiting Feedback'; }
3908 - }
3909 -
3910 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' )
3911 - {
3912 - $feedback_passed_on = get_post_meta(get_the_ID(), '_feedback_passed_on', TRUE);
3913 - echo '<br>' . ( ($feedback_passed_on == 'yes') ? 'Feedback Passed On' : 'Feedback Not Passed On' );
3914 - }
3915 - }
3916 - echo '</td>';
3917 - echo '</tr>';
3918 - }
3919 -
3920 - echo '
3921 - </tbody>
3922 - </table>
3923 - <br>';
3924 - }
3925 - else
3926 - {
3927 - echo '<p>' . __( 'No viewings exist for this property', 'propertyhive') . '</p>';
3928 - }
3929 - wp_reset_postdata();
3930 -
3931 7099 do_action('propertyhive_property_viewings_fields');
3932 -
3933 - echo '</div>';
3934 -
3935 - echo '</div>';
3936 7100
7101 + // Quit out
3937 7102 die();
3938 7103 }
3939 7104
3940 7105 public function get_contact_viewings_meta_box()
3941 7106 {
3942 - check_ajax_referer( 'get_contact_viewings_meta_box', 'security' );
7107 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
3943 7108
3944 - global $post;
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 + }
3945 7116
3946 - echo '<div class="propertyhive_meta_box">';
3947 -
3948 - echo '<div class="options_group">';
7117 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-viewings-meta-box.php' );
3949 7118
3950 - $args = array(
3951 - 'post_type' => 'viewing',
3952 - 'nopaging' => true,
3953 - 'orderby' => 'meta_value',
3954 - 'order' => 'DESC',
3955 - 'post_status' => 'publish',
3956 - 'meta_key' => '_start_date_time',
3957 - 'meta_query' => array(
3958 - array(
3959 - 'key' => '_applicant_contact_id',
3960 - 'value' => (int)$_POST['post_id']
3961 - )
3962 - )
3963 - );
3964 - $viewings_query = new WP_Query( $args );
3965 -
3966 - if ( $viewings_query->have_posts() )
3967 - {
3968 - echo '<table style="width:100%">
3969 - <thead>
3970 - <tr>
3971 - <th style="text-align:left;">' . __( 'Date', 'propertyhive' ) . ' / ' . __( 'Time', 'propertyhive' ) . '</th>
3972 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
3973 - <th style="text-align:left;">' . __( 'Attending Negotiator(s)', 'propertyhive' ) . '</th>
3974 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3975 - </tr>
3976 - </thead>
3977 - <tbody>';
3978 -
3979 - while ( $viewings_query->have_posts() )
3980 - {
3981 - $viewings_query->the_post();
3982 -
3983 - $property = new PH_Property((int)get_post_meta(get_the_ID(), '_property_id', TRUE));
3984 -
3985 - echo '<tr>';
3986 - echo '<td style="text-align:left;"><a href="' . get_edit_post_link( get_the_ID(), '') . '">' . date("H:i jS F Y", strtotime(get_post_meta(get_the_ID(), '_start_date_time', TRUE))) . '</a></td>';
3987 - echo '<td style="text-align:left;">';
3988 - if ( get_post_meta(get_the_ID(), '_property_id', TRUE) != '' )
3989 - {
3990 - echo '<a href="' . get_edit_post_link( get_post_meta(get_the_ID(), '_property_id', TRUE), '' ) . '">' . $property->get_formatted_full_address() . '</a>';
3991 - }
3992 - else
3993 - {
3994 - echo '-';
3995 - }
3996 - echo '</td>';
3997 -
3998 - echo '<td style="text-align:left;">';
3999 -
4000 - $negotiator_ids = get_post_meta(get_the_ID(), '_negotiator_id');
4001 -
4002 - if (!empty($negotiator_ids))
4003 - {
4004 - $i = 0;
4005 - foreach ($negotiator_ids as $negotiator_id)
4006 - {
4007 - if ( $i > 0 ) { echo ', '; }
4008 -
4009 - $userdata = get_userdata( $negotiator_id );
4010 - if ( $userdata !== FALSE )
4011 - {
4012 - echo $userdata->display_name;
4013 - }
4014 - else
4015 - {
4016 - echo '<em>Unknown user</em>';
4017 - }
4018 - ++$i;
4019 - }
4020 - }
4021 - else
4022 - {
4023 - echo 'Unattended';
4024 - }
4025 -
4026 - echo '</td>';
4027 - echo '<td style="text-align:left;">';
4028 -
4029 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
4030 - echo ucwords(str_replace("_", " ", $status));
4031 - if ( $status == 'pending' )
4032 - {
4033 - echo '<br>';
4034 - // confirmation status
4035 - if ( get_post_meta(get_the_ID(), '_all_confirmed', TRUE) == 'yes' )
4036 - {
4037 - echo __( 'All Parties Confirmed', 'propertyhive' );
4038 - }
4039 - else
4040 - {
4041 - echo __( 'Awaiting Confirmation', 'propertyhive' );
4042 - }
4043 - }
4044 - if ( $status == 'carried_out' )
4045 - {
4046 - echo '<br>';
4047 - $feedback_status = get_post_meta(get_the_ID(), '_feedback_status', TRUE);
4048 - switch ( get_post_meta(get_the_ID(), '_feedback_status', TRUE) )
4049 - {
4050 - case "interested": { echo 'Applicant Interested'; break; }
4051 - case "not_interested": { echo 'Applicant Not Interested'; break; }
4052 - case "not_required": { echo 'Feedback Not Required'; break; }
4053 - default: { echo 'Awaiting Feedback'; }
4054 - }
4055 -
4056 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' )
4057 - {
4058 - $feedback_passed_on = get_post_meta(get_the_ID(), '_feedback_passed_on', TRUE);
4059 - echo '<br>' . ( ($feedback_passed_on == 'yes') ? 'Feedback Passed On' : 'Feedback Not Passed On' );
4060 - }
4061 - }
4062 - echo '</td>';
4063 - echo '</tr>';
4064 - }
4065 -
4066 - echo '
4067 - </tbody>
4068 - </table>
4069 - <br>';
4070 - }
4071 - else
4072 - {
4073 - echo '<p>' . __( 'No viewings exist for this contact', 'propertyhive') . '</p>';
4074 - }
4075 - wp_reset_postdata();
4076 -
4077 7119 do_action('propertyhive_contact_viewings_fields');
4078 -
4079 - echo '</div>';
4080 -
4081 - echo '</div>';
4082 7120
7121 + // Quit out
4083 7122 die();
4084 7123 }
4085 7124
4086 7125 // Offer related functions
@@ -4089,10 +7128,19 @@
4089 7128 check_ajax_referer( 'record-offer', 'security' );
4090 7129
4091 7130 $this->json_headers();
4092 7131
4093 - // TO DO: Should do validation on server side also
4094 - if (empty($_POST['property_id']))
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)
4095 7143 {
4096 7144 $return = array('error' => 'No property selected');
4097 7145 echo json_encode( $return );
4098 7146 die();
@@ -4097,18 +7145,18 @@
4097 7145 echo json_encode( $return );
4098 7146 die();
4099 7147 }
4100 7148
4101 - $property = new PH_Property((int)$_POST['property_id']);
7149 + $property = new PH_Property($property_id);
4102 7150
4103 7151 $applicant_contact_ids = array();
4104 7152
4105 7153 // Create applicant record if required
4106 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
7154 + if (empty($input['applicant_ids']) && !empty($input['applicant_name']))
4107 7155 {
4108 7156 // Need to create contact/applicant
4109 7157 $contact_post = array(
4110 - 'post_title' => ph_clean($_POST['applicant_name']),
7158 + 'post_title' => $input['applicant_name'],
4111 7159 'post_content' => '',
4112 7160 'post_type' => 'contact',
4113 7161 'post_status' => 'publish',
4114 7162 'comment_status' => 'closed',
@@ -4115,9 +7163,9 @@
4115 7163 'ping_status' => 'closed',
4116 7164 );
4117 7165
4118 7166 // Insert the post into the database
4119 - $contact_post_id = wp_insert_post( $contact_post );
7167 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
4120 7168
4121 7169 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
4122 7170 {
4123 7171 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -4126,8 +7174,27 @@
4126 7174 }
4127 7175
4128 7176 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
4129 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 +
4130 7197 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
4131 7198 update_post_meta( $contact_post_id, '_applicant_profile_0', array( 'department' => $property->department, 'send_matching_properties' => '' ) );
4132 7199
4133 7200 $applicant_contact_ids[] = $contact_post_id;
@@ -4132,18 +7199,13 @@
4132 7199
4133 7200 $applicant_contact_ids[] = $contact_post_id;
4134 7201 }
4135 7202
4136 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
7203 + if (!empty($input['applicant_ids']) && empty($input['applicant_name']))
4137 7204 {
4138 7205 // This is an existing contact
4139 - if ( !is_array($_POST['applicant_ids']) )
7206 + foreach ( $input['applicant_ids'] as $applicant_id )
4140 7207 {
4141 - $_POST['applicant_ids'] = array($_POST['applicant_ids']);
4142 - }
4143 -
4144 - foreach ( $_POST['applicant_ids'] as $applicant_id )
4145 - {
4146 7208 $applicant_contact_ids[] = (int)$applicant_id;
4147 7209 }
4148 7210 }
4149 7211
@@ -4179,15 +7241,35 @@
4179 7241 echo json_encode( $return );
4180 7242 die();
4181 7243 }
4182 7244
4183 - $amount = preg_replace("/[^0-9]/", '', $_POST['amount']);
7245 + $amount = $input['amount'];
4184 7246
4185 - add_post_meta( $offer_post_id, '_offer_date_time', ph_clean($_POST['offer_date']) . ' ' . ph_clean($_POST['offer_time']) );
4186 - add_post_meta( $offer_post_id, '_property_id', (int)$_POST['property_id'] );
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 );
4187 7249 add_post_meta( $offer_post_id, '_applicant_contact_id', $applicant_contact_id );
4188 7250 add_post_meta( $offer_post_id, '_amount', $amount );
4189 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 + }
4190 7272 }
4191 7273
4192 7274 $applicant_contacts = array();
4193 7275 foreach ( $applicant_contact_ids as $applicant_contact_id )
@@ -4217,10 +7299,16 @@
4217 7299 check_ajax_referer( 'record-offer', 'security' );
4218 7300
4219 7301 $this->json_headers();
4220 7302
4221 - // TO DO: Should do validation on server side also
4222 - if (empty($_POST['contact_id']))
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)
4223 7311 {
4224 7312 $return = array('error' => 'No contact selected');
4225 7313 echo json_encode( $return );
4226 7314 die();
@@ -4225,9 +7313,9 @@
4225 7313 echo json_encode( $return );
4226 7314 die();
4227 7315 }
4228 7316
4229 - if (empty($_POST['property_ids']))
7317 + if (empty($input['property_ids']))
4230 7318 {
4231 7319 $return = array('error' => 'No property selected');
4232 7320 echo json_encode( $return );
4233 7321 die();
@@ -4234,9 +7322,9 @@
4234 7322 }
4235 7323
4236 7324 // Loop through contacts and create one offer each
4237 7325 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
4238 - foreach ( $_POST['property_ids'] as $property_id )
7326 + foreach ( $input['property_ids'] as $property_id )
4239 7327 {
4240 7328 // Insert offer record
4241 7329 $offer_post = array(
4242 7330 'post_title' => '',
@@ -4256,19 +7344,39 @@
4256 7344 echo json_encode( $return );
4257 7345 die();
4258 7346 }
4259 7347
4260 - $amount = preg_replace("/[^0-9]/", '', ph_clean($_POST['amount']));
7348 + $amount = $input['amount'];
4261 7349
4262 - add_post_meta( $offer_post_id, '_offer_date_time', ph_clean($_POST['offer_date']) . ' ' . ph_clean($_POST['offer_time']) );
7350 + add_post_meta( $offer_post_id, '_offer_date_time', $input['offer_date'] . ' ' . $input['offer_time'] );
4263 7351 add_post_meta( $offer_post_id, '_property_id', (int)$property_id );
4264 - add_post_meta( $offer_post_id, '_applicant_contact_id', (int)$_POST['contact_id'] );
7352 + add_post_meta( $offer_post_id, '_applicant_contact_id', $contact_id );
4265 7353 add_post_meta( $offer_post_id, '_amount', $amount );
4266 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 + }
4267 7375 }
4268 7376
4269 7377 $properties = array();
4270 - foreach ( $_POST['property_ids'] as $property_id )
7378 + foreach ( $input['property_ids'] as $property_id )
4271 7379 {
4272 7380 $properties[] = array(
4273 7381 'ID' => (int)$property_id,
4274 7382 'post_title' => get_the_title((int)$property_id),
@@ -4294,12 +7402,14 @@
4294 7402 global $post;
4295 7403
4296 7404 check_ajax_referer( 'offer-details-meta-box', 'security' );
4297 7405
4298 - $post = get_post((int)$_POST['offer_id']);
7406 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4299 7407
4300 - $offer = new PH_Offer((int)$_POST['offer_id']);
7408 + $post = get_post( $post_id );
4301 7409
7410 + $offer = new PH_Offer( $post_id );
7411 +
4302 7412 echo '<div class="propertyhive_meta_box">';
4303 7413
4304 7414 echo '<div class="options_group">';
4305 7415
@@ -4306,11 +7416,11 @@
4306 7416 if ( $offer->status != '' )
4307 7417 {
4308 7418 echo '<p class="form-field">
4309 7419
4310 - <label for="">' . __('Status', 'propertyhive') . '</label>
7420 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
4311 7421
4312 - ' . ucwords(str_replace("_", " ", $offer->status)) . '
7422 + ' . esc_html(propertyhive_get_status_label( $offer->status )) . '
4313 7423
4314 7424 </p>';
4315 7425 }
4316 7426
@@ -4316,32 +7426,32 @@
4316 7426
4317 7427 $offer_date_time = $offer->offer_date_time;
4318 7428 if ( empty($offer_date_time) )
4319 7429 {
4320 - $offer_date_time = date("Y-m-d H:i:s");
7430 + $offer_date_time = gmdate("Y-m-d H:i:s");
4321 7431 }
4322 7432
4323 7433 echo '<p class="form-field offer_date_time_field">
4324 7434
4325 - <label for="_offer_date">' . __('Offer Date / Time', 'propertyhive') . '</label>
7435 + <label for="_offer_date">' . esc_html(__('Offer Date / Time', 'propertyhive')) . '</label>
4326 7436
4327 - <input type="text" id="_offer_date" name="_offer_date" class="date-picker short" placeholder="yyyy-mm-dd" style="width:120px;" value="' . date("Y-m-d", strtotime($offer_date_time)) . '">
7437 + <input type="date" class="small" name="_offer_date" id="_offer_date" value="' . esc_attr(gmdate("Y-m-d", strtotime($offer_date_time))) . '" placeholder="">
4328 7438 <select id="_offer_time_hours" name="_offer_time_hours" class="select short" style="width:55px">';
4329 7439
4330 7440 if ( empty($offer_date_time) )
4331 7441 {
4332 - $value = date("H");
7442 + $value = gmdate("H");
4333 7443 }
4334 7444 else
4335 7445 {
4336 - $value = date( "H", strtotime( $offer_date_time ) );
7446 + $value = gmdate( "H", strtotime( $offer_date_time ) );
4337 7447 }
4338 7448 for ( $i = 0; $i < 23; ++$i )
4339 7449 {
4340 7450 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
4341 - echo '<option value="' . $j . '"';
7451 + echo '<option value="' . esc_attr($j) . '"';
4342 7452 if ($i == $value) { echo ' selected'; }
4343 - echo '>' . $j . '</option>';
7453 + echo '>' . esc_html($j) . '</option>';
4344 7454 }
4345 7455
4346 7456 echo '</select>
4347 7457 :
@@ -4352,16 +7462,16 @@
4352 7462 $value = '';
4353 7463 }
4354 7464 else
4355 7465 {
4356 - $value = date( "i", strtotime( $offer_date_time ) );
7466 + $value = gmdate( "i", strtotime( $offer_date_time ) );
4357 7467 }
4358 7468 for ( $i = 0; $i < 60; $i+=5 )
4359 7469 {
4360 7470 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
4361 - echo '<option value="' . $j . '"';
7471 + echo '<option value="' . esc_attr($j) . '"';
4362 7472 if ($i == $value) { echo ' selected'; }
4363 - echo '>' . $j . '</option>';
7473 + echo '>' . esc_html($j) . '</option>';
4364 7474 }
4365 7475
4366 7476 echo '</select>
4367 7477
@@ -4371,9 +7481,9 @@
4371 7481 'id' => '_amount',
4372 7482 'label' => __( 'Offer Amount', 'propertyhive' ) . ' (&pound;)',
4373 7483 'desc_tip' => false,
4374 7484 'class' => 'short',
4375 - 'value' => ( is_numeric($offer->amount) ? number_format($offer->amount) : '' ),
7485 + 'value' => ( is_numeric($offer->amount) ? ph_display_price_field( $offer->amount ) : '' ),
4376 7486 'custom_attributes' => array(
4377 7487 //'style' => 'width:95%; max-width:500px;'
4378 7488 )
4379 7489 );
@@ -4391,12 +7501,28 @@
4391 7501 public function get_offer_actions()
4392 7502 {
4393 7503 check_ajax_referer( 'offer-actions', 'security' );
4394 7504
4395 - $post_id = (int)$_POST['offer_id'];
7505 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4396 7506
4397 7507 $status = get_post_meta( $post_id, '_status', TRUE );
4398 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 +
4399 7525 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_offer_actions_meta_box">
4400 7526
4401 7527 <div class="options_group" style="padding-top:8px;">';
4402 7528
@@ -4413,8 +7539,13 @@
4413 7539 href="#action_panel_offer_declined"
4414 7540 class="button button-danger offer-action"
4415 7541 style="width:100%; margin-bottom:7px; text-align:center"
4416 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>';
4417 7548 }
4418 7549
4419 7550 if ( $status == 'accepted' )
4420 7551 {
@@ -4427,9 +7558,9 @@
4427 7558
4428 7559 if ( $sale_id != '' )
4429 7560 {
4430 7561 $actions[] = '<a
4431 - href="' . get_edit_post_link( $sale_id, '' ) . '"
7562 + href="' . esc_url(get_edit_post_link( $sale_id, '' )) . '"
4432 7563 class="button"
4433 7564 style="width:100%; margin-bottom:7px; text-align:center"
4434 7565 >' . wp_kses_post( __('View Sale', 'propertyhive') ) . '</a>';
4435 7566 }
@@ -4435,22 +7566,23 @@
4435 7566 }
4436 7567 else
4437 7568 {
4438 7569 $actions[] = '<a
4439 - href="' . wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), '1', 'create_sale' ) . '"
4440 - class="button button-success"
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"
4441 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);"
4442 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>';
4443 7580 }
4444 7581 }
4445 7582
4446 - if ( $status == 'declined' )
7583 + if ( $status == 'accepted' || $status == 'declined' || $status == 'withdrawn' )
4447 7584 {
4448 -
4449 - }
4450 -
4451 - if ( $status == 'accepted' || $status == 'declined' )
4452 - {
4453 7585 $actions[] = '<a
4454 7586 href="#action_panel_offer_revert_pending"
4455 7587 class="button offer-action"
4456 7588 style="width:100%; margin-bottom:7px; text-align:center"
@@ -4457,16 +7589,18 @@
4457 7589 >' . wp_kses_post( __('Revert To Pending', 'propertyhive') ) . '</a>';
4458 7590 }
4459 7591
4460 7592 $actions = apply_filters( 'propertyhive_admin_offer_actions', $actions, $post_id );
7593 + $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
4461 7594
4462 7595 if ( !empty($actions) )
4463 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.
4464 7598 echo implode("", $actions);
4465 7599 }
4466 7600 else
4467 7601 {
4468 - echo '<div style="text-align:center">' . __( 'No actions to display', 'propertyhive' ) . '</div>';
7602 + echo '<div style="text-align:center">' . esc_html(__( 'No actions to display', 'propertyhive' )) . '</div>';
4469 7603 }
4470 7604
4471 7605 echo '</div>
4472 7606
@@ -4478,9 +7612,9 @@
4478 7612 public function offer_accepted()
4479 7613 {
4480 7614 check_ajax_referer( 'offer-actions', 'security' );
4481 7615
4482 - $post_id = (int)$_POST['offer_id'];
7616 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4483 7617
4484 7618 $status = get_post_meta( $post_id, '_status', TRUE );
4485 7619
4486 7620 if ( $status == 'pending' )
@@ -4493,11 +7627,13 @@
4493 7627 'action' => 'offer_accepted',
4494 7628 );
4495 7629
4496 7630 PH_Comments::insert_note( $post_id, $comment );
7631 +
7632 + wp_send_json_success();
4497 7633 }
4498 7634
4499 - die();
7635 + wp_send_json_error();
4500 7636 }
4501 7637
4502 7638 public function offer_declined()
4503 7639 {
@@ -4502,9 +7638,9 @@
4502 7638 public function offer_declined()
4503 7639 {
4504 7640 check_ajax_referer( 'offer-actions', 'security' );
4505 7641
4506 - $post_id = (int)$_POST['offer_id'];
7642 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4507 7643
4508 7644 $status = get_post_meta( $post_id, '_status', TRUE );
4509 7645
4510 7646 if ( $status == 'pending' )
@@ -4517,213 +7653,104 @@
4517 7653 'action' => 'offer_declined',
4518 7654 );
4519 7655
4520 7656 PH_Comments::insert_note( $post_id, $comment );
7657 +
7658 + wp_send_json_success();
4521 7659 }
4522 7660
4523 - die();
7661 + wp_send_json_error();
4524 7662 }
4525 7663
4526 - public function offer_revert_pending()
7664 + public function offer_withdrawn()
4527 7665 {
4528 7666 check_ajax_referer( 'offer-actions', 'security' );
4529 7667
4530 - $post_id = (int)$_POST['offer_id'];
7668 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4531 7669
4532 7670 $status = get_post_meta( $post_id, '_status', TRUE );
4533 7671
4534 - if ( $status == 'accepted' || $status == 'declined' )
7672 + if ( $status == 'pending' || $status == 'accepted' )
4535 7673 {
4536 - update_post_meta( $post_id, '_status', 'pending' );
7674 + update_post_meta( $post_id, '_status', 'withdrawn' );
4537 7675
4538 7676 // Add note/comment to offer
4539 7677 $comment = array(
4540 7678 'note_type' => 'action',
4541 - 'action' => 'offer_revert_pending',
7679 + 'action' => 'offer_withdrawn',
4542 7680 );
4543 7681
4544 7682 PH_Comments::insert_note( $post_id, $comment );
7683 +
7684 + wp_send_json_success();
4545 7685 }
4546 7686
4547 - die();
7687 + wp_send_json_error();
4548 7688 }
4549 7689
4550 - public function get_property_offers_meta_box()
7690 + public function offer_revert_pending()
4551 7691 {
4552 - check_ajax_referer( 'get_property_offers_meta_box', 'security' );
7692 + check_ajax_referer( 'offer-actions', 'security' );
4553 7693
4554 - global $post;
7694 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4555 7695
4556 - echo '<div class="propertyhive_meta_box">';
4557 -
4558 - echo '<div class="options_group">';
7696 + $status = get_post_meta( $post_id, '_status', TRUE );
4559 7697
4560 - $args = array(
4561 - 'post_type' => 'offer',
4562 - 'nopaging' => true,
4563 - 'orderby' => 'meta_value',
4564 - 'order' => 'DESC',
4565 - 'meta_key' => '_offer_date_time',
4566 - 'post_status' => 'publish',
4567 - 'meta_query' => array(
4568 - array(
4569 - 'key' => '_property_id',
4570 - 'value' => (int)$_POST['post_id']
4571 - )
4572 - )
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',
4573 7706 );
4574 - $offers_query = new WP_Query( $args );
4575 7707
4576 - if ( $offers_query->have_posts() )
4577 - {
4578 - echo '<table style="width:100%">
4579 - <thead>
4580 - <tr>
4581 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
4582 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
4583 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
4584 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
4585 - </tr>
4586 - </thead>
4587 - <tbody>';
7708 + PH_Comments::insert_note( $post_id, $comment );
4588 7709
4589 - while ( $offers_query->have_posts() )
4590 - {
4591 - $offers_query->the_post();
7710 + wp_send_json_success();
7711 + }
4592 7712
4593 - $offer = new PH_Offer(get_the_ID());
7713 + wp_send_json_error();
7714 + }
4594 7715
4595 - echo '<tr>';
4596 - echo '<td style="text-align:left;"><a href="' . get_edit_post_link( get_the_ID(), '' ) . '">' . date("jS F Y", strtotime(get_post_meta(get_the_ID(), '_offer_date_time', TRUE))) . '</a></td>';
4597 - echo '<td style="text-align:left;"><a href="' . get_edit_post_link( get_post_meta(get_the_ID(), '_applicant_contact_id', TRUE), '' ) . '">' . get_the_title(get_post_meta(get_the_ID(), '_applicant_contact_id', TRUE)) . '</a></td>';
4598 - echo '<td style="text-align:left;">' . $offer->get_formatted_amount() . '</td>';
4599 - echo '<td style="text-align:left;">';
4600 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
4601 - echo ucwords(str_replace("_", " ", $status));
4602 - echo '</td>';
4603 - echo '</tr>';
4604 - }
7716 + public function get_property_offers_meta_box()
7717 + {
7718 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
4605 7719
4606 - echo '
4607 - </tbody>
4608 - </table>
4609 - <br>';
4610 - }
4611 - else
4612 - {
4613 - echo '<p>' . __( 'No offers exist for this property', 'propertyhive') . '</p>';
4614 - }
4615 - wp_reset_postdata();
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 + }
4616 7727
7728 + include( PH()->plugin_path() . '/includes/admin/views/html-property-offers-meta-box.php' );
7729 +
4617 7730 do_action('propertyhive_property_offers_fields');
4618 -
4619 - echo '</div>';
4620 -
4621 - echo '</div>';
4622 7731
7732 + // Quit out
4623 7733 die();
4624 7734 }
4625 7735
4626 7736 public function get_contact_offers_meta_box()
4627 7737 {
4628 - check_ajax_referer( 'get_contact_offers_meta_box', 'security' );
7738 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
4629 7739
4630 - global $post;
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 + }
4631 7747
4632 - echo '<div class="propertyhive_meta_box">';
4633 -
4634 - echo '<div class="options_group">';
7748 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-offers-meta-box.php' );
4635 7749
4636 - $args = array(
4637 - 'post_type' => 'offer',
4638 - 'nopaging' => true,
4639 - 'orderby' => 'meta_value',
4640 - 'order' => 'DESC',
4641 - 'post_status' => 'publish',
4642 - 'meta_key' => '_offer_date_time',
4643 - 'meta_query' => array(
4644 - array(
4645 - 'key' => '_applicant_contact_id',
4646 - 'value' => (int)$_POST['post_id']
4647 - )
4648 - )
4649 - );
4650 - $offers_query = new WP_Query( $args );
4651 -
4652 - if ( $offers_query->have_posts() )
4653 - {
4654 - echo '<table style="width:100%">
4655 - <thead>
4656 - <tr>
4657 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
4658 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
4659 - <th style="text-align:left;">' . __( 'Property Owner', 'propertyhive' ) . '</th>
4660 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
4661 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
4662 - </tr>
4663 - </thead>
4664 - <tbody>';
4665 -
4666 - while ( $offers_query->have_posts() )
4667 - {
4668 - $offers_query->the_post();
4669 -
4670 - $property = new PH_Property((int)get_post_meta(get_the_ID(), '_property_id', TRUE));
4671 - $offer = new PH_Offer(get_the_ID());
4672 -
4673 - echo '<tr>';
4674 - echo '<td style="text-align:left;"><a href="' . get_edit_post_link( get_the_ID(), '') . '">' . date("jS F Y", strtotime(get_post_meta(get_the_ID(), '_offer_date_time', TRUE))) . '</a></td>';
4675 - echo '<td style="text-align:left;"><a href="' . get_edit_post_link( get_post_meta(get_the_ID(), '_property_id', TRUE), '' ) . '">' . $property->get_formatted_full_address() . '</a></td>';
4676 - echo '<td style="text-align:left;">';
4677 -
4678 - $owner_contact_ids = $property->_owner_contact_id;
4679 - if (
4680 - ( !is_array($owner_contact_ids) && $owner_contact_ids != '' && $owner_contact_ids != 0 )
4681 - ||
4682 - ( is_array($owner_contact_ids) && !empty($owner_contact_ids) )
4683 - )
4684 - {
4685 - if ( !is_array($owner_contact_ids) )
4686 - {
4687 - $owner_contact_ids = array($owner_contact_ids);
4688 - }
4689 -
4690 - foreach ( $owner_contact_ids as $owner_contact_id )
4691 - {
4692 - echo get_the_title($owner_contact_id) . '<br>';
4693 - echo '<div style="color:#BBB">';
4694 - echo 'T: ' . get_post_meta($owner_contact_id, '_telephone_number', TRUE) . '<br>';
4695 - echo 'E: ' . get_post_meta($owner_contact_id, '_email_address', TRUE);
4696 - echo '</div>';
4697 - }
4698 - }
4699 -
4700 - echo '</td>';
4701 - echo '<td style="text-align:left;">' . $offer->get_formatted_amount() . '</td>';
4702 - echo '<td style="text-align:left;">';
4703 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
4704 - echo ucwords(str_replace("_", " ", $status));
4705 - echo '</td>';
4706 - echo '</tr>';
4707 - }
4708 -
4709 - echo '
4710 - </tbody>
4711 - </table>
4712 - <br>';
4713 - }
4714 - else
4715 - {
4716 - echo '<p>' . __( 'No offers exist for this contact', 'propertyhive') . '</p>';
4717 - }
4718 - wp_reset_postdata();
4719 -
4720 7750 do_action('propertyhive_contact_offers_fields');
4721 -
4722 - echo '</div>';
4723 -
4724 - echo '</div>';
4725 7751
7752 + // Quit out
4726 7753 die();
4727 7754 }
4728 7755
4729 7756 // Sale related functions
@@ -4732,12 +7759,14 @@
4732 7759 global $post;
4733 7760
4734 7761 check_ajax_referer( 'sale-details-meta-box', 'security' );
4735 7762
4736 - $post = get_post((int)$_POST['sale_id']);
7763 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
4737 7764
4738 - $sale = new PH_Offer((int)$_POST['sale_id']);
7765 + $post = get_post( $post_id );
4739 7766
7767 + $sale = new PH_Offer( $post_id );
7768 +
4740 7769 echo '<div class="propertyhive_meta_box">';
4741 7770
4742 7771 echo '<div class="options_group">';
4743 7772
@@ -4744,11 +7773,11 @@
4744 7773 if ( $sale->status != '' )
4745 7774 {
4746 7775 echo '<p class="form-field">
4747 7776
4748 - <label for="">' . __('Status', 'propertyhive') . '</label>
7777 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
4749 7778
4750 - ' . ucwords(str_replace("_", " ", $sale->status)) . '
7779 + ' . esc_html(propertyhive_get_status_label( $sale->status )) . '
4751 7780
4752 7781 </p>';
4753 7782 }
4754 7783
@@ -4754,17 +7783,17 @@
4754 7783
4755 7784 $sale_date_time = $sale->sale_date_time;
4756 7785 if ( empty($sale_date_time) )
4757 7786 {
4758 - $sale_date_time = date("Y-m-d H:i:s");
7787 + $sale_date_time = gmdate("Y-m-d H:i:s");
4759 7788 }
4760 7789
4761 7790 echo '<p class="form-field sale_date_field">
4762 7791
4763 - <label for="_sale_date">' . __('Sale Date', 'propertyhive') . '</label>
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="">
4764 7795
4765 - <input type="text" id="_sale_date" name="_sale_date" class="date-picker short" placeholder="yyyy-mm-dd" style="width:120px;" value="' . date("Y-m-d", strtotime($sale_date_time)) . '">
4766 -
4767 7796 </p>';
4768 7797
4769 7798 $args = array(
4770 7799 'id' => '_amount',
@@ -4770,9 +7799,9 @@
4770 7799 'id' => '_amount',
4771 7800 'label' => __( 'Sale Amount', 'propertyhive' ) . ' (&pound;)',
4772 7801 'desc_tip' => false,
4773 7802 'class' => 'short',
4774 - 'value' => ( is_numeric($sale->amount) ? number_format($sale->amount) : '' ),
7803 + 'value' => ( is_numeric($sale->amount) ? ph_display_price_field( $sale->amount ) : '' ),
4775 7804 'custom_attributes' => array(
4776 7805 //'style' => 'width:95%; max-width:500px;'
4777 7806 )
4778 7807 );
@@ -4790,12 +7819,28 @@
4790 7819 public function get_sale_actions()
4791 7820 {
4792 7821 check_ajax_referer( 'sale-actions', 'security' );
4793 7822
4794 - $post_id = (int)$_POST['sale_id'];
7823 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
4795 7824
4796 7825 $status = get_post_meta( $post_id, '_status', TRUE );
4797 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 +
4798 7843 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_sale_actions_meta_box">
4799 7844
4800 7845 <div class="options_group" style="padding-top:8px;">';
4801 7846
@@ -4806,9 +7851,9 @@
4806 7851 $actions[] = '<a
4807 7852 href="#action_panel_sale_exchanged"
4808 7853 class="button button-success sale-action"
4809 7854 style="width:100%; margin-bottom:7px; text-align:center"
4810 - >' . __('Sale Exchanged', 'propertyhive') . '</a>';
7855 + >' . esc_html(__('Sale Exchanged', 'propertyhive')) . '</a>';
4811 7856
4812 7857 }
4813 7858
4814 7859 if ( $status == 'exchanged' )
@@ -4816,9 +7861,9 @@
4816 7861 $actions[] = '<a
4817 7862 href="#action_panel_sale_completed"
4818 7863 class="button button-success sale-action"
4819 7864 style="width:100%; margin-bottom:7px; text-align:center"
4820 - >' . __('Sale Completed', 'propertyhive') . '</a>';
7865 + >' . esc_html(__('Sale Completed', 'propertyhive')) . '</a>';
4821 7866 }
4822 7867
4823 7868 if ( $status == 'completed' )
4824 7869 {
@@ -4830,20 +7875,22 @@
4830 7875 $actions[] = '<a
4831 7876 href="#action_panel_sale_fallen_through"
4832 7877 class="button sale-action"
4833 7878 style="width:100%; margin-bottom:7px; text-align:center"
4834 - >' . __('Sale Fallen Through', 'propertyhive') . '</a>';
7879 + >' . esc_html(__('Sale Fallen Through', 'propertyhive')) . '</a>';
4835 7880 }
4836 7881
4837 7882 $actions = apply_filters( 'propertyhive_admin_sale_actions', $actions, $post_id );
7883 + $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
4838 7884
4839 7885 if ( !empty($actions) )
4840 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.
4841 7888 echo implode("", $actions);
4842 7889 }
4843 7890 else
4844 7891 {
4845 - echo '<div style="text-align:center">' . __( 'No actions to display', 'propertyhive' ) . '</div>';
7892 + echo '<div style="text-align:center">' . esc_html(__( 'No actions to display', 'propertyhive' )) . '</div>';
4846 7893 }
4847 7894
4848 7895 echo '</div>
4849 7896
@@ -4855,9 +7902,9 @@
4855 7902 public function sale_exchanged()
4856 7903 {
4857 7904 check_ajax_referer( 'sale-actions', 'security' );
4858 7905
4859 - $post_id = (int)$_POST['sale_id'];
7906 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
4860 7907
4861 7908 $status = get_post_meta( $post_id, '_status', TRUE );
4862 7909
4863 7910 if ( $status == 'current' )
@@ -4870,11 +7917,13 @@
4870 7917 'action' => 'sale_exchanged',
4871 7918 );
4872 7919
4873 7920 PH_Comments::insert_note( $post_id, $comment );
7921 +
7922 + wp_send_json_success();
4874 7923 }
4875 7924
4876 - die();
7925 + wp_send_json_error();
4877 7926 }
4878 7927
4879 7928 public function sale_completed()
4880 7929 {
@@ -4879,9 +7928,9 @@
4879 7928 public function sale_completed()
4880 7929 {
4881 7930 check_ajax_referer( 'sale-actions', 'security' );
4882 7931
4883 - $post_id = (int)$_POST['sale_id'];
7932 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
4884 7933
4885 7934 $status = get_post_meta( $post_id, '_status', TRUE );
4886 7935
4887 7936 if ( $status == 'exchanged' )
@@ -4894,11 +7943,13 @@
4894 7943 'action' => 'sale_completed',
4895 7944 );
4896 7945
4897 7946 PH_Comments::insert_note( $post_id, $comment );
7947 +
7948 + wp_send_json_success();
4898 7949 }
4899 7950
4900 - die();
7951 + wp_send_json_error();
4901 7952 }
4902 7953
4903 7954 public function sale_fallen_through()
4904 7955 {
@@ -4903,9 +7954,9 @@
4903 7954 public function sale_fallen_through()
4904 7955 {
4905 7956 check_ajax_referer( 'sale-actions', 'security' );
4906 7957
4907 - $post_id = (int)$_POST['sale_id'];
7958 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
4908 7959
4909 7960 $status = get_post_meta( $post_id, '_status', TRUE );
4910 7961
4911 7962 if ( $status == 'current' || $status == 'exchanged' )
@@ -4918,191 +7969,735 @@
4918 7969 'action' => 'sale_fallen_through',
4919 7970 );
4920 7971
4921 7972 PH_Comments::insert_note( $post_id, $comment );
7973 +
7974 + wp_send_json_success();
4922 7975 }
4923 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
4924 7997 die();
4925 7998 }
4926 7999
4927 - public function get_property_sales_meta_box()
8000 + public function get_contact_sales_meta_box()
4928 8001 {
4929 - check_ajax_referer( 'get_property_sales_meta_box', 'security' );
8002 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
4930 8003
4931 - global $post;
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 + }
4932 8011
4933 - echo '<div class="propertyhive_meta_box">';
4934 -
4935 - echo '<div class="options_group">';
8012 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-sales-meta-box.php' );
4936 8013
4937 - $args = array(
4938 - 'post_type' => 'sale',
4939 - 'nopaging' => true,
4940 - 'orderby' => 'meta_value',
4941 - 'order' => 'DESC',
4942 - 'meta_key' => '_sale_date_time',
4943 - 'post_status' => 'publish',
4944 - 'meta_query' => array(
4945 - array(
4946 - 'key' => '_property_id',
4947 - 'value' => (int)$_POST['post_id']
4948 - )
4949 - )
4950 - );
4951 - $sales_query = new WP_Query( $args );
8014 + do_action('propertyhive_contact_sales_fields');
4952 8015
4953 - if ( $sales_query->have_posts() )
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 )
4954 8167 {
4955 - echo '<table style="width:100%">
4956 - <thead>
4957 - <tr>
4958 - <th style="text-align:left;">' . __( 'Sale Date', 'propertyhive' ) . '</th>
4959 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
4960 - <th style="text-align:left;">' . __( 'Sale Amount', 'propertyhive' ) . '</th>
4961 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
4962 - </tr>
4963 - </thead>
4964 - <tbody>';
8168 + list($key, $value) = explode('=', $key_value_pair);
8169 + $recurrence[strtolower($key)] = $value;
8170 + }
4965 8171
4966 - while ( $sales_query->have_posts() )
8172 + if ( isset($recurrence['freq']) && $recurrence['freq'] != 'ONCE' )
8173 + {
8174 + $interval = isset($recurrence['interval']) ? $recurrence['interval'] : '1';
8175 + switch( $recurrence['freq'] )
4967 8176 {
4968 - $sales_query->the_post();
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 + }
4969 8190
4970 - $sale = new PH_Sale(get_the_ID());
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 + }
4971 8198
4972 - echo '<tr>';
4973 - echo '<td style="text-align:left;"><a href="' . get_edit_post_link( get_the_ID(), '' ) . '">' . date("jS F Y", strtotime(get_post_meta(get_the_ID(), '_sale_date_time', TRUE))) . '</a></td>';
4974 - echo '<td style="text-align:left;"><a href="' . get_edit_post_link( get_post_meta(get_the_ID(), '_applicant_contact_id', TRUE), '' ) . '">' . get_the_title(get_post_meta(get_the_ID(), '_applicant_contact_id', TRUE)) . '</a></td>';
4975 - echo '<td style="text-align:left;">' . $sale->get_formatted_amount() . '</td>';
4976 - echo '<td style="text-align:left;">';
4977 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
4978 - echo ucwords(str_replace("_", " ", $status));
4979 - echo '</td>';
4980 - echo '</tr>';
4981 - }
8199 + echo esc_html($next_key_date);
4982 8200
4983 - echo '
4984 - </tbody>
4985 - </table>
4986 - <br>';
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 );
4987 8222 }
4988 - else
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 )
4989 8260 {
4990 - echo '<p>' . __( 'No sales exist for this property', 'propertyhive') . '</p>';
8261 + $return = array('error' => 'Failed to create next key date post. Please try again');
8262 + echo json_encode( $return );
8263 + die();
4991 8264 }
4992 - wp_reset_postdata();
4993 8265
4994 - do_action('propertyhive_property_sales_fields');
4995 -
4996 - echo '</div>';
4997 -
4998 - echo '</div>';
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'] ) );
4999 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 +
5000 8279 die();
5001 8280 }
5002 8281
5003 - public function get_contact_sales_meta_box()
8282 + public function delete_key_date()
5004 8283 {
5005 - check_ajax_referer( 'get_contact_sales_meta_box', 'security' );
8284 + check_ajax_referer( 'delete-key-date', 'security' );
5006 8285
5007 - global $post;
8286 + $this->json_headers();
5008 8287
5009 - echo '<div class="propertyhive_meta_box">';
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 + }
5010 8412
5011 - echo '<div class="options_group">';
8413 + // check plugin status
8414 + $slug = isset( $_POST['slug'] ) && is_string( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
5012 8415
5013 - $args = array(
5014 - 'post_type' => 'sale',
5015 - 'nopaging' => true,
5016 - 'orderby' => 'meta_value',
5017 - 'order' => 'DESC',
5018 - 'post_status' => 'publish',
5019 - 'meta_key' => '_sale_date_time',
5020 - 'meta_query' => array(
5021 - array(
5022 - 'key' => '_applicant_contact_id',
5023 - 'value' => (int)$_POST['post_id']
5024 - )
5025 - )
8416 + $feature = get_ph_pro_feature( $slug );
8417 +
8418 + if ( $feature === false )
8419 + {
8420 + $return = array(
8421 + 'errorMessage' => 'Feature not found'
5026 8422 );
5027 - $sales_query = new WP_Query( $args );
8423 + wp_send_json_error($return);
8424 + }
5028 8425
5029 - if ( $sales_query->have_posts() )
5030 - {
5031 - echo '<table style="width:100%">
5032 - <thead>
5033 - <tr>
5034 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
5035 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
5036 - <th style="text-align:left;">' . __( 'Property Owner', 'propertyhive' ) . '</th>
5037 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
5038 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
5039 - </tr>
5040 - </thead>
5041 - <tbody>';
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 + }
5042 8433
5043 - while ( $sales_query->have_posts() )
5044 - {
5045 - $sales_query->the_post();
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 + }
5046 8440
5047 - $sale = new PH_Sale(get_the_ID());
8441 + // check it's not a pro feature if they don't have pro enabled
8442 + if ( $pro )
8443 + {
8444 + $valid_license_key = false;
5048 8445
5049 - $property = new PH_Property((int)get_post_meta(get_the_ID(), '_property_id', TRUE));
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 + }
5050 8457
5051 - echo '<tr>';
5052 - echo '<td style="text-align:left;"><a href="' . get_edit_post_link( get_the_ID(), '') . '">' . date("jS F Y", strtotime(get_post_meta(get_the_ID(), '_sale_date_time', TRUE))) . '</a></td>';
5053 - echo '<td style="text-align:left;"><a href="' . get_edit_post_link( get_post_meta(get_the_ID(), '_property_id', TRUE), '' ) . '">' . $property->get_formatted_full_address() . '</a></td>';
5054 - echo '<td style="text-align:left;">';
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();
5055 8464
5056 - $owner_contact_ids = $property->_owner_contact_id;
8465 + if ( isset($product_id_and_package['success']) && $product_id_and_package['success'] === true )
8466 + {
5057 8467 if (
5058 - ( !is_array($owner_contact_ids) && $owner_contact_ids != '' && $owner_contact_ids != 0 )
5059 - ||
5060 - ( is_array($owner_contact_ids) && !empty($owner_contact_ids) )
8468 + isset($feature['plans']) &&
8469 + isset($product_id_and_package['package']) &&
8470 + in_array($product_id_and_package['package'], $feature['plans'])
5061 8471 )
5062 8472 {
5063 - if ( !is_array($owner_contact_ids) )
5064 - {
5065 - $owner_contact_ids = array($owner_contact_ids);
5066 - }
5067 -
5068 - foreach ( $owner_contact_ids as $owner_contact_id )
5069 - {
5070 - echo get_the_title($owner_contact_id) . '<br>';
5071 - echo '<div style="color:#BBB">';
5072 - echo 'T: ' . get_post_meta($owner_contact_id, '_telephone_number', TRUE) . '<br>';
5073 - echo 'E: ' . get_post_meta($owner_contact_id, '_email_address', TRUE);
5074 - echo '</div>';
5075 - }
8473 + $valid_license_key = true;
5076 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 + }
5077 8499
5078 - echo '</td>';
5079 - echo '<td style="text-align:left;">' . $sale->get_formatted_amount() . '</td>';
5080 - echo '<td style="text-align:left;">';
5081 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
5082 - echo ucwords(str_replace("_", " ", $status));
5083 - echo '</td>';
5084 - echo '</tr>';
5085 - }
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 + }
5086 8508
5087 - echo '
5088 - </tbody>
5089 - </table>
5090 - <br>';
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 + );
5091 8522 }
5092 8523 else
5093 8524 {
5094 - echo '<p>' . __( 'No sales exist for this contact', 'propertyhive') . '</p>';
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 + );
5095 8543 }
5096 - wp_reset_postdata();
5097 8544
5098 - do_action('propertyhive_contact_sales_fields');
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
5099 8562
5100 - echo '</div>';
5101 -
5102 - echo '</div>';
8563 + if ( empty($zip_contents) )
8564 + {
8565 + $return = array(
8566 + 'errorMessage' => 'Failed to obtain plugin'
8567 + );
8568 + wp_send_json_error($return);
8569 + }
5103 8570
5104 - die();
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();
5105 8700 }
5106 8701 }
5107 8702
5108 8703 new PH_AJAX();