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