PluginProbe
Property Hive / 2.4.0
Property Hive v2.4.0
2.4.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 All 262 releases
← All changes | includes/class-ph-ajax.php +5015 -1419 1.4.56 → 2.4.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
@@ -1122,14 +1914,18 @@
1122 1914 // Quit out
1123 1915 die();
1124 1916 }
1125 1917
1126 - public function search_contacts_where( $where, &$wp_query )
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( like_escape( 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,38 +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 - $where .= " AND (
1229 - (mt1.meta_key='_address_name_number' AND mt1.meta_value LIKE '" . esc_sql(ph_clean($_POST['keyword'])) . "%')
1230 - OR
1231 - (mt1.meta_key='_address_street' AND mt1.meta_value LIKE '" . esc_sql(ph_clean($_POST['keyword'])) . "%')
1232 - OR
1233 - (mt1.meta_key='_address_2' AND mt1.meta_value LIKE '" . esc_sql(ph_clean($_POST['keyword'])) . "%')
1234 - OR
1235 - (mt1.meta_key='_address_3' AND mt1.meta_value LIKE '" . esc_sql(ph_clean($_POST['keyword'])) . "%')
1236 - OR
1237 - (mt1.meta_key='_address_4' AND mt1.meta_value LIKE '" . esc_sql(ph_clean($_POST['keyword'])) . "%')
1238 - OR
1239 - (mt1.meta_key='_address_postcode' AND mt1.meta_value LIKE '" . esc_sql(ph_clean($_POST['keyword'])) . "%')
1240 - OR
1241 - (mt1.meta_key='_reference_number' AND mt1.meta_value = '" . esc_sql(ph_clean($_POST['keyword'])) . "')
1242 - ) ";
1243 -
1244 - return $where;
1245 - }
1246 -
1247 2064 /**
1248 2065 * Search users/negotiators via ajax
1249 2066 */
1250 2067 public function search_negotiators() {
@@ -1254,9 +2071,9 @@
1254 2071 check_ajax_referer( 'search-negotiators', 'security' );
1255 2072
1256 2073 $return = array();
1257 2074
1258 - $keyword = ph_clean($_POST['keyword']);
2075 + $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
1259 2076
1260 2077 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1261 2078 {
1262 2079 // Get all contacts that match the name
@@ -1263,10 +2080,13 @@
1263 2080 $args = array(
1264 2081 'number' => 9999,
1265 2082 'search' => $keyword . '*',
1266 2083 'orderby' => 'display_name',
1267 - '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') )
1268 2086 );
2087 +
2088 + $args = apply_filters( 'propertyhive_negotiators_query', $args );
1269 2089
1270 2090 $user_query = new WP_User_Query( $args );
1271 2091
1272 2092 // Get the results
@@ -1298,15 +2118,34 @@
1298 2118 */
1299 2119 public function add_note() {
1300 2120
1301 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 );
1302 2125
1303 - $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 + }
1304 2130
1305 2131 if ( $post_id > 0 ) {
1306 2132
1307 - $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'] ) );
1308 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 +
1309 2148 // Add note/comment to property
1310 2149 $comment = array(
1311 2150 'note_type' => 'note',
1312 2151 'note' => $note
@@ -1311,8 +2150,13 @@
1311 2150 'note_type' => 'note',
1312 2151 'note' => $note
1313 2152 );
1314 2153
2154 + if ( isset($_POST['pinned']) )
2155 + {
2156 + $comment['pinned'] = '1';
2157 + }
2158 +
1315 2159 $comment_id = PH_Comments::insert_note( $post_id, $comment );
1316 2160
1317 2161 if ($comment_id !== FALSE)
1318 2162 {
@@ -1319,14 +2163,14 @@
1319 2163 $comment = get_comment($comment_id);
1320 2164 ?>
1321 2165 <li rel="<?php echo absint( $comment_id ) ; ?>" class="note">
1322 2166 <div class="note_content">
1323 - <?php echo wpautop( wptexturize( wp_kses_post( $note ) ) ); ?>
2167 + <?php echo wp_kses_post( wpautop( wptexturize( wp_kses_post( $note ) ) ) ); ?>
1324 2168 </div>
1325 2169 <p class="meta">
1326 - <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>
1327 - <?php if ( $comment->comment_author !== __( 'Property Hive', 'propertyhive' ) ) printf( ' ' . __( 'by %s', 'propertyhive' ), $comment->comment_author ); ?>
1328 - <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>
1329 2173 </p>
1330 2174 </li>
1331 2175 <?php
1332 2176 }
@@ -1333,9 +2177,9 @@
1333 2177 }
1334 2178
1335 2179 // Quit out
1336 2180 die();
1337 - }
2181 + }
1338 2182
1339 2183 /**
1340 2184 * Delete order note via ajax
1341 2185 */
@@ -1342,19 +2186,229 @@
1342 2186 public function delete_note() {
1343 2187
1344 2188 check_ajax_referer( 'delete-note', 'security' );
1345 2189
1346 - $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 );
1347 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 +
1348 2199 if ( $note_id > 0 ) {
1349 2200 wp_delete_comment( $note_id );
2201 +
2202 + wp_send_json_success();
1350 2203 }
1351 2204
1352 - // Quit out
1353 - die();
2205 + wp_send_json_error();
1354 2206 }
1355 -
2207 +
1356 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 + /**
1357 2411 * Delete order note via ajax
1358 2412 */
1359 2413 public function make_property_enquiry() {
1360 2414
@@ -1365,11 +2419,12 @@
1365 2419 // Validate
1366 2420 $errors = array();
1367 2421 $form_controls = array();
1368 2422
1369 - 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'] ) )
1370 2425 {
1371 - $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' );
1372 2427 }
1373 2428 else
1374 2429 {
1375 2430 //$post = get_post((int)$_POST['property_id']);
@@ -1375,9 +2430,10 @@
1375 2430 //$post = get_post((int)$_POST['property_id']);
1376 2431
1377 2432 $form_controls = ph_get_property_enquiry_form_fields();
1378 2433
1379 - $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'] ) ) );
1380 2436 }
1381 2437
1382 2438 foreach ( $form_controls as $key => $control )
1383 2439 {
@@ -1383,30 +2439,38 @@
1383 2439 {
1384 2440 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
1385 2441 {
1386 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.
1387 2444 if ( ! isset( $_POST[$key] ) || ( isset( $_POST[$key] ) && empty( $_POST[$key] ) ) )
1388 2445 {
1389 2446 $errors[] = __( 'Missing required field', 'propertyhive' ) . ': ' . $key;
1390 2447 }
1391 2448 }
1392 - 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] ) ) ) )
1393 2451 {
1394 2452 $errors[] = __( 'Invalid email address provided', 'propertyhive' ) . ': ' . $key;
1395 2453 }
1396 - if ( $key == 'recaptcha' )
2454 + if ( in_array( $key, array('recaptcha', 'recaptcha-v3') ) )
1397 2455 {
2456 + $errors = $this->check_recaptcha_form_response($errors, $key, $control);
2457 + }
2458 + if ( $key == 'hCaptcha' )
2459 + {
1398 2460 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
1399 - $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'] ) ) : '';
1400 2463
1401 - $response = wp_remote_post(
1402 - 'https://www.google.com/recaptcha/api/siteverify',
2464 + $response = wp_remote_post(
2465 + 'https://hcaptcha.com/siteverify',
1403 2466 array(
1404 2467 'method' => 'POST',
1405 2468 'body' => array( 'secret' => $secret, 'response' => $response ),
1406 2469 )
1407 2470 );
1408 - if ( is_wp_error( $response ) )
2471 +
2472 + if ( is_wp_error( $response ) )
1409 2473 {
1410 2474 $errors[] = $response->get_error_message();
1411 2475 }
1412 2476 else
@@ -1413,9 +2477,9 @@
1413 2477 {
1414 2478 $response = json_decode($response['body'], TRUE);
1415 2479 if ( $response === FALSE )
1416 2480 {
1417 - $errors[] = 'Error decoding response from reCAPTCHA check';
2481 + $errors[] = __( 'Error decoding response from hCaptcha check', 'propertyhive' );
1418 2482 }
1419 2483 else
1420 2484 {
1421 2485 if ( isset($response['success']) && $response['success'] == true )
@@ -1423,15 +2487,123 @@
1423 2487
1424 2488 }
1425 2489 else
1426 2490 {
1427 - $errors[] = 'Failed reCAPTCHA validation';
2491 + $errors[] = __( 'Failed hCaptcha validation', 'propertyhive' );
1428 2492 }
1429 2493 }
1430 2494 }
1431 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 + }
1432 2537 }
1433 -
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 +
1434 2606 if ( !empty($errors) )
1435 2607 {
1436 2608 // Failed validation
1437 2609
@@ -1440,11 +2612,8 @@
1440 2612 $return['errors'] = $errors;
1441 2613 }
1442 2614 else
1443 2615 {
1444 - // Passed validation
1445 - $property_ids = explode("|", ph_clean($_POST['property_id']));
1446 -
1447 2616 // Get recipient email address
1448 2617 $to = '';
1449 2618
1450 2619 // Try and get office's email address first, else fallback to admin email
@@ -1478,8 +2647,16 @@
1478 2647 $fields_to_check[] = '_office_email_address_lettings';
1479 2648 $fields_to_check[] = '_office_email_address_sales';
1480 2649 break;
1481 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 + }
1482 2659 }
1483 2660
1484 2661 foreach ( $fields_to_check as $field_to_check )
1485 2662 {
@@ -1494,9 +2671,9 @@
1494 2671 if ( $to == '' )
1495 2672 {
1496 2673 $to = get_option( 'admin_email' );
1497 2674 }
1498 -
2675 +
1499 2676 if ( count($property_ids) == 1 )
1500 2677 {
1501 2678 $subject = __( 'New Property Enquiry', 'propertyhive' ) . ': ' . get_the_title( (int)$property_ids[0] );
1502 2679 }
@@ -1510,11 +2687,11 @@
1510 2687
1511 2688 $message .= ( count($property_ids) > 1 ? __( 'Properties', 'propertyhive' ) : __( 'Property', 'propertyhive' ) ) . ":\n";
1512 2689 foreach ( $property_ids as $property_id )
1513 2690 {
1514 - $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";
1515 2693 }
1516 - $message .= "\n";
1517 2694
1518 2695 unset($form_controls['action']);
1519 2696 unset($_POST['action']);
1520 2697 unset($form_controls['property_id']); // Unset so the field doesn't get shown in the enquiry details
@@ -1522,17 +2699,31 @@
1522 2699 $form_controls = apply_filters( 'propertyhive_property_enquiry_body_form_fields', $form_controls );
1523 2700
1524 2701 foreach ($form_controls as $key => $control)
1525 2702 {
1526 - 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; }
1527 2704
1528 2705 $label = ( isset($control['label']) ) ? $control['label'] : $key;
1529 2706 $label = ( isset($control['email_label']) ) ? $control['email_label'] : $label;
1530 - $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] ) ) : '';
1531 2709
1532 - $message .= strip_tags($label) . ": " . strip_tags($value) . "\n";
2710 + $message .= wp_strip_all_tags($label) . ": " . wp_strip_all_tags($value) . "\n";
1533 2711 }
1534 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 +
1535 2726 $message = apply_filters( 'propertyhive_property_enquiry_post_body', $message, $property_ids );
1536 2727
1537 2728 $from_email_address = get_option('propertyhive_email_from_address', '');
1538 2729 if ( $from_email_address == '' )
@@ -1541,31 +2732,54 @@
1541 2732 }
1542 2733 if ( $from_email_address == '' )
1543 2734 {
1544 2735 // Should never get here
1545 - $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'] ) ) : '';
1546 2738 }
1547 2739
1548 2740 $headers = array();
1549 - 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 !== '' )
1550 2752 {
1551 - $headers[] = 'From: ' . ph_clean( $_POST['name'] ) . ' <' . sanitize_email( $from_email_address ) . '>';
2753 + $headers[] = sprintf( 'From: %s <%s>', $name, $from_email_address );
1552 2754 }
1553 2755 else
1554 2756 {
1555 - $headers[] = 'From: <' . sanitize_email( $from_email_address ) . '>';
2757 + $headers[] = sprintf( 'From: <%s>', $from_email_address );
1556 2758 }
1557 - 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']) )
1558 2762 {
1559 - $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 + }
1560 2770 }
1561 2771
1562 2772 $to = apply_filters( 'propertyhive_property_enquiry_to', $to, $property_ids );
1563 2773 $subject = apply_filters( 'propertyhive_property_enquiry_subject', $subject, $property_ids );
2774 + $headers = apply_filters( 'propertyhive_property_enquiry_headers', $headers, $property_ids );
1564 2775 $message = apply_filters( 'propertyhive_property_enquiry_body', $message, $property_ids );
1565 - $headers = apply_filters( 'propertyhive_property_enquiry_headers', $headers, $property_ids );
2776 +
2777 + do_action( 'propertyhive_before_property_enquiry_sent' );
1566 2778
1567 2779 $sent = wp_mail( $to, $subject, $message, $headers );
2780 +
2781 + do_action( 'propertyhive_after_property_enquiry_sent' );
1568 2782
1569 2783 if ( ! $sent )
1570 2784 {
1571 2785 $return['success'] = false;
@@ -1574,8 +2788,10 @@
1574 2788 }
1575 2789 else
1576 2790 {
1577 2791 $return['success'] = true;
2792 +
2793 + $enquiry_post_id = '';
1578 2794
1579 2795 if ( get_option( 'propertyhive_store_property_enquiries', 'yes' ) == 'yes' )
1580 2796 {
1581 2797 // Now insert into enquiries section of WordPress
@@ -1586,11 +2802,13 @@
1586 2802 else
1587 2803 {
1588 2804 $title = __( 'Multiple Property Enquiry', 'propertyhive' );
1589 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.
1590 2807 if ( isset($_POST['name']) && ! empty($_POST['name']) )
1591 2808 {
1592 - $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']));
1593 2811 }
1594 2812
1595 2813 $enquiry_post = array(
1596 2814 'post_title' => $title,
@@ -1608,32 +2826,43 @@
1608 2826 add_post_meta( $enquiry_post_id, '_source', 'website' );
1609 2827 add_post_meta( $enquiry_post_id, '_negotiator_id', '' );
1610 2828 add_post_meta( $enquiry_post_id, '_office_id', $office_id );
1611 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.
1612 2831 foreach ($_POST as $key => $value)
1613 2832 {
1614 - 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 ) )
1615 2837 {
2838 + continue;
2839 + }
2840 +
2841 + if ( $meta_key == 'property_id' )
2842 + {
1616 2843 foreach ( $property_ids as $property_id )
1617 2844 {
1618 - add_post_meta( $enquiry_post_id, $key, (int)$property_id );
2845 + add_post_meta( $enquiry_post_id, $meta_key, (int)$property_id );
1619 2846 }
1620 2847 }
1621 2848 else
1622 2849 {
1623 - 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)) );
1624 2851 }
1625 2852 }
1626 2853 }
1627 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 +
1628 2858 // Send auto-responder
1629 2859 if ( get_option( 'propertyhive_enquiry_auto_responder', '' ) == 'yes' )
1630 2860 {
1631 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.
1632 2863 PH()->email->send_enquiry_auto_responder( $_POST );
1633 2864 }
1634 -
1635 - do_action('propertyhive_property_enquiry_sent', $_POST, $to);
1636 2865 }
1637 2866 }
1638 2867
1639 2868 $this->json_headers();
@@ -1649,12 +2878,14 @@
1649 2878 public function create_contact_from_enquiry()
1650 2879 {
1651 2880 global $post;
1652 2881
1653 - $enquiry_post_id = ( (isset($_POST['post_id'])) ? (int)$_POST['post_id'] : '' );
1654 - $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'] ) ) : '';
1655 2886
1656 - 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 ) )
1657 2888 {
1658 2889 // This nonce is not valid.
1659 2890 die( json_encode( array('error' => 'Invalid nonce. Please refresh and try again') ) );
1660 2891 }
@@ -1663,36 +2894,70 @@
1663 2894
1664 2895 $name = false;
1665 2896 $email = false;
1666 2897 $telephone = false;
2898 + $address = false;
2899 + $postcode = false;
2900 + $property_id = false;
1667 2901
1668 2902 foreach ($enquiry_meta as $key => $value)
1669 2903 {
1670 - if ( strpos($key, 'name') !== false )
2904 + if ( strpos(strtolower($key), 'name') !== false && strpos(strtolower($key), 'property') === false && $value[0] != '' )
1671 2905 {
1672 - $name = $value[0];
2906 + if ( $name === false )
2907 + {
2908 + $name = $value[0];
2909 + }
2910 + else
2911 + {
2912 + $name .= ' ' . $value[0];
2913 + }
1673 2914 }
1674 - elseif ( strpos($key, 'email') !== false )
2915 + elseif ( strpos(strtolower($key), 'email') !== false && $value[0] != '' )
1675 2916 {
1676 - $email = $value[0];
2917 + if ( $email === false )
2918 + {
2919 + $email = $value[0];
2920 + }
2921 + else
2922 + {
2923 + $email .= ',' . $value[0];
2924 + }
1677 2925 }
1678 - elseif ( strpos($key, 'telephone') !== false )
2926 + elseif ( strpos(strtolower($key), 'phone') !== false && $value[0] != '' )
1679 2927 {
1680 - $telephone = $value[0];
2928 + if ( $telephone === false )
2929 + {
2930 + $telephone = $value[0];
2931 + }
2932 + else
2933 + {
2934 + $telephone .= ',' . $value[0];
2935 + }
1681 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 + }
1682 2949 }
1683 2950
1684 - if ( $name === false || $email === false )
2951 + if ( $name === false && $email === false )
1685 2952 {
1686 - // This nonce is not valid.
1687 - die( json_encode( array('error' => 'Name or email address not found') ) );
2953 + die( json_encode( array('error' => 'Name and email address not found') ) );
1688 2954 }
1689 2955
1690 - // We've not imported this property before
1691 2956 $postdata = array(
1692 2957 'post_excerpt' => '',
1693 2958 'post_content' => '',
1694 - 'post_title' => utf8_encode(wp_strip_all_tags( $name )),
2959 + 'post_title' => wp_strip_all_tags( $name ),
1695 2960 'post_status' => 'publish',
1696 2961 'post_type' => 'contact',
1697 2962 'ping_status' => 'closed',
1698 2963 'comment_status' => 'closed',
@@ -1708,15 +2973,134 @@
1708 2973 {
1709 2974 die( json_encode( array('error' => 'Error creating contact') ) );
1710 2975 }
1711 2976
2977 + update_post_meta( $enquiry_post_id, '_contact_id', $contact_post_id );
2978 +
1712 2979 if ( $telephone !== FALSE ) {
1713 - update_post_meta( $contact_post_id, '_telephone_number', ph_clean( $telephone ) );
1714 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) ) );
1715 2982 }
1716 2983
1717 2984 if ( $email !== FALSE ) { update_post_meta( $contact_post_id, '_email_address', ph_clean( $email ) ); }
1718 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 +
1719 3103 do_action('propertyhive_create_contact_from_enquiry', $enquiry_post_id, $contact_post_id);
1720 3104
1721 3105 die( json_encode( array('success' => get_edit_post_link($contact_post_id, '')) ) );
1722 3106 }
@@ -1728,15 +3112,21 @@
1728 3112 check_ajax_referer( 'contact-save-validation', 'security' );
1729 3113
1730 3114 $this->json_headers();
1731 3115
1732 - 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;
1733 3123
1734 3124 $return = array('errors' => array());
1735 3125
1736 - if ( isset($_email_address) && $_email_address != '' )
3126 + if ( '' !== $email_address_input )
1737 3127 {
1738 - $email_addresses = explode( ",", $_email_address );
3128 + $email_addresses = explode( ",", $email_address_input );
1739 3129
1740 3130 foreach ( $email_addresses as $email_address )
1741 3131 {
1742 3132 $email_address = trim( $email_address );
@@ -1751,8 +3141,9 @@
1751 3141 'post_type' => 'contact',
1752 3142 'post_status' => 'any',
1753 3143 'posts_per_page' => 1,
1754 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.
1755 3146 'meta_query' => array(
1756 3147 'relation' => 'OR',
1757 3148 array(
1758 3149 'key' => '_email_address',
@@ -1771,11 +3162,12 @@
1771 3162 'compare' => 'LIKE'
1772 3163 )
1773 3164 )
1774 3165 );
1775 - if ( isset($post_ID) && $post_ID != '' )
3166 + if ( $contact_id )
1776 3167 {
1777 - $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 );
1778 3170 }
1779 3171
1780 3172 $contact_query = new WP_Query( $args );
1781 3173
@@ -1784,9 +3176,10 @@
1784 3176 while ( $contact_query->have_posts() )
1785 3177 {
1786 3178 $contact_query->the_post();
1787 3179
1788 - $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 );
1789 3182 }
1790 3183 }
1791 3184 }
1792 3185 }
@@ -1795,8 +3188,80 @@
1795 3188
1796 3189 die();
1797 3190 }
1798 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 +
1799 3264 // Dashboard related functions
1800 3265 public function get_news()
1801 3266 {
1802 3267 $this->json_headers();
@@ -1821,9 +3286,9 @@
1821 3286 foreach ( $rss_items as $item )
1822 3287 {
1823 3288 $return[] = array(
1824 3289 'title' => esc_html( $item->get_title() ),
1825 - 'permalink' => esc_url( $item->get_permalink() ),
3290 + 'permalink' => esc_url( $item->get_permalink() ) . '?src=dashboard',
1826 3291 'date' => $item->get_date('F d, Y')
1827 3292 );
1828 3293 }
1829 3294
@@ -1845,8 +3310,9 @@
1845 3310 $args = array(
1846 3311 'post_type' => 'viewing',
1847 3312 'fields' => 'ids',
1848 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.
1849 3315 'meta_query' => array(
1850 3316 array(
1851 3317 'key' => '_status',
1852 3318 'value' => 'carried_out'
@@ -1857,8 +3323,10 @@
1857 3323 )
1858 3324 )
1859 3325 );
1860 3326
3327 + $args = apply_filters( 'propertyhive_admin_dashboard_viewings_awaiting_applicant_feedback_args', $args );
3328 +
1861 3329 $viewings_query = new WP_Query( $args );
1862 3330
1863 3331 if ( $viewings_query->have_posts() )
1864 3332 {
@@ -1868,19 +3336,19 @@
1868 3336
1869 3337 $property_id = get_post_meta( get_the_ID(), '_property_id', TRUE );
1870 3338 $property = new PH_Property((int)$property_id);
1871 3339
1872 - $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' );
1873 3341
1874 3342 $return[] = array(
1875 3343 'ID' => get_the_ID(),
1876 3344 'edit_link' => get_edit_post_link( get_the_ID() ),
1877 3345 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
1878 - '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 ))),
1879 3347 'property_id' => $property_id,
1880 3348 'property_address' => $property->get_formatted_full_address(),
1881 - 'applicant_contact_id' => $applicant_contact_id,
1882 - '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] ),
1883 3351 );
1884 3352 }
1885 3353 }
1886 3354
@@ -1902,8 +3370,9 @@
1902 3370 $args = array(
1903 3371 'post_type' => 'viewing',
1904 3372 'fields' => 'ids',
1905 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.
1906 3375 'meta_query' => array(
1907 3376 array(
1908 3377 'key' => '_status',
1909 3378 'value' => 'pending'
@@ -1909,9 +3378,9 @@
1909 3378 'value' => 'pending'
1910 3379 ),
1911 3380 array(
1912 3381 'key' => '_start_date_time',
1913 - 'value' => date("Y-m-d H:i:s"),
3382 + 'value' => gmdate("Y-m-d H:i:s"),
1914 3383 'compare' => '>='
1915 3384 ),
1916 3385 array(
1917 3386 'key' => '_negotiator_id',
@@ -1919,8 +3388,11 @@
1919 3388 ),
1920 3389 )
1921 3390 );
1922 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 +
1923 3395 $viewings_query = new WP_Query( $args );
1924 3396
1925 3397 if ( $viewings_query->have_posts() )
1926 3398 {
@@ -1934,9 +3406,9 @@
1934 3406 $return[] = array(
1935 3407 'ID' => get_the_ID(),
1936 3408 'edit_link' => get_edit_post_link( get_the_ID() ),
1937 3409 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
1938 - '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 ))),
1939 3411 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
1940 3412 'title' => 'Viewing at ' . $property->get_formatted_full_address(),
1941 3413 );
1942 3414 }
@@ -1947,8 +3419,9 @@
1947 3419 $args = array(
1948 3420 'post_type' => 'appraisal',
1949 3421 'fields' => 'ids',
1950 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.
1951 3424 'meta_query' => array(
1952 3425 array(
1953 3426 'key' => '_status',
1954 3427 'value' => 'pending'
@@ -1954,9 +3427,9 @@
1954 3427 'value' => 'pending'
1955 3428 ),
1956 3429 array(
1957 3430 'key' => '_start_date_time',
1958 - 'value' => date("Y-m-d H:i:s"),
3431 + 'value' => gmdate("Y-m-d H:i:s"),
1959 3432 'compare' => '>='
1960 3433 ),
1961 3434 array(
1962 3435 'key' => '_negotiator_id',
@@ -1964,8 +3437,11 @@
1964 3437 ),
1965 3438 )
1966 3439 );
1967 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 +
1968 3444 $appraisals_query = new WP_Query( $args );
1969 3445
1970 3446 if ( $appraisals_query->have_posts() )
1971 3447 {
@@ -1978,9 +3454,9 @@
1978 3454 $return[] = array(
1979 3455 'ID' => get_the_ID(),
1980 3456 'edit_link' => get_edit_post_link( get_the_ID() ),
1981 3457 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
1982 - '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 ))),
1983 3459 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
1984 3460 'title' => 'Appraisal at ' . $appraisal->get_formatted_full_address(),
1985 3461 );
1986 3462 }
@@ -2005,39 +3481,265 @@
2005 3481
2006 3482 die();
2007 3483 }
2008 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 +
2009 3704 public function get_property_marketing_statistics_meta_box()
2010 3705 {
2011 3706 check_ajax_referer( 'get_property_marketing_statistics_meta_box', 'security' );
2012 3707
2013 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 + }
2014 3713
2015 - echo '<div class="propertyhive_meta_box">';
2016 -
2017 - echo '<div class="options_group">';
2018 3714
2019 - echo '<h3>' . __( 'Views On Website', 'propertyhive' ) . '</h3>';
2020 3715
2021 - $view_statistics = get_post_meta( (int)$_POST['post_id'], '_view_statistics', TRUE );
3716 +
3717 + $view_statistics = get_post_meta( $post_id, '_view_statistics', TRUE );
2022 3718 if ( !is_array($view_statistics) )
2023 3719 {
2024 3720 $view_statistics = array();
2025 3721 }
2026 3722
2027 - $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'));
2028 3724 $date_from = strtotime($date_from);
2029 3725
2030 - $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");
2031 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 + }
2032 3731
3732 + echo '<div class="propertyhive_meta_box"><div class="options_group">';
2033 3733 $view_statistics_output = array();
3734 + $total_views = 0;
2034 3735
2035 3736 for ($i = $date_from; $i <= $date_to; $i += 86400)
2036 3737 {
2037 - if ( isset($view_statistics[date("Y-m-d", $i)]) )
3738 + if ( isset($view_statistics[gmdate("Y-m-d", $i)]) )
2038 3739 {
2039 - $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)];
2040 3742 }
2041 3743 else
2042 3744 {
2043 3745 $view_statistics_output[] = array( $i * 1000, 0 );
@@ -2043,9 +3745,11 @@
2043 3745 $view_statistics_output[] = array( $i * 1000, 0 );
2044 3746 }
2045 3747 }
2046 3748
2047 - 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>';
2048 3752
2049 3753 echo '</div>';
2050 3754
2051 3755 echo '</div>';
@@ -2060,11 +3764,12 @@
2060 3764 global $post;
2061 3765
2062 3766 check_ajax_referer( 'appraisal-details-meta-box', 'security' );
2063 3767
2064 - $post = get_post((int)$_POST['appraisal_id']);
3768 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
3769 + $post = get_post( $post_id );
2065 3770
2066 - $appraisal = new PH_Appraisal((int)$_POST['appraisal_id']);
3771 + $appraisal = new PH_Appraisal( $post_id );
2067 3772
2068 3773 echo '<div class="propertyhive_meta_box">';
2069 3774
2070 3775 echo '<div class="options_group">';
@@ -2070,11 +3775,11 @@
2070 3775 echo '<div class="options_group">';
2071 3776
2072 3777 echo '<p class="form-field">
2073 3778
2074 - <label for="">' . __('Status', 'propertyhive') . '</label>
3779 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
2075 3780
2076 - ' . ucwords(str_replace("_", " ", $appraisal->status));
3781 + ' . esc_html(ucwords(str_replace("_", " ", $appraisal->status)));
2077 3782
2078 3783 echo '</p>';
2079 3784
2080 3785 if ( $appraisal->status == 'cancelled' )
@@ -2093,16 +3798,39 @@
2093 3798 }
2094 3799
2095 3800 if ( $appraisal->status == 'carried_out' || $appraisal->status == 'won' || $appraisal->status == 'instructed' )
2096 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 +
2097 3825 if ( $appraisal->department == 'residential-sales' )
2098 3826 {
2099 3827 $args = array(
2100 3828 'id' => '_valued_price',
2101 - 'label' => __( 'Valued Price', 'propertyhive' ) . ' (&pound;)',
3829 + 'label' => __( 'Valued Price', 'propertyhive' ) . ' (' . $currency_symbol . ')',
2102 3830 'desc_tip' => false,
2103 3831 'class' => 'short',
2104 - 'value' => $appraisal->valued_price,
3832 + 'value' => ph_display_price_field( $appraisal->valued_price ),
2105 3833 );
2106 3834 propertyhive_wp_text_input( $args );
2107 3835 }
2108 3836 elseif ( $appraisal->department == 'residential-lettings' )
@@ -2110,18 +3838,19 @@
2110 3838 $rent_frequency = $appraisal->valued_rent_frequency;
2111 3839
2112 3840 echo '<p class="form-field">
2113 3841
2114 - <label for="">' . __('Valued Rent', 'propertyhive') . ' (&pound;)</label>
3842 + <label for="">' . esc_html(__('Valued Rent', 'propertyhive')) . ' (' . esc_html($currency_symbol) . ')</label>
2115 3843
2116 - <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;">
2117 3845
2118 3846 <select id="_valued_rent_frequency" name="_valued_rent_frequency" class="select" style="width:auto">
2119 - <option value="pppw"' . ( ($rent_frequency == 'pppw') ? ' selected' : '') . '>' . __('Per Person Per Week', 'propertyhive') . '</option>
2120 - <option value="pw"' . ( ($rent_frequency == 'pw') ? ' selected' : '') . '>' . __('Per Week', 'propertyhive') . '</option>
2121 - <option value="pcm"' . ( ($rent_frequency == 'pcm' || $rent_frequency == '') ? ' selected' : '') . '>' . __('Per Calendar Month', 'propertyhive') . '</option>
2122 - <option value="pq"' . ( ($rent_frequency == 'pq') ? ' selected' : '') . '>' . __('Per Quarter', 'propertyhive') . '</option>
2123 - <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>
2124 3853 </select>
2125 3854
2126 3855 </p>';
2127 3856 }
@@ -2154,9 +3883,9 @@
2154 3883 public function get_appraisal_actions()
2155 3884 {
2156 3885 check_ajax_referer( 'appraisal-actions', 'security' );
2157 3886
2158 - $post_id = (int)$_POST['appraisal_id'];
3887 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2159 3888
2160 3889 $status = get_post_meta( $post_id, '_status', TRUE );
2161 3890 $department = get_post_meta( $post_id, '_department', TRUE );
2162 3891
@@ -2167,13 +3896,46 @@
2167 3896 $show_cancelled_meta_boxes = false;
2168 3897 $show_carried_out_meta_boxes = false;
2169 3898 $show_instructed_meta_boxes = false;
2170 3899 $show_lost_meta_boxes = false;
3900 + $show_customise_confirmation_meta_boxes = false;
2171 3901
2172 3902 $actions = array();
2173 3903
2174 3904 if ( $status == 'pending' )
2175 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 +
2176 3938 /*$actions[] = '<a
2177 3939 href=""
2178 3940 class="button"
2179 3941 style="width:100%; margin-bottom:7px; text-align:center"
@@ -2189,14 +3951,14 @@
2189 3951 $actions[] = '<a
2190 3952 href="#action_panel_appraisal_carried_out"
2191 3953 class="button button-success appraisal-action"
2192 3954 style="width:100%; margin-bottom:7px; text-align:center"
2193 - >' . __('Appraisal Carried Out', 'propertyhive') . '</a>';
3955 + >' . esc_html(__('Appraisal Carried Out', 'propertyhive')) . '</a>';
2194 3956 $actions[] = '<a
2195 3957 href="#action_panel_appraisal_cancelled"
2196 3958 class="button appraisal-action"
2197 3959 style="width:100%; margin-bottom:7px; text-align:center"
2198 - >' . __('Appraisal Cancelled', 'propertyhive') . '</a>';
3960 + >' . esc_html(__('Appraisal Cancelled', 'propertyhive')) . '</a>';
2199 3961
2200 3962 $show_cancelled_meta_boxes = true;
2201 3963 $show_carried_out_meta_boxes = true;
2202 3964 }
@@ -2206,15 +3968,15 @@
2206 3968 $actions[] = '<a
2207 3969 href="#action_panel_appraisal_won"
2208 3970 class="button button-success appraisal-action"
2209 3971 style="width:100%; margin-bottom:7px; text-align:center"
2210 - >' . __('Appraisal Won', 'propertyhive') . '</a>';
3972 + >' . esc_html(__('Appraisal Won', 'propertyhive')) . '</a>';
2211 3973
2212 3974 $actions[] = '<a
2213 3975 href="#action_panel_appraisal_lost"
2214 3976 class="button button-danger appraisal-action"
2215 3977 style="width:100%; margin-bottom:7px; text-align:center"
2216 - >' . __('Appraisal Lost', 'propertyhive') . '</a>';
3978 + >' . esc_html(__('Appraisal Lost', 'propertyhive')) . '</a>';
2217 3979
2218 3980 $show_lost_meta_boxes = true;
2219 3981 }
2220 3982
@@ -2223,9 +3985,9 @@
2223 3985 $actions[] = '<a
2224 3986 href="#action_panel_appraisal_instruct"
2225 3987 class="button button-success appraisal-action"
2226 3988 style="width:100%; margin-bottom:7px; text-align:center"
2227 - >' . __('Instruct Property', 'propertyhive') . '</a>';
3989 + >' . esc_html(__('Instruct Property', 'propertyhive')) . '</a>';
2228 3990
2229 3991 $show_instructed_meta_boxes = true;
2230 3992 }
2231 3993
@@ -2234,9 +3996,9 @@
2234 3996 $actions[] = '<a
2235 3997 href="#action_panel_appraisal_revert_carried_out"
2236 3998 class="button appraisal-action"
2237 3999 style="width:100%; margin-bottom:7px; text-align:center"
2238 - >' . __('Revert To Carried Out', 'propertyhive') . '</a>';
4000 + >' . esc_html(__('Revert To Carried Out', 'propertyhive')) . '</a>';
2239 4001 }
2240 4002
2241 4003 if ( $status == 'instructed' )
2242 4004 {
@@ -2242,12 +4004,12 @@
2242 4004 {
2243 4005 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
2244 4006
2245 4007 $actions[] = '<a
2246 - href="' . get_edit_post_link($property_id) . '"
4008 + href="' . esc_url(get_edit_post_link($property_id)) . '"
2247 4009 class="button"
2248 4010 style="width:100%; margin-bottom:7px; text-align:center"
2249 - >' . __('View Instructed Property', 'propertyhive') . '</a>';
4011 + >' . esc_html(__('View Instructed Property', 'propertyhive')) . '</a>';
2250 4012
2251 4013 /*$actions[] = '<a
2252 4014 href="#action_panel_appraisal_revert_won"
2253 4015 class="button appraisal-action"
@@ -2260,20 +4022,22 @@
2260 4022 $actions[] = '<a
2261 4023 href="#action_panel_appraisal_revert_pending"
2262 4024 class="button appraisal-action"
2263 4025 style="width:100%; margin-bottom:7px; text-align:center"
2264 - >' . __('Revert To Pending', 'propertyhive') . '</a>';
4026 + >' . esc_html(__('Revert To Pending', 'propertyhive')) . '</a>';
2265 4027 }
2266 4028
2267 4029 $actions = apply_filters( 'propertyhive_admin_appraisal_actions', $actions, $post_id );
4030 + $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
2268 4031
2269 4032 if ( !empty($actions) )
2270 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.
2271 4035 echo implode("", $actions);
2272 4036 }
2273 4037 else
2274 4038 {
2275 - 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>';
2276 4040 }
2277 4041
2278 4042 echo '</div>
2279 4043
@@ -2278,8 +4042,57 @@
2278 4042 echo '</div>
2279 4043
2280 4044 </div>';
2281 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 +
2282 4095 if ( $show_cancelled_meta_boxes )
2283 4096 {
2284 4097 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_cancelled" style="display:none;">
2285 4098
@@ -2286,16 +4099,16 @@
2286 4099 <div class="options_group" style="padding-top:8px;">
2287 4100
2288 4101 <div class="form-field">
2289 4102
2290 - <label for="_appraisal_cancelled_reason">' . __( 'Reason Cancelled', 'propertyhive' ) . '</label>
4103 + <label for="_appraisal_cancelled_reason">' . esc_html(__( 'Reason Cancelled', 'propertyhive' )) . '</label>
2291 4104
2292 - <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>
2293 4106
2294 4107 </div>
2295 4108
2296 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2297 - <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>
2298 4111
2299 4112 </div>
2300 4113
2301 4114 </div>';
@@ -2306,15 +4119,38 @@
2306 4119 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_carried_out" style="display:none;">
2307 4120
2308 4121 <div class="options_group" style="padding-top:8px;">';
2309 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 +
2310 4146 if ( $department == 'residential-sales' )
2311 4147 {
2312 4148 echo '<div class="form-field">
2313 4149
2314 - <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>
2315 4151
2316 - <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 )) . '">
2317 4153
2318 4154 </div>';
2319 4155 }
2320 4156 else
@@ -2321,25 +4157,26 @@
2321 4157 {
2322 4158 $rent_frequency = get_post_meta( $post_id, '_valued_rent_frequency', TRUE );
2323 4159 echo '<div class="form-field">
2324 4160
2325 - <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>
2326 4162
2327 - <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 )) . '">
2328 4164
2329 4165 <select id="_rent_frequency" name="_rent_frequency" class="select" style="width:100%">
2330 - <option value="pppw"' . ( ($rent_frequency == 'pppw') ? ' selected' : '') . '>' . __('Per Person Per Week', 'propertyhive') . '</option>
2331 - <option value="pw"' . ( ($rent_frequency == 'pw') ? ' selected' : '') . '>' . __('Per Week', 'propertyhive') . '</option>
2332 - <option value="pcm"' . ( ($rent_frequency == 'pcm' || $rent_frequency == '') ? ' selected' : '') . '>' . __('Per Calendar Month', 'propertyhive') . '</option>
2333 - <option value="pq"' . ( ($rent_frequency == 'pq') ? ' selected' : '') . '>' . __('Per Quarter', 'propertyhive') . '</option>
2334 - <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>
2335 4172 </select>
2336 4173
2337 4174 </div>';
2338 4175 }
2339 4176
2340 - echo '<a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2341 - <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>
2342 4179
2343 4180 </div>
2344 4181
2345 4182 </div>';
@@ -2350,12 +4187,12 @@
2350 4187 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_instruct" style="display:none;">
2351 4188
2352 4189 <div class="options_group" style="padding-top:8px;">';
2353 4190
2354 - 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>';
2355 4192
2356 - echo '<a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2357 - <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>
2358 4195
2359 4196 </div>
2360 4197
2361 4198 </div>';
@@ -2368,16 +4205,16 @@
2368 4205 <div class="options_group" style="padding-top:8px;">
2369 4206
2370 4207 <div class="form-field">
2371 4208
2372 - <label for="_lost_reason">' . __( 'Reason Lost', 'propertyhive' ) . '</label>
4209 + <label for="_lost_reason">' . esc_html(__( 'Reason Lost', 'propertyhive' )) . '</label>
2373 4210
2374 - <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>
2375 4212
2376 4213 </div>
2377 4214
2378 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2379 - <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>
2380 4217
2381 4218 </div>
2382 4219
2383 4220 </div>';
@@ -2389,34 +4226,57 @@
2389 4226 public function appraisal_carried_out()
2390 4227 {
2391 4228 check_ajax_referer( 'appraisal-actions', 'security' );
2392 4229
2393 - $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 + }
2394 4234
2395 4235 $status = get_post_meta( $post_id, '_status', TRUE );
2396 4236
2397 4237 if ( $status == 'pending' )
2398 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 + }
2399 4258 update_post_meta( $post_id, '_status', 'carried_out' );
2400 4259
2401 4260 if ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-sales' )
2402 4261 {
2403 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['price']));
4262 + $price = preg_replace("/[^0-9.]/", '', $valuation_input['price']);
2404 4263 update_post_meta( $post_id, '_valued_price', $price );
2405 4264 update_post_meta( $post_id, '_valued_price_actual', $price );
2406 4265 }
2407 4266 elseif ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-lettings' )
2408 4267 {
2409 - $rent = preg_replace("/[^0-9]/", '', ph_clean($_POST['rent']));
4268 + $rent = preg_replace("/[^0-9.]/", '', $valuation_input['rent']);
2410 4269 update_post_meta( $post_id, '_valued_rent', $rent );
2411 4270
2412 - 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'] );
2413 4272
2414 - switch (ph_clean($_POST['rent_frequency']))
4273 + switch ($valuation_input['rent_frequency'])
2415 4274 {
4275 + case "pd": { $price = ($rent * 365) / 12; break; }
2416 4276 case "pppw":
2417 4277 {
2418 - $bedrooms = get_post_meta( $postID, '_bedrooms', true );
4278 + $bedrooms = get_post_meta( $post_id, '_bedrooms', true );
2419 4279 if ( ( $bedrooms !== FALSE && $bedrooms != 0 && $bedrooms != '' ) && apply_filters( 'propertyhive_pppw_to_consider_bedrooms', true ) == true )
2420 4280 {
2421 4281 $price = (($rent * 52) / 12) * $bedrooms;
2422 4282 }
@@ -2440,11 +4300,13 @@
2440 4300 'action' => 'appraisal_carried_out',
2441 4301 );
2442 4302
2443 4303 PH_Comments::insert_note( $post_id, $comment );
4304 +
4305 + wp_send_json_success();
2444 4306 }
2445 4307
2446 - die();
4308 + wp_send_json_success();
2447 4309 }
2448 4310
2449 4311 public function appraisal_cancelled()
2450 4312 {
@@ -2449,16 +4311,24 @@
2449 4311 public function appraisal_cancelled()
2450 4312 {
2451 4313 check_ajax_referer( 'appraisal-actions', 'security' );
2452 4314
2453 - $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 + }
2454 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 +
2455 4325 $status = get_post_meta( $post_id, '_status', TRUE );
2456 4326
2457 4327 if ( $status == 'pending' )
2458 4328 {
2459 4329 update_post_meta( $post_id, '_status', 'cancelled' );
2460 - update_post_meta( $post_id, '_cancelled_reason', sanitize_textarea_field( $_POST['cancelled_reason'] ) );
4330 + update_post_meta( $post_id, '_cancelled_reason', wp_slash( $reason ) );
2461 4331
2462 4332 // Add note/comment to appraisal
2463 4333 $comment = array(
2464 4334 'note_type' => 'action',
@@ -2465,11 +4335,13 @@
2465 4335 'action' => 'appraisal_cancelled',
2466 4336 );
2467 4337
2468 4338 PH_Comments::insert_note( $post_id, $comment );
4339 +
4340 + wp_send_json_success();
2469 4341 }
2470 4342
2471 - die();
4343 + wp_send_json_error();
2472 4344 }
2473 4345
2474 4346 public function appraisal_won()
2475 4347 {
@@ -2474,9 +4346,12 @@
2474 4346 public function appraisal_won()
2475 4347 {
2476 4348 check_ajax_referer( 'appraisal-actions', 'security' );
2477 4349
2478 - $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 + }
2479 4354
2480 4355 $status = get_post_meta( $post_id, '_status', TRUE );
2481 4356
2482 4357 if ( $status == 'carried_out' )
@@ -2489,11 +4364,13 @@
2489 4364 'action' => 'appraisal_won',
2490 4365 );
2491 4366
2492 4367 PH_Comments::insert_note( $post_id, $comment );
4368 +
4369 + wp_send_json_success();
2493 4370 }
2494 4371
2495 - die();
4372 + wp_send_json_error();
2496 4373 }
2497 4374
2498 4375 public function appraisal_lost_reason()
2499 4376 {
@@ -2498,16 +4375,24 @@
2498 4375 public function appraisal_lost_reason()
2499 4376 {
2500 4377 check_ajax_referer( 'appraisal-actions', 'security' );
2501 4378
2502 - $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 + }
2503 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 +
2504 4389 $status = get_post_meta( $post_id, '_status', TRUE );
2505 4390
2506 4391 if ( $status == 'carried_out' )
2507 4392 {
2508 4393 update_post_meta( $post_id, '_status', 'lost' );
2509 - update_post_meta( $post_id, '_lost_reason', sanitize_textarea_field( $_POST['lost_reason'] ) );
4394 + update_post_meta( $post_id, '_lost_reason', wp_slash( $reason ) );
2510 4395
2511 4396 // Add note/comment to appraisal
2512 4397 $comment = array(
2513 4398 'note_type' => 'action',
@@ -2514,11 +4399,13 @@
2514 4399 'action' => 'appraisal_lost',
2515 4400 );
2516 4401
2517 4402 PH_Comments::insert_note( $post_id, $comment );
4403 +
4404 + wp_send_json_success();
2518 4405 }
2519 4406
2520 - die();
4407 + wp_send_json_error();
2521 4408 }
2522 4409
2523 4410 public function appraisal_instructed()
2524 4411 {
@@ -2523,9 +4410,9 @@
2523 4410 public function appraisal_instructed()
2524 4411 {
2525 4412 check_ajax_referer( 'appraisal-actions', 'security' );
2526 4413
2527 - $post_id = (int)$_POST['appraisal_id'];
4414 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2528 4415
2529 4416 $status = get_post_meta( $post_id, '_status', TRUE );
2530 4417
2531 4418 if ( $status == 'won' )
@@ -2569,10 +4456,10 @@
2569 4456 {
2570 4457 // Failed. Don't really know at the moment how to handle this
2571 4458
2572 4459 $return = array('error' => 'Failed to create property post. Please try again');
2573 - //echo json_encode( $return );
2574 - //die();
4460 + echo json_encode( $return );
4461 + die();
2575 4462 }
2576 4463 else
2577 4464 {
2578 4465 // Successfully added property post
@@ -2578,8 +4465,24 @@
2578 4465 // Successfully added property post
2579 4466
2580 4467 $department = get_post_meta( $post_id, '_department', TRUE );
2581 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 +
2582 4485 update_post_meta( $property_post_id, '_address_name_number', get_post_meta( $post_id, '_address_name_number', TRUE ) );
2583 4486 update_post_meta( $property_post_id, '_address_street', get_post_meta( $post_id, '_address_street', TRUE ) );
2584 4487 update_post_meta( $property_post_id, '_address_two', get_post_meta( $post_id, '_address_two', TRUE ) );
2585 4488 update_post_meta( $property_post_id, '_address_three', get_post_meta( $post_id, '_address_three', TRUE ) );
@@ -2598,36 +4501,70 @@
2598 4501 if ( get_post_meta( $post_id, '_address_four', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_four', TRUE ); }
2599 4502 if ( get_post_meta( $post_id, '_address_postcode', TRUE ) ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_postcode', TRUE ); }
2600 4503
2601 4504 $country = get_option( 'propertyhive_default_country', 'GB' );
2602 - $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
2603 -
2604 - $api_key = get_option('propertyhive_google_maps_api_key', '');
2605 - if ( $api_key != '' ) { $request_url .= "&key=" . $api_key; }
2606 4505
2607 - $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);
2608 4522
2609 - 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
2610 4537 {
2611 - $header = $response['headers']; // array of http header lines
2612 - $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
2613 4539
2614 - $xml = simplexml_load_string($body);
4540 + $api_key = get_option('propertyhive_google_maps_api_key', '');
4541 + if ( $api_key != '' ) { $request_url .= "&key=" . $api_key; }
2615 4542
2616 - if ( $xml !== FALSE )
4543 + $response = wp_remote_get($request_url);
4544 +
4545 + if ( is_array( $response ) && !is_wp_error( $response ) )
2617 4546 {
2618 - $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
2619 4549
2620 - if ($status == "OK")
4550 + $xml = simplexml_load_string($body);
4551 +
4552 + if ( $xml !== FALSE )
2621 4553 {
2622 - //request returned completed time to get lat / lng for storage
2623 - $lat = (string)$xml->result->geometry->location->lat;
2624 - $lng = (string)$xml->result->geometry->location->lng;
2625 -
2626 - if ($lat != '' && $lng != '')
4554 + $status = $xml->status; // Get the request status as google's api can return several responses
4555 +
4556 + if ($status == "OK")
2627 4557 {
2628 - update_post_meta( $post_id, '_latitude', $lat );
2629 - 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 + }
2630 4567 }
2631 4568 }
2632 4569 }
2633 4570 }
@@ -2640,9 +4577,9 @@
2640 4577 case "residential-sales":
2641 4578 {
2642 4579 update_post_meta( $property_post_id, '_currency', 'GBP' );
2643 4580
2644 - $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 ));
2645 4582 update_post_meta( $property_post_id, '_price', $price );
2646 4583
2647 4584 break;
2648 4585 }
@@ -2673,8 +4610,10 @@
2673 4610 wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'property_type', array("fields" => "ids") ), 'property_type' );
2674 4611 wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'parking', array("fields" => "ids") ), 'parking' );
2675 4612 wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'outside_space', array("fields" => "ids") ), 'outside_space' );
2676 4613
4614 + update_post_meta( $property_post_id, '_council_tax_band', get_post_meta( $post_id, '_council_tax_band', TRUE ) );
4615 +
2677 4616 $owner_contact_ids = get_post_meta( $post_id, '_property_owner_contact_id', TRUE );
2678 4617 if ( !is_array($owner_contact_ids) )
2679 4618 {
2680 4619 $owner_contact_ids = array($owner_contact_ids);
@@ -2698,12 +4637,13 @@
2698 4637 // get appraisals where this is the owner and where not instructed
2699 4638 $args = array(
2700 4639 'post_type' => 'appraisal',
2701 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.
2702 4642 'meta_query' => array(
2703 4643 array(
2704 4644 'key' => '_property_owner_contact_id',
2705 - 'value' => $post->ID,
4645 + 'value' => $owner_contact_id,
2706 4646 'compare' => '='
2707 4647 ),
2708 4648 array(
2709 4649 'key' => '_status',
@@ -2734,19 +4674,197 @@
2734 4674 'action' => 'appraisal_instructed',
2735 4675 );
2736 4676
2737 4677 PH_Comments::insert_note( $post_id, $comment );
4678 +
4679 + wp_send_json_success();
2738 4680 }
2739 4681 }
2740 4682
2741 - die();
4683 + wp_send_json_error();
2742 4684 }
2743 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 +
2744 4862 public function appraisal_revert_pending()
2745 4863 {
2746 4864 check_ajax_referer( 'appraisal-actions', 'security' );
2747 4865
2748 - $post_id = (int)$_POST['appraisal_id'];
4866 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2749 4867
2750 4868 $status = get_post_meta( $post_id, '_status', TRUE );
2751 4869
2752 4870 if ( $status == 'carried_out' || $status == 'cancelled' )
@@ -2759,11 +4877,13 @@
2759 4877 'action' => 'appraisal_revert_pending',
2760 4878 );
2761 4879
2762 4880 PH_Comments::insert_note( $post_id, $comment );
4881 +
4882 + wp_send_json_success();
2763 4883 }
2764 4884
2765 - die();
4885 + wp_send_json_error();
2766 4886 }
2767 4887
2768 4888 public function appraisal_revert_carried_out()
2769 4889 {
@@ -2768,9 +4888,9 @@
2768 4888 public function appraisal_revert_carried_out()
2769 4889 {
2770 4890 check_ajax_referer( 'appraisal-actions', 'security' );
2771 4891
2772 - $post_id = (int)$_POST['appraisal_id'];
4892 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2773 4893
2774 4894 $status = get_post_meta( $post_id, '_status', TRUE );
2775 4895
2776 4896 if ( $status == 'won' || $status == 'lost' )
@@ -2783,11 +4903,13 @@
2783 4903 'action' => 'appraisal_revert_carried_out',
2784 4904 );
2785 4905
2786 4906 PH_Comments::insert_note( $post_id, $comment );
4907 +
4908 + wp_send_json_success();
2787 4909 }
2788 4910
2789 - die();
4911 + wp_send_json_error();
2790 4912 }
2791 4913
2792 4914 public function appraisal_revert_won()
2793 4915 {
@@ -2792,9 +4914,9 @@
2792 4914 public function appraisal_revert_won()
2793 4915 {
2794 4916 check_ajax_referer( 'appraisal-actions', 'security' );
2795 4917
2796 - $post_id = (int)$_POST['appraisal_id'];
4918 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2797 4919
2798 4920 $status = get_post_meta( $post_id, '_status', TRUE );
2799 4921
2800 4922 if ( $status == 'instructed' )
@@ -2807,11 +4929,13 @@
2807 4929 'action' => 'appraisal_revert_won',
2808 4930 );
2809 4931
2810 4932 PH_Comments::insert_note( $post_id, $comment );
4933 +
4934 + wp_send_json_success();
2811 4935 }
2812 4936
2813 - die();
4937 + wp_send_json_error();
2814 4938 }
2815 4939
2816 4940 // Viewing related functions
2817 4941 public function book_viewing_property()
@@ -2819,10 +4943,11 @@
2819 4943 check_ajax_referer( 'book-viewing', 'security' );
2820 4944
2821 4945 $this->json_headers();
2822 4946
2823 - // TO DO: Should do validation on server side also
2824 - 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)
2825 4950 {
2826 4951 $return = array('error' => 'No property selected');
2827 4952 echo json_encode( $return );
2828 4953 die();
@@ -2827,18 +4952,26 @@
2827 4952 echo json_encode( $return );
2828 4953 die();
2829 4954 }
2830 4955
2831 - $property = new PH_Property((int)$_POST['property_id']);
4956 + $property = new PH_Property( $property_id );
2832 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 + }
2833 4966 $applicant_contact_ids = array();
2834 4967
2835 4968 // Create applicant record if required
2836 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
4969 + if (empty($booking['applicant_ids']) && !empty($booking['applicant_name']))
2837 4970 {
2838 4971 // Need to create contact/applicant
2839 4972 $contact_post = array(
2840 - 'post_title' => ph_clean($_POST['applicant_name']),
4973 + 'post_title' => $booking['applicant_name'],
2841 4974 'post_content' => '',
2842 4975 'post_type' => 'contact',
2843 4976 'post_status' => 'publish',
2844 4977 'comment_status' => 'closed',
@@ -2845,9 +4978,9 @@
2845 4978 'ping_status' => 'closed',
2846 4979 );
2847 4980
2848 4981 // Insert the post into the database
2849 - $contact_post_id = wp_insert_post( $contact_post );
4982 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
2850 4983
2851 4984 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
2852 4985 {
2853 4986 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -2856,8 +4989,27 @@
2856 4989 }
2857 4990
2858 4991 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
2859 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 +
2860 5012 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
2861 5013 update_post_meta( $contact_post_id, '_applicant_profile_0', array( 'department' => $property->department, 'send_matching_properties' => '' ) );
2862 5014
2863 5015 $applicant_contact_ids[] = $contact_post_id;
@@ -2862,20 +5014,12 @@
2862 5014
2863 5015 $applicant_contact_ids[] = $contact_post_id;
2864 5016 }
2865 5017
2866 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
5018 + if (!empty($booking['applicant_ids']) && empty($booking['applicant_name']))
2867 5019 {
2868 5020 // This is an existing contact
2869 - if ( !is_array($_POST['applicant_ids']) )
2870 - {
2871 - $_POST['applicant_ids'] = array(ph_clean($_POST['applicant_ids']));
2872 - }
2873 -
2874 - foreach ( $_POST['applicant_ids'] as $applicant_id )
2875 - {
2876 - $applicant_contact_ids[] = (int)$applicant_id;
2877 - }
5021 + $applicant_contact_ids = $booking['applicant_ids'];
2878 5022 }
2879 5023
2880 5024 $applicant_contact_ids = array_unique($applicant_contact_ids);
2881 5025
@@ -2940,53 +5084,37 @@
2940 5084 update_post_meta( $applicant_contact_id, '_applicant_profile_' . $num_applicant_profiles, array( 'department' => $property->department ) );
2941 5085 }
2942 5086 }*/
2943 5087
2944 - // Loop through contacts and create one viewing each
2945 - // At the moment it's a 1-to-1 relationship, but might support multiple in the future
2946 - foreach ( $applicant_contact_ids as $applicant_contact_id )
2947 - {
2948 - // Insert viewing record
2949 - $viewing_post = array(
2950 - 'post_title' => '',
2951 - 'post_content' => '',
2952 - 'post_type' => 'viewing',
2953 - 'post_status' => 'publish',
2954 - 'comment_status' => 'closed',
2955 - 'ping_status' => 'closed',
2956 - );
2957 -
2958 - // Insert the post into the database
2959 - $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 + );
2960 5097
2961 - if ( is_wp_error($viewing_post_id) || $viewing_post_id == 0 )
2962 - {
2963 - $return = array('error' => 'Failed to create viewing post. Please try again');
2964 - echo json_encode( $return );
2965 - die();
2966 - }
2967 -
2968 - add_post_meta( $viewing_post_id, '_start_date_time', ph_clean($_POST['start_date']) . ' ' . ph_clean($_POST['start_time']) );
2969 - add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
2970 - add_post_meta( $viewing_post_id, '_property_id', (int)$_POST['property_id'] );
2971 - add_post_meta( $viewing_post_id, '_applicant_contact_id', $applicant_contact_id );
2972 - add_post_meta( $viewing_post_id, '_status', 'pending' );
2973 - add_post_meta( $viewing_post_id, '_feedback_status', '' );
2974 - add_post_meta( $viewing_post_id, '_feedback', '' );
2975 - 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 );
2976 5100
2977 - if ( !empty($_POST['negotiator_ids']) )
2978 - {
2979 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
2980 - {
2981 - add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
2982 - }
2983 - }
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();
2984 5106 }
2985 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 +
2986 5112 $applicant_contacts = array();
2987 - foreach ( $applicant_contact_ids as $applicant_contact_id )
5113 + foreach ($applicant_contact_ids as $applicant_contact_id)
2988 5114 {
5115 + add_post_meta( $viewing_post_id, '_applicant_contact_id', $applicant_contact_id );
5116 +
2989 5117 $applicant_contacts[] = array(
2990 5118 'ID' => $applicant_contact_id,
2991 5119 'post_title' => get_the_title($applicant_contact_id),
2992 5120 'edit_link' => get_edit_post_link( $applicant_contact_id, '' ),
@@ -2992,8 +5120,21 @@
2992 5120 'edit_link' => get_edit_post_link( $applicant_contact_id, '' ),
2993 5121 );
2994 5122 }
2995 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 +
2996 5137 $return = array('success' => array(
2997 5138 'viewing' => array(
2998 5139 'ID' => $viewing_post_id,
2999 5140 'edit_link' => get_edit_post_link( $viewing_post_id, '' ),
@@ -3011,10 +5152,16 @@
3011 5152 check_ajax_referer( 'book-viewing', 'security' );
3012 5153
3013 5154 $this->json_headers();
3014 5155
3015 - // TO DO: Should do validation on server side also
3016 - 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)
3017 5164 {
3018 5165 $return = array('error' => 'No contact selected');
3019 5166 echo json_encode( $return );
3020 5167 die();
@@ -3019,9 +5166,9 @@
3019 5166 echo json_encode( $return );
3020 5167 die();
3021 5168 }
3022 5169
3023 - if (empty($_POST['property_ids']))
5170 + if (empty($booking['property_ids']))
3024 5171 {
3025 5172 $return = array('error' => 'No property selected');
3026 5173 echo json_encode( $return );
3027 5174 die();
@@ -3028,9 +5175,9 @@
3028 5175 }
3029 5176
3030 5177 // Loop through contacts and create one viewing each
3031 5178 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
3032 - foreach ( $_POST['property_ids'] as $property_id )
5179 + foreach ( $booking['property_ids'] as $property_id )
3033 5180 {
3034 5181 // Insert viewing record
3035 5182 $viewing_post = array(
3036 5183 'post_title' => '',
@@ -3050,20 +5197,20 @@
3050 5197 echo json_encode( $return );
3051 5198 die();
3052 5199 }
3053 5200
3054 - 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'] );
3055 5202 add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
3056 5203 add_post_meta( $viewing_post_id, '_property_id', (int)$property_id );
3057 - 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 );
3058 5205 add_post_meta( $viewing_post_id, '_status', 'pending' );
3059 5206 add_post_meta( $viewing_post_id, '_feedback_status', '' );
3060 5207 add_post_meta( $viewing_post_id, '_feedback', '' );
3061 5208 add_post_meta( $viewing_post_id, '_feedback_passed_on', '' );
3062 5209
3063 - if ( !empty($_POST['negotiator_ids']) )
5210 + if ( !empty($booking['negotiator_ids']) )
3064 5211 {
3065 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
5212 + foreach ( $booking['negotiator_ids'] as $negotiator_id )
3066 5213 {
3067 5214 add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
3068 5215 }
3069 5216 }
@@ -3069,9 +5216,9 @@
3069 5216 }
3070 5217 }
3071 5218
3072 5219 $properties = array();
3073 - foreach ( $_POST['property_ids'] as $property_id )
5220 + foreach ( $booking['property_ids'] as $property_id )
3074 5221 {
3075 5222 $properties[] = array(
3076 5223 'ID' => (int)$property_id,
3077 5224 'post_title' => get_the_title((int)$property_id),
@@ -3097,503 +5244,1144 @@
3097 5244 global $post;
3098 5245
3099 5246 check_ajax_referer( 'viewing-details-meta-box', 'security' );
3100 5247
3101 - $post = get_post((int)$_POST['viewing_id']);
5248 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3102 5249
3103 - $viewing = new PH_Viewing((int)$_POST['viewing_id']);
5250 + $post = get_post( $post_id );
3104 5251
3105 - 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;
3106 5275
3107 - 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 + }
3108 5281
3109 - echo '<p class="form-field">
3110 -
3111 - <label for="">' . __('Status', 'propertyhive') . '</label>
3112 -
3113 - ' . ucwords(str_replace("_", " ", $viewing->status));
5282 + $post = get_post((int)$post_id);
3114 5283
3115 - 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' )
3116 5300 {
3117 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
3118 - {
3119 - $offer_id = get_post_meta( $viewing->id, '_offer_id', TRUE );
3120 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
3121 - {
3122 - $offer_id = '';
3123 - }
5301 + update_post_meta( $post_id, '_status', 'carried_out' );
3124 5302
3125 - if ( $offer_id != '' )
3126 - {
3127 - echo ' (<a href="' . get_edit_post_link($offer_id) . '">' . __('View Offer', 'propertyhive') . '</a>)';
3128 - }
3129 - }
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();
3130 5312 }
3131 -
3132 - echo '</p>';
3133 5313
3134 - 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' )
3135 5326 {
3136 - $args = array(
3137 - 'id' => '_cancelled_reason',
3138 - 'label' => __( 'Reason Cancelled', 'propertyhive' ),
3139 - 'desc_tip' => false,
3140 - 'class' => '',
3141 - 'value' => $viewing->cancelled_reason,
3142 - 'custom_attributes' => array(
3143 - 'style' => 'width:95%; max-width:500px;'
3144 - )
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',
3145 5333 );
3146 - propertyhive_wp_textarea_input( $args );
5334 +
5335 + PH_Comments::insert_note( $post_id, $comment );
5336 +
5337 + wp_send_json_success();
3147 5338 }
3148 5339
3149 - 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' )
3150 5354 {
3151 - echo '<p class="form-field">
3152 -
3153 - <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' : '' );
3154 5358
3155 - 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 )
3156 5395 {
3157 - 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 )
3158 5434 {
3159 - echo 'Interested';
3160 - 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 + }
3161 5454 }
3162 - 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 == '' )
3163 5511 {
3164 - echo 'Not Interested';
3165 - break;
5512 + $from = $property->office_email_address;
3166 5513 }
3167 - 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)
3168 5535 {
3169 - echo 'Feedback Not Required';
3170 - 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 + }
3171 5561 }
3172 - default:
3173 - {
3174 - echo 'Awaiting Feedback';
3175 - }
3176 5562 }
3177 5563
3178 - 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';
3179 5568
3180 - 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)
3181 5574 {
3182 - $args = array(
3183 - 'id' => '_feedback',
3184 - 'label' => __( 'Feedback', 'propertyhive' ),
3185 - 'desc_tip' => false,
3186 - 'class' => '',
3187 - 'value' => $viewing->feedback,
3188 - 'custom_attributes' => array(
3189 - 'style' => 'width:95%; max-width:500px;'
3190 - )
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',
3191 5591 );
3192 - propertyhive_wp_textarea_input( $args );
5592 +
5593 + PH_Comments::insert_note( $post_id, $comment );
3193 5594 }
5595 +
5596 + wp_send_json_success();
3194 5597 }
3195 -
3196 - if ( $viewing->status == 'carried_out' && ( $viewing->feedback_status == 'interested' || $viewing->feedback_status == 'not_interested' ) )
5598 + else
3197 5599 {
3198 - echo '<p class="form-field">
3199 -
3200 - <label for="">' . __('Feedback Passed On', 'propertyhive') . '</label>';
3201 -
3202 - echo ( ($viewing->feedback_passed_on == 'yes') ? 'Yes' : 'No' );
3203 -
3204 - echo '</p>';
5600 + wp_send_json_error('No valid recipient email addresses');
3205 5601 }
3206 5602
3207 - do_action('propertyhive_viewing_details_fields');
3208 -
3209 - echo '</div>';
3210 -
3211 - echo '</div>';
3212 -
3213 - die();
5603 + wp_die();
3214 5604 }
3215 5605
3216 - public function get_viewing_actions()
5606 + public function viewing_email_owner_booking_confirmation()
3217 5607 {
3218 5608 check_ajax_referer( 'viewing-actions', 'security' );
3219 5609
3220 - $post_id = (int)$_POST['viewing_id'];
5610 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3221 5611
3222 - $status = get_post_meta( $post_id, '_status', TRUE );
3223 - $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' );
3224 5614
3225 - 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 ) {
3226 5619
3227 - <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);
3228 5627
3229 - $show_cancelled_meta_boxes = false;
3230 - $show_feedback_meta_boxes = false;
5628 + $owner_name = $owner_contact->post_title;
5629 + $owner_dear = $owner_contact->dear();
3231 5630
3232 - $actions = array();
5631 + if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
5632 + if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
3233 5633
3234 - if ( $status == 'pending' )
3235 - {
3236 - $applicant_contact_id = get_post_meta( $post_id, '_applicant_contact_id', TRUE );
3237 - $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3238 - $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 + }
3239 5641
3240 - 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) )
3241 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);
3242 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 + }
3243 5696 }
3244 - else
5697 + if ( !empty($negotiator_names) )
3245 5698 {
3246 - $applicant_booking_confirmation_sent_at = get_post_meta( $post_id, '_applicant_booking_confirmation_sent_at', TRUE );
3247 - $owner_booking_confirmation_sent_at = get_post_meta( $post_id, '_owner_booking_confirmation_sent_at', TRUE );
3248 -
3249 - //Applicant
3250 - $actions[] = '<a
3251 - href="#action_panel_viewing_email_applicant_booking_confirmation"
3252 - class="button viewing-action"
3253 - style="width:100%; margin-bottom:7px; text-align:center"
3254 - >' . ( ( $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 + }
3255 5718
3256 - $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);
3257 5720
3258 - // Owner/Landlord
3259 - $property_department = get_post_meta( $property_id, '_department', TRUE );
3260 - $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
3261 - $owner_or_landlord = ( $property_department == 'residential-lettings' ? 'Landlord' : 'Owner' );
5721 + $to = implode(",", $owner_emails);
3262 5722
3263 - 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', '' );
3264 5725
3265 - $actions[] = '<a
3266 - href="#action_panel_viewing_email_owner_booking_confirmation"
3267 - class="button viewing-action"
3268 - style="width:100%; margin-bottom:7px; text-align:center"
3269 - >' . ( ( $owner_booking_confirmation_sent_at == '' ) ? __('Email ' . $owner_or_landlord . ' Booking Confirmation', 'propertyhive') : __('Re-Email ' . $owner_or_landlord . ' Booking Confirmation', 'propertyhive') ) . '</a>';
3270 -
3271 - $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>';
3272 - }
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);
3273 5734
3274 - $actions[] = '<hr>';
3275 - }
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 );
3276 5737
3277 - $actions[] = '<a
3278 - href="#action_panel_viewing_carried_out"
3279 - class="button button-success viewing-action"
3280 - style="width:100%; margin-bottom:7px; text-align:center"
3281 - >' . __('Viewing Carried Out', 'propertyhive') . '</a>';
3282 - $actions[] = '<a
3283 - href="#action_panel_viewing_cancelled"
3284 - class="button viewing-action"
3285 - style="width:100%; margin-bottom:7px; text-align:center"
3286 - >' . __('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);
3287 5748
3288 - $show_cancelled_meta_boxes = true;
3289 - }
5749 + $body = html_entity_decode($body);
3290 5750
3291 - if ( $status == 'carried_out' )
3292 - {
3293 - 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' )
3294 5757 {
3295 - $actions[] = '<a
3296 - href="#action_panel_viewing_interested"
3297 - class="button button-success viewing-action"
3298 - style="width:100%; margin-bottom:7px; text-align:center"
3299 - >' . 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 : '' );
3300 5760
3301 - $actions[] = '<a
3302 - href="#action_panel_viewing_not_interested"
3303 - class="button button-danger viewing-action"
3304 - style="width:100%; margin-bottom:7px; text-align:center"
3305 - >' . wp_kses_post( __('Applicant Not Interested', 'propertyhive') ) . '</a>';
3306 -
3307 - $actions[] = '<a
3308 - href="#action_panel_viewing_feedback_not_required"
3309 - class="button viewing-action"
3310 - style="width:100%; margin-bottom:7px; text-align:center"
3311 - >' . wp_kses_post( __('Feedback Not Required', 'propertyhive') ) . '</a>';
3312 -
3313 - $show_feedback_meta_boxes = true;
5761 + if ( $from == '' )
5762 + {
5763 + $from = $property->office_email_address;
5764 + }
3314 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 + }
3315 5778
3316 - if ( $feedback_status == 'interested' )
5779 + $attachments = array();
5780 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
3317 5781 {
3318 - $actions[] = '<a
3319 - 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 .'"
3320 - class="button button-success"
3321 - style="width:100%; margin-bottom:7px; text-align:center"
3322 - >' . wp_kses_post( __('Book Second Viewing', 'propertyhive') ) . '</a>';
5782 + $uploaded_files = $this->get_viewing_email_uploads();
3323 5783
3324 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
5784 + // Handle each file upload
5785 + foreach ($uploaded_files['name'] as $key => $value)
3325 5786 {
3326 - $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3327 - if ( get_post_meta( $property_id, '_department', TRUE ) == 'residential-sales' )
5787 + if ($uploaded_files['name'][$key])
3328 5788 {
3329 - // See if an offer has this viewing id associated with it
3330 - $offer_id = get_post_meta( $post_id, '_offer_id', TRUE );
3331 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
3332 - {
3333 - $offer_id = '';
3334 - }
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 + );
3335 5796
3336 - 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']))
3337 5802 {
3338 - $actions[] = '<a
3339 - href="' . get_edit_post_link( $offer_id, '' ) . '"
3340 - class="button"
3341 - style="width:100%; margin-bottom:7px; text-align:center"
3342 - >' . wp_kses_post( __('View Offer', 'propertyhive') ) . '</a>';
3343 - }
5803 + // Add the file path to attachments array
5804 + $attachments[] = $movefile['file'];
5805 + }
3344 5806 else
3345 5807 {
3346 - $actions[] = '<a
3347 - href="' . wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), '1', 'create_offer' ) . '"
3348 - class="button button-success"
3349 - style="width:100%; margin-bottom:7px; text-align:center"
3350 - >' . wp_kses_post( __('Record Offer', 'propertyhive') ) . '</a>';
5808 + // Handle error in file upload
5809 + wp_send_json_error($movefile['error']);
3351 5810 }
3352 5811 }
3353 5812 }
3354 5813 }
3355 5814
3356 - 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)
3357 5825 {
3358 - $actions[] = '<a
3359 - href="#action_panel_viewing_revert_feedback_passed_on"
3360 - class="button viewing-action"
3361 - style="width:100%; margin-bottom:7px; text-align:center"
3362 - >' . wp_kses_post( __('Feedback Passed On To Owner', 'propertyhive') ) . '</a>';
5826 + @wp_delete_file($temp_file);
3363 5827 }
3364 5828
3365 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' || $feedback_status == 'not_required' )
5829 + if ( !$sent )
3366 5830 {
3367 - $actions[] = '<a
3368 - href="#action_panel_viewing_revert_feedback_pending"
3369 - class="button viewing-action"
3370 - style="width:100%; margin-bottom:7px; text-align:center"
3371 - >' . wp_kses_post( __('Revert To Feedback Pending', 'propertyhive') ) . '</a>';
5831 + wp_send_json_error('Failed to send email');
3372 5832 }
3373 - }
3374 5833
3375 - if ( $status == 'offer_made' )
3376 - {
3377 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
5834 + if ( apply_filters( 'propertyhive_log_booking_confirmation_emails', false ) === true )
3378 5835 {
3379 - $offer_id = get_post_meta( $post_id, '_offer_id', TRUE );
3380 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
3381 - {
3382 - $offer_id = '';
3383 - }
5836 + // Add note/comment to viewing
5837 + $comment = array(
5838 + 'note_type' => 'action',
5839 + 'action' => 'viewing_owner_booking_confirmation_email',
5840 + );
3384 5841
3385 - if ( $offer_id != '' )
3386 - {
3387 - $actions[] = '<a
3388 - href="' . get_edit_post_link( $offer_id, '' ) . '"
3389 - class="button"
3390 - style="width:100%; margin-bottom:7px; text-align:center"
3391 - >' . wp_kses_post( __('View Offer', 'propertyhive') ) . '</a>';
3392 - }
5842 + PH_Comments::insert_note( $post_id, $comment );
3393 5843 }
3394 - }
3395 5844
3396 - if ( ( $status == 'carried_out' && $feedback_status == '' ) || $status == 'cancelled' )
3397 - {
3398 - $actions[] = '<a
3399 - href="#action_panel_viewing_revert_pending"
3400 - class="button viewing-action"
3401 - style="width:100%; margin-bottom:7px; text-align:center"
3402 - >' . wp_kses_post( __('Revert To Pending', 'propertyhive') ) . '</a>';
3403 - }
5845 + update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
3404 5846
3405 - $actions = apply_filters( 'propertyhive_admin_viewing_actions', $actions, $post_id );
3406 -
3407 - if ( !empty($actions) )
3408 - {
3409 - echo implode("", $actions);
5847 + wp_send_json_success();
3410 5848 }
3411 5849 else
3412 5850 {
3413 - echo '<div style="text-align:center">' . wp_kses_post( __( 'No actions to display', 'propertyhive' ) ) . '</div>';
5851 + wp_send_json_error('No owner recipients');
3414 5852 }
3415 5853
3416 - echo '</div>
5854 + wp_die();
5855 + }
3417 5856
3418 - </div>';
5857 + public function viewing_email_attending_negotiator_booking_confirmation()
5858 + {
5859 + check_ajax_referer( 'viewing-actions', 'security' );
3419 5860
3420 - if ( $show_cancelled_meta_boxes )
3421 - {
3422 - 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 );
3423 5863
3424 - <div class="options_group" style="padding-top:8px;">
5864 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
3425 5865
3426 - <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) ) {
3427 5870
3428 - <label for="_viewing_cancelled_reason">' . __( 'Reason Cancelled', 'propertyhive' ) . '</label>
3429 -
3430 - <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);
3431 5878
3432 - </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);
3433 5889
3434 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
3435 - <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();
3436 5892
3437 - </div>
5893 + if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
5894 + if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
3438 5895
3439 - </div>';
3440 - }
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 + }
3441 5902
3442 - if ( $show_feedback_meta_boxes )
3443 - {
3444 - 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 + }
3445 5906
3446 - <div class="options_group" style="padding-top:8px;">
5907 + $owner_details = implode("\n\n", $owner_details);
3447 5908
3448 - <div class="form-field">
5909 + $owner_names_string = $this->get_list_string($owner_names);
5910 + $owner_dears_string = $this->get_list_string($owner_dears);
3449 5911
3450 - <label for="_viewing_interested_feedback">' . __( 'Applicant Feedback', 'propertyhive' ) . '</label>
3451 -
3452 - <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();
3453 5915
3454 - </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();
3455 5923
3456 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
3457 - <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 + }
3458 5927
3459 - </div>
5928 + $applicant_details = implode("\n\n", $applicant_details);
3460 5929
3461 - </div>';
5930 + $applicant_names = array_filter($applicant_names);
5931 + $applicant_dears = array_filter($applicant_dears);
3462 5932
3463 - 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);
3464 5935
3465 - <div class="options_group" style="padding-top:8px;">
5936 + $negotiator_names = array();
5937 + $negotiator_names_string = '';
3466 5938
3467 - <div class="form-field">
5939 + $negotiator_email_addresses = array();
5940 + $negotiator_email_addresses_string = '';
3468 5941
3469 - <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 + }
3470 5957
3471 - <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 + }
3472 5962
3473 - </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 + }
3474 5992
3475 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
3476 - <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);
3477 5994
3478 - </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', '' );
3479 5997
3480 - </div>';
3481 - }
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);
3482 6006
3483 - die();
3484 - }
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 );
3485 6009
3486 - public function viewing_carried_out()
3487 - {
3488 - 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);
3489 6022
3490 - $post_id = (int)$_POST['viewing_id'];
6023 + $body = html_entity_decode($body);
3491 6024
3492 - $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 );
3493 6027
3494 - if ( $status == 'pending' )
3495 - {
3496 - 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 : '' );
3497 6034
3498 - // Add note/comment to viewing
3499 - $comment = array(
3500 - 'note_type' => 'action',
3501 - 'action' => 'viewing_carried_out',
3502 - );
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 + }
3503 6052
3504 - PH_Comments::insert_note( $post_id, $comment );
3505 - }
6053 + $attachments = array();
6054 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6055 + {
6056 + $uploaded_files = $this->get_viewing_email_uploads();
3506 6057
3507 - die();
3508 - }
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 + );
3509 6070
3510 - public function viewing_cancelled()
3511 - {
3512 - 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);
3513 6074
3514 - $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 + }
3515 6088
3516 - $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';
3517 6093
3518 - if ( $status == 'pending' )
3519 - {
3520 - update_post_meta( $post_id, '_status', 'cancelled' );
3521 - 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 );
3522 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 +
3523 6108 // Add note/comment to viewing
3524 - $comment = array(
3525 - 'note_type' => 'action',
3526 - 'action' => 'viewing_cancelled',
3527 - );
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 + );
3528 6115
3529 - 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();
3530 6122 }
6123 + else
6124 + {
6125 + wp_send_json_error('No attending negotiator recipients');
6126 + }
3531 6127
3532 - die();
6128 + wp_die();
3533 6129 }
3534 6130
3535 - public function viewing_email_applicant_booking_confirmation()
6131 + public function viewing_email_applicant_cancellation_notification()
3536 6132 {
3537 6133 check_ajax_referer( 'viewing-actions', 'security' );
3538 6134
3539 - $post_id = (int)$_POST['viewing_id'];
6135 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3540 6136
3541 - $applicant_contact_id = get_post_meta( $post_id, '_applicant_contact_id', TRUE );
6137 + $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
3542 6138 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3543 6139
3544 - 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 )
3545 6141 {
3546 - die();
6142 + wp_send_json_error('Missing contact or property');
3547 6143 }
3548 6144
3549 6145 $property = new PH_Property((int)$property_id);
3550 6146
3551 - $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 + }
3552 6157
3553 - if ( sanitize_email($to) != '' )
6158 + $to = array_filter($to);
6159 +
6160 + if ( !empty(implode($to)) )
3554 6161 {
3555 - $subject = get_option( 'propertyhive_viewing_applicant_booking_confirmation_email_subject', '' );
3556 - $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', '' );
3557 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 +
3558 6236 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
3559 - $subject = str_replace('[applicant_name]', get_the_title($applicant_contact_id), $subject);
3560 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
3561 - $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);
3562 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 +
3563 6247 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
3564 - $body = str_replace('[applicant_name]', get_the_title($applicant_contact_id), $body);
3565 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
3566 - $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);
3567 6255
3568 - $from = $property->office_email_address;
3569 - 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 + )
3570 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 + {
3571 6293 $from = get_bloginfo('admin_email');
3572 6294 }
3573 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 +
3574 6332 $headers = array();
3575 - $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);
3576 6335 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
3577 6336
3578 - wp_mail($to, $subject, $body, $headers);
6337 + $headers = apply_filters( 'propertyhive_viewing_applicant_cancellation_notification_email_headers', $headers );
3579 6338
3580 - 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();
3581 6365 }
6366 + else
6367 + {
6368 + wp_send_json_error('No valid recipient email addresses');
6369 + }
3582 6370
3583 - die();
6371 + wp_die();
3584 6372 }
3585 6373
3586 - public function viewing_email_owner_booking_confirmation()
6374 + public function viewing_email_owner_cancellation_notification()
3587 6375 {
3588 6376 check_ajax_referer( 'viewing-actions', 'security' );
3589 6377
3590 - $post_id = (int)$_POST['viewing_id'];
6378 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3591 6379
3592 6380 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3593 6381 $property_department = get_post_meta( $property_id, '_department' );
3594 6382
3595 - $applicant_contact_id = get_post_meta( $post_id, '_applicant_contact_id', TRUE );
6383 + $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
3596 6384 $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
3597 6385
3598 6386 if ( $owner_contact_ids > 0 ) {
3599 6387
@@ -3598,66 +6386,551 @@
3598 6386 if ( $owner_contact_ids > 0 ) {
3599 6387
3600 6388 $owner_emails = array();
3601 6389 $owner_names = array();
6390 + $owner_dears = array();
3602 6391
3603 6392 foreach ($owner_contact_ids as $owner_id)
3604 6393 {
3605 - $owner_email = sanitize_email( get_post_meta($owner_id, '_email_address', TRUE) );
3606 - $owner_name = get_the_title($owner_id);
6394 + $owner_contact = new PH_Contact($owner_id);
3607 6395
3608 - if( ! empty($owner_email) ) array_push($owner_emails, $owner_email);
6396 + $owner_name = $owner_contact->post_title;
6397 + $owner_dear = $owner_contact->dear();
6398 +
3609 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 + }
3610 6408 }
3611 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 +
3612 6487 $property = new PH_Property((int)$property_id);
3613 6488
3614 6489 $to = implode(",", $owner_emails);
3615 6490
3616 - $subject = get_option( 'propertyhive_viewing_owner_booking_confirmation_email_subject', '' );
3617 - $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', '' );
3618 6493
3619 6494 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
3620 - $subject = str_replace('[owner_name]', implode(", ", $owner_names), $subject);
3621 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
3622 - $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);
3623 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 +
3624 6506 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
3625 - $body = str_replace('[owner_name]', implode(", ", $owner_names), $body);
3626 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
3627 - $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);
3628 6516
3629 - $from = $property->office_email_address;
3630 - 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 + )
3631 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 + {
3632 6554 $from = get_bloginfo('admin_email');
3633 6555 }
3634 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 +
3635 6593 $headers = array();
3636 - $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);
3637 6596 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
3638 6597
3639 - wp_mail($to, $subject, $body, $headers);
6598 + $headers = apply_filters( 'propertyhive_viewing_owner_cancellation_notification_email_headers', $headers );
3640 6599
3641 - 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);
3642 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();
3643 6626 }
6627 + else
6628 + {
6629 + wp_send_json_error('No owner recipients');
6630 + }
3644 6631
3645 - die();
6632 + wp_die();
3646 6633 }
3647 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 +
3648 6919 public function viewing_interested_feedback()
3649 6920 {
3650 6921 check_ajax_referer( 'viewing-actions', 'security' );
3651 6922
3652 - $post_id = (int)$_POST['viewing_id'];
6923 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3653 6924
6925 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6926 +
3654 6927 $status = get_post_meta( $post_id, '_status', TRUE );
3655 6928
3656 6929 if ( $status == 'carried_out' )
3657 6930 {
3658 6931 update_post_meta( $post_id, '_feedback_status', 'interested' );
3659 - update_post_meta( $post_id, '_feedback', sanitize_textarea_field( $_POST['feedback'] ) );
6932 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
3660 6933
3661 6934 // Add note/comment to viewing
3662 6935 $comment = array(
3663 6936 'note_type' => 'action',
@@ -3664,11 +6937,13 @@
3664 6937 'action' => 'viewing_applicant_interested',
3665 6938 );
3666 6939
3667 6940 PH_Comments::insert_note( $post_id, $comment );
6941 +
6942 + wp_send_json_success();
3668 6943 }
3669 6944
3670 - die();
6945 + wp_send_json_error();
3671 6946 }
3672 6947
3673 6948 public function viewing_not_interested_feedback()
3674 6949 {
@@ -3673,16 +6948,18 @@
3673 6948 public function viewing_not_interested_feedback()
3674 6949 {
3675 6950 check_ajax_referer( 'viewing-actions', 'security' );
3676 6951
3677 - $post_id = (int)$_POST['viewing_id'];
6952 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3678 6953
6954 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6955 +
3679 6956 $status = get_post_meta( $post_id, '_status', TRUE );
3680 6957
3681 6958 if ( $status == 'carried_out' )
3682 6959 {
3683 6960 update_post_meta( $post_id, '_feedback_status', 'not_interested' );
3684 - update_post_meta( $post_id, '_feedback', sanitize_textarea_field( $_POST['feedback'] ) );
6961 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
3685 6962
3686 6963 // Add note/comment to viewing
3687 6964 $comment = array(
3688 6965 'note_type' => 'action',
@@ -3689,11 +6966,13 @@
3689 6966 'action' => 'viewing_applicant_not_interested',
3690 6967 );
3691 6968
3692 6969 PH_Comments::insert_note( $post_id, $comment );
6970 +
6971 + wp_send_json_success();
3693 6972 }
3694 6973
3695 - die();
6974 + wp_send_json_error();
3696 6975 }
3697 6976
3698 6977 public function viewing_feedback_not_required()
3699 6978 {
@@ -3698,9 +6977,9 @@
3698 6977 public function viewing_feedback_not_required()
3699 6978 {
3700 6979 check_ajax_referer( 'viewing-actions', 'security' );
3701 6980
3702 - $post_id = (int)$_POST['viewing_id'];
6981 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3703 6982
3704 6983 $status = get_post_meta( $post_id, '_status', TRUE );
3705 6984
3706 6985 if ( $status == 'carried_out' )
@@ -3713,11 +6992,13 @@
3713 6992 'action' => 'viewing_feedback_not_required',
3714 6993 );
3715 6994
3716 6995 PH_Comments::insert_note( $post_id, $comment );
6996 +
6997 + wp_send_json_success();
3717 6998 }
3718 6999
3719 - die();
7000 + wp_send_json_error();
3720 7001 }
3721 7002
3722 7003 public function viewing_revert_feedback_pending()
3723 7004 {
@@ -3722,9 +7003,9 @@
3722 7003 public function viewing_revert_feedback_pending()
3723 7004 {
3724 7005 check_ajax_referer( 'viewing-actions', 'security' );
3725 7006
3726 - $post_id = (int)$_POST['viewing_id'];
7007 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3727 7008
3728 7009 $status = get_post_meta( $post_id, '_status', TRUE );
3729 7010
3730 7011 if ( $status == 'carried_out' )
@@ -3730,8 +7011,9 @@
3730 7011 if ( $status == 'carried_out' )
3731 7012 {
3732 7013 update_post_meta( $post_id, '_feedback_status', '' );
3733 7014 update_post_meta( $post_id, '_feedback_passed_on', '' );
7015 + delete_post_meta( $post_id, '_feedback_received_date' );
3734 7016
3735 7017 // Add note/comment to viewing
3736 7018 $comment = array(
3737 7019 'note_type' => 'action',
@@ -3738,11 +7020,13 @@
3738 7020 'action' => 'viewing_revert_feedback_pending',
3739 7021 );
3740 7022
3741 7023 PH_Comments::insert_note( $post_id, $comment );
7024 +
7025 + wp_send_json_success();
3742 7026 }
3743 7027
3744 - die();
7028 + wp_send_json_error();
3745 7029 }
3746 7030
3747 7031 public function viewing_revert_pending()
3748 7032 {
@@ -3747,16 +7031,17 @@
3747 7031 public function viewing_revert_pending()
3748 7032 {
3749 7033 check_ajax_referer( 'viewing-actions', 'security' );
3750 7034
3751 - $post_id = (int)$_POST['viewing_id'];
7035 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3752 7036
3753 7037 $status = get_post_meta( $post_id, '_status', TRUE );
3754 7038
3755 - if ( $status == 'carried_out' || $status == 'cancelled' )
7039 + if ( in_array( $status, array('carried_out', 'cancelled', 'no_show') ) )
3756 7040 {
3757 7041 update_post_meta( $post_id, '_status', 'pending' );
3758 7042 update_post_meta( $post_id, '_feedback_status', '' );
7043 + delete_post_meta( $post_id, '_feedback_received_date' );
3759 7044
3760 7045 // Add note/comment to viewing
3761 7046 $comment = array(
3762 7047 'note_type' => 'action',
@@ -3763,11 +7048,13 @@
3763 7048 'action' => 'viewing_revert_pending',
3764 7049 );
3765 7050
3766 7051 PH_Comments::insert_note( $post_id, $comment );
7052 +
7053 + wp_send_json_success();
3767 7054 }
3768 7055
3769 - die();
7056 + wp_send_json_error();
3770 7057 }
3771 7058
3772 7059 public function viewing_feedback_passed_on()
3773 7060 {
@@ -3772,9 +7059,9 @@
3772 7059 public function viewing_feedback_passed_on()
3773 7060 {
3774 7061 check_ajax_referer( 'viewing-actions', 'security' );
3775 7062
3776 - $post_id = (int)$_POST['viewing_id'];
7063 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3777 7064
3778 7065 $status = get_post_meta( $post_id, '_status', TRUE );
3779 7066
3780 7067 if ( $status == 'carried_out' )
@@ -3787,299 +7074,52 @@
3787 7074 'action' => 'viewing_feedback_passed_on',
3788 7075 );
3789 7076
3790 7077 PH_Comments::insert_note( $post_id, $comment );
7078 +
7079 + wp_send_json_success();
3791 7080 }
3792 7081
3793 - die();
7082 + wp_send_json_error();
3794 7083 }
3795 7084
3796 7085 public function get_property_viewings_meta_box()
3797 7086 {
3798 - check_ajax_referer( 'get_property_viewings_meta_box', 'security' );
7087 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
3799 7088
3800 - 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 + }
3801 7096
3802 - echo '<div class="propertyhive_meta_box">';
3803 -
3804 - echo '<div class="options_group">';
7097 + include( PH()->plugin_path() . '/includes/admin/views/html-property-viewings-meta-box.php' );
3805 7098
3806 - $args = array(
3807 - 'post_type' => 'viewing',
3808 - 'nopaging' => true,
3809 - 'orderby' => 'meta_value',
3810 - 'order' => 'DESC',
3811 - 'meta_key' => '_start_date_time',
3812 - 'post_status' => 'publish',
3813 - 'meta_query' => array(
3814 - array(
3815 - 'key' => '_property_id',
3816 - 'value' => (int)$_POST['post_id']
3817 - )
3818 - )
3819 - );
3820 - $viewings_query = new WP_Query( $args );
3821 -
3822 - if ( $viewings_query->have_posts() )
3823 - {
3824 - echo '<table style="width:100%">
3825 - <thead>
3826 - <tr>
3827 - <th style="text-align:left;">' . __( 'Date', 'propertyhive' ) . ' / ' . __( 'Time', 'propertyhive' ) . '</th>
3828 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
3829 - <th style="text-align:left;">' . __( 'Attending Negotiator(s)', 'propertyhive' ) . '</th>
3830 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3831 - </tr>
3832 - </thead>
3833 - <tbody>';
3834 -
3835 - while ( $viewings_query->have_posts() )
3836 - {
3837 - $viewings_query->the_post();
3838 -
3839 - echo '<tr>';
3840 - 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>';
3841 - echo '<td style="text-align:left;">';
3842 - if ( get_post_meta(get_the_ID(), '_applicant_contact_id', TRUE) != '' )
3843 - {
3844 - 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>';
3845 - }
3846 - else
3847 - {
3848 - echo '-';
3849 - }
3850 - echo '</td>';
3851 - echo '<td style="text-align:left;">';
3852 -
3853 - $negotiator_ids = get_post_meta(get_the_ID(), '_negotiator_id');
3854 -
3855 - if (!empty($negotiator_ids))
3856 - {
3857 - $i = 0;
3858 - foreach ($negotiator_ids as $negotiator_id)
3859 - {
3860 - if ( $i > 0 ) { echo ', '; }
3861 -
3862 - $userdata = get_userdata( $negotiator_id );
3863 - if ( $userdata !== FALSE )
3864 - {
3865 - echo $userdata->display_name;
3866 - }
3867 - else
3868 - {
3869 - echo '<em>Unknown user</em>';
3870 - }
3871 - ++$i;
3872 - }
3873 - }
3874 - else
3875 - {
3876 - echo 'Unattended';
3877 - }
3878 -
3879 - echo '</td>';
3880 - echo '<td style="text-align:left;">';
3881 -
3882 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
3883 - echo ucwords(str_replace("_", " ", $status));
3884 - if ( $status == 'pending' )
3885 - {
3886 - echo '<br>';
3887 - // confirmation status
3888 - if ( get_post_meta(get_the_ID(), '_all_confirmed', TRUE) == 'yes' )
3889 - {
3890 - echo __( 'All Parties Confirmed', 'propertyhive' );
3891 - }
3892 - else
3893 - {
3894 - echo __( 'Awaiting Confirmation', 'propertyhive' );
3895 - }
3896 - }
3897 - if ( $status == 'carried_out' )
3898 - {
3899 - echo '<br>';
3900 - $feedback_status = get_post_meta(get_the_ID(), '_feedback_status', TRUE);
3901 - switch ( $feedback_status )
3902 - {
3903 - case "interested": { echo 'Applicant Interested'; break; }
3904 - case "not_interested": { echo 'Applicant Not Interested'; break; }
3905 - case "not_required": { echo 'Feedback Not Required'; break; }
3906 - default: { echo 'Awaiting Feedback'; }
3907 - }
3908 -
3909 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' )
3910 - {
3911 - $feedback_passed_on = get_post_meta(get_the_ID(), '_feedback_passed_on', TRUE);
3912 - echo '<br>' . ( ($feedback_passed_on == 'yes') ? 'Feedback Passed On' : 'Feedback Not Passed On' );
3913 - }
3914 - }
3915 - echo '</td>';
3916 - echo '</tr>';
3917 - }
3918 -
3919 - echo '
3920 - </tbody>
3921 - </table>
3922 - <br>';
3923 - }
3924 - else
3925 - {
3926 - echo '<p>' . __( 'No viewings exist for this property', 'propertyhive') . '</p>';
3927 - }
3928 - wp_reset_postdata();
3929 -
3930 7099 do_action('propertyhive_property_viewings_fields');
3931 -
3932 - echo '</div>';
3933 -
3934 - echo '</div>';
3935 7100
7101 + // Quit out
3936 7102 die();
3937 7103 }
3938 7104
3939 7105 public function get_contact_viewings_meta_box()
3940 7106 {
3941 - check_ajax_referer( 'get_contact_viewings_meta_box', 'security' );
7107 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
3942 7108
3943 - 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 + }
3944 7116
3945 - echo '<div class="propertyhive_meta_box">';
3946 -
3947 - echo '<div class="options_group">';
7117 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-viewings-meta-box.php' );
3948 7118
3949 - $args = array(
3950 - 'post_type' => 'viewing',
3951 - 'nopaging' => true,
3952 - 'orderby' => 'meta_value',
3953 - 'order' => 'DESC',
3954 - 'post_status' => 'publish',
3955 - 'meta_key' => '_start_date_time',
3956 - 'meta_query' => array(
3957 - array(
3958 - 'key' => '_applicant_contact_id',
3959 - 'value' => (int)$_POST['post_id']
3960 - )
3961 - )
3962 - );
3963 - $viewings_query = new WP_Query( $args );
3964 -
3965 - if ( $viewings_query->have_posts() )
3966 - {
3967 - echo '<table style="width:100%">
3968 - <thead>
3969 - <tr>
3970 - <th style="text-align:left;">' . __( 'Date', 'propertyhive' ) . ' / ' . __( 'Time', 'propertyhive' ) . '</th>
3971 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
3972 - <th style="text-align:left;">' . __( 'Attending Negotiator(s)', 'propertyhive' ) . '</th>
3973 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3974 - </tr>
3975 - </thead>
3976 - <tbody>';
3977 -
3978 - while ( $viewings_query->have_posts() )
3979 - {
3980 - $viewings_query->the_post();
3981 -
3982 - $property = new PH_Property((int)get_post_meta(get_the_ID(), '_property_id', TRUE));
3983 -
3984 - echo '<tr>';
3985 - 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>';
3986 - echo '<td style="text-align:left;">';
3987 - if ( get_post_meta(get_the_ID(), '_property_id', TRUE) != '' )
3988 - {
3989 - echo '<a href="' . get_edit_post_link( get_post_meta(get_the_ID(), '_property_id', TRUE), '' ) . '">' . $property->get_formatted_full_address() . '</a>';
3990 - }
3991 - else
3992 - {
3993 - echo '-';
3994 - }
3995 - echo '</td>';
3996 -
3997 - echo '<td style="text-align:left;">';
3998 -
3999 - $negotiator_ids = get_post_meta(get_the_ID(), '_negotiator_id');
4000 -
4001 - if (!empty($negotiator_ids))
4002 - {
4003 - $i = 0;
4004 - foreach ($negotiator_ids as $negotiator_id)
4005 - {
4006 - if ( $i > 0 ) { echo ', '; }
4007 -
4008 - $userdata = get_userdata( $negotiator_id );
4009 - if ( $userdata !== FALSE )
4010 - {
4011 - echo $userdata->display_name;
4012 - }
4013 - else
4014 - {
4015 - echo '<em>Unknown user</em>';
4016 - }
4017 - ++$i;
4018 - }
4019 - }
4020 - else
4021 - {
4022 - echo 'Unattended';
4023 - }
4024 -
4025 - echo '</td>';
4026 - echo '<td style="text-align:left;">';
4027 -
4028 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
4029 - echo ucwords(str_replace("_", " ", $status));
4030 - if ( $status == 'pending' )
4031 - {
4032 - echo '<br>';
4033 - // confirmation status
4034 - if ( get_post_meta(get_the_ID(), '_all_confirmed', TRUE) == 'yes' )
4035 - {
4036 - echo __( 'All Parties Confirmed', 'propertyhive' );
4037 - }
4038 - else
4039 - {
4040 - echo __( 'Awaiting Confirmation', 'propertyhive' );
4041 - }
4042 - }
4043 - if ( $status == 'carried_out' )
4044 - {
4045 - echo '<br>';
4046 - $feedback_status = get_post_meta(get_the_ID(), '_feedback_status', TRUE);
4047 - switch ( get_post_meta(get_the_ID(), '_feedback_status', TRUE) )
4048 - {
4049 - case "interested": { echo 'Applicant Interested'; break; }
4050 - case "not_interested": { echo 'Applicant Not Interested'; break; }
4051 - case "not_required": { echo 'Feedback Not Required'; break; }
4052 - default: { echo 'Awaiting Feedback'; }
4053 - }
4054 -
4055 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' )
4056 - {
4057 - $feedback_passed_on = get_post_meta(get_the_ID(), '_feedback_passed_on', TRUE);
4058 - echo '<br>' . ( ($feedback_passed_on == 'yes') ? 'Feedback Passed On' : 'Feedback Not Passed On' );
4059 - }
4060 - }
4061 - echo '</td>';
4062 - echo '</tr>';
4063 - }
4064 -
4065 - echo '
4066 - </tbody>
4067 - </table>
4068 - <br>';
4069 - }
4070 - else
4071 - {
4072 - echo '<p>' . __( 'No viewings exist for this contact', 'propertyhive') . '</p>';
4073 - }
4074 - wp_reset_postdata();
4075 -
4076 7119 do_action('propertyhive_contact_viewings_fields');
4077 -
4078 - echo '</div>';
4079 -
4080 - echo '</div>';
4081 7120
7121 + // Quit out
4082 7122 die();
4083 7123 }
4084 7124
4085 7125 // Offer related functions
@@ -4088,10 +7128,19 @@
4088 7128 check_ajax_referer( 'record-offer', 'security' );
4089 7129
4090 7130 $this->json_headers();
4091 7131
4092 - // TO DO: Should do validation on server side also
4093 - 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)
4094 7143 {
4095 7144 $return = array('error' => 'No property selected');
4096 7145 echo json_encode( $return );
4097 7146 die();
@@ -4096,18 +7145,18 @@
4096 7145 echo json_encode( $return );
4097 7146 die();
4098 7147 }
4099 7148
4100 - $property = new PH_Property((int)$_POST['property_id']);
7149 + $property = new PH_Property($property_id);
4101 7150
4102 7151 $applicant_contact_ids = array();
4103 7152
4104 7153 // Create applicant record if required
4105 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
7154 + if (empty($input['applicant_ids']) && !empty($input['applicant_name']))
4106 7155 {
4107 7156 // Need to create contact/applicant
4108 7157 $contact_post = array(
4109 - 'post_title' => ph_clean($_POST['applicant_name']),
7158 + 'post_title' => $input['applicant_name'],
4110 7159 'post_content' => '',
4111 7160 'post_type' => 'contact',
4112 7161 'post_status' => 'publish',
4113 7162 'comment_status' => 'closed',
@@ -4114,9 +7163,9 @@
4114 7163 'ping_status' => 'closed',
4115 7164 );
4116 7165
4117 7166 // Insert the post into the database
4118 - $contact_post_id = wp_insert_post( $contact_post );
7167 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
4119 7168
4120 7169 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
4121 7170 {
4122 7171 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -4125,8 +7174,27 @@
4125 7174 }
4126 7175
4127 7176 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
4128 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 +
4129 7197 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
4130 7198 update_post_meta( $contact_post_id, '_applicant_profile_0', array( 'department' => $property->department, 'send_matching_properties' => '' ) );
4131 7199
4132 7200 $applicant_contact_ids[] = $contact_post_id;
@@ -4131,18 +7199,13 @@
4131 7199
4132 7200 $applicant_contact_ids[] = $contact_post_id;
4133 7201 }
4134 7202
4135 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
7203 + if (!empty($input['applicant_ids']) && empty($input['applicant_name']))
4136 7204 {
4137 7205 // This is an existing contact
4138 - if ( !is_array($_POST['applicant_ids']) )
7206 + foreach ( $input['applicant_ids'] as $applicant_id )
4139 7207 {
4140 - $_POST['applicant_ids'] = array($_POST['applicant_ids']);
4141 - }
4142 -
4143 - foreach ( $_POST['applicant_ids'] as $applicant_id )
4144 - {
4145 7208 $applicant_contact_ids[] = (int)$applicant_id;
4146 7209 }
4147 7210 }
4148 7211
@@ -4178,15 +7241,35 @@
4178 7241 echo json_encode( $return );
4179 7242 die();
4180 7243 }
4181 7244
4182 - $amount = preg_replace("/[^0-9]/", '', $_POST['amount']);
7245 + $amount = $input['amount'];
4183 7246
4184 - add_post_meta( $offer_post_id, '_offer_date_time', ph_clean($_POST['offer_date']) . ' ' . ph_clean($_POST['offer_time']) );
4185 - 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 );
4186 7249 add_post_meta( $offer_post_id, '_applicant_contact_id', $applicant_contact_id );
4187 7250 add_post_meta( $offer_post_id, '_amount', $amount );
4188 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 + }
4189 7272 }
4190 7273
4191 7274 $applicant_contacts = array();
4192 7275 foreach ( $applicant_contact_ids as $applicant_contact_id )
@@ -4216,10 +7299,16 @@
4216 7299 check_ajax_referer( 'record-offer', 'security' );
4217 7300
4218 7301 $this->json_headers();
4219 7302
4220 - // TO DO: Should do validation on server side also
4221 - 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)
4222 7311 {
4223 7312 $return = array('error' => 'No contact selected');
4224 7313 echo json_encode( $return );
4225 7314 die();
@@ -4224,9 +7313,9 @@
4224 7313 echo json_encode( $return );
4225 7314 die();
4226 7315 }
4227 7316
4228 - if (empty($_POST['property_ids']))
7317 + if (empty($input['property_ids']))
4229 7318 {
4230 7319 $return = array('error' => 'No property selected');
4231 7320 echo json_encode( $return );
4232 7321 die();
@@ -4233,9 +7322,9 @@
4233 7322 }
4234 7323
4235 7324 // Loop through contacts and create one offer each
4236 7325 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
4237 - foreach ( $_POST['property_ids'] as $property_id )
7326 + foreach ( $input['property_ids'] as $property_id )
4238 7327 {
4239 7328 // Insert offer record
4240 7329 $offer_post = array(
4241 7330 'post_title' => '',
@@ -4255,19 +7344,39 @@
4255 7344 echo json_encode( $return );
4256 7345 die();
4257 7346 }
4258 7347
4259 - $amount = preg_replace("/[^0-9]/", '', ph_clean($_POST['amount']));
7348 + $amount = $input['amount'];
4260 7349
4261 - 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'] );
4262 7351 add_post_meta( $offer_post_id, '_property_id', (int)$property_id );
4263 - 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 );
4264 7353 add_post_meta( $offer_post_id, '_amount', $amount );
4265 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 + }
4266 7375 }
4267 7376
4268 7377 $properties = array();
4269 - foreach ( $_POST['property_ids'] as $property_id )
7378 + foreach ( $input['property_ids'] as $property_id )
4270 7379 {
4271 7380 $properties[] = array(
4272 7381 'ID' => (int)$property_id,
4273 7382 'post_title' => get_the_title((int)$property_id),
@@ -4293,12 +7402,14 @@
4293 7402 global $post;
4294 7403
4295 7404 check_ajax_referer( 'offer-details-meta-box', 'security' );
4296 7405
4297 - $post = get_post((int)$_POST['offer_id']);
7406 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4298 7407
4299 - $offer = new PH_Offer((int)$_POST['offer_id']);
7408 + $post = get_post( $post_id );
4300 7409
7410 + $offer = new PH_Offer( $post_id );
7411 +
4301 7412 echo '<div class="propertyhive_meta_box">';
4302 7413
4303 7414 echo '<div class="options_group">';
4304 7415
@@ -4305,11 +7416,11 @@
4305 7416 if ( $offer->status != '' )
4306 7417 {
4307 7418 echo '<p class="form-field">
4308 7419
4309 - <label for="">' . __('Status', 'propertyhive') . '</label>
7420 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
4310 7421
4311 - ' . ucwords(str_replace("_", " ", $offer->status)) . '
7422 + ' . esc_html(propertyhive_get_status_label( $offer->status )) . '
4312 7423
4313 7424 </p>';
4314 7425 }
4315 7426
@@ -4315,32 +7426,32 @@
4315 7426
4316 7427 $offer_date_time = $offer->offer_date_time;
4317 7428 if ( empty($offer_date_time) )
4318 7429 {
4319 - $offer_date_time = date("Y-m-d H:i:s");
7430 + $offer_date_time = gmdate("Y-m-d H:i:s");
4320 7431 }
4321 7432
4322 7433 echo '<p class="form-field offer_date_time_field">
4323 7434
4324 - <label for="_offer_date">' . __('Offer Date / Time', 'propertyhive') . '</label>
7435 + <label for="_offer_date">' . esc_html(__('Offer Date / Time', 'propertyhive')) . '</label>
4325 7436
4326 - <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="">
4327 7438 <select id="_offer_time_hours" name="_offer_time_hours" class="select short" style="width:55px">';
4328 7439
4329 7440 if ( empty($offer_date_time) )
4330 7441 {
4331 - $value = date("H");
7442 + $value = gmdate("H");
4332 7443 }
4333 7444 else
4334 7445 {
4335 - $value = date( "H", strtotime( $offer_date_time ) );
7446 + $value = gmdate( "H", strtotime( $offer_date_time ) );
4336 7447 }
4337 7448 for ( $i = 0; $i < 23; ++$i )
4338 7449 {
4339 7450 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
4340 - echo '<option value="' . $j . '"';
7451 + echo '<option value="' . esc_attr($j) . '"';
4341 7452 if ($i == $value) { echo ' selected'; }
4342 - echo '>' . $j . '</option>';
7453 + echo '>' . esc_html($j) . '</option>';
4343 7454 }
4344 7455
4345 7456 echo '</select>
4346 7457 :
@@ -4351,16 +7462,16 @@
4351 7462 $value = '';
4352 7463 }
4353 7464 else
4354 7465 {
4355 - $value = date( "i", strtotime( $offer_date_time ) );
7466 + $value = gmdate( "i", strtotime( $offer_date_time ) );
4356 7467 }
4357 7468 for ( $i = 0; $i < 60; $i+=5 )
4358 7469 {
4359 7470 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
4360 - echo '<option value="' . $j . '"';
7471 + echo '<option value="' . esc_attr($j) . '"';
4361 7472 if ($i == $value) { echo ' selected'; }
4362 - echo '>' . $j . '</option>';
7473 + echo '>' . esc_html($j) . '</option>';
4363 7474 }
4364 7475
4365 7476 echo '</select>
4366 7477
@@ -4370,9 +7481,9 @@
4370 7481 'id' => '_amount',
4371 7482 'label' => __( 'Offer Amount', 'propertyhive' ) . ' (&pound;)',
4372 7483 'desc_tip' => false,
4373 7484 'class' => 'short',
4374 - 'value' => ( is_numeric($offer->amount) ? number_format($offer->amount) : '' ),
7485 + 'value' => ( is_numeric($offer->amount) ? ph_display_price_field( $offer->amount ) : '' ),
4375 7486 'custom_attributes' => array(
4376 7487 //'style' => 'width:95%; max-width:500px;'
4377 7488 )
4378 7489 );
@@ -4390,12 +7501,28 @@
4390 7501 public function get_offer_actions()
4391 7502 {
4392 7503 check_ajax_referer( 'offer-actions', 'security' );
4393 7504
4394 - $post_id = (int)$_POST['offer_id'];
7505 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4395 7506
4396 7507 $status = get_post_meta( $post_id, '_status', TRUE );
4397 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 +
4398 7525 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_offer_actions_meta_box">
4399 7526
4400 7527 <div class="options_group" style="padding-top:8px;">';
4401 7528
@@ -4412,8 +7539,13 @@
4412 7539 href="#action_panel_offer_declined"
4413 7540 class="button button-danger offer-action"
4414 7541 style="width:100%; margin-bottom:7px; text-align:center"
4415 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>';
4416 7548 }
4417 7549
4418 7550 if ( $status == 'accepted' )
4419 7551 {
@@ -4426,9 +7558,9 @@
4426 7558
4427 7559 if ( $sale_id != '' )
4428 7560 {
4429 7561 $actions[] = '<a
4430 - href="' . get_edit_post_link( $sale_id, '' ) . '"
7562 + href="' . esc_url(get_edit_post_link( $sale_id, '' )) . '"
4431 7563 class="button"
4432 7564 style="width:100%; margin-bottom:7px; text-align:center"
4433 7565 >' . wp_kses_post( __('View Sale', 'propertyhive') ) . '</a>';
4434 7566 }
@@ -4434,22 +7566,23 @@
4434 7566 }
4435 7567 else
4436 7568 {
4437 7569 $actions[] = '<a
4438 - href="' . wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), '1', 'create_sale' ) . '"
4439 - 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"
4440 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);"
4441 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>';
4442 7580 }
4443 7581 }
4444 7582
4445 - if ( $status == 'declined' )
7583 + if ( $status == 'accepted' || $status == 'declined' || $status == 'withdrawn' )
4446 7584 {
4447 -
4448 - }
4449 -
4450 - if ( $status == 'accepted' || $status == 'declined' )
4451 - {
4452 7585 $actions[] = '<a
4453 7586 href="#action_panel_offer_revert_pending"
4454 7587 class="button offer-action"
4455 7588 style="width:100%; margin-bottom:7px; text-align:center"
@@ -4456,16 +7589,18 @@
4456 7589 >' . wp_kses_post( __('Revert To Pending', 'propertyhive') ) . '</a>';
4457 7590 }
4458 7591
4459 7592 $actions = apply_filters( 'propertyhive_admin_offer_actions', $actions, $post_id );
7593 + $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
4460 7594
4461 7595 if ( !empty($actions) )
4462 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.
4463 7598 echo implode("", $actions);
4464 7599 }
4465 7600 else
4466 7601 {
4467 - 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>';
4468 7603 }
4469 7604
4470 7605 echo '</div>
4471 7606
@@ -4477,9 +7612,9 @@
4477 7612 public function offer_accepted()
4478 7613 {
4479 7614 check_ajax_referer( 'offer-actions', 'security' );
4480 7615
4481 - $post_id = (int)$_POST['offer_id'];
7616 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4482 7617
4483 7618 $status = get_post_meta( $post_id, '_status', TRUE );
4484 7619
4485 7620 if ( $status == 'pending' )
@@ -4492,11 +7627,13 @@
4492 7627 'action' => 'offer_accepted',
4493 7628 );
4494 7629
4495 7630 PH_Comments::insert_note( $post_id, $comment );
7631 +
7632 + wp_send_json_success();
4496 7633 }
4497 7634
4498 - die();
7635 + wp_send_json_error();
4499 7636 }
4500 7637
4501 7638 public function offer_declined()
4502 7639 {
@@ -4501,9 +7638,9 @@
4501 7638 public function offer_declined()
4502 7639 {
4503 7640 check_ajax_referer( 'offer-actions', 'security' );
4504 7641
4505 - $post_id = (int)$_POST['offer_id'];
7642 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4506 7643
4507 7644 $status = get_post_meta( $post_id, '_status', TRUE );
4508 7645
4509 7646 if ( $status == 'pending' )
@@ -4516,213 +7653,104 @@
4516 7653 'action' => 'offer_declined',
4517 7654 );
4518 7655
4519 7656 PH_Comments::insert_note( $post_id, $comment );
7657 +
7658 + wp_send_json_success();
4520 7659 }
4521 7660
4522 - die();
7661 + wp_send_json_error();
4523 7662 }
4524 7663
4525 - public function offer_revert_pending()
7664 + public function offer_withdrawn()
4526 7665 {
4527 7666 check_ajax_referer( 'offer-actions', 'security' );
4528 7667
4529 - $post_id = (int)$_POST['offer_id'];
7668 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4530 7669
4531 7670 $status = get_post_meta( $post_id, '_status', TRUE );
4532 7671
4533 - if ( $status == 'accepted' || $status == 'declined' )
7672 + if ( $status == 'pending' || $status == 'accepted' )
4534 7673 {
4535 - update_post_meta( $post_id, '_status', 'pending' );
7674 + update_post_meta( $post_id, '_status', 'withdrawn' );
4536 7675
4537 7676 // Add note/comment to offer
4538 7677 $comment = array(
4539 7678 'note_type' => 'action',
4540 - 'action' => 'offer_revert_pending',
7679 + 'action' => 'offer_withdrawn',
4541 7680 );
4542 7681
4543 7682 PH_Comments::insert_note( $post_id, $comment );
7683 +
7684 + wp_send_json_success();
4544 7685 }
4545 7686
4546 - die();
7687 + wp_send_json_error();
4547 7688 }
4548 7689
4549 - public function get_property_offers_meta_box()
7690 + public function offer_revert_pending()
4550 7691 {
4551 - check_ajax_referer( 'get_property_offers_meta_box', 'security' );
7692 + check_ajax_referer( 'offer-actions', 'security' );
4552 7693
4553 - global $post;
7694 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4554 7695
4555 - echo '<div class="propertyhive_meta_box">';
4556 -
4557 - echo '<div class="options_group">';
7696 + $status = get_post_meta( $post_id, '_status', TRUE );
4558 7697
4559 - $args = array(
4560 - 'post_type' => 'offer',
4561 - 'nopaging' => true,
4562 - 'orderby' => 'meta_value',
4563 - 'order' => 'DESC',
4564 - 'meta_key' => '_offer_date_time',
4565 - 'post_status' => 'publish',
4566 - 'meta_query' => array(
4567 - array(
4568 - 'key' => '_property_id',
4569 - 'value' => (int)$_POST['post_id']
4570 - )
4571 - )
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',
4572 7706 );
4573 - $offers_query = new WP_Query( $args );
4574 7707
4575 - if ( $offers_query->have_posts() )
4576 - {
4577 - echo '<table style="width:100%">
4578 - <thead>
4579 - <tr>
4580 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
4581 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
4582 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
4583 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
4584 - </tr>
4585 - </thead>
4586 - <tbody>';
7708 + PH_Comments::insert_note( $post_id, $comment );
4587 7709
4588 - while ( $offers_query->have_posts() )
4589 - {
4590 - $offers_query->the_post();
7710 + wp_send_json_success();
7711 + }
4591 7712
4592 - $offer = new PH_Offer(get_the_ID());
7713 + wp_send_json_error();
7714 + }
4593 7715
4594 - echo '<tr>';
4595 - 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>';
4596 - 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>';
4597 - echo '<td style="text-align:left;">' . $offer->get_formatted_amount() . '</td>';
4598 - echo '<td style="text-align:left;">';
4599 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
4600 - echo ucwords(str_replace("_", " ", $status));
4601 - echo '</td>';
4602 - echo '</tr>';
4603 - }
7716 + public function get_property_offers_meta_box()
7717 + {
7718 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
4604 7719
4605 - echo '
4606 - </tbody>
4607 - </table>
4608 - <br>';
4609 - }
4610 - else
4611 - {
4612 - echo '<p>' . __( 'No offers exist for this property', 'propertyhive') . '</p>';
4613 - }
4614 - 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 + }
4615 7727
7728 + include( PH()->plugin_path() . '/includes/admin/views/html-property-offers-meta-box.php' );
7729 +
4616 7730 do_action('propertyhive_property_offers_fields');
4617 -
4618 - echo '</div>';
4619 -
4620 - echo '</div>';
4621 7731
7732 + // Quit out
4622 7733 die();
4623 7734 }
4624 7735
4625 7736 public function get_contact_offers_meta_box()
4626 7737 {
4627 - check_ajax_referer( 'get_contact_offers_meta_box', 'security' );
7738 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
4628 7739
4629 - 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 + }
4630 7747
4631 - echo '<div class="propertyhive_meta_box">';
4632 -
4633 - echo '<div class="options_group">';
7748 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-offers-meta-box.php' );
4634 7749
4635 - $args = array(
4636 - 'post_type' => 'offer',
4637 - 'nopaging' => true,
4638 - 'orderby' => 'meta_value',
4639 - 'order' => 'DESC',
4640 - 'post_status' => 'publish',
4641 - 'meta_key' => '_offer_date_time',
4642 - 'meta_query' => array(
4643 - array(
4644 - 'key' => '_applicant_contact_id',
4645 - 'value' => (int)$_POST['post_id']
4646 - )
4647 - )
4648 - );
4649 - $offers_query = new WP_Query( $args );
4650 -
4651 - if ( $offers_query->have_posts() )
4652 - {
4653 - echo '<table style="width:100%">
4654 - <thead>
4655 - <tr>
4656 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
4657 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
4658 - <th style="text-align:left;">' . __( 'Property Owner', 'propertyhive' ) . '</th>
4659 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
4660 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
4661 - </tr>
4662 - </thead>
4663 - <tbody>';
4664 -
4665 - while ( $offers_query->have_posts() )
4666 - {
4667 - $offers_query->the_post();
4668 -
4669 - $property = new PH_Property((int)get_post_meta(get_the_ID(), '_property_id', TRUE));
4670 - $offer = new PH_Offer(get_the_ID());
4671 -
4672 - echo '<tr>';
4673 - 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>';
4674 - 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>';
4675 - echo '<td style="text-align:left;">';
4676 -
4677 - $owner_contact_ids = $property->_owner_contact_id;
4678 - if (
4679 - ( !is_array($owner_contact_ids) && $owner_contact_ids != '' && $owner_contact_ids != 0 )
4680 - ||
4681 - ( is_array($owner_contact_ids) && !empty($owner_contact_ids) )
4682 - )
4683 - {
4684 - if ( !is_array($owner_contact_ids) )
4685 - {
4686 - $owner_contact_ids = array($owner_contact_ids);
4687 - }
4688 -
4689 - foreach ( $owner_contact_ids as $owner_contact_id )
4690 - {
4691 - echo get_the_title($owner_contact_id) . '<br>';
4692 - echo '<div style="color:#BBB">';
4693 - echo 'T: ' . get_post_meta($owner_contact_id, '_telephone_number', TRUE) . '<br>';
4694 - echo 'E: ' . get_post_meta($owner_contact_id, '_email_address', TRUE);
4695 - echo '</div>';
4696 - }
4697 - }
4698 -
4699 - echo '</td>';
4700 - echo '<td style="text-align:left;">' . $offer->get_formatted_amount() . '</td>';
4701 - echo '<td style="text-align:left;">';
4702 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
4703 - echo ucwords(str_replace("_", " ", $status));
4704 - echo '</td>';
4705 - echo '</tr>';
4706 - }
4707 -
4708 - echo '
4709 - </tbody>
4710 - </table>
4711 - <br>';
4712 - }
4713 - else
4714 - {
4715 - echo '<p>' . __( 'No offers exist for this contact', 'propertyhive') . '</p>';
4716 - }
4717 - wp_reset_postdata();
4718 -
4719 7750 do_action('propertyhive_contact_offers_fields');
4720 -
4721 - echo '</div>';
4722 -
4723 - echo '</div>';
4724 7751
7752 + // Quit out
4725 7753 die();
4726 7754 }
4727 7755
4728 7756 // Sale related functions
@@ -4731,12 +7759,14 @@
4731 7759 global $post;
4732 7760
4733 7761 check_ajax_referer( 'sale-details-meta-box', 'security' );
4734 7762
4735 - $post = get_post((int)$_POST['sale_id']);
7763 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
4736 7764
4737 - $sale = new PH_Offer((int)$_POST['sale_id']);
7765 + $post = get_post( $post_id );
4738 7766
7767 + $sale = new PH_Offer( $post_id );
7768 +
4739 7769 echo '<div class="propertyhive_meta_box">';
4740 7770
4741 7771 echo '<div class="options_group">';
4742 7772
@@ -4743,11 +7773,11 @@
4743 7773 if ( $sale->status != '' )
4744 7774 {
4745 7775 echo '<p class="form-field">
4746 7776
4747 - <label for="">' . __('Status', 'propertyhive') . '</label>
7777 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
4748 7778
4749 - ' . ucwords(str_replace("_", " ", $sale->status)) . '
7779 + ' . esc_html(propertyhive_get_status_label( $sale->status )) . '
4750 7780
4751 7781 </p>';
4752 7782 }
4753 7783
@@ -4753,17 +7783,17 @@
4753 7783
4754 7784 $sale_date_time = $sale->sale_date_time;
4755 7785 if ( empty($sale_date_time) )
4756 7786 {
4757 - $sale_date_time = date("Y-m-d H:i:s");
7787 + $sale_date_time = gmdate("Y-m-d H:i:s");
4758 7788 }
4759 7789
4760 7790 echo '<p class="form-field sale_date_field">
4761 7791
4762 - <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="">
4763 7795
4764 - <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)) . '">
4765 -
4766 7796 </p>';
4767 7797
4768 7798 $args = array(
4769 7799 'id' => '_amount',
@@ -4769,9 +7799,9 @@
4769 7799 'id' => '_amount',
4770 7800 'label' => __( 'Sale Amount', 'propertyhive' ) . ' (&pound;)',
4771 7801 'desc_tip' => false,
4772 7802 'class' => 'short',
4773 - 'value' => ( is_numeric($sale->amount) ? number_format($sale->amount) : '' ),
7803 + 'value' => ( is_numeric($sale->amount) ? ph_display_price_field( $sale->amount ) : '' ),
4774 7804 'custom_attributes' => array(
4775 7805 //'style' => 'width:95%; max-width:500px;'
4776 7806 )
4777 7807 );
@@ -4789,12 +7819,28 @@
4789 7819 public function get_sale_actions()
4790 7820 {
4791 7821 check_ajax_referer( 'sale-actions', 'security' );
4792 7822
4793 - $post_id = (int)$_POST['sale_id'];
7823 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
4794 7824
4795 7825 $status = get_post_meta( $post_id, '_status', TRUE );
4796 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 +
4797 7843 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_sale_actions_meta_box">
4798 7844
4799 7845 <div class="options_group" style="padding-top:8px;">';
4800 7846
@@ -4805,9 +7851,9 @@
4805 7851 $actions[] = '<a
4806 7852 href="#action_panel_sale_exchanged"
4807 7853 class="button button-success sale-action"
4808 7854 style="width:100%; margin-bottom:7px; text-align:center"
4809 - >' . __('Sale Exchanged', 'propertyhive') . '</a>';
7855 + >' . esc_html(__('Sale Exchanged', 'propertyhive')) . '</a>';
4810 7856
4811 7857 }
4812 7858
4813 7859 if ( $status == 'exchanged' )
@@ -4815,9 +7861,9 @@
4815 7861 $actions[] = '<a
4816 7862 href="#action_panel_sale_completed"
4817 7863 class="button button-success sale-action"
4818 7864 style="width:100%; margin-bottom:7px; text-align:center"
4819 - >' . __('Sale Completed', 'propertyhive') . '</a>';
7865 + >' . esc_html(__('Sale Completed', 'propertyhive')) . '</a>';
4820 7866 }
4821 7867
4822 7868 if ( $status == 'completed' )
4823 7869 {
@@ -4829,20 +7875,22 @@
4829 7875 $actions[] = '<a
4830 7876 href="#action_panel_sale_fallen_through"
4831 7877 class="button sale-action"
4832 7878 style="width:100%; margin-bottom:7px; text-align:center"
4833 - >' . __('Sale Fallen Through', 'propertyhive') . '</a>';
7879 + >' . esc_html(__('Sale Fallen Through', 'propertyhive')) . '</a>';
4834 7880 }
4835 7881
4836 7882 $actions = apply_filters( 'propertyhive_admin_sale_actions', $actions, $post_id );
7883 + $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
4837 7884
4838 7885 if ( !empty($actions) )
4839 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.
4840 7888 echo implode("", $actions);
4841 7889 }
4842 7890 else
4843 7891 {
4844 - 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>';
4845 7893 }
4846 7894
4847 7895 echo '</div>
4848 7896
@@ -4854,9 +7902,9 @@
4854 7902 public function sale_exchanged()
4855 7903 {
4856 7904 check_ajax_referer( 'sale-actions', 'security' );
4857 7905
4858 - $post_id = (int)$_POST['sale_id'];
7906 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
4859 7907
4860 7908 $status = get_post_meta( $post_id, '_status', TRUE );
4861 7909
4862 7910 if ( $status == 'current' )
@@ -4869,11 +7917,13 @@
4869 7917 'action' => 'sale_exchanged',
4870 7918 );
4871 7919
4872 7920 PH_Comments::insert_note( $post_id, $comment );
7921 +
7922 + wp_send_json_success();
4873 7923 }
4874 7924
4875 - die();
7925 + wp_send_json_error();
4876 7926 }
4877 7927
4878 7928 public function sale_completed()
4879 7929 {
@@ -4878,9 +7928,9 @@
4878 7928 public function sale_completed()
4879 7929 {
4880 7930 check_ajax_referer( 'sale-actions', 'security' );
4881 7931
4882 - $post_id = (int)$_POST['sale_id'];
7932 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
4883 7933
4884 7934 $status = get_post_meta( $post_id, '_status', TRUE );
4885 7935
4886 7936 if ( $status == 'exchanged' )
@@ -4893,11 +7943,13 @@
4893 7943 'action' => 'sale_completed',
4894 7944 );
4895 7945
4896 7946 PH_Comments::insert_note( $post_id, $comment );
7947 +
7948 + wp_send_json_success();
4897 7949 }
4898 7950
4899 - die();
7951 + wp_send_json_error();
4900 7952 }
4901 7953
4902 7954 public function sale_fallen_through()
4903 7955 {
@@ -4902,9 +7954,9 @@
4902 7954 public function sale_fallen_through()
4903 7955 {
4904 7956 check_ajax_referer( 'sale-actions', 'security' );
4905 7957
4906 - $post_id = (int)$_POST['sale_id'];
7958 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
4907 7959
4908 7960 $status = get_post_meta( $post_id, '_status', TRUE );
4909 7961
4910 7962 if ( $status == 'current' || $status == 'exchanged' )
@@ -4917,191 +7969,735 @@
4917 7969 'action' => 'sale_fallen_through',
4918 7970 );
4919 7971
4920 7972 PH_Comments::insert_note( $post_id, $comment );
7973 +
7974 + wp_send_json_success();
4921 7975 }
4922 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
4923 7997 die();
4924 7998 }
4925 7999
4926 - public function get_property_sales_meta_box()
8000 + public function get_contact_sales_meta_box()
4927 8001 {
4928 - check_ajax_referer( 'get_property_sales_meta_box', 'security' );
8002 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
4929 8003
4930 - 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 + }
4931 8011
4932 - echo '<div class="propertyhive_meta_box">';
4933 -
4934 - echo '<div class="options_group">';
8012 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-sales-meta-box.php' );
4935 8013
4936 - $args = array(
4937 - 'post_type' => 'sale',
4938 - 'nopaging' => true,
4939 - 'orderby' => 'meta_value',
4940 - 'order' => 'DESC',
4941 - 'meta_key' => '_sale_date_time',
4942 - 'post_status' => 'publish',
4943 - 'meta_query' => array(
4944 - array(
4945 - 'key' => '_property_id',
4946 - 'value' => (int)$_POST['post_id']
4947 - )
4948 - )
4949 - );
4950 - $sales_query = new WP_Query( $args );
8014 + do_action('propertyhive_contact_sales_fields');
4951 8015
4952 - 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 )
4953 8167 {
4954 - echo '<table style="width:100%">
4955 - <thead>
4956 - <tr>
4957 - <th style="text-align:left;">' . __( 'Sale Date', 'propertyhive' ) . '</th>
4958 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
4959 - <th style="text-align:left;">' . __( 'Sale Amount', 'propertyhive' ) . '</th>
4960 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
4961 - </tr>
4962 - </thead>
4963 - <tbody>';
8168 + list($key, $value) = explode('=', $key_value_pair);
8169 + $recurrence[strtolower($key)] = $value;
8170 + }
4964 8171
4965 - 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'] )
4966 8176 {
4967 - $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 + }
4968 8190
4969 - $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 + }
4970 8198
4971 - echo '<tr>';
4972 - 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>';
4973 - 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>';
4974 - echo '<td style="text-align:left;">' . $sale->get_formatted_amount() . '</td>';
4975 - echo '<td style="text-align:left;">';
4976 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
4977 - echo ucwords(str_replace("_", " ", $status));
4978 - echo '</td>';
4979 - echo '</tr>';
4980 - }
8199 + echo esc_html($next_key_date);
4981 8200
4982 - echo '
4983 - </tbody>
4984 - </table>
4985 - <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 );
4986 8222 }
4987 - 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 )
4988 8260 {
4989 - 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();
4990 8264 }
4991 - wp_reset_postdata();
4992 8265
4993 - do_action('propertyhive_property_sales_fields');
4994 -
4995 - echo '</div>';
4996 -
4997 - 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'] ) );
4998 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 +
4999 8279 die();
5000 8280 }
5001 8281
5002 - public function get_contact_sales_meta_box()
8282 + public function delete_key_date()
5003 8283 {
5004 - check_ajax_referer( 'get_contact_sales_meta_box', 'security' );
8284 + check_ajax_referer( 'delete-key-date', 'security' );
5005 8285
5006 - global $post;
8286 + $this->json_headers();
5007 8287
5008 - 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 + }
5009 8412
5010 - echo '<div class="options_group">';
8413 + // check plugin status
8414 + $slug = isset( $_POST['slug'] ) && is_string( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
5011 8415
5012 - $args = array(
5013 - 'post_type' => 'sale',
5014 - 'nopaging' => true,
5015 - 'orderby' => 'meta_value',
5016 - 'order' => 'DESC',
5017 - 'post_status' => 'publish',
5018 - 'meta_key' => '_sale_date_time',
5019 - 'meta_query' => array(
5020 - array(
5021 - 'key' => '_applicant_contact_id',
5022 - 'value' => (int)$_POST['post_id']
5023 - )
5024 - )
8416 + $feature = get_ph_pro_feature( $slug );
8417 +
8418 + if ( $feature === false )
8419 + {
8420 + $return = array(
8421 + 'errorMessage' => 'Feature not found'
5025 8422 );
5026 - $sales_query = new WP_Query( $args );
8423 + wp_send_json_error($return);
8424 + }
5027 8425
5028 - if ( $sales_query->have_posts() )
5029 - {
5030 - echo '<table style="width:100%">
5031 - <thead>
5032 - <tr>
5033 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
5034 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
5035 - <th style="text-align:left;">' . __( 'Property Owner', 'propertyhive' ) . '</th>
5036 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
5037 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
5038 - </tr>
5039 - </thead>
5040 - <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 + }
5041 8433
5042 - while ( $sales_query->have_posts() )
5043 - {
5044 - $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 + }
5045 8440
5046 - $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;
5047 8445
5048 - $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 + }
5049 8457
5050 - echo '<tr>';
5051 - 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>';
5052 - 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>';
5053 - 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();
5054 8464
5055 - $owner_contact_ids = $property->_owner_contact_id;
8465 + if ( isset($product_id_and_package['success']) && $product_id_and_package['success'] === true )
8466 + {
5056 8467 if (
5057 - ( !is_array($owner_contact_ids) && $owner_contact_ids != '' && $owner_contact_ids != 0 )
5058 - ||
5059 - ( 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'])
5060 8471 )
5061 8472 {
5062 - if ( !is_array($owner_contact_ids) )
5063 - {
5064 - $owner_contact_ids = array($owner_contact_ids);
5065 - }
5066 -
5067 - foreach ( $owner_contact_ids as $owner_contact_id )
5068 - {
5069 - echo get_the_title($owner_contact_id) . '<br>';
5070 - echo '<div style="color:#BBB">';
5071 - echo 'T: ' . get_post_meta($owner_contact_id, '_telephone_number', TRUE) . '<br>';
5072 - echo 'E: ' . get_post_meta($owner_contact_id, '_email_address', TRUE);
5073 - echo '</div>';
5074 - }
8473 + $valid_license_key = true;
5075 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 + }
5076 8499
5077 - echo '</td>';
5078 - echo '<td style="text-align:left;">' . $sale->get_formatted_amount() . '</td>';
5079 - echo '<td style="text-align:left;">';
5080 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
5081 - echo ucwords(str_replace("_", " ", $status));
5082 - echo '</td>';
5083 - echo '</tr>';
5084 - }
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 + }
5085 8508
5086 - echo '
5087 - </tbody>
5088 - </table>
5089 - <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 + );
5090 8522 }
5091 8523 else
5092 8524 {
5093 - 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 + );
5094 8543 }
5095 - wp_reset_postdata();
5096 8544
5097 - 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
5098 8562
5099 - echo '</div>';
5100 -
5101 - 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 + }
5102 8570
5103 - 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();
5104 8700 }
5105 8701 }
5106 8702
5107 8703 new PH_AJAX();