PluginProbe
Property Hive / 2.4.0
Property Hive v2.4.0
2.4.0 2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 All 262 releases
← All changes | includes/class-ph-ajax.php +6323 -1392 1.4.6 → 2.4.0 View file →
@@ -1,6 +1,9 @@
1 1 <?php
2 +// phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 +// ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
2 4
5 +
3 6 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
4 7
5 8 /**
6 9 * PropertyHive PH_AJAX
@@ -12,8 +15,9 @@
12 15 * @package PropertyHive/Classes
13 16 * @category Class
14 17 * @author PropertyHive
15 18 */
19 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_AJAX; preserving the existing PH_* class name is required for plugin and extension compatibility.
16 20 class PH_AJAX {
17 21
18 22 /**
19 23 * Hook into ajax events
@@ -23,8 +27,12 @@
23 27 // propertyhive_EVENT => nopriv
24 28 $ajax_events = array(
25 29 'add_note' => false,
26 30 'delete_note' => false,
31 + 'toggle_note_pinned' => false,
32 + 'get_notes_grid' => false,
33 + 'get_pinned_notes_grid' => false,
34 + 'fetch_note_mentions' => false,
27 35 'search_contacts' => false,
28 36 'search_properties' => false,
29 37 'search_negotiators' => false,
30 38 'load_existing_owner_contact' => false,
@@ -30,23 +38,55 @@
30 38 'load_existing_owner_contact' => false,
31 39 'load_existing_features' => false,
32 40 'make_property_enquiry' => true,
33 41 'create_contact_from_enquiry' => false,
42 + 'merge_contact_records' => false,
34 43
35 44 // Dashboard components
36 45 'get_news' => false,
37 46 'get_viewings_awaiting_applicant_feedback' => false,
47 + 'get_my_upcoming_appointments' => false,
48 + 'get_upcoming_overdue_key_dates' => false,
38 49
50 + // Property actions
51 + 'check_duplicate_reference_number' => false,
52 + 'osm_geocoding_request' => false,
53 + 'get_property_marketing_statistics_meta_box' => false,
54 + 'get_property_tenancies_grid' => false,
55 +
39 56 // Contact actions
40 57 'create_contact_login' => false,
58 + 'get_contact_tenancies_grid' => false,
59 + 'get_contact_solicitor' => false,
41 60
61 + // Appraisal actions
62 + 'get_appraisal_details_meta_box' => false,
63 + 'get_appraisal_actions' => false,
64 + 'appraisal_carried_out' => false,
65 + 'appraisal_cancelled' => false,
66 + 'appraisal_won' => false,
67 + 'appraisal_lost_reason' => false,
68 + 'appraisal_instructed' => false,
69 + 'appraisal_email_owner_booking_confirmation' => false,
70 + 'appraisal_revert_pending' => false,
71 + 'appraisal_revert_carried_out' => false,
72 + 'appraisal_revert_won' => false,
73 +
42 74 // Viewing actions
43 75 'book_viewing_property' => false,
44 76 'book_viewing_contact' => false,
45 77 'get_viewing_details_meta_box' => false,
46 78 'get_viewing_actions' => false,
79 + 'get_viewing_lightbox' => false,
47 80 'viewing_carried_out' => false,
48 81 'viewing_cancelled' => false,
82 + 'viewing_no_show' => false,
83 + 'viewing_email_applicant_booking_confirmation' => false,
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,
49 89 'viewing_interested_feedback' => false,
50 90 'viewing_not_interested_feedback' => false,
51 91 'viewing_feedback_not_required' => false,
52 92 'viewing_revert_feedback_pending' => false,
@@ -62,8 +102,9 @@
62 102 'get_offer_actions' => false,
63 103 'get_property_offers_meta_box' => false,
64 104 'offer_accepted' => false,
65 105 'offer_declined' => false,
106 + 'offer_withdrawn' => false,
66 107 'offer_revert_pending' => false,
67 108 'get_contact_offers_meta_box' => false,
68 109
69 110 // Sale actions
@@ -76,16 +117,53 @@
76 117 'offer_declined' => false,
77 118 'get_property_sales_meta_box' => false,
78 119 'get_contact_sales_meta_box' => false,
79 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 +
80 133 'validate_save_contact' => false,
81 134 'applicant_registration' => true,
82 135 'login' => true,
136 + 'lost_password' => true,
137 + 'reset_password' => true,
83 138 'save_account_details' => true,
84 139 'save_account_requirements' => true,
140 +
141 + // Dismissing notices
142 + 'dismiss_notice_leave_review' => false,
143 + 'dismiss_notice_retired_template_assistant' => false,
144 + 'dismiss_notice_demo_data' => false,
145 + 'dismiss_notice_epl' => false,
146 + 'dismiss_notice_missing_search_results' => false,
147 + 'dismiss_notice_missing_google_maps_api_key' => false,
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,
85 159 );
86 160
87 - 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 + }
88 166 add_action( 'wp_ajax_propertyhive_' . $ajax_event, array( $this, $ajax_event ) );
89 167
90 168 if ( $nopriv ) {
91 169 add_action( 'wp_ajax_nopriv_propertyhive_' . $ajax_event, array( $this, $ajax_event ) );
@@ -92,8 +170,314 @@
92 170 }
93 171 }
94 172 }
95 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 +
410 + public function dismiss_notice_leave_review()
411 + {
412 + update_option( 'propertyhive_review_prompt_due_timestamp', 0 );
413 +
414 + // Quit out
415 + die();
416 + }
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 +
451 + public function dismiss_notice_missing_search_results()
452 + {
453 + update_option( 'missing_search_results_notice_dismissed', 'yes' );
454 +
455 + // Quit out
456 + die();
457 + }
458 +
459 + public function dismiss_notice_missing_google_maps_api_key()
460 + {
461 + update_option( 'missing_google_maps_api_key_notice_dismissed', 'yes' );
462 +
463 + // Quit out
464 + die();
465 + }
466 +
467 + public function dismiss_notice_invalid_expired_license_key()
468 + {
469 + update_option( 'missing_invalid_expired_license_key_notice_dismissed', 'yes' );
470 +
471 + // Quit out
472 + die();
473 + }
474 +
475 + public function dismiss_notice_email_cron_not_running()
476 + {
477 + update_option( 'email_cron_not_running_dismissed', 'yes' );
478 + }
479 +
96 480 /**
97 481 * Output headers for JSON requests
98 482 */
99 483 private function json_headers() {
@@ -99,40 +483,144 @@
99 483 private function json_headers() {
100 484 header( 'Content-Type: application/json; charset=utf-8' );
101 485 }
102 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 +
103 572 public function create_contact_login()
104 573 {
105 574 check_ajax_referer( 'create-login', 'security' );
106 575
107 - $this->json_headers();
108 -
109 - if (empty($_POST['contact_id']))
110 - {
111 - $return = array('error' => 'No contact selected');
112 - echo json_encode( $return );
113 - 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 );
114 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 + }
115 586
116 - if (empty($_POST['password']))
587 + if ( empty( $_POST['password'] ) || ! is_string( $_POST['password'] ) )
117 588 {
118 589 $return = array('error' => 'No password entered');
119 - echo json_encode( $return );
120 - die();
590 + wp_send_json( $return );
121 591 }
122 592
123 - $contact = new PH_Contact((int)$_POST['contact_id']);
593 + $contact = new PH_Contact($contact_id);
124 594
595 + $display_name = get_the_title($contact_id);
596 +
125 597 // Create user
126 598 $userdata = array(
127 - 'display_name' => get_the_title($_POST['contact_id']),
599 + 'display_name' => $display_name,
128 600 'user_login' => sanitize_email($contact->email_address),
129 601 'user_email' => sanitize_email($contact->email_address),
130 - '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'] ),
131 604 'role' => 'property_hive_contact',
132 605 'show_admin_bar_front' => 'false',
133 606 );
134 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 +
135 623 $user_id = wp_insert_user( $userdata );
136 624
137 625 // On success
138 626 if ( ! is_wp_error( $user_id ) )
@@ -137,9 +625,9 @@
137 625 // On success
138 626 if ( ! is_wp_error( $user_id ) )
139 627 {
140 628 // Assign user ID to CPT
141 - add_post_meta( $_POST['contact_id'], '_user_id', $user_id );
629 + add_post_meta( $contact_id, '_user_id', $user_id );
142 630
143 631 $return = array('success' => true);
144 632 }
145 633 else
@@ -146,10 +634,9 @@
146 634 {
147 635 $return = array('error' => 'Failed to create user login');
148 636 }
149 637
150 - echo json_encode( $return );
151 - die();
638 + wp_send_json( $return );
152 639 }
153 640
154 641 /**
155 642 * Login user
@@ -164,18 +651,19 @@
164 651 if ( check_ajax_referer( 'ph_login', 'security', false ) === FALSE )
165 652 {
166 653 $return['errors'][] = 'Invalid nonce';
167 654
168 - $this->json_headers();
169 - echo json_encode( $return );
170 -
171 - // Quit out
172 - die();
655 + wp_send_json( $return );
173 656 }
174 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 + }
175 662 $creds = array(
176 - 'user_login' => $_POST['email_address'],
177 - 'user_password' => $_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'] ),
178 666 );
179 667
180 668 $user = wp_signon( apply_filters( 'propertyhive_login_credentials', $creds ), is_ssl() );
181 669
@@ -186,12 +674,13 @@
186 674 else
187 675 {
188 676 // Check has associated contact CPT and is published
189 677 $args = array(
190 - 'post_type' => 'contact',
678 + 'post_type' => apply_filters( 'propertyhive_allowed_login_post_type', array( 'contact' ) ),
191 679 'fields' => 'ids',
192 680 'posts_per_page' => 1,
193 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.
194 683 'meta_query' => array(
195 684 array(
196 685 'key' => '_user_id',
197 686 'value' => $user->ID
@@ -202,23 +691,156 @@
202 691 $contact_query = new WP_Query( $args );
203 692
204 693 if ( $contact_query->have_posts() )
205 694 {
206 - // Has associated published contact CPT
207 - $return['success'] = true;
695 + while ( $contact_query->have_posts() )
696 + {
697 + $contact_query->the_post();
698 +
699 + // Has associated published contact CPT
700 + $return['success'] = true;
701 +
702 + do_action('propertyhive_user_logged_in', get_the_ID(), $user->ID);
703 + }
208 704 }
209 705
210 706 wp_reset_postdata();
211 707 }
212 708
213 - $this->json_headers();
214 - 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 );
215 766
216 - // Quit out
217 - die();
767 + $return['success'] = true;
768 +
769 + wp_send_json( $return );
218 770 }
219 771
220 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 + /**
221 843 * Register applicant
222 844 */
223 845 public function applicant_registration()
224 846 {
@@ -243,8 +865,48 @@
243 865
244 866 // Validate
245 867 $errors = array();
246 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 +
247 909 $form_controls = ph_get_user_details_form_fields();
248 910
249 911 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
250 912
@@ -249,12 +911,29 @@
249 911 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
250 912
251 913 $form_controls_2 = ph_get_applicant_requirements_form_fields();
252 914
253 - $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 );
254 916
255 917 $form_controls = array_merge( $form_controls, $form_controls_2 );
256 918
919 + // need to improve this as duplicated in ph-shortcodes.php
920 + if ( get_option( 'propertyhive_applicant_registration_form_disclaimer', '' ) != '' )
921 + {
922 + $disclaimer = get_option( 'propertyhive_applicant_registration_form_disclaimer', '' );
923 +
924 + $form_controls['disclaimer'] = array(
925 + 'type' => 'checkbox',
926 + 'label' => $disclaimer,
927 + 'label_style' => 'width:100%;',
928 + 'required' => true
929 + );
930 + }
931 +
932 + $form_controls = apply_filters( 'propertyhive_applicant_registration_form_fields', $form_controls );
933 +
934 + $contact_post_id = false;
935 +
257 936 foreach ( $form_controls as $key => $control )
258 937 {
259 938 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
260 939 {
@@ -265,9 +944,9 @@
265 944 }
266 945 }
267 946 if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) )
268 947 {
269 - if ( ! is_email( $_POST[$key] ) )
948 + if ( ! is_string( $_POST[$key] ) || ! is_email( wp_unslash( $_POST[$key] ) ) )
270 949 {
271 950 $errors[] = __( 'Invalid email address provided', 'propertyhive' );
272 951 }
273 952 else
@@ -277,12 +956,13 @@
277 956 'post_type' => 'contact',
278 957 'posts_per_page' => 1,
279 958 'fields' => 'ids',
280 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.
281 961 'meta_query' => array(
282 962 array(
283 963 'key' => '_email_address',
284 - 'value' => $_POST[$key]
964 + 'value' => sanitize_email( wp_unslash( $_POST[$key] ) )
285 965 )
286 966 )
287 967 );
288 968
@@ -289,13 +969,14 @@
289 969 $contacts_query = new WP_Query( $args );
290 970
291 971 if ( $contacts_query->have_posts() )
292 972 {
293 - $errors[] = __( 'This email address is already registered', 'propertyhive' );
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' );
294 975 }
295 976 else
296 977 {
297 - if ( email_exists( $_POST[$key] ) )
978 + if ( email_exists( sanitize_email( wp_unslash( $_POST[$key] ) ) ) )
298 979 {
299 980 $errors[] = __( 'This email address is already registered to a user', 'propertyhive' );
300 981 }
301 982 }
@@ -301,12 +982,95 @@
301 982 }
302 983 wp_reset_postdata();
303 984 }
304 985 }
986 + if ( in_array( $key, array('recaptcha', 'recaptcha-v3') ) )
987 + {
988 + $errors = $this->check_recaptcha_form_response($errors, $key, $control);
989 + }
990 +
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';
1014 + }
1015 + else
1016 + {
1017 + if ( isset($response['success']) && $response['success'] == true )
1018 + {
1019 +
1020 + }
1021 + else
1022 + {
1023 + $errors[] = 'Failed hCaptcha validation';
1024 + }
1025 + }
1026 + }
1027 + }
1028 +
1029 + if ( $key == 'turnstile' )
1030 + {
1031 + $secret = isset( $control['secret'] ) ? $control['secret'] : '';
1032 + $response = ( isset( $_POST['cf-turnstile-response'] ) && is_string( $_POST['cf-turnstile-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) ) : '';
1033 +
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.
1036 + array(
1037 + 'method' => 'POST',
1038 + 'headers' => array(
1039 + 'Content-Type' => 'application/x-www-form-urlencoded',
1040 + ),
1041 + 'body' => array( 'secret' => $secret, 'response' => $response ),
1042 + )
1043 + );
1044 +
1045 + if ( is_wp_error( $response ) )
1046 + {
1047 + $errors[] = $response->get_error_message();
1048 + }
1049 + else
1050 + {
1051 + $response = json_decode($response['body'], TRUE);
1052 + if ( $response === FALSE )
1053 + {
1054 + $errors[] = 'Error decoding response from turnstile check';
1055 + }
1056 + else
1057 + {
1058 + if ( isset($response['success']) && $response['success'] == true )
1059 + {
1060 +
1061 + }
1062 + else
1063 + {
1064 + $errors[] = 'Failed turnstile validation';
1065 + }
1066 + }
1067 + }
1068 + }
305 1069 }
306 1070
307 1071 // Check password and password2 match
308 - 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'] )
309 1073 {
310 1074 $errors[] = __( 'The passwords entered do not match', 'propertyhive' );
311 1075 }
312 1076
@@ -319,44 +1083,104 @@
319 1083 $return['errors'] = $errors;
320 1084 }
321 1085 else
322 1086 {
323 - // create CPT
324 - $contact_post = array(
325 - 'post_title' => $_POST['name'],
326 - 'post_content' => '',
327 - 'post_type' => 'contact',
328 - 'post_status' => 'publish',
329 - 'comment_status'=> 'closed',
330 - 'ping_status' => 'closed',
331 - );
1087 + if ( $contact_post_id === FALSE )
1088 + {
1089 + // create CPT
1090 + $contact_post = array(
1091 + 'post_title' => wp_slash( $registration_input['name'] ),
1092 + 'post_content' => '',
1093 + 'post_type' => 'contact',
1094 + 'post_status' => 'publish',
1095 + 'comment_status'=> 'closed',
1096 + 'ping_status' => 'closed',
1097 + );
1098 +
1099 + // Insert the post into the database
1100 + $contact_post_id = wp_insert_post( $contact_post );
1101 + }
1102 + else
1103 + {
1104 + // update CPT
1105 + $contact_post = array(
1106 + 'ID' => $contact_post_id,
1107 + 'post_title' => wp_slash( $registration_input['name'] ),
1108 + 'post_status' => 'publish',
1109 + );
1110 +
1111 + // Insert the post into the database
1112 + wp_update_post( $contact_post );
1113 + }
332 1114
333 - // Insert the post into the database
334 - $contact_post_id = wp_insert_post( $contact_post );
1115 + $forbidden_contact_methods = get_post_meta( $contact_post_id, '_forbidden_contact_methods', TRUE );
1116 + if ( !is_array($forbidden_contact_methods) )
1117 + {
1118 + $forbidden_contact_methods = array();
1119 + }
1120 + if ( ( $key = array_search('email', $forbidden_contact_methods) ) !== false ) {
1121 + unset($forbidden_contact_methods[$key]);
1122 + }
1123 + update_post_meta( $contact_post_id, '_forbidden_contact_methods', array_unique($forbidden_contact_methods) );
335 1124
336 1125 // Add post meta (contact details, requirements etc)
337 - add_post_meta( $contact_post_id, '_email_address', sanitize_email($_POST['email_address']) );
338 - add_post_meta( $contact_post_id, '_telephone_number', ( ( isset($_POST['telephone_number']) ) ? $_POST['telephone_number'] : '' ) );
1126 + update_post_meta( $contact_post_id, '_email_address', sanitize_email( $registration_input['email_address'] ) );
1127 +
1128 + $telephone_number = get_post_meta( $contact_post_id, '_telephone_number', TRUE );
1129 + if ( isset($_POST['telephone_number']) && $_POST['telephone_number'] != '' )
1130 + {
1131 + $telephone_number = $registration_input['telephone_number'];
1132 + }
1133 + update_post_meta( $contact_post_id, '_telephone_number', wp_slash( ph_clean($telephone_number) ) );
1134 + update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean( ph_clean_telephone_number($telephone_number) ) );
1135 +
1136 + $contact_types = get_post_meta( $contact_post_id, '_contact_types', TRUE );
1137 + if ( !is_array($contact_types) )
1138 + {
1139 + $contact_types = array();
1140 + }
1141 + if ( !in_array('applicant', $contact_types) )
1142 + {
1143 + $contact_types[] = 'applicant';
1144 + }
1145 + update_post_meta( $contact_post_id, '_contact_types', array_unique($contact_types) );
339 1146
340 - add_post_meta( $contact_post_id, '_contact_types', array('applicant') );
1147 + update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
341 1148
342 - add_post_meta( $contact_post_id, '_applicant_profiles', 1 );
1149 + $applicant_profile = array();
1150 + $applicant_profile['department'] = $registration_input['department'];
343 1151
344 - $applicant_profile = array();
345 - $applicant_profile['department'] = $_POST['department'];
1152 + $base_department = $registration_input['department'];
1153 + if ( !in_array( $base_department, array('residential-sales', 'residential-lettings', 'commercial') ) )
1154 + {
1155 + $base_department = ph_get_custom_department_based_on($base_department);
1156 + }
346 1157
347 - if ( $_POST['department'] == 'residential-sales' )
1158 + if ( $base_department == 'residential-sales' )
348 1159 {
349 - $price = preg_replace("/[^0-9]/", '', $_POST['maximum_price']);
1160 + $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_price']);
350 1161
351 1162 $applicant_profile['max_price'] = $price;
352 1163
353 1164 // Not used yet but could be if introducing currencies in the future.
354 1165 $applicant_profile['max_price_actual'] = $price;
1166 +
1167 + $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
1168 + $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
1169 +
1170 + if ( $percentage_lower != '' && $percentage_higher != '' && $registration_input['maximum_price'] != '' && $registration_input['maximum_price'] != 0 )
1171 + {
1172 + $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_price']);
1173 + $applicant_profile['match_price_range_lower'] = $price - ( $price * ( $percentage_lower / 100 ) );
1174 + $applicant_profile['match_price_range_lower_actual'] = $price - ( $price * ( $percentage_lower / 100 ) );
1175 +
1176 + $applicant_profile['match_price_range_higher'] = $price + ( $price * ( $percentage_higher / 100 ) );
1177 + $applicant_profile['match_price_range_higher_actual'] = $price + ( $price * ( $percentage_higher / 100 ) );
1178 + }
355 1179 }
356 - elseif ( $_POST['department'] == 'residential-lettings' )
1180 + elseif ( $base_department == 'residential-lettings' )
357 1181 {
358 - $price = preg_replace("/[^0-9]/", '', $_POST['maximum_rent']);
1182 + $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_rent']);
359 1183
360 1184 $applicant_profile['max_rent'] = $price;
361 1185 $applicant_profile['rent_frequency'] = 'pcm';
362 1186 $price_actual = $price; // Stored in pcm
@@ -362,60 +1186,118 @@
362 1186 $price_actual = $price; // Stored in pcm
363 1187 $applicant_profile['max_price_actual'] = $price_actual;
364 1188 }
365 1189
366 - if ( $_POST['department'] == 'residential-sales' || $_POST['department'] == 'residential-lettings' )
1190 + if ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' )
367 1191 {
368 - $beds = preg_replace("/[^0-9]/", '', $_POST['minimum_bedrooms']);
1192 + $beds = preg_replace("/[^0-9.]/", '', $registration_input['minimum_bedrooms']);
369 1193 $applicant_profile['min_beds'] = $beds;
370 1194
371 1195 if ( isset($_POST['property_type']) && !empty($_POST['property_type']) )
372 1196 {
373 - $applicant_profile['property_types'] = array($_POST['property_type']);
1197 + $applicant_profile['property_types'] = $registration_input['property_type'];
374 1198 }
375 1199 }
376 1200
1201 + if ( $base_department == 'commercial' )
1202 + {
1203 + $available_as = array();
1204 + if ( isset($_POST['available_as_sale']) && $registration_input['available_as_sale'] == 'yes' )
1205 + {
1206 + $available_as[] = 'sale';
1207 + }
1208 + if ( isset($_POST['available_as_rent']) && $registration_input['available_as_rent'] == 'yes' )
1209 + {
1210 + $available_as[] = 'rent';
1211 + }
1212 + $applicant_profile['available_as'] = $available_as;
1213 +
1214 + $floor_area = preg_replace("/[^0-9.]/", '', $registration_input['minimum_floor_area']);
1215 + $applicant_profile['min_floor_area'] = $floor_area;
1216 + $applicant_profile['min_floor_area_actual'] = $floor_area;
1217 +
1218 + $floor_area = preg_replace("/[^0-9.]/", '', $registration_input['maximum_floor_area']);
1219 + $applicant_profile['max_floor_area'] = $floor_area;
1220 + $applicant_profile['max_floor_area_actual'] = $floor_area;
1221 +
1222 + if ( isset($_POST['commercial_property_type']) && !empty($_POST['commercial_property_type']) )
1223 + {
1224 + $applicant_profile['commercial_property_types'] = $registration_input['commercial_property_type'];
1225 + }
1226 + }
1227 +
377 1228 if ( isset($_POST['location']) && !empty($_POST['location']) )
378 1229 {
379 - $applicant_profile['locations'] = array($_POST['location']);
1230 + $applicant_profile['locations'] = $registration_input['location'];
380 1231 }
381 1232
382 - $applicant_profile['notes'] = ( ( isset($_POST['additional_requirements']) ) ? $_POST['additional_requirements'] : '' );
1233 + if ( isset($_POST['location_text']) && !empty($_POST['location_text']) )
1234 + {
1235 + $applicant_profile['location_text'] = $registration_input['location_text'];
1236 + }
383 1237
1238 + $applicant_profile['notes'] = $registration_input['additional_requirements'];
1239 +
384 1240 $applicant_profile['send_matching_properties'] = 'yes';
385 1241 //$applicant_profile['auto_match_disabled'] = ''; // don't know what to do about this yet. Should probably look at global setting and reflect that
386 1242
387 - 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 ) );
388 1244
389 - // Create user
390 - $userdata = array(
391 - 'display_name' => $_POST['name'],
392 - 'user_login' => sanitize_email($_POST['email_address']),
393 - 'user_email' => sanitize_email($_POST['email_address']),
394 - 'user_pass' => $_POST['password'],
395 - 'role' => 'property_hive_contact',
396 - 'show_admin_bar_front' => 'false',
397 - );
1245 + if ( get_option( 'propertyhive_applicant_users', '' ) == 'yes' )
1246 + {
1247 + $display_name = wp_slash( $registration_input['name'] );
398 1248
399 - $user_id = wp_insert_user( $userdata );
1249 + // Create user
1250 + $userdata = array(
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'],
1255 + 'role' => 'property_hive_contact',
1256 + 'show_admin_bar_front' => 'false',
1257 + );
400 1258
401 - //On success
402 - if ( ! is_wp_error( $user_id ) )
403 - {
404 - // Assign user ID to CPT
405 - add_post_meta( $contact_post_id, '_user_id', $user_id );
1259 + if ( !empty($display_name) )
1260 + {
1261 + $name_parts = explode( ' ', $display_name );
406 1262
407 - $return['success'] = true;
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 + }
408 1273
409 - wp_set_auth_cookie( $user_id, true );
1274 + $user_id = wp_insert_user( $userdata );
410 1275
411 - do_action( 'propertyhive_applicant_registered', $contact_post_id, $user_id );
1276 + //On success
1277 + if ( ! is_wp_error( $user_id ) )
1278 + {
1279 + // Assign user ID to CPT
1280 + add_post_meta( $contact_post_id, '_user_id', $user_id );
1281 +
1282 + $return['success'] = true;
1283 +
1284 + wp_set_auth_cookie( $user_id, true );
1285 +
1286 + do_action( 'propertyhive_applicant_registered', $contact_post_id, $user_id );
1287 + }
1288 + else
1289 + {
1290 + $return['success'] = false;
1291 + $return['reason'] = 'validation';
1292 + $return['errors'] = array('Failed to create user. You might experience issues with logging in');
1293 + }
412 1294 }
413 1295 else
414 1296 {
415 - $return['success'] = false;
416 - $return['reason'] = 'validation';
417 - $return['errors'] = array('Failed to create user. You might experience issues with logging in');
1297 + $return['success'] = true;
1298 +
1299 + do_action( 'propertyhive_applicant_registered', $contact_post_id, 0 );
418 1300 }
419 1301 }
420 1302
421 1303 $this->json_headers();
@@ -436,13 +1318,14 @@
436 1318
437 1319 $return = array(
438 1320 'success' => false,
439 1321 'errors' => array(),
1322 + 'new_details_nonce' => wp_create_nonce( "ph_userdetails" ),
440 1323 );
441 1324
442 1325 // Got an issue with nonce being declined on second submission.
443 1326 // Need to sort before putting this back in
444 - /*if ( check_ajax_referer( 'ph_details', 'security', false ) === FALSE )
1327 + if ( check_ajax_referer( 'ph_userdetails', 'ph_account_details_security', false ) === FALSE )
445 1328 {
446 1329 $return['errors'][] = 'Invalid nonce';
447 1330
448 1331 $this->json_headers();
@@ -449,9 +1332,9 @@
449 1332 echo json_encode( $return );
450 1333
451 1334 // Quit out
452 1335 die();
453 - }*/
1336 + }
454 1337
455 1338 // Validate
456 1339 $errors = array();
457 1340
@@ -469,8 +1352,22 @@
469 1352 // Quit out
470 1353 die();
471 1354 }
472 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 + }
473 1370 $form_controls = ph_get_user_details_form_fields();
474 1371
475 1372 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
476 1373
@@ -485,9 +1382,9 @@
485 1382 }
486 1383 }
487 1384 if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) )
488 1385 {
489 - if ( ! is_email( $_POST[$key] ) )
1386 + if ( ! is_string( $_POST[$key] ) || ! is_email( sanitize_email( wp_unslash( $_POST[$key] ) ) ) )
490 1387 {
491 1388 $errors[] = __( 'Invalid email address provided', 'propertyhive' );
492 1389 }
493 1390
@@ -495,13 +1392,27 @@
495 1392 }
496 1393 }
497 1394
498 1395 // Check password and password2 match
499 - 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'] )
500 1397 {
501 1398 $errors[] = __( 'The passwords entered do not match', 'propertyhive' );
502 1399 }
503 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 +
504 1415 if ( !empty($errors) )
505 1416 {
506 1417 // Failed validation
507 1418
@@ -511,46 +1422,52 @@
511 1422 }
512 1423 else
513 1424 {
514 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 + }
515 1431
516 1432 // create CPT
517 1433 $contact_post = array(
518 1434 'ID' => $contact->id,
519 - 'post_title' => $_POST['name'],
1435 + 'post_title' => wp_slash( $account_input['name'] ),
520 1436 );
521 1437
522 1438 // Update the post in the database
523 1439 $contact_post_id = wp_update_post( $contact_post );
524 1440
525 - 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'] ) );
526 1442 if (isset($_POST['telephone_number']))
527 1443 {
528 - update_post_meta( $contact_post_id, '_telephone_number', $_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'] ) );
529 1446 }
530 1447
531 1448 // Update user
532 1449 $userdata = array(
533 1450 'ID' => $user_id,
534 - 'display_name' => $_POST['name'],
535 - 'user_email' => sanitize_email($_POST['email_address']),
1451 + 'display_name' => wp_slash( $account_input['name'] ),
1452 + 'user_email' => sanitize_email( $account_input['email_address'] ),
536 1453 );
537 1454
538 1455 if ( isset($_POST['password']) && !empty($_POST['password']) )
539 1456 {
540 - $userdata['user_pass'] = $_POST['password'];
1457 + $userdata['user_pass'] = $account_input['password'];
541 1458 }
542 1459
543 1460 $user_id = wp_update_user( $userdata );
544 1461
545 - $user_roles = $current_user->roles;
546 - $user_role = array_shift($user_roles);
547 -
548 - if ( $user_role === 'property_hive_contact' )
1462 + if ( ! is_wp_error( $user_id ) && $user_role === 'property_hive_contact' )
549 1463 {
550 1464 // Have to update login via SQL as wp_update_user won't allow altering
551 1465 // Only do it for property hive contacts though as admin or editor might be viewing this page
552 - $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 );
553 1470 }
554 1471
555 1472 //On success
556 1473 if ( ! is_wp_error( $user_id ) )
@@ -586,13 +1503,14 @@
586 1503
587 1504 $return = array(
588 1505 'success' => false,
589 1506 'errors' => array(),
1507 + 'new_requirements_nonce' => wp_create_nonce( "ph_requirements" ),
590 1508 );
591 1509
592 1510 // Got an issue with nonce being declined on second submission.
593 1511 // Need to sort before putting this back in
594 - /*if ( check_ajax_referer( 'ph_requirements', 'security', false ) === FALSE )
1512 + if ( check_ajax_referer( 'ph_requirements', 'ph_account_requirements_security', false ) === FALSE )
595 1513 {
596 1514 $return['errors'][] = 'Invalid nonce';
597 1515
598 1516 $this->json_headers();
@@ -599,9 +1517,9 @@
599 1517 echo json_encode( $return );
600 1518
601 1519 // Quit out
602 1520 die();
603 - }*/
1521 + }
604 1522
605 1523 // Validate
606 1524 $errors = array();
607 1525
@@ -619,11 +1537,52 @@
619 1537 // Quit out
620 1538 die();
621 1539 }
622 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'] );
623 1582 $form_controls = ph_get_applicant_requirements_form_fields();
624 1583
625 - $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 ) );
626 1585
627 1586 foreach ( $form_controls as $key => $control )
628 1587 {
629 1588 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
@@ -645,27 +1604,42 @@
645 1604 $return['errors'] = $errors;
646 1605 }
647 1606 else
648 1607 {
649 - $contact = new PH_Contact( '', $user_id );
1608 + $applicant_profile = array();
1609 + $applicant_profile['department'] = $requirements_input['department'];
650 1610
651 - $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 + }
652 1616
653 - $applicant_profile = array();
654 - $applicant_profile['department'] = $_POST['department'];
655 -
656 - if ( $_POST['department'] == 'residential-sales' )
1617 + if ( $base_department == 'residential-sales' )
657 1618 {
658 - $price = preg_replace("/[^0-9]/", '', $_POST['maximum_price']);
1619 + $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_price']);
659 1620
660 1621 $applicant_profile['max_price'] = $price;
661 1622
662 1623 // Not used yet but could be if introducing currencies in the future.
663 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 + }
664 1638 }
665 - elseif ( $_POST['department'] == 'residential-lettings' )
1639 + elseif ( $base_department == 'residential-lettings' )
666 1640 {
667 - $price = preg_replace("/[^0-9]/", '', $_POST['maximum_rent']);
1641 + $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_rent']);
668 1642
669 1643 $applicant_profile['max_rent'] = $price;
670 1644 $applicant_profile['rent_frequency'] = 'pcm';
671 1645 $price_actual = $price; // Stored in pcm
@@ -671,30 +1645,62 @@
671 1645 $price_actual = $price; // Stored in pcm
672 1646 $applicant_profile['max_price_actual'] = $price_actual;
673 1647 }
674 1648
675 - if ( $_POST['department'] == 'residential-sales' || $_POST['department'] == 'residential-lettings' )
1649 + if ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' )
676 1650 {
677 - $beds = preg_replace("/[^0-9]/", '', $_POST['minimum_bedrooms']);
1651 + $beds = preg_replace("/[^0-9]/", '', $requirements_input['minimum_bedrooms']);
678 1652 $applicant_profile['min_beds'] = $beds;
679 1653
680 1654 if ( isset($_POST['property_type']) && !empty($_POST['property_type']) )
681 1655 {
682 - $applicant_profile['property_types'] = array($_POST['property_type']);
1656 + $applicant_profile['property_types'] = $requirements_input['property_type'];
683 1657 }
684 1658 }
685 1659
1660 + if ( $base_department == 'commercial' )
1661 + {
1662 + $available_as = array();
1663 + if ( isset($_POST['available_as_sale']) && $requirements_input['available_as_sale'] == 'yes' )
1664 + {
1665 + $available_as[] = 'sale';
1666 + }
1667 + if ( isset($_POST['available_as_rent']) && $requirements_input['available_as_rent'] == 'yes' )
1668 + {
1669 + $available_as[] = 'rent';
1670 + }
1671 + $applicant_profile['available_as'] = $available_as;
1672 +
1673 + $floor_area = preg_replace("/[^0-9.]/", '', $requirements_input['minimum_floor_area']);
1674 + $applicant_profile['min_floor_area'] = $floor_area;
1675 + $applicant_profile['min_floor_area_actual'] = $floor_area;
1676 +
1677 + $floor_area = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_floor_area']);
1678 + $applicant_profile['max_floor_area'] = $floor_area;
1679 + $applicant_profile['max_floor_area_actual'] = $floor_area;
1680 +
1681 + if ( isset($_POST['commercial_property_type']) && !empty($_POST['commercial_property_type']) )
1682 + {
1683 + $applicant_profile['commercial_property_types'] = $requirements_input['commercial_property_type'];
1684 + }
1685 + }
1686 +
686 1687 if ( isset($_POST['location']) && !empty($_POST['location']) )
687 1688 {
688 - $applicant_profile['locations'] = array($_POST['location']);
1689 + $applicant_profile['locations'] = $requirements_input['location'];
689 1690 }
690 1691
691 - $applicant_profile['notes'] = ( ( isset($_POST['additional_requirements']) ) ? $_POST['additional_requirements'] : '' );
1692 + if ( isset($_POST['location_text']) && !empty($_POST['location_text']) )
1693 + {
1694 + $applicant_profile['location_text'] = $requirements_input['location_text'];
1695 + }
692 1696
1697 + $applicant_profile['notes'] = $requirements_input['additional_requirements'];
1698 +
693 1699 $applicant_profile['send_matching_properties'] = 'yes';
694 1700 //$applicant_profile['auto_match_disabled'] = ''; // don't know what to do about this yet. Should probably look at global setting and reflect that
695 1701
696 - 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 ) );
697 1703
698 1704 $return['success'] = true;
699 1705
700 1706 do_action( 'propertyhive_account_requirements_updated', $contact_post_id, $user_id );
@@ -719,10 +1725,11 @@
719 1725 $return = array();
720 1726
721 1727 $property_query = new WP_Query(array(
722 1728 'post_type' => 'property',
723 - 'post_status' => 'any',
724 - 'nopaging' => true
1729 + 'post_status' => 'publish',
1730 + 'nopaging' => true,
1731 + 'fields' => 'ids',
725 1732 ));
726 1733
727 1734 if ($property_query->have_posts())
728 1735 {
@@ -729,14 +1736,14 @@
729 1736 while ($property_query->have_posts())
730 1737 {
731 1738 $property_query->the_post();
732 1739
733 - $num_property_features = get_post_meta($post->ID, '_features', TRUE);
1740 + $num_property_features = get_post_meta(get_the_ID(), '_features', TRUE);
734 1741 if ($num_property_features == '') { $num_property_features = 0; }
735 1742
736 1743 for ($i = 0; $i < $num_property_features; ++$i)
737 1744 {
738 - $feature = get_post_meta($post->ID, '_feature_' . $i, TRUE);
1745 + $feature = get_post_meta(get_the_ID(), '_feature_' . $i, TRUE);
739 1746 if (!in_array($feature, $return) && trim($feature) != '')
740 1747 {
741 1748 $return[] = $feature;
742 1749 }
@@ -756,19 +1763,19 @@
756 1763 public function load_existing_owner_contact() {
757 1764
758 1765 check_ajax_referer( 'load-existing-owner-contact', 'security' );
759 1766
760 - $contact_id = $_POST['contact_id'];
1767 + $contact_id = isset( $_POST['contact_id'] ) && is_scalar( $_POST['contact_id'] ) ? absint( $_POST['contact_id'] ) : 0;
761 1768
762 - $contact = get_post($contact_id);
1769 + $contact = $contact_id > 0 && 'contact' === get_post_type( $contact_id ) ? get_post( $contact_id ) : null;
763 1770
764 - echo '<div id="existing-owner-details-' . $contact_id . '">';
1771 + echo '<div id="existing-owner-details-' . esc_attr($contact_id) . '">';
765 1772
766 1773 if ( !is_null( $contact ) )
767 1774 {
768 1775 echo '<p class="form-field">';
769 - echo '<label>' . __('Name', 'propertyhive') . '</label>';
770 - 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>';
771 1778 echo '</p>';
772 1779
773 1780 $address = array();
774 1781 $address_elements = array( '_address_name_number', '_address_street', '_address_two', '_address_three', '_address_four', '_address_postcode' );
@@ -780,30 +1787,42 @@
780 1787 }
781 1788 }
782 1789
783 1790 echo '<p class="form-field">';
784 - echo '<label>' . __('Address', 'propertyhive') . '</label>';
785 - echo ( ( !empty($address) ) ? implode(", ", $address) : '-' );
1791 + echo '<label>' . esc_html(__('Address', 'propertyhive')) . '</label>';
1792 + echo ( ( !empty($address) ) ? esc_html(implode(", ", $address)) : '-' );
786 1793 echo '</p>';
787 1794
788 1795 echo '<p class="form-field">';
789 - echo '<label>' . __('Telephone Number', 'propertyhive') . '</label>';
790 - 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)) : '-' );
791 1798 echo '</p>';
792 1799
793 1800 echo '<p class="form-field">';
794 - echo '<label>' . __('Email Address', 'propertyhive') . '</label>';
795 - 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)) : '-' );
796 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 + }
797 1816 }
798 1817 else
799 1818 {
800 - echo __( 'Invalid contact record', 'propertyhive' );
1819 + echo esc_html(__( 'Invalid contact record', 'propertyhive' ));
801 1820 }
802 1821
803 1822 echo '<p class="form-field">';
804 1823 echo '<label></label>';
805 - 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> ';
806 1825 echo '<a href="" class="button add-additional-owner-contact">Add Additional Owner</a>';
807 1826 echo '</p>';
808 1827
809 1828 echo '</div>';
@@ -823,9 +1842,11 @@
823 1842 check_ajax_referer( 'search-contacts', 'security' );
824 1843
825 1844 $return = array();
826 1845
827 - $keyword = trim( $_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'] ) ) : '';
828 1849
829 1850 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
830 1851 {
831 1852 // Get all contacts that match the name
@@ -830,22 +1851,29 @@
830 1851 {
831 1852 // Get all contacts that match the name
832 1853 $args = array(
833 1854 'post_type' => 'contact',
1855 + 'propertyhive_contact_search_keyword' => $keyword,
834 1856 'nopaging' => true,
835 - 'post_status' => array( 'publish' ),
1857 + 'post_status' => array( 'publish', 'private' ),
836 1858 'fields' => 'ids'
837 1859 );
838 - if ( isset($_POST['contact_type']) && $_POST['contact_type'] != '' )
1860 + if ( '' !== $contact_type )
839 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.
840 1863 $args['meta_query'] = array(
841 1864 array(
842 1865 'key' => '_contact_types',
843 - 'value' => $_POST['contact_type'],
1866 + 'value' => $contact_type,
844 1867 'compare' => 'LIKE',
845 1868 )
846 1869 );
847 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 + }
848 1876
849 1877 add_filter( 'posts_where', array( $this, 'search_contacts_where' ), 10, 2 );
850 1878
851 1879 $contact_query = new WP_Query( $args );
@@ -856,12 +1884,24 @@
856 1884 {
857 1885 while ( $contact_query->have_posts() )
858 1886 {
859 1887 $contact_query->the_post();
1888 +
1889 + $contact = new PH_Contact( get_the_ID() );
860 1890
861 1891 $return[] = array(
862 1892 'ID' => get_the_ID(),
863 - '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 . ')' : '' ) ,
1894 + 'address_name_number' => $contact->_address_name_number,
1895 + 'address_street' => $contact->_address_street,
1896 + 'address_two' => $contact->_address_two,
1897 + 'address_three' => $contact->_address_three,
1898 + 'address_four' => $contact->_address_four,
1899 + 'address_postcode' => $contact->_address_postcode,
1900 + 'address_country' => $contact->_address_country,
1901 + 'address_full_formatted' => $contact->get_formatted_full_address(', '),
1902 + 'telephone_number' => $contact->_telephone_number,
1903 + 'email_address' => $contact->_email_address,
864 1904 );
865 1905 }
866 1906 }
867 1907
@@ -874,14 +1914,18 @@
874 1914 // Quit out
875 1915 die();
876 1916 }
877 1917
878 - public function search_contacts_where( $where, &$wp_query )
1918 + public function search_contacts_where( $where, $wp_query )
879 1919 {
880 1920 global $wpdb;
881 1921
882 - $where .= ' AND ' . $wpdb->posts . '.post_title LIKE \'%' . esc_sql( like_escape( trim( $_POST['keyword'] ) ) ) . '%\'';
883 -
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 +
884 1928 return $where;
885 1929 }
886 1930
887 1931 /**
@@ -894,9 +1938,9 @@
894 1938 check_ajax_referer( 'search-properties', 'security' );
895 1939
896 1940 $return = array();
897 1941
898 - $keyword = trim( $_POST['keyword'] );
1942 + $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
899 1943
900 1944 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
901 1945 {
902 1946 // Get all contacts that match the name
@@ -902,33 +1946,78 @@
902 1946 // Get all contacts that match the name
903 1947 $args = array(
904 1948 'post_type' => 'property',
905 1949 'nopaging' => true,
906 - 'post_status' => array( 'publish' ),
1950 + 'post_status' => array( 'publish', 'draft', 'private' ),
907 1951 'fields' => 'ids'
908 1952 );
909 1953
910 - $meta_query = array();
911 - 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 )
912 1972 {
913 - $meta_query[] = array(
914 - 'key' => '_department',
915 - 'value' => $_POST['department'],
1973 + $departments_query = array(
1974 + 'relation' => 'OR',
916 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;
917 2010 }
2011 +
918 2012 if ( !empty($meta_query) )
919 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.
920 2015 $args['meta_query'] = $meta_query;
921 2016 }
922 2017
923 - add_filter( 'posts_join', array( $this, 'search_properties_join' ), 10, 2 );
924 - add_filter( 'posts_where', array( $this, 'search_properties_where' ), 10, 2 );
925 -
926 2018 $property_query = new WP_Query( $args );
927 2019
928 - remove_filter( 'posts_join', array( $this, 'search_properties_join' ) );
929 - remove_filter( 'posts_where', array( $this, 'search_properties_where' ) );
930 -
931 2020 if ( $property_query->have_posts() )
932 2021 {
933 2022 while ( $property_query->have_posts() )
934 2023 {
@@ -934,12 +2023,31 @@
934 2023 {
935 2024 $property_query->the_post();
936 2025
937 2026 $property = new PH_Property(get_the_ID());
2027 +
2028 + $owner_id = $property->_owner_contact_id;
2029 + $owner_name = '';
2030 + if ( ( is_array($owner_id) && !empty($owner_id) ) || ( !is_array($owner_id) && $owner_id != '' ) )
2031 + {
2032 + if ( is_array($owner_id) )
2033 + {
2034 + $owner_id = reset($owner_id);
2035 + }
2036 + $owner_name = get_the_title($owner_id);
2037 + }
2038 +
2039 + $post_title = $property->get_formatted_full_address();
2040 + if ( get_post_status() == 'draft' )
2041 + {
2042 + $post_title .= ' - Draft';
2043 + }
938 2044
939 2045 $return[] = array(
940 2046 'ID' => get_the_ID(),
941 - 'post_title' => $property->get_formatted_full_address(),
2047 + 'post_title' => $post_title,
2048 + 'owner_id' => $owner_id,
2049 + 'owner_name' => $owner_name
942 2050 );
943 2051 }
944 2052 }
945 2053
@@ -952,38 +2060,8 @@
952 2060 // Quit out
953 2061 die();
954 2062 }
955 2063
956 - public function search_properties_join( $joins )
957 - {
958 - global $wpdb;
959 -
960 - $joins .= " INNER JOIN {$wpdb->postmeta} AS mt1 ON {$wpdb->posts}.ID = mt1.post_id ";
961 -
962 - return $joins;
963 - }
964 -
965 - public function search_properties_where( $where )
966 - {
967 - $where .= " AND (
968 - (mt1.meta_key='_address_name_number' AND mt1.meta_value LIKE '" . esc_sql($_POST['keyword']). "%')
969 - OR
970 - (mt1.meta_key='_address_street' AND mt1.meta_value LIKE '" . esc_sql($_POST['keyword']). "%')
971 - OR
972 - (mt1.meta_key='_address_2' AND mt1.meta_value LIKE '" . esc_sql($_POST['keyword']). "%')
973 - OR
974 - (mt1.meta_key='_address_3' AND mt1.meta_value LIKE '" . esc_sql($_POST['keyword']). "%')
975 - OR
976 - (mt1.meta_key='_address_4' AND mt1.meta_value LIKE '" . esc_sql($_POST['keyword']). "%')
977 - OR
978 - (mt1.meta_key='_address_postcode' AND mt1.meta_value LIKE '" . esc_sql($_POST['keyword']). "%')
979 - OR
980 - (mt1.meta_key='_reference_number' AND mt1.meta_value = '" . esc_sql($_POST['keyword']). "')
981 - ) ";
982 -
983 - return $where;
984 - }
985 -
986 2064 /**
987 2065 * Search users/negotiators via ajax
988 2066 */
989 2067 public function search_negotiators() {
@@ -993,9 +2071,9 @@
993 2071 check_ajax_referer( 'search-negotiators', 'security' );
994 2072
995 2073 $return = array();
996 2074
997 - $keyword = trim( $_POST['keyword'] );
2075 + $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
998 2076
999 2077 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1000 2078 {
1001 2079 // Get all contacts that match the name
@@ -1001,10 +2079,14 @@
1001 2079 // Get all contacts that match the name
1002 2080 $args = array(
1003 2081 'number' => 9999,
1004 2082 'search' => $keyword . '*',
1005 - 'orderby' => 'display_name'
2083 + 'orderby' => 'display_name',
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') )
1006 2086 );
2087 +
2088 + $args = apply_filters( 'propertyhive_negotiators_query', $args );
1007 2089
1008 2090 $user_query = new WP_User_Query( $args );
1009 2091
1010 2092 // Get the results
@@ -1036,17 +2118,34 @@
1036 2118 */
1037 2119 public function add_note() {
1038 2120
1039 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 );
1040 2125
1041 - $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 + }
1042 2130
1043 2131 if ( $post_id > 0 ) {
1044 2132
1045 - $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'] ) );
1046 2135
1047 - $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);
1048 2143
2144 + $note = str_replace( array('<br>', '<br />'), "\n", $note );
2145 +
2146 + $note = wp_strip_all_tags( $note );
2147 +
1049 2148 // Add note/comment to property
1050 2149 $comment = array(
1051 2150 'note_type' => 'note',
1052 2151 'note' => $note
@@ -1051,33 +2150,27 @@
1051 2150 'note_type' => 'note',
1052 2151 'note' => $note
1053 2152 );
1054 2153
1055 - $data = array(
1056 - 'comment_post_ID' => $post_id,
1057 - 'comment_author' => $current_user->display_name,
1058 - 'comment_author_email' => '[email protected]',
1059 - 'comment_author_url' => '',
1060 - 'comment_date' => date("Y-m-d H:i:s"),
1061 - 'comment_content' => serialize($comment),
1062 - 'comment_approved' => 1,
1063 - 'comment_type' => 'propertyhive_note',
1064 - );
1065 - $comment_id = wp_insert_comment( $data );
2154 + if ( isset($_POST['pinned']) )
2155 + {
2156 + $comment['pinned'] = '1';
2157 + }
1066 2158
2159 + $comment_id = PH_Comments::insert_note( $post_id, $comment );
2160 +
1067 2161 if ($comment_id !== FALSE)
1068 2162 {
1069 2163 $comment = get_comment($comment_id);
1070 -
1071 2164 ?>
1072 2165 <li rel="<?php echo absint( $comment_id ) ; ?>" class="note">
1073 2166 <div class="note_content">
1074 - <?php echo wpautop( wptexturize( wp_kses_post( $note ) ) ); ?>
2167 + <?php echo wp_kses_post( wpautop( wptexturize( wp_kses_post( $note ) ) ) ); ?>
1075 2168 </div>
1076 2169 <p class="meta">
1077 - <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>
1078 - <?php if ( $comment->comment_author !== __( 'Property Hive', 'propertyhive' ) ) printf( ' ' . __( 'by %s', 'propertyhive' ), $comment->comment_author ); ?>
1079 - <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>
1080 2173 </p>
1081 2174 </li>
1082 2175 <?php
1083 2176 }
@@ -1084,9 +2177,9 @@
1084 2177 }
1085 2178
1086 2179 // Quit out
1087 2180 die();
1088 - }
2181 + }
1089 2182
1090 2183 /**
1091 2184 * Delete order note via ajax
1092 2185 */
@@ -1093,19 +2186,229 @@
1093 2186 public function delete_note() {
1094 2187
1095 2188 check_ajax_referer( 'delete-note', 'security' );
1096 2189
1097 - $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 );
1098 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 +
1099 2199 if ( $note_id > 0 ) {
1100 2200 wp_delete_comment( $note_id );
2201 +
2202 + wp_send_json_success();
1101 2203 }
1102 2204
1103 - // Quit out
1104 - die();
2205 + wp_send_json_error();
1105 2206 }
1106 -
2207 +
1107 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 + /**
1108 2411 * Delete order note via ajax
1109 2412 */
1110 2413 public function make_property_enquiry() {
1111 2414
@@ -1112,30 +2415,26 @@
1112 2415 global $post;
1113 2416
1114 2417 $return = array();
1115 2418
1116 -
1117 2419 // Validate
1118 2420 $errors = array();
1119 - //if ( ! array_key_exists( 'property_id', $form_controls ) )
1120 - //{
1121 - // $errors[] = __( 'Property ID is a required field and must be supplied when making an enquiry', 'propertyhive' );
1122 - //}
1123 - //else
1124 - //{
1125 - if ( ! isset( $_POST['property_id'] ) || ( isset( $_POST['property_id'] ) && empty( $_POST['property_id'] ) ) )
1126 - {
1127 - $errors[] = __( 'Property ID is a required field and must be supplied when making an enquiry', 'propertyhive' ) . ': ' . $key;
1128 - }
1129 - else
1130 - {
1131 - $post = get_post($_POST['property_id']);
1132 -
1133 - $form_controls = ph_get_property_enquiry_form_fields();
1134 -
1135 - $form_controls = apply_filters( 'propertyhive_property_enquiry_form_fields', $form_controls );
1136 - }
1137 - //}
2421 + $form_controls = array();
2422 +
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'] ) )
2425 + {
2426 + $errors[] = __( 'Property ID is a required field and must be supplied when making an enquiry', 'propertyhive' );
2427 + }
2428 + else
2429 + {
2430 + //$post = get_post((int)$_POST['property_id']);
2431 +
2432 + $form_controls = ph_get_property_enquiry_form_fields();
2433 +
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'] ) ) );
2436 + }
1138 2437
1139 2438 foreach ( $form_controls as $key => $control )
1140 2439 {
1141 2440 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
@@ -1140,19 +2439,171 @@
1140 2439 {
1141 2440 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
1142 2441 {
1143 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.
1144 2444 if ( ! isset( $_POST[$key] ) || ( isset( $_POST[$key] ) && empty( $_POST[$key] ) ) )
1145 2445 {
1146 2446 $errors[] = __( 'Missing required field', 'propertyhive' ) . ': ' . $key;
1147 2447 }
1148 2448 }
1149 - 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] ) ) ) )
1150 2451 {
1151 2452 $errors[] = __( 'Invalid email address provided', 'propertyhive' ) . ': ' . $key;
1152 2453 }
2454 + if ( in_array( $key, array('recaptcha', 'recaptcha-v3') ) )
2455 + {
2456 + $errors = $this->check_recaptcha_form_response($errors, $key, $control);
2457 + }
2458 + if ( $key == 'hCaptcha' )
2459 + {
2460 + $secret = isset( $control['secret'] ) ? $control['secret'] : '';
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'] ) ) : '';
2463 +
2464 + $response = wp_remote_post(
2465 + 'https://hcaptcha.com/siteverify',
2466 + array(
2467 + 'method' => 'POST',
2468 + 'body' => array( 'secret' => $secret, 'response' => $response ),
2469 + )
2470 + );
2471 +
2472 + if ( is_wp_error( $response ) )
2473 + {
2474 + $errors[] = $response->get_error_message();
2475 + }
2476 + else
2477 + {
2478 + $response = json_decode($response['body'], TRUE);
2479 + if ( $response === FALSE )
2480 + {
2481 + $errors[] = __( 'Error decoding response from hCaptcha check', 'propertyhive' );
2482 + }
2483 + else
2484 + {
2485 + if ( isset($response['success']) && $response['success'] == true )
2486 + {
2487 +
2488 + }
2489 + else
2490 + {
2491 + $errors[] = __( 'Failed hCaptcha validation', 'propertyhive' );
2492 + }
2493 + }
2494 + }
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 + }
1153 2537 }
1154 -
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 +
1155 2606 if ( !empty($errors) )
1156 2607 {
1157 2608 // Failed validation
1158 2609
@@ -1161,20 +2612,18 @@
1161 2612 $return['errors'] = $errors;
1162 2613 }
1163 2614 else
1164 2615 {
1165 - // Passed validation
1166 -
1167 2616 // Get recipient email address
1168 2617 $to = '';
1169 2618
1170 2619 // Try and get office's email address first, else fallback to admin email
1171 - $office_id = get_post_meta($_POST['property_id'], '_office_id', TRUE);
2620 + $office_id = get_post_meta((int)$property_ids[0], '_office_id', TRUE);
1172 2621 if ( $office_id != '' )
1173 2622 {
1174 2623 if ( get_post_type( $office_id ) == 'office' )
1175 2624 {
1176 - $property_department = get_post_meta($_POST['property_id'], '_department', TRUE);
2625 + $property_department = get_post_meta((int)$property_ids[0], '_department', TRUE);
1177 2626
1178 2627 $fields_to_check = array();
1179 2628 switch ( $property_department )
1180 2629 {
@@ -1198,8 +2647,16 @@
1198 2647 $fields_to_check[] = '_office_email_address_lettings';
1199 2648 $fields_to_check[] = '_office_email_address_sales';
1200 2649 break;
1201 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 + }
1202 2659 }
1203 2660
1204 2661 foreach ( $fields_to_check as $field_to_check )
1205 2662 {
@@ -1214,25 +2671,60 @@
1214 2671 if ( $to == '' )
1215 2672 {
1216 2673 $to = get_option( 'admin_email' );
1217 2674 }
1218 -
1219 - $subject = __( 'New Property Enquiry', 'propertyhive' ) . ': ' . get_the_title( $_POST['property_id'] );
2675 +
2676 + if ( count($property_ids) == 1 )
2677 + {
2678 + $subject = __( 'New Property Enquiry', 'propertyhive' ) . ': ' . get_the_title( (int)$property_ids[0] );
2679 + }
2680 + else
2681 + {
2682 + $subject = __( 'Multiple Property Enquiry', 'propertyhive' ) . ': ' . count($property_ids) . ' Properties';
2683 + }
1220 2684 $message = __( "You have received a property enquiry via your website. Please find details of the enquiry below", 'propertyhive' ) . "\n\n";
1221 2685
1222 - $message = apply_filters( 'propertyhive_property_enquiry_pre_body', $message, $_POST['property_id'] );
2686 + $message = apply_filters( 'propertyhive_property_enquiry_pre_body', $message, $property_ids );
1223 2687
1224 - $message .= __( 'Property', 'propertyhive' ) . ': ' . get_the_title( $_POST['property_id'] ) . " (" . get_permalink( $_POST['property_id'] ) . ")\n\n";
1225 -
2688 + $message .= ( count($property_ids) > 1 ? __( 'Properties', 'propertyhive' ) : __( 'Property', 'propertyhive' ) ) . ":\n";
2689 + foreach ( $property_ids as $property_id )
2690 + {
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";
2693 + }
2694 +
1226 2695 unset($form_controls['action']);
1227 2696 unset($_POST['action']);
1228 - unset($form_controls['property_id']); // Unset so the fields dosn't get shown in the enquiry details
2697 + unset($form_controls['property_id']); // Unset so the field doesn't get shown in the enquiry details
1229 2698
2699 + $form_controls = apply_filters( 'propertyhive_property_enquiry_body_form_fields', $form_controls );
2700 +
1230 2701 foreach ($form_controls as $key => $control)
1231 2702 {
2703 + if ( isset($control['type']) && in_array($control['type'], array('html', 'recaptcha', 'recaptcha-v3', 'hCaptcha', 'turnstile')) ) { continue; }
2704 +
1232 2705 $label = ( isset($control['label']) ) ? $control['label'] : $key;
1233 - $message .= $label . ": " . $_POST[$key] . "\n";
2706 + $label = ( isset($control['email_label']) ) ? $control['email_label'] : $label;
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] ) ) : '';
2709 +
2710 + $message .= wp_strip_all_tags($label) . ": " . wp_strip_all_tags($value) . "\n";
1234 2711 }
2712 +
2713 + if (
2714 + apply_filters('propertyhive_enquiry_email_show_manage_link', true) &&
2715 + count($property_ids) == 1 &&
2716 + get_option( 'propertyhive_module_disabled_enquiries', '' ) != 'yes' &&
2717 + get_option( 'propertyhive_store_property_enquiries', 'yes' ) == 'yes'
2718 + )
2719 + {
2720 + $post_type_object = get_post_type_object( 'property' );
2721 + $property_enquiries_url = admin_url( sprintf( $post_type_object->_edit_link . '&action=edit', (int)$property_ids[0] ) ) . '#propertyhive-property-enquiries';
2722 + $message .= "\n" . __( "To manage this enquiry please visit the following URL", 'propertyhive' ) . ':' . "\n\n";
2723 + $message .= $property_enquiries_url;
2724 + }
2725 +
2726 + $message = apply_filters( 'propertyhive_property_enquiry_post_body', $message, $property_ids );
1235 2727
1236 2728 $from_email_address = get_option('propertyhive_email_from_address', '');
1237 2729 if ( $from_email_address == '' )
1238 2730 {
@@ -1240,31 +2732,54 @@
1240 2732 }
1241 2733 if ( $from_email_address == '' )
1242 2734 {
1243 2735 // Should never get here
1244 - $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'] ) ) : '';
1245 2738 }
1246 2739
1247 2740 $headers = array();
1248 - 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 !== '' )
1249 2752 {
1250 - $headers[] = 'From: ' . sanitize_text_field( $_POST['name'] ) . ' <' . sanitize_email( $from_email_address ) . '>';
2753 + $headers[] = sprintf( 'From: %s <%s>', $name, $from_email_address );
1251 2754 }
1252 2755 else
1253 2756 {
1254 - $headers[] = 'From: <' . sanitize_email( $from_email_address ) . '>';
2757 + $headers[] = sprintf( 'From: <%s>', $from_email_address );
1255 2758 }
1256 - 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']) )
1257 2762 {
1258 - $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 + }
1259 2770 }
1260 2771
1261 - $to = apply_filters( 'propertyhive_property_enquiry_to', $to, $_POST['property_id'] );
1262 - $subject = apply_filters( 'propertyhive_property_enquiry_subject', $subject, $_POST['property_id'] );
1263 - $message = apply_filters( 'propertyhive_property_enquiry_body', $message, $_POST['property_id'] );
1264 - $headers = apply_filters( 'propertyhive_property_enquiry_headers', $headers, $_POST['property_id'] );
2772 + $to = apply_filters( 'propertyhive_property_enquiry_to', $to, $property_ids );
2773 + $subject = apply_filters( 'propertyhive_property_enquiry_subject', $subject, $property_ids );
2774 + $headers = apply_filters( 'propertyhive_property_enquiry_headers', $headers, $property_ids );
2775 + $message = apply_filters( 'propertyhive_property_enquiry_body', $message, $property_ids );
2776 +
2777 + do_action( 'propertyhive_before_property_enquiry_sent' );
1265 2778
1266 2779 $sent = wp_mail( $to, $subject, $message, $headers );
2780 +
2781 + do_action( 'propertyhive_after_property_enquiry_sent' );
1267 2782
1268 2783 if ( ! $sent )
1269 2784 {
1270 2785 $return['success'] = false;
@@ -1273,42 +2788,79 @@
1273 2788 }
1274 2789 else
1275 2790 {
1276 2791 $return['success'] = true;
2792 +
2793 + $enquiry_post_id = '';
1277 2794
1278 - // Now insert into enquiries section of WordPress
1279 - $title = __( 'Property Enquiry', 'propertyhive' ) . ': ' . get_the_title( $_POST['property_id'] );
1280 - if ( isset($_POST['name']) && ! empty($_POST['name']) )
2795 + if ( get_option( 'propertyhive_store_property_enquiries', 'yes' ) == 'yes' )
1281 2796 {
1282 - $title .= __( ' from ', 'propertyhive' ) . sanitize_text_field($_POST['name']);
2797 + // Now insert into enquiries section of WordPress
2798 + if ( count($property_ids) == 1 )
2799 + {
2800 + $title = __( 'Property Enquiry', 'propertyhive' ) . ': ' . get_the_title( (int)$property_ids[0] );
2801 + }
2802 + else
2803 + {
2804 + $title = __( 'Multiple Property Enquiry', 'propertyhive' );
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.
2807 + if ( isset($_POST['name']) && ! empty($_POST['name']) )
2808 + {
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']));
2811 + }
2812 +
2813 + $enquiry_post = array(
2814 + 'post_title' => $title,
2815 + 'post_content' => '',
2816 + 'post_type' => 'enquiry',
2817 + 'post_status' => 'publish',
2818 + 'comment_status' => 'closed',
2819 + 'ping_status' => 'closed',
2820 + );
2821 +
2822 + // Insert the post into the database
2823 + $enquiry_post_id = wp_insert_post( $enquiry_post );
2824 +
2825 + add_post_meta( $enquiry_post_id, '_status', 'open' );
2826 + add_post_meta( $enquiry_post_id, '_source', 'website' );
2827 + add_post_meta( $enquiry_post_id, '_negotiator_id', '' );
2828 + add_post_meta( $enquiry_post_id, '_office_id', $office_id );
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.
2831 + foreach ($_POST as $key => $value)
2832 + {
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 ) )
2837 + {
2838 + continue;
2839 + }
2840 +
2841 + if ( $meta_key == 'property_id' )
2842 + {
2843 + foreach ( $property_ids as $property_id )
2844 + {
2845 + add_post_meta( $enquiry_post_id, $meta_key, (int)$property_id );
2846 + }
2847 + }
2848 + else
2849 + {
2850 + add_post_meta( $enquiry_post_id, $meta_key, sanitize_textarea_field(wp_unslash($value)) );
2851 + }
2852 + }
1283 2853 }
1284 -
1285 - $enquiry_post = array(
1286 - 'post_title' => $title,
1287 - 'post_content' => '',
1288 - 'post_type' => 'enquiry',
1289 - 'post_status' => 'publish',
1290 - 'comment_status' => 'closed',
1291 - 'ping_status' => 'closed',
1292 - );
1293 -
1294 - // Insert the post into the database
1295 - $enquiry_post_id = wp_insert_post( $enquiry_post );
1296 -
1297 - add_post_meta( $enquiry_post_id, '_status', 'open' );
1298 - add_post_meta( $enquiry_post_id, '_source', 'website' );
1299 - add_post_meta( $enquiry_post_id, '_negotiator_id', '' );
1300 - add_post_meta( $enquiry_post_id, '_office_id', $office_id );
1301 -
1302 - foreach ($_POST as $key => $value)
1303 - {
1304 - add_post_meta( $enquiry_post_id, $key, $value );
1305 - }
1306 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 +
1307 2858 // Send auto-responder
1308 2859 if ( get_option( 'propertyhive_enquiry_auto_responder', '' ) == 'yes' )
1309 2860 {
1310 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.
1311 2863 PH()->email->send_enquiry_auto_responder( $_POST );
1312 2864 }
1313 2865 }
1314 2866 }
@@ -1326,12 +2878,14 @@
1326 2878 public function create_contact_from_enquiry()
1327 2879 {
1328 2880 global $post;
1329 2881
1330 - $enquiry_post_id = ( (isset($_POST['post_id'])) ? $_POST['post_id'] : '' );
1331 - $nonce = ( (isset($_POST['security'])) ? $_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'] ) ) : '';
1332 2886
1333 - 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 ) )
1334 2888 {
1335 2889 // This nonce is not valid.
1336 2890 die( json_encode( array('error' => 'Invalid nonce. Please refresh and try again') ) );
1337 2891 }
@@ -1340,36 +2894,70 @@
1340 2894
1341 2895 $name = false;
1342 2896 $email = false;
1343 2897 $telephone = false;
2898 + $address = false;
2899 + $postcode = false;
2900 + $property_id = false;
1344 2901
1345 2902 foreach ($enquiry_meta as $key => $value)
1346 2903 {
1347 - if ( strpos($key, 'name') !== false )
2904 + if ( strpos(strtolower($key), 'name') !== false && strpos(strtolower($key), 'property') === false && $value[0] != '' )
1348 2905 {
1349 - $name = $value[0];
2906 + if ( $name === false )
2907 + {
2908 + $name = $value[0];
2909 + }
2910 + else
2911 + {
2912 + $name .= ' ' . $value[0];
2913 + }
1350 2914 }
1351 - elseif ( strpos($key, 'email') !== false )
2915 + elseif ( strpos(strtolower($key), 'email') !== false && $value[0] != '' )
1352 2916 {
1353 - $email = $value[0];
2917 + if ( $email === false )
2918 + {
2919 + $email = $value[0];
2920 + }
2921 + else
2922 + {
2923 + $email .= ',' . $value[0];
2924 + }
1354 2925 }
1355 - elseif ( strpos($key, 'telephone') !== false )
2926 + elseif ( strpos(strtolower($key), 'phone') !== false && $value[0] != '' )
1356 2927 {
1357 - $telephone = $value[0];
2928 + if ( $telephone === false )
2929 + {
2930 + $telephone = $value[0];
2931 + }
2932 + else
2933 + {
2934 + $telephone .= ',' . $value[0];
2935 + }
1358 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 + }
1359 2949 }
1360 2950
1361 - if ( $name === false || $email === false )
2951 + if ( $name === false && $email === false )
1362 2952 {
1363 - // This nonce is not valid.
1364 - die( json_encode( array('error' => 'Name or email address not found') ) );
2953 + die( json_encode( array('error' => 'Name and email address not found') ) );
1365 2954 }
1366 2955
1367 - // We've not imported this property before
1368 2956 $postdata = array(
1369 2957 'post_excerpt' => '',
1370 2958 'post_content' => '',
1371 - 'post_title' => utf8_encode(wp_strip_all_tags( $name )),
2959 + 'post_title' => wp_strip_all_tags( $name ),
1372 2960 'post_status' => 'publish',
1373 2961 'post_type' => 'contact',
1374 2962 'ping_status' => 'closed',
1375 2963 'comment_status' => 'closed',
@@ -1385,11 +2973,136 @@
1385 2973 {
1386 2974 die( json_encode( array('error' => 'Error creating contact') ) );
1387 2975 }
1388 2976
1389 - if ( $telephone !== FALSE ) { update_post_meta( $contact_post_id, '_telephone_number', $telephone ); }
1390 - if ( $email !== FALSE ) { update_post_meta( $contact_post_id, '_email_address', $email ); }
2977 + update_post_meta( $enquiry_post_id, '_contact_id', $contact_post_id );
1391 2978
2979 + if ( $telephone !== FALSE ) {
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) ) );
2982 + }
2983 +
2984 + if ( $email !== FALSE ) { update_post_meta( $contact_post_id, '_email_address', ph_clean( $email ) ); }
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 +
3103 + do_action('propertyhive_create_contact_from_enquiry', $enquiry_post_id, $contact_post_id);
3104 +
1392 3105 die( json_encode( array('success' => get_edit_post_link($contact_post_id, '')) ) );
1393 3106 }
1394 3107
1395 3108 public function validate_save_contact()
@@ -1399,15 +3112,21 @@
1399 3112 check_ajax_referer( 'contact-save-validation', 'security' );
1400 3113
1401 3114 $this->json_headers();
1402 3115
1403 - parse_str($_POST['form_data']);
1404 - var_dump();
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;
3123 +
1405 3124 $return = array('errors' => array());
1406 3125
1407 - if ( isset($_email_address) && $_email_address != '' )
3126 + if ( '' !== $email_address_input )
1408 3127 {
1409 - $email_addresses = explode( ",", $_email_address );
3128 + $email_addresses = explode( ",", $email_address_input );
1410 3129
1411 3130 foreach ( $email_addresses as $email_address )
1412 3131 {
1413 3132 $email_address = trim( $email_address );
@@ -1422,8 +3141,9 @@
1422 3141 'post_type' => 'contact',
1423 3142 'post_status' => 'any',
1424 3143 'posts_per_page' => 1,
1425 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.
1426 3146 'meta_query' => array(
1427 3147 'relation' => 'OR',
1428 3148 array(
1429 3149 'key' => '_email_address',
@@ -1442,11 +3162,12 @@
1442 3162 'compare' => 'LIKE'
1443 3163 )
1444 3164 )
1445 3165 );
1446 - if ( isset($post_ID) && $post_ID != '' )
3166 + if ( $contact_id )
1447 3167 {
1448 - $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 );
1449 3170 }
1450 3171
1451 3172 $contact_query = new WP_Query( $args );
1452 3173
@@ -1455,9 +3176,10 @@
1455 3176 while ( $contact_query->have_posts() )
1456 3177 {
1457 3178 $contact_query->the_post();
1458 3179
1459 - $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 );
1460 3182 }
1461 3183 }
1462 3184 }
1463 3185 }
@@ -1466,8 +3188,80 @@
1466 3188
1467 3189 die();
1468 3190 }
1469 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 +
1470 3264 // Dashboard related functions
1471 3265 public function get_news()
1472 3266 {
1473 3267 $this->json_headers();
@@ -1492,9 +3286,9 @@
1492 3286 foreach ( $rss_items as $item )
1493 3287 {
1494 3288 $return[] = array(
1495 3289 'title' => esc_html( $item->get_title() ),
1496 - 'permalink' => esc_url( $item->get_permalink() ),
3290 + 'permalink' => esc_url( $item->get_permalink() ) . '?src=dashboard',
1497 3291 'date' => $item->get_date('F d, Y')
1498 3292 );
1499 3293 }
1500 3294
@@ -1516,8 +3310,9 @@
1516 3310 $args = array(
1517 3311 'post_type' => 'viewing',
1518 3312 'fields' => 'ids',
1519 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.
1520 3315 'meta_query' => array(
1521 3316 array(
1522 3317 'key' => '_status',
1523 3318 'value' => 'carried_out'
@@ -1528,8 +3323,10 @@
1528 3323 )
1529 3324 )
1530 3325 );
1531 3326
3327 + $args = apply_filters( 'propertyhive_admin_dashboard_viewings_awaiting_applicant_feedback_args', $args );
3328 +
1532 3329 $viewings_query = new WP_Query( $args );
1533 3330
1534 3331 if ( $viewings_query->have_posts() )
1535 3332 {
@@ -1539,19 +3336,19 @@
1539 3336
1540 3337 $property_id = get_post_meta( get_the_ID(), '_property_id', TRUE );
1541 3338 $property = new PH_Property((int)$property_id);
1542 3339
1543 - $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' );
1544 3341
1545 3342 $return[] = array(
1546 3343 'ID' => get_the_ID(),
1547 3344 'edit_link' => get_edit_post_link( get_the_ID() ),
1548 3345 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
1549 - '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 ))),
1550 3347 'property_id' => $property_id,
1551 3348 'property_address' => $property->get_formatted_full_address(),
1552 - 'applicant_contact_id' => $applicant_contact_id,
1553 - '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] ),
1554 3351 );
1555 3352 }
1556 3353 }
1557 3354
@@ -1561,8 +3358,1586 @@
1561 3358
1562 3359 die();
1563 3360 }
1564 3361
3362 + public function get_my_upcoming_appointments()
3363 + {
3364 + global $post;
3365 +
3366 + $this->json_headers();
3367 +
3368 + $return = array();
3369 +
3370 + $args = array(
3371 + 'post_type' => 'viewing',
3372 + 'fields' => 'ids',
3373 + 'post_status' => 'publish',
3374 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Dashboard scopes upcoming events by status, time and current negotiator metadata with WordPress's default page limit.
3375 + 'meta_query' => array(
3376 + array(
3377 + 'key' => '_status',
3378 + 'value' => 'pending'
3379 + ),
3380 + array(
3381 + 'key' => '_start_date_time',
3382 + 'value' => gmdate("Y-m-d H:i:s"),
3383 + 'compare' => '>='
3384 + ),
3385 + array(
3386 + 'key' => '_negotiator_id',
3387 + 'value' => get_current_user_id(),
3388 + ),
3389 + )
3390 + );
3391 +
3392 + $args = apply_filters( 'propertyhive_admin_dashboard_my_upcoming_appointments_viewing_args', $args );
3393 + $args = apply_filters( 'propertyhive_admin_dashboard_my_upcoming_appointments_args', $args );
3394 +
3395 + $viewings_query = new WP_Query( $args );
3396 +
3397 + if ( $viewings_query->have_posts() )
3398 + {
3399 + while ( $viewings_query->have_posts() )
3400 + {
3401 + $viewings_query->the_post();
3402 +
3403 + $property_id = get_post_meta( get_the_ID(), '_property_id', TRUE );
3404 + $property = new PH_Property((int)$property_id);
3405 +
3406 + $return[] = array(
3407 + 'ID' => get_the_ID(),
3408 + 'edit_link' => get_edit_post_link( get_the_ID() ),
3409 + 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
3410 + 'start_date_time_formatted_Hi_jSFY' => gmdate("H:i jS F Y", strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE ))),
3411 + 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
3412 + 'title' => 'Viewing at ' . $property->get_formatted_full_address(),
3413 + );
3414 + }
3415 + }
3416 +
3417 + wp_reset_postdata();
3418 +
3419 + $args = array(
3420 + 'post_type' => 'appraisal',
3421 + 'fields' => 'ids',
3422 + 'post_status' => 'publish',
3423 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Dashboard scopes upcoming events by status, time and current negotiator metadata with WordPress's default page limit.
3424 + 'meta_query' => array(
3425 + array(
3426 + 'key' => '_status',
3427 + 'value' => 'pending'
3428 + ),
3429 + array(
3430 + 'key' => '_start_date_time',
3431 + 'value' => gmdate("Y-m-d H:i:s"),
3432 + 'compare' => '>='
3433 + ),
3434 + array(
3435 + 'key' => '_negotiator_id',
3436 + 'value' => get_current_user_id(),
3437 + ),
3438 + )
3439 + );
3440 +
3441 + $args = apply_filters( 'propertyhive_admin_dashboard_my_upcoming_appointments_appraisal_args', $args );
3442 + $args = apply_filters( 'propertyhive_admin_dashboard_my_upcoming_appointments_args', $args );
3443 +
3444 + $appraisals_query = new WP_Query( $args );
3445 +
3446 + if ( $appraisals_query->have_posts() )
3447 + {
3448 + while ( $appraisals_query->have_posts() )
3449 + {
3450 + $appraisals_query->the_post();
3451 +
3452 + $appraisal = new PH_Appraisal(get_the_ID());
3453 +
3454 + $return[] = array(
3455 + 'ID' => get_the_ID(),
3456 + 'edit_link' => get_edit_post_link( get_the_ID() ),
3457 + 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
3458 + 'start_date_time_formatted_Hi_jSFY' => gmdate("H:i jS F Y", strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE ))),
3459 + 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
3460 + 'title' => 'Appraisal at ' . $appraisal->get_formatted_full_address(),
3461 + );
3462 + }
3463 + }
3464 +
3465 + wp_reset_postdata();
3466 +
3467 + $return = apply_filters( 'propertyhive_dashboard_my_upcoming_appointments', $return );
3468 +
3469 + if ( !empty($return) )
3470 + {
3471 + $sort = array();
3472 + foreach ($return as $key => $part) {
3473 + $sort[$key] = strtotime($part['start_date_time']);
3474 + }
3475 + array_multisort($sort, SORT_ASC, $return);
3476 +
3477 + $return = array_slice($return, 0, 10);
3478 + }
3479 +
3480 + echo json_encode($return);
3481 +
3482 + die();
3483 + }
3484 +
3485 + public function get_upcoming_overdue_key_dates()
3486 + {
3487 + global $post;
3488 +
3489 + $this->json_headers();
3490 +
3491 + $return = array();
3492 +
3493 + $meta_query = array(
3494 + array(
3495 + 'key' => '_key_date_status',
3496 + 'value' => 'pending',
3497 + ),
3498 + );
3499 +
3500 + $upcoming_threshold = new DateTime('+ ' . apply_filters( 'propertyhive_key_date_upcoming_days', 7 ) . ' DAYS');
3501 + $meta_query[] = array(
3502 + 'key' => '_date_due',
3503 + 'value' => $upcoming_threshold->format('Y-m-d'),
3504 + 'type' => 'date',
3505 + 'compare' => '<=',
3506 + );
3507 +
3508 + $args = array(
3509 + 'post_type' => 'key_date',
3510 + 'fields' => 'ids',
3511 + 'post_status' => 'publish',
3512 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Dashboard filters and orders due dates stored in key-date metadata with WordPress's default page limit.
3513 + 'meta_query' => $meta_query,
3514 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Dashboard filters and orders due dates stored in key-date metadata with WordPress's default page limit.
3515 + 'meta_key' => '_date_due',
3516 + 'orderby' => 'meta_value',
3517 + 'order' => 'ASC',
3518 + );
3519 +
3520 + $args = apply_filters( 'propertyhive_admin_dashboard_upcoming_overdue_key_dates_args', $args );
3521 +
3522 + $key_dates_query = new WP_Query( $args );
3523 +
3524 + if ( $key_dates_query->have_posts() )
3525 + {
3526 + while ( $key_dates_query->have_posts() )
3527 + {
3528 + $key_dates_query->the_post();
3529 +
3530 + $key_date = new PH_Key_Date( get_post( get_the_ID() ) );
3531 +
3532 + $property_id = get_post_meta( get_the_ID(), '_property_id', TRUE );
3533 + $property_edit_link = '';
3534 + $property_address = '';
3535 + if ( !empty($property_id) )
3536 + {
3537 + $property = new PH_Property((int)$property_id);
3538 + $property_edit_link = get_edit_post_link( $property_id );
3539 + $property_address = $property->get_formatted_full_address();
3540 + }
3541 +
3542 + $tenancy_id = get_post_meta( get_the_ID(), '_tenancy_id', TRUE );
3543 + if ( !empty($tenancy_id) )
3544 + {
3545 + $key_date_edit_link = get_edit_post_link( $tenancy_id ) . '#propertyhive-tenancy-management%7Cpropertyhive-management-dates';
3546 + }
3547 + else
3548 + {
3549 + $key_date_edit_link = $property_edit_link . '#propertyhive-property-tenancies%7Cpropertyhive-management-dates';
3550 + }
3551 +
3552 + $due_date = $key_date->date_due();
3553 + $date_format = 'jS F Y';
3554 + if ( $due_date->format('H:i') != '00:00' )
3555 + {
3556 + $date_format = 'H:i ' . $date_format;
3557 + }
3558 +
3559 + $return[] = array(
3560 + 'ID' => get_the_ID(),
3561 + 'key_date_edit_link' => $key_date_edit_link,
3562 + 'description' => $key_date->description(),
3563 + 'upcoming_overdue_status' => $key_date->status(),
3564 + 'property_edit_link' => $property_edit_link,
3565 + 'property_address' => $property_address,
3566 + 'due_date_time_formatted' => $due_date->format($date_format),
3567 + );
3568 + }
3569 + }
3570 +
3571 + wp_reset_postdata();
3572 +
3573 + echo json_encode($return);
3574 +
3575 + die();
3576 + }
3577 +
3578 + public function check_duplicate_reference_number()
3579 + {
3580 + check_ajax_referer( 'check-duplicate-reference-number', 'security' );
3581 +
3582 + if ( !isset($_POST['reference_number']) || empty($_POST['reference_number']) )
3583 + {
3584 + echo '';
3585 + die();
3586 + }
3587 +
3588 + $args = array(
3589 + 'post_type' => 'property',
3590 + 'post_status' => 'publish',
3591 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Duplicate detection must match on-market/reference metadata and exclude the current integer record ID; query retains the default page limit.
3592 + 'meta_query' => array(
3593 + array(
3594 + 'key' => '_on_market',
3595 + 'value' => 'yes'
3596 + ),
3597 + array(
3598 + 'key' => '_reference_number',
3599 + 'value' => sanitize_text_field( wp_unslash( $_POST['reference_number'] ) )
3600 + ),
3601 + ),
3602 + );
3603 +
3604 + if ( isset($_POST['post_id']) && !empty($_POST['post_id']) )
3605 + {
3606 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Duplicate detection must match on-market/reference metadata and exclude the current integer record ID; query retains the default page limit.
3607 + $args['post__not_in'] = array((int)$_POST['post_id']);
3608 + }
3609 +
3610 + $property_query = new WP_Query($args);
3611 +
3612 + if ( $property_query->have_posts() )
3613 + {
3614 + echo '1';
3615 + die();
3616 + }
3617 +
3618 + echo '';
3619 + die();
3620 + }
3621 +
3622 + public function osm_geocoding_request()
3623 + {
3624 + check_ajax_referer( 'osm_geocoding_request', 'security' );
3625 +
3626 + if ( ! isset( $_POST['country'], $_POST['address'] ) || ! is_string( $_POST['country'] ) || ! is_string( $_POST['address'] ) ) {
3627 + wp_send_json( array( 'error' => 'Invalid geocoding address.', 'lat' => '', 'lng' => '' ) );
3628 + }
3629 + $country = sanitize_text_field( wp_unslash( $_POST['country'] ) );
3630 + $address = sanitize_text_field( wp_unslash( $_POST['address'] ) );
3631 +
3632 + $lat = '';
3633 + $lng = '';
3634 + $error = '';
3635 +
3636 + // Rate limit: 1 request/second
3637 + $rate_key = 'ph_osm_geo_last_ts';
3638 + $last_ts = (int)get_transient( $rate_key );
3639 + $now = time();
3640 +
3641 + if ( $last_ts && ($now - $last_ts) < 1 )
3642 + {
3643 + // Too soon: tell client to retry shortly
3644 + $error = 'Too many geocoding requests. Please wait a second and try again.';
3645 + wp_send_json( array( 'error' => $error ) );
3646 + }
3647 +
3648 + // Set timestamp immediately to prevent stampedes
3649 + set_transient( $rate_key, $now );
3650 +
3651 + $request_url = add_query_arg( array(
3652 + 'format' => 'json',
3653 + 'limit' => 1,
3654 + 'countrycodes' => rawurlencode( strtolower( $country ) ),
3655 + 'addressdetails' => 1,
3656 + 'q' => rawurlencode( $address ),
3657 + ), 'https://nominatim.openstreetmap.org/search' );
3658 +
3659 + $response = wp_remote_get(
3660 + $request_url,
3661 + array(
3662 + 'headers' => array(
3663 + 'Referer' => home_url(),
3664 + 'User-Agent' => 'Property-Hive/' . PH_VERSION . ' (+https://wp-property-hive.com)',
3665 + ),
3666 + )
3667 + );
3668 +
3669 + if ( is_wp_error( $response ))
3670 + {
3671 + $error = $response->get_error_message();
3672 + wp_send_json( array( 'error' => $error, 'lat' => $lat, 'lng' => $lng ) );
3673 + }
3674 +
3675 + if ( wp_remote_retrieve_response_code($response) !== 200 )
3676 + {
3677 + $error = wp_remote_retrieve_response_code($response) . ' response received when geocoding address ' . $address . '. Error message: ' . wp_remote_retrieve_response_message($response);
3678 + wp_send_json( array( 'error' => $error, 'lat' => $lat, 'lng' => $lng ) );
3679 + }
3680 +
3681 + if ( is_array( $response ) )
3682 + {
3683 + $body = wp_remote_retrieve_body( $response );
3684 + $json = json_decode($body, true);
3685 +
3686 + if ( !empty($json) && isset($json[0]['lat']) && isset($json[0]['lon']) )
3687 + {
3688 + $lat = $json[0]['lat'];
3689 + $lng = $json[0]['lon'];
3690 + }
3691 + else
3692 + {
3693 + $error = 'No co-ordinates returned for the address provided ' . $address . ': ' . $body;
3694 + }
3695 + }
3696 + else
3697 + {
3698 + $error = 'Failed to parse JSON response from OSM Geocoding service: ' . wp_json_encode( $response );
3699 + }
3700 +
3701 + wp_send_json( array( 'error' => $error, 'lat' => $lat, 'lng' => $lng ) );
3702 + }
3703 +
3704 + public function get_property_marketing_statistics_meta_box()
3705 + {
3706 + check_ajax_referer( 'get_property_marketing_statistics_meta_box', 'security' );
3707 +
3708 + global $post;
3709 + $post_id = isset( $_POST['post_id'] ) && is_scalar( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
3710 + if ( $post_id < 1 || 'property' !== get_post_type( $post_id ) || ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'edit_post', $post_id ) ) {
3711 + wp_send_json_error( __( 'Invalid property or insufficient permissions.', 'propertyhive' ), 403 );
3712 + }
3713 +
3714 +
3715 +
3716 +
3717 + $view_statistics = get_post_meta( $post_id, '_view_statistics', TRUE );
3718 + if ( !is_array($view_statistics) )
3719 + {
3720 + $view_statistics = array();
3721 + }
3722 +
3723 + $date_from = isset( $_POST['statistics_date_from'] ) && is_string( $_POST['statistics_date_from'] ) ? sanitize_text_field( wp_unslash( $_POST['statistics_date_from'] ) ) : gmdate("Y-m-d", strtotime('7 days ago'));
3724 + $date_from = strtotime($date_from);
3725 +
3726 + $date_to = isset( $_POST['statistics_date_to'] ) && is_string( $_POST['statistics_date_to'] ) ? sanitize_text_field( wp_unslash( $_POST['statistics_date_to'] ) ) : gmdate("Y-m-d");
3727 + $date_to = strtotime($date_to);
3728 + if ( false === $date_from || false === $date_to ) {
3729 + wp_send_json_error( __( 'Invalid statistics dates.', 'propertyhive' ), 400 );
3730 + }
3731 +
3732 + echo '<div class="propertyhive_meta_box"><div class="options_group">';
3733 + $view_statistics_output = array();
3734 + $total_views = 0;
3735 +
3736 + for ($i = $date_from; $i <= $date_to; $i += 86400)
3737 + {
3738 + if ( isset($view_statistics[gmdate("Y-m-d", $i)]) )
3739 + {
3740 + $view_statistics_output[] = array( $i * 1000, $view_statistics[gmdate("Y-m-d", $i)] );
3741 + $total_views += $view_statistics[gmdate("Y-m-d", $i)];
3742 + }
3743 + else
3744 + {
3745 + $view_statistics_output[] = array( $i * 1000, 0 );
3746 + }
3747 + }
3748 +
3749 + echo '<h3>' . esc_html(__( 'Views On Website', 'propertyhive' )) . ' (' . esc_html(number_format($total_views, 0)) . ')</h3>';
3750 +
3751 + echo '<div id="marketing_statistics_website_view_graph" style="height:400px; width:100%;"></div>';
3752 +
3753 + echo '</div>';
3754 +
3755 + echo '</div>';
3756 +
3757 + echo '<input type="hidden" name="marketing_statistics" id="marketing_statistics" value="' . esc_attr(json_encode($view_statistics_output)) . '">';
3758 +
3759 + die();
3760 + }
3761 +
3762 + public function get_appraisal_details_meta_box()
3763 + {
3764 + global $post;
3765 +
3766 + check_ajax_referer( 'appraisal-details-meta-box', 'security' );
3767 +
3768 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
3769 + $post = get_post( $post_id );
3770 +
3771 + $appraisal = new PH_Appraisal( $post_id );
3772 +
3773 + echo '<div class="propertyhive_meta_box">';
3774 +
3775 + echo '<div class="options_group">';
3776 +
3777 + echo '<p class="form-field">
3778 +
3779 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
3780 +
3781 + ' . esc_html(ucwords(str_replace("_", " ", $appraisal->status)));
3782 +
3783 + echo '</p>';
3784 +
3785 + if ( $appraisal->status == 'cancelled' )
3786 + {
3787 + $args = array(
3788 + 'id' => '_cancelled_reason',
3789 + 'label' => __( 'Reason Cancelled', 'propertyhive' ),
3790 + 'desc_tip' => false,
3791 + 'class' => '',
3792 + 'value' => $appraisal->cancelled_reason,
3793 + 'custom_attributes' => array(
3794 + 'style' => 'width:95%; max-width:500px;'
3795 + )
3796 + );
3797 + propertyhive_wp_textarea_input( $args );
3798 + }
3799 +
3800 + if ( $appraisal->status == 'carried_out' || $appraisal->status == 'won' || $appraisal->status == 'instructed' )
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 +
3825 + if ( $appraisal->department == 'residential-sales' )
3826 + {
3827 + $args = array(
3828 + 'id' => '_valued_price',
3829 + 'label' => __( 'Valued Price', 'propertyhive' ) . ' (' . $currency_symbol . ')',
3830 + 'desc_tip' => false,
3831 + 'class' => 'short',
3832 + 'value' => ph_display_price_field( $appraisal->valued_price ),
3833 + );
3834 + propertyhive_wp_text_input( $args );
3835 + }
3836 + elseif ( $appraisal->department == 'residential-lettings' )
3837 + {
3838 + $rent_frequency = $appraisal->valued_rent_frequency;
3839 +
3840 + echo '<p class="form-field">
3841 +
3842 + <label for="">' . esc_html(__('Valued Rent', 'propertyhive')) . ' (' . esc_html($currency_symbol) . ')</label>
3843 +
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;">
3845 +
3846 + <select id="_valued_rent_frequency" name="_valued_rent_frequency" class="select" style="width:auto">
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>
3853 + </select>
3854 +
3855 + </p>';
3856 + }
3857 + }
3858 +
3859 + if ( $appraisal->status == 'lost' )
3860 + {
3861 + $args = array(
3862 + 'id' => '_lost_reason',
3863 + 'label' => __( 'Reason Lost', 'propertyhive' ),
3864 + 'desc_tip' => false,
3865 + 'class' => '',
3866 + 'value' => $appraisal->lost_reason,
3867 + 'custom_attributes' => array(
3868 + 'style' => 'width:95%; max-width:500px;'
3869 + )
3870 + );
3871 + propertyhive_wp_textarea_input( $args );
3872 + }
3873 +
3874 + do_action('propertyhive_appraisal_details_fields');
3875 +
3876 + echo '</div>';
3877 +
3878 + echo '</div>';
3879 +
3880 + die();
3881 + }
3882 +
3883 + public function get_appraisal_actions()
3884 + {
3885 + check_ajax_referer( 'appraisal-actions', 'security' );
3886 +
3887 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
3888 +
3889 + $status = get_post_meta( $post_id, '_status', TRUE );
3890 + $department = get_post_meta( $post_id, '_department', TRUE );
3891 +
3892 + echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_appraisal_actions_meta_box">
3893 +
3894 + <div class="options_group" style="padding-top:8px;">';
3895 +
3896 + $show_cancelled_meta_boxes = false;
3897 + $show_carried_out_meta_boxes = false;
3898 + $show_instructed_meta_boxes = false;
3899 + $show_lost_meta_boxes = false;
3900 + $show_customise_confirmation_meta_boxes = false;
3901 +
3902 + $actions = array();
3903 +
3904 + if ( $status == 'pending' )
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 +
3938 + /*$actions[] = '<a
3939 + href=""
3940 + class="button"
3941 + style="width:100%; margin-bottom:7px; text-align:center"
3942 + >' . __('Print Market Appraisal Sheet', 'propertyhive') . '</a>';
3943 + if ( get_option('propertyhive_module_disabled_contacts', '') != 'yes' )
3944 + {
3945 + $actions[] = '<a
3946 + href=""
3947 + class="button"
3948 + style="width:100%; margin-bottom:7px; text-align:center"
3949 + >' . __('Run Potential Applicant Match', 'propertyhive') . '</a>';
3950 + }*/
3951 + $actions[] = '<a
3952 + href="#action_panel_appraisal_carried_out"
3953 + class="button button-success appraisal-action"
3954 + style="width:100%; margin-bottom:7px; text-align:center"
3955 + >' . esc_html(__('Appraisal Carried Out', 'propertyhive')) . '</a>';
3956 + $actions[] = '<a
3957 + href="#action_panel_appraisal_cancelled"
3958 + class="button appraisal-action"
3959 + style="width:100%; margin-bottom:7px; text-align:center"
3960 + >' . esc_html(__('Appraisal Cancelled', 'propertyhive')) . '</a>';
3961 +
3962 + $show_cancelled_meta_boxes = true;
3963 + $show_carried_out_meta_boxes = true;
3964 + }
3965 +
3966 + if ( $status == 'carried_out' )
3967 + {
3968 + $actions[] = '<a
3969 + href="#action_panel_appraisal_won"
3970 + class="button button-success appraisal-action"
3971 + style="width:100%; margin-bottom:7px; text-align:center"
3972 + >' . esc_html(__('Appraisal Won', 'propertyhive')) . '</a>';
3973 +
3974 + $actions[] = '<a
3975 + href="#action_panel_appraisal_lost"
3976 + class="button button-danger appraisal-action"
3977 + style="width:100%; margin-bottom:7px; text-align:center"
3978 + >' . esc_html(__('Appraisal Lost', 'propertyhive')) . '</a>';
3979 +
3980 + $show_lost_meta_boxes = true;
3981 + }
3982 +
3983 + if ( $status == 'won' )
3984 + {
3985 + $actions[] = '<a
3986 + href="#action_panel_appraisal_instruct"
3987 + class="button button-success appraisal-action"
3988 + style="width:100%; margin-bottom:7px; text-align:center"
3989 + >' . esc_html(__('Instruct Property', 'propertyhive')) . '</a>';
3990 +
3991 + $show_instructed_meta_boxes = true;
3992 + }
3993 +
3994 + if ( $status == 'won' || $status == 'lost' )
3995 + {
3996 + $actions[] = '<a
3997 + href="#action_panel_appraisal_revert_carried_out"
3998 + class="button appraisal-action"
3999 + style="width:100%; margin-bottom:7px; text-align:center"
4000 + >' . esc_html(__('Revert To Carried Out', 'propertyhive')) . '</a>';
4001 + }
4002 +
4003 + if ( $status == 'instructed' )
4004 + {
4005 + $property_id = get_post_meta( $post_id, '_property_id', TRUE );
4006 +
4007 + $actions[] = '<a
4008 + href="' . esc_url(get_edit_post_link($property_id)) . '"
4009 + class="button"
4010 + style="width:100%; margin-bottom:7px; text-align:center"
4011 + >' . esc_html(__('View Instructed Property', 'propertyhive')) . '</a>';
4012 +
4013 + /*$actions[] = '<a
4014 + href="#action_panel_appraisal_revert_won"
4015 + class="button appraisal-action"
4016 + style="width:100%; margin-bottom:7px; text-align:center"
4017 + >' . __('Revert To Won', 'propertyhive') . '</a>';*/
4018 + }
4019 +
4020 + if ( $status == 'carried_out' || $status == 'cancelled' )
4021 + {
4022 + $actions[] = '<a
4023 + href="#action_panel_appraisal_revert_pending"
4024 + class="button appraisal-action"
4025 + style="width:100%; margin-bottom:7px; text-align:center"
4026 + >' . esc_html(__('Revert To Pending', 'propertyhive')) . '</a>';
4027 + }
4028 +
4029 + $actions = apply_filters( 'propertyhive_admin_appraisal_actions', $actions, $post_id );
4030 + $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
4031 +
4032 + if ( !empty($actions) )
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.
4035 + echo implode("", $actions);
4036 + }
4037 + else
4038 + {
4039 + echo '<div style="text-align:center">' . esc_html(__( 'No actions to display', 'propertyhive' )) . '</div>';
4040 + }
4041 +
4042 + echo '</div>
4043 +
4044 + </div>';
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 +
4095 + if ( $show_cancelled_meta_boxes )
4096 + {
4097 + echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_cancelled" style="display:none;">
4098 +
4099 + <div class="options_group" style="padding-top:8px;">
4100 +
4101 + <div class="form-field">
4102 +
4103 + <label for="_appraisal_cancelled_reason">' . esc_html(__( 'Reason Cancelled', 'propertyhive' )) . '</label>
4104 +
4105 + <textarea id="_cancelled_reason" name="_cancelled_reason" style="width:100%;">' . esc_html(get_post_meta( $post_id, '_cancelled_reason', TRUE )) . '</textarea>
4106 +
4107 + </div>
4108 +
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>
4111 +
4112 + </div>
4113 +
4114 + </div>';
4115 + }
4116 +
4117 + if ( $show_carried_out_meta_boxes )
4118 + {
4119 + echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_carried_out" style="display:none;">
4120 +
4121 + <div class="options_group" style="padding-top:8px;">';
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 +
4146 + if ( $department == 'residential-sales' )
4147 + {
4148 + echo '<div class="form-field">
4149 +
4150 + <label for="_price">' . esc_html(/* translators: %s: Currency symbol. */ sprintf( __( 'Valued Price (%s)', 'propertyhive' ), $currency_symbol )) . '</label>
4151 +
4152 + <input type="text" id="_price" name="_price" style="width:100%;" value="' . esc_attr(get_post_meta( $post_id, '_valued_price', TRUE )) . '">
4153 +
4154 + </div>';
4155 + }
4156 + else
4157 + {
4158 + $rent_frequency = get_post_meta( $post_id, '_valued_rent_frequency', TRUE );
4159 + echo '<div class="form-field">
4160 +
4161 + <label for="_price">' . esc_html(/* translators: %s: Currency symbol. */ sprintf( __( 'Valued Rent (%s)', 'propertyhive' ), $currency_symbol )) . '</label>
4162 +
4163 + <input type="text" id="_price" name="_price" style="width:100%;" value="' . esc_attr(get_post_meta( $post_id, '_valued_rent', TRUE )) . '">
4164 +
4165 + <select id="_rent_frequency" name="_rent_frequency" class="select" style="width:100%">
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>
4172 + </select>
4173 +
4174 + </div>';
4175 + }
4176 +
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>
4179 +
4180 + </div>
4181 +
4182 + </div>';
4183 + }
4184 +
4185 + if ( $show_instructed_meta_boxes )
4186 + {
4187 + echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_instruct" style="display:none;">
4188 +
4189 + <div class="options_group" style="padding-top:8px;">';
4190 +
4191 + echo '<div style="margin-bottom:13px;">' . esc_html(__( 'Upon instruction a new property record will be created within the \'Properties\' area.', 'propertyhive' )) . '</div>';
4192 +
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>
4195 +
4196 + </div>
4197 +
4198 + </div>';
4199 + }
4200 +
4201 + if ( $show_lost_meta_boxes )
4202 + {
4203 + echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_lost" style="display:none;">
4204 +
4205 + <div class="options_group" style="padding-top:8px;">
4206 +
4207 + <div class="form-field">
4208 +
4209 + <label for="_lost_reason">' . esc_html(__( 'Reason Lost', 'propertyhive' )) . '</label>
4210 +
4211 + <textarea id="_lost_reason" name="_lost_reason" style="width:100%;">' . esc_html(get_post_meta( $post_id, '_lost_reason', TRUE )) . '</textarea>
4212 +
4213 + </div>
4214 +
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>
4217 +
4218 + </div>
4219 +
4220 + </div>';
4221 + }
4222 +
4223 + die();
4224 + }
4225 +
4226 + public function appraisal_carried_out()
4227 + {
4228 + check_ajax_referer( 'appraisal-actions', 'security' );
4229 +
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 + }
4234 +
4235 + $status = get_post_meta( $post_id, '_status', TRUE );
4236 +
4237 + if ( $status == 'pending' )
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 + }
4258 + update_post_meta( $post_id, '_status', 'carried_out' );
4259 +
4260 + if ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-sales' )
4261 + {
4262 + $price = preg_replace("/[^0-9.]/", '', $valuation_input['price']);
4263 + update_post_meta( $post_id, '_valued_price', $price );
4264 + update_post_meta( $post_id, '_valued_price_actual', $price );
4265 + }
4266 + elseif ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-lettings' )
4267 + {
4268 + $rent = preg_replace("/[^0-9.]/", '', $valuation_input['rent']);
4269 + update_post_meta( $post_id, '_valued_rent', $rent );
4270 +
4271 + update_post_meta( $post_id, '_valued_rent_frequency', $valuation_input['rent_frequency'] );
4272 +
4273 + switch ($valuation_input['rent_frequency'])
4274 + {
4275 + case "pd": { $price = ($rent * 365) / 12; break; }
4276 + case "pppw":
4277 + {
4278 + $bedrooms = get_post_meta( $post_id, '_bedrooms', true );
4279 + if ( ( $bedrooms !== FALSE && $bedrooms != 0 && $bedrooms != '' ) && apply_filters( 'propertyhive_pppw_to_consider_bedrooms', true ) == true )
4280 + {
4281 + $price = (($rent * 52) / 12) * $bedrooms;
4282 + }
4283 + else
4284 + {
4285 + $price = ($rent * 52) / 12;
4286 + }
4287 + break;
4288 + }
4289 + case "pw": { $price = ($rent * 52) / 12; break; }
4290 + case "pcm": { $price = $rent; break; }
4291 + case "pq": { $price = ($rent * 4) / 12; break; }
4292 + case "pa": { $price = ($rent / 12); break; }
4293 + }
4294 + update_post_meta( $post_id, '_valued_price_actual', $price );
4295 + }
4296 +
4297 + // Add note/comment to appraisal
4298 + $comment = array(
4299 + 'note_type' => 'action',
4300 + 'action' => 'appraisal_carried_out',
4301 + );
4302 +
4303 + PH_Comments::insert_note( $post_id, $comment );
4304 +
4305 + wp_send_json_success();
4306 + }
4307 +
4308 + wp_send_json_success();
4309 + }
4310 +
4311 + public function appraisal_cancelled()
4312 + {
4313 + check_ajax_referer( 'appraisal-actions', 'security' );
4314 +
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 + }
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 +
4325 + $status = get_post_meta( $post_id, '_status', TRUE );
4326 +
4327 + if ( $status == 'pending' )
4328 + {
4329 + update_post_meta( $post_id, '_status', 'cancelled' );
4330 + update_post_meta( $post_id, '_cancelled_reason', wp_slash( $reason ) );
4331 +
4332 + // Add note/comment to appraisal
4333 + $comment = array(
4334 + 'note_type' => 'action',
4335 + 'action' => 'appraisal_cancelled',
4336 + );
4337 +
4338 + PH_Comments::insert_note( $post_id, $comment );
4339 +
4340 + wp_send_json_success();
4341 + }
4342 +
4343 + wp_send_json_error();
4344 + }
4345 +
4346 + public function appraisal_won()
4347 + {
4348 + check_ajax_referer( 'appraisal-actions', 'security' );
4349 +
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 + }
4354 +
4355 + $status = get_post_meta( $post_id, '_status', TRUE );
4356 +
4357 + if ( $status == 'carried_out' )
4358 + {
4359 + update_post_meta( $post_id, '_status', 'won' );
4360 +
4361 + // Add note/comment to appraisal
4362 + $comment = array(
4363 + 'note_type' => 'action',
4364 + 'action' => 'appraisal_won',
4365 + );
4366 +
4367 + PH_Comments::insert_note( $post_id, $comment );
4368 +
4369 + wp_send_json_success();
4370 + }
4371 +
4372 + wp_send_json_error();
4373 + }
4374 +
4375 + public function appraisal_lost_reason()
4376 + {
4377 + check_ajax_referer( 'appraisal-actions', 'security' );
4378 +
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 + }
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 +
4389 + $status = get_post_meta( $post_id, '_status', TRUE );
4390 +
4391 + if ( $status == 'carried_out' )
4392 + {
4393 + update_post_meta( $post_id, '_status', 'lost' );
4394 + update_post_meta( $post_id, '_lost_reason', wp_slash( $reason ) );
4395 +
4396 + // Add note/comment to appraisal
4397 + $comment = array(
4398 + 'note_type' => 'action',
4399 + 'action' => 'appraisal_lost',
4400 + );
4401 +
4402 + PH_Comments::insert_note( $post_id, $comment );
4403 +
4404 + wp_send_json_success();
4405 + }
4406 +
4407 + wp_send_json_error();
4408 + }
4409 +
4410 + public function appraisal_instructed()
4411 + {
4412 + check_ajax_referer( 'appraisal-actions', 'security' );
4413 +
4414 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4415 +
4416 + $status = get_post_meta( $post_id, '_status', TRUE );
4417 +
4418 + if ( $status == 'won' )
4419 + {
4420 + // Create property record and copy everything over
4421 + $display_address = array();
4422 + if ( get_post_meta( $post_id, '_address_street', TRUE ) != '' )
4423 + {
4424 + $display_address[] = get_post_meta( $post_id, '_address_street', TRUE );
4425 + }
4426 + if ( get_post_meta( $post_id, '_address_two', TRUE ) != '' )
4427 + {
4428 + $display_address[] = get_post_meta( $post_id, '_address_two', TRUE );
4429 + }
4430 + if ( get_post_meta( $post_id, '_address_three', TRUE ) != '' )
4431 + {
4432 + $display_address[] = get_post_meta( $post_id, '_address_three', TRUE );
4433 + }
4434 + else
4435 + {
4436 + if ( get_post_meta( $post_id, '_address_four', TRUE ) != '' )
4437 + {
4438 + $display_address[] = get_post_meta( $post_id, '_address_four', TRUE );
4439 + }
4440 + }
4441 + $display_address = implode(", ", $display_address);
4442 +
4443 + $property_post = array(
4444 + 'post_title' => ph_clean($display_address),
4445 + 'post_content' => '',
4446 + 'post_type' => 'property',
4447 + 'post_status' => 'publish',
4448 + 'comment_status' => 'closed',
4449 + 'ping_status' => 'closed',
4450 + );
4451 +
4452 + // Insert the post into the database
4453 + $property_post_id = wp_insert_post( $property_post );
4454 +
4455 + if ( is_wp_error($property_post_id) || $property_post_id == 0 )
4456 + {
4457 + // Failed. Don't really know at the moment how to handle this
4458 +
4459 + $return = array('error' => 'Failed to create property post. Please try again');
4460 + echo json_encode( $return );
4461 + die();
4462 + }
4463 + else
4464 + {
4465 + // Successfully added property post
4466 +
4467 + $department = get_post_meta( $post_id, '_department', TRUE );
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 +
4485 + update_post_meta( $property_post_id, '_address_name_number', get_post_meta( $post_id, '_address_name_number', TRUE ) );
4486 + update_post_meta( $property_post_id, '_address_street', get_post_meta( $post_id, '_address_street', TRUE ) );
4487 + update_post_meta( $property_post_id, '_address_two', get_post_meta( $post_id, '_address_two', TRUE ) );
4488 + update_post_meta( $property_post_id, '_address_three', get_post_meta( $post_id, '_address_three', TRUE ) );
4489 + update_post_meta( $property_post_id, '_address_four', get_post_meta( $post_id, '_address_four', TRUE ) );
4490 + update_post_meta( $property_post_id, '_address_postcode', get_post_meta( $post_id, '_address_postcode', TRUE ) );
4491 + update_post_meta( $property_post_id, '_address_country', get_post_meta( $post_id, '_address_country', TRUE ) );
4492 +
4493 + if ( ini_get('allow_url_fopen') )
4494 + {
4495 + // No lat lng. Let's get it
4496 + $address_to_geocode = array();
4497 + if ( get_post_meta( $post_id, '_address_name_number', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_name_number', TRUE ); }
4498 + if ( get_post_meta( $post_id, '_address_street', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_street', TRUE ); }
4499 + if ( get_post_meta( $post_id, '_address_two', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_two', TRUE ); }
4500 + if ( get_post_meta( $post_id, '_address_three', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_three', TRUE ); }
4501 + if ( get_post_meta( $post_id, '_address_four', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_four', TRUE ); }
4502 + if ( get_post_meta( $post_id, '_address_postcode', TRUE ) ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_postcode', TRUE ); }
4503 +
4504 + $country = get_option( 'propertyhive_default_country', 'GB' );
4505 +
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);
4522 +
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
4537 + {
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
4539 +
4540 + $api_key = get_option('propertyhive_google_maps_api_key', '');
4541 + if ( $api_key != '' ) { $request_url .= "&key=" . $api_key; }
4542 +
4543 + $response = wp_remote_get($request_url);
4544 +
4545 + if ( is_array( $response ) && !is_wp_error( $response ) )
4546 + {
4547 + $header = $response['headers']; // array of http header lines
4548 + $body = $response['body']; // use the content
4549 +
4550 + $xml = simplexml_load_string($body);
4551 +
4552 + if ( $xml !== FALSE )
4553 + {
4554 + $status = $xml->status; // Get the request status as google's api can return several responses
4555 +
4556 + if ($status == "OK")
4557 + {
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 + }
4567 + }
4568 + }
4569 + }
4570 + }
4571 + }
4572 +
4573 + update_post_meta( $property_post_id, '_department', $department );
4574 +
4575 + switch ( $department )
4576 + {
4577 + case "residential-sales":
4578 + {
4579 + update_post_meta( $property_post_id, '_currency', 'GBP' );
4580 +
4581 + $price = preg_replace("/[^0-9.]/", '', get_post_meta( $post_id, '_valued_price', TRUE ));
4582 + update_post_meta( $property_post_id, '_price', $price );
4583 +
4584 + break;
4585 + }
4586 + case "residential-lettings":
4587 + {
4588 + update_post_meta( $property_post_id, '_currency', 'GBP' );
4589 +
4590 + $rent = preg_replace("/[^0-9.]/", '', get_post_meta( $post_id, '_valued_rent', TRUE ));
4591 + update_post_meta( $property_post_id, '_rent', $rent );
4592 + update_post_meta( $property_post_id, '_rent_frequency', get_post_meta( $post_id, '_valued_rent_frequency', TRUE ) );
4593 +
4594 + break;
4595 + }
4596 + }
4597 +
4598 + // Store price in common currency (GBP) used for ordering
4599 + $ph_countries = new PH_Countries();
4600 + $ph_countries->update_property_price_actual( $property_post_id );
4601 +
4602 + update_post_meta( $property_post_id, '_bedrooms', get_post_meta( $post_id, '_bedrooms', TRUE ) );
4603 + update_post_meta( $property_post_id, '_bathrooms', get_post_meta( $post_id, '_bathrooms', TRUE ) );
4604 + update_post_meta( $property_post_id, '_reception_rooms', get_post_meta( $post_id, '_reception_rooms', TRUE ) );
4605 +
4606 + update_post_meta( $property_post_id, '_on_market', '' );
4607 + update_post_meta( $property_post_id, '_featured', '' );
4608 +
4609 + // Taxonomies
4610 + wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'property_type', array("fields" => "ids") ), 'property_type' );
4611 + wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'parking', array("fields" => "ids") ), 'parking' );
4612 + wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'outside_space', array("fields" => "ids") ), 'outside_space' );
4613 +
4614 + update_post_meta( $property_post_id, '_council_tax_band', get_post_meta( $post_id, '_council_tax_band', TRUE ) );
4615 +
4616 + $owner_contact_ids = get_post_meta( $post_id, '_property_owner_contact_id', TRUE );
4617 + if ( !is_array($owner_contact_ids) )
4618 + {
4619 + $owner_contact_ids = array($owner_contact_ids);
4620 + }
4621 + update_post_meta( $property_post_id, '_owner_contact_id', $owner_contact_ids );
4622 +
4623 + // Make updates to appraisal
4624 + update_post_meta( $post_id, '_status', 'instructed' );
4625 + update_post_meta( $post_id, '_property_id', $property_post_id );
4626 +
4627 + //Update owner(s)
4628 + foreach ( $owner_contact_ids as $owner_contact_id )
4629 + {
4630 + $contact_types = get_post_meta( $owner_contact_id, '_contact_types', TRUE );
4631 +
4632 + if ( !in_array('owner', $contact_types) )
4633 + {
4634 + $contact_types[] = 'owner';
4635 + }
4636 +
4637 + // get appraisals where this is the owner and where not instructed
4638 + $args = array(
4639 + 'post_type' => 'appraisal',
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.
4642 + 'meta_query' => array(
4643 + array(
4644 + 'key' => '_property_owner_contact_id',
4645 + 'value' => $owner_contact_id,
4646 + 'compare' => '='
4647 + ),
4648 + array(
4649 + 'key' => '_status',
4650 + 'value' => 'instructed',
4651 + 'compare' => '!='
4652 + )
4653 + )
4654 + );
4655 +
4656 + $appraisal_query = new WP_Query($args);
4657 +
4658 + if (!$appraisal_query->have_posts())
4659 + {
4660 + // no longer a potential owner.
4661 + if (($key = array_search('potentialowner', $contact_types)) !== false)
4662 + {
4663 + unset($contact_types[$key]);
4664 + }
4665 + }
4666 + wp_reset_postdata();
4667 +
4668 + update_post_meta( $owner_contact_id, '_contact_types', $contact_types );
4669 + }
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 + {
4813 + $current_user = wp_get_current_user();
4814 + $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
4815 + }
4816 + if ( $from == '' )
4817 + {
4818 + $from = get_option('propertyhive_email_from_address', '');
4819 + }
4820 + if ( $from == '' )
4821 + {
4822 + $from = get_bloginfo('admin_email');
4823 + }
4824 +
4825 + $headers = array();
4826 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
4827 + $headers[] = 'Reply-To: ' . sanitize_email($from);
4828 + $headers[] = 'Content-Type: text/plain; charset=UTF-8';
4829 +
4830 + $headers = apply_filters( 'propertyhive_appraisal_owner_booking_confirmation_email_headers', $headers );
4831 +
4832 + $sent = wp_mail($to, $subject, $body, $headers);
4833 +
4834 + if ( !$sent )
4835 + {
4836 + wp_send_json_error('Failed to send email');
4837 + }
4838 +
4839 + if ( apply_filters( 'propertyhive_log_booking_confirmation_emails', false ) === true )
4840 + {
4841 + // Add note/comment to appraisal
4842 + $comment = array(
4843 + 'note_type' => 'action',
4844 + 'action' => 'appraisal_owner_booking_confirmation_email',
4845 + );
4846 +
4847 + PH_Comments::insert_note( $post_id, $comment );
4848 + }
4849 +
4850 + update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
4851 +
4852 + wp_send_json_success();
4853 + }
4854 + else
4855 + {
4856 + wp_send_json_error('No owner recipients found');
4857 + }
4858 +
4859 + wp_die();
4860 + }
4861 +
4862 + public function appraisal_revert_pending()
4863 + {
4864 + check_ajax_referer( 'appraisal-actions', 'security' );
4865 +
4866 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4867 +
4868 + $status = get_post_meta( $post_id, '_status', TRUE );
4869 +
4870 + if ( $status == 'carried_out' || $status == 'cancelled' )
4871 + {
4872 + update_post_meta( $post_id, '_status', 'pending' );
4873 +
4874 + // Add note/comment to appraisal
4875 + $comment = array(
4876 + 'note_type' => 'action',
4877 + 'action' => 'appraisal_revert_pending',
4878 + );
4879 +
4880 + PH_Comments::insert_note( $post_id, $comment );
4881 +
4882 + wp_send_json_success();
4883 + }
4884 +
4885 + wp_send_json_error();
4886 + }
4887 +
4888 + public function appraisal_revert_carried_out()
4889 + {
4890 + check_ajax_referer( 'appraisal-actions', 'security' );
4891 +
4892 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4893 +
4894 + $status = get_post_meta( $post_id, '_status', TRUE );
4895 +
4896 + if ( $status == 'won' || $status == 'lost' )
4897 + {
4898 + update_post_meta( $post_id, '_status', 'carried_out' );
4899 +
4900 + // Add note/comment to appraisal
4901 + $comment = array(
4902 + 'note_type' => 'action',
4903 + 'action' => 'appraisal_revert_carried_out',
4904 + );
4905 +
4906 + PH_Comments::insert_note( $post_id, $comment );
4907 +
4908 + wp_send_json_success();
4909 + }
4910 +
4911 + wp_send_json_error();
4912 + }
4913 +
4914 + public function appraisal_revert_won()
4915 + {
4916 + check_ajax_referer( 'appraisal-actions', 'security' );
4917 +
4918 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4919 +
4920 + $status = get_post_meta( $post_id, '_status', TRUE );
4921 +
4922 + if ( $status == 'instructed' )
4923 + {
4924 + update_post_meta( $post_id, '_status', 'won' );
4925 +
4926 + // Add note/comment to appraisal
4927 + $comment = array(
4928 + 'note_type' => 'action',
4929 + 'action' => 'appraisal_revert_won',
4930 + );
4931 +
4932 + PH_Comments::insert_note( $post_id, $comment );
4933 +
4934 + wp_send_json_success();
4935 + }
4936 +
4937 + wp_send_json_error();
4938 + }
4939 +
1565 4940 // Viewing related functions
1566 4941 public function book_viewing_property()
1567 4942 {
1568 4943 check_ajax_referer( 'book-viewing', 'security' );
@@ -1568,10 +4943,11 @@
1568 4943 check_ajax_referer( 'book-viewing', 'security' );
1569 4944
1570 4945 $this->json_headers();
1571 4946
1572 - // TO DO: Should do validation on server side also
1573 - 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)
1574 4950 {
1575 4951 $return = array('error' => 'No property selected');
1576 4952 echo json_encode( $return );
1577 4953 die();
@@ -1576,18 +4952,26 @@
1576 4952 echo json_encode( $return );
1577 4953 die();
1578 4954 }
1579 4955
1580 - $property = new PH_Property((int)$_POST['property_id']);
4956 + $property = new PH_Property( $property_id );
1581 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 + }
1582 4966 $applicant_contact_ids = array();
1583 4967
1584 4968 // Create applicant record if required
1585 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
4969 + if (empty($booking['applicant_ids']) && !empty($booking['applicant_name']))
1586 4970 {
1587 4971 // Need to create contact/applicant
1588 4972 $contact_post = array(
1589 - 'post_title' => wp_strip_all_tags($_POST['applicant_name']),
4973 + 'post_title' => $booking['applicant_name'],
1590 4974 'post_content' => '',
1591 4975 'post_type' => 'contact',
1592 4976 'post_status' => 'publish',
1593 4977 'comment_status' => 'closed',
@@ -1594,9 +4978,9 @@
1594 4978 'ping_status' => 'closed',
1595 4979 );
1596 4980
1597 4981 // Insert the post into the database
1598 - $contact_post_id = wp_insert_post( $contact_post );
4982 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
1599 4983
1600 4984 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
1601 4985 {
1602 4986 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -1605,8 +4989,27 @@
1605 4989 }
1606 4990
1607 4991 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
1608 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 +
1609 5012 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
1610 5013 update_post_meta( $contact_post_id, '_applicant_profile_0', array( 'department' => $property->department, 'send_matching_properties' => '' ) );
1611 5014
1612 5015 $applicant_contact_ids[] = $contact_post_id;
@@ -1611,20 +5014,12 @@
1611 5014
1612 5015 $applicant_contact_ids[] = $contact_post_id;
1613 5016 }
1614 5017
1615 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
5018 + if (!empty($booking['applicant_ids']) && empty($booking['applicant_name']))
1616 5019 {
1617 5020 // This is an existing contact
1618 - if ( !is_array($_POST['applicant_ids']) )
1619 - {
1620 - $_POST['applicant_ids'] = array($_POST['applicant_ids']);
1621 - }
1622 -
1623 - foreach ( $_POST['applicant_ids'] as $applicant_id )
1624 - {
1625 - $applicant_contact_ids[] = $applicant_id;
1626 - }
5021 + $applicant_contact_ids = $booking['applicant_ids'];
1627 5022 }
1628 5023
1629 5024 $applicant_contact_ids = array_unique($applicant_contact_ids);
1630 5025
@@ -1689,53 +5084,37 @@
1689 5084 update_post_meta( $applicant_contact_id, '_applicant_profile_' . $num_applicant_profiles, array( 'department' => $property->department ) );
1690 5085 }
1691 5086 }*/
1692 5087
1693 - // Loop through contacts and create one viewing each
1694 - // At the moment it's a 1-to-1 relationship, but might support multiple in the future
1695 - foreach ( $applicant_contact_ids as $applicant_contact_id )
1696 - {
1697 - // Insert viewing record
1698 - $viewing_post = array(
1699 - 'post_title' => '',
1700 - 'post_content' => '',
1701 - 'post_type' => 'viewing',
1702 - 'post_status' => 'publish',
1703 - 'comment_status' => 'closed',
1704 - 'ping_status' => 'closed',
1705 - );
1706 -
1707 - // Insert the post into the database
1708 - $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 + );
1709 5097
1710 - if ( is_wp_error($viewing_post_id) || $viewing_post_id == 0 )
1711 - {
1712 - $return = array('error' => 'Failed to create viewing post. Please try again');
1713 - echo json_encode( $return );
1714 - die();
1715 - }
1716 -
1717 - add_post_meta( $viewing_post_id, '_start_date_time', $_POST['start_date'] . ' ' . $_POST['start_time'] );
1718 - add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
1719 - add_post_meta( $viewing_post_id, '_property_id', $_POST['property_id'] );
1720 - add_post_meta( $viewing_post_id, '_applicant_contact_id', $applicant_contact_id );
1721 - add_post_meta( $viewing_post_id, '_status', 'pending' );
1722 - add_post_meta( $viewing_post_id, '_feedback_status', '' );
1723 - add_post_meta( $viewing_post_id, '_feedback', '' );
1724 - 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 );
1725 5100
1726 - if ( !empty($_POST['negotiator_ids']) )
1727 - {
1728 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
1729 - {
1730 - add_post_meta( $viewing_post_id, '_negotiator_id', $negotiator_id );
1731 - }
1732 - }
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();
1733 5106 }
1734 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 +
1735 5112 $applicant_contacts = array();
1736 - foreach ( $applicant_contact_ids as $applicant_contact_id )
5113 + foreach ($applicant_contact_ids as $applicant_contact_id)
1737 5114 {
5115 + add_post_meta( $viewing_post_id, '_applicant_contact_id', $applicant_contact_id );
5116 +
1738 5117 $applicant_contacts[] = array(
1739 5118 'ID' => $applicant_contact_id,
1740 5119 'post_title' => get_the_title($applicant_contact_id),
1741 5120 'edit_link' => get_edit_post_link( $applicant_contact_id, '' ),
@@ -1741,8 +5120,21 @@
1741 5120 'edit_link' => get_edit_post_link( $applicant_contact_id, '' ),
1742 5121 );
1743 5122 }
1744 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 +
1745 5137 $return = array('success' => array(
1746 5138 'viewing' => array(
1747 5139 'ID' => $viewing_post_id,
1748 5140 'edit_link' => get_edit_post_link( $viewing_post_id, '' ),
@@ -1760,10 +5152,16 @@
1760 5152 check_ajax_referer( 'book-viewing', 'security' );
1761 5153
1762 5154 $this->json_headers();
1763 5155
1764 - // TO DO: Should do validation on server side also
1765 - 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)
1766 5164 {
1767 5165 $return = array('error' => 'No contact selected');
1768 5166 echo json_encode( $return );
1769 5167 die();
@@ -1768,9 +5166,9 @@
1768 5166 echo json_encode( $return );
1769 5167 die();
1770 5168 }
1771 5169
1772 - if (empty($_POST['property_ids']))
5170 + if (empty($booking['property_ids']))
1773 5171 {
1774 5172 $return = array('error' => 'No property selected');
1775 5173 echo json_encode( $return );
1776 5174 die();
@@ -1777,9 +5175,9 @@
1777 5175 }
1778 5176
1779 5177 // Loop through contacts and create one viewing each
1780 5178 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
1781 - foreach ( $_POST['property_ids'] as $property_id )
5179 + foreach ( $booking['property_ids'] as $property_id )
1782 5180 {
1783 5181 // Insert viewing record
1784 5182 $viewing_post = array(
1785 5183 'post_title' => '',
@@ -1799,33 +5197,33 @@
1799 5197 echo json_encode( $return );
1800 5198 die();
1801 5199 }
1802 5200
1803 - add_post_meta( $viewing_post_id, '_start_date_time', $_POST['start_date'] . ' ' . $_POST['start_time'] );
5201 + add_post_meta( $viewing_post_id, '_start_date_time', $booking['start_date'] . ' ' . $booking['start_time'] );
1804 5202 add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
1805 - add_post_meta( $viewing_post_id, '_property_id', $property_id );
1806 - add_post_meta( $viewing_post_id, '_applicant_contact_id', $_POST['contact_id'] );
5203 + add_post_meta( $viewing_post_id, '_property_id', (int)$property_id );
5204 + add_post_meta( $viewing_post_id, '_applicant_contact_id', $contact_id );
1807 5205 add_post_meta( $viewing_post_id, '_status', 'pending' );
1808 5206 add_post_meta( $viewing_post_id, '_feedback_status', '' );
1809 5207 add_post_meta( $viewing_post_id, '_feedback', '' );
1810 5208 add_post_meta( $viewing_post_id, '_feedback_passed_on', '' );
1811 5209
1812 - if ( !empty($_POST['negotiator_ids']) )
5210 + if ( !empty($booking['negotiator_ids']) )
1813 5211 {
1814 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
5212 + foreach ( $booking['negotiator_ids'] as $negotiator_id )
1815 5213 {
1816 - add_post_meta( $viewing_post_id, '_negotiator_id', $negotiator_id );
5214 + add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
1817 5215 }
1818 5216 }
1819 5217 }
1820 5218
1821 5219 $properties = array();
1822 - foreach ( $_POST['property_ids'] as $property_id )
5220 + foreach ( $booking['property_ids'] as $property_id )
1823 5221 {
1824 5222 $properties[] = array(
1825 - 'ID' => $property_id,
1826 - 'post_title' => get_the_title($property_id),
1827 - 'edit_link' => get_edit_post_link( $property_id, '' ),
5223 + 'ID' => (int)$property_id,
5224 + 'post_title' => get_the_title((int)$property_id),
5225 + 'edit_link' => get_edit_post_link( (int)$property_id, '' ),
1828 5226 );
1829 5227 }
1830 5228
1831 5229 $return = array('success' => array(
@@ -1842,382 +5240,1681 @@
1842 5240 }
1843 5241
1844 5242 public function get_viewing_details_meta_box()
1845 5243 {
5244 + global $post;
5245 +
1846 5246 check_ajax_referer( 'viewing-details-meta-box', 'security' );
1847 5247
1848 - $viewing = new PH_Viewing((int)$_POST['viewing_id']);
5248 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
1849 5249
1850 - echo '<div class="propertyhive_meta_box">';
5250 + $post = get_post( $post_id );
5251 +
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;
1851 5275
1852 - 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 + }
1853 5281
1854 - echo '<p class="form-field">
1855 -
1856 - <label for="">' . __('Status', 'propertyhive') . '</label>
1857 -
1858 - ' . ucwords(str_replace("_", " ", $viewing->status));
5282 + $post = get_post((int)$post_id);
1859 5283
1860 - 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' )
1861 5300 {
1862 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
5301 + update_post_meta( $post_id, '_status', 'carried_out' );
5302 +
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();
5312 + }
5313 +
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' )
5326 + {
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',
5333 + );
5334 +
5335 + PH_Comments::insert_note( $post_id, $comment );
5336 +
5337 + wp_send_json_success();
5338 + }
5339 +
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' )
5354 + {
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' : '' );
5358 +
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 )
1863 5395 {
1864 - $offer_id = get_post_meta( $viewing->id, '_offer_id', TRUE );
1865 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
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 )
1866 5434 {
1867 - $offer_id = '';
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 + }
1868 5454 }
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 + }
1869 5477
1870 - if ( $offer_id != '' )
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 == '' )
1871 5511 {
1872 - echo ' (<a href="' . get_edit_post_link($offer_id) . '">' . __('View Offer', 'propertyhive') . '</a>)';
5512 + $from = $property->office_email_address;
1873 5513 }
1874 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)
5535 + {
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 + }
5561 + }
5562 + }
5563 +
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';
5568 +
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)
5574 + {
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',
5591 + );
5592 +
5593 + PH_Comments::insert_note( $post_id, $comment );
5594 + }
5595 +
5596 + wp_send_json_success();
1875 5597 }
1876 -
1877 - echo '</p>';
5598 + else
5599 + {
5600 + wp_send_json_error('No valid recipient email addresses');
5601 + }
1878 5602
1879 - if ( $viewing->status == 'carried_out' )
1880 - {
1881 - echo '<p class="form-field">
1882 -
1883 - <label for="">' . __('Applicant Feedback', 'propertyhive') . '</label>';
5603 + wp_die();
5604 + }
1884 5605
1885 - switch ( $viewing->feedback_status )
5606 + public function viewing_email_owner_booking_confirmation()
5607 + {
5608 + check_ajax_referer( 'viewing-actions', 'security' );
5609 +
5610 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5611 +
5612 + $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5613 + $property_department = get_post_meta( $property_id, '_department' );
5614 +
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 ) {
5619 +
5620 + $owner_emails = array();
5621 + $owner_names = array();
5622 + $owner_dears = array();
5623 +
5624 + foreach ($owner_contact_ids as $owner_id)
1886 5625 {
1887 - case "interested":
5626 + $owner_contact = new PH_Contact($owner_id);
5627 +
5628 + $owner_name = $owner_contact->post_title;
5629 + $owner_dear = $owner_contact->dear();
5630 +
5631 + if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
5632 + if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
5633 +
5634 + $owner_email = $owner_contact->email_address;
5635 + $explode_owner_email = explode( ",", $owner_email );
5636 + foreach ( $explode_owner_email as $email_address )
1888 5637 {
1889 - echo 'Interested';
1890 - break;
5638 + $owner_emails[] = sanitize_email($email_address);
1891 5639 }
1892 - case "not_interested":
5640 + }
5641 +
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) )
5646 + {
5647 + $applicant_names = array();
5648 + $applicant_dears = array();
5649 + foreach ($applicant_contact_ids as $applicant_contact_id)
1893 5650 {
1894 - echo 'Not Interested';
1895 - break;
5651 + $applicant_contact = new PH_Contact($applicant_contact_id);
5652 + $applicant_names[] = $applicant_contact->post_title;
5653 + $applicant_dears[] = $applicant_contact->dear();
1896 5654 }
1897 - case "not_required":
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);
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 )
1898 5675 {
1899 - echo 'Feedback Not Required';
1900 - break;
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 + }
1901 5695 }
1902 - default:
5696 + }
5697 + if ( !empty($negotiator_names) )
5698 + {
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 + }
5718 +
5719 + $property = new PH_Property((int)$property_id);
5720 +
5721 + $to = implode(",", $owner_emails);
5722 +
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', '' );
5725 +
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);
5734 +
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 );
5737 +
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);
5748 +
5749 + $body = html_entity_decode($body);
5750 +
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' )
5757 + {
5758 + $current_user = wp_get_current_user();
5759 + $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
5760 +
5761 + if ( $from == '' )
1903 5762 {
1904 - echo 'Awaiting Feedback';
5763 + $from = $property->office_email_address;
1905 5764 }
1906 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 + }
1907 5778
1908 - echo '</p>';
5779 + $attachments = array();
5780 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5781 + {
5782 + $uploaded_files = $this->get_viewing_email_uploads();
1909 5783
1910 - if ( $viewing->feedback_status == 'interested' || $viewing->feedback_status == 'not_interested' )
5784 + // Handle each file upload
5785 + foreach ($uploaded_files['name'] as $key => $value)
5786 + {
5787 + if ($uploaded_files['name'][$key])
5788 + {
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 + );
5796 +
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']))
5802 + {
5803 + // Add the file path to attachments array
5804 + $attachments[] = $movefile['file'];
5805 + }
5806 + else
5807 + {
5808 + // Handle error in file upload
5809 + wp_send_json_error($movefile['error']);
5810 + }
5811 + }
5812 + }
5813 + }
5814 +
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)
1911 5825 {
1912 - $args = array(
1913 - 'id' => '_feedback',
1914 - 'label' => __( 'Feedback', 'propertyhive' ),
1915 - 'desc_tip' => false,
1916 - 'class' => '',
1917 - 'value' => $viewing->feedback,
1918 - 'custom_attributes' => array(
1919 - 'style' => 'width:95%; max-width:500px;'
1920 - )
5826 + @wp_delete_file($temp_file);
5827 + }
5828 +
5829 + if ( !$sent )
5830 + {
5831 + wp_send_json_error('Failed to send email');
5832 + }
5833 +
5834 + if ( apply_filters( 'propertyhive_log_booking_confirmation_emails', false ) === true )
5835 + {
5836 + // Add note/comment to viewing
5837 + $comment = array(
5838 + 'note_type' => 'action',
5839 + 'action' => 'viewing_owner_booking_confirmation_email',
1921 5840 );
1922 - propertyhive_wp_textarea_input( $args );
5841 +
5842 + PH_Comments::insert_note( $post_id, $comment );
1923 5843 }
5844 +
5845 + update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
5846 +
5847 + wp_send_json_success();
1924 5848 }
1925 -
1926 - if ( $viewing->status == 'carried_out' && ( $viewing->feedback_status == 'interested' || $viewing->feedback_status == 'not_interested' ) )
5849 + else
1927 5850 {
1928 - echo '<p class="form-field">
1929 -
1930 - <label for="">' . __('Feedback Passed On', 'propertyhive') . '</label>';
1931 -
1932 - echo ( ($viewing->feedback_passed_on == 'yes') ? 'Yes' : 'No' );
1933 -
1934 - echo '</p>';
5851 + wp_send_json_error('No owner recipients');
1935 5852 }
1936 5853
1937 - do_action('propertyhive_viewing_details_fields');
1938 -
1939 - echo '</div>';
1940 -
1941 - echo '</div>';
1942 -
1943 - die();
5854 + wp_die();
1944 5855 }
1945 5856
1946 - public function get_viewing_actions()
5857 + public function viewing_email_attending_negotiator_booking_confirmation()
1947 5858 {
1948 5859 check_ajax_referer( 'viewing-actions', 'security' );
1949 5860
1950 - $post_id = $_POST['viewing_id'];
5861 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5862 + $property_id = get_post_meta( $post_id, '_property_id', TRUE );
1951 5863
1952 - $status = get_post_meta( $post_id, '_status', TRUE );
1953 - $feedback_status = get_post_meta( $post_id, '_feedback_status', TRUE );
5864 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
1954 5865
1955 - echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_viewing_actions_meta_box">
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) ) {
1956 5870
1957 - <div class="options_group" style="padding-top:8px;">';
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);
1958 5878
1959 - $show_feedback_meta_boxes = false;
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);
1960 5889
1961 - $actions = array();
5890 + $owner_name = $owner_contact->post_title;
5891 + $owner_dear = $owner_contact->dear();
1962 5892
1963 - if ( $status == 'pending' )
1964 - {
1965 - $actions[] = '<a
1966 - href="#action_panel_viewing_carried_out"
1967 - class="button button-success viewing-action"
1968 - style="width:100%; margin-bottom:7px; text-align:center"
1969 - >' . __('Viewing Carried Out', 'propertyhive') . '</a>';
1970 - $actions[] = '<a
1971 - href="#action_panel_viewing_cancelled"
1972 - class="button viewing-action"
1973 - style="width:100%; margin-bottom:7px; text-align:center"
1974 - >' . __('Viewing Cancelled', 'propertyhive') . '</a>';
1975 - }
5893 + if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
5894 + if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
1976 5895
1977 - if ( $status == 'carried_out' )
1978 - {
1979 - if ( $feedback_status == '' )
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 + }
5902 +
5903 + $owner_details[] = $owner_contact->post_title . "\nT: " . $owner_contact->telephone_number . "\nE: " . $owner_contact->email_address;
5904 + }
5905 + }
5906 +
5907 + $owner_details = implode("\n\n", $owner_details);
5908 +
5909 + $owner_names_string = $this->get_list_string($owner_names);
5910 + $owner_dears_string = $this->get_list_string($owner_dears);
5911 +
5912 + $applicant_names = array();
5913 + $applicant_dears = array();
5914 + $applicant_details = array();
5915 +
5916 + if ( !empty($applicant_contact_ids) )
1980 5917 {
1981 - $actions[] = '<a
1982 - href="#action_panel_viewing_interested"
1983 - class="button button-success viewing-action"
1984 - style="width:100%; margin-bottom:7px; text-align:center"
1985 - >' . __('Applicant Interested', 'propertyhive') . '</a>';
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();
1986 5923
1987 - $actions[] = '<a
1988 - href="#action_panel_viewing_not_interested"
1989 - class="button button-danger viewing-action"
1990 - style="width:100%; margin-bottom:7px; text-align:center"
1991 - >' . __('Applicant Not Interested', 'propertyhive') . '</a>';
5924 + $applicant_details[] = $applicant_contact->post_title . "\nT: " . $applicant_contact->telephone_number . "\nE: " . $applicant_contact->email_address;
5925 + }
5926 + }
1992 5927
1993 - $actions[] = '<a
1994 - href="#action_panel_viewing_feedback_not_required"
1995 - class="button viewing-action"
1996 - style="width:100%; margin-bottom:7px; text-align:center"
1997 - >' . __('Feedback Not Required', 'propertyhive') . '</a>';
5928 + $applicant_details = implode("\n\n", $applicant_details);
1998 5929
1999 - $show_feedback_meta_boxes = true;
2000 - }
5930 + $applicant_names = array_filter($applicant_names);
5931 + $applicant_dears = array_filter($applicant_dears);
2001 5932
2002 - if ( $feedback_status == 'interested' )
5933 + $applicant_names_string = $this->get_list_string($applicant_names);
5934 + $applicant_dears_string = $this->get_list_string($applicant_dears);
5935 +
5936 + $negotiator_names = array();
5937 + $negotiator_names_string = '';
5938 +
5939 + $negotiator_email_addresses = array();
5940 + $negotiator_email_addresses_string = '';
5941 +
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) )
2003 5947 {
2004 - $actions[] = '<a
2005 - 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 .'"
2006 - class="button button-success"
2007 - style="width:100%; margin-bottom:7px; text-align:center"
2008 - >' . __('Book Second Viewing', 'propertyhive') . '</a>';
2009 -
2010 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
5948 + foreach ( $negotiator_ids as $negotiator_id )
2011 5949 {
2012 - $property_id = get_post_meta( $post_id, '_property_id', TRUE );
2013 - if ( get_post_meta( $property_id, '_department', TRUE ) == 'residential-sales' )
5950 + $negotiator = get_user_by( 'id', $negotiator_id );
5951 + if ( $negotiator !== false )
2014 5952 {
2015 - // See if an offer has this viewing id associated with it
2016 - $offer_id = get_post_meta( $post_id, '_offer_id', TRUE );
2017 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
5953 + if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
2018 5954 {
2019 - $offer_id = '';
5955 + $negotiator_names[] = $negotiator->display_name;
2020 5956 }
5957 +
5958 + if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
5959 + {
5960 + $negotiator_email_addresses[] = $negotiator->user_email;
5961 + }
2021 5962
2022 - if ( $offer_id != '' )
5963 + $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
5964 + if ( !empty($telephone_number) )
2023 5965 {
2024 - $actions[] = '<a
2025 - href="' . get_edit_post_link( $offer_id, '' ) . '"
2026 - class="button"
2027 - style="width:100%; margin-bottom:7px; text-align:center"
2028 - >' . __('View Offer', 'propertyhive') . '</a>';
5966 + $negotiator_telephone_numbers[] = $telephone_number;
2029 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 + }
5992 +
5993 + $property = new PH_Property((int)$property_id);
5994 +
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', '' );
5997 +
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);
6006 +
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 );
6009 +
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);
6022 +
6023 + $body = html_entity_decode($body);
6024 +
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 );
6027 +
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 : '' );
6034 +
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 + }
6052 +
6053 + $attachments = array();
6054 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6055 + {
6056 + $uploaded_files = $this->get_viewing_email_uploads();
6057 +
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 + );
6070 +
6071 + // Move the file to a temporary location
6072 + $upload_overrides = array('test_form' => false);
6073 + $movefile = wp_handle_upload($file, $upload_overrides);
6074 +
6075 + if ($movefile && !isset($movefile['error']))
6076 + {
6077 + // Add the file path to attachments array
6078 + $attachments[] = $movefile['file'];
6079 + }
2030 6080 else
2031 6081 {
2032 - $actions[] = '<a
2033 - href="' . wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), '1', 'create_offer' ) . '"
2034 - class="button button-success"
2035 - style="width:100%; margin-bottom:7px; text-align:center"
2036 - >' . __('Record Offer', 'propertyhive') . '</a>';
6082 + // Handle error in file upload
6083 + wp_send_json_error($movefile['error']);
2037 6084 }
2038 6085 }
2039 6086 }
2040 6087 }
2041 6088
2042 - if ( get_post_meta( $post_id, '_feedback_passed_on', TRUE ) != 'yes' && ( $feedback_status == 'interested' || $feedback_status == 'not_interested' ) )
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';
6093 +
6094 + $headers = apply_filters( 'propertyhive_viewing_attending_negotiator_booking_confirmation_email_headers', $headers );
6095 +
6096 + $sent = wp_mail($to, $subject, $body, $headers, $attachments);
6097 +
6098 + foreach ($attachments as $temp_file)
2043 6099 {
2044 - $actions[] = '<a
2045 - href="#action_panel_viewing_revert_feedback_passed_on"
2046 - class="button viewing-action"
2047 - style="width:100%; margin-bottom:7px; text-align:center"
2048 - >' . __('Feedback Passed On To Owner', 'propertyhive') . '</a>';
6100 + @wp_delete_file($temp_file);
2049 6101 }
2050 6102
2051 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' || $feedback_status == 'not_required' )
6103 + if ( !$sent )
2052 6104 {
2053 - $actions[] = '<a
2054 - href="#action_panel_viewing_revert_feedback_pending"
2055 - class="button viewing-action"
2056 - style="width:100%; margin-bottom:7px; text-align:center"
2057 - >' . __('Revert To Feedback Pending', 'propertyhive') . '</a>';
6105 + wp_send_json_error('Failed to send email');
2058 6106 }
6107 +
6108 + // Add note/comment to viewing
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 + );
6115 +
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();
2059 6122 }
6123 + else
6124 + {
6125 + wp_send_json_error('No attending negotiator recipients');
6126 + }
2060 6127
2061 - if ( $status == 'offer_made' )
6128 + wp_die();
6129 + }
6130 +
6131 + public function viewing_email_applicant_cancellation_notification()
6132 + {
6133 + check_ajax_referer( 'viewing-actions', 'security' );
6134 +
6135 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6136 +
6137 + $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
6138 + $property_id = get_post_meta( $post_id, '_property_id', TRUE );
6139 +
6140 + if ( !is_array($applicant_contact_ids) || (int)$property_id == '' || count($applicant_contact_ids) == 0 || (int)$property_id == 0 )
2062 6141 {
2063 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
6142 + wp_send_json_error('Missing contact or property');
6143 + }
6144 +
6145 + $property = new PH_Property((int)$property_id);
6146 +
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 )
2064 6153 {
2065 - $offer_id = get_post_meta( $post_id, '_offer_id', TRUE );
2066 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
6154 + $to[] = sanitize_email($email_address);
6155 + }
6156 + }
6157 +
6158 + $to = array_filter($to);
6159 +
6160 + if ( !empty(implode($to)) )
6161 + {
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', '' );
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 )
2067 6192 {
2068 - $offer_id = '';
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 + }
2069 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 + }
2070 6235
2071 - if ( $offer_id != '' )
6236 + $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $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);
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 +
6247 + $body = str_replace('[property_address]', $property->get_formatted_full_address(), $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);
6255 +
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 + )
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 == '' )
2072 6279 {
2073 - $actions[] = '<a
2074 - href="' . get_edit_post_link( $offer_id, '' ) . '"
2075 - class="button"
2076 - style="width:100%; margin-bottom:7px; text-align:center"
2077 - >' . __('View Offer', 'propertyhive') . '</a>';
6280 + $from = $property->office_email_address;
2078 6281 }
2079 6282 }
2080 - }
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 + {
6293 + $from = get_bloginfo('admin_email');
6294 + }
2081 6295
2082 - if ( ( $status == 'carried_out' && $feedback_status == '' ) || $status == 'cancelled' )
2083 - {
2084 - $actions[] = '<a
2085 - href="#action_panel_viewing_revert_pending"
2086 - class="button viewing-action"
2087 - style="width:100%; margin-bottom:7px; text-align:center"
2088 - >' . __('Revert To Pending', 'propertyhive') . '</a>';
2089 - }
6296 + $attachments = array();
6297 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6298 + {
6299 + $uploaded_files = $this->get_viewing_email_uploads();
2090 6300
2091 - $actions = apply_filters( 'propertyhive_admin_viewing_actions', $actions, $post->ID );
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 + );
2092 6313
2093 - if ( !empty($actions) )
2094 - {
2095 - echo implode("", $actions);
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 +
6332 + $headers = array();
6333 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
6334 + $headers[] = 'Reply-To: ' . sanitize_email($from);
6335 + $headers[] = 'Content-Type: text/plain; charset=UTF-8';
6336 +
6337 + $headers = apply_filters( 'propertyhive_viewing_applicant_cancellation_notification_email_headers', $headers );
6338 +
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();
2096 6365 }
2097 6366 else
2098 6367 {
2099 - echo '<div style="text-align:center">' . __( 'No actions to display', 'propertyhive' ) . '</div>';
6368 + wp_send_json_error('No valid recipient email addresses');
2100 6369 }
2101 6370
2102 - echo '</div>
6371 + wp_die();
6372 + }
2103 6373
2104 - </div>';
6374 + public function viewing_email_owner_cancellation_notification()
6375 + {
6376 + check_ajax_referer( 'viewing-actions', 'security' );
2105 6377
2106 - if ( $show_feedback_meta_boxes )
2107 - {
2108 - echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_viewing_interested" style="display:none;">
6378 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2109 6379
2110 - <div class="options_group" style="padding-top:8px;">
6380 + $property_id = get_post_meta( $post_id, '_property_id', TRUE );
6381 + $property_department = get_post_meta( $property_id, '_department' );
2111 6382
2112 - <div class="form-field">
6383 + $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
6384 + $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
6385 +
6386 + if ( $owner_contact_ids > 0 ) {
2113 6387
2114 - <label for="_viewing_interested_feedback">' . __( 'Applicant Feedback', 'propertyhive' ) . '</label>
6388 + $owner_emails = array();
6389 + $owner_names = array();
6390 + $owner_dears = array();
6391 +
6392 + foreach ($owner_contact_ids as $owner_id)
6393 + {
6394 + $owner_contact = new PH_Contact($owner_id);
6395 +
6396 + $owner_name = $owner_contact->post_title;
6397 + $owner_dear = $owner_contact->dear();
6398 +
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 + }
6408 + }
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 + }
2115 6451
2116 - <textarea id="_interested_feedback" name="_interested_feedback" style="width:100%;">' . get_post_meta( $post_id, '_feedback', TRUE ) . '</textarea>
6452 + if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
6453 + {
6454 + $negotiator_email_addresses[] = $negotiator->user_email;
6455 + }
2117 6456
2118 - </div>
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 + }
2119 6486
2120 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2121 - <a class="button button-primary interested-feedback-action-submit" href="#">' . __( 'Save Feedback', 'propertyhive' ) . '</a>
6487 + $property = new PH_Property((int)$property_id);
2122 6488
2123 - </div>
6489 + $to = implode(",", $owner_emails);
2124 6490
2125 - </div>';
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', '' );
2126 6493
2127 - echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_viewing_not_interested" style="display:none;">
6494 + $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $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);
2128 6502
2129 - <div class="options_group" style="padding-top:8px;">
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 );
2130 6505
2131 - <div class="form-field">
6506 + $body = str_replace('[property_address]', $property->get_formatted_full_address(), $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);
2132 6516
2133 - <label for="_viewing_not_interested_feedback">' . __( 'Applicant Feedback', 'propertyhive' ) . '</label>
2134 -
2135 - <textarea id="_not_interested_feedback" name="_not_interested_feedback" style="width:100%;">' . get_post_meta( $post_id, '_feedback', TRUE ) . '</textarea>
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 + )
6522 + {
6523 + $cancelled_reason .= "\n\nReason: " . get_post_meta( $post_id, '_cancelled_reason', true );
6524 + }
6525 + $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
2136 6526
2137 - </div>
6527 + $body = html_entity_decode($body);
2138 6528
2139 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2140 - <a class="button button-primary not-interested-feedback-action-submit" href="#">' . __( 'Save Feedback', 'propertyhive' ) . '</a>
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 );
2141 6531
2142 - </div>
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 : '' );
2143 6538
2144 - </div>';
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 + {
6554 + $from = get_bloginfo('admin_email');
6555 + }
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 +
6593 + $headers = array();
6594 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
6595 + $headers[] = 'Reply-To: ' . sanitize_email($from);
6596 + $headers[] = 'Content-Type: text/plain; charset=UTF-8';
6597 +
6598 + $headers = apply_filters( 'propertyhive_viewing_owner_cancellation_notification_email_headers', $headers );
6599 +
6600 + $sent = wp_mail($to, $subject, $body, $headers, $attachments);
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();
2145 6626 }
6627 + else
6628 + {
6629 + wp_send_json_error('No owner recipients');
6630 + }
2146 6631
2147 - die();
6632 + wp_die();
2148 6633 }
2149 6634
2150 - public function viewing_carried_out()
6635 + public function viewing_email_attending_negotiator_cancellation_notification()
2151 6636 {
2152 6637 check_ajax_referer( 'viewing-actions', 'security' );
2153 6638
2154 - $post_id = $_POST['viewing_id'];
6639 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6640 + $property_id = get_post_meta( $post_id, '_property_id', TRUE );
2155 6641
2156 - $status = get_post_meta( $post_id, '_status', TRUE );
6642 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
2157 6643
2158 - if ( $status == 'pending' )
2159 - {
2160 - update_post_meta( $post_id, '_status', 'carried_out' );
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) ) {
2161 6648
2162 - $current_user = wp_get_current_user();
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);
2163 6656
2164 - // Add note/comment to viewing
2165 - $comment = array(
2166 - 'note_type' => 'action',
2167 - 'action' => 'viewing_carried_out',
2168 - );
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);
2169 6667
2170 - $data = array(
2171 - 'comment_post_ID' => $post_id,
2172 - 'comment_author' => $current_user->display_name,
2173 - 'comment_author_email' => '[email protected]',
2174 - 'comment_author_url' => '',
2175 - 'comment_date' => date("Y-m-d H:i:s"),
2176 - 'comment_content' => serialize($comment),
2177 - 'comment_approved' => 1,
2178 - 'comment_type' => 'propertyhive_note',
2179 - );
2180 - $comment_id = wp_insert_comment( $data );
2181 - }
6668 + $owner_name = $owner_contact->post_title;
6669 + $owner_dear = $owner_contact->dear();
2182 6670
2183 - die();
2184 - }
6671 + if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
6672 + if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
2185 6673
2186 - public function viewing_cancelled()
2187 - {
2188 - check_ajax_referer( 'viewing-actions', 'security' );
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 + }
2189 6680
2190 - $post_id = $_POST['viewing_id'];
6681 + $owner_details[] = $owner_contact->post_title . "\nT: " . $owner_contact->telephone_number . "\nE: " . $owner_contact->email_address;
6682 + }
6683 + }
2191 6684
2192 - $status = get_post_meta( $post_id, '_status', TRUE );
6685 + $owner_details = implode("\n\n", $owner_details);
2193 6686
2194 - if ( $status == 'pending' )
2195 - {
2196 - update_post_meta( $post_id, '_status', 'cancelled' );
6687 + $owner_names_string = $this->get_list_string($owner_names);
6688 + $owner_dears_string = $this->get_list_string($owner_dears);
2197 6689
2198 - $current_user = wp_get_current_user();
6690 + $applicant_names = array();
6691 + $applicant_dears = array();
6692 + $applicant_details = array();
2199 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 +
2200 6896 // Add note/comment to viewing
2201 - $comment = array(
2202 - 'note_type' => 'action',
2203 - 'action' => 'viewing_cancelled',
2204 - );
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 + );
2205 6903
2206 - $data = array(
2207 - 'comment_post_ID' => $post_id,
2208 - 'comment_author' => $current_user->display_name,
2209 - 'comment_author_email' => '[email protected]',
2210 - 'comment_author_url' => '',
2211 - 'comment_date' => date("Y-m-d H:i:s"),
2212 - 'comment_content' => serialize($comment),
2213 - 'comment_approved' => 1,
2214 - 'comment_type' => 'propertyhive_note',
2215 - );
2216 - $comment_id = wp_insert_comment( $data );
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();
2217 6910 }
6911 + else
6912 + {
6913 + wp_send_json_error('No attending negotiator recipients');
6914 + }
2218 6915
2219 - die();
6916 + wp_die();
2220 6917 }
2221 6918
2222 6919 public function viewing_interested_feedback()
2223 6920 {
@@ -2222,19 +6919,19 @@
2222 6919 public function viewing_interested_feedback()
2223 6920 {
2224 6921 check_ajax_referer( 'viewing-actions', 'security' );
2225 6922
2226 - $post_id = $_POST['viewing_id'];
6923 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2227 6924
6925 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6926 +
2228 6927 $status = get_post_meta( $post_id, '_status', TRUE );
2229 6928
2230 6929 if ( $status == 'carried_out' )
2231 6930 {
2232 6931 update_post_meta( $post_id, '_feedback_status', 'interested' );
2233 - update_post_meta( $post_id, '_feedback', $_POST['feedback'] );
6932 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
2234 6933
2235 - $current_user = wp_get_current_user();
2236 -
2237 6934 // Add note/comment to viewing
2238 6935 $comment = array(
2239 6936 'note_type' => 'action',
2240 6937 'action' => 'viewing_applicant_interested',
@@ -2239,22 +6936,14 @@
2239 6936 'note_type' => 'action',
2240 6937 'action' => 'viewing_applicant_interested',
2241 6938 );
2242 6939
2243 - $data = array(
2244 - 'comment_post_ID' => $post_id,
2245 - 'comment_author' => $current_user->display_name,
2246 - 'comment_author_email' => '[email protected]',
2247 - 'comment_author_url' => '',
2248 - 'comment_date' => date("Y-m-d H:i:s"),
2249 - 'comment_content' => serialize($comment),
2250 - 'comment_approved' => 1,
2251 - 'comment_type' => 'propertyhive_note',
2252 - );
2253 - $comment_id = wp_insert_comment( $data );
6940 + PH_Comments::insert_note( $post_id, $comment );
6941 +
6942 + wp_send_json_success();
2254 6943 }
2255 6944
2256 - die();
6945 + wp_send_json_error();
2257 6946 }
2258 6947
2259 6948 public function viewing_not_interested_feedback()
2260 6949 {
@@ -2259,19 +6948,19 @@
2259 6948 public function viewing_not_interested_feedback()
2260 6949 {
2261 6950 check_ajax_referer( 'viewing-actions', 'security' );
2262 6951
2263 - $post_id = $_POST['viewing_id'];
6952 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2264 6953
6954 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6955 +
2265 6956 $status = get_post_meta( $post_id, '_status', TRUE );
2266 6957
2267 6958 if ( $status == 'carried_out' )
2268 6959 {
2269 6960 update_post_meta( $post_id, '_feedback_status', 'not_interested' );
2270 - update_post_meta( $post_id, '_feedback', $_POST['feedback'] );
6961 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
2271 6962
2272 - $current_user = wp_get_current_user();
2273 -
2274 6963 // Add note/comment to viewing
2275 6964 $comment = array(
2276 6965 'note_type' => 'action',
2277 6966 'action' => 'viewing_applicant_not_interested',
@@ -2276,22 +6965,14 @@
2276 6965 'note_type' => 'action',
2277 6966 'action' => 'viewing_applicant_not_interested',
2278 6967 );
2279 6968
2280 - $data = array(
2281 - 'comment_post_ID' => $post_id,
2282 - 'comment_author' => $current_user->display_name,
2283 - 'comment_author_email' => '[email protected]',
2284 - 'comment_author_url' => '',
2285 - 'comment_date' => date("Y-m-d H:i:s"),
2286 - 'comment_content' => serialize($comment),
2287 - 'comment_approved' => 1,
2288 - 'comment_type' => 'propertyhive_note',
2289 - );
2290 - $comment_id = wp_insert_comment( $data );
6969 + PH_Comments::insert_note( $post_id, $comment );
6970 +
6971 + wp_send_json_success();
2291 6972 }
2292 6973
2293 - die();
6974 + wp_send_json_error();
2294 6975 }
2295 6976
2296 6977 public function viewing_feedback_not_required()
2297 6978 {
@@ -2296,9 +6977,9 @@
2296 6977 public function viewing_feedback_not_required()
2297 6978 {
2298 6979 check_ajax_referer( 'viewing-actions', 'security' );
2299 6980
2300 - $post_id = $_POST['viewing_id'];
6981 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2301 6982
2302 6983 $status = get_post_meta( $post_id, '_status', TRUE );
2303 6984
2304 6985 if ( $status == 'carried_out' )
@@ -2304,10 +6985,8 @@
2304 6985 if ( $status == 'carried_out' )
2305 6986 {
2306 6987 update_post_meta( $post_id, '_feedback_status', 'not_required' );
2307 6988
2308 - $current_user = wp_get_current_user();
2309 -
2310 6989 // Add note/comment to viewing
2311 6990 $comment = array(
2312 6991 'note_type' => 'action',
2313 6992 'action' => 'viewing_feedback_not_required',
@@ -2312,22 +6991,14 @@
2312 6991 'note_type' => 'action',
2313 6992 'action' => 'viewing_feedback_not_required',
2314 6993 );
2315 6994
2316 - $data = array(
2317 - 'comment_post_ID' => $post_id,
2318 - 'comment_author' => $current_user->display_name,
2319 - 'comment_author_email' => '[email protected]',
2320 - 'comment_author_url' => '',
2321 - 'comment_date' => date("Y-m-d H:i:s"),
2322 - 'comment_content' => serialize($comment),
2323 - 'comment_approved' => 1,
2324 - 'comment_type' => 'propertyhive_note',
2325 - );
2326 - $comment_id = wp_insert_comment( $data );
6995 + PH_Comments::insert_note( $post_id, $comment );
6996 +
6997 + wp_send_json_success();
2327 6998 }
2328 6999
2329 - die();
7000 + wp_send_json_error();
2330 7001 }
2331 7002
2332 7003 public function viewing_revert_feedback_pending()
2333 7004 {
@@ -2332,9 +7003,9 @@
2332 7003 public function viewing_revert_feedback_pending()
2333 7004 {
2334 7005 check_ajax_referer( 'viewing-actions', 'security' );
2335 7006
2336 - $post_id = $_POST['viewing_id'];
7007 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2337 7008
2338 7009 $status = get_post_meta( $post_id, '_status', TRUE );
2339 7010
2340 7011 if ( $status == 'carried_out' )
@@ -2340,11 +7011,10 @@
2340 7011 if ( $status == 'carried_out' )
2341 7012 {
2342 7013 update_post_meta( $post_id, '_feedback_status', '' );
2343 7014 update_post_meta( $post_id, '_feedback_passed_on', '' );
7015 + delete_post_meta( $post_id, '_feedback_received_date' );
2344 7016
2345 - $current_user = wp_get_current_user();
2346 -
2347 7017 // Add note/comment to viewing
2348 7018 $comment = array(
2349 7019 'note_type' => 'action',
2350 7020 'action' => 'viewing_revert_feedback_pending',
@@ -2349,22 +7019,14 @@
2349 7019 'note_type' => 'action',
2350 7020 'action' => 'viewing_revert_feedback_pending',
2351 7021 );
2352 7022
2353 - $data = array(
2354 - 'comment_post_ID' => $post_id,
2355 - 'comment_author' => $current_user->display_name,
2356 - 'comment_author_email' => '[email protected]',
2357 - 'comment_author_url' => '',
2358 - 'comment_date' => date("Y-m-d H:i:s"),
2359 - 'comment_content' => serialize($comment),
2360 - 'comment_approved' => 1,
2361 - 'comment_type' => 'propertyhive_note',
2362 - );
2363 - $comment_id = wp_insert_comment( $data );
7023 + PH_Comments::insert_note( $post_id, $comment );
7024 +
7025 + wp_send_json_success();
2364 7026 }
2365 7027
2366 - die();
7028 + wp_send_json_error();
2367 7029 }
2368 7030
2369 7031 public function viewing_revert_pending()
2370 7032 {
@@ -2369,19 +7031,18 @@
2369 7031 public function viewing_revert_pending()
2370 7032 {
2371 7033 check_ajax_referer( 'viewing-actions', 'security' );
2372 7034
2373 - $post_id = $_POST['viewing_id'];
7035 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2374 7036
2375 7037 $status = get_post_meta( $post_id, '_status', TRUE );
2376 7038
2377 - if ( $status == 'carried_out' || $status == 'cancelled' )
7039 + if ( in_array( $status, array('carried_out', 'cancelled', 'no_show') ) )
2378 7040 {
2379 7041 update_post_meta( $post_id, '_status', 'pending' );
2380 7042 update_post_meta( $post_id, '_feedback_status', '' );
7043 + delete_post_meta( $post_id, '_feedback_received_date' );
2381 7044
2382 - $current_user = wp_get_current_user();
2383 -
2384 7045 // Add note/comment to viewing
2385 7046 $comment = array(
2386 7047 'note_type' => 'action',
2387 7048 'action' => 'viewing_revert_pending',
@@ -2386,22 +7047,14 @@
2386 7047 'note_type' => 'action',
2387 7048 'action' => 'viewing_revert_pending',
2388 7049 );
2389 7050
2390 - $data = array(
2391 - 'comment_post_ID' => $post_id,
2392 - 'comment_author' => $current_user->display_name,
2393 - 'comment_author_email' => '[email protected]',
2394 - 'comment_author_url' => '',
2395 - 'comment_date' => date("Y-m-d H:i:s"),
2396 - 'comment_content' => serialize($comment),
2397 - 'comment_approved' => 1,
2398 - 'comment_type' => 'propertyhive_note',
2399 - );
2400 - $comment_id = wp_insert_comment( $data );
7051 + PH_Comments::insert_note( $post_id, $comment );
7052 +
7053 + wp_send_json_success();
2401 7054 }
2402 7055
2403 - die();
7056 + wp_send_json_error();
2404 7057 }
2405 7058
2406 7059 public function viewing_feedback_passed_on()
2407 7060 {
@@ -2406,9 +7059,9 @@
2406 7059 public function viewing_feedback_passed_on()
2407 7060 {
2408 7061 check_ajax_referer( 'viewing-actions', 'security' );
2409 7062
2410 - $post_id = $_POST['viewing_id'];
7063 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2411 7064
2412 7065 $status = get_post_meta( $post_id, '_status', TRUE );
2413 7066
2414 7067 if ( $status == 'carried_out' )
@@ -2414,10 +7067,8 @@
2414 7067 if ( $status == 'carried_out' )
2415 7068 {
2416 7069 update_post_meta( $post_id, '_feedback_passed_on', 'yes' );
2417 7070
2418 - $current_user = wp_get_current_user();
2419 -
2420 7071 // Add note/comment to viewing
2421 7072 $comment = array(
2422 7073 'note_type' => 'action',
2423 7074 'action' => 'viewing_feedback_passed_on',
@@ -2422,265 +7073,53 @@
2422 7073 'note_type' => 'action',
2423 7074 'action' => 'viewing_feedback_passed_on',
2424 7075 );
2425 7076
2426 - $data = array(
2427 - 'comment_post_ID' => $post_id,
2428 - 'comment_author' => $current_user->display_name,
2429 - 'comment_author_email' => '[email protected]',
2430 - 'comment_author_url' => '',
2431 - 'comment_date' => date("Y-m-d H:i:s"),
2432 - 'comment_content' => serialize($comment),
2433 - 'comment_approved' => 1,
2434 - 'comment_type' => 'propertyhive_note',
2435 - );
2436 - $comment_id = wp_insert_comment( $data );
7077 + PH_Comments::insert_note( $post_id, $comment );
7078 +
7079 + wp_send_json_success();
2437 7080 }
2438 7081
2439 - die();
7082 + wp_send_json_error();
2440 7083 }
2441 7084
2442 7085 public function get_property_viewings_meta_box()
2443 7086 {
2444 - check_ajax_referer( 'get_property_viewings_meta_box', 'security' );
7087 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
2445 7088
2446 - 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 + }
2447 7096
2448 - echo '<div class="propertyhive_meta_box">';
2449 -
2450 - echo '<div class="options_group">';
7097 + include( PH()->plugin_path() . '/includes/admin/views/html-property-viewings-meta-box.php' );
2451 7098
2452 - $args = array(
2453 - 'post_type' => 'viewing',
2454 - 'nopaging' => true,
2455 - 'orderby' => 'meta_value',
2456 - 'order' => 'DESC',
2457 - 'meta_key' => '_start_date_time',
2458 - 'post_status' => 'publish',
2459 - 'meta_query' => array(
2460 - array(
2461 - 'key' => '_property_id',
2462 - 'value' => $_POST['post_id']
2463 - )
2464 - )
2465 - );
2466 - $viewings_query = new WP_Query( $args );
2467 -
2468 - if ( $viewings_query->have_posts() )
2469 - {
2470 - echo '<table style="width:100%">
2471 - <thead>
2472 - <tr>
2473 - <th style="text-align:left;">' . __( 'Date', 'propertyhive' ) . ' / ' . __( 'Time', 'propertyhive' ) . '</th>
2474 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
2475 - <th style="text-align:left;">' . __( 'Attending Negotiator(s)', 'propertyhive' ) . '</th>
2476 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
2477 - </tr>
2478 - </thead>
2479 - <tbody>';
2480 -
2481 - while ( $viewings_query->have_posts() )
2482 - {
2483 - $viewings_query->the_post();
2484 -
2485 - echo '<tr>';
2486 - 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>';
2487 - 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>';
2488 - echo '<td style="text-align:left;">';
2489 -
2490 - $negotiator_ids = get_post_meta(get_the_ID(), '_negotiator_id');
2491 -
2492 - if (!empty($negotiator_ids))
2493 - {
2494 - $i = 0;
2495 - foreach ($negotiator_ids as $negotiator_id)
2496 - {
2497 - if ( $i > 0 ) { echo ', '; }
2498 -
2499 - $userdata = get_userdata( $negotiator_id );
2500 - if ( $userdata !== FALSE )
2501 - {
2502 - echo $userdata->display_name;
2503 - }
2504 - else
2505 - {
2506 - echo '<em>Unknown user</em>';
2507 - }
2508 - ++$i;
2509 - }
2510 - }
2511 - else
2512 - {
2513 - echo 'Unattended';
2514 - }
2515 -
2516 - echo '</td>';
2517 - echo '<td style="text-align:left;">';
2518 -
2519 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
2520 - echo ucwords(str_replace("_", " ", $status));
2521 - if ( $status == 'carried_out' )
2522 - {
2523 - echo '<br>';
2524 - $feedback_status = get_post_meta(get_the_ID(), '_feedback_status', TRUE);
2525 - switch ( $feedback_status )
2526 - {
2527 - case "interested": { echo 'Applicant Interested'; break; }
2528 - case "not_interested": { echo 'Applicant Not Interested'; break; }
2529 - case "not_required": { echo 'Feedback Not Required'; break; }
2530 - default: { echo 'Awaiting Feedback'; }
2531 - }
2532 -
2533 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' )
2534 - {
2535 - $feedback_passed_on = get_post_meta(get_the_ID(), '_feedback_passed_on', TRUE);
2536 - echo '<br>' . ( ($feedback_passed_on == 'yes') ? 'Feedback Passed On' : 'Feedback Not Passed On' );
2537 - }
2538 - }
2539 - echo '</td>';
2540 - echo '</tr>';
2541 - }
2542 -
2543 - echo '
2544 - </tbody>
2545 - </table>
2546 - <br>';
2547 - }
2548 - else
2549 - {
2550 - echo '<p>' . __( 'No viewings exist for this property', 'propertyhive') . '</p>';
2551 - }
2552 - wp_reset_postdata();
2553 -
2554 7099 do_action('propertyhive_property_viewings_fields');
2555 -
2556 - echo '</div>';
2557 -
2558 - echo '</div>';
2559 7100
7101 + // Quit out
2560 7102 die();
2561 7103 }
2562 7104
2563 7105 public function get_contact_viewings_meta_box()
2564 7106 {
2565 - check_ajax_referer( 'get_contact_viewings_meta_box', 'security' );
7107 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
2566 7108
2567 - 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 + }
2568 7116
2569 - echo '<div class="propertyhive_meta_box">';
2570 -
2571 - echo '<div class="options_group">';
7117 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-viewings-meta-box.php' );
2572 7118
2573 - $args = array(
2574 - 'post_type' => 'viewing',
2575 - 'nopaging' => true,
2576 - 'orderby' => 'meta_value',
2577 - 'order' => 'DESC',
2578 - 'post_status' => 'publish',
2579 - 'meta_key' => '_start_date_time',
2580 - 'meta_query' => array(
2581 - array(
2582 - 'key' => '_applicant_contact_id',
2583 - 'value' => $_POST['post_id']
2584 - )
2585 - )
2586 - );
2587 - $viewings_query = new WP_Query( $args );
2588 -
2589 - if ( $viewings_query->have_posts() )
2590 - {
2591 - echo '<table style="width:100%">
2592 - <thead>
2593 - <tr>
2594 - <th style="text-align:left;">' . __( 'Date', 'propertyhive' ) . ' / ' . __( 'Time', 'propertyhive' ) . '</th>
2595 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
2596 - <th style="text-align:left;">' . __( 'Attending Negotiator(s)', 'propertyhive' ) . '</th>
2597 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
2598 - </tr>
2599 - </thead>
2600 - <tbody>';
2601 -
2602 - while ( $viewings_query->have_posts() )
2603 - {
2604 - $viewings_query->the_post();
2605 -
2606 - $property = new PH_Property((int)get_post_meta(get_the_ID(), '_property_id', TRUE));
2607 -
2608 - echo '<tr>';
2609 - 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>';
2610 - 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>';
2611 - echo '<td style="text-align:left;">';
2612 -
2613 - $negotiator_ids = get_post_meta(get_the_ID(), '_negotiator_id');
2614 -
2615 - if (!empty($negotiator_ids))
2616 - {
2617 - $i = 0;
2618 - foreach ($negotiator_ids as $negotiator_id)
2619 - {
2620 - if ( $i > 0 ) { echo ', '; }
2621 -
2622 - $userdata = get_userdata( $negotiator_id );
2623 - if ( $userdata !== FALSE )
2624 - {
2625 - echo $userdata->display_name;
2626 - }
2627 - else
2628 - {
2629 - echo '<em>Unknown user</em>';
2630 - }
2631 - ++$i;
2632 - }
2633 - }
2634 - else
2635 - {
2636 - echo 'Unattended';
2637 - }
2638 -
2639 - echo '</td>';
2640 - echo '<td style="text-align:left;">';
2641 -
2642 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
2643 - echo ucwords(str_replace("_", " ", $status));
2644 - if ( $status == 'carried_out' )
2645 - {
2646 - echo '<br>';
2647 - $feedback_status = get_post_meta(get_the_ID(), '_feedback_status', TRUE);
2648 - switch ( get_post_meta(get_the_ID(), '_feedback_status', TRUE) )
2649 - {
2650 - case "interested": { echo 'Applicant Interested'; break; }
2651 - case "not_interested": { echo 'Applicant Not Interested'; break; }
2652 - case "not_required": { echo 'Feedback Not Required'; break; }
2653 - default: { echo 'Awaiting Feedback'; }
2654 - }
2655 -
2656 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' )
2657 - {
2658 - $feedback_passed_on = get_post_meta(get_the_ID(), '_feedback_passed_on', TRUE);
2659 - echo '<br>' . ( ($feedback_passed_on == 'yes') ? 'Feedback Passed On' : 'Feedback Not Passed On' );
2660 - }
2661 - }
2662 - echo '</td>';
2663 - echo '</tr>';
2664 - }
2665 -
2666 - echo '
2667 - </tbody>
2668 - </table>
2669 - <br>';
2670 - }
2671 - else
2672 - {
2673 - echo '<p>' . __( 'No viewings exist for this contact', 'propertyhive') . '</p>';
2674 - }
2675 - wp_reset_postdata();
2676 -
2677 7119 do_action('propertyhive_contact_viewings_fields');
2678 -
2679 - echo '</div>';
2680 -
2681 - echo '</div>';
2682 7120
7121 + // Quit out
2683 7122 die();
2684 7123 }
2685 7124
2686 7125 // Offer related functions
@@ -2689,10 +7128,19 @@
2689 7128 check_ajax_referer( 'record-offer', 'security' );
2690 7129
2691 7130 $this->json_headers();
2692 7131
2693 - // TO DO: Should do validation on server side also
2694 - 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)
2695 7143 {
2696 7144 $return = array('error' => 'No property selected');
2697 7145 echo json_encode( $return );
2698 7146 die();
@@ -2697,18 +7145,18 @@
2697 7145 echo json_encode( $return );
2698 7146 die();
2699 7147 }
2700 7148
2701 - $property = new PH_Property((int)$_POST['property_id']);
7149 + $property = new PH_Property($property_id);
2702 7150
2703 7151 $applicant_contact_ids = array();
2704 7152
2705 7153 // Create applicant record if required
2706 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
7154 + if (empty($input['applicant_ids']) && !empty($input['applicant_name']))
2707 7155 {
2708 7156 // Need to create contact/applicant
2709 7157 $contact_post = array(
2710 - 'post_title' => wp_strip_all_tags($_POST['applicant_name']),
7158 + 'post_title' => $input['applicant_name'],
2711 7159 'post_content' => '',
2712 7160 'post_type' => 'contact',
2713 7161 'post_status' => 'publish',
2714 7162 'comment_status' => 'closed',
@@ -2715,9 +7163,9 @@
2715 7163 'ping_status' => 'closed',
2716 7164 );
2717 7165
2718 7166 // Insert the post into the database
2719 - $contact_post_id = wp_insert_post( $contact_post );
7167 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
2720 7168
2721 7169 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
2722 7170 {
2723 7171 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -2726,8 +7174,27 @@
2726 7174 }
2727 7175
2728 7176 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
2729 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 +
2730 7197 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
2731 7198 update_post_meta( $contact_post_id, '_applicant_profile_0', array( 'department' => $property->department, 'send_matching_properties' => '' ) );
2732 7199
2733 7200 $applicant_contact_ids[] = $contact_post_id;
@@ -2732,20 +7199,15 @@
2732 7199
2733 7200 $applicant_contact_ids[] = $contact_post_id;
2734 7201 }
2735 7202
2736 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
7203 + if (!empty($input['applicant_ids']) && empty($input['applicant_name']))
2737 7204 {
2738 7205 // This is an existing contact
2739 - if ( !is_array($_POST['applicant_ids']) )
7206 + foreach ( $input['applicant_ids'] as $applicant_id )
2740 7207 {
2741 - $_POST['applicant_ids'] = array($_POST['applicant_ids']);
7208 + $applicant_contact_ids[] = (int)$applicant_id;
2742 7209 }
2743 -
2744 - foreach ( $_POST['applicant_ids'] as $applicant_id )
2745 - {
2746 - $applicant_contact_ids[] = $applicant_id;
2747 - }
2748 7210 }
2749 7211
2750 7212 $applicant_contact_ids = array_unique($applicant_contact_ids);
2751 7213
@@ -2779,15 +7241,35 @@
2779 7241 echo json_encode( $return );
2780 7242 die();
2781 7243 }
2782 7244
2783 - $amount = preg_replace("/[^0-9]/", '', $_POST['amount']);
7245 + $amount = $input['amount'];
2784 7246
2785 - add_post_meta( $offer_post_id, '_offer_date_time', $_POST['offer_date'] . ' ' . $_POST['offer_time'] );
2786 - add_post_meta( $offer_post_id, '_property_id', $_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 );
2787 7249 add_post_meta( $offer_post_id, '_applicant_contact_id', $applicant_contact_id );
2788 7250 add_post_meta( $offer_post_id, '_amount', $amount );
2789 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 + }
2790 7272 }
2791 7273
2792 7274 $applicant_contacts = array();
2793 7275 foreach ( $applicant_contact_ids as $applicant_contact_id )
@@ -2817,10 +7299,16 @@
2817 7299 check_ajax_referer( 'record-offer', 'security' );
2818 7300
2819 7301 $this->json_headers();
2820 7302
2821 - // TO DO: Should do validation on server side also
2822 - 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)
2823 7311 {
2824 7312 $return = array('error' => 'No contact selected');
2825 7313 echo json_encode( $return );
2826 7314 die();
@@ -2825,9 +7313,9 @@
2825 7313 echo json_encode( $return );
2826 7314 die();
2827 7315 }
2828 7316
2829 - if (empty($_POST['property_ids']))
7317 + if (empty($input['property_ids']))
2830 7318 {
2831 7319 $return = array('error' => 'No property selected');
2832 7320 echo json_encode( $return );
2833 7321 die();
@@ -2834,9 +7322,9 @@
2834 7322 }
2835 7323
2836 7324 // Loop through contacts and create one offer each
2837 7325 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
2838 - foreach ( $_POST['property_ids'] as $property_id )
7326 + foreach ( $input['property_ids'] as $property_id )
2839 7327 {
2840 7328 // Insert offer record
2841 7329 $offer_post = array(
2842 7330 'post_title' => '',
@@ -2856,24 +7344,44 @@
2856 7344 echo json_encode( $return );
2857 7345 die();
2858 7346 }
2859 7347
2860 - $amount = preg_replace("/[^0-9]/", '', $_POST['amount']);
7348 + $amount = $input['amount'];
2861 7349
2862 - add_post_meta( $offer_post_id, '_offer_date_time', $_POST['offer_date'] . ' ' . $_POST['offer_time'] );
2863 - add_post_meta( $offer_post_id, '_property_id', $property_id );
2864 - add_post_meta( $offer_post_id, '_applicant_contact_id', $_POST['contact_id'] );
7350 + add_post_meta( $offer_post_id, '_offer_date_time', $input['offer_date'] . ' ' . $input['offer_time'] );
7351 + add_post_meta( $offer_post_id, '_property_id', (int)$property_id );
7352 + add_post_meta( $offer_post_id, '_applicant_contact_id', $contact_id );
2865 7353 add_post_meta( $offer_post_id, '_amount', $amount );
2866 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 + }
2867 7375 }
2868 7376
2869 7377 $properties = array();
2870 - foreach ( $_POST['property_ids'] as $property_id )
7378 + foreach ( $input['property_ids'] as $property_id )
2871 7379 {
2872 7380 $properties[] = array(
2873 - 'ID' => $property_id,
2874 - 'post_title' => get_the_title($property_id),
2875 - 'edit_link' => get_edit_post_link( $property_id, '' ),
7381 + 'ID' => (int)$property_id,
7382 + 'post_title' => get_the_title((int)$property_id),
7383 + 'edit_link' => get_edit_post_link( (int)$property_id, '' ),
2876 7384 );
2877 7385 }
2878 7386
2879 7387 $return = array('success' => array(
@@ -2890,12 +7398,18 @@
2890 7398 }
2891 7399
2892 7400 public function get_offer_details_meta_box()
2893 7401 {
7402 + global $post;
7403 +
2894 7404 check_ajax_referer( 'offer-details-meta-box', 'security' );
2895 7405
2896 - $offer = new PH_Offer((int)$_POST['offer_id']);
7406 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
2897 7407
7408 + $post = get_post( $post_id );
7409 +
7410 + $offer = new PH_Offer( $post_id );
7411 +
2898 7412 echo '<div class="propertyhive_meta_box">';
2899 7413
2900 7414 echo '<div class="options_group">';
2901 7415
@@ -2902,11 +7416,11 @@
2902 7416 if ( $offer->status != '' )
2903 7417 {
2904 7418 echo '<p class="form-field">
2905 7419
2906 - <label for="">' . __('Status', 'propertyhive') . '</label>
7420 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
2907 7421
2908 - ' . ucwords(str_replace("_", " ", $offer->status)) . '
7422 + ' . esc_html(propertyhive_get_status_label( $offer->status )) . '
2909 7423
2910 7424 </p>';
2911 7425 }
2912 7426
@@ -2912,32 +7426,32 @@
2912 7426
2913 7427 $offer_date_time = $offer->offer_date_time;
2914 7428 if ( empty($offer_date_time) )
2915 7429 {
2916 - $offer_date_time = date("Y-m-d H:i:s");
7430 + $offer_date_time = gmdate("Y-m-d H:i:s");
2917 7431 }
2918 7432
2919 7433 echo '<p class="form-field offer_date_time_field">
2920 7434
2921 - <label for="_offer_date">' . __('Offer Date / Time', 'propertyhive') . '</label>
7435 + <label for="_offer_date">' . esc_html(__('Offer Date / Time', 'propertyhive')) . '</label>
2922 7436
2923 - <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="">
2924 7438 <select id="_offer_time_hours" name="_offer_time_hours" class="select short" style="width:55px">';
2925 7439
2926 7440 if ( empty($offer_date_time) )
2927 7441 {
2928 - $value = date("H");
7442 + $value = gmdate("H");
2929 7443 }
2930 7444 else
2931 7445 {
2932 - $value = date( "H", strtotime( $offer_date_time ) );
7446 + $value = gmdate( "H", strtotime( $offer_date_time ) );
2933 7447 }
2934 7448 for ( $i = 0; $i < 23; ++$i )
2935 7449 {
2936 7450 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
2937 - echo '<option value="' . $j . '"';
7451 + echo '<option value="' . esc_attr($j) . '"';
2938 7452 if ($i == $value) { echo ' selected'; }
2939 - echo '>' . $j . '</option>';
7453 + echo '>' . esc_html($j) . '</option>';
2940 7454 }
2941 7455
2942 7456 echo '</select>
2943 7457 :
@@ -2948,16 +7462,16 @@
2948 7462 $value = '';
2949 7463 }
2950 7464 else
2951 7465 {
2952 - $value = date( "i", strtotime( $offer_date_time ) );
7466 + $value = gmdate( "i", strtotime( $offer_date_time ) );
2953 7467 }
2954 7468 for ( $i = 0; $i < 60; $i+=5 )
2955 7469 {
2956 7470 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
2957 - echo '<option value="' . $j . '"';
7471 + echo '<option value="' . esc_attr($j) . '"';
2958 7472 if ($i == $value) { echo ' selected'; }
2959 - echo '>' . $j . '</option>';
7473 + echo '>' . esc_html($j) . '</option>';
2960 7474 }
2961 7475
2962 7476 echo '</select>
2963 7477
@@ -2967,9 +7481,9 @@
2967 7481 'id' => '_amount',
2968 7482 'label' => __( 'Offer Amount', 'propertyhive' ) . ' (&pound;)',
2969 7483 'desc_tip' => false,
2970 7484 'class' => 'short',
2971 - 'value' => ( is_numeric($offer->amount) ? number_format($offer->amount) : '' ),
7485 + 'value' => ( is_numeric($offer->amount) ? ph_display_price_field( $offer->amount ) : '' ),
2972 7486 'custom_attributes' => array(
2973 7487 //'style' => 'width:95%; max-width:500px;'
2974 7488 )
2975 7489 );
@@ -2987,12 +7501,28 @@
2987 7501 public function get_offer_actions()
2988 7502 {
2989 7503 check_ajax_referer( 'offer-actions', 'security' );
2990 7504
2991 - $post_id = $_POST['offer_id'];
7505 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
2992 7506
2993 7507 $status = get_post_meta( $post_id, '_status', TRUE );
2994 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 +
2995 7525 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_offer_actions_meta_box">
2996 7526
2997 7527 <div class="options_group" style="padding-top:8px;">';
2998 7528
@@ -3003,14 +7533,19 @@
3003 7533 $actions[] = '<a
3004 7534 href="#action_panel_offer_accepted"
3005 7535 class="button button-success offer-action"
3006 7536 style="width:100%; margin-bottom:7px; text-align:center"
3007 - >' . __('Accept Offer', 'propertyhive') . '</a>';
7537 + >' . wp_kses_post( __('Accept Offer', 'propertyhive') ) . '</a>';
3008 7538 $actions[] = '<a
3009 7539 href="#action_panel_offer_declined"
3010 7540 class="button button-danger offer-action"
3011 7541 style="width:100%; margin-bottom:7px; text-align:center"
3012 - >' . __('Decline Offer', 'propertyhive') . '</a>';
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>';
3013 7548 }
3014 7549
3015 7550 if ( $status == 'accepted' )
3016 7551 {
@@ -3023,46 +7558,49 @@
3023 7558
3024 7559 if ( $sale_id != '' )
3025 7560 {
3026 7561 $actions[] = '<a
3027 - href="' . get_edit_post_link( $sale_id, '' ) . '"
7562 + href="' . esc_url(get_edit_post_link( $sale_id, '' )) . '"
3028 7563 class="button"
3029 7564 style="width:100%; margin-bottom:7px; text-align:center"
3030 - >' . __('View Sale', 'propertyhive') . '</a>';
7565 + >' . wp_kses_post( __('View Sale', 'propertyhive') ) . '</a>';
3031 7566 }
3032 7567 else
3033 7568 {
3034 7569 $actions[] = '<a
3035 - href="' . wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), '1', 'create_sale' ) . '"
3036 - 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"
3037 7572 style="width:100%; margin-bottom:7px; text-align:center"
3038 - >' . __('Create Sale', 'propertyhive') . '</a>';
7573 + onclick="setTimeout(function() { jQuery(\'.button-create-sale\').attr(\'href\', \'#\'); jQuery(\'.button-create-sale\').attr(\'disabled\', \'disabled\'); jQuery(\'.button-create-sale\').html(\'Creating...\'); }, 50);"
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>';
3039 7580 }
3040 7581 }
3041 7582
3042 - if ( $status == 'declined' )
7583 + if ( $status == 'accepted' || $status == 'declined' || $status == 'withdrawn' )
3043 7584 {
3044 -
3045 - }
3046 -
3047 - if ( $status == 'accepted' || $status == 'declined' )
3048 - {
3049 7585 $actions[] = '<a
3050 7586 href="#action_panel_offer_revert_pending"
3051 7587 class="button offer-action"
3052 7588 style="width:100%; margin-bottom:7px; text-align:center"
3053 - >' . __('Revert To Pending', 'propertyhive') . '</a>';
7589 + >' . wp_kses_post( __('Revert To Pending', 'propertyhive') ) . '</a>';
3054 7590 }
3055 7591
3056 - $actions = apply_filters( 'propertyhive_admin_offer_actions', $actions, $post->ID );
7592 + $actions = apply_filters( 'propertyhive_admin_offer_actions', $actions, $post_id );
7593 + $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
3057 7594
3058 7595 if ( !empty($actions) )
3059 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.
3060 7598 echo implode("", $actions);
3061 7599 }
3062 7600 else
3063 7601 {
3064 - 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>';
3065 7603 }
3066 7604
3067 7605 echo '</div>
3068 7606
@@ -3074,9 +7612,9 @@
3074 7612 public function offer_accepted()
3075 7613 {
3076 7614 check_ajax_referer( 'offer-actions', 'security' );
3077 7615
3078 - $post_id = $_POST['offer_id'];
7616 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
3079 7617
3080 7618 $status = get_post_meta( $post_id, '_status', TRUE );
3081 7619
3082 7620 if ( $status == 'pending' )
@@ -3082,10 +7620,8 @@
3082 7620 if ( $status == 'pending' )
3083 7621 {
3084 7622 update_post_meta( $post_id, '_status', 'accepted' );
3085 7623
3086 - $current_user = wp_get_current_user();
3087 -
3088 7624 // Add note/comment to offer
3089 7625 $comment = array(
3090 7626 'note_type' => 'action',
3091 7627 'action' => 'offer_accepted',
@@ -3090,22 +7626,14 @@
3090 7626 'note_type' => 'action',
3091 7627 'action' => 'offer_accepted',
3092 7628 );
3093 7629
3094 - $data = array(
3095 - 'comment_post_ID' => $post_id,
3096 - 'comment_author' => $current_user->display_name,
3097 - 'comment_author_email' => '[email protected]',
3098 - 'comment_author_url' => '',
3099 - 'comment_date' => date("Y-m-d H:i:s"),
3100 - 'comment_content' => serialize($comment),
3101 - 'comment_approved' => 1,
3102 - 'comment_type' => 'propertyhive_note',
3103 - );
3104 - $comment_id = wp_insert_comment( $data );
7630 + PH_Comments::insert_note( $post_id, $comment );
7631 +
7632 + wp_send_json_success();
3105 7633 }
3106 7634
3107 - die();
7635 + wp_send_json_error();
3108 7636 }
3109 7637
3110 7638 public function offer_declined()
3111 7639 {
@@ -3110,9 +7638,9 @@
3110 7638 public function offer_declined()
3111 7639 {
3112 7640 check_ajax_referer( 'offer-actions', 'security' );
3113 7641
3114 - $post_id = $_POST['offer_id'];
7642 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
3115 7643
3116 7644 $status = get_post_meta( $post_id, '_status', TRUE );
3117 7645
3118 7646 if ( $status == 'pending' )
@@ -3118,10 +7646,8 @@
3118 7646 if ( $status == 'pending' )
3119 7647 {
3120 7648 update_post_meta( $post_id, '_status', 'declined' );
3121 7649
3122 - $current_user = wp_get_current_user();
3123 -
3124 7650 // Add note/comment to offer
3125 7651 $comment = array(
3126 7652 'note_type' => 'action',
3127 7653 'action' => 'offer_declined',
@@ -3126,236 +7652,105 @@
3126 7652 'note_type' => 'action',
3127 7653 'action' => 'offer_declined',
3128 7654 );
3129 7655
3130 - $data = array(
3131 - 'comment_post_ID' => $post_id,
3132 - 'comment_author' => $current_user->display_name,
3133 - 'comment_author_email' => '[email protected]',
3134 - 'comment_author_url' => '',
3135 - 'comment_date' => date("Y-m-d H:i:s"),
3136 - 'comment_content' => serialize($comment),
3137 - 'comment_approved' => 1,
3138 - 'comment_type' => 'propertyhive_note',
3139 - );
3140 - $comment_id = wp_insert_comment( $data );
7656 + PH_Comments::insert_note( $post_id, $comment );
7657 +
7658 + wp_send_json_success();
3141 7659 }
3142 7660
3143 - die();
7661 + wp_send_json_error();
3144 7662 }
3145 7663
3146 - public function offer_revert_pending()
7664 + public function offer_withdrawn()
3147 7665 {
3148 7666 check_ajax_referer( 'offer-actions', 'security' );
3149 7667
3150 - $post_id = $_POST['offer_id'];
7668 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
3151 7669
3152 7670 $status = get_post_meta( $post_id, '_status', TRUE );
3153 7671
3154 - if ( $status == 'accepted' || $status == 'declined' )
7672 + if ( $status == 'pending' || $status == 'accepted' )
3155 7673 {
3156 - update_post_meta( $post_id, '_status', 'pending' );
7674 + update_post_meta( $post_id, '_status', 'withdrawn' );
3157 7675
3158 - $current_user = wp_get_current_user();
3159 -
3160 7676 // Add note/comment to offer
3161 7677 $comment = array(
3162 7678 'note_type' => 'action',
3163 - 'action' => 'offer_revert_pending',
7679 + 'action' => 'offer_withdrawn',
3164 7680 );
3165 7681
3166 - $data = array(
3167 - 'comment_post_ID' => $post_id,
3168 - 'comment_author' => $current_user->display_name,
3169 - 'comment_author_email' => '[email protected]',
3170 - 'comment_author_url' => '',
3171 - 'comment_date' => date("Y-m-d H:i:s"),
3172 - 'comment_content' => serialize($comment),
3173 - 'comment_approved' => 1,
3174 - 'comment_type' => 'propertyhive_note',
3175 - );
3176 - $comment_id = wp_insert_comment( $data );
7682 + PH_Comments::insert_note( $post_id, $comment );
7683 +
7684 + wp_send_json_success();
3177 7685 }
3178 7686
3179 - die();
7687 + wp_send_json_error();
3180 7688 }
3181 7689
3182 - public function get_property_offers_meta_box()
7690 + public function offer_revert_pending()
3183 7691 {
3184 - check_ajax_referer( 'get_property_offers_meta_box', 'security' );
7692 + check_ajax_referer( 'offer-actions', 'security' );
3185 7693
3186 - global $post;
7694 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
3187 7695
3188 - echo '<div class="propertyhive_meta_box">';
3189 -
3190 - echo '<div class="options_group">';
7696 + $status = get_post_meta( $post_id, '_status', TRUE );
3191 7697
3192 - $args = array(
3193 - 'post_type' => 'offer',
3194 - 'nopaging' => true,
3195 - 'orderby' => 'meta_value',
3196 - 'order' => 'DESC',
3197 - 'meta_key' => '_offer_date_time',
3198 - 'post_status' => 'publish',
3199 - 'meta_query' => array(
3200 - array(
3201 - 'key' => '_property_id',
3202 - 'value' => $_POST['post_id']
3203 - )
3204 - )
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',
3205 7706 );
3206 - $offers_query = new WP_Query( $args );
3207 7707
3208 - if ( $offers_query->have_posts() )
3209 - {
3210 - echo '<table style="width:100%">
3211 - <thead>
3212 - <tr>
3213 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
3214 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
3215 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
3216 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3217 - </tr>
3218 - </thead>
3219 - <tbody>';
7708 + PH_Comments::insert_note( $post_id, $comment );
3220 7709
3221 - while ( $offers_query->have_posts() )
3222 - {
3223 - $offers_query->the_post();
7710 + wp_send_json_success();
7711 + }
3224 7712
3225 - $offer = new PH_Offer(get_the_ID());
7713 + wp_send_json_error();
7714 + }
3226 7715
3227 - echo '<tr>';
3228 - 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>';
3229 - 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>';
3230 - echo '<td style="text-align:left;">' . $offer->get_formatted_amount() . '</td>';
3231 - echo '<td style="text-align:left;">';
3232 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
3233 - echo ucwords(str_replace("_", " ", $status));
3234 - echo '</td>';
3235 - echo '</tr>';
3236 - }
7716 + public function get_property_offers_meta_box()
7717 + {
7718 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
3237 7719
3238 - echo '
3239 - </tbody>
3240 - </table>
3241 - <br>';
3242 - }
3243 - else
3244 - {
3245 - echo '<p>' . __( 'No offers exist for this property', 'propertyhive') . '</p>';
3246 - }
3247 - 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 + }
3248 7727
7728 + include( PH()->plugin_path() . '/includes/admin/views/html-property-offers-meta-box.php' );
7729 +
3249 7730 do_action('propertyhive_property_offers_fields');
3250 -
3251 - echo '</div>';
3252 -
3253 - echo '</div>';
3254 7731
7732 + // Quit out
3255 7733 die();
3256 7734 }
3257 7735
3258 7736 public function get_contact_offers_meta_box()
3259 7737 {
3260 - check_ajax_referer( 'get_contact_offers_meta_box', 'security' );
7738 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
3261 7739
3262 - 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 + }
3263 7747
3264 - echo '<div class="propertyhive_meta_box">';
3265 -
3266 - echo '<div class="options_group">';
7748 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-offers-meta-box.php' );
3267 7749
3268 - $args = array(
3269 - 'post_type' => 'offer',
3270 - 'nopaging' => true,
3271 - 'orderby' => 'meta_value',
3272 - 'order' => 'DESC',
3273 - 'post_status' => 'publish',
3274 - 'meta_key' => '_offer_date_time',
3275 - 'meta_query' => array(
3276 - array(
3277 - 'key' => '_applicant_contact_id',
3278 - 'value' => $_POST['post_id']
3279 - )
3280 - )
3281 - );
3282 - $offers_query = new WP_Query( $args );
3283 -
3284 - if ( $offers_query->have_posts() )
3285 - {
3286 - echo '<table style="width:100%">
3287 - <thead>
3288 - <tr>
3289 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
3290 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
3291 - <th style="text-align:left;">' . __( 'Property Owner', 'propertyhive' ) . '</th>
3292 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
3293 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3294 - </tr>
3295 - </thead>
3296 - <tbody>';
3297 -
3298 - while ( $offers_query->have_posts() )
3299 - {
3300 - $offers_query->the_post();
3301 -
3302 - $property = new PH_Property((int)get_post_meta(get_the_ID(), '_property_id', TRUE));
3303 - $offer = new PH_Offer(get_the_ID());
3304 -
3305 - echo '<tr>';
3306 - 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>';
3307 - 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>';
3308 - echo '<td style="text-align:left;">';
3309 -
3310 - $owner_contact_ids = $property->_owner_contact_id;
3311 - if (
3312 - ( !is_array($owner_contact_ids) && $owner_contact_ids != '' && $owner_contact_ids != 0 )
3313 - ||
3314 - ( is_array($owner_contact_ids) && !empty($owner_contact_ids) )
3315 - )
3316 - {
3317 - if ( !is_array($owner_contact_ids) )
3318 - {
3319 - $owner_contact_ids = array($owner_contact_ids);
3320 - }
3321 -
3322 - foreach ( $owner_contact_ids as $owner_contact_id )
3323 - {
3324 - echo get_the_title($owner_contact_id) . '<br>';
3325 - echo '<div style="color:#BBB">';
3326 - echo 'T: ' . get_post_meta($owner_contact_id, '_telephone_number', TRUE) . '<br>';
3327 - echo 'E: ' . get_post_meta($owner_contact_id, '_email_address', TRUE);
3328 - echo '</div>';
3329 - }
3330 - }
3331 -
3332 - echo '</td>';
3333 - echo '<td style="text-align:left;">' . $offer->get_formatted_amount() . '</td>';
3334 - echo '<td style="text-align:left;">';
3335 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
3336 - echo ucwords(str_replace("_", " ", $status));
3337 - echo '</td>';
3338 - echo '</tr>';
3339 - }
3340 -
3341 - echo '
3342 - </tbody>
3343 - </table>
3344 - <br>';
3345 - }
3346 - else
3347 - {
3348 - echo '<p>' . __( 'No offers exist for this contact', 'propertyhive') . '</p>';
3349 - }
3350 - wp_reset_postdata();
3351 -
3352 7750 do_action('propertyhive_contact_offers_fields');
3353 -
3354 - echo '</div>';
3355 -
3356 - echo '</div>';
3357 7751
7752 + // Quit out
3358 7753 die();
3359 7754 }
3360 7755
3361 7756 // Sale related functions
@@ -3360,12 +7755,18 @@
3360 7755
3361 7756 // Sale related functions
3362 7757 public function get_sale_details_meta_box()
3363 7758 {
7759 + global $post;
7760 +
3364 7761 check_ajax_referer( 'sale-details-meta-box', 'security' );
3365 7762
3366 - $sale = new PH_Offer((int)$_POST['sale_id']);
7763 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
3367 7764
7765 + $post = get_post( $post_id );
7766 +
7767 + $sale = new PH_Offer( $post_id );
7768 +
3368 7769 echo '<div class="propertyhive_meta_box">';
3369 7770
3370 7771 echo '<div class="options_group">';
3371 7772
@@ -3372,11 +7773,11 @@
3372 7773 if ( $sale->status != '' )
3373 7774 {
3374 7775 echo '<p class="form-field">
3375 7776
3376 - <label for="">' . __('Status', 'propertyhive') . '</label>
7777 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
3377 7778
3378 - ' . ucwords(str_replace("_", " ", $sale->status)) . '
7779 + ' . esc_html(propertyhive_get_status_label( $sale->status )) . '
3379 7780
3380 7781 </p>';
3381 7782 }
3382 7783
@@ -3382,17 +7783,17 @@
3382 7783
3383 7784 $sale_date_time = $sale->sale_date_time;
3384 7785 if ( empty($sale_date_time) )
3385 7786 {
3386 - $sale_date_time = date("Y-m-d H:i:s");
7787 + $sale_date_time = gmdate("Y-m-d H:i:s");
3387 7788 }
3388 7789
3389 7790 echo '<p class="form-field sale_date_field">
3390 7791
3391 - <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="">
3392 7795
3393 - <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)) . '">
3394 -
3395 7796 </p>';
3396 7797
3397 7798 $args = array(
3398 7799 'id' => '_amount',
@@ -3398,9 +7799,9 @@
3398 7799 'id' => '_amount',
3399 7800 'label' => __( 'Sale Amount', 'propertyhive' ) . ' (&pound;)',
3400 7801 'desc_tip' => false,
3401 7802 'class' => 'short',
3402 - 'value' => ( is_numeric($sale->amount) ? number_format($sale->amount) : '' ),
7803 + 'value' => ( is_numeric($sale->amount) ? ph_display_price_field( $sale->amount ) : '' ),
3403 7804 'custom_attributes' => array(
3404 7805 //'style' => 'width:95%; max-width:500px;'
3405 7806 )
3406 7807 );
@@ -3418,12 +7819,28 @@
3418 7819 public function get_sale_actions()
3419 7820 {
3420 7821 check_ajax_referer( 'sale-actions', 'security' );
3421 7822
3422 - $post_id = $_POST['sale_id'];
7823 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
3423 7824
3424 7825 $status = get_post_meta( $post_id, '_status', TRUE );
3425 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 +
3426 7843 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_sale_actions_meta_box">
3427 7844
3428 7845 <div class="options_group" style="padding-top:8px;">';
3429 7846
@@ -3434,9 +7851,9 @@
3434 7851 $actions[] = '<a
3435 7852 href="#action_panel_sale_exchanged"
3436 7853 class="button button-success sale-action"
3437 7854 style="width:100%; margin-bottom:7px; text-align:center"
3438 - >' . __('Sale Exchanged', 'propertyhive') . '</a>';
7855 + >' . esc_html(__('Sale Exchanged', 'propertyhive')) . '</a>';
3439 7856
3440 7857 }
3441 7858
3442 7859 if ( $status == 'exchanged' )
@@ -3444,9 +7861,9 @@
3444 7861 $actions[] = '<a
3445 7862 href="#action_panel_sale_completed"
3446 7863 class="button button-success sale-action"
3447 7864 style="width:100%; margin-bottom:7px; text-align:center"
3448 - >' . __('Sale Completed', 'propertyhive') . '</a>';
7865 + >' . esc_html(__('Sale Completed', 'propertyhive')) . '</a>';
3449 7866 }
3450 7867
3451 7868 if ( $status == 'completed' )
3452 7869 {
@@ -3458,20 +7875,22 @@
3458 7875 $actions[] = '<a
3459 7876 href="#action_panel_sale_fallen_through"
3460 7877 class="button sale-action"
3461 7878 style="width:100%; margin-bottom:7px; text-align:center"
3462 - >' . __('Sale Fallen Through', 'propertyhive') . '</a>';
7879 + >' . esc_html(__('Sale Fallen Through', 'propertyhive')) . '</a>';
3463 7880 }
3464 7881
3465 - $actions = apply_filters( 'propertyhive_admin_sale_actions', $actions, $post->ID );
7882 + $actions = apply_filters( 'propertyhive_admin_sale_actions', $actions, $post_id );
7883 + $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
3466 7884
3467 7885 if ( !empty($actions) )
3468 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.
3469 7888 echo implode("", $actions);
3470 7889 }
3471 7890 else
3472 7891 {
3473 - 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>';
3474 7893 }
3475 7894
3476 7895 echo '</div>
3477 7896
@@ -3483,9 +7902,9 @@
3483 7902 public function sale_exchanged()
3484 7903 {
3485 7904 check_ajax_referer( 'sale-actions', 'security' );
3486 7905
3487 - $post_id = $_POST['sale_id'];
7906 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
3488 7907
3489 7908 $status = get_post_meta( $post_id, '_status', TRUE );
3490 7909
3491 7910 if ( $status == 'current' )
@@ -3491,10 +7910,8 @@
3491 7910 if ( $status == 'current' )
3492 7911 {
3493 7912 update_post_meta( $post_id, '_status', 'exchanged' );
3494 7913
3495 - $current_user = wp_get_current_user();
3496 -
3497 7914 // Add note/comment to sale
3498 7915 $comment = array(
3499 7916 'note_type' => 'action',
3500 7917 'action' => 'sale_exchanged',
@@ -3499,22 +7916,14 @@
3499 7916 'note_type' => 'action',
3500 7917 'action' => 'sale_exchanged',
3501 7918 );
3502 7919
3503 - $data = array(
3504 - 'comment_post_ID' => $post_id,
3505 - 'comment_author' => $current_user->display_name,
3506 - 'comment_author_email' => '[email protected]',
3507 - 'comment_author_url' => '',
3508 - 'comment_date' => date("Y-m-d H:i:s"),
3509 - 'comment_content' => serialize($comment),
3510 - 'comment_approved' => 1,
3511 - 'comment_type' => 'propertyhive_note',
3512 - );
3513 - $comment_id = wp_insert_comment( $data );
7920 + PH_Comments::insert_note( $post_id, $comment );
7921 +
7922 + wp_send_json_success();
3514 7923 }
3515 7924
3516 - die();
7925 + wp_send_json_error();
3517 7926 }
3518 7927
3519 7928 public function sale_completed()
3520 7929 {
@@ -3519,9 +7928,9 @@
3519 7928 public function sale_completed()
3520 7929 {
3521 7930 check_ajax_referer( 'sale-actions', 'security' );
3522 7931
3523 - $post_id = $_POST['sale_id'];
7932 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
3524 7933
3525 7934 $status = get_post_meta( $post_id, '_status', TRUE );
3526 7935
3527 7936 if ( $status == 'exchanged' )
@@ -3527,10 +7936,8 @@
3527 7936 if ( $status == 'exchanged' )
3528 7937 {
3529 7938 update_post_meta( $post_id, '_status', 'completed' );
3530 7939
3531 - $current_user = wp_get_current_user();
3532 -
3533 7940 // Add note/comment to sale
3534 7941 $comment = array(
3535 7942 'note_type' => 'action',
3536 7943 'action' => 'sale_completed',
@@ -3535,22 +7942,14 @@
3535 7942 'note_type' => 'action',
3536 7943 'action' => 'sale_completed',
3537 7944 );
3538 7945
3539 - $data = array(
3540 - 'comment_post_ID' => $post_id,
3541 - 'comment_author' => $current_user->display_name,
3542 - 'comment_author_email' => '[email protected]',
3543 - 'comment_author_url' => '',
3544 - 'comment_date' => date("Y-m-d H:i:s"),
3545 - 'comment_content' => serialize($comment),
3546 - 'comment_approved' => 1,
3547 - 'comment_type' => 'propertyhive_note',
3548 - );
3549 - $comment_id = wp_insert_comment( $data );
7946 + PH_Comments::insert_note( $post_id, $comment );
7947 +
7948 + wp_send_json_success();
3550 7949 }
3551 7950
3552 - die();
7951 + wp_send_json_error();
3553 7952 }
3554 7953
3555 7954 public function sale_fallen_through()
3556 7955 {
@@ -3555,9 +7954,9 @@
3555 7954 public function sale_fallen_through()
3556 7955 {
3557 7956 check_ajax_referer( 'sale-actions', 'security' );
3558 7957
3559 - $post_id = $_POST['sale_id'];
7958 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
3560 7959
3561 7960 $status = get_post_meta( $post_id, '_status', TRUE );
3562 7961
3563 7962 if ( $status == 'current' || $status == 'exchanged' )
@@ -3563,10 +7962,8 @@
3563 7962 if ( $status == 'current' || $status == 'exchanged' )
3564 7963 {
3565 7964 update_post_meta( $post_id, '_status', 'fallen_through' );
3566 7965
3567 - $current_user = wp_get_current_user();
3568 -
3569 7966 // Add note/comment to sale
3570 7967 $comment = array(
3571 7968 'note_type' => 'action',
3572 7969 'action' => 'sale_fallen_through',
@@ -3571,202 +7968,736 @@
3571 7968 'note_type' => 'action',
3572 7969 'action' => 'sale_fallen_through',
3573 7970 );
3574 7971
3575 - $data = array(
3576 - 'comment_post_ID' => $post_id,
3577 - 'comment_author' => $current_user->display_name,
3578 - 'comment_author_email' => '[email protected]',
3579 - 'comment_author_url' => '',
3580 - 'comment_date' => date("Y-m-d H:i:s"),
3581 - 'comment_content' => serialize($comment),
3582 - 'comment_approved' => 1,
3583 - 'comment_type' => 'propertyhive_note',
3584 - );
3585 - $comment_id = wp_insert_comment( $data );
7972 + PH_Comments::insert_note( $post_id, $comment );
7973 +
7974 + wp_send_json_success();
3586 7975 }
3587 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
3588 7997 die();
3589 7998 }
3590 7999
3591 - public function get_property_sales_meta_box()
8000 + public function get_contact_sales_meta_box()
3592 8001 {
3593 - check_ajax_referer( 'get_property_sales_meta_box', 'security' );
8002 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
3594 8003
3595 - 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 + }
3596 8011
3597 - echo '<div class="propertyhive_meta_box">';
3598 -
3599 - echo '<div class="options_group">';
8012 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-sales-meta-box.php' );
3600 8013
3601 - $args = array(
3602 - 'post_type' => 'sale',
3603 - 'nopaging' => true,
3604 - 'orderby' => 'meta_value',
3605 - 'order' => 'DESC',
3606 - 'meta_key' => '_sale_date_time',
3607 - 'post_status' => 'publish',
3608 - 'meta_query' => array(
3609 - array(
3610 - 'key' => '_property_id',
3611 - 'value' => $_POST['post_id']
3612 - )
3613 - )
3614 - );
3615 - $sales_query = new WP_Query( $args );
8014 + do_action('propertyhive_contact_sales_fields');
3616 8015
3617 - 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 )
3618 8167 {
3619 - echo '<table style="width:100%">
3620 - <thead>
3621 - <tr>
3622 - <th style="text-align:left;">' . __( 'Sale Date', 'propertyhive' ) . '</th>
3623 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
3624 - <th style="text-align:left;">' . __( 'Sale Amount', 'propertyhive' ) . '</th>
3625 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3626 - </tr>
3627 - </thead>
3628 - <tbody>';
8168 + list($key, $value) = explode('=', $key_value_pair);
8169 + $recurrence[strtolower($key)] = $value;
8170 + }
3629 8171
3630 - 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'] )
3631 8176 {
3632 - $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 + }
3633 8190
3634 - $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 + }
3635 8198
3636 - echo '<tr>';
3637 - 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>';
3638 - 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>';
3639 - echo '<td style="text-align:left;">' . $sale->get_formatted_amount() . '</td>';
3640 - echo '<td style="text-align:left;">';
3641 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
3642 - echo ucwords(str_replace("_", " ", $status));
3643 - echo '</td>';
3644 - echo '</tr>';
3645 - }
8199 + echo esc_html($next_key_date);
3646 8200
3647 - echo '
3648 - </tbody>
3649 - </table>
3650 - <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 );
3651 8222 }
3652 - 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 )
3653 8260 {
3654 - 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();
3655 8264 }
3656 - wp_reset_postdata();
3657 8265
3658 - do_action('propertyhive_property_sales_fields');
3659 -
3660 - echo '</div>';
3661 -
3662 - 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'] ) );
3663 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 +
3664 8279 die();
3665 8280 }
3666 8281
3667 - public function get_contact_sales_meta_box()
8282 + public function delete_key_date()
3668 8283 {
3669 - check_ajax_referer( 'get_contact_sales_meta_box', 'security' );
8284 + check_ajax_referer( 'delete-key-date', 'security' );
3670 8285
3671 - global $post;
8286 + $this->json_headers();
3672 8287
3673 - 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 + }
3674 8412
3675 - echo '<div class="options_group">';
8413 + // check plugin status
8414 + $slug = isset( $_POST['slug'] ) && is_string( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
3676 8415
3677 - $args = array(
3678 - 'post_type' => 'sale',
3679 - 'nopaging' => true,
3680 - 'orderby' => 'meta_value',
3681 - 'order' => 'DESC',
3682 - 'post_status' => 'publish',
3683 - 'meta_key' => '_sale_date_time',
3684 - 'meta_query' => array(
3685 - array(
3686 - 'key' => '_applicant_contact_id',
3687 - 'value' => $_POST['post_id']
3688 - )
3689 - )
8416 + $feature = get_ph_pro_feature( $slug );
8417 +
8418 + if ( $feature === false )
8419 + {
8420 + $return = array(
8421 + 'errorMessage' => 'Feature not found'
3690 8422 );
3691 - $sales_query = new WP_Query( $args );
8423 + wp_send_json_error($return);
8424 + }
3692 8425
3693 - if ( $sales_query->have_posts() )
3694 - {
3695 - echo '<table style="width:100%">
3696 - <thead>
3697 - <tr>
3698 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
3699 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
3700 - <th style="text-align:left;">' . __( 'Property Owner', 'propertyhive' ) . '</th>
3701 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
3702 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3703 - </tr>
3704 - </thead>
3705 - <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 + }
3706 8433
3707 - while ( $sales_query->have_posts() )
3708 - {
3709 - $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 + }
3710 8440
3711 - $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;
3712 8445
3713 - $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 + }
3714 8457
3715 - echo '<tr>';
3716 - 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>';
3717 - 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>';
3718 - 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();
3719 8464
3720 - $owner_contact_ids = $property->_owner_contact_id;
8465 + if ( isset($product_id_and_package['success']) && $product_id_and_package['success'] === true )
8466 + {
3721 8467 if (
3722 - ( !is_array($owner_contact_ids) && $owner_contact_ids != '' && $owner_contact_ids != 0 )
3723 - ||
3724 - ( 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'])
3725 8471 )
3726 8472 {
3727 - if ( !is_array($owner_contact_ids) )
3728 - {
3729 - $owner_contact_ids = array($owner_contact_ids);
3730 - }
3731 -
3732 - foreach ( $owner_contact_ids as $owner_contact_id )
3733 - {
3734 - echo get_the_title($owner_contact_id) . '<br>';
3735 - echo '<div style="color:#BBB">';
3736 - echo 'T: ' . get_post_meta($owner_contact_id, '_telephone_number', TRUE) . '<br>';
3737 - echo 'E: ' . get_post_meta($owner_contact_id, '_email_address', TRUE);
3738 - echo '</div>';
3739 - }
8473 + $valid_license_key = true;
3740 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 + }
3741 8499
3742 - echo '</td>';
3743 - echo '<td style="text-align:left;">' . $sale->get_formatted_amount() . '</td>';
3744 - echo '<td style="text-align:left;">';
3745 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
3746 - echo ucwords(str_replace("_", " ", $status));
3747 - echo '</td>';
3748 - echo '</tr>';
3749 - }
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 + }
3750 8508
3751 - echo '
3752 - </tbody>
3753 - </table>
3754 - <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 + );
3755 8522 }
3756 8523 else
3757 8524 {
3758 - 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 + );
3759 8543 }
3760 - wp_reset_postdata();
3761 8544
3762 - 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
3763 8562
3764 - echo '</div>';
3765 -
3766 - 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 + }
3767 8570
3768 - 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();
3769 8700 }
3770 8701 }
3771 8702
3772 8703 new PH_AJAX();