PluginProbe
Property Hive / 2.3.1
Property Hive v2.3.1
2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 1.4.61 All 261 releases
← All changes | includes/class-ph-ajax.php +6325 -1391 1.4.52.3.1 View file →
@@ -1,6 +1,9 @@
1 1 <?php
2 +// phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 +// ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
2 4
5 +
3 6 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
4 7
5 8 /**
6 9 * PropertyHive PH_AJAX
@@ -12,8 +15,9 @@
12 15 * @package PropertyHive/Classes
13 16 * @category Class
14 17 * @author PropertyHive
15 18 */
19 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_AJAX; preserving the existing PH_* class name is required for plugin and extension compatibility.
16 20 class PH_AJAX {
17 21
18 22 /**
19 23 * Hook into ajax events
@@ -23,8 +27,12 @@
23 27 // propertyhive_EVENT => nopriv
24 28 $ajax_events = array(
25 29 'add_note' => false,
26 30 'delete_note' => false,
31 + 'toggle_note_pinned' => false,
32 + 'get_notes_grid' => false,
33 + 'get_pinned_notes_grid' => false,
34 + 'fetch_note_mentions' => false,
27 35 'search_contacts' => false,
28 36 'search_properties' => false,
29 37 'search_negotiators' => false,
30 38 'load_existing_owner_contact' => false,
@@ -30,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' => 'propertyhive@noreply.com',
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,28 +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 - $headers[] = 'Reply-To: ' . sanitize_email( $_POST['email_address'] );
1257 2759
1258 - $to = apply_filters( 'propertyhive_property_enquiry_to', $to, $_POST['property_id'] );
1259 - $subject = apply_filters( 'propertyhive_property_enquiry_subject', $subject, $_POST['property_id'] );
1260 - $message = apply_filters( 'propertyhive_property_enquiry_body', $message, $_POST['property_id'] );
1261 - $headers = apply_filters( 'propertyhive_property_enquiry_headers', $headers, $_POST['property_id'] );
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']) )
2762 + {
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']));
1262 2765
2766 + if ( is_email($reply_to) )
2767 + {
2768 + $headers[] = 'Reply-To: ' . $reply_to;
2769 + }
2770 + }
2771 +
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' );
2778 +
1263 2779 $sent = wp_mail( $to, $subject, $message, $headers );
2780 +
2781 + do_action( 'propertyhive_after_property_enquiry_sent' );
1264 2782
1265 2783 if ( ! $sent )
1266 2784 {
1267 2785 $return['success'] = false;
@@ -1270,42 +2788,79 @@
1270 2788 }
1271 2789 else
1272 2790 {
1273 2791 $return['success'] = true;
2792 +
2793 + $enquiry_post_id = '';
1274 2794
1275 - // Now insert into enquiries section of WordPress
1276 - $title = __( 'Property Enquiry', 'propertyhive' ) . ': ' . get_the_title( $_POST['property_id'] );
1277 - if ( isset($_POST['name']) && ! empty($_POST['name']) )
2795 + if ( get_option( 'propertyhive_store_property_enquiries', 'yes' ) == 'yes' )
1278 2796 {
1279 - $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 + }
1280 2853 }
1281 -
1282 - $enquiry_post = array(
1283 - 'post_title' => $title,
1284 - 'post_content' => '',
1285 - 'post_type' => 'enquiry',
1286 - 'post_status' => 'publish',
1287 - 'comment_status' => 'closed',
1288 - 'ping_status' => 'closed',
1289 - );
1290 -
1291 - // Insert the post into the database
1292 - $enquiry_post_id = wp_insert_post( $enquiry_post );
1293 -
1294 - add_post_meta( $enquiry_post_id, '_status', 'open' );
1295 - add_post_meta( $enquiry_post_id, '_source', 'website' );
1296 - add_post_meta( $enquiry_post_id, '_negotiator_id', '' );
1297 - add_post_meta( $enquiry_post_id, '_office_id', $office_id );
1298 -
1299 - foreach ($_POST as $key => $value)
1300 - {
1301 - add_post_meta( $enquiry_post_id, $key, $value );
1302 - }
1303 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 +
1304 2858 // Send auto-responder
1305 2859 if ( get_option( 'propertyhive_enquiry_auto_responder', '' ) == 'yes' )
1306 2860 {
1307 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.
1308 2863 PH()->email->send_enquiry_auto_responder( $_POST );
1309 2864 }
1310 2865 }
1311 2866 }
@@ -1323,12 +2878,14 @@
1323 2878 public function create_contact_from_enquiry()
1324 2879 {
1325 2880 global $post;
1326 2881
1327 - $enquiry_post_id = ( (isset($_POST['post_id'])) ? $_POST['post_id'] : '' );
1328 - $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'] ) ) : '';
1329 2886
1330 - 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 ) )
1331 2888 {
1332 2889 // This nonce is not valid.
1333 2890 die( json_encode( array('error' => 'Invalid nonce. Please refresh and try again') ) );
1334 2891 }
@@ -1337,36 +2894,70 @@
1337 2894
1338 2895 $name = false;
1339 2896 $email = false;
1340 2897 $telephone = false;
2898 + $address = false;
2899 + $postcode = false;
2900 + $property_id = false;
1341 2901
1342 2902 foreach ($enquiry_meta as $key => $value)
1343 2903 {
1344 - if ( strpos($key, 'name') !== false )
2904 + if ( strpos(strtolower($key), 'name') !== false && strpos(strtolower($key), 'property') === false && $value[0] != '' )
1345 2905 {
1346 - $name = $value[0];
2906 + if ( $name === false )
2907 + {
2908 + $name = $value[0];
2909 + }
2910 + else
2911 + {
2912 + $name .= ' ' . $value[0];
2913 + }
1347 2914 }
1348 - elseif ( strpos($key, 'email') !== false )
2915 + elseif ( strpos(strtolower($key), 'email') !== false && $value[0] != '' )
1349 2916 {
1350 - $email = $value[0];
2917 + if ( $email === false )
2918 + {
2919 + $email = $value[0];
2920 + }
2921 + else
2922 + {
2923 + $email .= ',' . $value[0];
2924 + }
1351 2925 }
1352 - elseif ( strpos($key, 'telephone') !== false )
2926 + elseif ( strpos(strtolower($key), 'phone') !== false && $value[0] != '' )
1353 2927 {
1354 - $telephone = $value[0];
2928 + if ( $telephone === false )
2929 + {
2930 + $telephone = $value[0];
2931 + }
2932 + else
2933 + {
2934 + $telephone .= ',' . $value[0];
2935 + }
1355 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 + }
1356 2949 }
1357 2950
1358 - if ( $name === false || $email === false )
2951 + if ( $name === false && $email === false )
1359 2952 {
1360 - // This nonce is not valid.
1361 - die( json_encode( array('error' => 'Name or email address not found') ) );
2953 + die( json_encode( array('error' => 'Name and email address not found') ) );
1362 2954 }
1363 2955
1364 - // We've not imported this property before
1365 2956 $postdata = array(
1366 2957 'post_excerpt' => '',
1367 2958 'post_content' => '',
1368 - 'post_title' => utf8_encode(wp_strip_all_tags( $name )),
2959 + 'post_title' => wp_strip_all_tags( $name ),
1369 2960 'post_status' => 'publish',
1370 2961 'post_type' => 'contact',
1371 2962 'ping_status' => 'closed',
1372 2963 'comment_status' => 'closed',
@@ -1382,11 +2973,136 @@
1382 2973 {
1383 2974 die( json_encode( array('error' => 'Error creating contact') ) );
1384 2975 }
1385 2976
1386 - if ( $telephone !== FALSE ) { update_post_meta( $contact_post_id, '_telephone_number', $telephone ); }
1387 - if ( $email !== FALSE ) { update_post_meta( $contact_post_id, '_email_address', $email ); }
2977 + update_post_meta( $enquiry_post_id, '_contact_id', $contact_post_id );
1388 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 +
1389 3105 die( json_encode( array('success' => get_edit_post_link($contact_post_id, '')) ) );
1390 3106 }
1391 3107
1392 3108 public function validate_save_contact()
@@ -1396,15 +3112,21 @@
1396 3112 check_ajax_referer( 'contact-save-validation', 'security' );
1397 3113
1398 3114 $this->json_headers();
1399 3115
1400 - parse_str($_POST['form_data']);
1401 - 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 +
1402 3124 $return = array('errors' => array());
1403 3125
1404 - if ( isset($_email_address) && $_email_address != '' )
3126 + if ( '' !== $email_address_input )
1405 3127 {
1406 - $email_addresses = explode( ",", $_email_address );
3128 + $email_addresses = explode( ",", $email_address_input );
1407 3129
1408 3130 foreach ( $email_addresses as $email_address )
1409 3131 {
1410 3132 $email_address = trim( $email_address );
@@ -1419,8 +3141,9 @@
1419 3141 'post_type' => 'contact',
1420 3142 'post_status' => 'any',
1421 3143 'posts_per_page' => 1,
1422 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.
1423 3146 'meta_query' => array(
1424 3147 'relation' => 'OR',
1425 3148 array(
1426 3149 'key' => '_email_address',
@@ -1439,11 +3162,12 @@
1439 3162 'compare' => 'LIKE'
1440 3163 )
1441 3164 )
1442 3165 );
1443 - if ( isset($post_ID) && $post_ID != '' )
3166 + if ( $contact_id )
1444 3167 {
1445 - $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 );
1446 3170 }
1447 3171
1448 3172 $contact_query = new WP_Query( $args );
1449 3173
@@ -1452,9 +3176,10 @@
1452 3176 while ( $contact_query->have_posts() )
1453 3177 {
1454 3178 $contact_query->the_post();
1455 3179
1456 - $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 );
1457 3182 }
1458 3183 }
1459 3184 }
1460 3185 }
@@ -1463,8 +3188,80 @@
1463 3188
1464 3189 die();
1465 3190 }
1466 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 +
1467 3264 // Dashboard related functions
1468 3265 public function get_news()
1469 3266 {
1470 3267 $this->json_headers();
@@ -1489,9 +3286,9 @@
1489 3286 foreach ( $rss_items as $item )
1490 3287 {
1491 3288 $return[] = array(
1492 3289 'title' => esc_html( $item->get_title() ),
1493 - 'permalink' => esc_url( $item->get_permalink() ),
3290 + 'permalink' => esc_url( $item->get_permalink() ) . '?src=dashboard',
1494 3291 'date' => $item->get_date('F d, Y')
1495 3292 );
1496 3293 }
1497 3294
@@ -1513,8 +3310,9 @@
1513 3310 $args = array(
1514 3311 'post_type' => 'viewing',
1515 3312 'fields' => 'ids',
1516 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.
1517 3315 'meta_query' => array(
1518 3316 array(
1519 3317 'key' => '_status',
1520 3318 'value' => 'carried_out'
@@ -1525,8 +3323,10 @@
1525 3323 )
1526 3324 )
1527 3325 );
1528 3326
3327 + $args = apply_filters( 'propertyhive_admin_dashboard_viewings_awaiting_applicant_feedback_args', $args );
3328 +
1529 3329 $viewings_query = new WP_Query( $args );
1530 3330
1531 3331 if ( $viewings_query->have_posts() )
1532 3332 {
@@ -1536,19 +3336,19 @@
1536 3336
1537 3337 $property_id = get_post_meta( get_the_ID(), '_property_id', TRUE );
1538 3338 $property = new PH_Property((int)$property_id);
1539 3339
1540 - $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' );
1541 3341
1542 3342 $return[] = array(
1543 3343 'ID' => get_the_ID(),
1544 3344 'edit_link' => get_edit_post_link( get_the_ID() ),
1545 3345 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
1546 - '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 ))),
1547 3347 'property_id' => $property_id,
1548 3348 'property_address' => $property->get_formatted_full_address(),
1549 - 'applicant_contact_id' => $applicant_contact_id,
1550 - '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] ),
1551 3351 );
1552 3352 }
1553 3353 }
1554 3354
@@ -1558,8 +3358,1586 @@
1558 3358
1559 3359 die();
1560 3360 }
1561 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 +
1562 4940 // Viewing related functions
1563 4941 public function book_viewing_property()
1564 4942 {
1565 4943 check_ajax_referer( 'book-viewing', 'security' );
@@ -1565,10 +4943,11 @@
1565 4943 check_ajax_referer( 'book-viewing', 'security' );
1566 4944
1567 4945 $this->json_headers();
1568 4946
1569 - // TO DO: Should do validation on server side also
1570 - 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)
1571 4950 {
1572 4951 $return = array('error' => 'No property selected');
1573 4952 echo json_encode( $return );
1574 4953 die();
@@ -1573,18 +4952,26 @@
1573 4952 echo json_encode( $return );
1574 4953 die();
1575 4954 }
1576 4955
1577 - $property = new PH_Property((int)$_POST['property_id']);
4956 + $property = new PH_Property( $property_id );
1578 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 + }
1579 4966 $applicant_contact_ids = array();
1580 4967
1581 4968 // Create applicant record if required
1582 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
4969 + if (empty($booking['applicant_ids']) && !empty($booking['applicant_name']))
1583 4970 {
1584 4971 // Need to create contact/applicant
1585 4972 $contact_post = array(
1586 - 'post_title' => wp_strip_all_tags($_POST['applicant_name']),
4973 + 'post_title' => $booking['applicant_name'],
1587 4974 'post_content' => '',
1588 4975 'post_type' => 'contact',
1589 4976 'post_status' => 'publish',
1590 4977 'comment_status' => 'closed',
@@ -1591,9 +4978,9 @@
1591 4978 'ping_status' => 'closed',
1592 4979 );
1593 4980
1594 4981 // Insert the post into the database
1595 - $contact_post_id = wp_insert_post( $contact_post );
4982 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
1596 4983
1597 4984 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
1598 4985 {
1599 4986 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -1602,8 +4989,27 @@
1602 4989 }
1603 4990
1604 4991 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
1605 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 +
1606 5012 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
1607 5013 update_post_meta( $contact_post_id, '_applicant_profile_0', array( 'department' => $property->department, 'send_matching_properties' => '' ) );
1608 5014
1609 5015 $applicant_contact_ids[] = $contact_post_id;
@@ -1608,20 +5014,12 @@
1608 5014
1609 5015 $applicant_contact_ids[] = $contact_post_id;
1610 5016 }
1611 5017
1612 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
5018 + if (!empty($booking['applicant_ids']) && empty($booking['applicant_name']))
1613 5019 {
1614 5020 // This is an existing contact
1615 - if ( !is_array($_POST['applicant_ids']) )
1616 - {
1617 - $_POST['applicant_ids'] = array($_POST['applicant_ids']);
1618 - }
1619 -
1620 - foreach ( $_POST['applicant_ids'] as $applicant_id )
1621 - {
1622 - $applicant_contact_ids[] = $applicant_id;
1623 - }
5021 + $applicant_contact_ids = $booking['applicant_ids'];
1624 5022 }
1625 5023
1626 5024 $applicant_contact_ids = array_unique($applicant_contact_ids);
1627 5025
@@ -1686,53 +5084,37 @@
1686 5084 update_post_meta( $applicant_contact_id, '_applicant_profile_' . $num_applicant_profiles, array( 'department' => $property->department ) );
1687 5085 }
1688 5086 }*/
1689 5087
1690 - // Loop through contacts and create one viewing each
1691 - // At the moment it's a 1-to-1 relationship, but might support multiple in the future
1692 - foreach ( $applicant_contact_ids as $applicant_contact_id )
1693 - {
1694 - // Insert viewing record
1695 - $viewing_post = array(
1696 - 'post_title' => '',
1697 - 'post_content' => '',
1698 - 'post_type' => 'viewing',
1699 - 'post_status' => 'publish',
1700 - 'comment_status' => 'closed',
1701 - 'ping_status' => 'closed',
1702 - );
1703 -
1704 - // Insert the post into the database
1705 - $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 + );
1706 5097
1707 - if ( is_wp_error($viewing_post_id) || $viewing_post_id == 0 )
1708 - {
1709 - $return = array('error' => 'Failed to create viewing post. Please try again');
1710 - echo json_encode( $return );
1711 - die();
1712 - }
1713 -
1714 - add_post_meta( $viewing_post_id, '_start_date_time', $_POST['start_date'] . ' ' . $_POST['start_time'] );
1715 - add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
1716 - add_post_meta( $viewing_post_id, '_property_id', $_POST['property_id'] );
1717 - add_post_meta( $viewing_post_id, '_applicant_contact_id', $applicant_contact_id );
1718 - add_post_meta( $viewing_post_id, '_status', 'pending' );
1719 - add_post_meta( $viewing_post_id, '_feedback_status', '' );
1720 - add_post_meta( $viewing_post_id, '_feedback', '' );
1721 - 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 );
1722 5100
1723 - if ( !empty($_POST['negotiator_ids']) )
1724 - {
1725 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
1726 - {
1727 - add_post_meta( $viewing_post_id, '_negotiator_id', $negotiator_id );
1728 - }
1729 - }
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();
1730 5106 }
1731 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 +
1732 5112 $applicant_contacts = array();
1733 - foreach ( $applicant_contact_ids as $applicant_contact_id )
5113 + foreach ($applicant_contact_ids as $applicant_contact_id)
1734 5114 {
5115 + add_post_meta( $viewing_post_id, '_applicant_contact_id', $applicant_contact_id );
5116 +
1735 5117 $applicant_contacts[] = array(
1736 5118 'ID' => $applicant_contact_id,
1737 5119 'post_title' => get_the_title($applicant_contact_id),
1738 5120 'edit_link' => get_edit_post_link( $applicant_contact_id, '' ),
@@ -1738,8 +5120,21 @@
1738 5120 'edit_link' => get_edit_post_link( $applicant_contact_id, '' ),
1739 5121 );
1740 5122 }
1741 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 +
1742 5137 $return = array('success' => array(
1743 5138 'viewing' => array(
1744 5139 'ID' => $viewing_post_id,
1745 5140 'edit_link' => get_edit_post_link( $viewing_post_id, '' ),
@@ -1757,10 +5152,16 @@
1757 5152 check_ajax_referer( 'book-viewing', 'security' );
1758 5153
1759 5154 $this->json_headers();
1760 5155
1761 - // TO DO: Should do validation on server side also
1762 - 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)
1763 5164 {
1764 5165 $return = array('error' => 'No contact selected');
1765 5166 echo json_encode( $return );
1766 5167 die();
@@ -1765,9 +5166,9 @@
1765 5166 echo json_encode( $return );
1766 5167 die();
1767 5168 }
1768 5169
1769 - if (empty($_POST['property_ids']))
5170 + if (empty($booking['property_ids']))
1770 5171 {
1771 5172 $return = array('error' => 'No property selected');
1772 5173 echo json_encode( $return );
1773 5174 die();
@@ -1774,9 +5175,9 @@
1774 5175 }
1775 5176
1776 5177 // Loop through contacts and create one viewing each
1777 5178 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
1778 - foreach ( $_POST['property_ids'] as $property_id )
5179 + foreach ( $booking['property_ids'] as $property_id )
1779 5180 {
1780 5181 // Insert viewing record
1781 5182 $viewing_post = array(
1782 5183 'post_title' => '',
@@ -1796,33 +5197,33 @@
1796 5197 echo json_encode( $return );
1797 5198 die();
1798 5199 }
1799 5200
1800 - 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'] );
1801 5202 add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
1802 - add_post_meta( $viewing_post_id, '_property_id', $property_id );
1803 - 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 );
1804 5205 add_post_meta( $viewing_post_id, '_status', 'pending' );
1805 5206 add_post_meta( $viewing_post_id, '_feedback_status', '' );
1806 5207 add_post_meta( $viewing_post_id, '_feedback', '' );
1807 5208 add_post_meta( $viewing_post_id, '_feedback_passed_on', '' );
1808 5209
1809 - if ( !empty($_POST['negotiator_ids']) )
5210 + if ( !empty($booking['negotiator_ids']) )
1810 5211 {
1811 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
5212 + foreach ( $booking['negotiator_ids'] as $negotiator_id )
1812 5213 {
1813 - add_post_meta( $viewing_post_id, '_negotiator_id', $negotiator_id );
5214 + add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
1814 5215 }
1815 5216 }
1816 5217 }
1817 5218
1818 5219 $properties = array();
1819 - foreach ( $_POST['property_ids'] as $property_id )
5220 + foreach ( $booking['property_ids'] as $property_id )
1820 5221 {
1821 5222 $properties[] = array(
1822 - 'ID' => $property_id,
1823 - 'post_title' => get_the_title($property_id),
1824 - '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, '' ),
1825 5226 );
1826 5227 }
1827 5228
1828 5229 $return = array('success' => array(
@@ -1839,382 +5240,1681 @@
1839 5240 }
1840 5241
1841 5242 public function get_viewing_details_meta_box()
1842 5243 {
5244 + global $post;
5245 +
1843 5246 check_ajax_referer( 'viewing-details-meta-box', 'security' );
1844 5247
1845 - $viewing = new PH_Viewing((int)$_POST['viewing_id']);
5248 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
1846 5249
1847 - 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;
1848 5275
1849 - 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 + }
1850 5281
1851 - echo '<p class="form-field">
1852 -
1853 - <label for="">' . __('Status', 'propertyhive') . '</label>
1854 -
1855 - ' . ucwords(str_replace("_", " ", $viewing->status));
5282 + $post = get_post((int)$post_id);
1856 5283
1857 - 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' )
1858 5300 {
1859 - 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 )
1860 5395 {
1861 - $offer_id = get_post_meta( $viewing->id, '_offer_id', TRUE );
1862 - 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 )
1863 5434 {
1864 - $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 + }
1865 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 + }
1866 5477
1867 - 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 == '' )
1868 5511 {
1869 - echo ' (<a href="' . get_edit_post_link($offer_id) . '">' . __('View Offer', 'propertyhive') . '</a>)';
5512 + $from = $property->office_email_address;
1870 5513 }
1871 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();
1872 5597 }
1873 -
1874 - echo '</p>';
5598 + else
5599 + {
5600 + wp_send_json_error('No valid recipient email addresses');
5601 + }
1875 5602
1876 - if ( $viewing->status == 'carried_out' )
1877 - {
1878 - echo '<p class="form-field">
1879 -
1880 - <label for="">' . __('Applicant Feedback', 'propertyhive') . '</label>';
5603 + wp_die();
5604 + }
1881 5605
1882 - 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)
1883 5625 {
1884 - 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 )
1885 5637 {
1886 - echo 'Interested';
1887 - break;
5638 + $owner_emails[] = sanitize_email($email_address);
1888 5639 }
1889 - 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)
1890 5650 {
1891 - echo 'Not Interested';
1892 - break;
5651 + $applicant_contact = new PH_Contact($applicant_contact_id);
5652 + $applicant_names[] = $applicant_contact->post_title;
5653 + $applicant_dears[] = $applicant_contact->dear();
1893 5654 }
1894 - 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 )
1895 5675 {
1896 - echo 'Feedback Not Required';
1897 - 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 + }
1898 5695 }
1899 - 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 == '' )
1900 5762 {
1901 - echo 'Awaiting Feedback';
5763 + $from = $property->office_email_address;
1902 5764 }
1903 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 + }
1904 5778
1905 - echo '</p>';
5779 + $attachments = array();
5780 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5781 + {
5782 + $uploaded_files = $this->get_viewing_email_uploads();
1906 5783
1907 - 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)
1908 5825 {
1909 - $args = array(
1910 - 'id' => '_feedback',
1911 - 'label' => __( 'Feedback', 'propertyhive' ),
1912 - 'desc_tip' => false,
1913 - 'class' => '',
1914 - 'value' => $viewing->feedback,
1915 - 'custom_attributes' => array(
1916 - 'style' => 'width:95%; max-width:500px;'
1917 - )
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',
1918 5840 );
1919 - propertyhive_wp_textarea_input( $args );
5841 +
5842 + PH_Comments::insert_note( $post_id, $comment );
1920 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();
1921 5848 }
1922 -
1923 - if ( $viewing->status == 'carried_out' && ( $viewing->feedback_status == 'interested' || $viewing->feedback_status == 'not_interested' ) )
5849 + else
1924 5850 {
1925 - echo '<p class="form-field">
1926 -
1927 - <label for="">' . __('Feedback Passed On', 'propertyhive') . '</label>';
1928 -
1929 - echo ( ($viewing->feedback_passed_on == 'yes') ? 'Yes' : 'No' );
1930 -
1931 - echo '</p>';
5851 + wp_send_json_error('No owner recipients');
1932 5852 }
1933 5853
1934 - do_action('propertyhive_viewing_details_fields');
1935 -
1936 - echo '</div>';
1937 -
1938 - echo '</div>';
1939 -
1940 - die();
5854 + wp_die();
1941 5855 }
1942 5856
1943 - public function get_viewing_actions()
5857 + public function viewing_email_attending_negotiator_booking_confirmation()
1944 5858 {
1945 5859 check_ajax_referer( 'viewing-actions', 'security' );
1946 5860
1947 - $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 );
1948 5863
1949 - $status = get_post_meta( $post_id, '_status', TRUE );
1950 - $feedback_status = get_post_meta( $post_id, '_feedback_status', TRUE );
5864 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
1951 5865
1952 - 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) ) {
1953 5870
1954 - <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);
1955 5878
1956 - $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);
1957 5889
1958 - $actions = array();
5890 + $owner_name = $owner_contact->post_title;
5891 + $owner_dear = $owner_contact->dear();
1959 5892
1960 - if ( $status == 'pending' )
1961 - {
1962 - $actions[] = '<a
1963 - href="#action_panel_viewing_carried_out"
1964 - class="button button-success viewing-action"
1965 - style="width:100%; margin-bottom:7px; text-align:center"
1966 - >' . __('Viewing Carried Out', 'propertyhive') . '</a>';
1967 - $actions[] = '<a
1968 - href="#action_panel_viewing_cancelled"
1969 - class="button viewing-action"
1970 - style="width:100%; margin-bottom:7px; text-align:center"
1971 - >' . __('Viewing Cancelled', 'propertyhive') . '</a>';
1972 - }
5893 + if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
5894 + if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
1973 5895
1974 - if ( $status == 'carried_out' )
1975 - {
1976 - 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) )
1977 5917 {
1978 - $actions[] = '<a
1979 - href="#action_panel_viewing_interested"
1980 - class="button button-success viewing-action"
1981 - style="width:100%; margin-bottom:7px; text-align:center"
1982 - >' . __('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();
1983 5923
1984 - $actions[] = '<a
1985 - href="#action_panel_viewing_not_interested"
1986 - class="button button-danger viewing-action"
1987 - style="width:100%; margin-bottom:7px; text-align:center"
1988 - >' . __('Applicant Not Interested', 'propertyhive') . '</a>';
5924 + $applicant_details[] = $applicant_contact->post_title . "\nT: " . $applicant_contact->telephone_number . "\nE: " . $applicant_contact->email_address;
5925 + }
5926 + }
1989 5927
1990 - $actions[] = '<a
1991 - href="#action_panel_viewing_feedback_not_required"
1992 - class="button viewing-action"
1993 - style="width:100%; margin-bottom:7px; text-align:center"
1994 - >' . __('Feedback Not Required', 'propertyhive') . '</a>';
5928 + $applicant_details = implode("\n\n", $applicant_details);
1995 5929
1996 - $show_feedback_meta_boxes = true;
1997 - }
5930 + $applicant_names = array_filter($applicant_names);
5931 + $applicant_dears = array_filter($applicant_dears);
1998 5932
1999 - 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) )
2000 5947 {
2001 - $actions[] = '<a
2002 - 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 .'"
2003 - class="button button-success"
2004 - style="width:100%; margin-bottom:7px; text-align:center"
2005 - >' . __('Book Second Viewing', 'propertyhive') . '</a>';
2006 -
2007 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
5948 + foreach ( $negotiator_ids as $negotiator_id )
2008 5949 {
2009 - $property_id = get_post_meta( $post_id, '_property_id', TRUE );
2010 - if ( get_post_meta( $property_id, '_department', TRUE ) == 'residential-sales' )
5950 + $negotiator = get_user_by( 'id', $negotiator_id );
5951 + if ( $negotiator !== false )
2011 5952 {
2012 - // See if an offer has this viewing id associated with it
2013 - $offer_id = get_post_meta( $post_id, '_offer_id', TRUE );
2014 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
5953 + if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
2015 5954 {
2016 - $offer_id = '';
5955 + $negotiator_names[] = $negotiator->display_name;
2017 5956 }
5957 +
5958 + if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
5959 + {
5960 + $negotiator_email_addresses[] = $negotiator->user_email;
5961 + }
2018 5962
2019 - if ( $offer_id != '' )
5963 + $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
5964 + if ( !empty($telephone_number) )
2020 5965 {
2021 - $actions[] = '<a
2022 - href="' . get_edit_post_link( $offer_id, '' ) . '"
2023 - class="button"
2024 - style="width:100%; margin-bottom:7px; text-align:center"
2025 - >' . __('View Offer', 'propertyhive') . '</a>';
5966 + $negotiator_telephone_numbers[] = $telephone_number;
2026 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 + }
2027 6080 else
2028 6081 {
2029 - $actions[] = '<a
2030 - href="' . wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), '1', 'create_offer' ) . '"
2031 - class="button button-success"
2032 - style="width:100%; margin-bottom:7px; text-align:center"
2033 - >' . __('Record Offer', 'propertyhive') . '</a>';
6082 + // Handle error in file upload
6083 + wp_send_json_error($movefile['error']);
2034 6084 }
2035 6085 }
2036 6086 }
2037 6087 }
2038 6088
2039 - 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)
2040 6099 {
2041 - $actions[] = '<a
2042 - href="#action_panel_viewing_revert_feedback_passed_on"
2043 - class="button viewing-action"
2044 - style="width:100%; margin-bottom:7px; text-align:center"
2045 - >' . __('Feedback Passed On To Owner', 'propertyhive') . '</a>';
6100 + @wp_delete_file($temp_file);
2046 6101 }
2047 6102
2048 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' || $feedback_status == 'not_required' )
6103 + if ( !$sent )
2049 6104 {
2050 - $actions[] = '<a
2051 - href="#action_panel_viewing_revert_feedback_pending"
2052 - class="button viewing-action"
2053 - style="width:100%; margin-bottom:7px; text-align:center"
2054 - >' . __('Revert To Feedback Pending', 'propertyhive') . '</a>';
6105 + wp_send_json_error('Failed to send email');
2055 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();
2056 6122 }
6123 + else
6124 + {
6125 + wp_send_json_error('No attending negotiator recipients');
6126 + }
2057 6127
2058 - 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 )
2059 6141 {
2060 - 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 )
2061 6153 {
2062 - $offer_id = get_post_meta( $post_id, '_offer_id', TRUE );
2063 - 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 )
2064 6192 {
2065 - $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 + }
2066 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 + }
2067 6235
2068 - 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 == '' )
2069 6279 {
2070 - $actions[] = '<a
2071 - href="' . get_edit_post_link( $offer_id, '' ) . '"
2072 - class="button"
2073 - style="width:100%; margin-bottom:7px; text-align:center"
2074 - >' . __('View Offer', 'propertyhive') . '</a>';
6280 + $from = $property->office_email_address;
2075 6281 }
2076 6282 }
2077 - }
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 + }
2078 6295
2079 - if ( ( $status == 'carried_out' && $feedback_status == '' ) || $status == 'cancelled' )
2080 - {
2081 - $actions[] = '<a
2082 - href="#action_panel_viewing_revert_pending"
2083 - class="button viewing-action"
2084 - style="width:100%; margin-bottom:7px; text-align:center"
2085 - >' . __('Revert To Pending', 'propertyhive') . '</a>';
2086 - }
6296 + $attachments = array();
6297 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6298 + {
6299 + $uploaded_files = $this->get_viewing_email_uploads();
2087 6300
2088 - $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 + );
2089 6313
2090 - if ( !empty($actions) )
2091 - {
2092 - 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();
2093 6365 }
2094 6366 else
2095 6367 {
2096 - echo '<div style="text-align:center">' . __( 'No actions to display', 'propertyhive' ) . '</div>';
6368 + wp_send_json_error('No valid recipient email addresses');
2097 6369 }
2098 6370
2099 - echo '</div>
6371 + wp_die();
6372 + }
2100 6373
2101 - </div>';
6374 + public function viewing_email_owner_cancellation_notification()
6375 + {
6376 + check_ajax_referer( 'viewing-actions', 'security' );
2102 6377
2103 - if ( $show_feedback_meta_boxes )
2104 - {
2105 - 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' );
2106 6379
2107 - <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' );
2108 6382
2109 - <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 ) {
2110 6387
2111 - <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 + }
2112 6451
2113 - <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 + }
2114 6456
2115 - </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 + }
2116 6486
2117 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2118 - <a class="button button-primary interested-feedback-action-submit" href="#">' . __( 'Save Feedback', 'propertyhive' ) . '</a>
6487 + $property = new PH_Property((int)$property_id);
2119 6488
2120 - </div>
6489 + $to = implode(",", $owner_emails);
2121 6490
2122 - </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', '' );
2123 6493
2124 - 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);
2125 6502
2126 - <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 );
2127 6505
2128 - <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);
2129 6516
2130 - <label for="_viewing_not_interested_feedback">' . __( 'Applicant Feedback', 'propertyhive' ) . '</label>
2131 -
2132 - <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);
2133 6526
2134 - </div>
6527 + $body = html_entity_decode($body);
2135 6528
2136 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2137 - <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 );
2138 6531
2139 - </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 : '' );
2140 6538
2141 - </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();
2142 6626 }
6627 + else
6628 + {
6629 + wp_send_json_error('No owner recipients');
6630 + }
2143 6631
2144 - die();
6632 + wp_die();
2145 6633 }
2146 6634
2147 - public function viewing_carried_out()
6635 + public function viewing_email_attending_negotiator_cancellation_notification()
2148 6636 {
2149 6637 check_ajax_referer( 'viewing-actions', 'security' );
2150 6638
2151 - $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 );
2152 6641
2153 - $status = get_post_meta( $post_id, '_status', TRUE );
6642 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
2154 6643
2155 - if ( $status == 'pending' )
2156 - {
2157 - 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) ) {
2158 6648
2159 - $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);
2160 6656
2161 - // Add note/comment to viewing
2162 - $comment = array(
2163 - 'note_type' => 'action',
2164 - 'action' => 'viewing_carried_out',
2165 - );
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);
2166 6667
2167 - $data = array(
2168 - 'comment_post_ID' => $post_id,
2169 - 'comment_author' => $current_user->display_name,
2170 - 'comment_author_email' => 'propertyhive@noreply.com',
2171 - 'comment_author_url' => '',
2172 - 'comment_date' => date("Y-m-d H:i:s"),
2173 - 'comment_content' => serialize($comment),
2174 - 'comment_approved' => 1,
2175 - 'comment_type' => 'propertyhive_note',
2176 - );
2177 - $comment_id = wp_insert_comment( $data );
2178 - }
6668 + $owner_name = $owner_contact->post_title;
6669 + $owner_dear = $owner_contact->dear();
2179 6670
2180 - die();
2181 - }
6671 + if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
6672 + if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
2182 6673
2183 - public function viewing_cancelled()
2184 - {
2185 - 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 + }
2186 6680
2187 - $post_id = $_POST['viewing_id'];
6681 + $owner_details[] = $owner_contact->post_title . "\nT: " . $owner_contact->telephone_number . "\nE: " . $owner_contact->email_address;
6682 + }
6683 + }
2188 6684
2189 - $status = get_post_meta( $post_id, '_status', TRUE );
6685 + $owner_details = implode("\n\n", $owner_details);
2190 6686
2191 - if ( $status == 'pending' )
2192 - {
2193 - 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);
2194 6689
2195 - $current_user = wp_get_current_user();
6690 + $applicant_names = array();
6691 + $applicant_dears = array();
6692 + $applicant_details = array();
2196 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 +
2197 6896 // Add note/comment to viewing
2198 - $comment = array(
2199 - 'note_type' => 'action',
2200 - 'action' => 'viewing_cancelled',
2201 - );
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 + );
2202 6903
2203 - $data = array(
2204 - 'comment_post_ID' => $post_id,
2205 - 'comment_author' => $current_user->display_name,
2206 - 'comment_author_email' => 'propertyhive@noreply.com',
2207 - 'comment_author_url' => '',
2208 - 'comment_date' => date("Y-m-d H:i:s"),
2209 - 'comment_content' => serialize($comment),
2210 - 'comment_approved' => 1,
2211 - 'comment_type' => 'propertyhive_note',
2212 - );
2213 - $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();
2214 6910 }
6911 + else
6912 + {
6913 + wp_send_json_error('No attending negotiator recipients');
6914 + }
2215 6915
2216 - die();
6916 + wp_die();
2217 6917 }
2218 6918
2219 6919 public function viewing_interested_feedback()
2220 6920 {
@@ -2219,19 +6919,19 @@
2219 6919 public function viewing_interested_feedback()
2220 6920 {
2221 6921 check_ajax_referer( 'viewing-actions', 'security' );
2222 6922
2223 - $post_id = $_POST['viewing_id'];
6923 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2224 6924
6925 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6926 +
2225 6927 $status = get_post_meta( $post_id, '_status', TRUE );
2226 6928
2227 6929 if ( $status == 'carried_out' )
2228 6930 {
2229 6931 update_post_meta( $post_id, '_feedback_status', 'interested' );
2230 - update_post_meta( $post_id, '_feedback', $_POST['feedback'] );
6932 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
2231 6933
2232 - $current_user = wp_get_current_user();
2233 -
2234 6934 // Add note/comment to viewing
2235 6935 $comment = array(
2236 6936 'note_type' => 'action',
2237 6937 'action' => 'viewing_applicant_interested',
@@ -2236,22 +6936,14 @@
2236 6936 'note_type' => 'action',
2237 6937 'action' => 'viewing_applicant_interested',
2238 6938 );
2239 6939
2240 - $data = array(
2241 - 'comment_post_ID' => $post_id,
2242 - 'comment_author' => $current_user->display_name,
2243 - 'comment_author_email' => 'propertyhive@noreply.com',
2244 - 'comment_author_url' => '',
2245 - 'comment_date' => date("Y-m-d H:i:s"),
2246 - 'comment_content' => serialize($comment),
2247 - 'comment_approved' => 1,
2248 - 'comment_type' => 'propertyhive_note',
2249 - );
2250 - $comment_id = wp_insert_comment( $data );
6940 + PH_Comments::insert_note( $post_id, $comment );
6941 +
6942 + wp_send_json_success();
2251 6943 }
2252 6944
2253 - die();
6945 + wp_send_json_error();
2254 6946 }
2255 6947
2256 6948 public function viewing_not_interested_feedback()
2257 6949 {
@@ -2256,19 +6948,19 @@
2256 6948 public function viewing_not_interested_feedback()
2257 6949 {
2258 6950 check_ajax_referer( 'viewing-actions', 'security' );
2259 6951
2260 - $post_id = $_POST['viewing_id'];
6952 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2261 6953
6954 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6955 +
2262 6956 $status = get_post_meta( $post_id, '_status', TRUE );
2263 6957
2264 6958 if ( $status == 'carried_out' )
2265 6959 {
2266 6960 update_post_meta( $post_id, '_feedback_status', 'not_interested' );
2267 - update_post_meta( $post_id, '_feedback', $_POST['feedback'] );
6961 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
2268 6962
2269 - $current_user = wp_get_current_user();
2270 -
2271 6963 // Add note/comment to viewing
2272 6964 $comment = array(
2273 6965 'note_type' => 'action',
2274 6966 'action' => 'viewing_applicant_not_interested',
@@ -2273,22 +6965,14 @@
2273 6965 'note_type' => 'action',
2274 6966 'action' => 'viewing_applicant_not_interested',
2275 6967 );
2276 6968
2277 - $data = array(
2278 - 'comment_post_ID' => $post_id,
2279 - 'comment_author' => $current_user->display_name,
2280 - 'comment_author_email' => 'propertyhive@noreply.com',
2281 - 'comment_author_url' => '',
2282 - 'comment_date' => date("Y-m-d H:i:s"),
2283 - 'comment_content' => serialize($comment),
2284 - 'comment_approved' => 1,
2285 - 'comment_type' => 'propertyhive_note',
2286 - );
2287 - $comment_id = wp_insert_comment( $data );
6969 + PH_Comments::insert_note( $post_id, $comment );
6970 +
6971 + wp_send_json_success();
2288 6972 }
2289 6973
2290 - die();
6974 + wp_send_json_error();
2291 6975 }
2292 6976
2293 6977 public function viewing_feedback_not_required()
2294 6978 {
@@ -2293,9 +6977,9 @@
2293 6977 public function viewing_feedback_not_required()
2294 6978 {
2295 6979 check_ajax_referer( 'viewing-actions', 'security' );
2296 6980
2297 - $post_id = $_POST['viewing_id'];
6981 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2298 6982
2299 6983 $status = get_post_meta( $post_id, '_status', TRUE );
2300 6984
2301 6985 if ( $status == 'carried_out' )
@@ -2301,10 +6985,8 @@
2301 6985 if ( $status == 'carried_out' )
2302 6986 {
2303 6987 update_post_meta( $post_id, '_feedback_status', 'not_required' );
2304 6988
2305 - $current_user = wp_get_current_user();
2306 -
2307 6989 // Add note/comment to viewing
2308 6990 $comment = array(
2309 6991 'note_type' => 'action',
2310 6992 'action' => 'viewing_feedback_not_required',
@@ -2309,22 +6991,14 @@
2309 6991 'note_type' => 'action',
2310 6992 'action' => 'viewing_feedback_not_required',
2311 6993 );
2312 6994
2313 - $data = array(
2314 - 'comment_post_ID' => $post_id,
2315 - 'comment_author' => $current_user->display_name,
2316 - 'comment_author_email' => 'propertyhive@noreply.com',
2317 - 'comment_author_url' => '',
2318 - 'comment_date' => date("Y-m-d H:i:s"),
2319 - 'comment_content' => serialize($comment),
2320 - 'comment_approved' => 1,
2321 - 'comment_type' => 'propertyhive_note',
2322 - );
2323 - $comment_id = wp_insert_comment( $data );
6995 + PH_Comments::insert_note( $post_id, $comment );
6996 +
6997 + wp_send_json_success();
2324 6998 }
2325 6999
2326 - die();
7000 + wp_send_json_error();
2327 7001 }
2328 7002
2329 7003 public function viewing_revert_feedback_pending()
2330 7004 {
@@ -2329,9 +7003,9 @@
2329 7003 public function viewing_revert_feedback_pending()
2330 7004 {
2331 7005 check_ajax_referer( 'viewing-actions', 'security' );
2332 7006
2333 - $post_id = $_POST['viewing_id'];
7007 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2334 7008
2335 7009 $status = get_post_meta( $post_id, '_status', TRUE );
2336 7010
2337 7011 if ( $status == 'carried_out' )
@@ -2337,11 +7011,10 @@
2337 7011 if ( $status == 'carried_out' )
2338 7012 {
2339 7013 update_post_meta( $post_id, '_feedback_status', '' );
2340 7014 update_post_meta( $post_id, '_feedback_passed_on', '' );
7015 + delete_post_meta( $post_id, '_feedback_received_date' );
2341 7016
2342 - $current_user = wp_get_current_user();
2343 -
2344 7017 // Add note/comment to viewing
2345 7018 $comment = array(
2346 7019 'note_type' => 'action',
2347 7020 'action' => 'viewing_revert_feedback_pending',
@@ -2346,22 +7019,14 @@
2346 7019 'note_type' => 'action',
2347 7020 'action' => 'viewing_revert_feedback_pending',
2348 7021 );
2349 7022
2350 - $data = array(
2351 - 'comment_post_ID' => $post_id,
2352 - 'comment_author' => $current_user->display_name,
2353 - 'comment_author_email' => 'propertyhive@noreply.com',
2354 - 'comment_author_url' => '',
2355 - 'comment_date' => date("Y-m-d H:i:s"),
2356 - 'comment_content' => serialize($comment),
2357 - 'comment_approved' => 1,
2358 - 'comment_type' => 'propertyhive_note',
2359 - );
2360 - $comment_id = wp_insert_comment( $data );
7023 + PH_Comments::insert_note( $post_id, $comment );
7024 +
7025 + wp_send_json_success();
2361 7026 }
2362 7027
2363 - die();
7028 + wp_send_json_error();
2364 7029 }
2365 7030
2366 7031 public function viewing_revert_pending()
2367 7032 {
@@ -2366,19 +7031,18 @@
2366 7031 public function viewing_revert_pending()
2367 7032 {
2368 7033 check_ajax_referer( 'viewing-actions', 'security' );
2369 7034
2370 - $post_id = $_POST['viewing_id'];
7035 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2371 7036
2372 7037 $status = get_post_meta( $post_id, '_status', TRUE );
2373 7038
2374 - if ( $status == 'carried_out' || $status == 'cancelled' )
7039 + if ( in_array( $status, array('carried_out', 'cancelled', 'no_show') ) )
2375 7040 {
2376 7041 update_post_meta( $post_id, '_status', 'pending' );
2377 7042 update_post_meta( $post_id, '_feedback_status', '' );
7043 + delete_post_meta( $post_id, '_feedback_received_date' );
2378 7044
2379 - $current_user = wp_get_current_user();
2380 -
2381 7045 // Add note/comment to viewing
2382 7046 $comment = array(
2383 7047 'note_type' => 'action',
2384 7048 'action' => 'viewing_revert_pending',
@@ -2383,22 +7047,14 @@
2383 7047 'note_type' => 'action',
2384 7048 'action' => 'viewing_revert_pending',
2385 7049 );
2386 7050
2387 - $data = array(
2388 - 'comment_post_ID' => $post_id,
2389 - 'comment_author' => $current_user->display_name,
2390 - 'comment_author_email' => 'propertyhive@noreply.com',
2391 - 'comment_author_url' => '',
2392 - 'comment_date' => date("Y-m-d H:i:s"),
2393 - 'comment_content' => serialize($comment),
2394 - 'comment_approved' => 1,
2395 - 'comment_type' => 'propertyhive_note',
2396 - );
2397 - $comment_id = wp_insert_comment( $data );
7051 + PH_Comments::insert_note( $post_id, $comment );
7052 +
7053 + wp_send_json_success();
2398 7054 }
2399 7055
2400 - die();
7056 + wp_send_json_error();
2401 7057 }
2402 7058
2403 7059 public function viewing_feedback_passed_on()
2404 7060 {
@@ -2403,9 +7059,9 @@
2403 7059 public function viewing_feedback_passed_on()
2404 7060 {
2405 7061 check_ajax_referer( 'viewing-actions', 'security' );
2406 7062
2407 - $post_id = $_POST['viewing_id'];
7063 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
2408 7064
2409 7065 $status = get_post_meta( $post_id, '_status', TRUE );
2410 7066
2411 7067 if ( $status == 'carried_out' )
@@ -2411,10 +7067,8 @@
2411 7067 if ( $status == 'carried_out' )
2412 7068 {
2413 7069 update_post_meta( $post_id, '_feedback_passed_on', 'yes' );
2414 7070
2415 - $current_user = wp_get_current_user();
2416 -
2417 7071 // Add note/comment to viewing
2418 7072 $comment = array(
2419 7073 'note_type' => 'action',
2420 7074 'action' => 'viewing_feedback_passed_on',
@@ -2419,265 +7073,53 @@
2419 7073 'note_type' => 'action',
2420 7074 'action' => 'viewing_feedback_passed_on',
2421 7075 );
2422 7076
2423 - $data = array(
2424 - 'comment_post_ID' => $post_id,
2425 - 'comment_author' => $current_user->display_name,
2426 - 'comment_author_email' => 'propertyhive@noreply.com',
2427 - 'comment_author_url' => '',
2428 - 'comment_date' => date("Y-m-d H:i:s"),
2429 - 'comment_content' => serialize($comment),
2430 - 'comment_approved' => 1,
2431 - 'comment_type' => 'propertyhive_note',
2432 - );
2433 - $comment_id = wp_insert_comment( $data );
7077 + PH_Comments::insert_note( $post_id, $comment );
7078 +
7079 + wp_send_json_success();
2434 7080 }
2435 7081
2436 - die();
7082 + wp_send_json_error();
2437 7083 }
2438 7084
2439 7085 public function get_property_viewings_meta_box()
2440 7086 {
2441 - check_ajax_referer( 'get_property_viewings_meta_box', 'security' );
7087 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
2442 7088
2443 - 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 + }
2444 7096
2445 - echo '<div class="propertyhive_meta_box">';
2446 -
2447 - echo '<div class="options_group">';
7097 + include( PH()->plugin_path() . '/includes/admin/views/html-property-viewings-meta-box.php' );
2448 7098
2449 - $args = array(
2450 - 'post_type' => 'viewing',
2451 - 'nopaging' => true,
2452 - 'orderby' => 'meta_value',
2453 - 'order' => 'DESC',
2454 - 'meta_key' => '_start_date_time',
2455 - 'post_status' => 'publish',
2456 - 'meta_query' => array(
2457 - array(
2458 - 'key' => '_property_id',
2459 - 'value' => $_POST['post_id']
2460 - )
2461 - )
2462 - );
2463 - $viewings_query = new WP_Query( $args );
2464 -
2465 - if ( $viewings_query->have_posts() )
2466 - {
2467 - echo '<table style="width:100%">
2468 - <thead>
2469 - <tr>
2470 - <th style="text-align:left;">' . __( 'Date', 'propertyhive' ) . ' / ' . __( 'Time', 'propertyhive' ) . '</th>
2471 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
2472 - <th style="text-align:left;">' . __( 'Attending Negotiator(s)', 'propertyhive' ) . '</th>
2473 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
2474 - </tr>
2475 - </thead>
2476 - <tbody>';
2477 -
2478 - while ( $viewings_query->have_posts() )
2479 - {
2480 - $viewings_query->the_post();
2481 -
2482 - echo '<tr>';
2483 - 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>';
2484 - 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>';
2485 - echo '<td style="text-align:left;">';
2486 -
2487 - $negotiator_ids = get_post_meta(get_the_ID(), '_negotiator_id');
2488 -
2489 - if (!empty($negotiator_ids))
2490 - {
2491 - $i = 0;
2492 - foreach ($negotiator_ids as $negotiator_id)
2493 - {
2494 - if ( $i > 0 ) { echo ', '; }
2495 -
2496 - $userdata = get_userdata( $negotiator_id );
2497 - if ( $userdata !== FALSE )
2498 - {
2499 - echo $userdata->display_name;
2500 - }
2501 - else
2502 - {
2503 - echo '<em>Unknown user</em>';
2504 - }
2505 - ++$i;
2506 - }
2507 - }
2508 - else
2509 - {
2510 - echo 'Unattended';
2511 - }
2512 -
2513 - echo '</td>';
2514 - echo '<td style="text-align:left;">';
2515 -
2516 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
2517 - echo ucwords(str_replace("_", " ", $status));
2518 - if ( $status == 'carried_out' )
2519 - {
2520 - echo '<br>';
2521 - $feedback_status = get_post_meta(get_the_ID(), '_feedback_status', TRUE);
2522 - switch ( $feedback_status )
2523 - {
2524 - case "interested": { echo 'Applicant Interested'; break; }
2525 - case "not_interested": { echo 'Applicant Not Interested'; break; }
2526 - case "not_required": { echo 'Feedback Not Required'; break; }
2527 - default: { echo 'Awaiting Feedback'; }
2528 - }
2529 -
2530 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' )
2531 - {
2532 - $feedback_passed_on = get_post_meta(get_the_ID(), '_feedback_passed_on', TRUE);
2533 - echo '<br>' . ( ($feedback_passed_on == 'yes') ? 'Feedback Passed On' : 'Feedback Not Passed On' );
2534 - }
2535 - }
2536 - echo '</td>';
2537 - echo '</tr>';
2538 - }
2539 -
2540 - echo '
2541 - </tbody>
2542 - </table>
2543 - <br>';
2544 - }
2545 - else
2546 - {
2547 - echo '<p>' . __( 'No viewings exist for this property', 'propertyhive') . '</p>';
2548 - }
2549 - wp_reset_postdata();
2550 -
2551 7099 do_action('propertyhive_property_viewings_fields');
2552 -
2553 - echo '</div>';
2554 -
2555 - echo '</div>';
2556 7100
7101 + // Quit out
2557 7102 die();
2558 7103 }
2559 7104
2560 7105 public function get_contact_viewings_meta_box()
2561 7106 {
2562 - check_ajax_referer( 'get_contact_viewings_meta_box', 'security' );
7107 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
2563 7108
2564 - 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 + }
2565 7116
2566 - echo '<div class="propertyhive_meta_box">';
2567 -
2568 - echo '<div class="options_group">';
7117 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-viewings-meta-box.php' );
2569 7118
2570 - $args = array(
2571 - 'post_type' => 'viewing',
2572 - 'nopaging' => true,
2573 - 'orderby' => 'meta_value',
2574 - 'order' => 'DESC',
2575 - 'post_status' => 'publish',
2576 - 'meta_key' => '_start_date_time',
2577 - 'meta_query' => array(
2578 - array(
2579 - 'key' => '_applicant_contact_id',
2580 - 'value' => $_POST['post_id']
2581 - )
2582 - )
2583 - );
2584 - $viewings_query = new WP_Query( $args );
2585 -
2586 - if ( $viewings_query->have_posts() )
2587 - {
2588 - echo '<table style="width:100%">
2589 - <thead>
2590 - <tr>
2591 - <th style="text-align:left;">' . __( 'Date', 'propertyhive' ) . ' / ' . __( 'Time', 'propertyhive' ) . '</th>
2592 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
2593 - <th style="text-align:left;">' . __( 'Attending Negotiator(s)', 'propertyhive' ) . '</th>
2594 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
2595 - </tr>
2596 - </thead>
2597 - <tbody>';
2598 -
2599 - while ( $viewings_query->have_posts() )
2600 - {
2601 - $viewings_query->the_post();
2602 -
2603 - $property = new PH_Property((int)get_post_meta(get_the_ID(), '_property_id', TRUE));
2604 -
2605 - echo '<tr>';
2606 - 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>';
2607 - 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>';
2608 - echo '<td style="text-align:left;">';
2609 -
2610 - $negotiator_ids = get_post_meta(get_the_ID(), '_negotiator_id');
2611 -
2612 - if (!empty($negotiator_ids))
2613 - {
2614 - $i = 0;
2615 - foreach ($negotiator_ids as $negotiator_id)
2616 - {
2617 - if ( $i > 0 ) { echo ', '; }
2618 -
2619 - $userdata = get_userdata( $negotiator_id );
2620 - if ( $userdata !== FALSE )
2621 - {
2622 - echo $userdata->display_name;
2623 - }
2624 - else
2625 - {
2626 - echo '<em>Unknown user</em>';
2627 - }
2628 - ++$i;
2629 - }
2630 - }
2631 - else
2632 - {
2633 - echo 'Unattended';
2634 - }
2635 -
2636 - echo '</td>';
2637 - echo '<td style="text-align:left;">';
2638 -
2639 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
2640 - echo ucwords(str_replace("_", " ", $status));
2641 - if ( $status == 'carried_out' )
2642 - {
2643 - echo '<br>';
2644 - $feedback_status = get_post_meta(get_the_ID(), '_feedback_status', TRUE);
2645 - switch ( get_post_meta(get_the_ID(), '_feedback_status', TRUE) )
2646 - {
2647 - case "interested": { echo 'Applicant Interested'; break; }
2648 - case "not_interested": { echo 'Applicant Not Interested'; break; }
2649 - case "not_required": { echo 'Feedback Not Required'; break; }
2650 - default: { echo 'Awaiting Feedback'; }
2651 - }
2652 -
2653 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' )
2654 - {
2655 - $feedback_passed_on = get_post_meta(get_the_ID(), '_feedback_passed_on', TRUE);
2656 - echo '<br>' . ( ($feedback_passed_on == 'yes') ? 'Feedback Passed On' : 'Feedback Not Passed On' );
2657 - }
2658 - }
2659 - echo '</td>';
2660 - echo '</tr>';
2661 - }
2662 -
2663 - echo '
2664 - </tbody>
2665 - </table>
2666 - <br>';
2667 - }
2668 - else
2669 - {
2670 - echo '<p>' . __( 'No viewings exist for this contact', 'propertyhive') . '</p>';
2671 - }
2672 - wp_reset_postdata();
2673 -
2674 7119 do_action('propertyhive_contact_viewings_fields');
2675 -
2676 - echo '</div>';
2677 -
2678 - echo '</div>';
2679 7120
7121 + // Quit out
2680 7122 die();
2681 7123 }
2682 7124
2683 7125 // Offer related functions
@@ -2686,10 +7128,19 @@
2686 7128 check_ajax_referer( 'record-offer', 'security' );
2687 7129
2688 7130 $this->json_headers();
2689 7131
2690 - // TO DO: Should do validation on server side also
2691 - 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)
2692 7143 {
2693 7144 $return = array('error' => 'No property selected');
2694 7145 echo json_encode( $return );
2695 7146 die();
@@ -2694,18 +7145,18 @@
2694 7145 echo json_encode( $return );
2695 7146 die();
2696 7147 }
2697 7148
2698 - $property = new PH_Property((int)$_POST['property_id']);
7149 + $property = new PH_Property($property_id);
2699 7150
2700 7151 $applicant_contact_ids = array();
2701 7152
2702 7153 // Create applicant record if required
2703 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
7154 + if (empty($input['applicant_ids']) && !empty($input['applicant_name']))
2704 7155 {
2705 7156 // Need to create contact/applicant
2706 7157 $contact_post = array(
2707 - 'post_title' => wp_strip_all_tags($_POST['applicant_name']),
7158 + 'post_title' => $input['applicant_name'],
2708 7159 'post_content' => '',
2709 7160 'post_type' => 'contact',
2710 7161 'post_status' => 'publish',
2711 7162 'comment_status' => 'closed',
@@ -2712,9 +7163,9 @@
2712 7163 'ping_status' => 'closed',
2713 7164 );
2714 7165
2715 7166 // Insert the post into the database
2716 - $contact_post_id = wp_insert_post( $contact_post );
7167 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
2717 7168
2718 7169 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
2719 7170 {
2720 7171 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -2723,8 +7174,27 @@
2723 7174 }
2724 7175
2725 7176 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
2726 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 +
2727 7197 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
2728 7198 update_post_meta( $contact_post_id, '_applicant_profile_0', array( 'department' => $property->department, 'send_matching_properties' => '' ) );
2729 7199
2730 7200 $applicant_contact_ids[] = $contact_post_id;
@@ -2729,20 +7199,15 @@
2729 7199
2730 7200 $applicant_contact_ids[] = $contact_post_id;
2731 7201 }
2732 7202
2733 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
7203 + if (!empty($input['applicant_ids']) && empty($input['applicant_name']))
2734 7204 {
2735 7205 // This is an existing contact
2736 - if ( !is_array($_POST['applicant_ids']) )
7206 + foreach ( $input['applicant_ids'] as $applicant_id )
2737 7207 {
2738 - $_POST['applicant_ids'] = array($_POST['applicant_ids']);
7208 + $applicant_contact_ids[] = (int)$applicant_id;
2739 7209 }
2740 -
2741 - foreach ( $_POST['applicant_ids'] as $applicant_id )
2742 - {
2743 - $applicant_contact_ids[] = $applicant_id;
2744 - }
2745 7210 }
2746 7211
2747 7212 $applicant_contact_ids = array_unique($applicant_contact_ids);
2748 7213
@@ -2776,15 +7241,35 @@
2776 7241 echo json_encode( $return );
2777 7242 die();
2778 7243 }
2779 7244
2780 - $amount = preg_replace("/[^0-9]/", '', $_POST['amount']);
7245 + $amount = $input['amount'];
2781 7246
2782 - add_post_meta( $offer_post_id, '_offer_date_time', $_POST['offer_date'] . ' ' . $_POST['offer_time'] );
2783 - 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 );
2784 7249 add_post_meta( $offer_post_id, '_applicant_contact_id', $applicant_contact_id );
2785 7250 add_post_meta( $offer_post_id, '_amount', $amount );
2786 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 + }
2787 7272 }
2788 7273
2789 7274 $applicant_contacts = array();
2790 7275 foreach ( $applicant_contact_ids as $applicant_contact_id )
@@ -2814,10 +7299,16 @@
2814 7299 check_ajax_referer( 'record-offer', 'security' );
2815 7300
2816 7301 $this->json_headers();
2817 7302
2818 - // TO DO: Should do validation on server side also
2819 - 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)
2820 7311 {
2821 7312 $return = array('error' => 'No contact selected');
2822 7313 echo json_encode( $return );
2823 7314 die();
@@ -2822,9 +7313,9 @@
2822 7313 echo json_encode( $return );
2823 7314 die();
2824 7315 }
2825 7316
2826 - if (empty($_POST['property_ids']))
7317 + if (empty($input['property_ids']))
2827 7318 {
2828 7319 $return = array('error' => 'No property selected');
2829 7320 echo json_encode( $return );
2830 7321 die();
@@ -2831,9 +7322,9 @@
2831 7322 }
2832 7323
2833 7324 // Loop through contacts and create one offer each
2834 7325 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
2835 - foreach ( $_POST['property_ids'] as $property_id )
7326 + foreach ( $input['property_ids'] as $property_id )
2836 7327 {
2837 7328 // Insert offer record
2838 7329 $offer_post = array(
2839 7330 'post_title' => '',
@@ -2853,24 +7344,44 @@
2853 7344 echo json_encode( $return );
2854 7345 die();
2855 7346 }
2856 7347
2857 - $amount = preg_replace("/[^0-9]/", '', $_POST['amount']);
7348 + $amount = $input['amount'];
2858 7349
2859 - add_post_meta( $offer_post_id, '_offer_date_time', $_POST['offer_date'] . ' ' . $_POST['offer_time'] );
2860 - add_post_meta( $offer_post_id, '_property_id', $property_id );
2861 - 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 );
2862 7353 add_post_meta( $offer_post_id, '_amount', $amount );
2863 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 + }
2864 7375 }
2865 7376
2866 7377 $properties = array();
2867 - foreach ( $_POST['property_ids'] as $property_id )
7378 + foreach ( $input['property_ids'] as $property_id )
2868 7379 {
2869 7380 $properties[] = array(
2870 - 'ID' => $property_id,
2871 - 'post_title' => get_the_title($property_id),
2872 - '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, '' ),
2873 7384 );
2874 7385 }
2875 7386
2876 7387 $return = array('success' => array(
@@ -2887,12 +7398,18 @@
2887 7398 }
2888 7399
2889 7400 public function get_offer_details_meta_box()
2890 7401 {
7402 + global $post;
7403 +
2891 7404 check_ajax_referer( 'offer-details-meta-box', 'security' );
2892 7405
2893 - $offer = new PH_Offer((int)$_POST['offer_id']);
7406 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
2894 7407
7408 + $post = get_post( $post_id );
7409 +
7410 + $offer = new PH_Offer( $post_id );
7411 +
2895 7412 echo '<div class="propertyhive_meta_box">';
2896 7413
2897 7414 echo '<div class="options_group">';
2898 7415
@@ -2899,11 +7416,11 @@
2899 7416 if ( $offer->status != '' )
2900 7417 {
2901 7418 echo '<p class="form-field">
2902 7419
2903 - <label for="">' . __('Status', 'propertyhive') . '</label>
7420 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
2904 7421
2905 - ' . ucwords(str_replace("_", " ", $offer->status)) . '
7422 + ' . esc_html(propertyhive_get_status_label( $offer->status )) . '
2906 7423
2907 7424 </p>';
2908 7425 }
2909 7426
@@ -2909,32 +7426,32 @@
2909 7426
2910 7427 $offer_date_time = $offer->offer_date_time;
2911 7428 if ( empty($offer_date_time) )
2912 7429 {
2913 - $offer_date_time = date("Y-m-d H:i:s");
7430 + $offer_date_time = gmdate("Y-m-d H:i:s");
2914 7431 }
2915 7432
2916 7433 echo '<p class="form-field offer_date_time_field">
2917 7434
2918 - <label for="_offer_date">' . __('Offer Date / Time', 'propertyhive') . '</label>
7435 + <label for="_offer_date">' . esc_html(__('Offer Date / Time', 'propertyhive')) . '</label>
2919 7436
2920 - <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="">
2921 7438 <select id="_offer_time_hours" name="_offer_time_hours" class="select short" style="width:55px">';
2922 7439
2923 7440 if ( empty($offer_date_time) )
2924 7441 {
2925 - $value = date("H");
7442 + $value = gmdate("H");
2926 7443 }
2927 7444 else
2928 7445 {
2929 - $value = date( "H", strtotime( $offer_date_time ) );
7446 + $value = gmdate( "H", strtotime( $offer_date_time ) );
2930 7447 }
2931 7448 for ( $i = 0; $i < 23; ++$i )
2932 7449 {
2933 7450 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
2934 - echo '<option value="' . $j . '"';
7451 + echo '<option value="' . esc_attr($j) . '"';
2935 7452 if ($i == $value) { echo ' selected'; }
2936 - echo '>' . $j . '</option>';
7453 + echo '>' . esc_html($j) . '</option>';
2937 7454 }
2938 7455
2939 7456 echo '</select>
2940 7457 :
@@ -2945,16 +7462,16 @@
2945 7462 $value = '';
2946 7463 }
2947 7464 else
2948 7465 {
2949 - $value = date( "i", strtotime( $offer_date_time ) );
7466 + $value = gmdate( "i", strtotime( $offer_date_time ) );
2950 7467 }
2951 7468 for ( $i = 0; $i < 60; $i+=5 )
2952 7469 {
2953 7470 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
2954 - echo '<option value="' . $j . '"';
7471 + echo '<option value="' . esc_attr($j) . '"';
2955 7472 if ($i == $value) { echo ' selected'; }
2956 - echo '>' . $j . '</option>';
7473 + echo '>' . esc_html($j) . '</option>';
2957 7474 }
2958 7475
2959 7476 echo '</select>
2960 7477
@@ -2964,9 +7481,9 @@
2964 7481 'id' => '_amount',
2965 7482 'label' => __( 'Offer Amount', 'propertyhive' ) . ' (&pound;)',
2966 7483 'desc_tip' => false,
2967 7484 'class' => 'short',
2968 - 'value' => ( is_numeric($offer->amount) ? number_format($offer->amount) : '' ),
7485 + 'value' => ( is_numeric($offer->amount) ? ph_display_price_field( $offer->amount ) : '' ),
2969 7486 'custom_attributes' => array(
2970 7487 //'style' => 'width:95%; max-width:500px;'
2971 7488 )
2972 7489 );
@@ -2984,12 +7501,28 @@
2984 7501 public function get_offer_actions()
2985 7502 {
2986 7503 check_ajax_referer( 'offer-actions', 'security' );
2987 7504
2988 - $post_id = $_POST['offer_id'];
7505 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
2989 7506
2990 7507 $status = get_post_meta( $post_id, '_status', TRUE );
2991 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 +
2992 7525 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_offer_actions_meta_box">
2993 7526
2994 7527 <div class="options_group" style="padding-top:8px;">';
2995 7528
@@ -3000,14 +7533,19 @@
3000 7533 $actions[] = '<a
3001 7534 href="#action_panel_offer_accepted"
3002 7535 class="button button-success offer-action"
3003 7536 style="width:100%; margin-bottom:7px; text-align:center"
3004 - >' . __('Accept Offer', 'propertyhive') . '</a>';
7537 + >' . wp_kses_post( __('Accept Offer', 'propertyhive') ) . '</a>';
3005 7538 $actions[] = '<a
3006 7539 href="#action_panel_offer_declined"
3007 7540 class="button button-danger offer-action"
3008 7541 style="width:100%; margin-bottom:7px; text-align:center"
3009 - >' . __('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>';
3010 7548 }
3011 7549
3012 7550 if ( $status == 'accepted' )
3013 7551 {
@@ -3020,46 +7558,49 @@
3020 7558
3021 7559 if ( $sale_id != '' )
3022 7560 {
3023 7561 $actions[] = '<a
3024 - href="' . get_edit_post_link( $sale_id, '' ) . '"
7562 + href="' . esc_url(get_edit_post_link( $sale_id, '' )) . '"
3025 7563 class="button"
3026 7564 style="width:100%; margin-bottom:7px; text-align:center"
3027 - >' . __('View Sale', 'propertyhive') . '</a>';
7565 + >' . wp_kses_post( __('View Sale', 'propertyhive') ) . '</a>';
3028 7566 }
3029 7567 else
3030 7568 {
3031 7569 $actions[] = '<a
3032 - href="' . wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), '1', 'create_sale' ) . '"
3033 - 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"
3034 7572 style="width:100%; margin-bottom:7px; text-align:center"
3035 - >' . __('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>';
3036 7580 }
3037 7581 }
3038 7582
3039 - if ( $status == 'declined' )
7583 + if ( $status == 'accepted' || $status == 'declined' || $status == 'withdrawn' )
3040 7584 {
3041 -
3042 - }
3043 -
3044 - if ( $status == 'accepted' || $status == 'declined' )
3045 - {
3046 7585 $actions[] = '<a
3047 7586 href="#action_panel_offer_revert_pending"
3048 7587 class="button offer-action"
3049 7588 style="width:100%; margin-bottom:7px; text-align:center"
3050 - >' . __('Revert To Pending', 'propertyhive') . '</a>';
7589 + >' . wp_kses_post( __('Revert To Pending', 'propertyhive') ) . '</a>';
3051 7590 }
3052 7591
3053 - $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 );
3054 7594
3055 7595 if ( !empty($actions) )
3056 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.
3057 7598 echo implode("", $actions);
3058 7599 }
3059 7600 else
3060 7601 {
3061 - 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>';
3062 7603 }
3063 7604
3064 7605 echo '</div>
3065 7606
@@ -3071,9 +7612,9 @@
3071 7612 public function offer_accepted()
3072 7613 {
3073 7614 check_ajax_referer( 'offer-actions', 'security' );
3074 7615
3075 - $post_id = $_POST['offer_id'];
7616 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
3076 7617
3077 7618 $status = get_post_meta( $post_id, '_status', TRUE );
3078 7619
3079 7620 if ( $status == 'pending' )
@@ -3079,10 +7620,8 @@
3079 7620 if ( $status == 'pending' )
3080 7621 {
3081 7622 update_post_meta( $post_id, '_status', 'accepted' );
3082 7623
3083 - $current_user = wp_get_current_user();
3084 -
3085 7624 // Add note/comment to offer
3086 7625 $comment = array(
3087 7626 'note_type' => 'action',
3088 7627 'action' => 'offer_accepted',
@@ -3087,22 +7626,14 @@
3087 7626 'note_type' => 'action',
3088 7627 'action' => 'offer_accepted',
3089 7628 );
3090 7629
3091 - $data = array(
3092 - 'comment_post_ID' => $post_id,
3093 - 'comment_author' => $current_user->display_name,
3094 - 'comment_author_email' => 'propertyhive@noreply.com',
3095 - 'comment_author_url' => '',
3096 - 'comment_date' => date("Y-m-d H:i:s"),
3097 - 'comment_content' => serialize($comment),
3098 - 'comment_approved' => 1,
3099 - 'comment_type' => 'propertyhive_note',
3100 - );
3101 - $comment_id = wp_insert_comment( $data );
7630 + PH_Comments::insert_note( $post_id, $comment );
7631 +
7632 + wp_send_json_success();
3102 7633 }
3103 7634
3104 - die();
7635 + wp_send_json_error();
3105 7636 }
3106 7637
3107 7638 public function offer_declined()
3108 7639 {
@@ -3107,9 +7638,9 @@
3107 7638 public function offer_declined()
3108 7639 {
3109 7640 check_ajax_referer( 'offer-actions', 'security' );
3110 7641
3111 - $post_id = $_POST['offer_id'];
7642 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
3112 7643
3113 7644 $status = get_post_meta( $post_id, '_status', TRUE );
3114 7645
3115 7646 if ( $status == 'pending' )
@@ -3115,10 +7646,8 @@
3115 7646 if ( $status == 'pending' )
3116 7647 {
3117 7648 update_post_meta( $post_id, '_status', 'declined' );
3118 7649
3119 - $current_user = wp_get_current_user();
3120 -
3121 7650 // Add note/comment to offer
3122 7651 $comment = array(
3123 7652 'note_type' => 'action',
3124 7653 'action' => 'offer_declined',
@@ -3123,236 +7652,105 @@
3123 7652 'note_type' => 'action',
3124 7653 'action' => 'offer_declined',
3125 7654 );
3126 7655
3127 - $data = array(
3128 - 'comment_post_ID' => $post_id,
3129 - 'comment_author' => $current_user->display_name,
3130 - 'comment_author_email' => 'propertyhive@noreply.com',
3131 - 'comment_author_url' => '',
3132 - 'comment_date' => date("Y-m-d H:i:s"),
3133 - 'comment_content' => serialize($comment),
3134 - 'comment_approved' => 1,
3135 - 'comment_type' => 'propertyhive_note',
3136 - );
3137 - $comment_id = wp_insert_comment( $data );
7656 + PH_Comments::insert_note( $post_id, $comment );
7657 +
7658 + wp_send_json_success();
3138 7659 }
3139 7660
3140 - die();
7661 + wp_send_json_error();
3141 7662 }
3142 7663
3143 - public function offer_revert_pending()
7664 + public function offer_withdrawn()
3144 7665 {
3145 7666 check_ajax_referer( 'offer-actions', 'security' );
3146 7667
3147 - $post_id = $_POST['offer_id'];
7668 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
3148 7669
3149 7670 $status = get_post_meta( $post_id, '_status', TRUE );
3150 7671
3151 - if ( $status == 'accepted' || $status == 'declined' )
7672 + if ( $status == 'pending' || $status == 'accepted' )
3152 7673 {
3153 - update_post_meta( $post_id, '_status', 'pending' );
7674 + update_post_meta( $post_id, '_status', 'withdrawn' );
3154 7675
3155 - $current_user = wp_get_current_user();
3156 -
3157 7676 // Add note/comment to offer
3158 7677 $comment = array(
3159 7678 'note_type' => 'action',
3160 - 'action' => 'offer_revert_pending',
7679 + 'action' => 'offer_withdrawn',
3161 7680 );
3162 7681
3163 - $data = array(
3164 - 'comment_post_ID' => $post_id,
3165 - 'comment_author' => $current_user->display_name,
3166 - 'comment_author_email' => 'propertyhive@noreply.com',
3167 - 'comment_author_url' => '',
3168 - 'comment_date' => date("Y-m-d H:i:s"),
3169 - 'comment_content' => serialize($comment),
3170 - 'comment_approved' => 1,
3171 - 'comment_type' => 'propertyhive_note',
3172 - );
3173 - $comment_id = wp_insert_comment( $data );
7682 + PH_Comments::insert_note( $post_id, $comment );
7683 +
7684 + wp_send_json_success();
3174 7685 }
3175 7686
3176 - die();
7687 + wp_send_json_error();
3177 7688 }
3178 7689
3179 - public function get_property_offers_meta_box()
7690 + public function offer_revert_pending()
3180 7691 {
3181 - check_ajax_referer( 'get_property_offers_meta_box', 'security' );
7692 + check_ajax_referer( 'offer-actions', 'security' );
3182 7693
3183 - global $post;
7694 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
3184 7695
3185 - echo '<div class="propertyhive_meta_box">';
3186 -
3187 - echo '<div class="options_group">';
7696 + $status = get_post_meta( $post_id, '_status', TRUE );
3188 7697
3189 - $args = array(
3190 - 'post_type' => 'offer',
3191 - 'nopaging' => true,
3192 - 'orderby' => 'meta_value',
3193 - 'order' => 'DESC',
3194 - 'meta_key' => '_offer_date_time',
3195 - 'post_status' => 'publish',
3196 - 'meta_query' => array(
3197 - array(
3198 - 'key' => '_property_id',
3199 - 'value' => $_POST['post_id']
3200 - )
3201 - )
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',
3202 7706 );
3203 - $offers_query = new WP_Query( $args );
3204 7707
3205 - if ( $offers_query->have_posts() )
3206 - {
3207 - echo '<table style="width:100%">
3208 - <thead>
3209 - <tr>
3210 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
3211 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
3212 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
3213 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3214 - </tr>
3215 - </thead>
3216 - <tbody>';
7708 + PH_Comments::insert_note( $post_id, $comment );
3217 7709
3218 - while ( $offers_query->have_posts() )
3219 - {
3220 - $offers_query->the_post();
7710 + wp_send_json_success();
7711 + }
3221 7712
3222 - $offer = new PH_Offer(get_the_ID());
7713 + wp_send_json_error();
7714 + }
3223 7715
3224 - echo '<tr>';
3225 - 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>';
3226 - 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>';
3227 - echo '<td style="text-align:left;">' . $offer->get_formatted_amount() . '</td>';
3228 - echo '<td style="text-align:left;">';
3229 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
3230 - echo ucwords(str_replace("_", " ", $status));
3231 - echo '</td>';
3232 - echo '</tr>';
3233 - }
7716 + public function get_property_offers_meta_box()
7717 + {
7718 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
3234 7719
3235 - echo '
3236 - </tbody>
3237 - </table>
3238 - <br>';
3239 - }
3240 - else
3241 - {
3242 - echo '<p>' . __( 'No offers exist for this property', 'propertyhive') . '</p>';
3243 - }
3244 - 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 + }
3245 7727
7728 + include( PH()->plugin_path() . '/includes/admin/views/html-property-offers-meta-box.php' );
7729 +
3246 7730 do_action('propertyhive_property_offers_fields');
3247 -
3248 - echo '</div>';
3249 -
3250 - echo '</div>';
3251 7731
7732 + // Quit out
3252 7733 die();
3253 7734 }
3254 7735
3255 7736 public function get_contact_offers_meta_box()
3256 7737 {
3257 - check_ajax_referer( 'get_contact_offers_meta_box', 'security' );
7738 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
3258 7739
3259 - 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 + }
3260 7747
3261 - echo '<div class="propertyhive_meta_box">';
3262 -
3263 - echo '<div class="options_group">';
7748 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-offers-meta-box.php' );
3264 7749
3265 - $args = array(
3266 - 'post_type' => 'offer',
3267 - 'nopaging' => true,
3268 - 'orderby' => 'meta_value',
3269 - 'order' => 'DESC',
3270 - 'post_status' => 'publish',
3271 - 'meta_key' => '_offer_date_time',
3272 - 'meta_query' => array(
3273 - array(
3274 - 'key' => '_applicant_contact_id',
3275 - 'value' => $_POST['post_id']
3276 - )
3277 - )
3278 - );
3279 - $offers_query = new WP_Query( $args );
3280 -
3281 - if ( $offers_query->have_posts() )
3282 - {
3283 - echo '<table style="width:100%">
3284 - <thead>
3285 - <tr>
3286 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
3287 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
3288 - <th style="text-align:left;">' . __( 'Property Owner', 'propertyhive' ) . '</th>
3289 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
3290 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3291 - </tr>
3292 - </thead>
3293 - <tbody>';
3294 -
3295 - while ( $offers_query->have_posts() )
3296 - {
3297 - $offers_query->the_post();
3298 -
3299 - $property = new PH_Property((int)get_post_meta(get_the_ID(), '_property_id', TRUE));
3300 - $offer = new PH_Offer(get_the_ID());
3301 -
3302 - echo '<tr>';
3303 - 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>';
3304 - 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>';
3305 - echo '<td style="text-align:left;">';
3306 -
3307 - $owner_contact_ids = $property->_owner_contact_id;
3308 - if (
3309 - ( !is_array($owner_contact_ids) && $owner_contact_ids != '' && $owner_contact_ids != 0 )
3310 - ||
3311 - ( is_array($owner_contact_ids) && !empty($owner_contact_ids) )
3312 - )
3313 - {
3314 - if ( !is_array($owner_contact_ids) )
3315 - {
3316 - $owner_contact_ids = array($owner_contact_ids);
3317 - }
3318 -
3319 - foreach ( $owner_contact_ids as $owner_contact_id )
3320 - {
3321 - echo get_the_title($owner_contact_id) . '<br>';
3322 - echo '<div style="color:#BBB">';
3323 - echo 'T: ' . get_post_meta($owner_contact_id, '_telephone_number', TRUE) . '<br>';
3324 - echo 'E: ' . get_post_meta($owner_contact_id, '_email_address', TRUE);
3325 - echo '</div>';
3326 - }
3327 - }
3328 -
3329 - echo '</td>';
3330 - echo '<td style="text-align:left;">' . $offer->get_formatted_amount() . '</td>';
3331 - echo '<td style="text-align:left;">';
3332 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
3333 - echo ucwords(str_replace("_", " ", $status));
3334 - echo '</td>';
3335 - echo '</tr>';
3336 - }
3337 -
3338 - echo '
3339 - </tbody>
3340 - </table>
3341 - <br>';
3342 - }
3343 - else
3344 - {
3345 - echo '<p>' . __( 'No offers exist for this contact', 'propertyhive') . '</p>';
3346 - }
3347 - wp_reset_postdata();
3348 -
3349 7750 do_action('propertyhive_contact_offers_fields');
3350 -
3351 - echo '</div>';
3352 -
3353 - echo '</div>';
3354 7751
7752 + // Quit out
3355 7753 die();
3356 7754 }
3357 7755
3358 7756 // Sale related functions
@@ -3357,12 +7755,18 @@
3357 7755
3358 7756 // Sale related functions
3359 7757 public function get_sale_details_meta_box()
3360 7758 {
7759 + global $post;
7760 +
3361 7761 check_ajax_referer( 'sale-details-meta-box', 'security' );
3362 7762
3363 - $sale = new PH_Offer((int)$_POST['sale_id']);
7763 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
3364 7764
7765 + $post = get_post( $post_id );
7766 +
7767 + $sale = new PH_Offer( $post_id );
7768 +
3365 7769 echo '<div class="propertyhive_meta_box">';
3366 7770
3367 7771 echo '<div class="options_group">';
3368 7772
@@ -3369,11 +7773,11 @@
3369 7773 if ( $sale->status != '' )
3370 7774 {
3371 7775 echo '<p class="form-field">
3372 7776
3373 - <label for="">' . __('Status', 'propertyhive') . '</label>
7777 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
3374 7778
3375 - ' . ucwords(str_replace("_", " ", $sale->status)) . '
7779 + ' . esc_html(propertyhive_get_status_label( $sale->status )) . '
3376 7780
3377 7781 </p>';
3378 7782 }
3379 7783
@@ -3379,17 +7783,17 @@
3379 7783
3380 7784 $sale_date_time = $sale->sale_date_time;
3381 7785 if ( empty($sale_date_time) )
3382 7786 {
3383 - $sale_date_time = date("Y-m-d H:i:s");
7787 + $sale_date_time = gmdate("Y-m-d H:i:s");
3384 7788 }
3385 7789
3386 7790 echo '<p class="form-field sale_date_field">
3387 7791
3388 - <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="">
3389 7795
3390 - <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)) . '">
3391 -
3392 7796 </p>';
3393 7797
3394 7798 $args = array(
3395 7799 'id' => '_amount',
@@ -3395,9 +7799,9 @@
3395 7799 'id' => '_amount',
3396 7800 'label' => __( 'Sale Amount', 'propertyhive' ) . ' (&pound;)',
3397 7801 'desc_tip' => false,
3398 7802 'class' => 'short',
3399 - 'value' => ( is_numeric($sale->amount) ? number_format($sale->amount) : '' ),
7803 + 'value' => ( is_numeric($sale->amount) ? ph_display_price_field( $sale->amount ) : '' ),
3400 7804 'custom_attributes' => array(
3401 7805 //'style' => 'width:95%; max-width:500px;'
3402 7806 )
3403 7807 );
@@ -3415,12 +7819,28 @@
3415 7819 public function get_sale_actions()
3416 7820 {
3417 7821 check_ajax_referer( 'sale-actions', 'security' );
3418 7822
3419 - $post_id = $_POST['sale_id'];
7823 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
3420 7824
3421 7825 $status = get_post_meta( $post_id, '_status', TRUE );
3422 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 +
3423 7843 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_sale_actions_meta_box">
3424 7844
3425 7845 <div class="options_group" style="padding-top:8px;">';
3426 7846
@@ -3431,9 +7851,9 @@
3431 7851 $actions[] = '<a
3432 7852 href="#action_panel_sale_exchanged"
3433 7853 class="button button-success sale-action"
3434 7854 style="width:100%; margin-bottom:7px; text-align:center"
3435 - >' . __('Sale Exchanged', 'propertyhive') . '</a>';
7855 + >' . esc_html(__('Sale Exchanged', 'propertyhive')) . '</a>';
3436 7856
3437 7857 }
3438 7858
3439 7859 if ( $status == 'exchanged' )
@@ -3441,9 +7861,9 @@
3441 7861 $actions[] = '<a
3442 7862 href="#action_panel_sale_completed"
3443 7863 class="button button-success sale-action"
3444 7864 style="width:100%; margin-bottom:7px; text-align:center"
3445 - >' . __('Sale Completed', 'propertyhive') . '</a>';
7865 + >' . esc_html(__('Sale Completed', 'propertyhive')) . '</a>';
3446 7866 }
3447 7867
3448 7868 if ( $status == 'completed' )
3449 7869 {
@@ -3455,20 +7875,22 @@
3455 7875 $actions[] = '<a
3456 7876 href="#action_panel_sale_fallen_through"
3457 7877 class="button sale-action"
3458 7878 style="width:100%; margin-bottom:7px; text-align:center"
3459 - >' . __('Sale Fallen Through', 'propertyhive') . '</a>';
7879 + >' . esc_html(__('Sale Fallen Through', 'propertyhive')) . '</a>';
3460 7880 }
3461 7881
3462 - $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 );
3463 7884
3464 7885 if ( !empty($actions) )
3465 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.
3466 7888 echo implode("", $actions);
3467 7889 }
3468 7890 else
3469 7891 {
3470 - 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>';
3471 7893 }
3472 7894
3473 7895 echo '</div>
3474 7896
@@ -3480,9 +7902,9 @@
3480 7902 public function sale_exchanged()
3481 7903 {
3482 7904 check_ajax_referer( 'sale-actions', 'security' );
3483 7905
3484 - $post_id = $_POST['sale_id'];
7906 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
3485 7907
3486 7908 $status = get_post_meta( $post_id, '_status', TRUE );
3487 7909
3488 7910 if ( $status == 'current' )
@@ -3488,10 +7910,8 @@
3488 7910 if ( $status == 'current' )
3489 7911 {
3490 7912 update_post_meta( $post_id, '_status', 'exchanged' );
3491 7913
3492 - $current_user = wp_get_current_user();
3493 -
3494 7914 // Add note/comment to sale
3495 7915 $comment = array(
3496 7916 'note_type' => 'action',
3497 7917 'action' => 'sale_exchanged',
@@ -3496,22 +7916,14 @@
3496 7916 'note_type' => 'action',
3497 7917 'action' => 'sale_exchanged',
3498 7918 );
3499 7919
3500 - $data = array(
3501 - 'comment_post_ID' => $post_id,
3502 - 'comment_author' => $current_user->display_name,
3503 - 'comment_author_email' => 'propertyhive@noreply.com',
3504 - 'comment_author_url' => '',
3505 - 'comment_date' => date("Y-m-d H:i:s"),
3506 - 'comment_content' => serialize($comment),
3507 - 'comment_approved' => 1,
3508 - 'comment_type' => 'propertyhive_note',
3509 - );
3510 - $comment_id = wp_insert_comment( $data );
7920 + PH_Comments::insert_note( $post_id, $comment );
7921 +
7922 + wp_send_json_success();
3511 7923 }
3512 7924
3513 - die();
7925 + wp_send_json_error();
3514 7926 }
3515 7927
3516 7928 public function sale_completed()
3517 7929 {
@@ -3516,9 +7928,9 @@
3516 7928 public function sale_completed()
3517 7929 {
3518 7930 check_ajax_referer( 'sale-actions', 'security' );
3519 7931
3520 - $post_id = $_POST['sale_id'];
7932 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
3521 7933
3522 7934 $status = get_post_meta( $post_id, '_status', TRUE );
3523 7935
3524 7936 if ( $status == 'exchanged' )
@@ -3524,10 +7936,8 @@
3524 7936 if ( $status == 'exchanged' )
3525 7937 {
3526 7938 update_post_meta( $post_id, '_status', 'completed' );
3527 7939
3528 - $current_user = wp_get_current_user();
3529 -
3530 7940 // Add note/comment to sale
3531 7941 $comment = array(
3532 7942 'note_type' => 'action',
3533 7943 'action' => 'sale_completed',
@@ -3532,22 +7942,14 @@
3532 7942 'note_type' => 'action',
3533 7943 'action' => 'sale_completed',
3534 7944 );
3535 7945
3536 - $data = array(
3537 - 'comment_post_ID' => $post_id,
3538 - 'comment_author' => $current_user->display_name,
3539 - 'comment_author_email' => 'propertyhive@noreply.com',
3540 - 'comment_author_url' => '',
3541 - 'comment_date' => date("Y-m-d H:i:s"),
3542 - 'comment_content' => serialize($comment),
3543 - 'comment_approved' => 1,
3544 - 'comment_type' => 'propertyhive_note',
3545 - );
3546 - $comment_id = wp_insert_comment( $data );
7946 + PH_Comments::insert_note( $post_id, $comment );
7947 +
7948 + wp_send_json_success();
3547 7949 }
3548 7950
3549 - die();
7951 + wp_send_json_error();
3550 7952 }
3551 7953
3552 7954 public function sale_fallen_through()
3553 7955 {
@@ -3552,9 +7954,9 @@
3552 7954 public function sale_fallen_through()
3553 7955 {
3554 7956 check_ajax_referer( 'sale-actions', 'security' );
3555 7957
3556 - $post_id = $_POST['sale_id'];
7958 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
3557 7959
3558 7960 $status = get_post_meta( $post_id, '_status', TRUE );
3559 7961
3560 7962 if ( $status == 'current' || $status == 'exchanged' )
@@ -3560,10 +7962,8 @@
3560 7962 if ( $status == 'current' || $status == 'exchanged' )
3561 7963 {
3562 7964 update_post_meta( $post_id, '_status', 'fallen_through' );
3563 7965
3564 - $current_user = wp_get_current_user();
3565 -
3566 7966 // Add note/comment to sale
3567 7967 $comment = array(
3568 7968 'note_type' => 'action',
3569 7969 'action' => 'sale_fallen_through',
@@ -3568,202 +7968,736 @@
3568 7968 'note_type' => 'action',
3569 7969 'action' => 'sale_fallen_through',
3570 7970 );
3571 7971
3572 - $data = array(
3573 - 'comment_post_ID' => $post_id,
3574 - 'comment_author' => $current_user->display_name,
3575 - 'comment_author_email' => 'propertyhive@noreply.com',
3576 - 'comment_author_url' => '',
3577 - 'comment_date' => date("Y-m-d H:i:s"),
3578 - 'comment_content' => serialize($comment),
3579 - 'comment_approved' => 1,
3580 - 'comment_type' => 'propertyhive_note',
3581 - );
3582 - $comment_id = wp_insert_comment( $data );
7972 + PH_Comments::insert_note( $post_id, $comment );
7973 +
7974 + wp_send_json_success();
3583 7975 }
3584 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
3585 7997 die();
3586 7998 }
3587 7999
3588 - public function get_property_sales_meta_box()
8000 + public function get_contact_sales_meta_box()
3589 8001 {
3590 - check_ajax_referer( 'get_property_sales_meta_box', 'security' );
8002 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
3591 8003
3592 - 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 + }
3593 8011
3594 - echo '<div class="propertyhive_meta_box">';
3595 -
3596 - echo '<div class="options_group">';
8012 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-sales-meta-box.php' );
3597 8013
3598 - $args = array(
3599 - 'post_type' => 'sale',
3600 - 'nopaging' => true,
3601 - 'orderby' => 'meta_value',
3602 - 'order' => 'DESC',
3603 - 'meta_key' => '_sale_date_time',
3604 - 'post_status' => 'publish',
3605 - 'meta_query' => array(
3606 - array(
3607 - 'key' => '_property_id',
3608 - 'value' => $_POST['post_id']
3609 - )
3610 - )
3611 - );
3612 - $sales_query = new WP_Query( $args );
8014 + do_action('propertyhive_contact_sales_fields');
3613 8015
3614 - 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 )
3615 8167 {
3616 - echo '<table style="width:100%">
3617 - <thead>
3618 - <tr>
3619 - <th style="text-align:left;">' . __( 'Sale Date', 'propertyhive' ) . '</th>
3620 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
3621 - <th style="text-align:left;">' . __( 'Sale Amount', 'propertyhive' ) . '</th>
3622 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3623 - </tr>
3624 - </thead>
3625 - <tbody>';
8168 + list($key, $value) = explode('=', $key_value_pair);
8169 + $recurrence[strtolower($key)] = $value;
8170 + }
3626 8171
3627 - 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'] )
3628 8176 {
3629 - $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 + }
3630 8190
3631 - $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 + }
3632 8198
3633 - echo '<tr>';
3634 - 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>';
3635 - 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>';
3636 - echo '<td style="text-align:left;">' . $sale->get_formatted_amount() . '</td>';
3637 - echo '<td style="text-align:left;">';
3638 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
3639 - echo ucwords(str_replace("_", " ", $status));
3640 - echo '</td>';
3641 - echo '</tr>';
3642 - }
8199 + echo esc_html($next_key_date);
3643 8200
3644 - echo '
3645 - </tbody>
3646 - </table>
3647 - <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 );
3648 8222 }
3649 - 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 )
3650 8260 {
3651 - 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();
3652 8264 }
3653 - wp_reset_postdata();
3654 8265
3655 - do_action('propertyhive_property_sales_fields');
3656 -
3657 - echo '</div>';
3658 -
3659 - 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'] ) );
3660 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 +
3661 8279 die();
3662 8280 }
3663 8281
3664 - public function get_contact_sales_meta_box()
8282 + public function delete_key_date()
3665 8283 {
3666 - check_ajax_referer( 'get_contact_sales_meta_box', 'security' );
8284 + check_ajax_referer( 'delete-key-date', 'security' );
3667 8285
3668 - global $post;
8286 + $this->json_headers();
3669 8287
3670 - 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 + }
3671 8412
3672 - echo '<div class="options_group">';
8413 + // check plugin status
8414 + $slug = isset( $_POST['slug'] ) && is_string( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
3673 8415
3674 - $args = array(
3675 - 'post_type' => 'sale',
3676 - 'nopaging' => true,
3677 - 'orderby' => 'meta_value',
3678 - 'order' => 'DESC',
3679 - 'post_status' => 'publish',
3680 - 'meta_key' => '_sale_date_time',
3681 - 'meta_query' => array(
3682 - array(
3683 - 'key' => '_applicant_contact_id',
3684 - 'value' => $_POST['post_id']
3685 - )
3686 - )
8416 + $feature = get_ph_pro_feature( $slug );
8417 +
8418 + if ( $feature === false )
8419 + {
8420 + $return = array(
8421 + 'errorMessage' => 'Feature not found'
3687 8422 );
3688 - $sales_query = new WP_Query( $args );
8423 + wp_send_json_error($return);
8424 + }
3689 8425
3690 - if ( $sales_query->have_posts() )
3691 - {
3692 - echo '<table style="width:100%">
3693 - <thead>
3694 - <tr>
3695 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
3696 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
3697 - <th style="text-align:left;">' . __( 'Property Owner', 'propertyhive' ) . '</th>
3698 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
3699 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3700 - </tr>
3701 - </thead>
3702 - <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 + }
3703 8433
3704 - while ( $sales_query->have_posts() )
3705 - {
3706 - $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 + }
3707 8440
3708 - $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;
3709 8445
3710 - $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 + }
3711 8457
3712 - echo '<tr>';
3713 - 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>';
3714 - 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>';
3715 - 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();
3716 8464
3717 - $owner_contact_ids = $property->_owner_contact_id;
8465 + if ( isset($product_id_and_package['success']) && $product_id_and_package['success'] === true )
8466 + {
3718 8467 if (
3719 - ( !is_array($owner_contact_ids) && $owner_contact_ids != '' && $owner_contact_ids != 0 )
3720 - ||
3721 - ( 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'])
3722 8471 )
3723 8472 {
3724 - if ( !is_array($owner_contact_ids) )
3725 - {
3726 - $owner_contact_ids = array($owner_contact_ids);
3727 - }
3728 -
3729 - foreach ( $owner_contact_ids as $owner_contact_id )
3730 - {
3731 - echo get_the_title($owner_contact_id) . '<br>';
3732 - echo '<div style="color:#BBB">';
3733 - echo 'T: ' . get_post_meta($owner_contact_id, '_telephone_number', TRUE) . '<br>';
3734 - echo 'E: ' . get_post_meta($owner_contact_id, '_email_address', TRUE);
3735 - echo '</div>';
3736 - }
8473 + $valid_license_key = true;
3737 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 + }
3738 8499
3739 - echo '</td>';
3740 - echo '<td style="text-align:left;">' . $sale->get_formatted_amount() . '</td>';
3741 - echo '<td style="text-align:left;">';
3742 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
3743 - echo ucwords(str_replace("_", " ", $status));
3744 - echo '</td>';
3745 - echo '</tr>';
3746 - }
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 + }
3747 8508
3748 - echo '
3749 - </tbody>
3750 - </table>
3751 - <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 + );
3752 8522 }
3753 8523 else
3754 8524 {
3755 - 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 + );
3756 8543 }
3757 - wp_reset_postdata();
3758 8544
3759 - 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
3760 8562
3761 - echo '</div>';
3762 -
3763 - 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 + }
3764 8570
3765 - 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();
3766 8700 }
3767 8701 }
3768 8702
3769 8703 new PH_AJAX();