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