PluginProbe
Property Hive / 2.4.0
Property Hive v2.4.0
2.4.0 2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 All 262 releases
← All changes | includes/class-ph-ajax.php +5077 -1704 1.4.48 → 2.4.0 View file →
@@ -1,6 +1,9 @@
1 1 <?php
2 +// phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 +// ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
2 4
5 +
3 6 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
4 7
5 8 /**
6 9 * PropertyHive PH_AJAX
@@ -12,8 +15,9 @@
12 15 * @package PropertyHive/Classes
13 16 * @category Class
14 17 * @author PropertyHive
15 18 */
19 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_AJAX; preserving the existing PH_* class name is required for plugin and extension compatibility.
16 20 class PH_AJAX {
17 21
18 22 /**
19 23 * Hook into ajax events
@@ -23,8 +27,12 @@
23 27 // propertyhive_EVENT => nopriv
24 28 $ajax_events = array(
25 29 'add_note' => false,
26 30 'delete_note' => false,
31 + 'toggle_note_pinned' => false,
32 + 'get_notes_grid' => false,
33 + 'get_pinned_notes_grid' => false,
34 + 'fetch_note_mentions' => false,
27 35 'search_contacts' => false,
28 36 'search_properties' => false,
29 37 'search_negotiators' => false,
30 38 'load_existing_owner_contact' => false,
@@ -30,16 +38,26 @@
30 38 'load_existing_owner_contact' => false,
31 39 'load_existing_features' => false,
32 40 'make_property_enquiry' => true,
33 41 'create_contact_from_enquiry' => false,
42 + 'merge_contact_records' => false,
34 43
35 44 // Dashboard components
36 45 'get_news' => false,
37 46 'get_viewings_awaiting_applicant_feedback' => false,
38 47 'get_my_upcoming_appointments' => false,
48 + 'get_upcoming_overdue_key_dates' => false,
39 49
50 + // Property actions
51 + 'check_duplicate_reference_number' => false,
52 + 'osm_geocoding_request' => false,
53 + 'get_property_marketing_statistics_meta_box' => false,
54 + 'get_property_tenancies_grid' => false,
55 +
40 56 // Contact actions
41 57 'create_contact_login' => false,
58 + 'get_contact_tenancies_grid' => false,
59 + 'get_contact_solicitor' => false,
42 60
43 61 // Appraisal actions
44 62 'get_appraisal_details_meta_box' => false,
45 63 'get_appraisal_actions' => false,
@@ -47,8 +65,9 @@
47 65 'appraisal_cancelled' => false,
48 66 'appraisal_won' => false,
49 67 'appraisal_lost_reason' => false,
50 68 'appraisal_instructed' => false,
69 + 'appraisal_email_owner_booking_confirmation' => false,
51 70 'appraisal_revert_pending' => false,
52 71 'appraisal_revert_carried_out' => false,
53 72 'appraisal_revert_won' => false,
54 73
@@ -56,12 +75,18 @@
56 75 'book_viewing_property' => false,
57 76 'book_viewing_contact' => false,
58 77 'get_viewing_details_meta_box' => false,
59 78 'get_viewing_actions' => false,
79 + 'get_viewing_lightbox' => false,
60 80 'viewing_carried_out' => false,
61 81 'viewing_cancelled' => false,
82 + 'viewing_no_show' => false,
62 83 'viewing_email_applicant_booking_confirmation' => false,
63 84 'viewing_email_owner_booking_confirmation' => false,
85 + 'viewing_email_attending_negotiator_booking_confirmation' => false,
86 + 'viewing_email_applicant_cancellation_notification' => false,
87 + 'viewing_email_owner_cancellation_notification' => false,
88 + 'viewing_email_attending_negotiator_cancellation_notification' => false,
64 89 'viewing_interested_feedback' => false,
65 90 'viewing_not_interested_feedback' => false,
66 91 'viewing_feedback_not_required' => false,
67 92 'viewing_revert_feedback_pending' => false,
@@ -77,8 +102,9 @@
77 102 'get_offer_actions' => false,
78 103 'get_property_offers_meta_box' => false,
79 104 'offer_accepted' => false,
80 105 'offer_declined' => false,
106 + 'offer_withdrawn' => false,
81 107 'offer_revert_pending' => false,
82 108 'get_contact_offers_meta_box' => false,
83 109
84 110 // Sale actions
@@ -91,21 +117,53 @@
91 117 'offer_declined' => false,
92 118 'get_property_sales_meta_box' => false,
93 119 'get_contact_sales_meta_box' => false,
94 120
121 + // Enquiry actions
122 + 'get_property_enquiries_meta_box' => false,
123 + 'get_contact_enquiries_meta_box' => false,
124 +
125 + // Tenancy actions
126 + 'add_key_date' => false,
127 + 'get_management_dates_grid' => false,
128 + 'get_key_dates_quick_edit_row' => false,
129 + 'check_key_date_recurrence' => false,
130 + 'save_key_date' => false,
131 + 'delete_key_date' => false,
132 +
95 133 'validate_save_contact' => false,
96 134 'applicant_registration' => true,
97 135 'login' => true,
136 + 'lost_password' => true,
137 + 'reset_password' => true,
98 138 'save_account_details' => true,
99 139 'save_account_requirements' => true,
100 140
141 + // Dismissing notices
101 142 'dismiss_notice_leave_review' => false,
143 + 'dismiss_notice_retired_template_assistant' => false,
144 + 'dismiss_notice_demo_data' => false,
145 + 'dismiss_notice_epl' => false,
102 146 'dismiss_notice_missing_search_results' => false,
103 147 'dismiss_notice_missing_google_maps_api_key' => false,
104 148 'dismiss_notice_invalid_expired_license_key' => false,
149 + 'dismiss_notice_email_cron_not_running' => false,
150 +
151 + // Settings
152 + 'save_term_order' => false,
153 +
154 + // PRO features activate/deactivate
155 + 'activate_pro_feature' => false,
156 + 'deactivate_pro_feature' => false,
157 +
158 + 'deactivate_survey' => false,
105 159 );
106 160
107 - foreach ( $ajax_events as $ajax_event => $nopriv ) {
161 + foreach ( $ajax_events as $ajax_event => $nopriv )
162 + {
163 + if ( ! $nopriv ) {
164 + add_action( 'wp_ajax_propertyhive_' . $ajax_event, array( $this, 'authorize_admin_ajax' ), 0 );
165 + }
108 166 add_action( 'wp_ajax_propertyhive_' . $ajax_event, array( $this, $ajax_event ) );
109 167
110 168 if ( $nopriv ) {
111 169 add_action( 'wp_ajax_nopriv_propertyhive_' . $ajax_event, array( $this, $ajax_event ) );
@@ -112,8 +170,244 @@
112 170 }
113 171 }
114 172 }
115 173
174 + /**
175 + * Require CRM access before dispatching an administrative AJAX action.
176 + * Individual callbacks still enforce their nonces and record permissions.
177 + */
178 + public function authorize_admin_ajax()
179 + {
180 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
181 + wp_send_json_error( esc_html__( 'Insufficient permissions', 'propertyhive' ), 403 );
182 + }
183 + }
184 +
185 + /** Validate a CRM action's target before rendering or changing a record. */
186 + private function get_authorized_record_id( $field, $post_type )
187 + {
188 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Shared record guard: mutating callers verify their own action nonce; read-only callers are CRM-only through authorize_admin_ajax. This helper performs no writes.
189 + $post_id = isset( $_POST[$field] ) && is_scalar( $_POST[$field] ) ? absint( $_POST[$field] ) : 0;
190 + if ( !is_array($post_type) ) { $post_type = array($post_type); }
191 + if (
192 + $post_id < 1 ||
193 + ! in_array( get_post_type( $post_id ), $post_type, true ) ||
194 + ! current_user_can( 'manage_propertyhive' ) ||
195 + ! current_user_can( 'edit_post', $post_id ) )
196 + {
197 + wp_send_json_error( __( 'Invalid record or insufficient permissions.', 'propertyhive' ), 403 );
198 + }
199 + return $post_id;
200 + }
201 +
202 + /** Normalize viewing booking fields before creating any records. */
203 + private function get_viewing_booking_input()
204 + {
205 + $input = array();
206 + foreach ( array( 'start_date', 'start_time', 'applicant_name', 'applicant_email_address', 'applicant_telephone_number', 'applicant_address' ) as $field ) {
207 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Both booking callbacks verify book-viewing before calling this input-only helper.
208 + if ( isset( $_POST[$field] ) && ! is_string( $_POST[$field] ) ) {
209 + wp_send_json_error( __( 'Invalid booking details.', 'propertyhive' ), 400 );
210 + }
211 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Both booking callbacks verify book-viewing before calling this input-only helper.
212 + $input[$field] = isset( $_POST[$field] ) ? ( 'applicant_address' === $field ? sanitize_textarea_field( wp_unslash( $_POST[$field] ) ) : sanitize_text_field( wp_unslash( $_POST[$field] ) ) ) : '';
213 + }
214 + if ( '' === $input['start_date'] || '' === $input['start_time'] || false === strtotime( $input['start_date'] . ' ' . $input['start_time'] ) ) {
215 + wp_send_json_error( __( 'Invalid viewing date or time.', 'propertyhive' ), 400 );
216 + }
217 + foreach ( array( 'applicant_ids', 'property_ids', 'negotiator_ids' ) as $field ) {
218 + // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Inspect scalar/list shape first; each accepted ID is validated as a positive decimal string and converted with absint below.
219 + $values = isset( $_POST[$field] ) ? $_POST[$field] : array();
220 + $values = is_array( $values ) ? $values : ( '' === $values ? array() : array( $values ) );
221 + $input[$field] = array();
222 + foreach ( $values as $value ) {
223 + if ( ! is_scalar( $value ) || ! ctype_digit( (string) $value ) || (int) $value < 1 ) {
224 + wp_send_json_error( __( 'Invalid booking selection.', 'propertyhive' ), 400 );
225 + }
226 + $input[$field][] = absint( $value );
227 + }
228 + }
229 + $viewing_type = get_post_type_object( 'viewing' );
230 + if ( ! current_user_can( 'manage_propertyhive' ) || ! $viewing_type || ! current_user_can( $viewing_type->cap->create_posts ) ) {
231 + wp_send_json_error( __( 'Insufficient permissions.', 'propertyhive' ), 403 );
232 + }
233 + return $input;
234 + }
235 +
236 + /** Normalize offer recording fields before creating any records. */
237 + private function get_offer_input()
238 + {
239 + $input = array();
240 + foreach ( array( 'offer_date', 'offer_time', 'amount', 'applicant_name', 'applicant_email_address', 'applicant_telephone_number', 'applicant_address' ) as $field ) {
241 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Both offer callbacks verify record-offer before calling this input-only helper.
242 + if ( isset( $_POST[$field] ) && ! is_string( $_POST[$field] ) ) {
243 + wp_send_json_error( __( 'Invalid offer details.', 'propertyhive' ), 400 );
244 + }
245 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Both offer callbacks verify record-offer before calling this input-only helper.
246 + $input[$field] = isset( $_POST[$field] ) ? ( 'applicant_address' === $field ? sanitize_textarea_field( wp_unslash( $_POST[$field] ) ) : sanitize_text_field( wp_unslash( $_POST[$field] ) ) ) : '';
247 + }
248 + if ( '' === $input['offer_date'] || '' === $input['offer_time'] || false === strtotime( $input['offer_date'] . ' ' . $input['offer_time'] ) ) {
249 + wp_send_json_error( __( 'Invalid offer date or time.', 'propertyhive' ), 400 );
250 + }
251 + foreach ( array( 'applicant_ids', 'property_ids' ) as $field ) {
252 + // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Inspect scalar/list shape first; each accepted ID is validated as a positive decimal string and converted with absint below.
253 + $values = isset( $_POST[$field] ) ? $_POST[$field] : array();
254 + $values = is_array( $values ) ? $values : ( '' === $values ? array() : array( $values ) );
255 + $input[$field] = array();
256 + foreach ( $values as $value ) {
257 + if ( ! is_scalar( $value ) || ! ctype_digit( (string) $value ) || (int) $value < 1 ) {
258 + wp_send_json_error( __( 'Invalid offer selection.', 'propertyhive' ), 400 );
259 + }
260 + $input[$field][] = absint( $value );
261 + }
262 + }
263 + $offer_type = get_post_type_object( 'offer' );
264 + if ( ! current_user_can( 'manage_propertyhive' ) || ! $offer_type || ! current_user_can( $offer_type->cap->create_posts ) ) {
265 + wp_send_json_error( __( 'Insufficient permissions.', 'propertyhive' ), 403 );
266 + }
267 + $input['amount'] = preg_replace( '/[^0-9.]/', '', $input['amount'] );
268 + if ( '' === $input['amount'] || ! is_numeric( $input['amount'] ) ) {
269 + wp_send_json_error( __( 'Invalid offer amount.', 'propertyhive' ), 400 );
270 + }
271 + return $input;
272 + }
273 +
274 + /** Preserve PHP upload metadata for WordPress's upload validator. */
275 + private function get_viewing_email_uploads()
276 + {
277 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing -- Calling email callbacks verify viewing-actions first. File metadata must reach wp_handle_upload unchanged; shape is checked below, and core verifies uploaded-file provenance, MIME/extension, size and safe destination filename.
278 + $files = isset( $_FILES['attachments'] ) ? $_FILES['attachments'] : array();
279 + foreach ( array( 'name', 'type', 'tmp_name', 'error', 'size' ) as $key ) {
280 + if ( ! isset( $files[$key] ) || ! is_array( $files[$key] ) ) {
281 + wp_send_json_error( __( 'Invalid attachment data.', 'propertyhive' ), 400 );
282 + }
283 + }
284 + foreach ( $files['name'] as $index => $name ) {
285 + foreach ( array( 'name', 'type', 'tmp_name' ) as $key ) {
286 + if ( ! isset( $files[$key][$index] ) || ! is_string( $files[$key][$index] ) ) {
287 + wp_send_json_error( __( 'Invalid attachment data.', 'propertyhive' ), 400 );
288 + }
289 + }
290 + foreach ( array( 'error', 'size' ) as $key ) {
291 + if ( ! isset( $files[$key][$index] ) || ! is_scalar( $files[$key][$index] ) || ! ctype_digit( (string) $files[$key][$index] ) ) {
292 + wp_send_json_error( __( 'Invalid attachment data.', 'propertyhive' ), 400 );
293 + }
294 + }
295 + }
296 + return $files;
297 + }
298 +
299 + public function deactivate_survey()
300 + {
301 + // Verify the nonce
302 + if ( !isset($_POST['nonce']) || !wp_verify_nonce( ( isset( $_POST['nonce'] ) && is_string( $_POST['nonce'] ) ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'deactivate-survey') )
303 + {
304 + wp_send_json_error('Invalid nonce', 403);
305 + die();
306 + }
307 +
308 + if ( !isset($_POST['reason']) || !is_string($_POST['reason']) || empty($_POST['reason']) )
309 + {
310 + wp_send_json_error('Reason is required', 400);
311 + die();
312 + }
313 +
314 + $reason = sanitize_text_field( wp_unslash( $_POST['reason'] ) );
315 + $comments = ( isset($_POST['comments']) && is_string($_POST['comments']) ) ? sanitize_textarea_field( wp_unslash( $_POST['comments'] ) ) : '';
316 + $anonymous = isset($_POST['anonymous']) && $_POST['anonymous'] === 'yes';
317 +
318 + $license_type = get_option('propertyhive_license_type');
319 + if ( $license_type == 'pro' )
320 + {
321 + $license_key = get_option('propertyhive_pro_license_key');
322 + }
323 + else
324 + {
325 + $license_key = get_option('propertyhive_license_key');
326 + }
327 + $propertyhive_install_timestamp = get_option('propertyhive_install_timestamp');
328 + $active_plugins = get_option('active_plugins');
329 + $all_plugins = get_plugins(); // Fetch detailed data for all plugins
330 +
331 + $active_plugins_with_versions = array();
332 +
333 + foreach ( $active_plugins as $plugin )
334 + {
335 + if ( isset($all_plugins[$plugin]) )
336 + {
337 + $active_plugins_with_versions[] = array(
338 + 'name' => $all_plugins[$plugin]['Name'],
339 + 'version' => $all_plugins[$plugin]['Version'],
340 + 'path' => $plugin,
341 + );
342 + }
343 + }
344 + $server_software = ( isset( $_SERVER['SERVER_SOFTWARE'] ) && is_string( $_SERVER['SERVER_SOFTWARE'] ) ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'Unknown';
345 +
346 + // Prepare data for third-party POST
347 + $third_party_data = array(
348 + 'reason' => $reason,
349 + 'comments' => $comments,
350 + 'anonymous' => $anonymous ? 'yes' : 'no',
351 + );
352 +
353 + if (!$anonymous)
354 + {
355 + $third_party_data['site_url'] = get_site_url();
356 + $third_party_data['admin_email'] = get_option('admin_email');
357 + $third_party_data['license_type'] = $license_type;
358 + $third_party_data['license_key'] = $license_key;
359 + $third_party_data['active_plugins'] = $active_plugins_with_versions;
360 + $third_party_data['active_theme'] = wp_get_theme()->get('Name');
361 + $third_party_data['wordpress_version'] = get_bloginfo('version');
362 + $third_party_data['php_version'] = phpversion();
363 + $third_party_data['server_software'] = $server_software;
364 + }
365 +
366 + //wp_send_json_success(json_encode($third_party_data, true));
367 +
368 + // Make the remote POST request
369 + $response = wp_remote_post('https://wp-property-hive.com/deactivate-survey.php', array(
370 + 'method' => 'POST',
371 + 'body' => $third_party_data
372 + ));
373 +
374 + if ( is_wp_error($response) )
375 + {
376 + wp_send_json_error($response->get_error_message(), 500);
377 + die();
378 + }
379 +
380 + $response_body = wp_remote_retrieve_body($response);
381 + wp_send_json_success(json_decode($response_body, true));
382 +
383 + die();
384 + }
385 +
386 + public function save_term_order()
387 + {
388 + check_ajax_referer( 'updates', 'security' );
389 +
390 + if ( ! isset( $_POST['taxonomy'], $_POST['term'] ) || ! is_string( $_POST['taxonomy'] ) || ! is_array( $_POST['term'] ) || empty( $_POST['term'] ) ) {
391 + die();
392 + }
393 + $taxonomy_name = sanitize_key( wp_unslash( $_POST['taxonomy'] ) );
394 + $taxonomy = get_taxonomy( $taxonomy_name );
395 + if ( ! $taxonomy || ! current_user_can( $taxonomy->cap->manage_terms ) ) {
396 + wp_send_json_error( esc_html__( 'Insufficient permissions', 'propertyhive' ), 403 );
397 + }
398 + $term_ids = array();
399 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Validate raw term ID types before accepting only positive decimal integers below; no text is stored.
400 + foreach ( $_POST['term'] as $term_id ) {
401 + if ( ! is_string( $term_id ) || ! ctype_digit( $term_id ) || 0 === absint( $term_id ) ) {
402 + die();
403 + }
404 + $term_ids[] = absint( $term_id );
405 + }
406 + update_option( 'propertyhive_taxonomy_terms_order_' . $taxonomy_name, implode( '|', $term_ids ) );
407 + die();
408 + }
409 +
116 410 public function dismiss_notice_leave_review()
117 411 {
118 412 update_option( 'propertyhive_review_prompt_due_timestamp', 0 );
119 413
@@ -120,8 +414,41 @@
120 414 // Quit out
121 415 die();
122 416 }
123 417
418 + public function dismiss_notice_retired_template_assistant()
419 + {
420 + if ( is_multisite() )
421 + {
422 + if ( ! is_super_admin() ) return;
423 + delete_site_option( 'propertyhive_template_assistant_retired_notice' );
424 + }
425 + else
426 + {
427 + if ( ! current_user_can( 'activate_plugins' ) ) return;
428 + delete_option( 'propertyhive_template_assistant_retired_notice' );
429 + }
430 +
431 + // Quit out
432 + die();
433 + }
434 +
435 + public function dismiss_notice_demo_data()
436 + {
437 + update_option( 'propertyhive_hide_demo_data_tab', 'yes' );
438 +
439 + // Quit out
440 + die();
441 + }
442 +
443 + public function dismiss_notice_epl()
444 + {
445 + update_option( 'epl_notice_dismissed', 'yes' );
446 +
447 + // Quit out
448 + die();
449 + }
450 +
124 451 public function dismiss_notice_missing_search_results()
125 452 {
126 453 update_option( 'missing_search_results_notice_dismissed', 'yes' );
127 454
@@ -144,8 +471,13 @@
144 471 // Quit out
145 472 die();
146 473 }
147 474
475 + public function dismiss_notice_email_cron_not_running()
476 + {
477 + update_option( 'email_cron_not_running_dismissed', 'yes' );
478 + }
479 +
148 480 /**
149 481 * Output headers for JSON requests
150 482 */
151 483 private function json_headers() {
@@ -151,40 +483,144 @@
151 483 private function json_headers() {
152 484 header( 'Content-Type: application/json; charset=utf-8' );
153 485 }
154 486
487 + /**
488 + * Return a list string, comma delimited with an ampersand(&) before the final item
489 + */
490 + private function get_list_string( $list_items )
491 + {
492 + $list_string = '';
493 + if ( count($list_items) == 1 )
494 + {
495 + $list_string = $list_items[0];
496 + }
497 + elseif ( count($list_items) > 1 )
498 + {
499 + $last_item = array_pop($list_items);
500 + $list_string = implode(', ', $list_items) . ' & ' . $last_item;
501 + }
502 + return $list_string;
503 + }
504 +
505 + private function check_recaptcha_form_response($errors, $key, $control)
506 + {
507 + $secret = isset( $control['secret'] ) ? $control['secret'] : '';
508 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reads a CAPTCHA response token and performs remote validation; the helper does not write state. It is called from nonce-protected applicant_registration and from the separately assessed public enquiry endpoint. This line alone is not a CSRF sink.
509 + $response = ( isset( $_POST['g-recaptcha-response'] ) && is_string( $_POST['g-recaptcha-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['g-recaptcha-response'] ) ) : '';
510 +
511 + $response = wp_remote_post(
512 + 'https://www.google.com/recaptcha/api/siteverify',
513 + array(
514 + 'method' => 'POST',
515 + 'body' => array( 'secret' => $secret, 'response' => $response ),
516 + )
517 + );
518 + if ( is_wp_error( $response ) )
519 + {
520 + $errors[] = $response->get_error_message();
521 + }
522 + else
523 + {
524 + $response = json_decode($response['body'], TRUE);
525 +
526 + if ( $response === FALSE )
527 + {
528 + $errors[] = __( 'Error decoding response from reCAPTCHA check', 'propertyhive' );
529 + }
530 + else
531 + {
532 + if ( isset($response['success']) && $response['success'] == true )
533 + {
534 + if ( $key == 'recaptcha' )
535 + {
536 +
537 + }
538 + elseif ( $key == 'recaptcha-v3' )
539 + {
540 + $score_threshold = round((float)get_option('propertyhive_captcha_score_threshold', 0.5), 1);
541 + if ( !is_numeric($score_threshold) || $score_threshold < 0 || $score_threshold > 1 )
542 + {
543 + $score_threshold = 0.5;
544 + }
545 + if ( isset($response['score']) && $response['score'] >= $score_threshold )
546 + {
547 +
548 + }
549 + else
550 + {
551 + $errors[] = __('Failed reCAPTCHA validation due to high spam score', 'propertyhive' ) . ': ' . $response['score'];
552 + }
553 + }
554 + }
555 + else
556 + {
557 + $error_message = __( 'Failed reCAPTCHA validation', 'propertyhive' );
558 +
559 + // Check if Google returned error codes
560 + if ( isset($response['error-codes']) && is_array($response['error-codes']) )
561 + {
562 + $error_message .= ' (' . implode(', ', $response['error-codes']) . ')';
563 + }
564 +
565 + $errors[] = $error_message;
566 + }
567 + }
568 + }
569 + return $errors;
570 + }
571 +
155 572 public function create_contact_login()
156 573 {
157 574 check_ajax_referer( 'create-login', 'security' );
158 575
159 - $this->json_headers();
160 -
161 - if (empty($_POST['contact_id']))
162 - {
163 - $return = array('error' => 'No contact selected');
164 - echo json_encode( $return );
165 - die();
576 + $contact_id = isset( $_POST['contact_id'] ) && is_scalar( $_POST['contact_id'] ) ? absint( $_POST['contact_id'] ) : 0;
577 + if ( ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'edit_post', $contact_id ) ) {
578 + wp_send_json_error( __( 'Insufficient permissions', 'propertyhive' ), 403 );
166 579 }
580 + if ( 'contact' !== get_post_type( $contact_id ) ) {
581 + wp_send_json_error( __( 'Invalid contact.', 'propertyhive' ), 400 );
582 + }
583 + if ( get_post_meta( $contact_id, '_user_id', true ) ) {
584 + wp_send_json_error( __( 'This contact already has a login.', 'propertyhive' ), 409 );
585 + }
167 586
168 - if (empty($_POST['password']))
587 + if ( empty( $_POST['password'] ) || ! is_string( $_POST['password'] ) )
169 588 {
170 589 $return = array('error' => 'No password entered');
171 - echo json_encode( $return );
172 - die();
590 + wp_send_json( $return );
173 591 }
174 592
175 - $contact = new PH_Contact((int)$_POST['contact_id']);
593 + $contact = new PH_Contact($contact_id);
176 594
595 + $display_name = get_the_title($contact_id);
596 +
177 597 // Create user
178 598 $userdata = array(
179 - 'display_name' => get_the_title((int)$_POST['contact_id']),
599 + 'display_name' => $display_name,
180 600 'user_login' => sanitize_email($contact->email_address),
181 601 'user_email' => sanitize_email($contact->email_address),
182 - 'user_pass' => $_POST['password'],
602 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Opaque password is type checked above, unslashed once and passed directly to WordPress hashing; text sanitization would change the credential.
603 + 'user_pass' => wp_unslash( $_POST['password'] ),
183 604 'role' => 'property_hive_contact',
184 605 'show_admin_bar_front' => 'false',
185 606 );
186 607
608 + if ( !empty($display_name) )
609 + {
610 + $name_parts = explode( ' ', $display_name );
611 +
612 + if ( count($name_parts) > 1 )
613 + {
614 + $userdata['last_name'] = array_pop($name_parts);
615 + $userdata['first_name'] = implode(' ', $name_parts);
616 + }
617 + else
618 + {
619 + $userdata['last_name'] = $display_name;
620 + }
621 + }
622 +
187 623 $user_id = wp_insert_user( $userdata );
188 624
189 625 // On success
190 626 if ( ! is_wp_error( $user_id ) )
@@ -189,9 +625,9 @@
189 625 // On success
190 626 if ( ! is_wp_error( $user_id ) )
191 627 {
192 628 // Assign user ID to CPT
193 - add_post_meta( (int)$_POST['contact_id'], '_user_id', $user_id );
629 + add_post_meta( $contact_id, '_user_id', $user_id );
194 630
195 631 $return = array('success' => true);
196 632 }
197 633 else
@@ -198,10 +634,9 @@
198 634 {
199 635 $return = array('error' => 'Failed to create user login');
200 636 }
201 637
202 - echo json_encode( $return );
203 - die();
638 + wp_send_json( $return );
204 639 }
205 640
206 641 /**
207 642 * Login user
@@ -216,18 +651,19 @@
216 651 if ( check_ajax_referer( 'ph_login', 'security', false ) === FALSE )
217 652 {
218 653 $return['errors'][] = 'Invalid nonce';
219 654
220 - $this->json_headers();
221 - echo json_encode( $return );
222 -
223 - // Quit out
224 - die();
655 + wp_send_json( $return );
225 656 }
226 657
658 + if ( ! isset( $_POST['email_address'], $_POST['password'] ) || ! is_string( $_POST['email_address'] ) || ! is_string( $_POST['password'] ) ) {
659 + $return['errors'][] = __( 'Enter your login details.', 'propertyhive' );
660 + wp_send_json( $return );
661 + }
227 662 $creds = array(
228 - 'user_login' => ph_clean($_POST['email_address']),
229 - 'user_password' => ph_clean($_POST['password']),
663 + 'user_login' => sanitize_text_field( wp_unslash( $_POST['email_address'] ) ),
664 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Authentication requires the exact password, without text or HTML sanitization.
665 + 'user_password' => wp_unslash( $_POST['password'] ),
230 666 );
231 667
232 668 $user = wp_signon( apply_filters( 'propertyhive_login_credentials', $creds ), is_ssl() );
233 669
@@ -238,12 +674,13 @@
238 674 else
239 675 {
240 676 // Check has associated contact CPT and is published
241 677 $args = array(
242 - 'post_type' => 'contact',
678 + 'post_type' => apply_filters( 'propertyhive_allowed_login_post_type', array( 'contact' ) ),
243 679 'fields' => 'ids',
244 680 'posts_per_page' => 1,
245 681 'post_status' => array( 'publish' ),
682 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Login/contact duplicate/address lookups use a fixed meta relation and return a small result set (1 row for identity checks, 10 for the address autocomplete). posts_per_page=1; posts_per_page=10; fields=ids on all four; values are the authenticated user, submitted email, search text, or current contact.
246 683 'meta_query' => array(
247 684 array(
248 685 'key' => '_user_id',
249 686 'value' => $user->ID
@@ -268,16 +705,142 @@
268 705
269 706 wp_reset_postdata();
270 707 }
271 708
272 - $this->json_headers();
273 - echo json_encode( $return );
709 + wp_send_json( $return );
710 + }
711 +
712 + /**
713 + * Lost password
714 + */
715 + public function lost_password()
716 + {
717 + $return = array(
718 + 'success' => false,
719 + 'errors' => array(),
720 + );
721 +
722 + if ( check_ajax_referer( 'ph_lost_password', 'security', false ) === FALSE )
723 + {
724 + $return['errors'][] = 'Invalid nonce';
725 +
726 + wp_send_json( $return );
727 + }
728 +
729 + $email_address = isset( $_POST['email_address'] ) && is_string( $_POST['email_address'] ) ? sanitize_email( wp_unslash( $_POST['email_address'] ) ) : '';
730 +
731 + $user_data = get_user_by( 'email', $email_address );
732 +
733 + // check email address exists
734 + if ( !$user_data )
735 + {
736 + $return['errors'][] = 'Email address not found';
737 +
738 + wp_send_json( $return );
739 + }
740 +
741 + // Send reset email
742 + $to = $email_address;
743 + $subject = __( 'Password Reset Request for', 'propertyhive' ) . ' ' . get_bloginfo('name');
744 + $body = __( 'Someone has requested a new password for an account on', 'propertyhive' ) . ' ' . get_bloginfo('name') . ".\n\n";
745 + $body .= __( 'If you didn\'t make this request you can ignore this email. If you\'d like to proceed please follow the link below', 'propertyhive' ) . ":\n\n";
746 + $body .= add_query_arg( array(
747 + 'key' => get_password_reset_key( $user_data ),
748 + 'id' => $user_data->ID,
749 + ), get_permalink( get_option( 'propertyhive_applicant_reset_password_page_id', '' ) ) );
750 +
751 +
752 + $from = get_option('propertyhive_email_from_address', '');
753 + if ( $from == '' )
754 + {
755 + $from = get_bloginfo('admin_email');
756 + }
757 +
758 + $headers = array();
759 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
760 + $headers[] = 'Reply-To: ' . sanitize_email($from);
761 + $headers[] = 'Content-Type: text/plain; charset=UTF-8';
762 +
763 + $headers = apply_filters( 'propertyhive_lost_password_email_headers', $headers );
764 +
765 + wp_mail( $to, $subject, $body, $headers );
274 766
275 - // Quit out
276 - die();
767 + $return['success'] = true;
768 +
769 + wp_send_json( $return );
277 770 }
278 771
279 772 /**
773 + * Reset password
774 + */
775 + public function reset_password()
776 + {
777 + $return = array(
778 + 'success' => false,
779 + 'errors' => array(),
780 + );
781 +
782 + if ( check_ajax_referer( 'ph_reset_password', 'security', false ) === FALSE )
783 + {
784 + $return['errors'][] = 'Invalid nonce';
785 +
786 + wp_send_json( $return );
787 + }
788 +
789 + // check key and user login again
790 + if ( ! isset( $_POST['reset_key'], $_POST['reset_login'], $_POST['password_1'], $_POST['password_2'] ) || ! is_string( $_POST['reset_key'] ) || ! is_string( $_POST['reset_login'] ) || ! is_string( $_POST['password_1'] ) || ! is_string( $_POST['password_2'] ) ) {
791 + $return['errors'][] = __( 'Please enter valid password reset details.', 'propertyhive' );
792 + wp_send_json( $return );
793 + }
794 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Core validates the exact opaque reset token and login; text sanitization would change credentials.
795 + $user = check_password_reset_key( wp_unslash( $_POST['reset_key'] ), wp_unslash( $_POST['reset_login'] ) );
796 +
797 + // check passwords match and are strong enough
798 + if ( $user instanceof WP_User )
799 + {
800 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Preserve the exact password; authentication secrets must not be text-sanitized.
801 + $password_1 = wp_unslash( $_POST['password_1'] );
802 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Preserve the exact password; authentication secrets must not be text-sanitized.
803 + $password_2 = wp_unslash( $_POST['password_2'] );
804 +
805 + if ( empty( $password_1 ) )
806 + {
807 + $return['errors'][] = __( 'Please enter your password.', 'propertyhive' );
808 + }
809 +
810 + if ( $password_1 !== $password_2 )
811 + {
812 + $return['errors'][] = __( 'Passwords do not match.', 'propertyhive' );
813 + }
814 +
815 + // Check password strength?
816 + }
817 + else
818 + {
819 + $return['errors'][] = __( 'This key is invalid or has already been used. Please reset your password again if needed..', 'propertyhive' );
820 + }
821 +
822 + if ( !empty($return['errors']) )
823 + {
824 + wp_send_json( $return );
825 + }
826 +
827 + // do actual reset
828 + $errors = new WP_Error();
829 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core hook validate_password_reset; renaming it would break the core hook contract.
830 + do_action( 'validate_password_reset', $errors, $user );
831 +
832 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core hook password_reset; renaming it would break the core hook contract.
833 + do_action( 'password_reset', $user, $password_1 );
834 +
835 + wp_set_password( $password_1, $user->ID );
836 +
837 + $return['success'] = true;
838 +
839 + wp_send_json( $return );
840 + }
841 +
842 + /**
280 843 * Register applicant
281 844 */
282 845 public function applicant_registration()
283 846 {
@@ -302,8 +865,48 @@
302 865
303 866 // Validate
304 867 $errors = array();
305 868
869 + $registration_input = array();
870 + foreach ( array( 'name', 'email_address', 'telephone_number', 'department', 'maximum_price', 'maximum_rent', 'minimum_bedrooms', 'available_as_sale', 'available_as_rent', 'minimum_floor_area', 'maximum_floor_area', 'location_text', 'additional_requirements' ) as $input_key ) {
871 + if ( isset( $_POST[$input_key] ) && ! is_string( $_POST[$input_key] ) ) {
872 + $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
873 + $registration_input[$input_key] = '';
874 + continue;
875 + }
876 + if ( 'additional_requirements' === $input_key ) {
877 + $registration_input[$input_key] = isset( $_POST[$input_key] ) ? sanitize_textarea_field( wp_unslash( $_POST[$input_key] ) ) : '';
878 + } else {
879 + $registration_input[$input_key] = isset( $_POST[$input_key] ) ? sanitize_text_field( wp_unslash( $_POST[$input_key] ) ) : '';
880 + }
881 + }
882 + foreach ( array( 'property_type', 'commercial_property_type', 'location' ) as $input_key ) {
883 + $registration_input[$input_key] = array();
884 + if ( isset( $_POST[$input_key] ) ) {
885 + if ( ! is_string( $_POST[$input_key] ) && ! is_array( $_POST[$input_key] ) ) {
886 + $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
887 + continue;
888 + }
889 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Validate element types before unslashing and sanitizing each accepted selection below.
890 + foreach ( (array) $_POST[$input_key] as $selection ) {
891 + if ( ! is_string( $selection ) ) {
892 + $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
893 + continue;
894 + }
895 + $registration_input[$input_key][] = sanitize_text_field( wp_unslash( $selection ) );
896 + }
897 + }
898 + }
899 + foreach ( array( 'password', 'password2' ) as $input_key ) {
900 + if ( isset( $_POST[$input_key] ) && ! is_string( $_POST[$input_key] ) ) {
901 + $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
902 + $registration_input[$input_key] = '';
903 + } else {
904 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Passwords are type-checked opaque strings, unslashed once and passed unchanged to WordPress hashing.
905 + $registration_input[$input_key] = isset( $_POST[$input_key] ) ? wp_unslash( $_POST[$input_key] ) : '';
906 + }
907 + }
908 +
306 909 $form_controls = ph_get_user_details_form_fields();
307 910
308 911 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
309 912
@@ -308,9 +911,9 @@
308 911 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
309 912
310 913 $form_controls_2 = ph_get_applicant_requirements_form_fields();
311 914
312 - $form_controls_2 = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls_2 );
915 + $form_controls_2 = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls_2, false );
313 916
314 917 $form_controls = array_merge( $form_controls, $form_controls_2 );
315 918
316 919 // need to improve this as duplicated in ph-shortcodes.php
@@ -341,9 +944,9 @@
341 944 }
342 945 }
343 946 if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) )
344 947 {
345 - if ( ! is_email( $_POST[$key] ) )
948 + if ( ! is_string( $_POST[$key] ) || ! is_email( wp_unslash( $_POST[$key] ) ) )
346 949 {
347 950 $errors[] = __( 'Invalid email address provided', 'propertyhive' );
348 951 }
349 952 else
@@ -353,12 +956,13 @@
353 956 'post_type' => 'contact',
354 957 'posts_per_page' => 1,
355 958 'fields' => 'ids',
356 959 'post_status' => array( 'publish' ),
960 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Login/contact duplicate/address lookups use a fixed meta relation and return a small result set (1 row for identity checks, 10 for the address autocomplete). posts_per_page=1; posts_per_page=10; fields=ids on all four; values are the authenticated user, submitted email, search text, or current contact.
357 961 'meta_query' => array(
358 962 array(
359 963 'key' => '_email_address',
360 - 'value' => $_POST[$key]
964 + 'value' => sanitize_email( wp_unslash( $_POST[$key] ) )
361 965 )
362 966 )
363 967 );
364 968
@@ -365,39 +969,81 @@
365 969 $contacts_query = new WP_Query( $args );
366 970
367 971 if ( $contacts_query->have_posts() )
368 972 {
369 - while ( $contacts_query->have_posts() )
973 + // Public registration does not prove ownership of an existing CRM contact.
974 + $errors[] = __( 'This email address is already registered to a user. Please sign in or contact the agency.', 'propertyhive' );
975 + }
976 + else
977 + {
978 + if ( email_exists( sanitize_email( wp_unslash( $_POST[$key] ) ) ) )
370 979 {
371 - $contacts_query->the_post();
980 + $errors[] = __( 'This email address is already registered to a user', 'propertyhive' );
981 + }
982 + }
983 + wp_reset_postdata();
984 + }
985 + }
986 + if ( in_array( $key, array('recaptcha', 'recaptcha-v3') ) )
987 + {
988 + $errors = $this->check_recaptcha_form_response($errors, $key, $control);
989 + }
372 990
373 - $contact_post_id = get_the_ID();
374 - }
375 - //$errors[] = __( 'This email address is already registered', 'propertyhive' );
991 + if ( $key == 'hCaptcha' )
992 + {
993 + $secret = isset( $control['secret'] ) ? $control['secret'] : '';
994 + $response = ( isset( $_POST['h-captcha-response'] ) && is_string( $_POST['h-captcha-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['h-captcha-response'] ) ) : '';
995 +
996 + $response = wp_remote_post(
997 + 'https://hcaptcha.com/siteverify',
998 + array(
999 + 'method' => 'POST',
1000 + 'body' => array( 'secret' => $secret, 'response' => $response ),
1001 + )
1002 + );
1003 +
1004 + if ( is_wp_error( $response ) )
1005 + {
1006 + $errors[] = $response->get_error_message();
1007 + }
1008 + else
1009 + {
1010 + $response = json_decode($response['body'], TRUE);
1011 + if ( $response === FALSE )
1012 + {
1013 + $errors[] = 'Error decoding response from hCaptcha check';
376 1014 }
377 1015 else
378 1016 {
379 - if ( email_exists( $_POST[$key] ) )
1017 + if ( isset($response['success']) && $response['success'] == true )
380 1018 {
381 - $errors[] = __( 'This email address is already registered to a user', 'propertyhive' );
1019 +
382 1020 }
1021 + else
1022 + {
1023 + $errors[] = 'Failed hCaptcha validation';
1024 + }
383 1025 }
384 - wp_reset_postdata();
385 1026 }
386 1027 }
387 - if ( $key == 'recaptcha' )
1028 +
1029 + if ( $key == 'turnstile' )
388 1030 {
389 1031 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
390 - $response = isset( $_POST['g-recaptcha-response'] ) ? ph_clean($_POST['g-recaptcha-response']) : '';
1032 + $response = ( isset( $_POST['cf-turnstile-response'] ) && is_string( $_POST['cf-turnstile-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) ) : '';
391 1033
392 - $response = wp_remote_post(
393 - 'https://www.google.com/recaptcha/api/siteverify',
1034 + $response = wp_remote_post(
1035 + 'https://challenges.cloudflare.com/turnstile/v0/siteverify', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Server-side CAPTCHA token verification API.
394 1036 array(
395 1037 'method' => 'POST',
1038 + 'headers' => array(
1039 + 'Content-Type' => 'application/x-www-form-urlencoded',
1040 + ),
396 1041 'body' => array( 'secret' => $secret, 'response' => $response ),
397 1042 )
398 1043 );
399 - if ( is_wp_error( $response ) )
1044 +
1045 + if ( is_wp_error( $response ) )
400 1046 {
401 1047 $errors[] = $response->get_error_message();
402 1048 }
403 1049 else
@@ -404,9 +1050,9 @@
404 1050 {
405 1051 $response = json_decode($response['body'], TRUE);
406 1052 if ( $response === FALSE )
407 1053 {
408 - $errors[] = __( 'Error decoding response from reCAPTCHA check', 'propertyhive' );
1054 + $errors[] = 'Error decoding response from turnstile check';
409 1055 }
410 1056 else
411 1057 {
412 1058 if ( isset($response['success']) && $response['success'] == true )
@@ -414,9 +1060,9 @@
414 1060
415 1061 }
416 1062 else
417 1063 {
418 - $errors[] = __( 'Failed reCAPTCHA validation', 'propertyhive' );
1064 + $errors[] = 'Failed turnstile validation';
419 1065 }
420 1066 }
421 1067 }
422 1068 }
@@ -422,9 +1068,9 @@
422 1068 }
423 1069 }
424 1070
425 1071 // Check password and password2 match
426 - if ( isset( $_POST['password'] ) && isset( $_POST['password2'] ) && $_POST['password'] != $_POST['password2'] )
1072 + if ( isset( $_POST['password'] ) && isset( $_POST['password2'] ) && $registration_input['password'] !== $registration_input['password2'] )
427 1073 {
428 1074 $errors[] = __( 'The passwords entered do not match', 'propertyhive' );
429 1075 }
430 1076
@@ -441,9 +1087,9 @@
441 1087 if ( $contact_post_id === FALSE )
442 1088 {
443 1089 // create CPT
444 1090 $contact_post = array(
445 - 'post_title' => ph_clean($_POST['name']),
1091 + 'post_title' => wp_slash( $registration_input['name'] ),
446 1092 'post_content' => '',
447 1093 'post_type' => 'contact',
448 1094 'post_status' => 'publish',
449 1095 'comment_status'=> 'closed',
@@ -457,9 +1103,9 @@
457 1103 {
458 1104 // update CPT
459 1105 $contact_post = array(
460 1106 'ID' => $contact_post_id,
461 - 'post_title' => ph_clean($_POST['name']),
1107 + 'post_title' => wp_slash( $registration_input['name'] ),
462 1108 'post_status' => 'publish',
463 1109 );
464 1110
465 1111 // Insert the post into the database
@@ -476,16 +1122,16 @@
476 1122 }
477 1123 update_post_meta( $contact_post_id, '_forbidden_contact_methods', array_unique($forbidden_contact_methods) );
478 1124
479 1125 // Add post meta (contact details, requirements etc)
480 - update_post_meta( $contact_post_id, '_email_address', sanitize_email($_POST['email_address']) );
1126 + update_post_meta( $contact_post_id, '_email_address', sanitize_email( $registration_input['email_address'] ) );
481 1127
482 1128 $telephone_number = get_post_meta( $contact_post_id, '_telephone_number', TRUE );
483 1129 if ( isset($_POST['telephone_number']) && $_POST['telephone_number'] != '' )
484 1130 {
485 - $telephone_number = $_POST['telephone_number'];
1131 + $telephone_number = $registration_input['telephone_number'];
486 1132 }
487 - update_post_meta( $contact_post_id, '_telephone_number', ph_clean($telephone_number) );
1133 + update_post_meta( $contact_post_id, '_telephone_number', wp_slash( ph_clean($telephone_number) ) );
488 1134 update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean( ph_clean_telephone_number($telephone_number) ) );
489 1135
490 1136 $contact_types = get_post_meta( $contact_post_id, '_contact_types', TRUE );
491 1137 if ( !is_array($contact_types) )
@@ -500,14 +1146,20 @@
500 1146
501 1147 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
502 1148
503 1149 $applicant_profile = array();
504 - $applicant_profile['department'] = $_POST['department'];
1150 + $applicant_profile['department'] = $registration_input['department'];
505 1151
506 - if ( $_POST['department'] == 'residential-sales' )
1152 + $base_department = $registration_input['department'];
1153 + if ( !in_array( $base_department, array('residential-sales', 'residential-lettings', 'commercial') ) )
507 1154 {
508 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_price']));
1155 + $base_department = ph_get_custom_department_based_on($base_department);
1156 + }
509 1157
1158 + if ( $base_department == 'residential-sales' )
1159 + {
1160 + $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_price']);
1161 +
510 1162 $applicant_profile['max_price'] = $price;
511 1163
512 1164 // Not used yet but could be if introducing currencies in the future.
513 1165 $applicant_profile['max_price_actual'] = $price;
@@ -514,11 +1166,11 @@
514 1166
515 1167 $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
516 1168 $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
517 1169
518 - if ( $percentage_lower != '' && $percentage_higher != '' && $_POST['maximum_price'] != '' && $_POST['maximum_price'] != 0 )
1170 + if ( $percentage_lower != '' && $percentage_higher != '' && $registration_input['maximum_price'] != '' && $registration_input['maximum_price'] != 0 )
519 1171 {
520 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_price']));
1172 + $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_price']);
521 1173 $applicant_profile['match_price_range_lower'] = $price - ( $price * ( $percentage_lower / 100 ) );
522 1174 $applicant_profile['match_price_range_lower_actual'] = $price - ( $price * ( $percentage_lower / 100 ) );
523 1175
524 1176 $applicant_profile['match_price_range_higher'] = $price + ( $price * ( $percentage_higher / 100 ) );
@@ -524,11 +1176,11 @@
524 1176 $applicant_profile['match_price_range_higher'] = $price + ( $price * ( $percentage_higher / 100 ) );
525 1177 $applicant_profile['match_price_range_higher_actual'] = $price + ( $price * ( $percentage_higher / 100 ) );
526 1178 }
527 1179 }
528 - elseif ( $_POST['department'] == 'residential-lettings' )
1180 + elseif ( $base_department == 'residential-lettings' )
529 1181 {
530 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_rent']));
1182 + $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_rent']);
531 1183
532 1184 $applicant_profile['max_rent'] = $price;
533 1185 $applicant_profile['rent_frequency'] = 'pcm';
534 1186 $price_actual = $price; // Stored in pcm
@@ -534,70 +1186,92 @@
534 1186 $price_actual = $price; // Stored in pcm
535 1187 $applicant_profile['max_price_actual'] = $price_actual;
536 1188 }
537 1189
538 - if ( $_POST['department'] == 'residential-sales' || $_POST['department'] == 'residential-lettings' )
1190 + if ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' )
539 1191 {
540 - $beds = preg_replace("/[^0-9]/", '', ph_clean($_POST['minimum_bedrooms']));
1192 + $beds = preg_replace("/[^0-9.]/", '', $registration_input['minimum_bedrooms']);
541 1193 $applicant_profile['min_beds'] = $beds;
542 1194
543 1195 if ( isset($_POST['property_type']) && !empty($_POST['property_type']) )
544 1196 {
545 - $applicant_profile['property_types'] = is_array(ph_clean($_POST['property_type'])) ? ph_clean($_POST['property_type']) : array(ph_clean($_POST['property_type']));
1197 + $applicant_profile['property_types'] = $registration_input['property_type'];
546 1198 }
547 1199 }
548 1200
549 - if ( $_POST['department'] == 'commercial' )
1201 + if ( $base_department == 'commercial' )
550 1202 {
551 1203 $available_as = array();
552 - if ( isset($_POST['available_as_sale']) && $_POST['available_as_sale'] == 'yes' )
1204 + if ( isset($_POST['available_as_sale']) && $registration_input['available_as_sale'] == 'yes' )
553 1205 {
554 1206 $available_as[] = 'sale';
555 1207 }
556 - if ( isset($_POST['available_as_rent']) && $_POST['available_as_rent'] == 'yes' )
1208 + if ( isset($_POST['available_as_rent']) && $registration_input['available_as_rent'] == 'yes' )
557 1209 {
558 1210 $available_as[] = 'rent';
559 1211 }
560 1212 $applicant_profile['available_as'] = $available_as;
561 1213
562 - $floor_area = preg_replace("/[^0-9.]/", '', ph_clean($_POST['minimum_floor_area']));
1214 + $floor_area = preg_replace("/[^0-9.]/", '', $registration_input['minimum_floor_area']);
563 1215 $applicant_profile['min_floor_area'] = $floor_area;
564 1216 $applicant_profile['min_floor_area_actual'] = $floor_area;
565 1217
566 - $floor_area = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_floor_area']));
1218 + $floor_area = preg_replace("/[^0-9.]/", '', $registration_input['maximum_floor_area']);
567 1219 $applicant_profile['max_floor_area'] = $floor_area;
568 1220 $applicant_profile['max_floor_area_actual'] = $floor_area;
569 1221
570 1222 if ( isset($_POST['commercial_property_type']) && !empty($_POST['commercial_property_type']) )
571 1223 {
572 - $applicant_profile['commercial_property_types'] = is_array(ph_clean($_POST['commercial_property_type'])) ? ph_clean($_POST['commercial_property_type']) : array(ph_clean($_POST['commercial_property_type']));
1224 + $applicant_profile['commercial_property_types'] = $registration_input['commercial_property_type'];
573 1225 }
574 1226 }
575 1227
576 1228 if ( isset($_POST['location']) && !empty($_POST['location']) )
577 1229 {
578 - $applicant_profile['locations'] = is_array(ph_clean($_POST['location'])) ? ph_clean($_POST['location']) : array(ph_clean($_POST['location']));
1230 + $applicant_profile['locations'] = $registration_input['location'];
579 1231 }
580 1232
581 - $applicant_profile['notes'] = ( ( isset($_POST['additional_requirements']) ) ? sanitize_textarea_field($_POST['additional_requirements']) : '' );
1233 + if ( isset($_POST['location_text']) && !empty($_POST['location_text']) )
1234 + {
1235 + $applicant_profile['location_text'] = $registration_input['location_text'];
1236 + }
582 1237
1238 + $applicant_profile['notes'] = $registration_input['additional_requirements'];
1239 +
583 1240 $applicant_profile['send_matching_properties'] = 'yes';
584 1241 //$applicant_profile['auto_match_disabled'] = ''; // don't know what to do about this yet. Should probably look at global setting and reflect that
585 1242
586 - update_post_meta( $contact_post_id, '_applicant_profile_0', $applicant_profile );
1243 + update_post_meta( $contact_post_id, '_applicant_profile_0', wp_slash( $applicant_profile ) );
587 1244
588 1245 if ( get_option( 'propertyhive_applicant_users', '' ) == 'yes' )
589 1246 {
1247 + $display_name = wp_slash( $registration_input['name'] );
1248 +
590 1249 // Create user
591 1250 $userdata = array(
592 - 'display_name' => ph_clean($_POST['name']),
593 - 'user_login' => sanitize_email($_POST['email_address']),
594 - 'user_email' => sanitize_email($_POST['email_address']),
595 - 'user_pass' => ph_clean($_POST['password']),
1251 + 'display_name' => $display_name,
1252 + 'user_login' => sanitize_email( $registration_input['email_address'] ),
1253 + 'user_email' => sanitize_email( $registration_input['email_address'] ),
1254 + 'user_pass' => $registration_input['password'],
596 1255 'role' => 'property_hive_contact',
597 1256 'show_admin_bar_front' => 'false',
598 1257 );
599 1258
1259 + if ( !empty($display_name) )
1260 + {
1261 + $name_parts = explode( ' ', $display_name );
1262 +
1263 + if ( count($name_parts) > 1 )
1264 + {
1265 + $userdata['last_name'] = array_pop($name_parts);
1266 + $userdata['first_name'] = implode(' ', $name_parts);
1267 + }
1268 + else
1269 + {
1270 + $userdata['last_name'] = $display_name;
1271 + }
1272 + }
1273 +
600 1274 $user_id = wp_insert_user( $userdata );
601 1275
602 1276 //On success
603 1277 if ( ! is_wp_error( $user_id ) )
@@ -644,13 +1318,14 @@
644 1318
645 1319 $return = array(
646 1320 'success' => false,
647 1321 'errors' => array(),
1322 + 'new_details_nonce' => wp_create_nonce( "ph_userdetails" ),
648 1323 );
649 1324
650 1325 // Got an issue with nonce being declined on second submission.
651 1326 // Need to sort before putting this back in
652 - /*if ( check_ajax_referer( 'ph_details', 'security', false ) === FALSE )
1327 + if ( check_ajax_referer( 'ph_userdetails', 'ph_account_details_security', false ) === FALSE )
653 1328 {
654 1329 $return['errors'][] = 'Invalid nonce';
655 1330
656 1331 $this->json_headers();
@@ -657,9 +1332,9 @@
657 1332 echo json_encode( $return );
658 1333
659 1334 // Quit out
660 1335 die();
661 - }*/
1336 + }
662 1337
663 1338 // Validate
664 1339 $errors = array();
665 1340
@@ -677,8 +1352,22 @@
677 1352 // Quit out
678 1353 die();
679 1354 }
680 1355
1356 + $account_input = array();
1357 + foreach ( array( 'name', 'email_address', 'telephone_number', 'password', 'password2' ) as $input_key ) {
1358 + if ( isset( $_POST[$input_key] ) && ! is_string( $_POST[$input_key] ) ) {
1359 + $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
1360 + $account_input[$input_key] = '';
1361 + continue;
1362 + }
1363 + if ( in_array( $input_key, array( 'password', 'password2' ), true ) ) {
1364 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Passwords are opaque strings: type checked above and unslashed exactly once, never text-sanitized or modified before WordPress hashes them.
1365 + $account_input[$input_key] = isset( $_POST[$input_key] ) ? wp_unslash( $_POST[$input_key] ) : '';
1366 + } else {
1367 + $account_input[$input_key] = isset( $_POST[$input_key] ) ? sanitize_text_field( wp_unslash( $_POST[$input_key] ) ) : '';
1368 + }
1369 + }
681 1370 $form_controls = ph_get_user_details_form_fields();
682 1371
683 1372 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
684 1373
@@ -693,9 +1382,9 @@
693 1382 }
694 1383 }
695 1384 if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) )
696 1385 {
697 - if ( ! is_email( $_POST[$key] ) )
1386 + if ( ! is_string( $_POST[$key] ) || ! is_email( sanitize_email( wp_unslash( $_POST[$key] ) ) ) )
698 1387 {
699 1388 $errors[] = __( 'Invalid email address provided', 'propertyhive' );
700 1389 }
701 1390
@@ -703,13 +1392,27 @@
703 1392 }
704 1393 }
705 1394
706 1395 // Check password and password2 match
707 - if ( isset( $_POST['password'] ) && isset( $_POST['password2'] ) && !empty( $_POST['password'] ) && $_POST['password'] != $_POST['password2'] )
1396 + if ( isset( $_POST['password'] ) && isset( $_POST['password2'] ) && $account_input['password'] !== '' && $account_input['password'] !== $account_input['password2'] )
708 1397 {
709 1398 $errors[] = __( 'The passwords entered do not match', 'propertyhive' );
710 1399 }
711 1400
1401 + $user_roles = $current_user->roles;
1402 + $user_role = array_shift( $user_roles );
1403 + if ( 'property_hive_contact' === $user_role ) {
1404 + $existing_login_user = username_exists( sanitize_email( $account_input['email_address'] ) );
1405 + if ( $existing_login_user && (int) $existing_login_user !== $user_id ) {
1406 + $errors[] = __( 'This email address is already used as a login.', 'propertyhive' );
1407 + }
1408 + }
1409 +
1410 + $existing_email_user = email_exists( sanitize_email( $account_input['email_address'] ) );
1411 + if ( $existing_email_user && (int) $existing_email_user !== $user_id ) {
1412 + $errors[] = __( 'This email address is already registered to a user', 'propertyhive' );
1413 + }
1414 +
712 1415 if ( !empty($errors) )
713 1416 {
714 1417 // Failed validation
715 1418
@@ -719,46 +1422,52 @@
719 1422 }
720 1423 else
721 1424 {
722 1425 $contact = new PH_Contact( '', $user_id );
1426 + if ( empty( $contact->id ) || 'contact' !== get_post_type( $contact->id ) ) {
1427 + $return['reason'] = 'validation';
1428 + $return['errors'] = array( __( 'Unable to find your contact record. Please contact the agency.', 'propertyhive' ) );
1429 + wp_send_json( $return );
1430 + }
723 1431
724 1432 // create CPT
725 1433 $contact_post = array(
726 1434 'ID' => $contact->id,
727 - 'post_title' => ph_clean($_POST['name']),
1435 + 'post_title' => wp_slash( $account_input['name'] ),
728 1436 );
729 1437
730 1438 // Update the post in the database
731 1439 $contact_post_id = wp_update_post( $contact_post );
732 1440
733 - update_post_meta( $contact_post_id, '_email_address', sanitize_email($_POST['email_address']) );
1441 + update_post_meta( $contact_post_id, '_email_address', sanitize_email( $account_input['email_address'] ) );
734 1442 if (isset($_POST['telephone_number']))
735 1443 {
736 - update_post_meta( $contact_post_id, '_telephone_number', ph_clean($_POST['telephone_number']) );
1444 + update_post_meta( $contact_post_id, '_telephone_number', wp_slash( $account_input['telephone_number'] ) );
1445 + update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean_telephone_number( $account_input['telephone_number'] ) );
737 1446 }
738 1447
739 1448 // Update user
740 1449 $userdata = array(
741 1450 'ID' => $user_id,
742 - 'display_name' => ph_clean($_POST['name']),
743 - 'user_email' => sanitize_email($_POST['email_address']),
1451 + 'display_name' => wp_slash( $account_input['name'] ),
1452 + 'user_email' => sanitize_email( $account_input['email_address'] ),
744 1453 );
745 1454
746 1455 if ( isset($_POST['password']) && !empty($_POST['password']) )
747 1456 {
748 - $userdata['user_pass'] = ph_clean($_POST['password']);
1457 + $userdata['user_pass'] = $account_input['password'];
749 1458 }
750 1459
751 1460 $user_id = wp_update_user( $userdata );
752 1461
753 - $user_roles = $current_user->roles;
754 - $user_role = array_shift($user_roles);
755 -
756 - if ( $user_role === 'property_hive_contact' )
1462 + if ( ! is_wp_error( $user_id ) && $user_role === 'property_hive_contact' )
757 1463 {
758 1464 // Have to update login via SQL as wp_update_user won't allow altering
759 1465 // Only do it for property hive contacts though as admin or editor might be viewing this page
760 - $wpdb->update($wpdb->users, array('user_login' => sanitize_email($_POST['email_address'])), array('ID' => $user_id));
1466 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- WordPress cannot rename a login via wp_update_user; uniqueness is validated above, and old/new user caches are cleared immediately below.
1467 + $wpdb->update( $wpdb->users, array( 'user_login' => sanitize_email( $account_input['email_address'] ) ), array( 'ID' => $user_id ), array( '%s' ), array( '%d' ) );
1468 + clean_user_cache( $current_user );
1469 + clean_user_cache( $user_id );
761 1470 }
762 1471
763 1472 //On success
764 1473 if ( ! is_wp_error( $user_id ) )
@@ -794,13 +1503,14 @@
794 1503
795 1504 $return = array(
796 1505 'success' => false,
797 1506 'errors' => array(),
1507 + 'new_requirements_nonce' => wp_create_nonce( "ph_requirements" ),
798 1508 );
799 1509
800 1510 // Got an issue with nonce being declined on second submission.
801 1511 // Need to sort before putting this back in
802 - /*if ( check_ajax_referer( 'ph_requirements', 'security', false ) === FALSE )
1512 + if ( check_ajax_referer( 'ph_requirements', 'ph_account_requirements_security', false ) === FALSE )
803 1513 {
804 1514 $return['errors'][] = 'Invalid nonce';
805 1515
806 1516 $this->json_headers();
@@ -807,9 +1517,9 @@
807 1517 echo json_encode( $return );
808 1518
809 1519 // Quit out
810 1520 die();
811 - }*/
1521 + }
812 1522
813 1523 // Validate
814 1524 $errors = array();
815 1525
@@ -827,11 +1537,52 @@
827 1537 // Quit out
828 1538 die();
829 1539 }
830 1540
1541 + $contact = new PH_Contact( '', $user_id );
1542 +
1543 + $contact_post_id = $contact->id;
1544 +
1545 + if ( empty( $contact_post_id ) ) {
1546 + $errors[] = __( 'Unable to find your contact record. Please contact the agency.', 'propertyhive' );
1547 + }
1548 + $requirements_input = array();
1549 + foreach ( array( 'profile_id', 'department', 'maximum_price', 'maximum_rent', 'minimum_bedrooms', 'available_as_sale', 'available_as_rent', 'minimum_floor_area', 'maximum_floor_area', 'location_text', 'additional_requirements' ) as $input_key ) {
1550 + if ( isset( $_POST[$input_key] ) && ! is_string( $_POST[$input_key] ) ) {
1551 + $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
1552 + $requirements_input[$input_key] = '';
1553 + continue;
1554 + }
1555 + if ( 'additional_requirements' === $input_key ) {
1556 + $requirements_input[$input_key] = isset( $_POST[$input_key] ) ? sanitize_textarea_field( wp_unslash( $_POST[$input_key] ) ) : '';
1557 + } else {
1558 + $requirements_input[$input_key] = isset( $_POST[$input_key] ) ? sanitize_text_field( wp_unslash( $_POST[$input_key] ) ) : '';
1559 + }
1560 + }
1561 + foreach ( array( 'property_type', 'commercial_property_type', 'location' ) as $input_key ) {
1562 + $requirements_input[$input_key] = array();
1563 + if ( isset( $_POST[$input_key] ) ) {
1564 + if ( ! is_string( $_POST[$input_key] ) && ! is_array( $_POST[$input_key] ) ) {
1565 + $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
1566 + continue;
1567 + }
1568 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Validate element types before unslashing and sanitizing each accepted selection below.
1569 + foreach ( (array) $_POST[$input_key] as $selection ) {
1570 + if ( ! is_string( $selection ) ) {
1571 + $errors[] = __( 'Invalid field value', 'propertyhive' ) . ': ' . $input_key;
1572 + continue;
1573 + }
1574 + $requirements_input[$input_key][] = sanitize_text_field( wp_unslash( $selection ) );
1575 + }
1576 + }
1577 + }
1578 + if ( '' !== $requirements_input['profile_id'] && ! ctype_digit( $requirements_input['profile_id'] ) ) {
1579 + $errors[] = __( 'Invalid applicant profile', 'propertyhive' );
1580 + }
1581 + $profile_id = absint( $requirements_input['profile_id'] );
831 1582 $form_controls = ph_get_applicant_requirements_form_fields();
832 1583
833 - $form_controls = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls );
1584 + $form_controls = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls, get_post_meta( $contact_post_id, '_applicant_profile_' . $profile_id, true ) );
834 1585
835 1586 foreach ( $form_controls as $key => $control )
836 1587 {
837 1588 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
@@ -853,27 +1604,42 @@
853 1604 $return['errors'] = $errors;
854 1605 }
855 1606 else
856 1607 {
857 - $contact = new PH_Contact( '', $user_id );
1608 + $applicant_profile = array();
1609 + $applicant_profile['department'] = $requirements_input['department'];
858 1610
859 - $contact_post_id = $contact->id;
1611 + $base_department = $requirements_input['department'];
1612 + if ( !in_array( $base_department, array('residential-sales', 'residential-lettings', 'commercial') ) )
1613 + {
1614 + $base_department = ph_get_custom_department_based_on($base_department);
1615 + }
860 1616
861 - $applicant_profile = array();
862 - $applicant_profile['department'] = ph_clean($_POST['department']);
863 -
864 - if ( $_POST['department'] == 'residential-sales' )
1617 + if ( $base_department == 'residential-sales' )
865 1618 {
866 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_price']));
1619 + $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_price']);
867 1620
868 1621 $applicant_profile['max_price'] = $price;
869 1622
870 1623 // Not used yet but could be if introducing currencies in the future.
871 1624 $applicant_profile['max_price_actual'] = $price;
1625 +
1626 + $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
1627 + $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
1628 +
1629 + if ( $percentage_lower != '' && $percentage_higher != '' && $requirements_input['maximum_price'] != '' && $requirements_input['maximum_price'] != 0 )
1630 + {
1631 + $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_price']);
1632 + $applicant_profile['match_price_range_lower'] = $price - ( $price * ( $percentage_lower / 100 ) );
1633 + $applicant_profile['match_price_range_lower_actual'] = $price - ( $price * ( $percentage_lower / 100 ) );
1634 +
1635 + $applicant_profile['match_price_range_higher'] = $price + ( $price * ( $percentage_higher / 100 ) );
1636 + $applicant_profile['match_price_range_higher_actual'] = $price + ( $price * ( $percentage_higher / 100 ) );
1637 + }
872 1638 }
873 - elseif ( $_POST['department'] == 'residential-lettings' )
1639 + elseif ( $base_department == 'residential-lettings' )
874 1640 {
875 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_rent']));
1641 + $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_rent']);
876 1642
877 1643 $applicant_profile['max_rent'] = $price;
878 1644 $applicant_profile['rent_frequency'] = 'pcm';
879 1645 $price_actual = $price; // Stored in pcm
@@ -879,57 +1645,62 @@
879 1645 $price_actual = $price; // Stored in pcm
880 1646 $applicant_profile['max_price_actual'] = $price_actual;
881 1647 }
882 1648
883 - if ( $_POST['department'] == 'residential-sales' || $_POST['department'] == 'residential-lettings' )
1649 + if ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' )
884 1650 {
885 - $beds = preg_replace("/[^0-9]/", '', ph_clean($_POST['minimum_bedrooms']));
1651 + $beds = preg_replace("/[^0-9]/", '', $requirements_input['minimum_bedrooms']);
886 1652 $applicant_profile['min_beds'] = $beds;
887 1653
888 1654 if ( isset($_POST['property_type']) && !empty($_POST['property_type']) )
889 1655 {
890 - $applicant_profile['property_types'] = array(ph_clean($_POST['property_type']));
1656 + $applicant_profile['property_types'] = $requirements_input['property_type'];
891 1657 }
892 1658 }
893 1659
894 - if ( $_POST['department'] == 'commercial' )
1660 + if ( $base_department == 'commercial' )
895 1661 {
896 1662 $available_as = array();
897 - if ( isset($_POST['available_as_sale']) && $_POST['available_as_sale'] == 'yes' )
1663 + if ( isset($_POST['available_as_sale']) && $requirements_input['available_as_sale'] == 'yes' )
898 1664 {
899 1665 $available_as[] = 'sale';
900 1666 }
901 - if ( isset($_POST['available_as_rent']) && $_POST['available_as_rent'] == 'yes' )
1667 + if ( isset($_POST['available_as_rent']) && $requirements_input['available_as_rent'] == 'yes' )
902 1668 {
903 1669 $available_as[] = 'rent';
904 1670 }
905 1671 $applicant_profile['available_as'] = $available_as;
906 1672
907 - $floor_area = preg_replace("/[^0-9.]/", '', ph_clean($_POST['minimum_floor_area']));
1673 + $floor_area = preg_replace("/[^0-9.]/", '', $requirements_input['minimum_floor_area']);
908 1674 $applicant_profile['min_floor_area'] = $floor_area;
909 1675 $applicant_profile['min_floor_area_actual'] = $floor_area;
910 1676
911 - $floor_area = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_floor_area']));
1677 + $floor_area = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_floor_area']);
912 1678 $applicant_profile['max_floor_area'] = $floor_area;
913 1679 $applicant_profile['max_floor_area_actual'] = $floor_area;
914 1680
915 1681 if ( isset($_POST['commercial_property_type']) && !empty($_POST['commercial_property_type']) )
916 1682 {
917 - $applicant_profile['commercial_property_types'] = array(ph_clean($_POST['commercial_property_type']));
1683 + $applicant_profile['commercial_property_types'] = $requirements_input['commercial_property_type'];
918 1684 }
919 1685 }
920 1686
921 1687 if ( isset($_POST['location']) && !empty($_POST['location']) )
922 1688 {
923 - $applicant_profile['locations'] = array(ph_clean($_POST['location']));
1689 + $applicant_profile['locations'] = $requirements_input['location'];
924 1690 }
925 1691
926 - $applicant_profile['notes'] = ( ( isset($_POST['additional_requirements']) ) ? sanitize_textarea_field($_POST['additional_requirements']) : '' );
1692 + if ( isset($_POST['location_text']) && !empty($_POST['location_text']) )
1693 + {
1694 + $applicant_profile['location_text'] = $requirements_input['location_text'];
1695 + }
927 1696
1697 + $applicant_profile['notes'] = $requirements_input['additional_requirements'];
1698 +
928 1699 $applicant_profile['send_matching_properties'] = 'yes';
929 1700 //$applicant_profile['auto_match_disabled'] = ''; // don't know what to do about this yet. Should probably look at global setting and reflect that
930 1701
931 - update_post_meta( $contact_post_id, '_applicant_profile_0', $applicant_profile );
1702 + update_post_meta( $contact_post_id, '_applicant_profile_' . $profile_id, wp_slash( $applicant_profile ) );
932 1703
933 1704 $return['success'] = true;
934 1705
935 1706 do_action( 'propertyhive_account_requirements_updated', $contact_post_id, $user_id );
@@ -954,10 +1725,11 @@
954 1725 $return = array();
955 1726
956 1727 $property_query = new WP_Query(array(
957 1728 'post_type' => 'property',
958 - 'post_status' => 'any',
959 - 'nopaging' => true
1729 + 'post_status' => 'publish',
1730 + 'nopaging' => true,
1731 + 'fields' => 'ids',
960 1732 ));
961 1733
962 1734 if ($property_query->have_posts())
963 1735 {
@@ -964,14 +1736,14 @@
964 1736 while ($property_query->have_posts())
965 1737 {
966 1738 $property_query->the_post();
967 1739
968 - $num_property_features = get_post_meta($post->ID, '_features', TRUE);
1740 + $num_property_features = get_post_meta(get_the_ID(), '_features', TRUE);
969 1741 if ($num_property_features == '') { $num_property_features = 0; }
970 1742
971 1743 for ($i = 0; $i < $num_property_features; ++$i)
972 1744 {
973 - $feature = get_post_meta($post->ID, '_feature_' . $i, TRUE);
1745 + $feature = get_post_meta(get_the_ID(), '_feature_' . $i, TRUE);
974 1746 if (!in_array($feature, $return) && trim($feature) != '')
975 1747 {
976 1748 $return[] = $feature;
977 1749 }
@@ -991,19 +1763,19 @@
991 1763 public function load_existing_owner_contact() {
992 1764
993 1765 check_ajax_referer( 'load-existing-owner-contact', 'security' );
994 1766
995 - $contact_id = (int)$_POST['contact_id'];
1767 + $contact_id = isset( $_POST['contact_id'] ) && is_scalar( $_POST['contact_id'] ) ? absint( $_POST['contact_id'] ) : 0;
996 1768
997 - $contact = get_post($contact_id);
1769 + $contact = $contact_id > 0 && 'contact' === get_post_type( $contact_id ) ? get_post( $contact_id ) : null;
998 1770
999 - echo '<div id="existing-owner-details-' . $contact_id . '">';
1771 + echo '<div id="existing-owner-details-' . esc_attr($contact_id) . '">';
1000 1772
1001 1773 if ( !is_null( $contact ) )
1002 1774 {
1003 1775 echo '<p class="form-field">';
1004 - echo '<label>' . __('Name', 'propertyhive') . '</label>';
1005 - echo '<a href="' . get_edit_post_link( $contact_id ) . '">' . get_the_title($contact_id) . '</a>';
1776 + echo '<label>' . esc_html(__('Name', 'propertyhive')) . '</label>';
1777 + echo '<a href="' . esc_url(get_edit_post_link( $contact_id )) . '">' . esc_html(get_the_title($contact_id)) . '</a>';
1006 1778 echo '</p>';
1007 1779
1008 1780 $address = array();
1009 1781 $address_elements = array( '_address_name_number', '_address_street', '_address_two', '_address_three', '_address_four', '_address_postcode' );
@@ -1015,30 +1787,42 @@
1015 1787 }
1016 1788 }
1017 1789
1018 1790 echo '<p class="form-field">';
1019 - echo '<label>' . __('Address', 'propertyhive') . '</label>';
1020 - echo ( ( !empty($address) ) ? implode(", ", $address) : '-' );
1791 + echo '<label>' . esc_html(__('Address', 'propertyhive')) . '</label>';
1792 + echo ( ( !empty($address) ) ? esc_html(implode(", ", $address)) : '-' );
1021 1793 echo '</p>';
1022 1794
1023 1795 echo '<p class="form-field">';
1024 - echo '<label>' . __('Telephone Number', 'propertyhive') . '</label>';
1025 - echo ( ( get_post_meta($contact_id, '_telephone_number', TRUE) != '' ) ? get_post_meta($contact_id, '_telephone_number', TRUE) : '-' );
1796 + echo '<label>' . esc_html(__('Telephone Number', 'propertyhive')) . '</label>';
1797 + echo ( ( get_post_meta($contact_id, '_telephone_number', TRUE) != '' ) ? esc_html(get_post_meta($contact_id, '_telephone_number', TRUE)) : '-' );
1026 1798 echo '</p>';
1027 1799
1028 1800 echo '<p class="form-field">';
1029 - echo '<label>' . __('Email Address', 'propertyhive') . '</label>';
1030 - echo ( ( get_post_meta($contact_id, '_email_address', TRUE) != '' ) ? get_post_meta($contact_id, '_email_address', TRUE) : '-' );
1801 + echo '<label>' . esc_html(__('Email Address', 'propertyhive')) . '</label>';
1802 + echo ( ( get_post_meta($contact_id, '_email_address', TRUE) != '' ) ? esc_html(get_post_meta($contact_id, '_email_address', TRUE)) : '-' );
1031 1803 echo '</p>';
1804 +
1805 + $contact_solicitor_contact_id = get_post_meta( $contact_id, '_contact_solicitor_contact_id', true );
1806 +
1807 + if ( !empty($contact_solicitor_contact_id) )
1808 + {
1809 + $solicitor_contact = new PH_Contact($contact_solicitor_contact_id);
1810 +
1811 + echo '<p class="form-field">';
1812 + echo '<label>' . esc_html(__('Solicitor', 'propertyhive')) . '</label>';
1813 + echo '<a href="' . esc_url(get_edit_post_link($contact_solicitor_contact_id, '')) . '">' . esc_html(get_the_title($contact_solicitor_contact_id) . ( $solicitor_contact->company_name != '' && $solicitor_contact->company_name != get_the_title($contact_solicitor_contact_id) ? ' (' . $solicitor_contact->company_name . ')' : '' )) . '</a>';
1814 + echo '</p>';
1815 + }
1032 1816 }
1033 1817 else
1034 1818 {
1035 - echo __( 'Invalid contact record', 'propertyhive' );
1819 + echo esc_html(__( 'Invalid contact record', 'propertyhive' ));
1036 1820 }
1037 1821
1038 1822 echo '<p class="form-field">';
1039 1823 echo '<label></label>';
1040 - echo '<a href="" class="button" id="remove-owner-contact-' . $contact_id . '">Remove Owner</a> ';
1824 + echo '<a href="" class="button" id="remove-owner-contact-' . esc_attr($contact_id) . '">Remove Owner</a> ';
1041 1825 echo '<a href="" class="button add-additional-owner-contact">Add Additional Owner</a>';
1042 1826 echo '</p>';
1043 1827
1044 1828 echo '</div>';
@@ -1058,9 +1842,11 @@
1058 1842 check_ajax_referer( 'search-contacts', 'security' );
1059 1843
1060 1844 $return = array();
1061 1845
1062 - $keyword = ph_clean($_POST['keyword']);
1846 + $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
1847 + $contact_type = isset( $_POST['contact_type'] ) && is_string( $_POST['contact_type'] ) ? sanitize_text_field( wp_unslash( $_POST['contact_type'] ) ) : '';
1848 + $exclude_ids = isset( $_POST['exclude_ids'] ) && is_string( $_POST['exclude_ids'] ) ? sanitize_text_field( wp_unslash( $_POST['exclude_ids'] ) ) : '';
1063 1849
1064 1850 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1065 1851 {
1066 1852 // Get all contacts that match the name
@@ -1065,22 +1851,29 @@
1065 1851 {
1066 1852 // Get all contacts that match the name
1067 1853 $args = array(
1068 1854 'post_type' => 'contact',
1855 + 'propertyhive_contact_search_keyword' => $keyword,
1069 1856 'nopaging' => true,
1070 - 'post_status' => array( 'publish' ),
1857 + 'post_status' => array( 'publish', 'private' ),
1071 1858 'fields' => 'ids'
1072 1859 );
1073 - if ( isset($_POST['contact_type']) && $_POST['contact_type'] != '' )
1860 + if ( '' !== $contact_type )
1074 1861 {
1862 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Contact roles are stored in legacy contact metadata; preserve complete keyword-matched ID results and caller exclusions.
1075 1863 $args['meta_query'] = array(
1076 1864 array(
1077 1865 'key' => '_contact_types',
1078 - 'value' => ph_clean($_POST['contact_type']),
1866 + 'value' => $contact_type,
1079 1867 'compare' => 'LIKE',
1080 1868 )
1081 1869 );
1082 1870 }
1871 + if ( '' !== $exclude_ids )
1872 + {
1873 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Contact roles are stored in legacy contact metadata; preserve complete keyword-matched ID results and caller exclusions.
1874 + $args['post__not_in'] = array_map( 'absint', explode( '|', $exclude_ids ) );
1875 + }
1083 1876
1084 1877 add_filter( 'posts_where', array( $this, 'search_contacts_where' ), 10, 2 );
1085 1878
1086 1879 $contact_query = new WP_Query( $args );
@@ -1096,9 +1889,9 @@
1096 1889 $contact = new PH_Contact( get_the_ID() );
1097 1890
1098 1891 $return[] = array(
1099 1892 'ID' => get_the_ID(),
1100 - 'post_title' => get_the_title(get_the_ID()),
1893 + 'post_title' => get_the_title(get_the_ID()) . ( $contact_type == 'thirdparty' && $contact->company_name != '' && $contact->company_name != get_the_title(get_the_ID()) ? ' (' . $contact->company_name . ')' : '' ) ,
1101 1894 'address_name_number' => $contact->_address_name_number,
1102 1895 'address_street' => $contact->_address_street,
1103 1896 'address_two' => $contact->_address_two,
1104 1897 'address_three' => $contact->_address_three,
@@ -1104,9 +1897,11 @@
1104 1897 'address_three' => $contact->_address_three,
1105 1898 'address_four' => $contact->_address_four,
1106 1899 'address_postcode' => $contact->_address_postcode,
1107 1900 'address_country' => $contact->_address_country,
1108 - 'address_full_formatted' => $contact->get_formatted_full_address('<br>'),
1901 + 'address_full_formatted' => $contact->get_formatted_full_address(', '),
1902 + 'telephone_number' => $contact->_telephone_number,
1903 + 'email_address' => $contact->_email_address,
1109 1904 );
1110 1905 }
1111 1906 }
1112 1907
@@ -1119,14 +1914,18 @@
1119 1914 // Quit out
1120 1915 die();
1121 1916 }
1122 1917
1123 - public function search_contacts_where( $where, &$wp_query )
1918 + public function search_contacts_where( $where, $wp_query )
1124 1919 {
1125 1920 global $wpdb;
1126 1921
1127 - $where .= ' AND ' . $wpdb->posts . '.post_title LIKE \'%' . esc_sql( like_escape( ph_clean($_POST['keyword']) ) ) . '%\'';
1128 -
1922 + $keyword = $wp_query->get( 'propertyhive_contact_search_keyword', '' );
1923 + if ( ! is_string( $keyword ) || '' === $keyword ) {
1924 + return $where;
1925 + }
1926 + $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_title LIKE %s", '%' . $wpdb->esc_like( $keyword ) . '%' );
1927 +
1129 1928 return $where;
1130 1929 }
1131 1930
1132 1931 /**
@@ -1139,9 +1938,9 @@
1139 1938 check_ajax_referer( 'search-properties', 'security' );
1140 1939
1141 1940 $return = array();
1142 1941
1143 - $keyword = ph_clean($_POST['keyword']);
1942 + $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
1144 1943
1145 1944 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1146 1945 {
1147 1946 // Get all contacts that match the name
@@ -1147,33 +1946,78 @@
1147 1946 // Get all contacts that match the name
1148 1947 $args = array(
1149 1948 'post_type' => 'property',
1150 1949 'nopaging' => true,
1151 - 'post_status' => array( 'publish' ),
1950 + 'post_status' => array( 'publish', 'draft', 'private' ),
1152 1951 'fields' => 'ids'
1153 1952 );
1154 1953
1155 - $meta_query = array();
1156 - if ( isset($_POST['department']) && $_POST['department'] != '' )
1954 + $meta_query = array(
1955 + array(
1956 + 'relation' => 'OR',
1957 + array(
1958 + 'key' => '_address_concatenated',
1959 + 'value' => $keyword,
1960 + 'compare' => 'LIKE'
1961 + ),
1962 + array(
1963 + 'key' => '_reference_number',
1964 + 'value' => $keyword,
1965 + 'compare' => '='
1966 + ),
1967 + ),
1968 + );
1969 +
1970 + $department_input = isset( $_POST['department'] ) && is_string( $_POST['department'] ) ? sanitize_text_field( wp_unslash( $_POST['department'] ) ) : '';
1971 + if ( '' !== $department_input )
1157 1972 {
1158 - $meta_query[] = array(
1159 - 'key' => '_department',
1160 - 'value' => ph_clean($_POST['department']),
1973 + $departments_query = array(
1974 + 'relation' => 'OR',
1161 1975 );
1976 +
1977 + $explode_departments = explode("|", $department_input);
1978 + $new_departments = array();
1979 + foreach ( $explode_departments as $department )
1980 + {
1981 + $explode_department = explode("~", $department);
1982 +
1983 + $new_departments[] = $explode_department[0];
1984 +
1985 + $departments_sub_query = array();
1986 +
1987 + $departments_sub_query[] = array(
1988 + 'key' => '_department',
1989 + 'value' => $explode_department[0],
1990 + );
1991 +
1992 + if ( $explode_department[0] == 'commercial' && isset($explode_department[1]) )
1993 + {
1994 + switch ($explode_department[1])
1995 + {
1996 + case "forsale":
1997 + {
1998 + $departments_sub_query[] = array(
1999 + 'key' => '_for_sale',
2000 + 'value' => 'yes',
2001 + );
2002 + break;
2003 + }
2004 + }
2005 + }
2006 +
2007 + $departments_query[] = $departments_sub_query;
2008 + }
2009 + $meta_query[] = $departments_query;
1162 2010 }
2011 +
1163 2012 if ( !empty($meta_query) )
1164 2013 {
2014 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Department/market filters use existing property metadata; preserve the established property-search result set.
1165 2015 $args['meta_query'] = $meta_query;
1166 2016 }
1167 2017
1168 - add_filter( 'posts_join', array( $this, 'search_properties_join' ), 10, 2 );
1169 - add_filter( 'posts_where', array( $this, 'search_properties_where' ), 10, 2 );
1170 -
1171 2018 $property_query = new WP_Query( $args );
1172 2019
1173 - remove_filter( 'posts_join', array( $this, 'search_properties_join' ) );
1174 - remove_filter( 'posts_where', array( $this, 'search_properties_where' ) );
1175 -
1176 2020 if ( $property_query->have_posts() )
1177 2021 {
1178 2022 while ( $property_query->have_posts() )
1179 2023 {
@@ -1190,12 +2034,18 @@
1190 2034 $owner_id = reset($owner_id);
1191 2035 }
1192 2036 $owner_name = get_the_title($owner_id);
1193 2037 }
2038 +
2039 + $post_title = $property->get_formatted_full_address();
2040 + if ( get_post_status() == 'draft' )
2041 + {
2042 + $post_title .= ' - Draft';
2043 + }
1194 2044
1195 2045 $return[] = array(
1196 2046 'ID' => get_the_ID(),
1197 - 'post_title' => $property->get_formatted_full_address(),
2047 + 'post_title' => $post_title,
1198 2048 'owner_id' => $owner_id,
1199 2049 'owner_name' => $owner_name
1200 2050 );
1201 2051 }
@@ -1210,38 +2060,8 @@
1210 2060 // Quit out
1211 2061 die();
1212 2062 }
1213 2063
1214 - public function search_properties_join( $joins )
1215 - {
1216 - global $wpdb;
1217 -
1218 - $joins .= " INNER JOIN {$wpdb->postmeta} AS mt1 ON {$wpdb->posts}.ID = mt1.post_id ";
1219 -
1220 - return $joins;
1221 - }
1222 -
1223 - public function search_properties_where( $where )
1224 - {
1225 - $where .= " AND (
1226 - (mt1.meta_key='_address_name_number' AND mt1.meta_value LIKE '" . esc_sql(ph_clean($_POST['keyword'])) . "%')
1227 - OR
1228 - (mt1.meta_key='_address_street' AND mt1.meta_value LIKE '" . esc_sql(ph_clean($_POST['keyword'])) . "%')
1229 - OR
1230 - (mt1.meta_key='_address_2' AND mt1.meta_value LIKE '" . esc_sql(ph_clean($_POST['keyword'])) . "%')
1231 - OR
1232 - (mt1.meta_key='_address_3' AND mt1.meta_value LIKE '" . esc_sql(ph_clean($_POST['keyword'])) . "%')
1233 - OR
1234 - (mt1.meta_key='_address_4' AND mt1.meta_value LIKE '" . esc_sql(ph_clean($_POST['keyword'])) . "%')
1235 - OR
1236 - (mt1.meta_key='_address_postcode' AND mt1.meta_value LIKE '" . esc_sql(ph_clean($_POST['keyword'])) . "%')
1237 - OR
1238 - (mt1.meta_key='_reference_number' AND mt1.meta_value = '" . esc_sql(ph_clean($_POST['keyword'])) . "')
1239 - ) ";
1240 -
1241 - return $where;
1242 - }
1243 -
1244 2064 /**
1245 2065 * Search users/negotiators via ajax
1246 2066 */
1247 2067 public function search_negotiators() {
@@ -1251,9 +2071,9 @@
1251 2071 check_ajax_referer( 'search-negotiators', 'security' );
1252 2072
1253 2073 $return = array();
1254 2074
1255 - $keyword = ph_clean($_POST['keyword']);
2075 + $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
1256 2076
1257 2077 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1258 2078 {
1259 2079 // Get all contacts that match the name
@@ -1260,10 +2080,13 @@
1260 2080 $args = array(
1261 2081 'number' => 9999,
1262 2082 'search' => $keyword . '*',
1263 2083 'orderby' => 'display_name',
1264 - 'role__not_in' => array('property_hive_contact')
2084 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy Property Negotiator compatibility filter; existing role filters depend on this exact public hook name.
2085 + 'role__not_in' => apply_filters( 'property_negotiator_exclude_roles', array('property_hive_contact', 'subscriber') )
1265 2086 );
2087 +
2088 + $args = apply_filters( 'propertyhive_negotiators_query', $args );
1266 2089
1267 2090 $user_query = new WP_User_Query( $args );
1268 2091
1269 2092 // Get the results
@@ -1295,17 +2118,34 @@
1295 2118 */
1296 2119 public function add_note() {
1297 2120
1298 2121 check_ajax_referer( 'add-note', 'security' );
2122 +
2123 + if ( ! current_user_can( 'manage_propertyhive' ) )
2124 + wp_die( esc_html(__( 'You do not have permission to manage notes', 'propertyhive' )), 403 );
1299 2125
1300 - $post_id = (int)$_POST['post_id'];
2126 + $post_id = isset( $_POST['post_id'] ) && is_scalar( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
2127 + if ( $post_id < 1 || ! get_post( $post_id ) || ! current_user_can( 'edit_post', $post_id ) || ! isset( $_POST['note'] ) || ! is_string( $_POST['note'] ) ) {
2128 + wp_send_json_error( __( 'Invalid note or insufficient permissions.', 'propertyhive' ), 403 );
2129 + }
1301 2130
1302 2131 if ( $post_id > 0 ) {
1303 2132
1304 - $current_user = wp_get_current_user();
2133 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Rich mention spans are converted to the established text token below, then all HTML is stripped before storage.
2134 + $note = trim( wp_unslash( $_POST['note'] ) );
1305 2135
1306 - $note = wp_kses_post( trim( stripslashes( $_POST['note'] ) ) );
2136 + $pattern = '/<span [^>]*data-post-id="(\d+)"[^>]*>([^<]*)<\/span>/i';
2137 + $replacement = function($matches) {
2138 + $post_id = $matches[1];
2139 + $text = $matches[2];
2140 + return '{{mention-' . $post_id . '|' . $text . '}}';
2141 + };
2142 + $note = preg_replace_callback($pattern, $replacement, $note);
1307 2143
2144 + $note = str_replace( array('<br>', '<br />'), "\n", $note );
2145 +
2146 + $note = wp_strip_all_tags( $note );
2147 +
1308 2148 // Add note/comment to property
1309 2149 $comment = array(
1310 2150 'note_type' => 'note',
1311 2151 'note' => $note
@@ -1310,33 +2150,27 @@
1310 2150 'note_type' => 'note',
1311 2151 'note' => $note
1312 2152 );
1313 2153
1314 - $data = array(
1315 - 'comment_post_ID' => $post_id,
1316 - 'comment_author' => $current_user->display_name,
1317 - 'comment_author_email' => '[email protected]',
1318 - 'comment_author_url' => '',
1319 - 'comment_date' => date("Y-m-d H:i:s"),
1320 - 'comment_content' => serialize($comment),
1321 - 'comment_approved' => 1,
1322 - 'comment_type' => 'propertyhive_note',
1323 - );
1324 - $comment_id = wp_insert_comment( $data );
2154 + if ( isset($_POST['pinned']) )
2155 + {
2156 + $comment['pinned'] = '1';
2157 + }
1325 2158
2159 + $comment_id = PH_Comments::insert_note( $post_id, $comment );
2160 +
1326 2161 if ($comment_id !== FALSE)
1327 2162 {
1328 2163 $comment = get_comment($comment_id);
1329 -
1330 2164 ?>
1331 2165 <li rel="<?php echo absint( $comment_id ) ; ?>" class="note">
1332 2166 <div class="note_content">
1333 - <?php echo wpautop( wptexturize( wp_kses_post( $note ) ) ); ?>
2167 + <?php echo wp_kses_post( wpautop( wptexturize( wp_kses_post( $note ) ) ) ); ?>
1334 2168 </div>
1335 2169 <p class="meta">
1336 - <abbr class="exact-date" title="<?php echo $comment->comment_date_gmt; ?> GMT"><?php printf( __( '%s ago', 'propertyhive' ), human_time_diff( strtotime( $comment->comment_date_gmt ), current_time( 'timestamp', 1 ) ) ); ?></abbr>
1337 - <?php if ( $comment->comment_author !== __( 'Property Hive', 'propertyhive' ) ) printf( ' ' . __( 'by %s', 'propertyhive' ), $comment->comment_author ); ?>
1338 - <a href="#" class="delete_note"><?php _e( 'Delete', 'propertyhive' ); ?></a>
2170 + <abbr class="exact-date" title="<?php echo esc_attr($comment->comment_date_gmt); ?> GMT"><?php /* translators: %s: Elapsed time. */ printf( esc_html__( '%s ago', 'propertyhive' ), esc_html( human_time_diff( strtotime( $comment->comment_date_gmt ), current_time( 'timestamp', 1 ) ) ) ); ?></abbr>
2171 + <?php if ( $comment->comment_author !== esc_html__( 'Property Hive', 'propertyhive' ) ) /* translators: %s: Note author. */ printf( ' ' . esc_html__( 'by %s', 'propertyhive' ), esc_html( $comment->comment_author ) ); ?>
2172 + <a href="#" class="delete_note"><?php echo esc_html(__( 'Delete', 'propertyhive' )); ?></a>
1339 2173 </p>
1340 2174 </li>
1341 2175 <?php
1342 2176 }
@@ -1343,9 +2177,9 @@
1343 2177 }
1344 2178
1345 2179 // Quit out
1346 2180 die();
1347 - }
2181 + }
1348 2182
1349 2183 /**
1350 2184 * Delete order note via ajax
1351 2185 */
@@ -1352,19 +2186,229 @@
1352 2186 public function delete_note() {
1353 2187
1354 2188 check_ajax_referer( 'delete-note', 'security' );
1355 2189
1356 - $note_id = (int)$_POST['note_id'];
2190 + if ( ! current_user_can( 'manage_propertyhive' ) )
2191 + wp_send_json_error( __( 'You do not have permission to manage notes', 'propertyhive' ), 403 );
1357 2192
2193 + $note_id = isset( $_POST['note_id'] ) && is_scalar( $_POST['note_id'] ) ? absint( $_POST['note_id'] ) : 0;
2194 + $note_comment = get_comment( $note_id );
2195 + if ( $note_id < 1 || ! $note_comment || 'propertyhive_note' !== $note_comment->comment_type || ! current_user_can( 'edit_post', $note_comment->comment_post_ID ) ) {
2196 + wp_send_json_error( __( 'Invalid note or insufficient permissions.', 'propertyhive' ), 403 );
2197 + }
2198 +
1358 2199 if ( $note_id > 0 ) {
1359 2200 wp_delete_comment( $note_id );
2201 +
2202 + wp_send_json_success();
1360 2203 }
1361 2204
1362 - // Quit out
1363 - die();
2205 + wp_send_json_error();
1364 2206 }
1365 -
2207 +
1366 2208 /**
2209 + * Change existing note entry to be pinned
2210 + */
2211 + public function toggle_note_pinned() {
2212 +
2213 + check_ajax_referer( 'pin-note', 'security' );
2214 +
2215 + if ( ! current_user_can( 'manage_propertyhive' ) )
2216 + wp_send_json_error( __( 'You do not have permission to manage notes', 'propertyhive' ), 403 );
2217 +
2218 + $note_id = isset( $_POST['note_id'] ) && is_scalar( $_POST['note_id'] ) ? absint( $_POST['note_id'] ) : 0;
2219 + $note_comment = get_comment( $note_id );
2220 + if ( $note_id < 1 || ! $note_comment || 'propertyhive_note' !== $note_comment->comment_type || ! current_user_can( 'edit_post', $note_comment->comment_post_ID ) ) {
2221 + wp_send_json_error( __( 'Invalid note or insufficient permissions.', 'propertyhive' ), 403 );
2222 + }
2223 +
2224 + if ( $note_id > 0 ) {
2225 +
2226 + $comment = get_comment($note_id);
2227 + $comment_content = @unserialize($comment->comment_content, ['allowed_classes' => false]);
2228 +
2229 + if ( is_array( $comment_content ) )
2230 + {
2231 + if ( isset($comment_content['pinned']))
2232 + {
2233 + unset($comment_content['pinned']);
2234 + }
2235 + else
2236 + {
2237 + $comment_content['pinned'] = '1';
2238 + }
2239 + }
2240 +
2241 + else {
2242 + wp_send_json_error( __( 'Invalid note data.', 'propertyhive' ), 400 );
2243 + }
2244 + wp_update_comment( wp_slash( array( 'comment_ID' => $note_id, 'comment_content' => serialize( $comment_content ) ) ) );
2245 +
2246 + wp_send_json_success();
2247 + }
2248 +
2249 + wp_send_json_error();
2250 + }
2251 +
2252 + public function get_notes_grid() {
2253 +
2254 + global $wpdb, $post;
2255 +
2256 + check_ajax_referer( 'get-notes', 'security' );
2257 +
2258 + if ( ! current_user_can( 'manage_propertyhive' ) )
2259 + wp_die( esc_html(__( 'You do not have permission to manage notes', 'propertyhive' )), 403 );
2260 +
2261 + $post_id = isset( $_POST['post_id'] ) && is_scalar( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
2262 + $post = get_post( $post_id );
2263 + if ( $post_id < 1 || ! $post || ! current_user_can( 'edit_post', $post_id ) ) {
2264 + wp_send_json_error( __( 'Invalid record or insufficient permissions.', 'propertyhive' ), 403 );
2265 + }
2266 +
2267 + $section = isset( $_POST['section'] ) && is_string( $_POST['section'] ) ? sanitize_text_field( wp_unslash( $_POST['section'] ) ) : '';
2268 + include( PH()->plugin_path() . '/includes/admin/views/html-display-notes.php' );
2269 +
2270 + // Quit out
2271 + die();
2272 + }
2273 +
2274 + public function get_pinned_notes_grid() {
2275 +
2276 + global $wpdb, $post;
2277 +
2278 + check_ajax_referer( 'get-notes', 'security' );
2279 +
2280 + if ( ! current_user_can( 'manage_propertyhive' ) )
2281 + wp_die( esc_html(__( 'You do not have permission to manage notes', 'propertyhive' )), 403 );
2282 +
2283 + $post_id = isset( $_POST['post_id'] ) && is_scalar( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
2284 + $post = get_post( $post_id );
2285 + if ( $post_id < 1 || ! $post || ! current_user_can( 'edit_post', $post_id ) ) {
2286 + wp_send_json_error( __( 'Invalid record or insufficient permissions.', 'propertyhive' ), 403 );
2287 + }
2288 +
2289 + $section = isset( $_POST['section'] ) && is_string( $_POST['section'] ) ? sanitize_text_field( wp_unslash( $_POST['section'] ) ) : '';
2290 + include( PH()->plugin_path() . '/includes/admin/views/html-display-notes.php' );
2291 +
2292 + // Quit out
2293 + die();
2294 + }
2295 +
2296 + public function fetch_note_mentions() {
2297 +
2298 + global $wpdb;
2299 +
2300 + check_ajax_referer( 'get-notes', 'security' );
2301 +
2302 + if ( ! current_user_can( 'manage_propertyhive' ) )
2303 + wp_die( esc_html(__( 'You do not have permission to manage notes', 'propertyhive' )), 403 );
2304 +
2305 + $query = isset( $_POST['query'] ) && is_string( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
2306 +
2307 + $mentions = array();
2308 +
2309 + // Get contacts
2310 + $args = array(
2311 + 'post_type' => 'contact',
2312 + 'posts_per_page' => 10,
2313 + 'post_status' => array( 'publish' ),
2314 + 's' => $query
2315 + );
2316 +
2317 + $contacts_query = new WP_Query( $args );
2318 +
2319 + if ( $contacts_query->have_posts() )
2320 + {
2321 + while ( $contacts_query->have_posts() )
2322 + {
2323 + $contacts_query->the_post();
2324 +
2325 + $contact = new PH_Contact(get_the_ID());
2326 +
2327 + $details = array();
2328 + if ( $contact->get_formatted_full_address() != '' )
2329 + {
2330 + $details[] = $contact->get_formatted_full_address();
2331 + }
2332 + if ( $contact->email_address != '' || $contact->telephone_number != '' )
2333 + {
2334 + $sub_details = array();
2335 + if ( $contact->email_address != '' )
2336 + {
2337 + $sub_details[] = 'E: ' . $contact->email_address;
2338 + }
2339 + if ( $contact->telephone_number != '' )
2340 + {
2341 + $sub_details[] = 'T: ' . $contact->telephone_number;
2342 + }
2343 + $details[] = implode(" | ", $sub_details);
2344 + }
2345 +
2346 + $mentions[] = array(
2347 + 'type' => 'contact',
2348 + 'id' => get_the_ID(),
2349 + 'name' => get_the_title(),
2350 + 'details' => implode("<br>", $details),
2351 + );
2352 + }
2353 + }
2354 + wp_reset_postdata();
2355 +
2356 + // Get properties
2357 + $args = array(
2358 + 'post_type' => 'property',
2359 + 'posts_per_page' => 10,
2360 + 'post_status' => array( 'publish' ),
2361 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Login/contact duplicate/address lookups use a fixed meta relation and return a small result set (1 row for identity checks, 10 for the address autocomplete). posts_per_page=1; posts_per_page=10; fields=ids on all four; values are the authenticated user, submitted email, search text, or current contact.
2362 + 'meta_query' => array(
2363 + 'relation' => 'OR',
2364 + array(
2365 + 'key' => '_address_concatenated',
2366 + 'value' => $query,
2367 + 'compare' => 'LIKE'
2368 + ),
2369 + array(
2370 + 'key' => '_reference_number',
2371 + 'value' => $query,
2372 + 'compare' => '='
2373 + )
2374 + )
2375 + );
2376 +
2377 + $properties_query = new WP_Query( $args );
2378 +
2379 + if ( $properties_query->have_posts() )
2380 + {
2381 + while ( $properties_query->have_posts() )
2382 + {
2383 + $properties_query->the_post();
2384 +
2385 + $property = new PH_Property(get_the_ID());
2386 +
2387 + $details = array();
2388 + if ( $property->get_formatted_price() != '' )
2389 + {
2390 + $details[] = $property->get_formatted_price();
2391 + }
2392 + if ( $property->property_type != '' )
2393 + {
2394 + $details[] = $property->property_type;
2395 + }
2396 +
2397 + $mentions[] = array(
2398 + 'type' => 'property',
2399 + 'id' => get_the_ID(),
2400 + 'name' => $property->get_formatted_full_address(),
2401 + 'details' => implode(" | ", $details),
2402 + );
2403 + }
2404 + }
2405 + wp_reset_postdata();
2406 +
2407 + wp_send_json($mentions);
2408 + }
2409 +
2410 + /**
1367 2411 * Delete order note via ajax
1368 2412 */
1369 2413 public function make_property_enquiry() {
1370 2414
@@ -1375,11 +2419,12 @@
1375 2419 // Validate
1376 2420 $errors = array();
1377 2421 $form_controls = array();
1378 2422
1379 - if ( ! isset( $_POST['property_id'] ) || ( isset( $_POST['property_id'] ) && empty( $_POST['property_id'] ) ) )
2423 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2424 + if ( ! isset( $_POST['property_id'] ) || ! is_string( $_POST['property_id'] ) || empty( $_POST['property_id'] ) )
1380 2425 {
1381 - $errors[] = __( 'Property ID is a required field and must be supplied when making an enquiry', 'propertyhive' ) . ': ' . $key;
2426 + $errors[] = __( 'Property ID is a required field and must be supplied when making an enquiry', 'propertyhive' );
1382 2427 }
1383 2428 else
1384 2429 {
1385 2430 //$post = get_post((int)$_POST['property_id']);
@@ -1385,9 +2430,10 @@
1385 2430 //$post = get_post((int)$_POST['property_id']);
1386 2431
1387 2432 $form_controls = ph_get_property_enquiry_form_fields();
1388 2433
1389 - $form_controls = apply_filters( 'propertyhive_property_enquiry_form_fields', $form_controls );
2434 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2435 + $form_controls = apply_filters( 'propertyhive_property_enquiry_form_fields', $form_controls, sanitize_text_field( wp_unslash( $_POST['property_id'] ) ) );
1390 2436 }
1391 2437
1392 2438 foreach ( $form_controls as $key => $control )
1393 2439 {
@@ -1393,30 +2439,38 @@
1393 2439 {
1394 2440 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
1395 2441 {
1396 2442 // This field is mandatory. Lets check we received it in the post
2443 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
1397 2444 if ( ! isset( $_POST[$key] ) || ( isset( $_POST[$key] ) && empty( $_POST[$key] ) ) )
1398 2445 {
1399 2446 $errors[] = __( 'Missing required field', 'propertyhive' ) . ': ' . $key;
1400 2447 }
1401 2448 }
1402 - if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) && ! is_email( $_POST[$key] ) )
2449 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2450 + if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) && ( ! is_string( $_POST[$key] ) || ! is_email( wp_unslash( $_POST[$key] ) ) ) )
1403 2451 {
1404 2452 $errors[] = __( 'Invalid email address provided', 'propertyhive' ) . ': ' . $key;
1405 2453 }
1406 - if ( $key == 'recaptcha' )
2454 + if ( in_array( $key, array('recaptcha', 'recaptcha-v3') ) )
1407 2455 {
2456 + $errors = $this->check_recaptcha_form_response($errors, $key, $control);
2457 + }
2458 + if ( $key == 'hCaptcha' )
2459 + {
1408 2460 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
1409 - $response = isset( $_POST['g-recaptcha-response'] ) ? ph_clean($_POST['g-recaptcha-response']) : '';
2461 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2462 + $response = ( isset( $_POST['h-captcha-response'] ) && is_string( $_POST['h-captcha-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['h-captcha-response'] ) ) : '';
1410 2463
1411 - $response = wp_remote_post(
1412 - 'https://www.google.com/recaptcha/api/siteverify',
2464 + $response = wp_remote_post(
2465 + 'https://hcaptcha.com/siteverify',
1413 2466 array(
1414 2467 'method' => 'POST',
1415 2468 'body' => array( 'secret' => $secret, 'response' => $response ),
1416 2469 )
1417 2470 );
1418 - if ( is_wp_error( $response ) )
2471 +
2472 + if ( is_wp_error( $response ) )
1419 2473 {
1420 2474 $errors[] = $response->get_error_message();
1421 2475 }
1422 2476 else
@@ -1423,9 +2477,9 @@
1423 2477 {
1424 2478 $response = json_decode($response['body'], TRUE);
1425 2479 if ( $response === FALSE )
1426 2480 {
1427 - $errors[] = 'Error decoding response from reCAPTCHA check';
2481 + $errors[] = __( 'Error decoding response from hCaptcha check', 'propertyhive' );
1428 2482 }
1429 2483 else
1430 2484 {
1431 2485 if ( isset($response['success']) && $response['success'] == true )
@@ -1433,15 +2487,123 @@
1433 2487
1434 2488 }
1435 2489 else
1436 2490 {
1437 - $errors[] = 'Failed reCAPTCHA validation';
2491 + $errors[] = __( 'Failed hCaptcha validation', 'propertyhive' );
1438 2492 }
1439 2493 }
1440 2494 }
1441 2495 }
2496 + if ( $key == 'turnstile' )
2497 + {
2498 + $secret = isset( $control['secret'] ) ? $control['secret'] : '';
2499 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2500 + $response = ( isset( $_POST['cf-turnstile-response'] ) && is_string( $_POST['cf-turnstile-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) ) : '';
2501 +
2502 + $response = wp_remote_post(
2503 + 'https://challenges.cloudflare.com/turnstile/v0/siteverify', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Server-side CAPTCHA token verification API.
2504 + array(
2505 + 'method' => 'POST',
2506 + 'headers' => array(
2507 + 'Content-Type' => 'application/x-www-form-urlencoded',
2508 + ),
2509 + 'body' => array( 'secret' => $secret, 'response' => $response ),
2510 + )
2511 + );
2512 +
2513 + if ( is_wp_error( $response ) )
2514 + {
2515 + $errors[] = $response->get_error_message();
2516 + }
2517 + else
2518 + {
2519 + $response = json_decode($response['body'], TRUE);
2520 + if ( $response === FALSE )
2521 + {
2522 + $errors[] = 'Error decoding response from turnstile check';
2523 + }
2524 + else
2525 + {
2526 + if ( isset($response['success']) && $response['success'] == true )
2527 + {
2528 +
2529 + }
2530 + else
2531 + {
2532 + $errors[] = 'Failed turnstile validation';
2533 + }
2534 + }
2535 + }
2536 + }
1442 2537 }
1443 -
2538 +
2539 + if (
2540 + get_option( 'propertyhive_property_enquiry_form_disclaimer', '' ) != '' &&
2541 + (
2542 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2543 + !isset( $_POST['disclaimer'] ) ||
2544 + (
2545 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2546 + isset( $_POST['disclaimer'] ) && empty( $_POST['disclaimer'] )
2547 + )
2548 + )
2549 + )
2550 + {
2551 + $errors[] = __( 'Missing required field', 'propertyhive' ) . ': disclaimer';
2552 + }
2553 +
2554 + // Check only expected fields are received
2555 + /*$allowed_keys = array_keys($form_controls);
2556 + $allowed_keys[] = 'action';
2557 + $allowed_keys[] = 'utm_source';
2558 + $allowed_keys[] = 'utm_medium';
2559 + $allowed_keys[] = 'utm_term';
2560 + $allowed_keys[] = 'utm_content';
2561 + $allowed_keys[] = 'utm_campaign';
2562 + $allowed_keys[] = 'gclid';
2563 + $allowed_keys[] = 'fbclid';
2564 + $allowed_keys[] = 'property_id';
2565 + $allowed_keys[] = 'disclaimer';
2566 + $allowed_keys[] = 'g-recaptcha-response';
2567 + $allowed_keys[] = 'h-captcha-response';
2568 + $allowed_keys[] = 'cf-turnstile-response';
2569 +
2570 + $allowed_keys = apply_filters(
2571 + 'propertyhive_property_enquiry_allowed_keys',
2572 + $allowed_keys
2573 + );
2574 +
2575 + foreach ( $_POST as $key => $value )
2576 + {
2577 + if ( !in_array($key, $allowed_keys) )
2578 + {
2579 + // Unexpected field
2580 + $errors[] = sprintf(
2581 + esc_html__( 'Unexpected field %s received', 'propertyhive' ),
2582 + esc_html( $key )
2583 + );
2584 + break;
2585 + }
2586 + }*/
2587 +
2588 + // Passed validation
2589 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2590 + $property_ids = isset( $_POST['property_id'] ) && is_string( $_POST['property_id'] ) ? array_values( array_filter( array_map( 'absint', explode( '|', sanitize_text_field( wp_unslash( $_POST['property_id'] ) ) ) ) ) ) : array();
2591 + if ( empty( $property_ids ) ) {
2592 + $errors[] = __( 'Invalid property supplied', 'propertyhive' );
2593 + }
2594 + if ( count( $property_ids ) > 100 ) {
2595 + $errors[] = __( 'Too many properties supplied.', 'propertyhive' );
2596 + }
2597 + foreach ( $property_ids as $property_id )
2598 + {
2599 + if ( get_post_type( $property_id ) !== 'property' || ! propertyhive_is_post_publicly_viewable( $property_id ) )
2600 + {
2601 + $errors[] = __( 'Invalid property supplied', 'propertyhive' );
2602 + break;
2603 + }
2604 + }
2605 +
1444 2606 if ( !empty($errors) )
1445 2607 {
1446 2608 // Failed validation
1447 2609
@@ -1450,11 +2612,8 @@
1450 2612 $return['errors'] = $errors;
1451 2613 }
1452 2614 else
1453 2615 {
1454 - // Passed validation
1455 - $property_ids = explode("|", ph_clean($_POST['property_id']));
1456 -
1457 2616 // Get recipient email address
1458 2617 $to = '';
1459 2618
1460 2619 // Try and get office's email address first, else fallback to admin email
@@ -1488,8 +2647,16 @@
1488 2647 $fields_to_check[] = '_office_email_address_lettings';
1489 2648 $fields_to_check[] = '_office_email_address_sales';
1490 2649 break;
1491 2650 }
2651 + default:
2652 + {
2653 + $fields_to_check[] = '_office_email_address_' . str_replace("residential-", "", $property_department);
2654 + $fields_to_check[] = '_office_email_address_sales';
2655 + $fields_to_check[] = '_office_email_address_lettings';
2656 + $fields_to_check[] = '_office_email_address_commercial';
2657 + break;
2658 + }
1492 2659 }
1493 2660
1494 2661 foreach ( $fields_to_check as $field_to_check )
1495 2662 {
@@ -1504,9 +2671,9 @@
1504 2671 if ( $to == '' )
1505 2672 {
1506 2673 $to = get_option( 'admin_email' );
1507 2674 }
1508 -
2675 +
1509 2676 if ( count($property_ids) == 1 )
1510 2677 {
1511 2678 $subject = __( 'New Property Enquiry', 'propertyhive' ) . ': ' . get_the_title( (int)$property_ids[0] );
1512 2679 }
@@ -1517,14 +2684,14 @@
1517 2684 $message = __( "You have received a property enquiry via your website. Please find details of the enquiry below", 'propertyhive' ) . "\n\n";
1518 2685
1519 2686 $message = apply_filters( 'propertyhive_property_enquiry_pre_body', $message, $property_ids );
1520 2687
1521 - $message .= __( 'Properties', 'propertyhive' ) . ":\n";
2688 + $message .= ( count($property_ids) > 1 ? __( 'Properties', 'propertyhive' ) : __( 'Property', 'propertyhive' ) ) . ":\n";
1522 2689 foreach ( $property_ids as $property_id )
1523 2690 {
1524 - $message .= get_the_title( (int)$property_id ) . " (" . get_permalink( (int)$property_id ) . ")\n";
2691 + $property = new PH_Property((int)$property_id);
2692 + $message .= apply_filters( 'propertyhive_property_enquiry_property_output', $property->get_formatted_full_address() . "\n" . html_entity_decode(wp_strip_all_tags($property->get_formatted_price())) . "\n" . get_permalink( (int)$property_id ), (int)$property_id ) . "\n\n";
1525 2693 }
1526 - $message .= "\n";
1527 2694
1528 2695 unset($form_controls['action']);
1529 2696 unset($_POST['action']);
1530 2697 unset($form_controls['property_id']); // Unset so the field doesn't get shown in the enquiry details
@@ -1532,16 +2699,32 @@
1532 2699 $form_controls = apply_filters( 'propertyhive_property_enquiry_body_form_fields', $form_controls );
1533 2700
1534 2701 foreach ($form_controls as $key => $control)
1535 2702 {
1536 - if ( isset($control['type']) && $control['type'] == 'html' ) { continue; }
2703 + if ( isset($control['type']) && in_array($control['type'], array('html', 'recaptcha', 'recaptcha-v3', 'hCaptcha', 'turnstile')) ) { continue; }
1537 2704
1538 2705 $label = ( isset($control['label']) ) ? $control['label'] : $key;
1539 2706 $label = ( isset($control['email_label']) ) ? $control['email_label'] : $label;
1540 - $value = ( isset($_POST[$key]) ) ? sanitize_textarea_field($_POST[$key]) : '';
2707 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2708 + $value = ( isset($_POST[$key]) && is_string($_POST[$key]) ) ? sanitize_textarea_field( wp_unslash( $_POST[$key] ) ) : '';
1541 2709
1542 - $message .= strip_tags($label) . ": " . strip_tags($value) . "\n";
2710 + $message .= wp_strip_all_tags($label) . ": " . wp_strip_all_tags($value) . "\n";
1543 2711 }
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 );
1544 2727
1545 2728 $from_email_address = get_option('propertyhive_email_from_address', '');
1546 2729 if ( $from_email_address == '' )
1547 2730 {
@@ -1549,31 +2732,54 @@
1549 2732 }
1550 2733 if ( $from_email_address == '' )
1551 2734 {
1552 2735 // Should never get here
1553 - $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'] ) ) : '';
1554 2738 }
1555 2739
1556 2740 $headers = array();
1557 - 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 !== '' )
1558 2752 {
1559 - $headers[] = 'From: ' . ph_clean( $_POST['name'] ) . ' <' . sanitize_email( $from_email_address ) . '>';
2753 + $headers[] = sprintf( 'From: %s <%s>', $name, $from_email_address );
1560 2754 }
1561 2755 else
1562 2756 {
1563 - $headers[] = 'From: <' . sanitize_email( $from_email_address ) . '>';
2757 + $headers[] = sprintf( 'From: <%s>', $from_email_address );
1564 2758 }
1565 - if ( isset($_POST['email_address']) && sanitize_email( $_POST['email_address'] ) != '' )
2759 +
2760 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2761 + if ( isset($_POST['email_address']) )
1566 2762 {
1567 - $headers[] = 'Reply-To: ' . sanitize_email( $_POST['email_address'] );
2763 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2764 + $reply_to = sanitize_email(wp_unslash($_POST['email_address']));
2765 +
2766 + if ( is_email($reply_to) )
2767 + {
2768 + $headers[] = 'Reply-To: ' . $reply_to;
2769 + }
1568 2770 }
1569 2771
1570 2772 $to = apply_filters( 'propertyhive_property_enquiry_to', $to, $property_ids );
1571 2773 $subject = apply_filters( 'propertyhive_property_enquiry_subject', $subject, $property_ids );
2774 + $headers = apply_filters( 'propertyhive_property_enquiry_headers', $headers, $property_ids );
1572 2775 $message = apply_filters( 'propertyhive_property_enquiry_body', $message, $property_ids );
1573 - $headers = apply_filters( 'propertyhive_property_enquiry_headers', $headers, $property_ids );
2776 +
2777 + do_action( 'propertyhive_before_property_enquiry_sent' );
1574 2778
1575 2779 $sent = wp_mail( $to, $subject, $message, $headers );
2780 +
2781 + do_action( 'propertyhive_after_property_enquiry_sent' );
1576 2782
1577 2783 if ( ! $sent )
1578 2784 {
1579 2785 $return['success'] = false;
@@ -1582,8 +2788,10 @@
1582 2788 }
1583 2789 else
1584 2790 {
1585 2791 $return['success'] = true;
2792 +
2793 + $enquiry_post_id = '';
1586 2794
1587 2795 if ( get_option( 'propertyhive_store_property_enquiries', 'yes' ) == 'yes' )
1588 2796 {
1589 2797 // Now insert into enquiries section of WordPress
@@ -1594,11 +2802,13 @@
1594 2802 else
1595 2803 {
1596 2804 $title = __( 'Multiple Property Enquiry', 'propertyhive' );
1597 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.
1598 2807 if ( isset($_POST['name']) && ! empty($_POST['name']) )
1599 2808 {
1600 - $title .= __( ' from ', 'propertyhive' ) . ph_clean($_POST['name']);
2809 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2810 + $title .= ' ' . __( 'from', 'propertyhive' ) . ' ' . ph_clean(wp_unslash($_POST['name']));
1601 2811 }
1602 2812
1603 2813 $enquiry_post = array(
1604 2814 'post_title' => $title,
@@ -1616,32 +2826,43 @@
1616 2826 add_post_meta( $enquiry_post_id, '_source', 'website' );
1617 2827 add_post_meta( $enquiry_post_id, '_negotiator_id', '' );
1618 2828 add_post_meta( $enquiry_post_id, '_office_id', $office_id );
1619 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.
1620 2831 foreach ($_POST as $key => $value)
1621 2832 {
1622 - if ( $key == 'property_id' )
2833 + $meta_key = is_string( $key ) ? $key : '';
2834 +
2835 + // Only store non-empty keys containing characters safe for use as post meta.
2836 + if ( $meta_key === '' || ! preg_match( '/\A[A-Za-z0-9_-]+\z/', $meta_key ) )
1623 2837 {
2838 + continue;
2839 + }
2840 +
2841 + if ( $meta_key == 'property_id' )
2842 + {
1624 2843 foreach ( $property_ids as $property_id )
1625 2844 {
1626 - add_post_meta( $enquiry_post_id, $key, (int)$property_id );
2845 + add_post_meta( $enquiry_post_id, $meta_key, (int)$property_id );
1627 2846 }
1628 2847 }
1629 2848 else
1630 2849 {
1631 - add_post_meta( $enquiry_post_id, $key, sanitize_textarea_field($value) );
2850 + add_post_meta( $enquiry_post_id, $meta_key, sanitize_textarea_field(wp_unslash($value)) );
1632 2851 }
1633 2852 }
1634 2853 }
1635 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 +
1636 2858 // Send auto-responder
1637 2859 if ( get_option( 'propertyhive_enquiry_auto_responder', '' ) == 'yes' )
1638 2860 {
1639 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.
1640 2863 PH()->email->send_enquiry_auto_responder( $_POST );
1641 2864 }
1642 -
1643 - do_action('propertyhive_property_enquiry_sent', $_POST, $to);
1644 2865 }
1645 2866 }
1646 2867
1647 2868 $this->json_headers();
@@ -1657,12 +2878,14 @@
1657 2878 public function create_contact_from_enquiry()
1658 2879 {
1659 2880 global $post;
1660 2881
1661 - $enquiry_post_id = ( (isset($_POST['post_id'])) ? (int)$_POST['post_id'] : '' );
1662 - $nonce = ( (isset($_POST['security'])) ? ph_clean($_POST['security']) : '' );
2882 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- create_contact_from_enquiry reads post_id to construct the action-specific nonce name and reads security as the nonce value; wp_verify_nonce occurs immediately. The event is admin-only and authorize_admin_ajax enforces manage_propertyhive before the callback. These are nonce inputs, not unguarded business mutations.
2883 + $enquiry_post_id = isset( $_POST['post_id'] ) && is_scalar( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
2884 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- create_contact_from_enquiry reads post_id to construct the action-specific nonce name and reads security as the nonce value; wp_verify_nonce occurs immediately. The event is admin-only and authorize_admin_ajax enforces manage_propertyhive before the callback. These are nonce inputs, not unguarded business mutations.
2885 + $nonce = isset( $_POST['security'] ) && is_string( $_POST['security'] ) ? sanitize_text_field( wp_unslash( $_POST['security'] ) ) : '';
1663 2886
1664 - 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 ) )
1665 2888 {
1666 2889 // This nonce is not valid.
1667 2890 die( json_encode( array('error' => 'Invalid nonce. Please refresh and try again') ) );
1668 2891 }
@@ -1671,36 +2894,70 @@
1671 2894
1672 2895 $name = false;
1673 2896 $email = false;
1674 2897 $telephone = false;
2898 + $address = false;
2899 + $postcode = false;
2900 + $property_id = false;
1675 2901
1676 2902 foreach ($enquiry_meta as $key => $value)
1677 2903 {
1678 - if ( strpos($key, 'name') !== false )
2904 + if ( strpos(strtolower($key), 'name') !== false && strpos(strtolower($key), 'property') === false && $value[0] != '' )
1679 2905 {
1680 - $name = $value[0];
2906 + if ( $name === false )
2907 + {
2908 + $name = $value[0];
2909 + }
2910 + else
2911 + {
2912 + $name .= ' ' . $value[0];
2913 + }
1681 2914 }
1682 - elseif ( strpos($key, 'email') !== false )
2915 + elseif ( strpos(strtolower($key), 'email') !== false && $value[0] != '' )
1683 2916 {
1684 - $email = $value[0];
2917 + if ( $email === false )
2918 + {
2919 + $email = $value[0];
2920 + }
2921 + else
2922 + {
2923 + $email .= ',' . $value[0];
2924 + }
1685 2925 }
1686 - elseif ( strpos($key, 'telephone') !== false )
2926 + elseif ( strpos(strtolower($key), 'phone') !== false && $value[0] != '' )
1687 2927 {
1688 - $telephone = $value[0];
2928 + if ( $telephone === false )
2929 + {
2930 + $telephone = $value[0];
2931 + }
2932 + else
2933 + {
2934 + $telephone .= ',' . $value[0];
2935 + }
1689 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 + }
1690 2949 }
1691 2950
1692 - if ( $name === false || $email === false )
2951 + if ( $name === false && $email === false )
1693 2952 {
1694 - // This nonce is not valid.
1695 - die( json_encode( array('error' => 'Name or email address not found') ) );
2953 + die( json_encode( array('error' => 'Name and email address not found') ) );
1696 2954 }
1697 2955
1698 - // We've not imported this property before
1699 2956 $postdata = array(
1700 2957 'post_excerpt' => '',
1701 2958 'post_content' => '',
1702 - 'post_title' => utf8_encode(wp_strip_all_tags( $name )),
2959 + 'post_title' => wp_strip_all_tags( $name ),
1703 2960 'post_status' => 'publish',
1704 2961 'post_type' => 'contact',
1705 2962 'ping_status' => 'closed',
1706 2963 'comment_status' => 'closed',
@@ -1716,15 +2973,134 @@
1716 2973 {
1717 2974 die( json_encode( array('error' => 'Error creating contact') ) );
1718 2975 }
1719 2976
2977 + update_post_meta( $enquiry_post_id, '_contact_id', $contact_post_id );
2978 +
1720 2979 if ( $telephone !== FALSE ) {
1721 - update_post_meta( $contact_post_id, '_telephone_number', ph_clean( $telephone ) );
1722 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) ) );
1723 2982 }
1724 2983
1725 2984 if ( $email !== FALSE ) { update_post_meta( $contact_post_id, '_email_address', ph_clean( $email ) ); }
1726 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 +
1727 3103 do_action('propertyhive_create_contact_from_enquiry', $enquiry_post_id, $contact_post_id);
1728 3104
1729 3105 die( json_encode( array('success' => get_edit_post_link($contact_post_id, '')) ) );
1730 3106 }
@@ -1736,15 +3112,21 @@
1736 3112 check_ajax_referer( 'contact-save-validation', 'security' );
1737 3113
1738 3114 $this->json_headers();
1739 3115
1740 - parse_str($_POST['form_data']);
3116 + $form_data = array();
3117 + if ( isset( $_POST['form_data'] ) && is_string( $_POST['form_data'] ) ) {
3118 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Decode serialized form input first; only the typed and sanitized email address and numeric contact ID below are consumed.
3119 + parse_str( wp_unslash( $_POST['form_data'] ), $form_data );
3120 + }
3121 + $email_address_input = isset( $form_data['_email_address'] ) && is_string( $form_data['_email_address'] ) ? sanitize_text_field( $form_data['_email_address'] ) : '';
3122 + $contact_id = isset( $form_data['post_ID'] ) && is_scalar( $form_data['post_ID'] ) ? absint( $form_data['post_ID'] ) : 0;
1741 3123
1742 3124 $return = array('errors' => array());
1743 3125
1744 - if ( isset($_email_address) && $_email_address != '' )
3126 + if ( '' !== $email_address_input )
1745 3127 {
1746 - $email_addresses = explode( ",", $_email_address );
3128 + $email_addresses = explode( ",", $email_address_input );
1747 3129
1748 3130 foreach ( $email_addresses as $email_address )
1749 3131 {
1750 3132 $email_address = trim( $email_address );
@@ -1759,8 +3141,9 @@
1759 3141 'post_type' => 'contact',
1760 3142 'post_status' => 'any',
1761 3143 'posts_per_page' => 1,
1762 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.
1763 3146 'meta_query' => array(
1764 3147 'relation' => 'OR',
1765 3148 array(
1766 3149 'key' => '_email_address',
@@ -1779,11 +3162,12 @@
1779 3162 'compare' => 'LIKE'
1780 3163 )
1781 3164 )
1782 3165 );
1783 - if ( isset($post_ID) && $post_ID != '' )
3166 + if ( $contact_id )
1784 3167 {
1785 - $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 );
1786 3170 }
1787 3171
1788 3172 $contact_query = new WP_Query( $args );
1789 3173
@@ -1792,9 +3176,10 @@
1792 3176 while ( $contact_query->have_posts() )
1793 3177 {
1794 3178 $contact_query->the_post();
1795 3179
1796 - $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 );
1797 3182 }
1798 3183 }
1799 3184 }
1800 3185 }
@@ -1803,8 +3188,80 @@
1803 3188
1804 3189 die();
1805 3190 }
1806 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 +
1807 3264 // Dashboard related functions
1808 3265 public function get_news()
1809 3266 {
1810 3267 $this->json_headers();
@@ -1829,9 +3286,9 @@
1829 3286 foreach ( $rss_items as $item )
1830 3287 {
1831 3288 $return[] = array(
1832 3289 'title' => esc_html( $item->get_title() ),
1833 - 'permalink' => esc_url( $item->get_permalink() ),
3290 + 'permalink' => esc_url( $item->get_permalink() ) . '?src=dashboard',
1834 3291 'date' => $item->get_date('F d, Y')
1835 3292 );
1836 3293 }
1837 3294
@@ -1853,8 +3310,9 @@
1853 3310 $args = array(
1854 3311 'post_type' => 'viewing',
1855 3312 'fields' => 'ids',
1856 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.
1857 3315 'meta_query' => array(
1858 3316 array(
1859 3317 'key' => '_status',
1860 3318 'value' => 'carried_out'
@@ -1865,8 +3323,10 @@
1865 3323 )
1866 3324 )
1867 3325 );
1868 3326
3327 + $args = apply_filters( 'propertyhive_admin_dashboard_viewings_awaiting_applicant_feedback_args', $args );
3328 +
1869 3329 $viewings_query = new WP_Query( $args );
1870 3330
1871 3331 if ( $viewings_query->have_posts() )
1872 3332 {
@@ -1876,19 +3336,19 @@
1876 3336
1877 3337 $property_id = get_post_meta( get_the_ID(), '_property_id', TRUE );
1878 3338 $property = new PH_Property((int)$property_id);
1879 3339
1880 - $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' );
1881 3341
1882 3342 $return[] = array(
1883 3343 'ID' => get_the_ID(),
1884 3344 'edit_link' => get_edit_post_link( get_the_ID() ),
1885 3345 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
1886 - '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 ))),
1887 3347 'property_id' => $property_id,
1888 3348 'property_address' => $property->get_formatted_full_address(),
1889 - 'applicant_contact_id' => $applicant_contact_id,
1890 - '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] ),
1891 3351 );
1892 3352 }
1893 3353 }
1894 3354
@@ -1910,8 +3370,9 @@
1910 3370 $args = array(
1911 3371 'post_type' => 'viewing',
1912 3372 'fields' => 'ids',
1913 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.
1914 3375 'meta_query' => array(
1915 3376 array(
1916 3377 'key' => '_status',
1917 3378 'value' => 'pending'
@@ -1917,9 +3378,9 @@
1917 3378 'value' => 'pending'
1918 3379 ),
1919 3380 array(
1920 3381 'key' => '_start_date_time',
1921 - 'value' => date("Y-m-d H:i:s"),
3382 + 'value' => gmdate("Y-m-d H:i:s"),
1922 3383 'compare' => '>='
1923 3384 ),
1924 3385 array(
1925 3386 'key' => '_negotiator_id',
@@ -1927,8 +3388,11 @@
1927 3388 ),
1928 3389 )
1929 3390 );
1930 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 +
1931 3395 $viewings_query = new WP_Query( $args );
1932 3396
1933 3397 if ( $viewings_query->have_posts() )
1934 3398 {
@@ -1942,9 +3406,9 @@
1942 3406 $return[] = array(
1943 3407 'ID' => get_the_ID(),
1944 3408 'edit_link' => get_edit_post_link( get_the_ID() ),
1945 3409 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
1946 - 'start_date_time_formatted_Hi_jSFY' => date("H:i jS F Y", strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE ))),
3410 + 'start_date_time_formatted_Hi_jSFY' => gmdate("H:i jS F Y", strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE ))),
1947 3411 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
1948 3412 'title' => 'Viewing at ' . $property->get_formatted_full_address(),
1949 3413 );
1950 3414 }
@@ -1955,8 +3419,9 @@
1955 3419 $args = array(
1956 3420 'post_type' => 'appraisal',
1957 3421 'fields' => 'ids',
1958 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.
1959 3424 'meta_query' => array(
1960 3425 array(
1961 3426 'key' => '_status',
1962 3427 'value' => 'pending'
@@ -1962,9 +3427,9 @@
1962 3427 'value' => 'pending'
1963 3428 ),
1964 3429 array(
1965 3430 'key' => '_start_date_time',
1966 - 'value' => date("Y-m-d H:i:s"),
3431 + 'value' => gmdate("Y-m-d H:i:s"),
1967 3432 'compare' => '>='
1968 3433 ),
1969 3434 array(
1970 3435 'key' => '_negotiator_id',
@@ -1972,8 +3437,11 @@
1972 3437 ),
1973 3438 )
1974 3439 );
1975 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 +
1976 3444 $appraisals_query = new WP_Query( $args );
1977 3445
1978 3446 if ( $appraisals_query->have_posts() )
1979 3447 {
@@ -1986,9 +3454,9 @@
1986 3454 $return[] = array(
1987 3455 'ID' => get_the_ID(),
1988 3456 'edit_link' => get_edit_post_link( get_the_ID() ),
1989 3457 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
1990 - 'start_date_time_formatted_Hi_jSFY' => date("H:i jS F Y", strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE ))),
3458 + 'start_date_time_formatted_Hi_jSFY' => gmdate("H:i jS F Y", strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE ))),
1991 3459 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
1992 3460 'title' => 'Appraisal at ' . $appraisal->get_formatted_full_address(),
1993 3461 );
1994 3462 }
@@ -2013,8 +3481,285 @@
2013 3481
2014 3482 die();
2015 3483 }
2016 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 +
2017 3762 public function get_appraisal_details_meta_box()
2018 3763 {
2019 3764 global $post;
2020 3765
@@ -2019,11 +3764,12 @@
2019 3764 global $post;
2020 3765
2021 3766 check_ajax_referer( 'appraisal-details-meta-box', 'security' );
2022 3767
2023 - $post = get_post((int)$_POST['appraisal_id']);
3768 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
3769 + $post = get_post( $post_id );
2024 3770
2025 - $appraisal = new PH_Appraisal((int)$_POST['appraisal_id']);
3771 + $appraisal = new PH_Appraisal( $post_id );
2026 3772
2027 3773 echo '<div class="propertyhive_meta_box">';
2028 3774
2029 3775 echo '<div class="options_group">';
@@ -2029,11 +3775,11 @@
2029 3775 echo '<div class="options_group">';
2030 3776
2031 3777 echo '<p class="form-field">
2032 3778
2033 - <label for="">' . __('Status', 'propertyhive') . '</label>
3779 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
2034 3780
2035 - ' . ucwords(str_replace("_", " ", $appraisal->status));
3781 + ' . esc_html(ucwords(str_replace("_", " ", $appraisal->status)));
2036 3782
2037 3783 echo '</p>';
2038 3784
2039 3785 if ( $appraisal->status == 'cancelled' )
@@ -2052,16 +3798,39 @@
2052 3798 }
2053 3799
2054 3800 if ( $appraisal->status == 'carried_out' || $appraisal->status == 'won' || $appraisal->status == 'instructed' )
2055 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 +
2056 3825 if ( $appraisal->department == 'residential-sales' )
2057 3826 {
2058 3827 $args = array(
2059 3828 'id' => '_valued_price',
2060 - 'label' => __( 'Valued Price', 'propertyhive' ) . ' (&pound;)',
3829 + 'label' => __( 'Valued Price', 'propertyhive' ) . ' (' . $currency_symbol . ')',
2061 3830 'desc_tip' => false,
2062 3831 'class' => 'short',
2063 - 'value' => $appraisal->valued_price,
3832 + 'value' => ph_display_price_field( $appraisal->valued_price ),
2064 3833 );
2065 3834 propertyhive_wp_text_input( $args );
2066 3835 }
2067 3836 elseif ( $appraisal->department == 'residential-lettings' )
@@ -2069,18 +3838,19 @@
2069 3838 $rent_frequency = $appraisal->valued_rent_frequency;
2070 3839
2071 3840 echo '<p class="form-field">
2072 3841
2073 - <label for="">' . __('Valued Rent', 'propertyhive') . ' (&pound;)</label>
3842 + <label for="">' . esc_html(__('Valued Rent', 'propertyhive')) . ' (' . esc_html($currency_symbol) . ')</label>
2074 3843
2075 - <input type="text" class="" name="_valued_rent" id="_valued_rent" value="' . $appraisal->valued_rent . '" placeholder="" style="width:10%; min-width:100px;">
3844 + <input type="text" class="" name="_valued_rent" id="_valued_rent" value="' . esc_attr(ph_display_price_field( $appraisal->valued_rent )) . '" placeholder="" style="width:10%; min-width:100px;">
2076 3845
2077 3846 <select id="_valued_rent_frequency" name="_valued_rent_frequency" class="select" style="width:auto">
2078 - <option value="pppw"' . ( ($rent_frequency == 'pppw') ? ' selected' : '') . '>' . __('Per Person Per Week', 'propertyhive') . '</option>
2079 - <option value="pw"' . ( ($rent_frequency == 'pw') ? ' selected' : '') . '>' . __('Per Week', 'propertyhive') . '</option>
2080 - <option value="pcm"' . ( ($rent_frequency == 'pcm' || $rent_frequency == '') ? ' selected' : '') . '>' . __('Per Calendar Month', 'propertyhive') . '</option>
2081 - <option value="pq"' . ( ($rent_frequency == 'pq') ? ' selected' : '') . '>' . __('Per Quarter', 'propertyhive') . '</option>
2082 - <option value="pa"' . ( ($rent_frequency == 'pa') ? ' selected' : '') . '>' . __('Per Annum', 'propertyhive') . '</option>
3847 + <option value="pd"' . ( ($rent_frequency == 'pd') ? ' selected' : '') . '>' . esc_html(__('Per Day', 'propertyhive')) . '</option>
3848 + <option value="pppw"' . ( ($rent_frequency == 'pppw') ? ' selected' : '') . '>' . esc_html(__('Per Person Per Week', 'propertyhive')) . '</option>
3849 + <option value="pw"' . ( ($rent_frequency == 'pw') ? ' selected' : '') . '>' . esc_html(__('Per Week', 'propertyhive')) . '</option>
3850 + <option value="pcm"' . ( ($rent_frequency == 'pcm' || $rent_frequency == '') ? ' selected' : '') . '>' . esc_html(__('Per Calendar Month', 'propertyhive')) . '</option>
3851 + <option value="pq"' . ( ($rent_frequency == 'pq') ? ' selected' : '') . '>' . esc_html(__('Per Quarter', 'propertyhive')) . '</option>
3852 + <option value="pa"' . ( ($rent_frequency == 'pa') ? ' selected' : '') . '>' . esc_html(__('Per Annum', 'propertyhive')) . '</option>
2083 3853 </select>
2084 3854
2085 3855 </p>';
2086 3856 }
@@ -2113,9 +3883,9 @@
2113 3883 public function get_appraisal_actions()
2114 3884 {
2115 3885 check_ajax_referer( 'appraisal-actions', 'security' );
2116 3886
2117 - $post_id = (int)$_POST['appraisal_id'];
3887 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2118 3888
2119 3889 $status = get_post_meta( $post_id, '_status', TRUE );
2120 3890 $department = get_post_meta( $post_id, '_department', TRUE );
2121 3891
@@ -2126,13 +3896,46 @@
2126 3896 $show_cancelled_meta_boxes = false;
2127 3897 $show_carried_out_meta_boxes = false;
2128 3898 $show_instructed_meta_boxes = false;
2129 3899 $show_lost_meta_boxes = false;
3900 + $show_customise_confirmation_meta_boxes = false;
2130 3901
2131 3902 $actions = array();
2132 3903
2133 3904 if ( $status == 'pending' )
2134 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 +
2135 3938 /*$actions[] = '<a
2136 3939 href=""
2137 3940 class="button"
2138 3941 style="width:100%; margin-bottom:7px; text-align:center"
@@ -2148,14 +3951,14 @@
2148 3951 $actions[] = '<a
2149 3952 href="#action_panel_appraisal_carried_out"
2150 3953 class="button button-success appraisal-action"
2151 3954 style="width:100%; margin-bottom:7px; text-align:center"
2152 - >' . __('Appraisal Carried Out', 'propertyhive') . '</a>';
3955 + >' . esc_html(__('Appraisal Carried Out', 'propertyhive')) . '</a>';
2153 3956 $actions[] = '<a
2154 3957 href="#action_panel_appraisal_cancelled"
2155 3958 class="button appraisal-action"
2156 3959 style="width:100%; margin-bottom:7px; text-align:center"
2157 - >' . __('Appraisal Cancelled', 'propertyhive') . '</a>';
3960 + >' . esc_html(__('Appraisal Cancelled', 'propertyhive')) . '</a>';
2158 3961
2159 3962 $show_cancelled_meta_boxes = true;
2160 3963 $show_carried_out_meta_boxes = true;
2161 3964 }
@@ -2165,15 +3968,15 @@
2165 3968 $actions[] = '<a
2166 3969 href="#action_panel_appraisal_won"
2167 3970 class="button button-success appraisal-action"
2168 3971 style="width:100%; margin-bottom:7px; text-align:center"
2169 - >' . __('Appraisal Won', 'propertyhive') . '</a>';
3972 + >' . esc_html(__('Appraisal Won', 'propertyhive')) . '</a>';
2170 3973
2171 3974 $actions[] = '<a
2172 3975 href="#action_panel_appraisal_lost"
2173 3976 class="button button-danger appraisal-action"
2174 3977 style="width:100%; margin-bottom:7px; text-align:center"
2175 - >' . __('Appraisal Lost', 'propertyhive') . '</a>';
3978 + >' . esc_html(__('Appraisal Lost', 'propertyhive')) . '</a>';
2176 3979
2177 3980 $show_lost_meta_boxes = true;
2178 3981 }
2179 3982
@@ -2182,9 +3985,9 @@
2182 3985 $actions[] = '<a
2183 3986 href="#action_panel_appraisal_instruct"
2184 3987 class="button button-success appraisal-action"
2185 3988 style="width:100%; margin-bottom:7px; text-align:center"
2186 - >' . __('Instruct Property', 'propertyhive') . '</a>';
3989 + >' . esc_html(__('Instruct Property', 'propertyhive')) . '</a>';
2187 3990
2188 3991 $show_instructed_meta_boxes = true;
2189 3992 }
2190 3993
@@ -2193,9 +3996,9 @@
2193 3996 $actions[] = '<a
2194 3997 href="#action_panel_appraisal_revert_carried_out"
2195 3998 class="button appraisal-action"
2196 3999 style="width:100%; margin-bottom:7px; text-align:center"
2197 - >' . __('Revert To Carried Out', 'propertyhive') . '</a>';
4000 + >' . esc_html(__('Revert To Carried Out', 'propertyhive')) . '</a>';
2198 4001 }
2199 4002
2200 4003 if ( $status == 'instructed' )
2201 4004 {
@@ -2201,12 +4004,12 @@
2201 4004 {
2202 4005 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
2203 4006
2204 4007 $actions[] = '<a
2205 - href="' . get_edit_post_link($property_id) . '"
4008 + href="' . esc_url(get_edit_post_link($property_id)) . '"
2206 4009 class="button"
2207 4010 style="width:100%; margin-bottom:7px; text-align:center"
2208 - >' . __('View Instructed Property', 'propertyhive') . '</a>';
4011 + >' . esc_html(__('View Instructed Property', 'propertyhive')) . '</a>';
2209 4012
2210 4013 /*$actions[] = '<a
2211 4014 href="#action_panel_appraisal_revert_won"
2212 4015 class="button appraisal-action"
@@ -2219,20 +4022,22 @@
2219 4022 $actions[] = '<a
2220 4023 href="#action_panel_appraisal_revert_pending"
2221 4024 class="button appraisal-action"
2222 4025 style="width:100%; margin-bottom:7px; text-align:center"
2223 - >' . __('Revert To Pending', 'propertyhive') . '</a>';
4026 + >' . esc_html(__('Revert To Pending', 'propertyhive')) . '</a>';
2224 4027 }
2225 4028
2226 4029 $actions = apply_filters( 'propertyhive_admin_appraisal_actions', $actions, $post_id );
4030 + $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
2227 4031
2228 4032 if ( !empty($actions) )
2229 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.
2230 4035 echo implode("", $actions);
2231 4036 }
2232 4037 else
2233 4038 {
2234 - echo '<div style="text-align:center">' . __( 'No actions to display', 'propertyhive' ) . '</div>';
4039 + echo '<div style="text-align:center">' . esc_html(__( 'No actions to display', 'propertyhive' )) . '</div>';
2235 4040 }
2236 4041
2237 4042 echo '</div>
2238 4043
@@ -2237,8 +4042,57 @@
2237 4042 echo '</div>
2238 4043
2239 4044 </div>';
2240 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 +
2241 4095 if ( $show_cancelled_meta_boxes )
2242 4096 {
2243 4097 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_cancelled" style="display:none;">
2244 4098
@@ -2245,16 +4099,16 @@
2245 4099 <div class="options_group" style="padding-top:8px;">
2246 4100
2247 4101 <div class="form-field">
2248 4102
2249 - <label for="_appraisal_cancelled_reason">' . __( 'Reason Cancelled', 'propertyhive' ) . '</label>
4103 + <label for="_appraisal_cancelled_reason">' . esc_html(__( 'Reason Cancelled', 'propertyhive' )) . '</label>
2250 4104
2251 - <textarea id="_cancelled_reason" name="_cancelled_reason" style="width:100%;">' . get_post_meta( $post_id, '_cancelled_reason', TRUE ) . '</textarea>
4105 + <textarea id="_cancelled_reason" name="_cancelled_reason" style="width:100%;">' . esc_html(get_post_meta( $post_id, '_cancelled_reason', TRUE )) . '</textarea>
2252 4106
2253 4107 </div>
2254 4108
2255 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2256 - <a class="button button-primary cancelled-reason-action-submit" href="#">' . __( 'Save', 'propertyhive' ) . '</a>
4109 + <a class="button action-cancel" href="#">' . esc_html(__( 'Cancel', 'propertyhive' )) . '</a>
4110 + <a class="button button-primary cancelled-reason-action-submit" href="#">' . esc_html(__( 'Save', 'propertyhive' )) . '</a>
2257 4111
2258 4112 </div>
2259 4113
2260 4114 </div>';
@@ -2265,15 +4119,38 @@
2265 4119 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_carried_out" style="display:none;">
2266 4120
2267 4121 <div class="options_group" style="padding-top:8px;">';
2268 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 +
2269 4146 if ( $department == 'residential-sales' )
2270 4147 {
2271 4148 echo '<div class="form-field">
2272 4149
2273 - <label for="_price">' . __( 'Valued Price (&pound;)', 'propertyhive' ) . '</label>
4150 + <label for="_price">' . esc_html(/* translators: %s: Currency symbol. */ sprintf( __( 'Valued Price (%s)', 'propertyhive' ), $currency_symbol )) . '</label>
2274 4151
2275 - <input type="text" id="_price" name="_price" style="width:100%;" value="' . get_post_meta( $post_id, '_valued_price', TRUE ) . '">
4152 + <input type="text" id="_price" name="_price" style="width:100%;" value="' . esc_attr(get_post_meta( $post_id, '_valued_price', TRUE )) . '">
2276 4153
2277 4154 </div>';
2278 4155 }
2279 4156 else
@@ -2280,25 +4157,26 @@
2280 4157 {
2281 4158 $rent_frequency = get_post_meta( $post_id, '_valued_rent_frequency', TRUE );
2282 4159 echo '<div class="form-field">
2283 4160
2284 - <label for="_price">' . __( 'Valued Rent (&pound;)', 'propertyhive' ) . '</label>
4161 + <label for="_price">' . esc_html(/* translators: %s: Currency symbol. */ sprintf( __( 'Valued Rent (%s)', 'propertyhive' ), $currency_symbol )) . '</label>
2285 4162
2286 - <input type="text" id="_price" name="_price" style="width:100%;" value="' . get_post_meta( $post_id, '_valued_rent', TRUE ) . '">
4163 + <input type="text" id="_price" name="_price" style="width:100%;" value="' . esc_attr(get_post_meta( $post_id, '_valued_rent', TRUE )) . '">
2287 4164
2288 4165 <select id="_rent_frequency" name="_rent_frequency" class="select" style="width:100%">
2289 - <option value="pppw"' . ( ($rent_frequency == 'pppw') ? ' selected' : '') . '>' . __('Per Person Per Week', 'propertyhive') . '</option>
2290 - <option value="pw"' . ( ($rent_frequency == 'pw') ? ' selected' : '') . '>' . __('Per Week', 'propertyhive') . '</option>
2291 - <option value="pcm"' . ( ($rent_frequency == 'pcm' || $rent_frequency == '') ? ' selected' : '') . '>' . __('Per Calendar Month', 'propertyhive') . '</option>
2292 - <option value="pq"' . ( ($rent_frequency == 'pq') ? ' selected' : '') . '>' . __('Per Quarter', 'propertyhive') . '</option>
2293 - <option value="pa"' . ( ($rent_frequency == 'pa') ? ' selected' : '') . '>' . __('Per Annum', 'propertyhive') . '</option>
4166 + <option value="pd"' . ( ($rent_frequency == 'pd') ? ' selected' : '') . '>' . esc_html(__('Per Day', 'propertyhive')) . '</option>
4167 + <option value="pppw"' . ( ($rent_frequency == 'pppw') ? ' selected' : '') . '>' . esc_html(__('Per Person Per Week', 'propertyhive')) . '</option>
4168 + <option value="pw"' . ( ($rent_frequency == 'pw') ? ' selected' : '') . '>' . esc_html(__('Per Week', 'propertyhive')) . '</option>
4169 + <option value="pcm"' . ( ($rent_frequency == 'pcm' || $rent_frequency == '') ? ' selected' : '') . '>' . esc_html(__('Per Calendar Month', 'propertyhive')) . '</option>
4170 + <option value="pq"' . ( ($rent_frequency == 'pq') ? ' selected' : '') . '>' . esc_html(__('Per Quarter', 'propertyhive')) . '</option>
4171 + <option value="pa"' . ( ($rent_frequency == 'pa') ? ' selected' : '') . '>' . esc_html(__('Per Annum', 'propertyhive')) . '</option>
2294 4172 </select>
2295 4173
2296 4174 </div>';
2297 4175 }
2298 4176
2299 - echo '<a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2300 - <a class="button button-primary carried-out-action-submit" href="#">' . __( 'Save', 'propertyhive' ) . '</a>
4177 + echo '<a class="button action-cancel" href="#">' . esc_html(__( 'Cancel', 'propertyhive' )) . '</a>
4178 + <a class="button button-primary carried-out-action-submit" href="#">' . esc_html(__( 'Save', 'propertyhive' )) . '</a>
2301 4179
2302 4180 </div>
2303 4181
2304 4182 </div>';
@@ -2309,12 +4187,12 @@
2309 4187 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_appraisal_instruct" style="display:none;">
2310 4188
2311 4189 <div class="options_group" style="padding-top:8px;">';
2312 4190
2313 - echo '<div style="margin-bottom:13px;">' . __( 'Upon instruction a new property record will be created within the \'Properties\' area.', 'propertyhive' ) . '</div>';
4191 + echo '<div style="margin-bottom:13px;">' . esc_html(__( 'Upon instruction a new property record will be created within the \'Properties\' area.', 'propertyhive' )) . '</div>';
2314 4192
2315 - echo '<a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2316 - <a class="button button-primary instructed-action-submit" href="#">' . __( 'OK', 'propertyhive' ) . '</a>
4193 + echo '<a class="button action-cancel" href="#">' . esc_html(__( 'Cancel', 'propertyhive' )) . '</a>
4194 + <a class="button button-primary instructed-action-submit" href="#">' . esc_html(__( 'OK', 'propertyhive' )) . '</a>
2317 4195
2318 4196 </div>
2319 4197
2320 4198 </div>';
@@ -2327,16 +4205,16 @@
2327 4205 <div class="options_group" style="padding-top:8px;">
2328 4206
2329 4207 <div class="form-field">
2330 4208
2331 - <label for="_lost_reason">' . __( 'Reason Lost', 'propertyhive' ) . '</label>
4209 + <label for="_lost_reason">' . esc_html(__( 'Reason Lost', 'propertyhive' )) . '</label>
2332 4210
2333 - <textarea id="_lost_reason" name="_lost_reason" style="width:100%;">' . get_post_meta( $post_id, '_lost_reason', TRUE ) . '</textarea>
4211 + <textarea id="_lost_reason" name="_lost_reason" style="width:100%;">' . esc_html(get_post_meta( $post_id, '_lost_reason', TRUE )) . '</textarea>
2334 4212
2335 4213 </div>
2336 4214
2337 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
2338 - <a class="button button-primary lost-reason-action-submit" href="#">' . wp_kses_post( __( 'Save Reason Lost', 'propertyhive' ) ) . '</a>
4215 + <a class="button action-cancel" href="#">' . esc_html(__( 'Cancel', 'propertyhive' )) . '</a>
4216 + <a class="button button-primary lost-reason-action-submit" href="#">' . esc_html( __( 'Save Reason Lost', 'propertyhive' ) ) . '</a>
2339 4217
2340 4218 </div>
2341 4219
2342 4220 </div>';
@@ -2348,34 +4226,57 @@
2348 4226 public function appraisal_carried_out()
2349 4227 {
2350 4228 check_ajax_referer( 'appraisal-actions', 'security' );
2351 4229
2352 - $post_id = (int)$_POST['appraisal_id'];
4230 + $post_id = isset( $_POST['appraisal_id'] ) && is_scalar( $_POST['appraisal_id'] ) ? absint( $_POST['appraisal_id'] ) : 0;
4231 + if ( $post_id < 1 || ! current_user_can( 'manage_propertyhive' ) || 'appraisal' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
4232 + wp_send_json_error( __( 'Invalid appraisal or insufficient permissions.', 'propertyhive' ), 403 );
4233 + }
2353 4234
2354 4235 $status = get_post_meta( $post_id, '_status', TRUE );
2355 4236
2356 4237 if ( $status == 'pending' )
2357 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 + }
2358 4258 update_post_meta( $post_id, '_status', 'carried_out' );
2359 4259
2360 4260 if ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-sales' )
2361 4261 {
2362 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['price']));
4262 + $price = preg_replace("/[^0-9.]/", '', $valuation_input['price']);
2363 4263 update_post_meta( $post_id, '_valued_price', $price );
2364 4264 update_post_meta( $post_id, '_valued_price_actual', $price );
2365 4265 }
2366 4266 elseif ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-lettings' )
2367 4267 {
2368 - $rent = preg_replace("/[^0-9]/", '', ph_clean($_POST['rent']));
4268 + $rent = preg_replace("/[^0-9.]/", '', $valuation_input['rent']);
2369 4269 update_post_meta( $post_id, '_valued_rent', $rent );
2370 4270
2371 - update_post_meta( $post_id, '_valued_rent_frequency', ph_clean($_POST['rent_frequency']) );
4271 + update_post_meta( $post_id, '_valued_rent_frequency', $valuation_input['rent_frequency'] );
2372 4272
2373 - switch (ph_clean($_POST['rent_frequency']))
4273 + switch ($valuation_input['rent_frequency'])
2374 4274 {
4275 + case "pd": { $price = ($rent * 365) / 12; break; }
2375 4276 case "pppw":
2376 4277 {
2377 - $bedrooms = get_post_meta( $postID, '_bedrooms', true );
4278 + $bedrooms = get_post_meta( $post_id, '_bedrooms', true );
2378 4279 if ( ( $bedrooms !== FALSE && $bedrooms != 0 && $bedrooms != '' ) && apply_filters( 'propertyhive_pppw_to_consider_bedrooms', true ) == true )
2379 4280 {
2380 4281 $price = (($rent * 52) / 12) * $bedrooms;
2381 4282 }
@@ -2392,10 +4293,8 @@
2392 4293 }
2393 4294 update_post_meta( $post_id, '_valued_price_actual', $price );
2394 4295 }
2395 4296
2396 - $current_user = wp_get_current_user();
2397 -
2398 4297 // Add note/comment to appraisal
2399 4298 $comment = array(
2400 4299 'note_type' => 'action',
2401 4300 'action' => 'appraisal_carried_out',
@@ -2400,22 +4299,14 @@
2400 4299 'note_type' => 'action',
2401 4300 'action' => 'appraisal_carried_out',
2402 4301 );
2403 4302
2404 - $data = array(
2405 - 'comment_post_ID' => $post_id,
2406 - 'comment_author' => $current_user->display_name,
2407 - 'comment_author_email' => '[email protected]',
2408 - 'comment_author_url' => '',
2409 - 'comment_date' => date("Y-m-d H:i:s"),
2410 - 'comment_content' => serialize($comment),
2411 - 'comment_approved' => 1,
2412 - 'comment_type' => 'propertyhive_note',
2413 - );
2414 - $comment_id = wp_insert_comment( $data );
4303 + PH_Comments::insert_note( $post_id, $comment );
4304 +
4305 + wp_send_json_success();
2415 4306 }
2416 4307
2417 - die();
4308 + wp_send_json_success();
2418 4309 }
2419 4310
2420 4311 public function appraisal_cancelled()
2421 4312 {
@@ -2420,19 +4311,25 @@
2420 4311 public function appraisal_cancelled()
2421 4312 {
2422 4313 check_ajax_referer( 'appraisal-actions', 'security' );
2423 4314
2424 - $post_id = (int)$_POST['appraisal_id'];
4315 + $post_id = isset( $_POST['appraisal_id'] ) && is_scalar( $_POST['appraisal_id'] ) ? absint( $_POST['appraisal_id'] ) : 0;
4316 + if ( $post_id < 1 || ! current_user_can( 'manage_propertyhive' ) || 'appraisal' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
4317 + wp_send_json_error( __( 'Invalid appraisal or insufficient permissions.', 'propertyhive' ), 403 );
4318 + }
2425 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 +
2426 4325 $status = get_post_meta( $post_id, '_status', TRUE );
2427 4326
2428 4327 if ( $status == 'pending' )
2429 4328 {
2430 4329 update_post_meta( $post_id, '_status', 'cancelled' );
2431 - update_post_meta( $post_id, '_cancelled_reason', sanitize_textarea_field( $_POST['cancelled_reason'] ) );
4330 + update_post_meta( $post_id, '_cancelled_reason', wp_slash( $reason ) );
2432 4331
2433 - $current_user = wp_get_current_user();
2434 -
2435 4332 // Add note/comment to appraisal
2436 4333 $comment = array(
2437 4334 'note_type' => 'action',
2438 4335 'action' => 'appraisal_cancelled',
@@ -2437,22 +4334,14 @@
2437 4334 'note_type' => 'action',
2438 4335 'action' => 'appraisal_cancelled',
2439 4336 );
2440 4337
2441 - $data = array(
2442 - 'comment_post_ID' => $post_id,
2443 - 'comment_author' => $current_user->display_name,
2444 - 'comment_author_email' => '[email protected]',
2445 - 'comment_author_url' => '',
2446 - 'comment_date' => date("Y-m-d H:i:s"),
2447 - 'comment_content' => serialize($comment),
2448 - 'comment_approved' => 1,
2449 - 'comment_type' => 'propertyhive_note',
2450 - );
2451 - $comment_id = wp_insert_comment( $data );
4338 + PH_Comments::insert_note( $post_id, $comment );
4339 +
4340 + wp_send_json_success();
2452 4341 }
2453 4342
2454 - die();
4343 + wp_send_json_error();
2455 4344 }
2456 4345
2457 4346 public function appraisal_won()
2458 4347 {
@@ -2457,9 +4346,12 @@
2457 4346 public function appraisal_won()
2458 4347 {
2459 4348 check_ajax_referer( 'appraisal-actions', 'security' );
2460 4349
2461 - $post_id = (int)$_POST['appraisal_id'];
4350 + $post_id = isset( $_POST['appraisal_id'] ) && is_scalar( $_POST['appraisal_id'] ) ? absint( $_POST['appraisal_id'] ) : 0;
4351 + if ( $post_id < 1 || ! current_user_can( 'manage_propertyhive' ) || 'appraisal' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
4352 + wp_send_json_error( __( 'Invalid appraisal or insufficient permissions.', 'propertyhive' ), 403 );
4353 + }
2462 4354
2463 4355 $status = get_post_meta( $post_id, '_status', TRUE );
2464 4356
2465 4357 if ( $status == 'carried_out' )
@@ -2465,10 +4357,8 @@
2465 4357 if ( $status == 'carried_out' )
2466 4358 {
2467 4359 update_post_meta( $post_id, '_status', 'won' );
2468 4360
2469 - $current_user = wp_get_current_user();
2470 -
2471 4361 // Add note/comment to appraisal
2472 4362 $comment = array(
2473 4363 'note_type' => 'action',
2474 4364 'action' => 'appraisal_won',
@@ -2473,22 +4363,14 @@
2473 4363 'note_type' => 'action',
2474 4364 'action' => 'appraisal_won',
2475 4365 );
2476 4366
2477 - $data = array(
2478 - 'comment_post_ID' => $post_id,
2479 - 'comment_author' => $current_user->display_name,
2480 - 'comment_author_email' => '[email protected]',
2481 - 'comment_author_url' => '',
2482 - 'comment_date' => date("Y-m-d H:i:s"),
2483 - 'comment_content' => serialize($comment),
2484 - 'comment_approved' => 1,
2485 - 'comment_type' => 'propertyhive_note',
2486 - );
2487 - $comment_id = wp_insert_comment( $data );
4367 + PH_Comments::insert_note( $post_id, $comment );
4368 +
4369 + wp_send_json_success();
2488 4370 }
2489 4371
2490 - die();
4372 + wp_send_json_error();
2491 4373 }
2492 4374
2493 4375 public function appraisal_lost_reason()
2494 4376 {
@@ -2493,19 +4375,25 @@
2493 4375 public function appraisal_lost_reason()
2494 4376 {
2495 4377 check_ajax_referer( 'appraisal-actions', 'security' );
2496 4378
2497 - $post_id = (int)$_POST['appraisal_id'];
4379 + $post_id = isset( $_POST['appraisal_id'] ) && is_scalar( $_POST['appraisal_id'] ) ? absint( $_POST['appraisal_id'] ) : 0;
4380 + if ( $post_id < 1 || ! current_user_can( 'manage_propertyhive' ) || 'appraisal' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
4381 + wp_send_json_error( __( 'Invalid appraisal or insufficient permissions.', 'propertyhive' ), 403 );
4382 + }
2498 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 +
2499 4389 $status = get_post_meta( $post_id, '_status', TRUE );
2500 4390
2501 4391 if ( $status == 'carried_out' )
2502 4392 {
2503 4393 update_post_meta( $post_id, '_status', 'lost' );
2504 - update_post_meta( $post_id, '_lost_reason', sanitize_textarea_field( $_POST['lost_reason'] ) );
4394 + update_post_meta( $post_id, '_lost_reason', wp_slash( $reason ) );
2505 4395
2506 - $current_user = wp_get_current_user();
2507 -
2508 4396 // Add note/comment to appraisal
2509 4397 $comment = array(
2510 4398 'note_type' => 'action',
2511 4399 'action' => 'appraisal_lost',
@@ -2510,22 +4398,14 @@
2510 4398 'note_type' => 'action',
2511 4399 'action' => 'appraisal_lost',
2512 4400 );
2513 4401
2514 - $data = array(
2515 - 'comment_post_ID' => $post_id,
2516 - 'comment_author' => $current_user->display_name,
2517 - 'comment_author_email' => '[email protected]',
2518 - 'comment_author_url' => '',
2519 - 'comment_date' => date("Y-m-d H:i:s"),
2520 - 'comment_content' => serialize($comment),
2521 - 'comment_approved' => 1,
2522 - 'comment_type' => 'propertyhive_note',
2523 - );
2524 - $comment_id = wp_insert_comment( $data );
4402 + PH_Comments::insert_note( $post_id, $comment );
4403 +
4404 + wp_send_json_success();
2525 4405 }
2526 4406
2527 - die();
4407 + wp_send_json_error();
2528 4408 }
2529 4409
2530 4410 public function appraisal_instructed()
2531 4411 {
@@ -2530,9 +4410,9 @@
2530 4410 public function appraisal_instructed()
2531 4411 {
2532 4412 check_ajax_referer( 'appraisal-actions', 'security' );
2533 4413
2534 - $post_id = (int)$_POST['appraisal_id'];
4414 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2535 4415
2536 4416 $status = get_post_meta( $post_id, '_status', TRUE );
2537 4417
2538 4418 if ( $status == 'won' )
@@ -2576,10 +4456,10 @@
2576 4456 {
2577 4457 // Failed. Don't really know at the moment how to handle this
2578 4458
2579 4459 $return = array('error' => 'Failed to create property post. Please try again');
2580 - //echo json_encode( $return );
2581 - //die();
4460 + echo json_encode( $return );
4461 + die();
2582 4462 }
2583 4463 else
2584 4464 {
2585 4465 // Successfully added property post
@@ -2585,8 +4465,24 @@
2585 4465 // Successfully added property post
2586 4466
2587 4467 $department = get_post_meta( $post_id, '_department', TRUE );
2588 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 +
2589 4485 update_post_meta( $property_post_id, '_address_name_number', get_post_meta( $post_id, '_address_name_number', TRUE ) );
2590 4486 update_post_meta( $property_post_id, '_address_street', get_post_meta( $post_id, '_address_street', TRUE ) );
2591 4487 update_post_meta( $property_post_id, '_address_two', get_post_meta( $post_id, '_address_two', TRUE ) );
2592 4488 update_post_meta( $property_post_id, '_address_three', get_post_meta( $post_id, '_address_three', TRUE ) );
@@ -2605,36 +4501,70 @@
2605 4501 if ( get_post_meta( $post_id, '_address_four', TRUE ) != '' ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_four', TRUE ); }
2606 4502 if ( get_post_meta( $post_id, '_address_postcode', TRUE ) ) { $address_to_geocode[] = get_post_meta( $post_id, '_address_postcode', TRUE ); }
2607 4503
2608 4504 $country = get_option( 'propertyhive_default_country', 'GB' );
2609 - $request_url = "https://maps.googleapis.com/maps/api/geocode/xml?address=" . urlencode( implode( ", ", $address_to_geocode ) ) . "&sensor=false&region=gb"; // the request URL you'll send to google to get back your XML feed
2610 -
2611 - $api_key = get_option('propertyhive_google_maps_api_key', '');
2612 - if ( $api_key != '' ) { $request_url .= "&key=" . $api_key; }
2613 4505
2614 - $response = wp_remote_get($request_url);
4506 + if ( get_option('propertyhive_geocoding_provider') == 'osm' )
4507 + {
4508 + $request_url = "https://nominatim.openstreetmap.org/search?format=json&limit=1&countrycodes=" . strtolower($country) . "&addressdetails=1&q=" . urlencode(implode( ", ", $address_to_geocode ));
4509 + $response = wp_remote_get(
4510 + $request_url,
4511 + array(
4512 + 'headers' => array(
4513 + 'Referer' => home_url(),
4514 + 'User-Agent' => 'Property-Hive/' . PH_VERSION . ' (+https://wp-property-hive.com)',
4515 + ),
4516 + )
4517 + );
4518 + if ( is_array( $response ) )
4519 + {
4520 + $body = wp_remote_retrieve_body( $response );
4521 + $json = json_decode($body, true);
2615 4522
2616 - if ( is_array( $response ) && !is_wp_error( $response ) )
4523 + if ( !empty($json) && isset($json[0]['lat']) && isset($json[0]['lon']) )
4524 + {
4525 + $lat = $json[0]['lat'];
4526 + $lng = $json[0]['lon'];
4527 +
4528 + if ($lat != '' && $lng != '')
4529 + {
4530 + update_post_meta( $property_post_id, '_latitude', $lat );
4531 + update_post_meta( $property_post_id, '_longitude', $lng );
4532 + }
4533 + }
4534 + }
4535 + }
4536 + else
2617 4537 {
2618 - $header = $response['headers']; // array of http header lines
2619 - $body = $response['body']; // use the content
4538 + $request_url = "https://maps.googleapis.com/maps/api/geocode/xml?address=" . urlencode( implode( ", ", $address_to_geocode ) ) . "&sensor=false&region=" . strtolower($country); // the request URL you'll send to google to get back your XML feed
2620 4539
2621 - $xml = simplexml_load_string($body);
4540 + $api_key = get_option('propertyhive_google_maps_api_key', '');
4541 + if ( $api_key != '' ) { $request_url .= "&key=" . $api_key; }
2622 4542
2623 - if ( $xml !== FALSE )
4543 + $response = wp_remote_get($request_url);
4544 +
4545 + if ( is_array( $response ) && !is_wp_error( $response ) )
2624 4546 {
2625 - $status = $xml->status; // Get the request status as google's api can return several responses
4547 + $header = $response['headers']; // array of http header lines
4548 + $body = $response['body']; // use the content
2626 4549
2627 - if ($status == "OK")
4550 + $xml = simplexml_load_string($body);
4551 +
4552 + if ( $xml !== FALSE )
2628 4553 {
2629 - //request returned completed time to get lat / lng for storage
2630 - $lat = (string)$xml->result->geometry->location->lat;
2631 - $lng = (string)$xml->result->geometry->location->lng;
2632 -
2633 - if ($lat != '' && $lng != '')
4554 + $status = $xml->status; // Get the request status as google's api can return several responses
4555 +
4556 + if ($status == "OK")
2634 4557 {
2635 - update_post_meta( $post_id, '_latitude', $lat );
2636 - update_post_meta( $post_id, '_longitude', $lng );
4558 + //request returned completed time to get lat / lng for storage
4559 + $lat = (string)$xml->result->geometry->location->lat;
4560 + $lng = (string)$xml->result->geometry->location->lng;
4561 +
4562 + if ($lat != '' && $lng != '')
4563 + {
4564 + update_post_meta( $property_post_id, '_latitude', $lat );
4565 + update_post_meta( $property_post_id, '_longitude', $lng );
4566 + }
2637 4567 }
2638 4568 }
2639 4569 }
2640 4570 }
@@ -2647,9 +4577,9 @@
2647 4577 case "residential-sales":
2648 4578 {
2649 4579 update_post_meta( $property_post_id, '_currency', 'GBP' );
2650 4580
2651 - $price = preg_replace("/[^0-9]/", '', get_post_meta( $post_id, '_valued_price', TRUE ));
4581 + $price = preg_replace("/[^0-9.]/", '', get_post_meta( $post_id, '_valued_price', TRUE ));
2652 4582 update_post_meta( $property_post_id, '_price', $price );
2653 4583
2654 4584 break;
2655 4585 }
@@ -2680,8 +4610,10 @@
2680 4610 wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'property_type', array("fields" => "ids") ), 'property_type' );
2681 4611 wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'parking', array("fields" => "ids") ), 'parking' );
2682 4612 wp_set_object_terms( $property_post_id, wp_get_object_terms( $post_id, 'outside_space', array("fields" => "ids") ), 'outside_space' );
2683 4613
4614 + update_post_meta( $property_post_id, '_council_tax_band', get_post_meta( $post_id, '_council_tax_band', TRUE ) );
4615 +
2684 4616 $owner_contact_ids = get_post_meta( $post_id, '_property_owner_contact_id', TRUE );
2685 4617 if ( !is_array($owner_contact_ids) )
2686 4618 {
2687 4619 $owner_contact_ids = array($owner_contact_ids);
@@ -2705,12 +4637,13 @@
2705 4637 // get appraisals where this is the owner and where not instructed
2706 4638 $args = array(
2707 4639 'post_type' => 'appraisal',
2708 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.
2709 4642 'meta_query' => array(
2710 4643 array(
2711 4644 'key' => '_property_owner_contact_id',
2712 - 'value' => $post->ID,
4645 + 'value' => $owner_contact_id,
2713 4646 'compare' => '='
2714 4647 ),
2715 4648 array(
2716 4649 'key' => '_status',
@@ -2734,31 +4667,197 @@
2734 4667
2735 4668 update_post_meta( $owner_contact_id, '_contact_types', $contact_types );
2736 4669 }
2737 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 + {
2738 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 + }
2739 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 + {
2740 4841 // Add note/comment to appraisal
2741 4842 $comment = array(
2742 4843 'note_type' => 'action',
2743 - 'action' => 'appraisal_instructed',
4844 + 'action' => 'appraisal_owner_booking_confirmation_email',
2744 4845 );
2745 4846
2746 - $data = array(
2747 - 'comment_post_ID' => $post_id,
2748 - 'comment_author' => $current_user->display_name,
2749 - 'comment_author_email' => '[email protected]',
2750 - 'comment_author_url' => '',
2751 - 'comment_date' => date("Y-m-d H:i:s"),
2752 - 'comment_content' => serialize($comment),
2753 - 'comment_approved' => 1,
2754 - 'comment_type' => 'propertyhive_note',
2755 - );
2756 - $comment_id = wp_insert_comment( $data );
4847 + PH_Comments::insert_note( $post_id, $comment );
2757 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();
2758 4853 }
4854 + else
4855 + {
4856 + wp_send_json_error('No owner recipients found');
4857 + }
2759 4858
2760 - die();
4859 + wp_die();
2761 4860 }
2762 4861
2763 4862 public function appraisal_revert_pending()
2764 4863 {
@@ -2763,9 +4862,9 @@
2763 4862 public function appraisal_revert_pending()
2764 4863 {
2765 4864 check_ajax_referer( 'appraisal-actions', 'security' );
2766 4865
2767 - $post_id = (int)$_POST['appraisal_id'];
4866 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2768 4867
2769 4868 $status = get_post_meta( $post_id, '_status', TRUE );
2770 4869
2771 4870 if ( $status == 'carried_out' || $status == 'cancelled' )
@@ -2771,10 +4870,8 @@
2771 4870 if ( $status == 'carried_out' || $status == 'cancelled' )
2772 4871 {
2773 4872 update_post_meta( $post_id, '_status', 'pending' );
2774 4873
2775 - $current_user = wp_get_current_user();
2776 -
2777 4874 // Add note/comment to appraisal
2778 4875 $comment = array(
2779 4876 'note_type' => 'action',
2780 4877 'action' => 'appraisal_revert_pending',
@@ -2779,22 +4876,14 @@
2779 4876 'note_type' => 'action',
2780 4877 'action' => 'appraisal_revert_pending',
2781 4878 );
2782 4879
2783 - $data = array(
2784 - 'comment_post_ID' => $post_id,
2785 - 'comment_author' => $current_user->display_name,
2786 - 'comment_author_email' => '[email protected]',
2787 - 'comment_author_url' => '',
2788 - 'comment_date' => date("Y-m-d H:i:s"),
2789 - 'comment_content' => serialize($comment),
2790 - 'comment_approved' => 1,
2791 - 'comment_type' => 'propertyhive_note',
2792 - );
2793 - $comment_id = wp_insert_comment( $data );
4880 + PH_Comments::insert_note( $post_id, $comment );
4881 +
4882 + wp_send_json_success();
2794 4883 }
2795 4884
2796 - die();
4885 + wp_send_json_error();
2797 4886 }
2798 4887
2799 4888 public function appraisal_revert_carried_out()
2800 4889 {
@@ -2799,9 +4888,9 @@
2799 4888 public function appraisal_revert_carried_out()
2800 4889 {
2801 4890 check_ajax_referer( 'appraisal-actions', 'security' );
2802 4891
2803 - $post_id = (int)$_POST['appraisal_id'];
4892 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2804 4893
2805 4894 $status = get_post_meta( $post_id, '_status', TRUE );
2806 4895
2807 4896 if ( $status == 'won' || $status == 'lost' )
@@ -2807,10 +4896,8 @@
2807 4896 if ( $status == 'won' || $status == 'lost' )
2808 4897 {
2809 4898 update_post_meta( $post_id, '_status', 'carried_out' );
2810 4899
2811 - $current_user = wp_get_current_user();
2812 -
2813 4900 // Add note/comment to appraisal
2814 4901 $comment = array(
2815 4902 'note_type' => 'action',
2816 4903 'action' => 'appraisal_revert_carried_out',
@@ -2815,22 +4902,14 @@
2815 4902 'note_type' => 'action',
2816 4903 'action' => 'appraisal_revert_carried_out',
2817 4904 );
2818 4905
2819 - $data = array(
2820 - 'comment_post_ID' => $post_id,
2821 - 'comment_author' => $current_user->display_name,
2822 - 'comment_author_email' => '[email protected]',
2823 - 'comment_author_url' => '',
2824 - 'comment_date' => date("Y-m-d H:i:s"),
2825 - 'comment_content' => serialize($comment),
2826 - 'comment_approved' => 1,
2827 - 'comment_type' => 'propertyhive_note',
2828 - );
2829 - $comment_id = wp_insert_comment( $data );
4906 + PH_Comments::insert_note( $post_id, $comment );
4907 +
4908 + wp_send_json_success();
2830 4909 }
2831 4910
2832 - die();
4911 + wp_send_json_error();
2833 4912 }
2834 4913
2835 4914 public function appraisal_revert_won()
2836 4915 {
@@ -2835,9 +4914,9 @@
2835 4914 public function appraisal_revert_won()
2836 4915 {
2837 4916 check_ajax_referer( 'appraisal-actions', 'security' );
2838 4917
2839 - $post_id = (int)$_POST['appraisal_id'];
4918 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
2840 4919
2841 4920 $status = get_post_meta( $post_id, '_status', TRUE );
2842 4921
2843 4922 if ( $status == 'instructed' )
@@ -2843,10 +4922,8 @@
2843 4922 if ( $status == 'instructed' )
2844 4923 {
2845 4924 update_post_meta( $post_id, '_status', 'won' );
2846 4925
2847 - $current_user = wp_get_current_user();
2848 -
2849 4926 // Add note/comment to appraisal
2850 4927 $comment = array(
2851 4928 'note_type' => 'action',
2852 4929 'action' => 'appraisal_revert_won',
@@ -2851,22 +4928,14 @@
2851 4928 'note_type' => 'action',
2852 4929 'action' => 'appraisal_revert_won',
2853 4930 );
2854 4931
2855 - $data = array(
2856 - 'comment_post_ID' => $post_id,
2857 - 'comment_author' => $current_user->display_name,
2858 - 'comment_author_email' => '[email protected]',
2859 - 'comment_author_url' => '',
2860 - 'comment_date' => date("Y-m-d H:i:s"),
2861 - 'comment_content' => serialize($comment),
2862 - 'comment_approved' => 1,
2863 - 'comment_type' => 'propertyhive_note',
2864 - );
2865 - $comment_id = wp_insert_comment( $data );
4932 + PH_Comments::insert_note( $post_id, $comment );
4933 +
4934 + wp_send_json_success();
2866 4935 }
2867 4936
2868 - die();
4937 + wp_send_json_error();
2869 4938 }
2870 4939
2871 4940 // Viewing related functions
2872 4941 public function book_viewing_property()
@@ -2874,10 +4943,11 @@
2874 4943 check_ajax_referer( 'book-viewing', 'security' );
2875 4944
2876 4945 $this->json_headers();
2877 4946
2878 - // TO DO: Should do validation on server side also
2879 - 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)
2880 4950 {
2881 4951 $return = array('error' => 'No property selected');
2882 4952 echo json_encode( $return );
2883 4953 die();
@@ -2882,18 +4952,26 @@
2882 4952 echo json_encode( $return );
2883 4953 die();
2884 4954 }
2885 4955
2886 - $property = new PH_Property((int)$_POST['property_id']);
4956 + $property = new PH_Property( $property_id );
2887 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 + }
2888 4966 $applicant_contact_ids = array();
2889 4967
2890 4968 // Create applicant record if required
2891 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
4969 + if (empty($booking['applicant_ids']) && !empty($booking['applicant_name']))
2892 4970 {
2893 4971 // Need to create contact/applicant
2894 4972 $contact_post = array(
2895 - 'post_title' => ph_clean($_POST['applicant_name']),
4973 + 'post_title' => $booking['applicant_name'],
2896 4974 'post_content' => '',
2897 4975 'post_type' => 'contact',
2898 4976 'post_status' => 'publish',
2899 4977 'comment_status' => 'closed',
@@ -2900,9 +4978,9 @@
2900 4978 'ping_status' => 'closed',
2901 4979 );
2902 4980
2903 4981 // Insert the post into the database
2904 - $contact_post_id = wp_insert_post( $contact_post );
4982 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
2905 4983
2906 4984 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
2907 4985 {
2908 4986 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -2911,8 +4989,27 @@
2911 4989 }
2912 4990
2913 4991 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
2914 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 +
2915 5012 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
2916 5013 update_post_meta( $contact_post_id, '_applicant_profile_0', array( 'department' => $property->department, 'send_matching_properties' => '' ) );
2917 5014
2918 5015 $applicant_contact_ids[] = $contact_post_id;
@@ -2917,20 +5014,12 @@
2917 5014
2918 5015 $applicant_contact_ids[] = $contact_post_id;
2919 5016 }
2920 5017
2921 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
5018 + if (!empty($booking['applicant_ids']) && empty($booking['applicant_name']))
2922 5019 {
2923 5020 // This is an existing contact
2924 - if ( !is_array($_POST['applicant_ids']) )
2925 - {
2926 - $_POST['applicant_ids'] = array(ph_clean($_POST['applicant_ids']));
2927 - }
2928 -
2929 - foreach ( $_POST['applicant_ids'] as $applicant_id )
2930 - {
2931 - $applicant_contact_ids[] = (int)$applicant_id;
2932 - }
5021 + $applicant_contact_ids = $booking['applicant_ids'];
2933 5022 }
2934 5023
2935 5024 $applicant_contact_ids = array_unique($applicant_contact_ids);
2936 5025
@@ -2995,53 +5084,37 @@
2995 5084 update_post_meta( $applicant_contact_id, '_applicant_profile_' . $num_applicant_profiles, array( 'department' => $property->department ) );
2996 5085 }
2997 5086 }*/
2998 5087
2999 - // Loop through contacts and create one viewing each
3000 - // At the moment it's a 1-to-1 relationship, but might support multiple in the future
3001 - foreach ( $applicant_contact_ids as $applicant_contact_id )
3002 - {
3003 - // Insert viewing record
3004 - $viewing_post = array(
3005 - 'post_title' => '',
3006 - 'post_content' => '',
3007 - 'post_type' => 'viewing',
3008 - 'post_status' => 'publish',
3009 - 'comment_status' => 'closed',
3010 - 'ping_status' => 'closed',
3011 - );
3012 -
3013 - // Insert the post into the database
3014 - $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 + );
3015 5097
3016 - if ( is_wp_error($viewing_post_id) || $viewing_post_id == 0 )
3017 - {
3018 - $return = array('error' => 'Failed to create viewing post. Please try again');
3019 - echo json_encode( $return );
3020 - die();
3021 - }
3022 -
3023 - add_post_meta( $viewing_post_id, '_start_date_time', ph_clean($_POST['start_date']) . ' ' . ph_clean($_POST['start_time']) );
3024 - add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
3025 - add_post_meta( $viewing_post_id, '_property_id', (int)$_POST['property_id'] );
3026 - add_post_meta( $viewing_post_id, '_applicant_contact_id', $applicant_contact_id );
3027 - add_post_meta( $viewing_post_id, '_status', 'pending' );
3028 - add_post_meta( $viewing_post_id, '_feedback_status', '' );
3029 - add_post_meta( $viewing_post_id, '_feedback', '' );
3030 - 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 );
3031 5100
3032 - if ( !empty($_POST['negotiator_ids']) )
3033 - {
3034 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
3035 - {
3036 - add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
3037 - }
3038 - }
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();
3039 5106 }
3040 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 +
3041 5112 $applicant_contacts = array();
3042 - foreach ( $applicant_contact_ids as $applicant_contact_id )
5113 + foreach ($applicant_contact_ids as $applicant_contact_id)
3043 5114 {
5115 + add_post_meta( $viewing_post_id, '_applicant_contact_id', $applicant_contact_id );
5116 +
3044 5117 $applicant_contacts[] = array(
3045 5118 'ID' => $applicant_contact_id,
3046 5119 'post_title' => get_the_title($applicant_contact_id),
3047 5120 'edit_link' => get_edit_post_link( $applicant_contact_id, '' ),
@@ -3047,8 +5120,21 @@
3047 5120 'edit_link' => get_edit_post_link( $applicant_contact_id, '' ),
3048 5121 );
3049 5122 }
3050 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 +
3051 5137 $return = array('success' => array(
3052 5138 'viewing' => array(
3053 5139 'ID' => $viewing_post_id,
3054 5140 'edit_link' => get_edit_post_link( $viewing_post_id, '' ),
@@ -3066,10 +5152,16 @@
3066 5152 check_ajax_referer( 'book-viewing', 'security' );
3067 5153
3068 5154 $this->json_headers();
3069 5155
3070 - // TO DO: Should do validation on server side also
3071 - 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)
3072 5164 {
3073 5165 $return = array('error' => 'No contact selected');
3074 5166 echo json_encode( $return );
3075 5167 die();
@@ -3074,9 +5166,9 @@
3074 5166 echo json_encode( $return );
3075 5167 die();
3076 5168 }
3077 5169
3078 - if (empty($_POST['property_ids']))
5170 + if (empty($booking['property_ids']))
3079 5171 {
3080 5172 $return = array('error' => 'No property selected');
3081 5173 echo json_encode( $return );
3082 5174 die();
@@ -3083,9 +5175,9 @@
3083 5175 }
3084 5176
3085 5177 // Loop through contacts and create one viewing each
3086 5178 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
3087 - foreach ( $_POST['property_ids'] as $property_id )
5179 + foreach ( $booking['property_ids'] as $property_id )
3088 5180 {
3089 5181 // Insert viewing record
3090 5182 $viewing_post = array(
3091 5183 'post_title' => '',
@@ -3105,20 +5197,20 @@
3105 5197 echo json_encode( $return );
3106 5198 die();
3107 5199 }
3108 5200
3109 - add_post_meta( $viewing_post_id, '_start_date_time', ph_clean($_POST['start_date']) . ' ' . ph_clean($_POST['start_time']) );
5201 + add_post_meta( $viewing_post_id, '_start_date_time', $booking['start_date'] . ' ' . $booking['start_time'] );
3110 5202 add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
3111 5203 add_post_meta( $viewing_post_id, '_property_id', (int)$property_id );
3112 - add_post_meta( $viewing_post_id, '_applicant_contact_id', (int)$_POST['contact_id'] );
5204 + add_post_meta( $viewing_post_id, '_applicant_contact_id', $contact_id );
3113 5205 add_post_meta( $viewing_post_id, '_status', 'pending' );
3114 5206 add_post_meta( $viewing_post_id, '_feedback_status', '' );
3115 5207 add_post_meta( $viewing_post_id, '_feedback', '' );
3116 5208 add_post_meta( $viewing_post_id, '_feedback_passed_on', '' );
3117 5209
3118 - if ( !empty($_POST['negotiator_ids']) )
5210 + if ( !empty($booking['negotiator_ids']) )
3119 5211 {
3120 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
5212 + foreach ( $booking['negotiator_ids'] as $negotiator_id )
3121 5213 {
3122 5214 add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
3123 5215 }
3124 5216 }
@@ -3124,9 +5216,9 @@
3124 5216 }
3125 5217 }
3126 5218
3127 5219 $properties = array();
3128 - foreach ( $_POST['property_ids'] as $property_id )
5220 + foreach ( $booking['property_ids'] as $property_id )
3129 5221 {
3130 5222 $properties[] = array(
3131 5223 'ID' => (int)$property_id,
3132 5224 'post_title' => get_the_title((int)$property_id),
@@ -3152,527 +5244,1144 @@
3152 5244 global $post;
3153 5245
3154 5246 check_ajax_referer( 'viewing-details-meta-box', 'security' );
3155 5247
3156 - $post = get_post((int)$_POST['viewing_id']);
5248 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3157 5249
3158 - $viewing = new PH_Viewing((int)$_POST['viewing_id']);
5250 + $post = get_post( $post_id );
3159 5251
3160 - echo '<div class="propertyhive_meta_box">';
5252 + $viewing = new PH_Viewing( $post_id );
5253 +
5254 + $readonly = isset( $_POST['readonly'] ) && is_scalar( $_POST['readonly'] ) ? filter_var( wp_unslash( $_POST['readonly'] ), FILTER_VALIDATE_BOOLEAN ) : false;
5255 +
5256 + include( PH()->plugin_path() . '/includes/admin/views/html-viewing-details-meta-box.php' );
5257 +
5258 + die();
5259 + }
5260 +
5261 + public function get_viewing_actions()
5262 + {
5263 + check_ajax_referer( 'viewing-actions', 'security' );
5264 +
5265 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5266 +
5267 + include( PH()->plugin_path() . '/includes/admin/views/html-viewing-actions.php' );
5268 +
5269 + die();
5270 + }
5271 +
5272 + public function get_viewing_lightbox()
5273 + {
5274 + global $post;
3161 5275
3162 - 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 + }
3163 5281
3164 - echo '<p class="form-field">
3165 -
3166 - <label for="">' . __('Status', 'propertyhive') . '</label>
3167 -
3168 - ' . ucwords(str_replace("_", " ", $viewing->status));
5282 + $post = get_post((int)$post_id);
3169 5283
3170 - 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' )
3171 5300 {
3172 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
3173 - {
3174 - $offer_id = get_post_meta( $viewing->id, '_offer_id', TRUE );
3175 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
3176 - {
3177 - $offer_id = '';
3178 - }
5301 + update_post_meta( $post_id, '_status', 'carried_out' );
3179 5302
3180 - if ( $offer_id != '' )
3181 - {
3182 - echo ' (<a href="' . get_edit_post_link($offer_id) . '">' . __('View Offer', 'propertyhive') . '</a>)';
3183 - }
3184 - }
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();
3185 5312 }
3186 -
3187 - echo '</p>';
3188 5313
3189 - if ( $viewing->status == 'cancelled' )
5314 + wp_send_json_error();
5315 + }
5316 +
5317 + public function viewing_no_show()
5318 + {
5319 + check_ajax_referer( 'viewing-actions', 'security' );
5320 +
5321 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5322 +
5323 + $status = get_post_meta( $post_id, '_status', TRUE );
5324 +
5325 + if ( $status == 'pending' )
3190 5326 {
3191 - $args = array(
3192 - 'id' => '_cancelled_reason',
3193 - 'label' => __( 'Reason Cancelled', 'propertyhive' ),
3194 - 'desc_tip' => false,
3195 - 'class' => '',
3196 - 'value' => $viewing->cancelled_reason,
3197 - 'custom_attributes' => array(
3198 - 'style' => 'width:95%; max-width:500px;'
3199 - )
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',
3200 5333 );
3201 - propertyhive_wp_textarea_input( $args );
5334 +
5335 + PH_Comments::insert_note( $post_id, $comment );
5336 +
5337 + wp_send_json_success();
3202 5338 }
3203 5339
3204 - if ( $viewing->status == 'carried_out' )
5340 + wp_send_json_error();
5341 + }
5342 +
5343 + public function viewing_cancelled()
5344 + {
5345 + check_ajax_referer( 'viewing-actions', 'security' );
5346 +
5347 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5348 +
5349 + $text = isset( $_POST['cancelled_reason'] ) && is_string( $_POST['cancelled_reason'] ) ? sanitize_textarea_field( wp_unslash( $_POST['cancelled_reason'] ) ) : '';
5350 +
5351 + $status = get_post_meta( $post_id, '_status', TRUE );
5352 +
5353 + if ( $status == 'pending' )
3205 5354 {
3206 - echo '<p class="form-field">
3207 -
3208 - <label for="">' . __('Applicant Feedback', 'propertyhive') . '</label>';
5355 + update_post_meta( $post_id, '_status', 'cancelled' );
5356 + update_post_meta( $post_id, '_cancelled_reason', wp_slash( $text ) );
5357 + update_post_meta( $post_id, '_cancelled_reason_public', isset($_POST['cancelled_reason_public']) && $_POST['cancelled_reason_public'] == 'yes' ? 'yes' : '' );
3209 5358
3210 - switch ( $viewing->feedback_status )
5359 + // Add note/comment to viewing
5360 + $comment = array(
5361 + 'note_type' => 'action',
5362 + 'action' => 'viewing_cancelled',
5363 + );
5364 +
5365 + PH_Comments::insert_note( $post_id, $comment );
5366 +
5367 + wp_send_json_success();
5368 + }
5369 +
5370 + wp_send_json_error();
5371 + }
5372 +
5373 + public function viewing_email_applicant_booking_confirmation()
5374 + {
5375 + check_ajax_referer( 'viewing-actions', 'security' );
5376 +
5377 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5378 +
5379 + $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
5380 + $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5381 +
5382 + if ( !is_array($applicant_contact_ids) || (int)$property_id == '' || count($applicant_contact_ids) == 0 || (int)$property_id == 0 )
5383 + {
5384 + wp_send_json_error('Missing contact or property');
5385 + }
5386 +
5387 + $property = new PH_Property((int)$property_id);
5388 +
5389 + $to = array();
5390 + foreach ($applicant_contact_ids as $applicant_contact_id)
5391 + {
5392 + $applicant_email_address = get_post_meta( $applicant_contact_id, '_email_address', TRUE );
5393 + $explode_applicant_email_address = explode( ",", $applicant_email_address );
5394 + foreach ( $explode_applicant_email_address as $email_address )
3211 5395 {
3212 - case "interested":
5396 + $to[] = sanitize_email($email_address);
5397 + }
5398 + }
5399 +
5400 + $to = array_filter($to);
5401 +
5402 + if ( !empty(implode($to)) )
5403 + {
5404 + $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_viewing_applicant_booking_confirmation_email_subject', '' );
5405 + $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_viewing_applicant_booking_confirmation_email_body', '' );
5406 +
5407 + $applicant_names = array();
5408 + $applicant_dears = array();
5409 + foreach ($applicant_contact_ids as $applicant_contact_id)
5410 + {
5411 + $applicant_contact = new PH_Contact($applicant_contact_id);
5412 + $applicant_names[] = $applicant_contact->post_title;
5413 + $applicant_dears[] = $applicant_contact->dear();
5414 + }
5415 + $applicant_names = array_filter($applicant_names);
5416 + $applicant_dears = array_filter($applicant_dears);
5417 +
5418 + $applicant_names_string = $this->get_list_string($applicant_names);
5419 + $applicant_dears_string = $this->get_list_string($applicant_dears);
5420 +
5421 + $negotiator_names = array();
5422 + $negotiator_names_string = '';
5423 +
5424 + $negotiator_email_addresses = array();
5425 + $negotiator_email_addresses_string = '';
5426 +
5427 + $negotiator_telephone_numbers = array();
5428 + $negotiator_telephone_numbers_string = '';
5429 +
5430 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
5431 + if ( !empty($negotiator_ids) )
5432 + {
5433 + foreach ( $negotiator_ids as $negotiator_id )
3213 5434 {
3214 - echo 'Interested';
3215 - break;
5435 + $negotiator = get_user_by( 'id', $negotiator_id );
5436 + if ( $negotiator !== false )
5437 + {
5438 + if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
5439 + {
5440 + $negotiator_names[] = $negotiator->display_name;
5441 + }
5442 +
5443 + if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
5444 + {
5445 + $negotiator_email_addresses[] = $negotiator->user_email;
5446 + }
5447 +
5448 + $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
5449 + if ( !empty($telephone_number) )
5450 + {
5451 + $negotiator_telephone_numbers[] = $telephone_number;
5452 + }
5453 + }
3216 5454 }
3217 - case "not_interested":
5455 + }
5456 + if ( !empty($negotiator_names) )
5457 + {
5458 + $last = array_slice($negotiator_names, -1);
5459 + $first = join(', ', array_slice($negotiator_names, 0, -1));
5460 + $both = array_filter(array_merge(array($first), $last), 'strlen');
5461 + $negotiator_names_string = join(' and ', $both);
5462 + }
5463 + if ( !empty($negotiator_email_addresses) )
5464 + {
5465 + $last = array_slice($negotiator_email_addresses, -1);
5466 + $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
5467 + $both = array_filter(array_merge(array($first), $last), 'strlen');
5468 + $negotiator_email_addresses_string = join(' and ', $both);
5469 + }
5470 + if ( !empty($negotiator_telephone_numbers) )
5471 + {
5472 + $last = array_slice($negotiator_telephone_numbers, -1);
5473 + $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
5474 + $both = array_filter(array_merge(array($first), $last), 'strlen');
5475 + $negotiator_telephone_numbers_string = join(' and ', $both);
5476 + }
5477 +
5478 + $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
5479 + $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
5480 + $subject = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5481 + $subject = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5482 + $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
5483 + $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
5484 + $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
5485 +
5486 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_applicant_booking_confirmation_email_subject; third-party email integrations depend on the established name.
5487 + $subject = apply_filters( 'viewing_applicant_booking_confirmation_email_subject', $subject, $post_id, $property_id );
5488 +
5489 + $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
5490 + $body = str_replace('[applicant_name]', $applicant_names_string, $body);
5491 + $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
5492 + $body = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5493 + $body = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5494 + $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
5495 + $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
5496 + $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5497 +
5498 + $body = html_entity_decode($body);
5499 +
5500 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_applicant_booking_confirmation_email_body; third-party email integrations depend on the established name.
5501 + $body = apply_filters( 'viewing_applicant_booking_confirmation_email_body', $body, $post_id, $property_id );
5502 +
5503 + $from = '';
5504 + $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
5505 + if ( $from_setting == 'user' )
5506 + {
5507 + $current_user = wp_get_current_user();
5508 + $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
5509 +
5510 + if ( $from == '' )
3218 5511 {
3219 - echo 'Not Interested';
3220 - break;
5512 + $from = $property->office_email_address;
3221 5513 }
3222 - case "not_required":
5514 + }
5515 + if ( $from_setting == 'office' )
5516 + {
5517 + $from = $property->office_email_address;
5518 + }
5519 + if ( $from == '' )
5520 + {
5521 + $from = get_option('propertyhive_email_from_address', '');
5522 + }
5523 + if ( $from == '' )
5524 + {
5525 + $from = get_bloginfo('admin_email');
5526 + }
5527 +
5528 + $attachments = array();
5529 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5530 + {
5531 + $uploaded_files = $this->get_viewing_email_uploads();
5532 +
5533 + // Handle each file upload
5534 + foreach ($uploaded_files['name'] as $key => $value)
3223 5535 {
3224 - echo 'Feedback Not Required';
3225 - break;
5536 + if ($uploaded_files['name'][$key])
5537 + {
5538 + $file = array(
5539 + 'name' => $uploaded_files['name'][$key],
5540 + 'type' => $uploaded_files['type'][$key],
5541 + 'tmp_name' => $uploaded_files['tmp_name'][$key],
5542 + 'error' => $uploaded_files['error'][$key],
5543 + 'size' => $uploaded_files['size'][$key]
5544 + );
5545 +
5546 + // Move the file to a temporary location
5547 + $upload_overrides = array('test_form' => false);
5548 + $movefile = wp_handle_upload($file, $upload_overrides);
5549 +
5550 + if ($movefile && !isset($movefile['error']))
5551 + {
5552 + // Add the file path to attachments array
5553 + $attachments[] = $movefile['file'];
5554 + }
5555 + else
5556 + {
5557 + // Handle error in file upload
5558 + wp_send_json_error($movefile['error']);
5559 + }
5560 + }
3226 5561 }
3227 - default:
3228 - {
3229 - echo 'Awaiting Feedback';
3230 - }
3231 5562 }
3232 5563
3233 - echo '</p>';
5564 + $headers = array();
5565 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
5566 + $headers[] = 'Reply-To: ' . sanitize_email($from);
5567 + $headers[] = 'Content-Type: text/plain; charset=UTF-8';
3234 5568
3235 - if ( $viewing->feedback_status == 'interested' || $viewing->feedback_status == 'not_interested' )
5569 + $headers = apply_filters( 'propertyhive_viewing_applicant_booking_confirmation_email_headers', $headers );
5570 +
5571 + $sent = wp_mail($to, $subject, $body, $headers, $attachments);
5572 +
5573 + foreach ($attachments as $temp_file)
3236 5574 {
3237 - $args = array(
3238 - 'id' => '_feedback',
3239 - 'label' => __( 'Feedback', 'propertyhive' ),
3240 - 'desc_tip' => false,
3241 - 'class' => '',
3242 - 'value' => $viewing->feedback,
3243 - 'custom_attributes' => array(
3244 - 'style' => 'width:95%; max-width:500px;'
3245 - )
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',
3246 5591 );
3247 - propertyhive_wp_textarea_input( $args );
5592 +
5593 + PH_Comments::insert_note( $post_id, $comment );
3248 5594 }
5595 +
5596 + wp_send_json_success();
3249 5597 }
3250 -
3251 - if ( $viewing->status == 'carried_out' && ( $viewing->feedback_status == 'interested' || $viewing->feedback_status == 'not_interested' ) )
5598 + else
3252 5599 {
3253 - echo '<p class="form-field">
3254 -
3255 - <label for="">' . __('Feedback Passed On', 'propertyhive') . '</label>';
3256 -
3257 - echo ( ($viewing->feedback_passed_on == 'yes') ? 'Yes' : 'No' );
3258 -
3259 - echo '</p>';
5600 + wp_send_json_error('No valid recipient email addresses');
3260 5601 }
3261 5602
3262 - do_action('propertyhive_viewing_details_fields');
3263 -
3264 - echo '</div>';
3265 -
3266 - echo '</div>';
3267 -
3268 - die();
5603 + wp_die();
3269 5604 }
3270 5605
3271 - public function get_viewing_actions()
5606 + public function viewing_email_owner_booking_confirmation()
3272 5607 {
3273 5608 check_ajax_referer( 'viewing-actions', 'security' );
3274 5609
3275 - $post_id = (int)$_POST['viewing_id'];
5610 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3276 5611
3277 - $status = get_post_meta( $post_id, '_status', TRUE );
3278 - $feedback_status = get_post_meta( $post_id, '_feedback_status', TRUE );
5612 + $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5613 + $property_department = get_post_meta( $property_id, '_department' );
3279 5614
3280 - echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_viewing_actions_meta_box">
5615 + $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
5616 + $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
5617 +
5618 + if ( $owner_contact_ids > 0 ) {
3281 5619
3282 - <div class="options_group" style="padding-top:8px;">';
5620 + $owner_emails = array();
5621 + $owner_names = array();
5622 + $owner_dears = array();
5623 +
5624 + foreach ($owner_contact_ids as $owner_id)
5625 + {
5626 + $owner_contact = new PH_Contact($owner_id);
3283 5627
3284 - $show_cancelled_meta_boxes = false;
3285 - $show_feedback_meta_boxes = false;
5628 + $owner_name = $owner_contact->post_title;
5629 + $owner_dear = $owner_contact->dear();
3286 5630
3287 - $actions = array();
5631 + if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
5632 + if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
3288 5633
3289 - if ( $status == 'pending' )
3290 - {
3291 - $applicant_contact_id = get_post_meta( $post_id, '_applicant_contact_id', TRUE );
3292 - $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3293 - $applicant_email_address = get_post_meta( $applicant_contact_id, '_email_address', TRUE );
5634 + $owner_email = $owner_contact->email_address;
5635 + $explode_owner_email = explode( ",", $owner_email );
5636 + foreach ( $explode_owner_email as $email_address )
5637 + {
5638 + $owner_emails[] = sanitize_email($email_address);
5639 + }
5640 + }
3294 5641
3295 - if ( (int)$applicant_contact_id == '' || (int)$property_id == '' || (int)$applicant_contact_id == 0 || (int)$property_id == 0 || sanitize_email($applicant_email_address) == '' )
5642 + $owner_names_string = $this->get_list_string($owner_names);
5643 + $owner_dears_string = $this->get_list_string($owner_dears);
5644 +
5645 + if ( !empty($applicant_contact_ids) )
3296 5646 {
5647 + $applicant_names = array();
5648 + $applicant_dears = array();
5649 + foreach ($applicant_contact_ids as $applicant_contact_id)
5650 + {
5651 + $applicant_contact = new PH_Contact($applicant_contact_id);
5652 + $applicant_names[] = $applicant_contact->post_title;
5653 + $applicant_dears[] = $applicant_contact->dear();
5654 + }
5655 + $applicant_names = array_filter($applicant_names);
5656 + $applicant_dears = array_filter($applicant_dears);
5657 + }
5658 +
5659 + $applicant_names_string = $this->get_list_string($applicant_names);
5660 + $applicant_dears_string = $this->get_list_string($applicant_dears);
3297 5661
5662 + $negotiator_names = array();
5663 + $negotiator_names_string = '';
5664 +
5665 + $negotiator_email_addresses = array();
5666 + $negotiator_email_addresses_string = '';
5667 +
5668 + $negotiator_telephone_numbers = array();
5669 + $negotiator_telephone_numbers_string = '';
5670 +
5671 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
5672 + if ( !empty($negotiator_ids) )
5673 + {
5674 + foreach ( $negotiator_ids as $negotiator_id )
5675 + {
5676 + $negotiator = get_user_by( 'id', $negotiator_id );
5677 + if ( $negotiator !== false )
5678 + {
5679 + if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
5680 + {
5681 + $negotiator_names[] = $negotiator->display_name;
5682 + }
5683 +
5684 + if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
5685 + {
5686 + $negotiator_email_addresses[] = $negotiator->user_email;
5687 + }
5688 +
5689 + $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
5690 + if ( !empty($telephone_number) )
5691 + {
5692 + $negotiator_telephone_numbers[] = $telephone_number;
5693 + }
5694 + }
5695 + }
3298 5696 }
3299 - else
5697 + if ( !empty($negotiator_names) )
3300 5698 {
3301 - $applicant_booking_confirmation_sent_at = get_post_meta( $post_id, '_applicant_booking_confirmation_sent_at', TRUE );
3302 - $owner_booking_confirmation_sent_at = get_post_meta( $post_id, '_owner_booking_confirmation_sent_at', TRUE );
3303 -
3304 - //Applicant
3305 - $actions[] = '<a
3306 - href="#action_panel_viewing_email_applicant_booking_confirmation"
3307 - class="button viewing-action"
3308 - style="width:100%; margin-bottom:7px; text-align:center"
3309 - >' . ( ( $applicant_booking_confirmation_sent_at == '' ) ? __('Email Applicant Booking Confirmation', 'propertyhive') : __('Re-Email Applicant Booking Confirmation', 'propertyhive') ) . '</a>';
5699 + $last = array_slice($negotiator_names, -1);
5700 + $first = join(', ', array_slice($negotiator_names, 0, -1));
5701 + $both = array_filter(array_merge(array($first), $last), 'strlen');
5702 + $negotiator_names_string = join(' and ', $both);
5703 + }
5704 + if ( !empty($negotiator_email_addresses) )
5705 + {
5706 + $last = array_slice($negotiator_email_addresses, -1);
5707 + $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
5708 + $both = array_filter(array_merge(array($first), $last), 'strlen');
5709 + $negotiator_email_addresses_string = join(' and ', $both);
5710 + }
5711 + if ( !empty($negotiator_telephone_numbers) )
5712 + {
5713 + $last = array_slice($negotiator_telephone_numbers, -1);
5714 + $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
5715 + $both = array_filter(array_merge(array($first), $last), 'strlen');
5716 + $negotiator_telephone_numbers_string = join(' and ', $both);
5717 + }
3310 5718
3311 - $actions[] = '<div id="viewing_applicant_confirmation_date" style="text-align:center; font-size:12px; color:#999; margin-bottom:7px;' . ( ( $applicant_booking_confirmation_sent_at == '' ) ? 'display:none' : '' ) . '">' . ( ( $applicant_booking_confirmation_sent_at != '' ) ? 'Previously sent to applicant on <span title="' . $applicant_booking_confirmation_sent_at . '">' . date("jS F", strtotime($applicant_booking_confirmation_sent_at)) : '' ) . '</span></div>';
5719 + $property = new PH_Property((int)$property_id);
3312 5720
3313 - // Owner/Landlord
3314 - $property_department = get_post_meta( $property_id, '_department', TRUE );
3315 - $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
3316 - $owner_or_landlord = ( $property_department == 'residential-lettings' ? 'Landlord' : 'Owner' );
5721 + $to = implode(",", $owner_emails);
3317 5722
3318 - if ( count($owner_contact_ids) > 0) {
5723 + $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_viewing_owner_booking_confirmation_email_subject', '' );
5724 + $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_viewing_owner_booking_confirmation_email_body', '' );
3319 5725
3320 - $actions[] = '<a
3321 - href="#action_panel_viewing_email_owner_booking_confirmation"
3322 - class="button viewing-action"
3323 - style="width:100%; margin-bottom:7px; text-align:center"
3324 - >' . ( ( $owner_booking_confirmation_sent_at == '' ) ? __('Email ' . $owner_or_landlord . ' Booking Confirmation', 'propertyhive') : __('Re-Email ' . $owner_or_landlord . ' Booking Confirmation', 'propertyhive') ) . '</a>';
3325 -
3326 - $actions[] = '<div id="viewing_owner_confirmation_date" style="text-align:center; font-size:12px; color:#999; margin-bottom:7px;' . ( ( $owner_booking_confirmation_sent_at == '' ) ? 'display:none' : '' ) . '">' . ( ( $owner_booking_confirmation_sent_at != '' ) ? 'Previously sent to ' . strtolower($owner_or_landlord) . ' on <span title="' . $owner_booking_confirmation_sent_at . '">' . date("jS F", strtotime($owner_booking_confirmation_sent_at)) : '' ) . '</span></div>';
3327 - }
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);
3328 5734
3329 - $actions[] = '<hr>';
3330 - }
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 );
3331 5737
3332 - $actions[] = '<a
3333 - href="#action_panel_viewing_carried_out"
3334 - class="button button-success viewing-action"
3335 - style="width:100%; margin-bottom:7px; text-align:center"
3336 - >' . __('Viewing Carried Out', 'propertyhive') . '</a>';
3337 - $actions[] = '<a
3338 - href="#action_panel_viewing_cancelled"
3339 - class="button viewing-action"
3340 - style="width:100%; margin-bottom:7px; text-align:center"
3341 - >' . __('Viewing Cancelled', 'propertyhive') . '</a>';
5738 + $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
5739 + $body = str_replace('[owner_name]', $owner_names_string, $body);
5740 + $body = str_replace('[owner_dear]', $owner_dears_string, $body);
5741 + $body = str_replace('[applicant_name]', $applicant_names_string, $body);
5742 + $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
5743 + $body = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5744 + $body = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5745 + $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
5746 + $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
5747 + $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
3342 5748
3343 - $show_cancelled_meta_boxes = true;
3344 - }
5749 + $body = html_entity_decode($body);
3345 5750
3346 - if ( $status == 'carried_out' )
3347 - {
3348 - if ( $feedback_status == '' )
5751 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_owner_booking_confirmation_email_body; third-party email integrations depend on the established name.
5752 + $body = apply_filters( 'viewing_owner_booking_confirmation_email_body', $body, $post_id, $property_id );
5753 +
5754 + $from = '';
5755 + $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
5756 + if ( $from_setting == 'user' )
3349 5757 {
3350 - $actions[] = '<a
3351 - href="#action_panel_viewing_interested"
3352 - class="button button-success viewing-action"
3353 - style="width:100%; margin-bottom:7px; text-align:center"
3354 - >' . wp_kses_post( __('Applicant Interested', 'propertyhive') ) . '</a>';
5758 + $current_user = wp_get_current_user();
5759 + $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
3355 5760
3356 - $actions[] = '<a
3357 - href="#action_panel_viewing_not_interested"
3358 - class="button button-danger viewing-action"
3359 - style="width:100%; margin-bottom:7px; text-align:center"
3360 - >' . wp_kses_post( __('Applicant Not Interested', 'propertyhive') ) . '</a>';
3361 -
3362 - $actions[] = '<a
3363 - href="#action_panel_viewing_feedback_not_required"
3364 - class="button viewing-action"
3365 - style="width:100%; margin-bottom:7px; text-align:center"
3366 - >' . wp_kses_post( __('Feedback Not Required', 'propertyhive') ) . '</a>';
3367 -
3368 - $show_feedback_meta_boxes = true;
5761 + if ( $from == '' )
5762 + {
5763 + $from = $property->office_email_address;
5764 + }
3369 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 + }
3370 5778
3371 - if ( $feedback_status == 'interested' )
5779 + $attachments = array();
5780 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
3372 5781 {
3373 - $actions[] = '<a
3374 - 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 .'"
3375 - class="button button-success"
3376 - style="width:100%; margin-bottom:7px; text-align:center"
3377 - >' . wp_kses_post( __('Book Second Viewing', 'propertyhive') ) . '</a>';
5782 + $uploaded_files = $this->get_viewing_email_uploads();
3378 5783
3379 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
5784 + // Handle each file upload
5785 + foreach ($uploaded_files['name'] as $key => $value)
3380 5786 {
3381 - $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3382 - if ( get_post_meta( $property_id, '_department', TRUE ) == 'residential-sales' )
5787 + if ($uploaded_files['name'][$key])
3383 5788 {
3384 - // See if an offer has this viewing id associated with it
3385 - $offer_id = get_post_meta( $post_id, '_offer_id', TRUE );
3386 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
3387 - {
3388 - $offer_id = '';
3389 - }
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 + );
3390 5796
3391 - if ( $offer_id != '' )
5797 + // Move the file to a temporary location
5798 + $upload_overrides = array('test_form' => false);
5799 + $movefile = wp_handle_upload($file, $upload_overrides);
5800 +
5801 + if ($movefile && !isset($movefile['error']))
3392 5802 {
3393 - $actions[] = '<a
3394 - href="' . get_edit_post_link( $offer_id, '' ) . '"
3395 - class="button"
3396 - style="width:100%; margin-bottom:7px; text-align:center"
3397 - >' . wp_kses_post( __('View Offer', 'propertyhive') ) . '</a>';
3398 - }
5803 + // Add the file path to attachments array
5804 + $attachments[] = $movefile['file'];
5805 + }
3399 5806 else
3400 5807 {
3401 - $actions[] = '<a
3402 - href="' . wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), '1', 'create_offer' ) . '"
3403 - class="button button-success"
3404 - style="width:100%; margin-bottom:7px; text-align:center"
3405 - >' . wp_kses_post( __('Record Offer', 'propertyhive') ) . '</a>';
5808 + // Handle error in file upload
5809 + wp_send_json_error($movefile['error']);
3406 5810 }
3407 5811 }
3408 5812 }
3409 5813 }
3410 5814
3411 - if ( get_post_meta( $post_id, '_feedback_passed_on', TRUE ) != 'yes' && ( $feedback_status == 'interested' || $feedback_status == 'not_interested' ) )
5815 + $headers = array();
5816 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
5817 + $headers[] = 'Reply-To: ' . sanitize_email($from);
5818 + $headers[] = 'Content-Type: text/plain; charset=UTF-8';
5819 +
5820 + $headers = apply_filters( 'propertyhive_viewing_owner_booking_confirmation_email_headers', $headers );
5821 +
5822 + $sent = wp_mail($to, $subject, $body, $headers, $attachments);
5823 +
5824 + foreach ($attachments as $temp_file)
3412 5825 {
3413 - $actions[] = '<a
3414 - href="#action_panel_viewing_revert_feedback_passed_on"
3415 - class="button viewing-action"
3416 - style="width:100%; margin-bottom:7px; text-align:center"
3417 - >' . wp_kses_post( __('Feedback Passed On To Owner', 'propertyhive') ) . '</a>';
5826 + @wp_delete_file($temp_file);
3418 5827 }
3419 5828
3420 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' || $feedback_status == 'not_required' )
5829 + if ( !$sent )
3421 5830 {
3422 - $actions[] = '<a
3423 - href="#action_panel_viewing_revert_feedback_pending"
3424 - class="button viewing-action"
3425 - style="width:100%; margin-bottom:7px; text-align:center"
3426 - >' . wp_kses_post( __('Revert To Feedback Pending', 'propertyhive') ) . '</a>';
5831 + wp_send_json_error('Failed to send email');
3427 5832 }
3428 - }
3429 5833
3430 - if ( $status == 'offer_made' )
3431 - {
3432 - if ( get_option('propertyhive_module_disabled_offers_sales', '') != 'yes' )
5834 + if ( apply_filters( 'propertyhive_log_booking_confirmation_emails', false ) === true )
3433 5835 {
3434 - $offer_id = get_post_meta( $post_id, '_offer_id', TRUE );
3435 - if ( $offer_id != '' && get_post_status($offer_id) != 'publish' )
3436 - {
3437 - $offer_id = '';
3438 - }
5836 + // Add note/comment to viewing
5837 + $comment = array(
5838 + 'note_type' => 'action',
5839 + 'action' => 'viewing_owner_booking_confirmation_email',
5840 + );
3439 5841
3440 - if ( $offer_id != '' )
3441 - {
3442 - $actions[] = '<a
3443 - href="' . get_edit_post_link( $offer_id, '' ) . '"
3444 - class="button"
3445 - style="width:100%; margin-bottom:7px; text-align:center"
3446 - >' . wp_kses_post( __('View Offer', 'propertyhive') ) . '</a>';
3447 - }
5842 + PH_Comments::insert_note( $post_id, $comment );
3448 5843 }
3449 - }
3450 5844
3451 - if ( ( $status == 'carried_out' && $feedback_status == '' ) || $status == 'cancelled' )
3452 - {
3453 - $actions[] = '<a
3454 - href="#action_panel_viewing_revert_pending"
3455 - class="button viewing-action"
3456 - style="width:100%; margin-bottom:7px; text-align:center"
3457 - >' . wp_kses_post( __('Revert To Pending', 'propertyhive') ) . '</a>';
3458 - }
5845 + update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
3459 5846
3460 - $actions = apply_filters( 'propertyhive_admin_viewing_actions', $actions, $post_id );
3461 -
3462 - if ( !empty($actions) )
3463 - {
3464 - echo implode("", $actions);
5847 + wp_send_json_success();
3465 5848 }
3466 5849 else
3467 5850 {
3468 - echo '<div style="text-align:center">' . wp_kses_post( __( 'No actions to display', 'propertyhive' ) ) . '</div>';
5851 + wp_send_json_error('No owner recipients');
3469 5852 }
3470 5853
3471 - echo '</div>
5854 + wp_die();
5855 + }
3472 5856
3473 - </div>';
5857 + public function viewing_email_attending_negotiator_booking_confirmation()
5858 + {
5859 + check_ajax_referer( 'viewing-actions', 'security' );
3474 5860
3475 - if ( $show_cancelled_meta_boxes )
3476 - {
3477 - echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_viewing_cancelled" style="display:none;">
5861 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5862 + $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3478 5863
3479 - <div class="options_group" style="padding-top:8px;">
5864 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
3480 5865
3481 - <div class="form-field">
5866 + $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
5867 + $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
5868 +
5869 + if ( !empty($negotiator_ids) ) {
3482 5870
3483 - <label for="_viewing_cancelled_reason">' . __( 'Reason Cancelled', 'propertyhive' ) . '</label>
3484 -
3485 - <textarea id="_cancelled_reason" name="_cancelled_reason" style="width:100%;">' . get_post_meta( $post_id, '_cancelled_reason', TRUE ) . '</textarea>
5871 + $tos = array();
5872 + foreach ($negotiator_ids as $negotiator_id)
5873 + {
5874 + $user_info = get_userdata((int)$negotiator_id);
5875 + $tos[] = sanitize_email($user_info->user_email);
5876 + }
5877 + $to = implode(",", $tos);
3486 5878
3487 - </div>
5879 + $owner_emails = array();
5880 + $owner_names = array();
5881 + $owner_dears = array();
5882 + $owner_details = array();
5883 +
5884 + if ( !empty($owner_contact_ids) )
5885 + {
5886 + foreach ($owner_contact_ids as $owner_id)
5887 + {
5888 + $owner_contact = new PH_Contact($owner_id);
3488 5889
3489 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
3490 - <a class="button button-primary cancelled-reason-action-submit" href="#">' . __( 'Save', 'propertyhive' ) . '</a>
5890 + $owner_name = $owner_contact->post_title;
5891 + $owner_dear = $owner_contact->dear();
3491 5892
3492 - </div>
5893 + if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
5894 + if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
3493 5895
3494 - </div>';
3495 - }
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 + }
3496 5902
3497 - if ( $show_feedback_meta_boxes )
3498 - {
3499 - echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_viewing_interested" style="display:none;">
5903 + $owner_details[] = $owner_contact->post_title . "\nT: " . $owner_contact->telephone_number . "\nE: " . $owner_contact->email_address;
5904 + }
5905 + }
3500 5906
3501 - <div class="options_group" style="padding-top:8px;">
5907 + $owner_details = implode("\n\n", $owner_details);
3502 5908
3503 - <div class="form-field">
5909 + $owner_names_string = $this->get_list_string($owner_names);
5910 + $owner_dears_string = $this->get_list_string($owner_dears);
3504 5911
3505 - <label for="_viewing_interested_feedback">' . __( 'Applicant Feedback', 'propertyhive' ) . '</label>
3506 -
3507 - <textarea id="_interested_feedback" name="_interested_feedback" style="width:100%;">' . get_post_meta( $post_id, '_feedback', TRUE ) . '</textarea>
5912 + $applicant_names = array();
5913 + $applicant_dears = array();
5914 + $applicant_details = array();
3508 5915
3509 - </div>
5916 + if ( !empty($applicant_contact_ids) )
5917 + {
5918 + foreach ($applicant_contact_ids as $applicant_contact_id)
5919 + {
5920 + $applicant_contact = new PH_Contact($applicant_contact_id);
5921 + $applicant_names[] = $applicant_contact->post_title;
5922 + $applicant_dears[] = $applicant_contact->dear();
3510 5923
3511 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
3512 - <a class="button button-primary interested-feedback-action-submit" href="#">' . wp_kses_post( __( 'Save Feedback', 'propertyhive' ) ) . '</a>
5924 + $applicant_details[] = $applicant_contact->post_title . "\nT: " . $applicant_contact->telephone_number . "\nE: " . $applicant_contact->email_address;
5925 + }
5926 + }
3513 5927
3514 - </div>
5928 + $applicant_details = implode("\n\n", $applicant_details);
3515 5929
3516 - </div>';
5930 + $applicant_names = array_filter($applicant_names);
5931 + $applicant_dears = array_filter($applicant_dears);
3517 5932
3518 - echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="action_panel_viewing_not_interested" style="display:none;">
5933 + $applicant_names_string = $this->get_list_string($applicant_names);
5934 + $applicant_dears_string = $this->get_list_string($applicant_dears);
3519 5935
3520 - <div class="options_group" style="padding-top:8px;">
5936 + $negotiator_names = array();
5937 + $negotiator_names_string = '';
3521 5938
3522 - <div class="form-field">
5939 + $negotiator_email_addresses = array();
5940 + $negotiator_email_addresses_string = '';
3523 5941
3524 - <label for="_viewing_not_interested_feedback">' . __( 'Applicant Feedback', 'propertyhive' ) . '</label>
5942 + $negotiator_telephone_numbers = array();
5943 + $negotiator_telephone_numbers_string = '';
5944 +
5945 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
5946 + if ( !empty($negotiator_ids) )
5947 + {
5948 + foreach ( $negotiator_ids as $negotiator_id )
5949 + {
5950 + $negotiator = get_user_by( 'id', $negotiator_id );
5951 + if ( $negotiator !== false )
5952 + {
5953 + if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
5954 + {
5955 + $negotiator_names[] = $negotiator->display_name;
5956 + }
3525 5957
3526 - <textarea id="_not_interested_feedback" name="_not_interested_feedback" style="width:100%;">' . get_post_meta( $post_id, '_feedback', TRUE ) . '</textarea>
5958 + if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
5959 + {
5960 + $negotiator_email_addresses[] = $negotiator->user_email;
5961 + }
3527 5962
3528 - </div>
5963 + $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
5964 + if ( !empty($telephone_number) )
5965 + {
5966 + $negotiator_telephone_numbers[] = $telephone_number;
5967 + }
5968 + }
5969 + }
5970 + }
5971 + if ( !empty($negotiator_names) )
5972 + {
5973 + $last = array_slice($negotiator_names, -1);
5974 + $first = join(', ', array_slice($negotiator_names, 0, -1));
5975 + $both = array_filter(array_merge(array($first), $last), 'strlen');
5976 + $negotiator_names_string = join(' and ', $both);
5977 + }
5978 + if ( !empty($negotiator_email_addresses) )
5979 + {
5980 + $last = array_slice($negotiator_email_addresses, -1);
5981 + $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
5982 + $both = array_filter(array_merge(array($first), $last), 'strlen');
5983 + $negotiator_email_addresses_string = join(' and ', $both);
5984 + }
5985 + if ( !empty($negotiator_telephone_numbers) )
5986 + {
5987 + $last = array_slice($negotiator_telephone_numbers, -1);
5988 + $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
5989 + $both = array_filter(array_merge(array($first), $last), 'strlen');
5990 + $negotiator_telephone_numbers_string = join(' and ', $both);
5991 + }
3529 5992
3530 - <a class="button action-cancel" href="#">' . __( 'Cancel', 'propertyhive' ) . '</a>
3531 - <a class="button button-primary not-interested-feedback-action-submit" href="#">' . wp_kses_post( __( 'Save Feedback', 'propertyhive' ) ) . '</a>
5993 + $property = new PH_Property((int)$property_id);
3532 5994
3533 - </div>
5995 + $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_viewing_attending_negotiator_booking_confirmation_email_subject', '' );
5996 + $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_viewing_attending_negotiator_booking_confirmation_email_body', '' );
3534 5997
3535 - </div>';
3536 - }
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);
3537 6006
3538 - die();
3539 - }
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 );
3540 6009
3541 - public function viewing_carried_out()
3542 - {
3543 - check_ajax_referer( 'viewing-actions', 'security' );
6010 + $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
6011 + $body = str_replace('[owner_name]', $owner_names_string, $body);
6012 + $body = str_replace('[owner_dear]', $owner_dears_string, $body);
6013 + $body = str_replace('[owner_details]', $owner_details, $body);
6014 + $body = str_replace('[applicant_name]', $applicant_names_string, $body);
6015 + $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
6016 + $body = str_replace('[applicant_details]', $applicant_details, $body);
6017 + $body = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6018 + $body = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6019 + $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
6020 + $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
6021 + $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
3544 6022
3545 - $post_id = (int)$_POST['viewing_id'];
6023 + $body = html_entity_decode($body);
3546 6024
3547 - $status = get_post_meta( $post_id, '_status', TRUE );
6025 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_attending_negotiator_booking_confirmation_email_body; third-party email integrations depend on the established name.
6026 + $body = apply_filters( 'viewing_attending_negotiator_booking_confirmation_email_body', $body, $post_id, $property_id );
3548 6027
3549 - if ( $status == 'pending' )
3550 - {
3551 - update_post_meta( $post_id, '_status', 'carried_out' );
6028 + $from = '';
6029 + $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
6030 + if ( $from_setting == 'user' )
6031 + {
6032 + $current_user = wp_get_current_user();
6033 + $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
3552 6034
3553 - $current_user = wp_get_current_user();
6035 + if ( $from == '' )
6036 + {
6037 + $from = $property->office_email_address;
6038 + }
6039 + }
6040 + if ( $from_setting == 'office' )
6041 + {
6042 + $from = $property->office_email_address;
6043 + }
6044 + if ( $from == '' )
6045 + {
6046 + $from = get_option('propertyhive_email_from_address', '');
6047 + }
6048 + if ( $from == '' )
6049 + {
6050 + $from = get_bloginfo('admin_email');
6051 + }
3554 6052
3555 - // Add note/comment to viewing
3556 - $comment = array(
3557 - 'note_type' => 'action',
3558 - 'action' => 'viewing_carried_out',
3559 - );
6053 + $attachments = array();
6054 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6055 + {
6056 + $uploaded_files = $this->get_viewing_email_uploads();
3560 6057
3561 - $data = array(
3562 - 'comment_post_ID' => $post_id,
3563 - 'comment_author' => $current_user->display_name,
3564 - 'comment_author_email' => '[email protected]',
3565 - 'comment_author_url' => '',
3566 - 'comment_date' => date("Y-m-d H:i:s"),
3567 - 'comment_content' => serialize($comment),
3568 - 'comment_approved' => 1,
3569 - 'comment_type' => 'propertyhive_note',
3570 - );
3571 - $comment_id = wp_insert_comment( $data );
3572 - }
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 + );
3573 6070
3574 - die();
3575 - }
6071 + // Move the file to a temporary location
6072 + $upload_overrides = array('test_form' => false);
6073 + $movefile = wp_handle_upload($file, $upload_overrides);
3576 6074
3577 - public function viewing_cancelled()
3578 - {
3579 - check_ajax_referer( 'viewing-actions', 'security' );
6075 + if ($movefile && !isset($movefile['error']))
6076 + {
6077 + // Add the file path to attachments array
6078 + $attachments[] = $movefile['file'];
6079 + }
6080 + else
6081 + {
6082 + // Handle error in file upload
6083 + wp_send_json_error($movefile['error']);
6084 + }
6085 + }
6086 + }
6087 + }
3580 6088
3581 - $post_id = (int)$_POST['viewing_id'];
6089 + $headers = array();
6090 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
6091 + $headers[] = 'Reply-To: ' . sanitize_email($from);
6092 + $headers[] = 'Content-Type: text/plain; charset=UTF-8';
3582 6093
3583 - $status = get_post_meta( $post_id, '_status', TRUE );
6094 + $headers = apply_filters( 'propertyhive_viewing_attending_negotiator_booking_confirmation_email_headers', $headers );
3584 6095
3585 - if ( $status == 'pending' )
3586 - {
3587 - update_post_meta( $post_id, '_status', 'cancelled' );
3588 - update_post_meta( $post_id, '_cancelled_reason', sanitize_textarea_field( $_POST['cancelled_reason'] ) );
6096 + $sent = wp_mail($to, $subject, $body, $headers, $attachments);
3589 6097
3590 - $current_user = wp_get_current_user();
6098 + foreach ($attachments as $temp_file)
6099 + {
6100 + @wp_delete_file($temp_file);
6101 + }
3591 6102
6103 + if ( !$sent )
6104 + {
6105 + wp_send_json_error('Failed to send email');
6106 + }
6107 +
3592 6108 // Add note/comment to viewing
3593 - $comment = array(
3594 - 'note_type' => 'action',
3595 - 'action' => 'viewing_cancelled',
3596 - );
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 + );
3597 6115
3598 - $data = array(
3599 - 'comment_post_ID' => $post_id,
3600 - 'comment_author' => $current_user->display_name,
3601 - 'comment_author_email' => '[email protected]',
3602 - 'comment_author_url' => '',
3603 - 'comment_date' => date("Y-m-d H:i:s"),
3604 - 'comment_content' => serialize($comment),
3605 - 'comment_approved' => 1,
3606 - 'comment_type' => 'propertyhive_note',
3607 - );
3608 - $comment_id = wp_insert_comment( $data );
6116 + PH_Comments::insert_note( $post_id, $comment );
6117 + }
6118 +
6119 + update_post_meta( $post_id, '_attending_negotiator_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
6120 +
6121 + wp_send_json_success();
3609 6122 }
6123 + else
6124 + {
6125 + wp_send_json_error('No attending negotiator recipients');
6126 + }
3610 6127
3611 - die();
6128 + wp_die();
3612 6129 }
3613 6130
3614 - public function viewing_email_applicant_booking_confirmation()
6131 + public function viewing_email_applicant_cancellation_notification()
3615 6132 {
3616 6133 check_ajax_referer( 'viewing-actions', 'security' );
3617 6134
3618 - $post_id = (int)$_POST['viewing_id'];
6135 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3619 6136
3620 - $applicant_contact_id = get_post_meta( $post_id, '_applicant_contact_id', TRUE );
6137 + $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
3621 6138 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3622 6139
3623 - if ( (int)$applicant_contact_id == '' || (int)$property_id == '' || (int)$applicant_contact_id == 0 || (int)$property_id == 0 )
6140 + if ( !is_array($applicant_contact_ids) || (int)$property_id == '' || count($applicant_contact_ids) == 0 || (int)$property_id == 0 )
3624 6141 {
3625 - die();
6142 + wp_send_json_error('Missing contact or property');
3626 6143 }
3627 6144
3628 6145 $property = new PH_Property((int)$property_id);
3629 6146
3630 - $to = get_post_meta( $applicant_contact_id, '_email_address', TRUE );
6147 + $to = array();
6148 + foreach ($applicant_contact_ids as $applicant_contact_id)
6149 + {
6150 + $applicant_email_address = get_post_meta( $applicant_contact_id, '_email_address', TRUE );
6151 + $explode_applicant_email_address = explode( ",", $applicant_email_address );
6152 + foreach ( $explode_applicant_email_address as $email_address )
6153 + {
6154 + $to[] = sanitize_email($email_address);
6155 + }
6156 + }
3631 6157
3632 - if ( sanitize_email($to) != '' )
6158 + $to = array_filter($to);
6159 +
6160 + if ( !empty(implode($to)) )
3633 6161 {
3634 - $subject = get_option( 'propertyhive_viewing_applicant_booking_confirmation_email_subject', '' );
3635 - $body = get_option( 'propertyhive_viewing_applicant_booking_confirmation_email_body', '' );
6162 + $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_viewing_applicant_cancellation_notification_email_subject', '' );
6163 + $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_viewing_applicant_cancellation_notification_email_body', '' );
3636 6164
6165 + $applicant_names = array();
6166 + $applicant_dears = array();
6167 + foreach ($applicant_contact_ids as $applicant_contact_id)
6168 + {
6169 + $applicant_contact = new PH_Contact($applicant_contact_id);
6170 + $applicant_names[] = $applicant_contact->post_title;
6171 + $applicant_dears[] = $applicant_contact->dear();
6172 + }
6173 + $applicant_names = array_filter($applicant_names);
6174 + $applicant_dears = array_filter($applicant_dears);
6175 +
6176 + $applicant_names_string = $this->get_list_string($applicant_names);
6177 + $applicant_dears_string = $this->get_list_string($applicant_dears);
6178 +
6179 + $negotiator_names = array();
6180 + $negotiator_names_string = '';
6181 +
6182 + $negotiator_email_addresses = array();
6183 + $negotiator_email_addresses_string = '';
6184 +
6185 + $negotiator_telephone_numbers = array();
6186 + $negotiator_telephone_numbers_string = '';
6187 +
6188 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
6189 + if ( !empty($negotiator_ids) )
6190 + {
6191 + foreach ( $negotiator_ids as $negotiator_id )
6192 + {
6193 + $negotiator = get_user_by( 'id', $negotiator_id );
6194 + if ( $negotiator !== false )
6195 + {
6196 + if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
6197 + {
6198 + $negotiator_names[] = $negotiator->display_name;
6199 + }
6200 +
6201 + if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
6202 + {
6203 + $negotiator_email_addresses[] = $negotiator->user_email;
6204 + }
6205 +
6206 + $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
6207 + if ( !empty($telephone_number) )
6208 + {
6209 + $negotiator_telephone_numbers[] = $telephone_number;
6210 + }
6211 + }
6212 + }
6213 + }
6214 + if ( !empty($negotiator_names) )
6215 + {
6216 + $last = array_slice($negotiator_names, -1);
6217 + $first = join(', ', array_slice($negotiator_names, 0, -1));
6218 + $both = array_filter(array_merge(array($first), $last), 'strlen');
6219 + $negotiator_names_string = join(' and ', $both);
6220 + }
6221 + if ( !empty($negotiator_email_addresses) )
6222 + {
6223 + $last = array_slice($negotiator_email_addresses, -1);
6224 + $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
6225 + $both = array_filter(array_merge(array($first), $last), 'strlen');
6226 + $negotiator_email_addresses_string = join(' and ', $both);
6227 + }
6228 + if ( !empty($negotiator_telephone_numbers) )
6229 + {
6230 + $last = array_slice($negotiator_telephone_numbers, -1);
6231 + $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
6232 + $both = array_filter(array_merge(array($first), $last), 'strlen');
6233 + $negotiator_telephone_numbers_string = join(' and ', $both);
6234 + }
6235 +
3637 6236 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
3638 - $subject = str_replace('[applicant_name]', get_the_title($applicant_contact_id), $subject);
3639 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
3640 - $subject = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6237 + $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
6238 + $subject = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6239 + $subject = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6240 + $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
6241 + $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
6242 + $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
3641 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 +
3642 6247 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
3643 - $body = str_replace('[applicant_name]', get_the_title($applicant_contact_id), $body);
3644 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
3645 - $body = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6248 + $body = str_replace('[applicant_name]', $applicant_names_string, $body);
6249 + $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
6250 + $body = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6251 + $body = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6252 + $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
6253 + $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
6254 + $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
3646 6255
3647 - $from = $property->office_email_address;
3648 - if ( sanitize_email($from) == '' )
6256 + $cancelled_reason = '';
6257 + if (
6258 + get_post_meta( $post_id, '_cancelled_reason_public', true ) == 'yes' &&
6259 + get_post_meta( $post_id, '_cancelled_reason', true ) != ''
6260 + )
3649 6261 {
6262 + $cancelled_reason .= "\n\nReason: " . get_post_meta( $post_id, '_cancelled_reason', true );
6263 + }
6264 + $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
6265 +
6266 + $body = html_entity_decode($body);
6267 +
6268 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_applicant_cancellation_notification_email_body; third-party email integrations depend on the established name.
6269 + $body = apply_filters( 'viewing_applicant_cancellation_notification_email_body', $body, $post_id, $property_id );
6270 +
6271 + $from = '';
6272 + $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
6273 + if ( $from_setting == 'user' )
6274 + {
6275 + $current_user = wp_get_current_user();
6276 + $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
6277 +
6278 + if ( $from == '' )
6279 + {
6280 + $from = $property->office_email_address;
6281 + }
6282 + }
6283 + if ( $from_setting == 'office' )
6284 + {
6285 + $from = $property->office_email_address;
6286 + }
6287 + if ( $from == '' )
6288 + {
6289 + $from = get_option('propertyhive_email_from_address', '');
6290 + }
6291 + if ( $from == '' )
6292 + {
3650 6293 $from = get_bloginfo('admin_email');
3651 6294 }
3652 6295
6296 + $attachments = array();
6297 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6298 + {
6299 + $uploaded_files = $this->get_viewing_email_uploads();
6300 +
6301 + // Handle each file upload
6302 + foreach ($uploaded_files['name'] as $key => $value)
6303 + {
6304 + if ($uploaded_files['name'][$key])
6305 + {
6306 + $file = array(
6307 + 'name' => $uploaded_files['name'][$key],
6308 + 'type' => $uploaded_files['type'][$key],
6309 + 'tmp_name' => $uploaded_files['tmp_name'][$key],
6310 + 'error' => $uploaded_files['error'][$key],
6311 + 'size' => $uploaded_files['size'][$key]
6312 + );
6313 +
6314 + // Move the file to a temporary location
6315 + $upload_overrides = array('test_form' => false);
6316 + $movefile = wp_handle_upload($file, $upload_overrides);
6317 +
6318 + if ($movefile && !isset($movefile['error']))
6319 + {
6320 + // Add the file path to attachments array
6321 + $attachments[] = $movefile['file'];
6322 + }
6323 + else
6324 + {
6325 + // Handle error in file upload
6326 + wp_send_json_error($movefile['error']);
6327 + }
6328 + }
6329 + }
6330 + }
6331 +
3653 6332 $headers = array();
3654 - $headers[] = 'From: ' . get_bloginfo('name') . ' <' . $from . '>';
6333 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
6334 + $headers[] = 'Reply-To: ' . sanitize_email($from);
3655 6335 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
3656 6336
3657 - wp_mail($to, $subject, $body, $headers);
6337 + $headers = apply_filters( 'propertyhive_viewing_applicant_cancellation_notification_email_headers', $headers );
3658 6338
3659 - update_post_meta( $post_id, '_applicant_booking_confirmation_sent_at', date("Y-m-d H:i:s") );
6339 + $sent = wp_mail($to, $subject, $body, $headers, $attachments);
6340 +
6341 + foreach ($attachments as $temp_file)
6342 + {
6343 + @wp_delete_file($temp_file);
6344 + }
6345 +
6346 + if ( !$sent )
6347 + {
6348 + wp_send_json_error('Failed to send email');
6349 + }
6350 +
6351 + update_post_meta( $post_id, '_applicant_cancellation_notification_sent_at', gmdate("Y-m-d H:i:s") );
6352 +
6353 + if ( apply_filters( 'propertyhive_log_cancellation_notification_emails', false ) === true )
6354 + {
6355 + // Add note/comment to viewing
6356 + $comment = array(
6357 + 'note_type' => 'action',
6358 + 'action' => 'viewing_applicant_cancellation_notification_email',
6359 + );
6360 +
6361 + PH_Comments::insert_note( $post_id, $comment );
6362 + }
6363 +
6364 + wp_send_json_success();
3660 6365 }
6366 + else
6367 + {
6368 + wp_send_json_error('No valid recipient email addresses');
6369 + }
3661 6370
3662 - die();
6371 + wp_die();
3663 6372 }
3664 6373
3665 - public function viewing_email_owner_booking_confirmation()
6374 + public function viewing_email_owner_cancellation_notification()
3666 6375 {
3667 6376 check_ajax_referer( 'viewing-actions', 'security' );
3668 6377
3669 - $post_id = (int)$_POST['viewing_id'];
6378 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3670 6379
3671 6380 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
3672 6381 $property_department = get_post_meta( $property_id, '_department' );
3673 6382
3674 - $applicant_contact_id = get_post_meta( $post_id, '_applicant_contact_id', TRUE );
6383 + $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
3675 6384 $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
3676 6385
3677 6386 if ( $owner_contact_ids > 0 ) {
3678 6387
@@ -3677,69 +6386,552 @@
3677 6386 if ( $owner_contact_ids > 0 ) {
3678 6387
3679 6388 $owner_emails = array();
3680 6389 $owner_names = array();
6390 + $owner_dears = array();
3681 6391
3682 6392 foreach ($owner_contact_ids as $owner_id)
3683 6393 {
3684 - $owner_email = sanitize_email( get_post_meta($owner_id, '_email_address', TRUE) );
3685 - $owner_name = get_the_title($owner_id);
6394 + $owner_contact = new PH_Contact($owner_id);
3686 6395
3687 - if( ! empty($owner_email) ) array_push($owner_emails, $owner_email);
6396 + $owner_name = $owner_contact->post_title;
6397 + $owner_dear = $owner_contact->dear();
6398 +
3688 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 + }
3689 6408 }
3690 6409
6410 + $owner_names_string = $this->get_list_string($owner_names);
6411 + $owner_dears_string = $this->get_list_string($owner_dears);
6412 +
6413 + if ( !empty($applicant_contact_ids) )
6414 + {
6415 + $applicant_names = array();
6416 + $applicant_dears = array();
6417 + foreach ($applicant_contact_ids as $applicant_contact_id)
6418 + {
6419 + $applicant_contact = new PH_Contact($applicant_contact_id);
6420 + $applicant_names[] = $applicant_contact->post_title;
6421 + $applicant_dears[] = $applicant_contact->dear();
6422 + }
6423 + $applicant_names = array_filter($applicant_names);
6424 + $applicant_dears = array_filter($applicant_dears);
6425 + }
6426 +
6427 + $applicant_names_string = $this->get_list_string($applicant_names);
6428 + $applicant_dears_string = $this->get_list_string($applicant_dears);
6429 +
6430 + $negotiator_names = array();
6431 + $negotiator_names_string = '';
6432 +
6433 + $negotiator_email_addresses = array();
6434 + $negotiator_email_addresses_string = '';
6435 +
6436 + $negotiator_telephone_numbers = array();
6437 + $negotiator_telephone_numbers_string = '';
6438 +
6439 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
6440 + if ( !empty($negotiator_ids) )
6441 + {
6442 + foreach ( $negotiator_ids as $negotiator_id )
6443 + {
6444 + $negotiator = get_user_by( 'id', $negotiator_id );
6445 + if ( $negotiator !== false )
6446 + {
6447 + if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
6448 + {
6449 + $negotiator_names[] = $negotiator->display_name;
6450 + }
6451 +
6452 + if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
6453 + {
6454 + $negotiator_email_addresses[] = $negotiator->user_email;
6455 + }
6456 +
6457 + $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
6458 + if ( !empty($telephone_number) )
6459 + {
6460 + $negotiator_telephone_numbers[] = $telephone_number;
6461 + }
6462 + }
6463 + }
6464 + }
6465 + if ( !empty($negotiator_names) )
6466 + {
6467 + $last = array_slice($negotiator_names, -1);
6468 + $first = join(', ', array_slice($negotiator_names, 0, -1));
6469 + $both = array_filter(array_merge(array($first), $last), 'strlen');
6470 + $negotiator_names_string = join(' and ', $both);
6471 + }
6472 + if ( !empty($negotiator_email_addresses) )
6473 + {
6474 + $last = array_slice($negotiator_email_addresses, -1);
6475 + $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
6476 + $both = array_filter(array_merge(array($first), $last), 'strlen');
6477 + $negotiator_email_addresses_string = join(' and ', $both);
6478 + }
6479 + if ( !empty($negotiator_telephone_numbers) )
6480 + {
6481 + $last = array_slice($negotiator_telephone_numbers, -1);
6482 + $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
6483 + $both = array_filter(array_merge(array($first), $last), 'strlen');
6484 + $negotiator_telephone_numbers_string = join(' and ', $both);
6485 + }
6486 +
3691 6487 $property = new PH_Property((int)$property_id);
3692 6488
3693 6489 $to = implode(",", $owner_emails);
3694 6490
3695 - $subject = get_option( 'propertyhive_viewing_owner_booking_confirmation_email_subject', '' );
3696 - $body = get_option( 'propertyhive_viewing_owner_booking_confirmation_email_body', '' );
6491 + $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_viewing_owner_cancellation_notification_email_subject', '' );
6492 + $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_viewing_owner_cancellation_notification_email_body', '' );
3697 6493
3698 6494 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
3699 - $subject = str_replace('[owner_name]', implode(", ", $owner_names), $subject);
3700 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
3701 - $subject = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6495 + $subject = str_replace('[owner_name]', $owner_names_string, $subject);
6496 + $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
6497 + $subject = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6498 + $subject = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6499 + $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
6500 + $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
6501 + $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
3702 6502
6503 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_owner_cancellation_notification_email_subject; third-party email integrations depend on the established name.
6504 + $subject = apply_filters( 'viewing_owner_cancellation_notification_email_subject', $subject, $post_id, $property_id );
6505 +
3703 6506 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
3704 - $body = str_replace('[owner_name]', implode(", ", $owner_names), $body);
3705 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
3706 - $body = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6507 + $body = str_replace('[owner_name]', $owner_names_string, $body);
6508 + $body = str_replace('[owner_dear]', $owner_dears_string, $body);
6509 + $body = str_replace('[applicant_name]', $applicant_names_string, $body);
6510 + $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
6511 + $body = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6512 + $body = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6513 + $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
6514 + $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
6515 + $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
3707 6516
3708 - $from = $property->office_email_address;
3709 - if ( sanitize_email($from) == '' )
6517 + $cancelled_reason = '';
6518 + if (
6519 + get_post_meta( $post_id, '_cancelled_reason_public', true ) == 'yes' &&
6520 + get_post_meta( $post_id, '_cancelled_reason', true ) != ''
6521 + )
3710 6522 {
6523 + $cancelled_reason .= "\n\nReason: " . get_post_meta( $post_id, '_cancelled_reason', true );
6524 + }
6525 + $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
6526 +
6527 + $body = html_entity_decode($body);
6528 +
6529 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_owner_cancellation_notification_email_body; third-party email integrations depend on the established name.
6530 + $body = apply_filters( 'viewing_owner_cancellation_notification_email_body', $body, $post_id, $property_id );
6531 +
6532 + $from = '';
6533 + $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
6534 + if ( $from_setting == 'user' )
6535 + {
6536 + $current_user = wp_get_current_user();
6537 + $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
6538 +
6539 + if ( $from == '' )
6540 + {
6541 + $from = $property->office_email_address;
6542 + }
6543 + }
6544 + if ( $from_setting == 'office' )
6545 + {
6546 + $from = $property->office_email_address;
6547 + }
6548 + if ( $from == '' )
6549 + {
6550 + $from = get_option('propertyhive_email_from_address', '');
6551 + }
6552 + if ( $from == '' )
6553 + {
3711 6554 $from = get_bloginfo('admin_email');
3712 6555 }
3713 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 +
3714 6593 $headers = array();
3715 - $headers[] = 'From: ' . get_bloginfo('name') . ' <' . $from . '>';
6594 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
6595 + $headers[] = 'Reply-To: ' . sanitize_email($from);
3716 6596 $headers[] = 'Content-Type: text/plain; charset=UTF-8';
3717 6597
3718 - wp_mail($to, $subject, $body, $headers);
6598 + $headers = apply_filters( 'propertyhive_viewing_owner_cancellation_notification_email_headers', $headers );
3719 6599
3720 - update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', date("Y-m-d H:i:s") );
6600 + $sent = wp_mail($to, $subject, $body, $headers, $attachments);
3721 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();
3722 6626 }
6627 + else
6628 + {
6629 + wp_send_json_error('No owner recipients');
6630 + }
3723 6631
3724 - die();
6632 + wp_die();
3725 6633 }
3726 6634
6635 + public function viewing_email_attending_negotiator_cancellation_notification()
6636 + {
6637 + check_ajax_referer( 'viewing-actions', 'security' );
6638 +
6639 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6640 + $property_id = get_post_meta( $post_id, '_property_id', TRUE );
6641 +
6642 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
6643 +
6644 + $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
6645 + $owner_contact_ids = get_post_meta( $property_id, '_owner_contact_id', TRUE );
6646 +
6647 + if ( !empty($negotiator_ids) ) {
6648 +
6649 + $tos = array();
6650 + foreach ($negotiator_ids as $negotiator_id)
6651 + {
6652 + $user_info = get_userdata((int)$negotiator_id);
6653 + $tos[] = sanitize_email($user_info->user_email);
6654 + }
6655 + $to = implode(",", $tos);
6656 +
6657 + $owner_emails = array();
6658 + $owner_names = array();
6659 + $owner_dears = array();
6660 + $owner_details = array();
6661 +
6662 + if ( !empty($owner_contact_ids) )
6663 + {
6664 + foreach ($owner_contact_ids as $owner_id)
6665 + {
6666 + $owner_contact = new PH_Contact($owner_id);
6667 +
6668 + $owner_name = $owner_contact->post_title;
6669 + $owner_dear = $owner_contact->dear();
6670 +
6671 + if( ! empty($owner_name) ) array_push($owner_names, $owner_name);
6672 + if( ! empty($owner_dear) ) array_push($owner_dears, $owner_dear);
6673 +
6674 + $owner_email = $owner_contact->email_address;
6675 + $explode_owner_email = explode( ",", $owner_email );
6676 + foreach ( $explode_owner_email as $email_address )
6677 + {
6678 + $owner_emails[] = sanitize_email($email_address);
6679 + }
6680 +
6681 + $owner_details[] = $owner_contact->post_title . "\nT: " . $owner_contact->telephone_number . "\nE: " . $owner_contact->email_address;
6682 + }
6683 + }
6684 +
6685 + $owner_details = implode("\n\n", $owner_details);
6686 +
6687 + $owner_names_string = $this->get_list_string($owner_names);
6688 + $owner_dears_string = $this->get_list_string($owner_dears);
6689 +
6690 + $applicant_names = array();
6691 + $applicant_dears = array();
6692 + $applicant_details = array();
6693 +
6694 + if ( !empty($applicant_contact_ids) )
6695 + {
6696 + foreach ($applicant_contact_ids as $applicant_contact_id)
6697 + {
6698 + $applicant_contact = new PH_Contact($applicant_contact_id);
6699 + $applicant_names[] = $applicant_contact->post_title;
6700 + $applicant_dears[] = $applicant_contact->dear();
6701 +
6702 + $applicant_details[] = $applicant_contact->post_title . "\nT: " . $applicant_contact->telephone_number . "\nE: " . $applicant_contact->email_address;
6703 + }
6704 + }
6705 +
6706 + $applicant_details = implode("\n\n", $applicant_details);
6707 +
6708 + $applicant_names = array_filter($applicant_names);
6709 + $applicant_dears = array_filter($applicant_dears);
6710 +
6711 + $applicant_names_string = $this->get_list_string($applicant_names);
6712 + $applicant_dears_string = $this->get_list_string($applicant_dears);
6713 +
6714 + $negotiator_names = array();
6715 + $negotiator_names_string = '';
6716 +
6717 + $negotiator_email_addresses = array();
6718 + $negotiator_email_addresses_string = '';
6719 +
6720 + $negotiator_telephone_numbers = array();
6721 + $negotiator_telephone_numbers_string = '';
6722 +
6723 + $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
6724 + if ( !empty($negotiator_ids) )
6725 + {
6726 + foreach ( $negotiator_ids as $negotiator_id )
6727 + {
6728 + $negotiator = get_user_by( 'id', $negotiator_id );
6729 + if ( $negotiator !== false )
6730 + {
6731 + if ( isset($negotiator->display_name) && !empty($negotiator->display_name) )
6732 + {
6733 + $negotiator_names[] = $negotiator->display_name;
6734 + }
6735 +
6736 + if ( isset($negotiator->user_email) && !empty($negotiator->user_email) )
6737 + {
6738 + $negotiator_email_addresses[] = $negotiator->user_email;
6739 + }
6740 +
6741 + $telephone_number = get_user_meta( $negotiator_id, 'telephone_number', true );
6742 + if ( !empty($telephone_number) )
6743 + {
6744 + $negotiator_telephone_numbers[] = $telephone_number;
6745 + }
6746 + }
6747 + }
6748 + }
6749 + if ( !empty($negotiator_names) )
6750 + {
6751 + $last = array_slice($negotiator_names, -1);
6752 + $first = join(', ', array_slice($negotiator_names, 0, -1));
6753 + $both = array_filter(array_merge(array($first), $last), 'strlen');
6754 + $negotiator_names_string = join(' and ', $both);
6755 + }
6756 + if ( !empty($negotiator_email_addresses) )
6757 + {
6758 + $last = array_slice($negotiator_email_addresses, -1);
6759 + $first = join(', ', array_slice($negotiator_email_addresses, 0, -1));
6760 + $both = array_filter(array_merge(array($first), $last), 'strlen');
6761 + $negotiator_email_addresses_string = join(' and ', $both);
6762 + }
6763 + if ( !empty($negotiator_telephone_numbers) )
6764 + {
6765 + $last = array_slice($negotiator_telephone_numbers, -1);
6766 + $first = join(', ', array_slice($negotiator_telephone_numbers, 0, -1));
6767 + $both = array_filter(array_merge(array($first), $last), 'strlen');
6768 + $negotiator_telephone_numbers_string = join(' and ', $both);
6769 + }
6770 +
6771 + $property = new PH_Property((int)$property_id);
6772 +
6773 + $subject = isset( $_POST['subject'] ) && is_string( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : get_option( 'propertyhive_viewing_attending_negotiator_cancellation_notification_email_subject', '' );
6774 + $body = isset( $_POST['body'] ) && is_string( $_POST['body'] ) ? sanitize_textarea_field( wp_unslash( $_POST['body'] ) ) : get_option( 'propertyhive_viewing_attending_negotiator_cancellation_notification_email_body', '' );
6775 +
6776 + $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
6777 + $subject = str_replace('[owner_name]', $owner_names_string, $subject);
6778 + $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
6779 + $subject = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6780 + $subject = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6781 + $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
6782 + $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
6783 + $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
6784 +
6785 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_attending_negotiator_cancellation_notification_email_subject; third-party email integrations depend on the established name.
6786 + $subject = apply_filters( 'viewing_attending_negotiator_cancellation_notification_email_subject', $subject, $post_id, $property_id );
6787 +
6788 + $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
6789 + $body = str_replace('[owner_name]', $owner_names_string, $body);
6790 + $body = str_replace('[owner_dear]', $owner_dears_string, $body);
6791 + $body = str_replace('[owner_details]', $owner_details, $body);
6792 + $body = str_replace('[applicant_name]', $applicant_names_string, $body);
6793 + $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
6794 + $body = str_replace('[applicant_details]', $applicant_details, $body);
6795 + $body = str_replace('[viewing_time]', gmdate("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6796 + $body = str_replace('[viewing_date]', gmdate("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6797 + $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
6798 + $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
6799 + $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
6800 +
6801 + $cancelled_reason = '';
6802 + if (
6803 + get_post_meta( $post_id, '_cancelled_reason_public', true ) == 'yes' &&
6804 + get_post_meta( $post_id, '_cancelled_reason', true ) != ''
6805 + )
6806 + {
6807 + $cancelled_reason .= "\n\nReason: " . get_post_meta( $post_id, '_cancelled_reason', true );
6808 + }
6809 + $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
6810 +
6811 + $body = html_entity_decode($body);
6812 +
6813 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_attending_negotiator_cancellation_notification_email_body; third-party email integrations depend on the established name.
6814 + $body = apply_filters( 'viewing_attending_negotiator_cancellation_notification_email_body', $body, $post_id, $property_id );
6815 +
6816 + $from = '';
6817 + $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
6818 + if ( $from_setting == 'user' )
6819 + {
6820 + $current_user = wp_get_current_user();
6821 + $from = ( isset($current_user->user_email) ? $current_user->user_email : '' );
6822 +
6823 + if ( $from == '' )
6824 + {
6825 + $from = $property->office_email_address;
6826 + }
6827 + }
6828 + if ( $from_setting == 'office' )
6829 + {
6830 + $from = $property->office_email_address;
6831 + }
6832 + if ( $from == '' )
6833 + {
6834 + $from = get_option('propertyhive_email_from_address', '');
6835 + }
6836 + if ( $from == '' )
6837 + {
6838 + $from = get_bloginfo('admin_email');
6839 + }
6840 +
6841 + $attachments = array();
6842 + if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6843 + {
6844 + $uploaded_files = $this->get_viewing_email_uploads();
6845 +
6846 + // Handle each file upload
6847 + foreach ($uploaded_files['name'] as $key => $value)
6848 + {
6849 + if ($uploaded_files['name'][$key])
6850 + {
6851 + $file = array(
6852 + 'name' => $uploaded_files['name'][$key],
6853 + 'type' => $uploaded_files['type'][$key],
6854 + 'tmp_name' => $uploaded_files['tmp_name'][$key],
6855 + 'error' => $uploaded_files['error'][$key],
6856 + 'size' => $uploaded_files['size'][$key]
6857 + );
6858 +
6859 + // Move the file to a temporary location
6860 + $upload_overrides = array('test_form' => false);
6861 + $movefile = wp_handle_upload($file, $upload_overrides);
6862 +
6863 + if ($movefile && !isset($movefile['error']))
6864 + {
6865 + // Add the file path to attachments array
6866 + $attachments[] = $movefile['file'];
6867 + }
6868 + else
6869 + {
6870 + // Handle error in file upload
6871 + wp_send_json_error($movefile['error']);
6872 + }
6873 + }
6874 + }
6875 + }
6876 +
6877 + $headers = array();
6878 + $headers[] = 'From: ' . html_entity_decode(get_bloginfo('name')) . ' <' . sanitize_email($from) . '>';
6879 + $headers[] = 'Reply-To: ' . sanitize_email($from);
6880 + $headers[] = 'Content-Type: text/plain; charset=UTF-8';
6881 +
6882 + $headers = apply_filters( 'propertyhive_viewing_attending_negotiator_cancellation_notification_email_headers', $headers );
6883 +
6884 + $sent = wp_mail($to, $subject, $body, $headers, $attachments);
6885 +
6886 + foreach ($attachments as $temp_file)
6887 + {
6888 + @wp_delete_file($temp_file);
6889 + }
6890 +
6891 + if ( !$sent )
6892 + {
6893 + wp_send_json_error('Failed to send email');
6894 + }
6895 +
6896 + // Add note/comment to viewing
6897 + if ( apply_filters( 'propertyhive_log_cancellation_notification_emails', false ) === true )
6898 + {
6899 + $comment = array(
6900 + 'note_type' => 'action',
6901 + 'action' => 'viewing_attending_negotiator_cancellation_notification_email',
6902 + );
6903 +
6904 + PH_Comments::insert_note( $post_id, $comment );
6905 + }
6906 +
6907 + update_post_meta( $post_id, '_attending_negotiator_cancellation_notification_sent_at', gmdate("Y-m-d H:i:s") );
6908 +
6909 + wp_send_json_success();
6910 + }
6911 + else
6912 + {
6913 + wp_send_json_error('No attending negotiator recipients');
6914 + }
6915 +
6916 + wp_die();
6917 + }
6918 +
3727 6919 public function viewing_interested_feedback()
3728 6920 {
3729 6921 check_ajax_referer( 'viewing-actions', 'security' );
3730 6922
3731 - $post_id = (int)$_POST['viewing_id'];
6923 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3732 6924
6925 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6926 +
3733 6927 $status = get_post_meta( $post_id, '_status', TRUE );
3734 6928
3735 6929 if ( $status == 'carried_out' )
3736 6930 {
3737 6931 update_post_meta( $post_id, '_feedback_status', 'interested' );
3738 - update_post_meta( $post_id, '_feedback', sanitize_textarea_field( $_POST['feedback'] ) );
6932 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
3739 6933
3740 - $current_user = wp_get_current_user();
3741 -
3742 6934 // Add note/comment to viewing
3743 6935 $comment = array(
3744 6936 'note_type' => 'action',
3745 6937 'action' => 'viewing_applicant_interested',
@@ -3744,22 +6936,14 @@
3744 6936 'note_type' => 'action',
3745 6937 'action' => 'viewing_applicant_interested',
3746 6938 );
3747 6939
3748 - $data = array(
3749 - 'comment_post_ID' => $post_id,
3750 - 'comment_author' => $current_user->display_name,
3751 - 'comment_author_email' => '[email protected]',
3752 - 'comment_author_url' => '',
3753 - 'comment_date' => date("Y-m-d H:i:s"),
3754 - 'comment_content' => serialize($comment),
3755 - 'comment_approved' => 1,
3756 - 'comment_type' => 'propertyhive_note',
3757 - );
3758 - $comment_id = wp_insert_comment( $data );
6940 + PH_Comments::insert_note( $post_id, $comment );
6941 +
6942 + wp_send_json_success();
3759 6943 }
3760 6944
3761 - die();
6945 + wp_send_json_error();
3762 6946 }
3763 6947
3764 6948 public function viewing_not_interested_feedback()
3765 6949 {
@@ -3764,19 +6948,19 @@
3764 6948 public function viewing_not_interested_feedback()
3765 6949 {
3766 6950 check_ajax_referer( 'viewing-actions', 'security' );
3767 6951
3768 - $post_id = (int)$_POST['viewing_id'];
6952 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3769 6953
6954 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6955 +
3770 6956 $status = get_post_meta( $post_id, '_status', TRUE );
3771 6957
3772 6958 if ( $status == 'carried_out' )
3773 6959 {
3774 6960 update_post_meta( $post_id, '_feedback_status', 'not_interested' );
3775 - update_post_meta( $post_id, '_feedback', sanitize_textarea_field( $_POST['feedback'] ) );
6961 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
3776 6962
3777 - $current_user = wp_get_current_user();
3778 -
3779 6963 // Add note/comment to viewing
3780 6964 $comment = array(
3781 6965 'note_type' => 'action',
3782 6966 'action' => 'viewing_applicant_not_interested',
@@ -3781,22 +6965,14 @@
3781 6965 'note_type' => 'action',
3782 6966 'action' => 'viewing_applicant_not_interested',
3783 6967 );
3784 6968
3785 - $data = array(
3786 - 'comment_post_ID' => $post_id,
3787 - 'comment_author' => $current_user->display_name,
3788 - 'comment_author_email' => '[email protected]',
3789 - 'comment_author_url' => '',
3790 - 'comment_date' => date("Y-m-d H:i:s"),
3791 - 'comment_content' => serialize($comment),
3792 - 'comment_approved' => 1,
3793 - 'comment_type' => 'propertyhive_note',
3794 - );
3795 - $comment_id = wp_insert_comment( $data );
6969 + PH_Comments::insert_note( $post_id, $comment );
6970 +
6971 + wp_send_json_success();
3796 6972 }
3797 6973
3798 - die();
6974 + wp_send_json_error();
3799 6975 }
3800 6976
3801 6977 public function viewing_feedback_not_required()
3802 6978 {
@@ -3801,9 +6977,9 @@
3801 6977 public function viewing_feedback_not_required()
3802 6978 {
3803 6979 check_ajax_referer( 'viewing-actions', 'security' );
3804 6980
3805 - $post_id = (int)$_POST['viewing_id'];
6981 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3806 6982
3807 6983 $status = get_post_meta( $post_id, '_status', TRUE );
3808 6984
3809 6985 if ( $status == 'carried_out' )
@@ -3809,10 +6985,8 @@
3809 6985 if ( $status == 'carried_out' )
3810 6986 {
3811 6987 update_post_meta( $post_id, '_feedback_status', 'not_required' );
3812 6988
3813 - $current_user = wp_get_current_user();
3814 -
3815 6989 // Add note/comment to viewing
3816 6990 $comment = array(
3817 6991 'note_type' => 'action',
3818 6992 'action' => 'viewing_feedback_not_required',
@@ -3817,22 +6991,14 @@
3817 6991 'note_type' => 'action',
3818 6992 'action' => 'viewing_feedback_not_required',
3819 6993 );
3820 6994
3821 - $data = array(
3822 - 'comment_post_ID' => $post_id,
3823 - 'comment_author' => $current_user->display_name,
3824 - 'comment_author_email' => '[email protected]',
3825 - 'comment_author_url' => '',
3826 - 'comment_date' => date("Y-m-d H:i:s"),
3827 - 'comment_content' => serialize($comment),
3828 - 'comment_approved' => 1,
3829 - 'comment_type' => 'propertyhive_note',
3830 - );
3831 - $comment_id = wp_insert_comment( $data );
6995 + PH_Comments::insert_note( $post_id, $comment );
6996 +
6997 + wp_send_json_success();
3832 6998 }
3833 6999
3834 - die();
7000 + wp_send_json_error();
3835 7001 }
3836 7002
3837 7003 public function viewing_revert_feedback_pending()
3838 7004 {
@@ -3837,9 +7003,9 @@
3837 7003 public function viewing_revert_feedback_pending()
3838 7004 {
3839 7005 check_ajax_referer( 'viewing-actions', 'security' );
3840 7006
3841 - $post_id = (int)$_POST['viewing_id'];
7007 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3842 7008
3843 7009 $status = get_post_meta( $post_id, '_status', TRUE );
3844 7010
3845 7011 if ( $status == 'carried_out' )
@@ -3845,11 +7011,10 @@
3845 7011 if ( $status == 'carried_out' )
3846 7012 {
3847 7013 update_post_meta( $post_id, '_feedback_status', '' );
3848 7014 update_post_meta( $post_id, '_feedback_passed_on', '' );
7015 + delete_post_meta( $post_id, '_feedback_received_date' );
3849 7016
3850 - $current_user = wp_get_current_user();
3851 -
3852 7017 // Add note/comment to viewing
3853 7018 $comment = array(
3854 7019 'note_type' => 'action',
3855 7020 'action' => 'viewing_revert_feedback_pending',
@@ -3854,22 +7019,14 @@
3854 7019 'note_type' => 'action',
3855 7020 'action' => 'viewing_revert_feedback_pending',
3856 7021 );
3857 7022
3858 - $data = array(
3859 - 'comment_post_ID' => $post_id,
3860 - 'comment_author' => $current_user->display_name,
3861 - 'comment_author_email' => '[email protected]',
3862 - 'comment_author_url' => '',
3863 - 'comment_date' => date("Y-m-d H:i:s"),
3864 - 'comment_content' => serialize($comment),
3865 - 'comment_approved' => 1,
3866 - 'comment_type' => 'propertyhive_note',
3867 - );
3868 - $comment_id = wp_insert_comment( $data );
7023 + PH_Comments::insert_note( $post_id, $comment );
7024 +
7025 + wp_send_json_success();
3869 7026 }
3870 7027
3871 - die();
7028 + wp_send_json_error();
3872 7029 }
3873 7030
3874 7031 public function viewing_revert_pending()
3875 7032 {
@@ -3874,19 +7031,18 @@
3874 7031 public function viewing_revert_pending()
3875 7032 {
3876 7033 check_ajax_referer( 'viewing-actions', 'security' );
3877 7034
3878 - $post_id = (int)$_POST['viewing_id'];
7035 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3879 7036
3880 7037 $status = get_post_meta( $post_id, '_status', TRUE );
3881 7038
3882 - if ( $status == 'carried_out' || $status == 'cancelled' )
7039 + if ( in_array( $status, array('carried_out', 'cancelled', 'no_show') ) )
3883 7040 {
3884 7041 update_post_meta( $post_id, '_status', 'pending' );
3885 7042 update_post_meta( $post_id, '_feedback_status', '' );
7043 + delete_post_meta( $post_id, '_feedback_received_date' );
3886 7044
3887 - $current_user = wp_get_current_user();
3888 -
3889 7045 // Add note/comment to viewing
3890 7046 $comment = array(
3891 7047 'note_type' => 'action',
3892 7048 'action' => 'viewing_revert_pending',
@@ -3891,22 +7047,14 @@
3891 7047 'note_type' => 'action',
3892 7048 'action' => 'viewing_revert_pending',
3893 7049 );
3894 7050
3895 - $data = array(
3896 - 'comment_post_ID' => $post_id,
3897 - 'comment_author' => $current_user->display_name,
3898 - 'comment_author_email' => '[email protected]',
3899 - 'comment_author_url' => '',
3900 - 'comment_date' => date("Y-m-d H:i:s"),
3901 - 'comment_content' => serialize($comment),
3902 - 'comment_approved' => 1,
3903 - 'comment_type' => 'propertyhive_note',
3904 - );
3905 - $comment_id = wp_insert_comment( $data );
7051 + PH_Comments::insert_note( $post_id, $comment );
7052 +
7053 + wp_send_json_success();
3906 7054 }
3907 7055
3908 - die();
7056 + wp_send_json_error();
3909 7057 }
3910 7058
3911 7059 public function viewing_feedback_passed_on()
3912 7060 {
@@ -3911,9 +7059,9 @@
3911 7059 public function viewing_feedback_passed_on()
3912 7060 {
3913 7061 check_ajax_referer( 'viewing-actions', 'security' );
3914 7062
3915 - $post_id = (int)$_POST['viewing_id'];
7063 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
3916 7064
3917 7065 $status = get_post_meta( $post_id, '_status', TRUE );
3918 7066
3919 7067 if ( $status == 'carried_out' )
@@ -3919,10 +7067,8 @@
3919 7067 if ( $status == 'carried_out' )
3920 7068 {
3921 7069 update_post_meta( $post_id, '_feedback_passed_on', 'yes' );
3922 7070
3923 - $current_user = wp_get_current_user();
3924 -
3925 7071 // Add note/comment to viewing
3926 7072 $comment = array(
3927 7073 'note_type' => 'action',
3928 7074 'action' => 'viewing_feedback_passed_on',
@@ -3927,310 +7073,53 @@
3927 7073 'note_type' => 'action',
3928 7074 'action' => 'viewing_feedback_passed_on',
3929 7075 );
3930 7076
3931 - $data = array(
3932 - 'comment_post_ID' => $post_id,
3933 - 'comment_author' => $current_user->display_name,
3934 - 'comment_author_email' => '[email protected]',
3935 - 'comment_author_url' => '',
3936 - 'comment_date' => date("Y-m-d H:i:s"),
3937 - 'comment_content' => serialize($comment),
3938 - 'comment_approved' => 1,
3939 - 'comment_type' => 'propertyhive_note',
3940 - );
3941 - $comment_id = wp_insert_comment( $data );
7077 + PH_Comments::insert_note( $post_id, $comment );
7078 +
7079 + wp_send_json_success();
3942 7080 }
3943 7081
3944 - die();
7082 + wp_send_json_error();
3945 7083 }
3946 7084
3947 7085 public function get_property_viewings_meta_box()
3948 7086 {
3949 - check_ajax_referer( 'get_property_viewings_meta_box', 'security' );
7087 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
3950 7088
3951 - 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 + }
3952 7096
3953 - echo '<div class="propertyhive_meta_box">';
3954 -
3955 - echo '<div class="options_group">';
7097 + include( PH()->plugin_path() . '/includes/admin/views/html-property-viewings-meta-box.php' );
3956 7098
3957 - $args = array(
3958 - 'post_type' => 'viewing',
3959 - 'nopaging' => true,
3960 - 'orderby' => 'meta_value',
3961 - 'order' => 'DESC',
3962 - 'meta_key' => '_start_date_time',
3963 - 'post_status' => 'publish',
3964 - 'meta_query' => array(
3965 - array(
3966 - 'key' => '_property_id',
3967 - 'value' => (int)$_POST['post_id']
3968 - )
3969 - )
3970 - );
3971 - $viewings_query = new WP_Query( $args );
3972 -
3973 - if ( $viewings_query->have_posts() )
3974 - {
3975 - echo '<table style="width:100%">
3976 - <thead>
3977 - <tr>
3978 - <th style="text-align:left;">' . __( 'Date', 'propertyhive' ) . ' / ' . __( 'Time', 'propertyhive' ) . '</th>
3979 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
3980 - <th style="text-align:left;">' . __( 'Attending Negotiator(s)', 'propertyhive' ) . '</th>
3981 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
3982 - </tr>
3983 - </thead>
3984 - <tbody>';
3985 -
3986 - while ( $viewings_query->have_posts() )
3987 - {
3988 - $viewings_query->the_post();
3989 -
3990 - echo '<tr>';
3991 - 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>';
3992 - echo '<td style="text-align:left;">';
3993 - if ( get_post_meta(get_the_ID(), '_applicant_contact_id', TRUE) != '' )
3994 - {
3995 - echo '<a href="' . get_edit_post_link( get_post_meta(get_the_ID(), '_applicant_contact_id', TRUE), '' ) . '">' . get_the_title(get_post_meta(get_the_ID(), '_applicant_contact_id', TRUE)) . '</a>';
3996 - }
3997 - else
3998 - {
3999 - echo '-';
4000 - }
4001 - echo '</td>';
4002 - echo '<td style="text-align:left;">';
4003 -
4004 - $negotiator_ids = get_post_meta(get_the_ID(), '_negotiator_id');
4005 -
4006 - if (!empty($negotiator_ids))
4007 - {
4008 - $i = 0;
4009 - foreach ($negotiator_ids as $negotiator_id)
4010 - {
4011 - if ( $i > 0 ) { echo ', '; }
4012 -
4013 - $userdata = get_userdata( $negotiator_id );
4014 - if ( $userdata !== FALSE )
4015 - {
4016 - echo $userdata->display_name;
4017 - }
4018 - else
4019 - {
4020 - echo '<em>Unknown user</em>';
4021 - }
4022 - ++$i;
4023 - }
4024 - }
4025 - else
4026 - {
4027 - echo 'Unattended';
4028 - }
4029 -
4030 - echo '</td>';
4031 - echo '<td style="text-align:left;">';
4032 -
4033 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
4034 - echo ucwords(str_replace("_", " ", $status));
4035 - if ( $status == 'pending' )
4036 - {
4037 - echo '<br>';
4038 - // confirmation status
4039 - if ( get_post_meta(get_the_ID(), '_all_confirmed', TRUE) == 'yes' )
4040 - {
4041 - echo __( 'All Parties Confirmed', 'propertyhive' );
4042 - }
4043 - else
4044 - {
4045 - echo __( 'Awaiting Confirmation', 'propertyhive' );
4046 - }
4047 - }
4048 - if ( $status == 'carried_out' )
4049 - {
4050 - echo '<br>';
4051 - $feedback_status = get_post_meta(get_the_ID(), '_feedback_status', TRUE);
4052 - switch ( $feedback_status )
4053 - {
4054 - case "interested": { echo 'Applicant Interested'; break; }
4055 - case "not_interested": { echo 'Applicant Not Interested'; break; }
4056 - case "not_required": { echo 'Feedback Not Required'; break; }
4057 - default: { echo 'Awaiting Feedback'; }
4058 - }
4059 -
4060 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' )
4061 - {
4062 - $feedback_passed_on = get_post_meta(get_the_ID(), '_feedback_passed_on', TRUE);
4063 - echo '<br>' . ( ($feedback_passed_on == 'yes') ? 'Feedback Passed On' : 'Feedback Not Passed On' );
4064 - }
4065 - }
4066 - echo '</td>';
4067 - echo '</tr>';
4068 - }
4069 -
4070 - echo '
4071 - </tbody>
4072 - </table>
4073 - <br>';
4074 - }
4075 - else
4076 - {
4077 - echo '<p>' . __( 'No viewings exist for this property', 'propertyhive') . '</p>';
4078 - }
4079 - wp_reset_postdata();
4080 -
4081 7099 do_action('propertyhive_property_viewings_fields');
4082 -
4083 - echo '</div>';
4084 -
4085 - echo '</div>';
4086 7100
7101 + // Quit out
4087 7102 die();
4088 7103 }
4089 7104
4090 7105 public function get_contact_viewings_meta_box()
4091 7106 {
4092 - check_ajax_referer( 'get_contact_viewings_meta_box', 'security' );
7107 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
4093 7108
4094 - 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 + }
4095 7116
4096 - echo '<div class="propertyhive_meta_box">';
4097 -
4098 - echo '<div class="options_group">';
7117 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-viewings-meta-box.php' );
4099 7118
4100 - $args = array(
4101 - 'post_type' => 'viewing',
4102 - 'nopaging' => true,
4103 - 'orderby' => 'meta_value',
4104 - 'order' => 'DESC',
4105 - 'post_status' => 'publish',
4106 - 'meta_key' => '_start_date_time',
4107 - 'meta_query' => array(
4108 - array(
4109 - 'key' => '_applicant_contact_id',
4110 - 'value' => (int)$_POST['post_id']
4111 - )
4112 - )
4113 - );
4114 - $viewings_query = new WP_Query( $args );
4115 -
4116 - if ( $viewings_query->have_posts() )
4117 - {
4118 - echo '<table style="width:100%">
4119 - <thead>
4120 - <tr>
4121 - <th style="text-align:left;">' . __( 'Date', 'propertyhive' ) . ' / ' . __( 'Time', 'propertyhive' ) . '</th>
4122 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
4123 - <th style="text-align:left;">' . __( 'Attending Negotiator(s)', 'propertyhive' ) . '</th>
4124 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
4125 - </tr>
4126 - </thead>
4127 - <tbody>';
4128 -
4129 - while ( $viewings_query->have_posts() )
4130 - {
4131 - $viewings_query->the_post();
4132 -
4133 - $property = new PH_Property((int)get_post_meta(get_the_ID(), '_property_id', TRUE));
4134 -
4135 - echo '<tr>';
4136 - 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>';
4137 - echo '<td style="text-align:left;">';
4138 - if ( get_post_meta(get_the_ID(), '_property_id', TRUE) != '' )
4139 - {
4140 - echo '<a href="' . get_edit_post_link( get_post_meta(get_the_ID(), '_property_id', TRUE), '' ) . '">' . $property->get_formatted_full_address() . '</a>';
4141 - }
4142 - else
4143 - {
4144 - echo '-';
4145 - }
4146 - echo '</td>';
4147 -
4148 - echo '<td style="text-align:left;">';
4149 -
4150 - $negotiator_ids = get_post_meta(get_the_ID(), '_negotiator_id');
4151 -
4152 - if (!empty($negotiator_ids))
4153 - {
4154 - $i = 0;
4155 - foreach ($negotiator_ids as $negotiator_id)
4156 - {
4157 - if ( $i > 0 ) { echo ', '; }
4158 -
4159 - $userdata = get_userdata( $negotiator_id );
4160 - if ( $userdata !== FALSE )
4161 - {
4162 - echo $userdata->display_name;
4163 - }
4164 - else
4165 - {
4166 - echo '<em>Unknown user</em>';
4167 - }
4168 - ++$i;
4169 - }
4170 - }
4171 - else
4172 - {
4173 - echo 'Unattended';
4174 - }
4175 -
4176 - echo '</td>';
4177 - echo '<td style="text-align:left;">';
4178 -
4179 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
4180 - echo ucwords(str_replace("_", " ", $status));
4181 - if ( $status == 'pending' )
4182 - {
4183 - echo '<br>';
4184 - // confirmation status
4185 - if ( get_post_meta(get_the_ID(), '_all_confirmed', TRUE) == 'yes' )
4186 - {
4187 - echo __( 'All Parties Confirmed', 'propertyhive' );
4188 - }
4189 - else
4190 - {
4191 - echo __( 'Awaiting Confirmation', 'propertyhive' );
4192 - }
4193 - }
4194 - if ( $status == 'carried_out' )
4195 - {
4196 - echo '<br>';
4197 - $feedback_status = get_post_meta(get_the_ID(), '_feedback_status', TRUE);
4198 - switch ( get_post_meta(get_the_ID(), '_feedback_status', TRUE) )
4199 - {
4200 - case "interested": { echo 'Applicant Interested'; break; }
4201 - case "not_interested": { echo 'Applicant Not Interested'; break; }
4202 - case "not_required": { echo 'Feedback Not Required'; break; }
4203 - default: { echo 'Awaiting Feedback'; }
4204 - }
4205 -
4206 - if ( $feedback_status == 'interested' || $feedback_status == 'not_interested' )
4207 - {
4208 - $feedback_passed_on = get_post_meta(get_the_ID(), '_feedback_passed_on', TRUE);
4209 - echo '<br>' . ( ($feedback_passed_on == 'yes') ? 'Feedback Passed On' : 'Feedback Not Passed On' );
4210 - }
4211 - }
4212 - echo '</td>';
4213 - echo '</tr>';
4214 - }
4215 -
4216 - echo '
4217 - </tbody>
4218 - </table>
4219 - <br>';
4220 - }
4221 - else
4222 - {
4223 - echo '<p>' . __( 'No viewings exist for this contact', 'propertyhive') . '</p>';
4224 - }
4225 - wp_reset_postdata();
4226 -
4227 7119 do_action('propertyhive_contact_viewings_fields');
4228 -
4229 - echo '</div>';
4230 -
4231 - echo '</div>';
4232 7120
7121 + // Quit out
4233 7122 die();
4234 7123 }
4235 7124
4236 7125 // Offer related functions
@@ -4239,10 +7128,19 @@
4239 7128 check_ajax_referer( 'record-offer', 'security' );
4240 7129
4241 7130 $this->json_headers();
4242 7131
4243 - // TO DO: Should do validation on server side also
4244 - 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)
4245 7143 {
4246 7144 $return = array('error' => 'No property selected');
4247 7145 echo json_encode( $return );
4248 7146 die();
@@ -4247,18 +7145,18 @@
4247 7145 echo json_encode( $return );
4248 7146 die();
4249 7147 }
4250 7148
4251 - $property = new PH_Property((int)$_POST['property_id']);
7149 + $property = new PH_Property($property_id);
4252 7150
4253 7151 $applicant_contact_ids = array();
4254 7152
4255 7153 // Create applicant record if required
4256 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
7154 + if (empty($input['applicant_ids']) && !empty($input['applicant_name']))
4257 7155 {
4258 7156 // Need to create contact/applicant
4259 7157 $contact_post = array(
4260 - 'post_title' => ph_clean($_POST['applicant_name']),
7158 + 'post_title' => $input['applicant_name'],
4261 7159 'post_content' => '',
4262 7160 'post_type' => 'contact',
4263 7161 'post_status' => 'publish',
4264 7162 'comment_status' => 'closed',
@@ -4265,9 +7163,9 @@
4265 7163 'ping_status' => 'closed',
4266 7164 );
4267 7165
4268 7166 // Insert the post into the database
4269 - $contact_post_id = wp_insert_post( $contact_post );
7167 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
4270 7168
4271 7169 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
4272 7170 {
4273 7171 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -4276,8 +7174,27 @@
4276 7174 }
4277 7175
4278 7176 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
4279 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 +
4280 7197 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
4281 7198 update_post_meta( $contact_post_id, '_applicant_profile_0', array( 'department' => $property->department, 'send_matching_properties' => '' ) );
4282 7199
4283 7200 $applicant_contact_ids[] = $contact_post_id;
@@ -4282,18 +7199,13 @@
4282 7199
4283 7200 $applicant_contact_ids[] = $contact_post_id;
4284 7201 }
4285 7202
4286 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
7203 + if (!empty($input['applicant_ids']) && empty($input['applicant_name']))
4287 7204 {
4288 7205 // This is an existing contact
4289 - if ( !is_array($_POST['applicant_ids']) )
7206 + foreach ( $input['applicant_ids'] as $applicant_id )
4290 7207 {
4291 - $_POST['applicant_ids'] = array($_POST['applicant_ids']);
4292 - }
4293 -
4294 - foreach ( $_POST['applicant_ids'] as $applicant_id )
4295 - {
4296 7208 $applicant_contact_ids[] = (int)$applicant_id;
4297 7209 }
4298 7210 }
4299 7211
@@ -4329,15 +7241,35 @@
4329 7241 echo json_encode( $return );
4330 7242 die();
4331 7243 }
4332 7244
4333 - $amount = preg_replace("/[^0-9]/", '', $_POST['amount']);
7245 + $amount = $input['amount'];
4334 7246
4335 - add_post_meta( $offer_post_id, '_offer_date_time', ph_clean($_POST['offer_date']) . ' ' . ph_clean($_POST['offer_time']) );
4336 - add_post_meta( $offer_post_id, '_property_id', (int)$_POST['property_id'] );
7247 + add_post_meta( $offer_post_id, '_offer_date_time', $input['offer_date'] . ' ' . $input['offer_time'] );
7248 + add_post_meta( $offer_post_id, '_property_id', $property_id );
4337 7249 add_post_meta( $offer_post_id, '_applicant_contact_id', $applicant_contact_id );
4338 7250 add_post_meta( $offer_post_id, '_amount', $amount );
4339 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 + }
4340 7272 }
4341 7273
4342 7274 $applicant_contacts = array();
4343 7275 foreach ( $applicant_contact_ids as $applicant_contact_id )
@@ -4367,10 +7299,16 @@
4367 7299 check_ajax_referer( 'record-offer', 'security' );
4368 7300
4369 7301 $this->json_headers();
4370 7302
4371 - // TO DO: Should do validation on server side also
4372 - 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)
4373 7311 {
4374 7312 $return = array('error' => 'No contact selected');
4375 7313 echo json_encode( $return );
4376 7314 die();
@@ -4375,9 +7313,9 @@
4375 7313 echo json_encode( $return );
4376 7314 die();
4377 7315 }
4378 7316
4379 - if (empty($_POST['property_ids']))
7317 + if (empty($input['property_ids']))
4380 7318 {
4381 7319 $return = array('error' => 'No property selected');
4382 7320 echo json_encode( $return );
4383 7321 die();
@@ -4384,9 +7322,9 @@
4384 7322 }
4385 7323
4386 7324 // Loop through contacts and create one offer each
4387 7325 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
4388 - foreach ( $_POST['property_ids'] as $property_id )
7326 + foreach ( $input['property_ids'] as $property_id )
4389 7327 {
4390 7328 // Insert offer record
4391 7329 $offer_post = array(
4392 7330 'post_title' => '',
@@ -4406,19 +7344,39 @@
4406 7344 echo json_encode( $return );
4407 7345 die();
4408 7346 }
4409 7347
4410 - $amount = preg_replace("/[^0-9]/", '', ph_clean($_POST['amount']));
7348 + $amount = $input['amount'];
4411 7349
4412 - add_post_meta( $offer_post_id, '_offer_date_time', ph_clean($_POST['offer_date']) . ' ' . ph_clean($_POST['offer_time']) );
7350 + add_post_meta( $offer_post_id, '_offer_date_time', $input['offer_date'] . ' ' . $input['offer_time'] );
4413 7351 add_post_meta( $offer_post_id, '_property_id', (int)$property_id );
4414 - add_post_meta( $offer_post_id, '_applicant_contact_id', (int)$_POST['contact_id'] );
7352 + add_post_meta( $offer_post_id, '_applicant_contact_id', $contact_id );
4415 7353 add_post_meta( $offer_post_id, '_amount', $amount );
4416 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 + }
4417 7375 }
4418 7376
4419 7377 $properties = array();
4420 - foreach ( $_POST['property_ids'] as $property_id )
7378 + foreach ( $input['property_ids'] as $property_id )
4421 7379 {
4422 7380 $properties[] = array(
4423 7381 'ID' => (int)$property_id,
4424 7382 'post_title' => get_the_title((int)$property_id),
@@ -4444,12 +7402,14 @@
4444 7402 global $post;
4445 7403
4446 7404 check_ajax_referer( 'offer-details-meta-box', 'security' );
4447 7405
4448 - $post = get_post((int)$_POST['offer_id']);
7406 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4449 7407
4450 - $offer = new PH_Offer((int)$_POST['offer_id']);
7408 + $post = get_post( $post_id );
4451 7409
7410 + $offer = new PH_Offer( $post_id );
7411 +
4452 7412 echo '<div class="propertyhive_meta_box">';
4453 7413
4454 7414 echo '<div class="options_group">';
4455 7415
@@ -4456,11 +7416,11 @@
4456 7416 if ( $offer->status != '' )
4457 7417 {
4458 7418 echo '<p class="form-field">
4459 7419
4460 - <label for="">' . __('Status', 'propertyhive') . '</label>
7420 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
4461 7421
4462 - ' . ucwords(str_replace("_", " ", $offer->status)) . '
7422 + ' . esc_html(propertyhive_get_status_label( $offer->status )) . '
4463 7423
4464 7424 </p>';
4465 7425 }
4466 7426
@@ -4466,32 +7426,32 @@
4466 7426
4467 7427 $offer_date_time = $offer->offer_date_time;
4468 7428 if ( empty($offer_date_time) )
4469 7429 {
4470 - $offer_date_time = date("Y-m-d H:i:s");
7430 + $offer_date_time = gmdate("Y-m-d H:i:s");
4471 7431 }
4472 7432
4473 7433 echo '<p class="form-field offer_date_time_field">
4474 7434
4475 - <label for="_offer_date">' . __('Offer Date / Time', 'propertyhive') . '</label>
7435 + <label for="_offer_date">' . esc_html(__('Offer Date / Time', 'propertyhive')) . '</label>
4476 7436
4477 - <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="">
4478 7438 <select id="_offer_time_hours" name="_offer_time_hours" class="select short" style="width:55px">';
4479 7439
4480 7440 if ( empty($offer_date_time) )
4481 7441 {
4482 - $value = date("H");
7442 + $value = gmdate("H");
4483 7443 }
4484 7444 else
4485 7445 {
4486 - $value = date( "H", strtotime( $offer_date_time ) );
7446 + $value = gmdate( "H", strtotime( $offer_date_time ) );
4487 7447 }
4488 7448 for ( $i = 0; $i < 23; ++$i )
4489 7449 {
4490 7450 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
4491 - echo '<option value="' . $j . '"';
7451 + echo '<option value="' . esc_attr($j) . '"';
4492 7452 if ($i == $value) { echo ' selected'; }
4493 - echo '>' . $j . '</option>';
7453 + echo '>' . esc_html($j) . '</option>';
4494 7454 }
4495 7455
4496 7456 echo '</select>
4497 7457 :
@@ -4502,16 +7462,16 @@
4502 7462 $value = '';
4503 7463 }
4504 7464 else
4505 7465 {
4506 - $value = date( "i", strtotime( $offer_date_time ) );
7466 + $value = gmdate( "i", strtotime( $offer_date_time ) );
4507 7467 }
4508 7468 for ( $i = 0; $i < 60; $i+=5 )
4509 7469 {
4510 7470 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
4511 - echo '<option value="' . $j . '"';
7471 + echo '<option value="' . esc_attr($j) . '"';
4512 7472 if ($i == $value) { echo ' selected'; }
4513 - echo '>' . $j . '</option>';
7473 + echo '>' . esc_html($j) . '</option>';
4514 7474 }
4515 7475
4516 7476 echo '</select>
4517 7477
@@ -4521,9 +7481,9 @@
4521 7481 'id' => '_amount',
4522 7482 'label' => __( 'Offer Amount', 'propertyhive' ) . ' (&pound;)',
4523 7483 'desc_tip' => false,
4524 7484 'class' => 'short',
4525 - 'value' => ( is_numeric($offer->amount) ? number_format($offer->amount) : '' ),
7485 + 'value' => ( is_numeric($offer->amount) ? ph_display_price_field( $offer->amount ) : '' ),
4526 7486 'custom_attributes' => array(
4527 7487 //'style' => 'width:95%; max-width:500px;'
4528 7488 )
4529 7489 );
@@ -4541,12 +7501,28 @@
4541 7501 public function get_offer_actions()
4542 7502 {
4543 7503 check_ajax_referer( 'offer-actions', 'security' );
4544 7504
4545 - $post_id = (int)$_POST['offer_id'];
7505 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4546 7506
4547 7507 $status = get_post_meta( $post_id, '_status', TRUE );
4548 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 +
4549 7525 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_offer_actions_meta_box">
4550 7526
4551 7527 <div class="options_group" style="padding-top:8px;">';
4552 7528
@@ -4563,8 +7539,13 @@
4563 7539 href="#action_panel_offer_declined"
4564 7540 class="button button-danger offer-action"
4565 7541 style="width:100%; margin-bottom:7px; text-align:center"
4566 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>';
4567 7548 }
4568 7549
4569 7550 if ( $status == 'accepted' )
4570 7551 {
@@ -4577,9 +7558,9 @@
4577 7558
4578 7559 if ( $sale_id != '' )
4579 7560 {
4580 7561 $actions[] = '<a
4581 - href="' . get_edit_post_link( $sale_id, '' ) . '"
7562 + href="' . esc_url(get_edit_post_link( $sale_id, '' )) . '"
4582 7563 class="button"
4583 7564 style="width:100%; margin-bottom:7px; text-align:center"
4584 7565 >' . wp_kses_post( __('View Sale', 'propertyhive') ) . '</a>';
4585 7566 }
@@ -4585,22 +7566,23 @@
4585 7566 }
4586 7567 else
4587 7568 {
4588 7569 $actions[] = '<a
4589 - href="' . wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), '1', 'create_sale' ) . '"
4590 - 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"
4591 7572 style="width:100%; margin-bottom:7px; text-align:center"
7573 + onclick="setTimeout(function() { jQuery(\'.button-create-sale\').attr(\'href\', \'#\'); jQuery(\'.button-create-sale\').attr(\'disabled\', \'disabled\'); jQuery(\'.button-create-sale\').html(\'Creating...\'); }, 50);"
4592 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>';
4593 7580 }
4594 7581 }
4595 7582
4596 - if ( $status == 'declined' )
7583 + if ( $status == 'accepted' || $status == 'declined' || $status == 'withdrawn' )
4597 7584 {
4598 -
4599 - }
4600 -
4601 - if ( $status == 'accepted' || $status == 'declined' )
4602 - {
4603 7585 $actions[] = '<a
4604 7586 href="#action_panel_offer_revert_pending"
4605 7587 class="button offer-action"
4606 7588 style="width:100%; margin-bottom:7px; text-align:center"
@@ -4607,16 +7589,18 @@
4607 7589 >' . wp_kses_post( __('Revert To Pending', 'propertyhive') ) . '</a>';
4608 7590 }
4609 7591
4610 7592 $actions = apply_filters( 'propertyhive_admin_offer_actions', $actions, $post_id );
7593 + $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
4611 7594
4612 7595 if ( !empty($actions) )
4613 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.
4614 7598 echo implode("", $actions);
4615 7599 }
4616 7600 else
4617 7601 {
4618 - 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>';
4619 7603 }
4620 7604
4621 7605 echo '</div>
4622 7606
@@ -4628,9 +7612,9 @@
4628 7612 public function offer_accepted()
4629 7613 {
4630 7614 check_ajax_referer( 'offer-actions', 'security' );
4631 7615
4632 - $post_id = (int)$_POST['offer_id'];
7616 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4633 7617
4634 7618 $status = get_post_meta( $post_id, '_status', TRUE );
4635 7619
4636 7620 if ( $status == 'pending' )
@@ -4636,10 +7620,8 @@
4636 7620 if ( $status == 'pending' )
4637 7621 {
4638 7622 update_post_meta( $post_id, '_status', 'accepted' );
4639 7623
4640 - $current_user = wp_get_current_user();
4641 -
4642 7624 // Add note/comment to offer
4643 7625 $comment = array(
4644 7626 'note_type' => 'action',
4645 7627 'action' => 'offer_accepted',
@@ -4644,22 +7626,14 @@
4644 7626 'note_type' => 'action',
4645 7627 'action' => 'offer_accepted',
4646 7628 );
4647 7629
4648 - $data = array(
4649 - 'comment_post_ID' => $post_id,
4650 - 'comment_author' => $current_user->display_name,
4651 - 'comment_author_email' => '[email protected]',
4652 - 'comment_author_url' => '',
4653 - 'comment_date' => date("Y-m-d H:i:s"),
4654 - 'comment_content' => serialize($comment),
4655 - 'comment_approved' => 1,
4656 - 'comment_type' => 'propertyhive_note',
4657 - );
4658 - $comment_id = wp_insert_comment( $data );
7630 + PH_Comments::insert_note( $post_id, $comment );
7631 +
7632 + wp_send_json_success();
4659 7633 }
4660 7634
4661 - die();
7635 + wp_send_json_error();
4662 7636 }
4663 7637
4664 7638 public function offer_declined()
4665 7639 {
@@ -4664,9 +7638,9 @@
4664 7638 public function offer_declined()
4665 7639 {
4666 7640 check_ajax_referer( 'offer-actions', 'security' );
4667 7641
4668 - $post_id = (int)$_POST['offer_id'];
7642 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4669 7643
4670 7644 $status = get_post_meta( $post_id, '_status', TRUE );
4671 7645
4672 7646 if ( $status == 'pending' )
@@ -4672,10 +7646,8 @@
4672 7646 if ( $status == 'pending' )
4673 7647 {
4674 7648 update_post_meta( $post_id, '_status', 'declined' );
4675 7649
4676 - $current_user = wp_get_current_user();
4677 -
4678 7650 // Add note/comment to offer
4679 7651 $comment = array(
4680 7652 'note_type' => 'action',
4681 7653 'action' => 'offer_declined',
@@ -4680,236 +7652,105 @@
4680 7652 'note_type' => 'action',
4681 7653 'action' => 'offer_declined',
4682 7654 );
4683 7655
4684 - $data = array(
4685 - 'comment_post_ID' => $post_id,
4686 - 'comment_author' => $current_user->display_name,
4687 - 'comment_author_email' => '[email protected]',
4688 - 'comment_author_url' => '',
4689 - 'comment_date' => date("Y-m-d H:i:s"),
4690 - 'comment_content' => serialize($comment),
4691 - 'comment_approved' => 1,
4692 - 'comment_type' => 'propertyhive_note',
4693 - );
4694 - $comment_id = wp_insert_comment( $data );
7656 + PH_Comments::insert_note( $post_id, $comment );
7657 +
7658 + wp_send_json_success();
4695 7659 }
4696 7660
4697 - die();
7661 + wp_send_json_error();
4698 7662 }
4699 7663
4700 - public function offer_revert_pending()
7664 + public function offer_withdrawn()
4701 7665 {
4702 7666 check_ajax_referer( 'offer-actions', 'security' );
4703 7667
4704 - $post_id = (int)$_POST['offer_id'];
7668 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4705 7669
4706 7670 $status = get_post_meta( $post_id, '_status', TRUE );
4707 7671
4708 - if ( $status == 'accepted' || $status == 'declined' )
7672 + if ( $status == 'pending' || $status == 'accepted' )
4709 7673 {
4710 - update_post_meta( $post_id, '_status', 'pending' );
7674 + update_post_meta( $post_id, '_status', 'withdrawn' );
4711 7675
4712 - $current_user = wp_get_current_user();
4713 -
4714 7676 // Add note/comment to offer
4715 7677 $comment = array(
4716 7678 'note_type' => 'action',
4717 - 'action' => 'offer_revert_pending',
7679 + 'action' => 'offer_withdrawn',
4718 7680 );
4719 7681
4720 - $data = array(
4721 - 'comment_post_ID' => $post_id,
4722 - 'comment_author' => $current_user->display_name,
4723 - 'comment_author_email' => '[email protected]',
4724 - 'comment_author_url' => '',
4725 - 'comment_date' => date("Y-m-d H:i:s"),
4726 - 'comment_content' => serialize($comment),
4727 - 'comment_approved' => 1,
4728 - 'comment_type' => 'propertyhive_note',
4729 - );
4730 - $comment_id = wp_insert_comment( $data );
7682 + PH_Comments::insert_note( $post_id, $comment );
7683 +
7684 + wp_send_json_success();
4731 7685 }
4732 7686
4733 - die();
7687 + wp_send_json_error();
4734 7688 }
4735 7689
4736 - public function get_property_offers_meta_box()
7690 + public function offer_revert_pending()
4737 7691 {
4738 - check_ajax_referer( 'get_property_offers_meta_box', 'security' );
7692 + check_ajax_referer( 'offer-actions', 'security' );
4739 7693
4740 - global $post;
7694 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
4741 7695
4742 - echo '<div class="propertyhive_meta_box">';
4743 -
4744 - echo '<div class="options_group">';
7696 + $status = get_post_meta( $post_id, '_status', TRUE );
4745 7697
4746 - $args = array(
4747 - 'post_type' => 'offer',
4748 - 'nopaging' => true,
4749 - 'orderby' => 'meta_value',
4750 - 'order' => 'DESC',
4751 - 'meta_key' => '_offer_date_time',
4752 - 'post_status' => 'publish',
4753 - 'meta_query' => array(
4754 - array(
4755 - 'key' => '_property_id',
4756 - 'value' => (int)$_POST['post_id']
4757 - )
4758 - )
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',
4759 7706 );
4760 - $offers_query = new WP_Query( $args );
4761 7707
4762 - if ( $offers_query->have_posts() )
4763 - {
4764 - echo '<table style="width:100%">
4765 - <thead>
4766 - <tr>
4767 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
4768 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
4769 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
4770 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
4771 - </tr>
4772 - </thead>
4773 - <tbody>';
7708 + PH_Comments::insert_note( $post_id, $comment );
4774 7709
4775 - while ( $offers_query->have_posts() )
4776 - {
4777 - $offers_query->the_post();
7710 + wp_send_json_success();
7711 + }
4778 7712
4779 - $offer = new PH_Offer(get_the_ID());
7713 + wp_send_json_error();
7714 + }
4780 7715
4781 - echo '<tr>';
4782 - 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>';
4783 - 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>';
4784 - echo '<td style="text-align:left;">' . $offer->get_formatted_amount() . '</td>';
4785 - echo '<td style="text-align:left;">';
4786 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
4787 - echo ucwords(str_replace("_", " ", $status));
4788 - echo '</td>';
4789 - echo '</tr>';
4790 - }
7716 + public function get_property_offers_meta_box()
7717 + {
7718 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
4791 7719
4792 - echo '
4793 - </tbody>
4794 - </table>
4795 - <br>';
4796 - }
4797 - else
4798 - {
4799 - echo '<p>' . __( 'No offers exist for this property', 'propertyhive') . '</p>';
4800 - }
4801 - 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 + }
4802 7727
7728 + include( PH()->plugin_path() . '/includes/admin/views/html-property-offers-meta-box.php' );
7729 +
4803 7730 do_action('propertyhive_property_offers_fields');
4804 -
4805 - echo '</div>';
4806 -
4807 - echo '</div>';
4808 7731
7732 + // Quit out
4809 7733 die();
4810 7734 }
4811 7735
4812 7736 public function get_contact_offers_meta_box()
4813 7737 {
4814 - check_ajax_referer( 'get_contact_offers_meta_box', 'security' );
7738 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
4815 7739
4816 - 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 + }
4817 7747
4818 - echo '<div class="propertyhive_meta_box">';
4819 -
4820 - echo '<div class="options_group">';
7748 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-offers-meta-box.php' );
4821 7749
4822 - $args = array(
4823 - 'post_type' => 'offer',
4824 - 'nopaging' => true,
4825 - 'orderby' => 'meta_value',
4826 - 'order' => 'DESC',
4827 - 'post_status' => 'publish',
4828 - 'meta_key' => '_offer_date_time',
4829 - 'meta_query' => array(
4830 - array(
4831 - 'key' => '_applicant_contact_id',
4832 - 'value' => (int)$_POST['post_id']
4833 - )
4834 - )
4835 - );
4836 - $offers_query = new WP_Query( $args );
4837 -
4838 - if ( $offers_query->have_posts() )
4839 - {
4840 - echo '<table style="width:100%">
4841 - <thead>
4842 - <tr>
4843 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
4844 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
4845 - <th style="text-align:left;">' . __( 'Property Owner', 'propertyhive' ) . '</th>
4846 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
4847 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
4848 - </tr>
4849 - </thead>
4850 - <tbody>';
4851 -
4852 - while ( $offers_query->have_posts() )
4853 - {
4854 - $offers_query->the_post();
4855 -
4856 - $property = new PH_Property((int)get_post_meta(get_the_ID(), '_property_id', TRUE));
4857 - $offer = new PH_Offer(get_the_ID());
4858 -
4859 - echo '<tr>';
4860 - 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>';
4861 - 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>';
4862 - echo '<td style="text-align:left;">';
4863 -
4864 - $owner_contact_ids = $property->_owner_contact_id;
4865 - if (
4866 - ( !is_array($owner_contact_ids) && $owner_contact_ids != '' && $owner_contact_ids != 0 )
4867 - ||
4868 - ( is_array($owner_contact_ids) && !empty($owner_contact_ids) )
4869 - )
4870 - {
4871 - if ( !is_array($owner_contact_ids) )
4872 - {
4873 - $owner_contact_ids = array($owner_contact_ids);
4874 - }
4875 -
4876 - foreach ( $owner_contact_ids as $owner_contact_id )
4877 - {
4878 - echo get_the_title($owner_contact_id) . '<br>';
4879 - echo '<div style="color:#BBB">';
4880 - echo 'T: ' . get_post_meta($owner_contact_id, '_telephone_number', TRUE) . '<br>';
4881 - echo 'E: ' . get_post_meta($owner_contact_id, '_email_address', TRUE);
4882 - echo '</div>';
4883 - }
4884 - }
4885 -
4886 - echo '</td>';
4887 - echo '<td style="text-align:left;">' . $offer->get_formatted_amount() . '</td>';
4888 - echo '<td style="text-align:left;">';
4889 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
4890 - echo ucwords(str_replace("_", " ", $status));
4891 - echo '</td>';
4892 - echo '</tr>';
4893 - }
4894 -
4895 - echo '
4896 - </tbody>
4897 - </table>
4898 - <br>';
4899 - }
4900 - else
4901 - {
4902 - echo '<p>' . __( 'No offers exist for this contact', 'propertyhive') . '</p>';
4903 - }
4904 - wp_reset_postdata();
4905 -
4906 7750 do_action('propertyhive_contact_offers_fields');
4907 -
4908 - echo '</div>';
4909 -
4910 - echo '</div>';
4911 7751
7752 + // Quit out
4912 7753 die();
4913 7754 }
4914 7755
4915 7756 // Sale related functions
@@ -4918,12 +7759,14 @@
4918 7759 global $post;
4919 7760
4920 7761 check_ajax_referer( 'sale-details-meta-box', 'security' );
4921 7762
4922 - $post = get_post((int)$_POST['sale_id']);
7763 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
4923 7764
4924 - $sale = new PH_Offer((int)$_POST['sale_id']);
7765 + $post = get_post( $post_id );
4925 7766
7767 + $sale = new PH_Offer( $post_id );
7768 +
4926 7769 echo '<div class="propertyhive_meta_box">';
4927 7770
4928 7771 echo '<div class="options_group">';
4929 7772
@@ -4930,11 +7773,11 @@
4930 7773 if ( $sale->status != '' )
4931 7774 {
4932 7775 echo '<p class="form-field">
4933 7776
4934 - <label for="">' . __('Status', 'propertyhive') . '</label>
7777 + <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
4935 7778
4936 - ' . ucwords(str_replace("_", " ", $sale->status)) . '
7779 + ' . esc_html(propertyhive_get_status_label( $sale->status )) . '
4937 7780
4938 7781 </p>';
4939 7782 }
4940 7783
@@ -4940,17 +7783,17 @@
4940 7783
4941 7784 $sale_date_time = $sale->sale_date_time;
4942 7785 if ( empty($sale_date_time) )
4943 7786 {
4944 - $sale_date_time = date("Y-m-d H:i:s");
7787 + $sale_date_time = gmdate("Y-m-d H:i:s");
4945 7788 }
4946 7789
4947 7790 echo '<p class="form-field sale_date_field">
4948 7791
4949 - <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="">
4950 7795
4951 - <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)) . '">
4952 -
4953 7796 </p>';
4954 7797
4955 7798 $args = array(
4956 7799 'id' => '_amount',
@@ -4956,9 +7799,9 @@
4956 7799 'id' => '_amount',
4957 7800 'label' => __( 'Sale Amount', 'propertyhive' ) . ' (&pound;)',
4958 7801 'desc_tip' => false,
4959 7802 'class' => 'short',
4960 - 'value' => ( is_numeric($sale->amount) ? number_format($sale->amount) : '' ),
7803 + 'value' => ( is_numeric($sale->amount) ? ph_display_price_field( $sale->amount ) : '' ),
4961 7804 'custom_attributes' => array(
4962 7805 //'style' => 'width:95%; max-width:500px;'
4963 7806 )
4964 7807 );
@@ -4976,12 +7819,28 @@
4976 7819 public function get_sale_actions()
4977 7820 {
4978 7821 check_ajax_referer( 'sale-actions', 'security' );
4979 7822
4980 - $post_id = (int)$_POST['sale_id'];
7823 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
4981 7824
4982 7825 $status = get_post_meta( $post_id, '_status', TRUE );
4983 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 +
4984 7843 echo '<div class="propertyhive_meta_box propertyhive_meta_box_actions" id="propertyhive_sale_actions_meta_box">
4985 7844
4986 7845 <div class="options_group" style="padding-top:8px;">';
4987 7846
@@ -4992,9 +7851,9 @@
4992 7851 $actions[] = '<a
4993 7852 href="#action_panel_sale_exchanged"
4994 7853 class="button button-success sale-action"
4995 7854 style="width:100%; margin-bottom:7px; text-align:center"
4996 - >' . __('Sale Exchanged', 'propertyhive') . '</a>';
7855 + >' . esc_html(__('Sale Exchanged', 'propertyhive')) . '</a>';
4997 7856
4998 7857 }
4999 7858
5000 7859 if ( $status == 'exchanged' )
@@ -5002,9 +7861,9 @@
5002 7861 $actions[] = '<a
5003 7862 href="#action_panel_sale_completed"
5004 7863 class="button button-success sale-action"
5005 7864 style="width:100%; margin-bottom:7px; text-align:center"
5006 - >' . __('Sale Completed', 'propertyhive') . '</a>';
7865 + >' . esc_html(__('Sale Completed', 'propertyhive')) . '</a>';
5007 7866 }
5008 7867
5009 7868 if ( $status == 'completed' )
5010 7869 {
@@ -5016,20 +7875,22 @@
5016 7875 $actions[] = '<a
5017 7876 href="#action_panel_sale_fallen_through"
5018 7877 class="button sale-action"
5019 7878 style="width:100%; margin-bottom:7px; text-align:center"
5020 - >' . __('Sale Fallen Through', 'propertyhive') . '</a>';
7879 + >' . esc_html(__('Sale Fallen Through', 'propertyhive')) . '</a>';
5021 7880 }
5022 7881
5023 7882 $actions = apply_filters( 'propertyhive_admin_sale_actions', $actions, $post_id );
7883 + $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
5024 7884
5025 7885 if ( !empty($actions) )
5026 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.
5027 7888 echo implode("", $actions);
5028 7889 }
5029 7890 else
5030 7891 {
5031 - 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>';
5032 7893 }
5033 7894
5034 7895 echo '</div>
5035 7896
@@ -5041,9 +7902,9 @@
5041 7902 public function sale_exchanged()
5042 7903 {
5043 7904 check_ajax_referer( 'sale-actions', 'security' );
5044 7905
5045 - $post_id = (int)$_POST['sale_id'];
7906 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
5046 7907
5047 7908 $status = get_post_meta( $post_id, '_status', TRUE );
5048 7909
5049 7910 if ( $status == 'current' )
@@ -5049,10 +7910,8 @@
5049 7910 if ( $status == 'current' )
5050 7911 {
5051 7912 update_post_meta( $post_id, '_status', 'exchanged' );
5052 7913
5053 - $current_user = wp_get_current_user();
5054 -
5055 7914 // Add note/comment to sale
5056 7915 $comment = array(
5057 7916 'note_type' => 'action',
5058 7917 'action' => 'sale_exchanged',
@@ -5057,22 +7916,14 @@
5057 7916 'note_type' => 'action',
5058 7917 'action' => 'sale_exchanged',
5059 7918 );
5060 7919
5061 - $data = array(
5062 - 'comment_post_ID' => $post_id,
5063 - 'comment_author' => $current_user->display_name,
5064 - 'comment_author_email' => '[email protected]',
5065 - 'comment_author_url' => '',
5066 - 'comment_date' => date("Y-m-d H:i:s"),
5067 - 'comment_content' => serialize($comment),
5068 - 'comment_approved' => 1,
5069 - 'comment_type' => 'propertyhive_note',
5070 - );
5071 - $comment_id = wp_insert_comment( $data );
7920 + PH_Comments::insert_note( $post_id, $comment );
7921 +
7922 + wp_send_json_success();
5072 7923 }
5073 7924
5074 - die();
7925 + wp_send_json_error();
5075 7926 }
5076 7927
5077 7928 public function sale_completed()
5078 7929 {
@@ -5077,9 +7928,9 @@
5077 7928 public function sale_completed()
5078 7929 {
5079 7930 check_ajax_referer( 'sale-actions', 'security' );
5080 7931
5081 - $post_id = (int)$_POST['sale_id'];
7932 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
5082 7933
5083 7934 $status = get_post_meta( $post_id, '_status', TRUE );
5084 7935
5085 7936 if ( $status == 'exchanged' )
@@ -5085,10 +7936,8 @@
5085 7936 if ( $status == 'exchanged' )
5086 7937 {
5087 7938 update_post_meta( $post_id, '_status', 'completed' );
5088 7939
5089 - $current_user = wp_get_current_user();
5090 -
5091 7940 // Add note/comment to sale
5092 7941 $comment = array(
5093 7942 'note_type' => 'action',
5094 7943 'action' => 'sale_completed',
@@ -5093,22 +7942,14 @@
5093 7942 'note_type' => 'action',
5094 7943 'action' => 'sale_completed',
5095 7944 );
5096 7945
5097 - $data = array(
5098 - 'comment_post_ID' => $post_id,
5099 - 'comment_author' => $current_user->display_name,
5100 - 'comment_author_email' => '[email protected]',
5101 - 'comment_author_url' => '',
5102 - 'comment_date' => date("Y-m-d H:i:s"),
5103 - 'comment_content' => serialize($comment),
5104 - 'comment_approved' => 1,
5105 - 'comment_type' => 'propertyhive_note',
5106 - );
5107 - $comment_id = wp_insert_comment( $data );
7946 + PH_Comments::insert_note( $post_id, $comment );
7947 +
7948 + wp_send_json_success();
5108 7949 }
5109 7950
5110 - die();
7951 + wp_send_json_error();
5111 7952 }
5112 7953
5113 7954 public function sale_fallen_through()
5114 7955 {
@@ -5113,9 +7954,9 @@
5113 7954 public function sale_fallen_through()
5114 7955 {
5115 7956 check_ajax_referer( 'sale-actions', 'security' );
5116 7957
5117 - $post_id = (int)$_POST['sale_id'];
7958 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
5118 7959
5119 7960 $status = get_post_meta( $post_id, '_status', TRUE );
5120 7961
5121 7962 if ( $status == 'current' || $status == 'exchanged' )
@@ -5121,10 +7962,8 @@
5121 7962 if ( $status == 'current' || $status == 'exchanged' )
5122 7963 {
5123 7964 update_post_meta( $post_id, '_status', 'fallen_through' );
5124 7965
5125 - $current_user = wp_get_current_user();
5126 -
5127 7966 // Add note/comment to sale
5128 7967 $comment = array(
5129 7968 'note_type' => 'action',
5130 7969 'action' => 'sale_fallen_through',
@@ -5129,202 +7968,736 @@
5129 7968 'note_type' => 'action',
5130 7969 'action' => 'sale_fallen_through',
5131 7970 );
5132 7971
5133 - $data = array(
5134 - 'comment_post_ID' => $post_id,
5135 - 'comment_author' => $current_user->display_name,
5136 - 'comment_author_email' => '[email protected]',
5137 - 'comment_author_url' => '',
5138 - 'comment_date' => date("Y-m-d H:i:s"),
5139 - 'comment_content' => serialize($comment),
5140 - 'comment_approved' => 1,
5141 - 'comment_type' => 'propertyhive_note',
5142 - );
5143 - $comment_id = wp_insert_comment( $data );
7972 + PH_Comments::insert_note( $post_id, $comment );
7973 +
7974 + wp_send_json_success();
5144 7975 }
5145 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
5146 7997 die();
5147 7998 }
5148 7999
5149 - public function get_property_sales_meta_box()
8000 + public function get_contact_sales_meta_box()
5150 8001 {
5151 - check_ajax_referer( 'get_property_sales_meta_box', 'security' );
8002 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
5152 8003
5153 - 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 + }
5154 8011
5155 - echo '<div class="propertyhive_meta_box">';
5156 -
5157 - echo '<div class="options_group">';
8012 + include( PH()->plugin_path() . '/includes/admin/views/html-contact-sales-meta-box.php' );
5158 8013
5159 - $args = array(
5160 - 'post_type' => 'sale',
5161 - 'nopaging' => true,
5162 - 'orderby' => 'meta_value',
5163 - 'order' => 'DESC',
5164 - 'meta_key' => '_sale_date_time',
5165 - 'post_status' => 'publish',
5166 - 'meta_query' => array(
5167 - array(
5168 - 'key' => '_property_id',
5169 - 'value' => (int)$_POST['post_id']
5170 - )
5171 - )
5172 - );
5173 - $sales_query = new WP_Query( $args );
8014 + do_action('propertyhive_contact_sales_fields');
5174 8015
5175 - 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 )
5176 8167 {
5177 - echo '<table style="width:100%">
5178 - <thead>
5179 - <tr>
5180 - <th style="text-align:left;">' . __( 'Sale Date', 'propertyhive' ) . '</th>
5181 - <th style="text-align:left;">' . __( 'Applicant', 'propertyhive' ) . '</th>
5182 - <th style="text-align:left;">' . __( 'Sale Amount', 'propertyhive' ) . '</th>
5183 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
5184 - </tr>
5185 - </thead>
5186 - <tbody>';
8168 + list($key, $value) = explode('=', $key_value_pair);
8169 + $recurrence[strtolower($key)] = $value;
8170 + }
5187 8171
5188 - 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'] )
5189 8176 {
5190 - $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 + }
5191 8190
5192 - $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 + }
5193 8198
5194 - echo '<tr>';
5195 - 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>';
5196 - 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>';
5197 - echo '<td style="text-align:left;">' . $sale->get_formatted_amount() . '</td>';
5198 - echo '<td style="text-align:left;">';
5199 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
5200 - echo ucwords(str_replace("_", " ", $status));
5201 - echo '</td>';
5202 - echo '</tr>';
5203 - }
8199 + echo esc_html($next_key_date);
5204 8200
5205 - echo '
5206 - </tbody>
5207 - </table>
5208 - <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 );
5209 8222 }
5210 - 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 )
5211 8260 {
5212 - 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();
5213 8264 }
5214 - wp_reset_postdata();
5215 8265
5216 - do_action('propertyhive_property_sales_fields');
5217 -
5218 - echo '</div>';
5219 -
5220 - 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'] ) );
5221 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 +
5222 8279 die();
5223 8280 }
5224 8281
5225 - public function get_contact_sales_meta_box()
8282 + public function delete_key_date()
5226 8283 {
5227 - check_ajax_referer( 'get_contact_sales_meta_box', 'security' );
8284 + check_ajax_referer( 'delete-key-date', 'security' );
5228 8285
5229 - global $post;
8286 + $this->json_headers();
5230 8287
5231 - 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 + }
5232 8412
5233 - echo '<div class="options_group">';
8413 + // check plugin status
8414 + $slug = isset( $_POST['slug'] ) && is_string( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
5234 8415
5235 - $args = array(
5236 - 'post_type' => 'sale',
5237 - 'nopaging' => true,
5238 - 'orderby' => 'meta_value',
5239 - 'order' => 'DESC',
5240 - 'post_status' => 'publish',
5241 - 'meta_key' => '_sale_date_time',
5242 - 'meta_query' => array(
5243 - array(
5244 - 'key' => '_applicant_contact_id',
5245 - 'value' => (int)$_POST['post_id']
5246 - )
5247 - )
8416 + $feature = get_ph_pro_feature( $slug );
8417 +
8418 + if ( $feature === false )
8419 + {
8420 + $return = array(
8421 + 'errorMessage' => 'Feature not found'
5248 8422 );
5249 - $sales_query = new WP_Query( $args );
8423 + wp_send_json_error($return);
8424 + }
5250 8425
5251 - if ( $sales_query->have_posts() )
5252 - {
5253 - echo '<table style="width:100%">
5254 - <thead>
5255 - <tr>
5256 - <th style="text-align:left;">' . __( 'Offer Date', 'propertyhive' ) . '</th>
5257 - <th style="text-align:left;">' . __( 'Property', 'propertyhive' ) . '</th>
5258 - <th style="text-align:left;">' . __( 'Property Owner', 'propertyhive' ) . '</th>
5259 - <th style="text-align:left;">' . __( 'Offer Amount', 'propertyhive' ) . '</th>
5260 - <th style="text-align:left;">' . __( 'Status', 'propertyhive' ) . '</th>
5261 - </tr>
5262 - </thead>
5263 - <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 + }
5264 8433
5265 - while ( $sales_query->have_posts() )
5266 - {
5267 - $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 + }
5268 8440
5269 - $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;
5270 8445
5271 - $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 + }
5272 8457
5273 - echo '<tr>';
5274 - 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>';
5275 - 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>';
5276 - 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();
5277 8464
5278 - $owner_contact_ids = $property->_owner_contact_id;
8465 + if ( isset($product_id_and_package['success']) && $product_id_and_package['success'] === true )
8466 + {
5279 8467 if (
5280 - ( !is_array($owner_contact_ids) && $owner_contact_ids != '' && $owner_contact_ids != 0 )
5281 - ||
5282 - ( 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'])
5283 8471 )
5284 8472 {
5285 - if ( !is_array($owner_contact_ids) )
5286 - {
5287 - $owner_contact_ids = array($owner_contact_ids);
5288 - }
5289 -
5290 - foreach ( $owner_contact_ids as $owner_contact_id )
5291 - {
5292 - echo get_the_title($owner_contact_id) . '<br>';
5293 - echo '<div style="color:#BBB">';
5294 - echo 'T: ' . get_post_meta($owner_contact_id, '_telephone_number', TRUE) . '<br>';
5295 - echo 'E: ' . get_post_meta($owner_contact_id, '_email_address', TRUE);
5296 - echo '</div>';
5297 - }
8473 + $valid_license_key = true;
5298 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 + }
5299 8499
5300 - echo '</td>';
5301 - echo '<td style="text-align:left;">' . $sale->get_formatted_amount() . '</td>';
5302 - echo '<td style="text-align:left;">';
5303 - $status = get_post_meta(get_the_ID(), '_status', TRUE);
5304 - echo ucwords(str_replace("_", " ", $status));
5305 - echo '</td>';
5306 - echo '</tr>';
5307 - }
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 + }
5308 8508
5309 - echo '
5310 - </tbody>
5311 - </table>
5312 - <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 + );
5313 8522 }
5314 8523 else
5315 8524 {
5316 - 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 + );
5317 8543 }
5318 - wp_reset_postdata();
5319 8544
5320 - 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
5321 8562
5322 - echo '</div>';
5323 -
5324 - 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 + }
5325 8570
5326 - 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();
5327 8700 }
5328 8701 }
5329 8702
5330 8703 new PH_AJAX();