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