PluginProbe
Property Hive / 2.3.1
Property Hive v2.3.1
2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 1.4.61 All 261 releases
← All changes | includes/class-ph-ajax.php +1042 -577 2.2.62.3.1 View file →
@@ -1,6 +1,9 @@
1 1 <?php
2 +// phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 +// ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
2 4
5 +
3 6 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
4 7
5 8 /**
6 9 * PropertyHive PH_AJAX
@@ -12,8 +15,9 @@
12 15 * @package PropertyHive/Classes
13 16 * @category Class
14 17 * @author PropertyHive
15 18 */
19 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_AJAX; preserving the existing PH_* class name is required for plugin and extension compatibility.
16 20 class PH_AJAX {
17 21
18 22 /**
19 23 * Hook into ajax events
@@ -155,8 +159,11 @@
155 159 );
156 160
157 161 foreach ( $ajax_events as $ajax_event => $nopriv )
158 162 {
163 + if ( ! $nopriv ) {
164 + add_action( 'wp_ajax_propertyhive_' . $ajax_event, array( $this, 'authorize_admin_ajax' ), 0 );
165 + }
159 166 add_action( 'wp_ajax_propertyhive_' . $ajax_event, array( $this, $ajax_event ) );
160 167
161 168 if ( $nopriv ) {
162 169 add_action( 'wp_ajax_nopriv_propertyhive_' . $ajax_event, array( $this, $ajax_event ) );
@@ -163,25 +170,150 @@
163 170 }
164 171 }
165 172 }
166 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 +
167 299 public function deactivate_survey()
168 300 {
169 301 // Verify the nonce
170 - if ( !isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'deactivate-survey') )
302 + if ( !isset($_POST['nonce']) || !wp_verify_nonce( ( isset( $_POST['nonce'] ) && is_string( $_POST['nonce'] ) ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '', 'deactivate-survey') )
171 303 {
172 304 wp_send_json_error('Invalid nonce', 403);
173 305 die();
174 306 }
175 307
176 - if ( !isset($_POST['reason']) || empty($_POST['reason']) )
308 + if ( !isset($_POST['reason']) || !is_string($_POST['reason']) || empty($_POST['reason']) )
177 309 {
178 310 wp_send_json_error('Reason is required', 400);
179 311 die();
180 312 }
181 313
182 - $reason = sanitize_text_field($_POST['reason']);
183 - $comments = isset($_POST['comments']) ? sanitize_textarea_field($_POST['comments']) : '';
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'] ) ) : '';
184 316 $anonymous = isset($_POST['anonymous']) && $_POST['anonymous'] === 'yes';
185 317
186 318 $license_type = get_option('propertyhive_license_type');
187 319 if ( $license_type == 'pro' )
@@ -208,9 +340,9 @@
208 340 'path' => $plugin,
209 341 );
210 342 }
211 343 }
212 - $server_software = $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown';
344 + $server_software = ( isset( $_SERVER['SERVER_SOFTWARE'] ) && is_string( $_SERVER['SERVER_SOFTWARE'] ) ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'Unknown';
213 345
214 346 // Prepare data for third-party POST
215 347 $third_party_data = array(
216 348 'reason' => $reason,
@@ -254,21 +386,25 @@
254 386 public function save_term_order()
255 387 {
256 388 check_ajax_referer( 'updates', 'security' );
257 389
258 - if ( !isset($_POST['taxonomy']) || ( isset($_POST['taxonomy']) && empty(ph_clean($_POST['taxonomy'])) ) )
259 - {
390 + if ( ! isset( $_POST['taxonomy'], $_POST['term'] ) || ! is_string( $_POST['taxonomy'] ) || ! is_array( $_POST['term'] ) || empty( $_POST['term'] ) ) {
260 391 die();
261 392 }
262 -
263 - if ( !isset($_POST['term']) || ( isset($_POST['term']) && empty(ph_clean($_POST['term'])) ) )
264 - {
265 - die();
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 );
266 397 }
267 -
268 - update_option( 'propertyhive_taxonomy_terms_order_' . ph_clean($_POST['taxonomy']), implode("|", ph_clean($_POST['term'])));
269 -
270 - // Quit out
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 ) );
271 407 die();
272 408 }
273 409
274 410 public function dismiss_notice_leave_review()
@@ -368,9 +504,10 @@
368 504
369 505 private function check_recaptcha_form_response($errors, $key, $control)
370 506 {
371 507 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
372 - $response = isset( $_POST['g-recaptcha-response'] ) ? ph_clean($_POST['g-recaptcha-response']) : '';
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'] ) ) : '';
373 510
374 511 $response = wp_remote_post(
375 512 'https://www.google.com/recaptcha/api/siteverify',
376 513 array(
@@ -435,27 +572,28 @@
435 572 public function create_contact_login()
436 573 {
437 574 check_ajax_referer( 'create-login', 'security' );
438 575
439 - $this->json_headers();
440 -
441 - if (empty($_POST['contact_id']))
442 - {
443 - $return = array('error' => 'No contact selected');
444 - echo json_encode( $return );
445 - 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 );
446 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 + }
447 586
448 - if (empty($_POST['password']))
587 + if ( empty( $_POST['password'] ) || ! is_string( $_POST['password'] ) )
449 588 {
450 589 $return = array('error' => 'No password entered');
451 - echo json_encode( $return );
452 - die();
590 + wp_send_json( $return );
453 591 }
454 592
455 - $contact = new PH_Contact((int)$_POST['contact_id']);
593 + $contact = new PH_Contact($contact_id);
456 594
457 - $display_name = get_the_title((int)$_POST['contact_id']);
595 + $display_name = get_the_title($contact_id);
458 596
459 597 // Create user
460 598 $userdata = array(
461 599 'display_name' => $display_name,
@@ -460,9 +598,10 @@
460 598 $userdata = array(
461 599 'display_name' => $display_name,
462 600 'user_login' => sanitize_email($contact->email_address),
463 601 'user_email' => sanitize_email($contact->email_address),
464 - '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'] ),
465 604 'role' => 'property_hive_contact',
466 605 'show_admin_bar_front' => 'false',
467 606 );
468 607
@@ -486,9 +625,9 @@
486 625 // On success
487 626 if ( ! is_wp_error( $user_id ) )
488 627 {
489 628 // Assign user ID to CPT
490 - add_post_meta( (int)$_POST['contact_id'], '_user_id', $user_id );
629 + add_post_meta( $contact_id, '_user_id', $user_id );
491 630
492 631 $return = array('success' => true);
493 632 }
494 633 else
@@ -495,10 +634,9 @@
495 634 {
496 635 $return = array('error' => 'Failed to create user login');
497 636 }
498 637
499 - echo json_encode( $return );
500 - die();
638 + wp_send_json( $return );
501 639 }
502 640
503 641 /**
504 642 * Login user
@@ -513,18 +651,19 @@
513 651 if ( check_ajax_referer( 'ph_login', 'security', false ) === FALSE )
514 652 {
515 653 $return['errors'][] = 'Invalid nonce';
516 654
517 - $this->json_headers();
518 - echo json_encode( $return );
519 -
520 - // Quit out
521 - die();
655 + wp_send_json( $return );
522 656 }
523 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 + }
524 662 $creds = array(
525 - 'user_login' => ph_clean($_POST['email_address']),
526 - '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'] ),
527 666 );
528 667
529 668 $user = wp_signon( apply_filters( 'propertyhive_login_credentials', $creds ), is_ssl() );
530 669
@@ -539,8 +678,9 @@
539 678 'post_type' => apply_filters( 'propertyhive_allowed_login_post_type', array( 'contact' ) ),
540 679 'fields' => 'ids',
541 680 'posts_per_page' => 1,
542 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.
543 683 'meta_query' => array(
544 684 array(
545 685 'key' => '_user_id',
546 686 'value' => $user->ID
@@ -565,13 +705,9 @@
565 705
566 706 wp_reset_postdata();
567 707 }
568 708
569 - $this->json_headers();
570 - echo json_encode( $return );
571 -
572 - // Quit out
573 - die();
709 + wp_send_json( $return );
574 710 }
575 711
576 712 /**
577 713 * Lost password
@@ -586,16 +722,12 @@
586 722 if ( check_ajax_referer( 'ph_lost_password', 'security', false ) === FALSE )
587 723 {
588 724 $return['errors'][] = 'Invalid nonce';
589 725
590 - $this->json_headers();
591 - echo json_encode( $return );
592 -
593 - // Quit out
594 - die();
726 + wp_send_json( $return );
595 727 }
596 728
597 - $email_address = sanitize_email($_POST['email_address']);
729 + $email_address = isset( $_POST['email_address'] ) && is_string( $_POST['email_address'] ) ? sanitize_email( wp_unslash( $_POST['email_address'] ) ) : '';
598 730
599 731 $user_data = get_user_by( 'email', $email_address );
600 732
601 733 // check email address exists
@@ -602,13 +734,9 @@
602 734 if ( !$user_data )
603 735 {
604 736 $return['errors'][] = 'Email address not found';
605 737
606 - $this->json_headers();
607 - echo json_encode( $return );
608 -
609 - // Quit out
610 - die();
738 + wp_send_json( $return );
611 739 }
612 740
613 741 // Send reset email
614 742 $to = $email_address;
@@ -637,13 +765,9 @@
637 765 wp_mail( $to, $subject, $body, $headers );
638 766
639 767 $return['success'] = true;
640 768
641 - $this->json_headers();
642 - echo json_encode( $return );
643 -
644 - // Quit out
645 - die();
769 + wp_send_json( $return );
646 770 }
647 771
648 772 /**
649 773 * Reset password
@@ -658,23 +782,26 @@
658 782 if ( check_ajax_referer( 'ph_reset_password', 'security', false ) === FALSE )
659 783 {
660 784 $return['errors'][] = 'Invalid nonce';
661 785
662 - $this->json_headers();
663 - echo json_encode( $return );
664 -
665 - // Quit out
666 - die();
786 + wp_send_json( $return );
667 787 }
668 788
669 789 // check key and user login again
670 - $user = check_password_reset_key( ph_clean($_POST['reset_key']), ph_clean($_POST['reset_login']) );
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'] ) );
671 796
672 797 // check passwords match and are strong enough
673 798 if ( $user instanceof WP_User )
674 799 {
675 - $password_1 = ph_clean($_POST['password_1']);
676 - $password_2 = ph_clean($_POST['password_2']);
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'] );
677 804
678 805 if ( empty( $password_1 ) )
679 806 {
680 807 $return['errors'][] = __( 'Please enter your password.', 'propertyhive' );
@@ -693,19 +820,17 @@
693 820 }
694 821
695 822 if ( !empty($return['errors']) )
696 823 {
697 - $this->json_headers();
698 - echo json_encode( $return );
699 -
700 - // Quit out
701 - die();
824 + wp_send_json( $return );
702 825 }
703 826
704 827 // do actual reset
705 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.
706 830 do_action( 'validate_password_reset', $errors, $user );
707 831
832 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core hook password_reset; renaming it would break the core hook contract.
708 833 do_action( 'password_reset', $user, $password_1 );
709 834
710 835 wp_set_password( $password_1, $user->ID );
711 836
@@ -710,13 +835,9 @@
710 835 wp_set_password( $password_1, $user->ID );
711 836
712 837 $return['success'] = true;
713 838
714 - $this->json_headers();
715 - echo json_encode( $return );
716 -
717 - // Quit out
718 - die();
839 + wp_send_json( $return );
719 840 }
720 841
721 842 /**
722 843 * Register applicant
@@ -744,8 +865,48 @@
744 865
745 866 // Validate
746 867 $errors = array();
747 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 +
748 909 $form_controls = ph_get_user_details_form_fields();
749 910
750 911 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
751 912
@@ -783,9 +944,9 @@
783 944 }
784 945 }
785 946 if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) )
786 947 {
787 - if ( ! is_email( $_POST[$key] ) )
948 + if ( ! is_string( $_POST[$key] ) || ! is_email( wp_unslash( $_POST[$key] ) ) )
788 949 {
789 950 $errors[] = __( 'Invalid email address provided', 'propertyhive' );
790 951 }
791 952 else
@@ -795,12 +956,13 @@
795 956 'post_type' => 'contact',
796 957 'posts_per_page' => 1,
797 958 'fields' => 'ids',
798 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.
799 961 'meta_query' => array(
800 962 array(
801 963 'key' => '_email_address',
802 - 'value' => $_POST[$key]
964 + 'value' => sanitize_email( wp_unslash( $_POST[$key] ) )
803 965 )
804 966 )
805 967 );
806 968
@@ -807,19 +969,14 @@
807 969 $contacts_query = new WP_Query( $args );
808 970
809 971 if ( $contacts_query->have_posts() )
810 972 {
811 - while ( $contacts_query->have_posts() )
812 - {
813 - $contacts_query->the_post();
814 -
815 - $contact_post_id = get_the_ID();
816 - }
817 - //$errors[] = __( 'This email address is already registered', 'propertyhive' );
973 + // Public registration does not prove ownership of an existing CRM contact.
974 + $errors[] = __( 'This email address is already registered to a user. Please sign in or contact the agency.', 'propertyhive' );
818 975 }
819 976 else
820 977 {
821 - if ( email_exists( $_POST[$key] ) )
978 + if ( email_exists( sanitize_email( wp_unslash( $_POST[$key] ) ) ) )
822 979 {
823 980 $errors[] = __( 'This email address is already registered to a user', 'propertyhive' );
824 981 }
825 982 }
@@ -833,9 +990,9 @@
833 990
834 991 if ( $key == 'hCaptcha' )
835 992 {
836 993 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
837 - $response = isset( $_POST['h-captcha-response'] ) ? ph_clean($_POST['h-captcha-response']) : '';
994 + $response = ( isset( $_POST['h-captcha-response'] ) && is_string( $_POST['h-captcha-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['h-captcha-response'] ) ) : '';
838 995
839 996 $response = wp_remote_post(
840 997 'https://hcaptcha.com/siteverify',
841 998 array(
@@ -871,12 +1028,12 @@
871 1028
872 1029 if ( $key == 'turnstile' )
873 1030 {
874 1031 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
875 - $response = isset( $_POST['cf-turnstile-response'] ) ? ph_clean($_POST['cf-turnstile-response']) : '';
1032 + $response = ( isset( $_POST['cf-turnstile-response'] ) && is_string( $_POST['cf-turnstile-response'] ) ) ? sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) ) : '';
876 1033
877 1034 $response = wp_remote_post(
878 - 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
1035 + 'https://challenges.cloudflare.com/turnstile/v0/siteverify', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Server-side CAPTCHA token verification API.
879 1036 array(
880 1037 'method' => 'POST',
881 1038 'headers' => array(
882 1039 'Content-Type' => 'application/x-www-form-urlencoded',
@@ -911,9 +1068,9 @@
911 1068 }
912 1069 }
913 1070
914 1071 // Check password and password2 match
915 - 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'] )
916 1073 {
917 1074 $errors[] = __( 'The passwords entered do not match', 'propertyhive' );
918 1075 }
919 1076
@@ -930,9 +1087,9 @@
930 1087 if ( $contact_post_id === FALSE )
931 1088 {
932 1089 // create CPT
933 1090 $contact_post = array(
934 - 'post_title' => ph_clean($_POST['name']),
1091 + 'post_title' => wp_slash( $registration_input['name'] ),
935 1092 'post_content' => '',
936 1093 'post_type' => 'contact',
937 1094 'post_status' => 'publish',
938 1095 'comment_status'=> 'closed',
@@ -946,9 +1103,9 @@
946 1103 {
947 1104 // update CPT
948 1105 $contact_post = array(
949 1106 'ID' => $contact_post_id,
950 - 'post_title' => ph_clean($_POST['name']),
1107 + 'post_title' => wp_slash( $registration_input['name'] ),
951 1108 'post_status' => 'publish',
952 1109 );
953 1110
954 1111 // Insert the post into the database
@@ -965,16 +1122,16 @@
965 1122 }
966 1123 update_post_meta( $contact_post_id, '_forbidden_contact_methods', array_unique($forbidden_contact_methods) );
967 1124
968 1125 // Add post meta (contact details, requirements etc)
969 - 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'] ) );
970 1127
971 1128 $telephone_number = get_post_meta( $contact_post_id, '_telephone_number', TRUE );
972 1129 if ( isset($_POST['telephone_number']) && $_POST['telephone_number'] != '' )
973 1130 {
974 - $telephone_number = $_POST['telephone_number'];
1131 + $telephone_number = $registration_input['telephone_number'];
975 1132 }
976 - 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) ) );
977 1134 update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean( ph_clean_telephone_number($telephone_number) ) );
978 1135
979 1136 $contact_types = get_post_meta( $contact_post_id, '_contact_types', TRUE );
980 1137 if ( !is_array($contact_types) )
@@ -989,11 +1146,11 @@
989 1146
990 1147 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
991 1148
992 1149 $applicant_profile = array();
993 - $applicant_profile['department'] = $_POST['department'];
1150 + $applicant_profile['department'] = $registration_input['department'];
994 1151
995 - $base_department = $_POST['department'];
1152 + $base_department = $registration_input['department'];
996 1153 if ( !in_array( $base_department, array('residential-sales', 'residential-lettings', 'commercial') ) )
997 1154 {
998 1155 $base_department = ph_get_custom_department_based_on($base_department);
999 1156 }
@@ -999,9 +1156,9 @@
999 1156 }
1000 1157
1001 1158 if ( $base_department == 'residential-sales' )
1002 1159 {
1003 - $price = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_price']));
1160 + $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_price']);
1004 1161
1005 1162 $applicant_profile['max_price'] = $price;
1006 1163
1007 1164 // Not used yet but could be if introducing currencies in the future.
@@ -1009,11 +1166,11 @@
1009 1166
1010 1167 $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
1011 1168 $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
1012 1169
1013 - 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 )
1014 1171 {
1015 - $price = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_price']));
1172 + $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_price']);
1016 1173 $applicant_profile['match_price_range_lower'] = $price - ( $price * ( $percentage_lower / 100 ) );
1017 1174 $applicant_profile['match_price_range_lower_actual'] = $price - ( $price * ( $percentage_lower / 100 ) );
1018 1175
1019 1176 $applicant_profile['match_price_range_higher'] = $price + ( $price * ( $percentage_higher / 100 ) );
@@ -1021,9 +1178,9 @@
1021 1178 }
1022 1179 }
1023 1180 elseif ( $base_department == 'residential-lettings' )
1024 1181 {
1025 - $price = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_rent']));
1182 + $price = preg_replace("/[^0-9.]/", '', $registration_input['maximum_rent']);
1026 1183
1027 1184 $applicant_profile['max_rent'] = $price;
1028 1185 $applicant_profile['rent_frequency'] = 'pcm';
1029 1186 $price_actual = $price; // Stored in pcm
@@ -1031,14 +1188,14 @@
1031 1188 }
1032 1189
1033 1190 if ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' )
1034 1191 {
1035 - $beds = preg_replace("/[^0-9.]/", '', ph_clean($_POST['minimum_bedrooms']));
1192 + $beds = preg_replace("/[^0-9.]/", '', $registration_input['minimum_bedrooms']);
1036 1193 $applicant_profile['min_beds'] = $beds;
1037 1194
1038 1195 if ( isset($_POST['property_type']) && !empty($_POST['property_type']) )
1039 1196 {
1040 - $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'];
1041 1198 }
1042 1199 }
1043 1200
1044 1201 if ( $base_department == 'commercial' )
@@ -1043,59 +1200,59 @@
1043 1200
1044 1201 if ( $base_department == 'commercial' )
1045 1202 {
1046 1203 $available_as = array();
1047 - if ( isset($_POST['available_as_sale']) && $_POST['available_as_sale'] == 'yes' )
1204 + if ( isset($_POST['available_as_sale']) && $registration_input['available_as_sale'] == 'yes' )
1048 1205 {
1049 1206 $available_as[] = 'sale';
1050 1207 }
1051 - if ( isset($_POST['available_as_rent']) && $_POST['available_as_rent'] == 'yes' )
1208 + if ( isset($_POST['available_as_rent']) && $registration_input['available_as_rent'] == 'yes' )
1052 1209 {
1053 1210 $available_as[] = 'rent';
1054 1211 }
1055 1212 $applicant_profile['available_as'] = $available_as;
1056 1213
1057 - $floor_area = preg_replace("/[^0-9.]/", '', ph_clean($_POST['minimum_floor_area']));
1214 + $floor_area = preg_replace("/[^0-9.]/", '', $registration_input['minimum_floor_area']);
1058 1215 $applicant_profile['min_floor_area'] = $floor_area;
1059 1216 $applicant_profile['min_floor_area_actual'] = $floor_area;
1060 1217
1061 - $floor_area = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_floor_area']));
1218 + $floor_area = preg_replace("/[^0-9.]/", '', $registration_input['maximum_floor_area']);
1062 1219 $applicant_profile['max_floor_area'] = $floor_area;
1063 1220 $applicant_profile['max_floor_area_actual'] = $floor_area;
1064 1221
1065 1222 if ( isset($_POST['commercial_property_type']) && !empty($_POST['commercial_property_type']) )
1066 1223 {
1067 - $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'];
1068 1225 }
1069 1226 }
1070 1227
1071 1228 if ( isset($_POST['location']) && !empty($_POST['location']) )
1072 1229 {
1073 - $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'];
1074 1231 }
1075 1232
1076 1233 if ( isset($_POST['location_text']) && !empty($_POST['location_text']) )
1077 1234 {
1078 - $applicant_profile['location_text'] = ph_clean($_POST['location_text']);
1235 + $applicant_profile['location_text'] = $registration_input['location_text'];
1079 1236 }
1080 1237
1081 - $applicant_profile['notes'] = ( ( isset($_POST['additional_requirements']) ) ? sanitize_textarea_field($_POST['additional_requirements']) : '' );
1238 + $applicant_profile['notes'] = $registration_input['additional_requirements'];
1082 1239
1083 1240 $applicant_profile['send_matching_properties'] = 'yes';
1084 1241 //$applicant_profile['auto_match_disabled'] = ''; // don't know what to do about this yet. Should probably look at global setting and reflect that
1085 1242
1086 - 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 ) );
1087 1244
1088 1245 if ( get_option( 'propertyhive_applicant_users', '' ) == 'yes' )
1089 1246 {
1090 - $display_name = ph_clean($_POST['name']);
1247 + $display_name = wp_slash( $registration_input['name'] );
1091 1248
1092 1249 // Create user
1093 1250 $userdata = array(
1094 1251 'display_name' => $display_name,
1095 - 'user_login' => sanitize_email($_POST['email_address']),
1096 - 'user_email' => sanitize_email($_POST['email_address']),
1097 - 'user_pass' => ph_clean($_POST['password']),
1252 + 'user_login' => sanitize_email( $registration_input['email_address'] ),
1253 + 'user_email' => sanitize_email( $registration_input['email_address'] ),
1254 + 'user_pass' => $registration_input['password'],
1098 1255 'role' => 'property_hive_contact',
1099 1256 'show_admin_bar_front' => 'false',
1100 1257 );
1101 1258
@@ -1195,8 +1352,22 @@
1195 1352 // Quit out
1196 1353 die();
1197 1354 }
1198 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 + }
1199 1370 $form_controls = ph_get_user_details_form_fields();
1200 1371
1201 1372 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
1202 1373
@@ -1211,9 +1382,9 @@
1211 1382 }
1212 1383 }
1213 1384 if ( isset( $control['type'] ) && $control['type'] == 'email' && isset( $_POST[$key] ) && ! empty( $_POST[$key] ) )
1214 1385 {
1215 - if ( ! is_email( $_POST[$key] ) )
1386 + if ( ! is_string( $_POST[$key] ) || ! is_email( sanitize_email( wp_unslash( $_POST[$key] ) ) ) )
1216 1387 {
1217 1388 $errors[] = __( 'Invalid email address provided', 'propertyhive' );
1218 1389 }
1219 1390
@@ -1221,13 +1392,27 @@
1221 1392 }
1222 1393 }
1223 1394
1224 1395 // Check password and password2 match
1225 - 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'] )
1226 1397 {
1227 1398 $errors[] = __( 'The passwords entered do not match', 'propertyhive' );
1228 1399 }
1229 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 +
1230 1415 if ( !empty($errors) )
1231 1416 {
1232 1417 // Failed validation
1233 1418
@@ -1237,47 +1422,52 @@
1237 1422 }
1238 1423 else
1239 1424 {
1240 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 + }
1241 1431
1242 1432 // create CPT
1243 1433 $contact_post = array(
1244 1434 'ID' => $contact->id,
1245 - 'post_title' => ph_clean($_POST['name']),
1435 + 'post_title' => wp_slash( $account_input['name'] ),
1246 1436 );
1247 1437
1248 1438 // Update the post in the database
1249 1439 $contact_post_id = wp_update_post( $contact_post );
1250 1440
1251 - 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'] ) );
1252 1442 if (isset($_POST['telephone_number']))
1253 1443 {
1254 - update_post_meta( $contact_post_id, '_telephone_number', ph_clean($_POST['telephone_number']) );
1255 - update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean(ph_clean_telephone_number($_POST['telephone_number'])) );
1444 + update_post_meta( $contact_post_id, '_telephone_number', wp_slash( $account_input['telephone_number'] ) );
1445 + update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean_telephone_number( $account_input['telephone_number'] ) );
1256 1446 }
1257 1447
1258 1448 // Update user
1259 1449 $userdata = array(
1260 1450 'ID' => $user_id,
1261 - 'display_name' => ph_clean($_POST['name']),
1262 - 'user_email' => sanitize_email($_POST['email_address']),
1451 + 'display_name' => wp_slash( $account_input['name'] ),
1452 + 'user_email' => sanitize_email( $account_input['email_address'] ),
1263 1453 );
1264 1454
1265 1455 if ( isset($_POST['password']) && !empty($_POST['password']) )
1266 1456 {
1267 - $userdata['user_pass'] = ph_clean($_POST['password']);
1457 + $userdata['user_pass'] = $account_input['password'];
1268 1458 }
1269 1459
1270 1460 $user_id = wp_update_user( $userdata );
1271 1461
1272 - $user_roles = $current_user->roles;
1273 - $user_role = array_shift($user_roles);
1274 -
1275 - if ( $user_role === 'property_hive_contact' )
1462 + if ( ! is_wp_error( $user_id ) && $user_role === 'property_hive_contact' )
1276 1463 {
1277 1464 // Have to update login via SQL as wp_update_user won't allow altering
1278 1465 // Only do it for property hive contacts though as admin or editor might be viewing this page
1279 - $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 );
1280 1470 }
1281 1471
1282 1472 //On success
1283 1473 if ( ! is_wp_error( $user_id ) )
@@ -1351,11 +1541,48 @@
1351 1541 $contact = new PH_Contact( '', $user_id );
1352 1542
1353 1543 $contact_post_id = $contact->id;
1354 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'] );
1355 1582 $form_controls = ph_get_applicant_requirements_form_fields();
1356 1583
1357 - $form_controls = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls, get_post_meta( $contact_post_id, '_applicant_profile_' . ( isset($_POST['profile_id']) && $_POST['profile_id'] != '' ? (int)$_POST['profile_id'] : '0' ), true ) );
1584 + $form_controls = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls, get_post_meta( $contact_post_id, '_applicant_profile_' . $profile_id, true ) );
1358 1585
1359 1586 foreach ( $form_controls as $key => $control )
1360 1587 {
1361 1588 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
@@ -1378,11 +1605,11 @@
1378 1605 }
1379 1606 else
1380 1607 {
1381 1608 $applicant_profile = array();
1382 - $applicant_profile['department'] = ph_clean($_POST['department']);
1609 + $applicant_profile['department'] = $requirements_input['department'];
1383 1610
1384 - $base_department = $_POST['department'];
1611 + $base_department = $requirements_input['department'];
1385 1612 if ( !in_array( $base_department, array('residential-sales', 'residential-lettings', 'commercial') ) )
1386 1613 {
1387 1614 $base_department = ph_get_custom_department_based_on($base_department);
1388 1615 }
@@ -1388,9 +1615,9 @@
1388 1615 }
1389 1616
1390 1617 if ( $base_department == 'residential-sales' )
1391 1618 {
1392 - $price = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_price']));
1619 + $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_price']);
1393 1620
1394 1621 $applicant_profile['max_price'] = $price;
1395 1622
1396 1623 // Not used yet but could be if introducing currencies in the future.
@@ -1398,11 +1625,11 @@
1398 1625
1399 1626 $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
1400 1627 $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
1401 1628
1402 - if ( $percentage_lower != '' && $percentage_higher != '' && $_POST['maximum_price'] != '' && $_POST['maximum_price'] != 0 )
1629 + if ( $percentage_lower != '' && $percentage_higher != '' && $requirements_input['maximum_price'] != '' && $requirements_input['maximum_price'] != 0 )
1403 1630 {
1404 - $price = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_price']));
1631 + $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_price']);
1405 1632 $applicant_profile['match_price_range_lower'] = $price - ( $price * ( $percentage_lower / 100 ) );
1406 1633 $applicant_profile['match_price_range_lower_actual'] = $price - ( $price * ( $percentage_lower / 100 ) );
1407 1634
1408 1635 $applicant_profile['match_price_range_higher'] = $price + ( $price * ( $percentage_higher / 100 ) );
@@ -1410,9 +1637,9 @@
1410 1637 }
1411 1638 }
1412 1639 elseif ( $base_department == 'residential-lettings' )
1413 1640 {
1414 - $price = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_rent']));
1641 + $price = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_rent']);
1415 1642
1416 1643 $applicant_profile['max_rent'] = $price;
1417 1644 $applicant_profile['rent_frequency'] = 'pcm';
1418 1645 $price_actual = $price; // Stored in pcm
@@ -1420,14 +1647,14 @@
1420 1647 }
1421 1648
1422 1649 if ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' )
1423 1650 {
1424 - $beds = preg_replace("/[^0-9]/", '', ph_clean($_POST['minimum_bedrooms']));
1651 + $beds = preg_replace("/[^0-9]/", '', $requirements_input['minimum_bedrooms']);
1425 1652 $applicant_profile['min_beds'] = $beds;
1426 1653
1427 1654 if ( isset($_POST['property_type']) && !empty($_POST['property_type']) )
1428 1655 {
1429 - $applicant_profile['property_types'] = is_array(ph_clean($_POST['property_type'])) ? ph_clean($_POST['property_type']) : array(ph_clean($_POST['property_type']));
1656 + $applicant_profile['property_types'] = $requirements_input['property_type'];
1430 1657 }
1431 1658 }
1432 1659
1433 1660 if ( $base_department == 'commercial' )
@@ -1432,48 +1659,48 @@
1432 1659
1433 1660 if ( $base_department == 'commercial' )
1434 1661 {
1435 1662 $available_as = array();
1436 - if ( isset($_POST['available_as_sale']) && $_POST['available_as_sale'] == 'yes' )
1663 + if ( isset($_POST['available_as_sale']) && $requirements_input['available_as_sale'] == 'yes' )
1437 1664 {
1438 1665 $available_as[] = 'sale';
1439 1666 }
1440 - if ( isset($_POST['available_as_rent']) && $_POST['available_as_rent'] == 'yes' )
1667 + if ( isset($_POST['available_as_rent']) && $requirements_input['available_as_rent'] == 'yes' )
1441 1668 {
1442 1669 $available_as[] = 'rent';
1443 1670 }
1444 1671 $applicant_profile['available_as'] = $available_as;
1445 1672
1446 - $floor_area = preg_replace("/[^0-9.]/", '', ph_clean($_POST['minimum_floor_area']));
1673 + $floor_area = preg_replace("/[^0-9.]/", '', $requirements_input['minimum_floor_area']);
1447 1674 $applicant_profile['min_floor_area'] = $floor_area;
1448 1675 $applicant_profile['min_floor_area_actual'] = $floor_area;
1449 1676
1450 - $floor_area = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_floor_area']));
1677 + $floor_area = preg_replace("/[^0-9.]/", '', $requirements_input['maximum_floor_area']);
1451 1678 $applicant_profile['max_floor_area'] = $floor_area;
1452 1679 $applicant_profile['max_floor_area_actual'] = $floor_area;
1453 1680
1454 1681 if ( isset($_POST['commercial_property_type']) && !empty($_POST['commercial_property_type']) )
1455 1682 {
1456 - $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']));
1683 + $applicant_profile['commercial_property_types'] = $requirements_input['commercial_property_type'];
1457 1684 }
1458 1685 }
1459 1686
1460 1687 if ( isset($_POST['location']) && !empty($_POST['location']) )
1461 1688 {
1462 - $applicant_profile['locations'] = is_array(ph_clean($_POST['location'])) ? ph_clean($_POST['location']) : array(ph_clean($_POST['location']));
1689 + $applicant_profile['locations'] = $requirements_input['location'];
1463 1690 }
1464 1691
1465 1692 if ( isset($_POST['location_text']) && !empty($_POST['location_text']) )
1466 1693 {
1467 - $applicant_profile['location_text'] = ph_clean($_POST['location_text']);
1694 + $applicant_profile['location_text'] = $requirements_input['location_text'];
1468 1695 }
1469 1696
1470 - $applicant_profile['notes'] = ( ( isset($_POST['additional_requirements']) ) ? sanitize_textarea_field($_POST['additional_requirements']) : '' );
1697 + $applicant_profile['notes'] = $requirements_input['additional_requirements'];
1471 1698
1472 1699 $applicant_profile['send_matching_properties'] = 'yes';
1473 1700 //$applicant_profile['auto_match_disabled'] = ''; // don't know what to do about this yet. Should probably look at global setting and reflect that
1474 1701
1475 - update_post_meta( $contact_post_id, '_applicant_profile_' . ( isset($_POST['profile_id']) && $_POST['profile_id'] != '' ? (int)$_POST['profile_id'] : '0' ), $applicant_profile );
1702 + update_post_meta( $contact_post_id, '_applicant_profile_' . $profile_id, wp_slash( $applicant_profile ) );
1476 1703
1477 1704 $return['success'] = true;
1478 1705
1479 1706 do_action( 'propertyhive_account_requirements_updated', $contact_post_id, $user_id );
@@ -1536,11 +1763,11 @@
1536 1763 public function load_existing_owner_contact() {
1537 1764
1538 1765 check_ajax_referer( 'load-existing-owner-contact', 'security' );
1539 1766
1540 - $contact_id = (int)$_POST['contact_id'];
1767 + $contact_id = isset( $_POST['contact_id'] ) && is_scalar( $_POST['contact_id'] ) ? absint( $_POST['contact_id'] ) : 0;
1541 1768
1542 - $contact = get_post($contact_id);
1769 + $contact = $contact_id > 0 && 'contact' === get_post_type( $contact_id ) ? get_post( $contact_id ) : null;
1543 1770
1544 1771 echo '<div id="existing-owner-details-' . esc_attr($contact_id) . '">';
1545 1772
1546 1773 if ( !is_null( $contact ) )
@@ -1615,9 +1842,11 @@
1615 1842 check_ajax_referer( 'search-contacts', 'security' );
1616 1843
1617 1844 $return = array();
1618 1845
1619 - $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'] ) ) : '';
1620 1849
1621 1850 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1622 1851 {
1623 1852 // Get all contacts that match the name
@@ -1622,25 +1851,28 @@
1622 1851 {
1623 1852 // Get all contacts that match the name
1624 1853 $args = array(
1625 1854 'post_type' => 'contact',
1855 + 'propertyhive_contact_search_keyword' => $keyword,
1626 1856 'nopaging' => true,
1627 1857 'post_status' => array( 'publish', 'private' ),
1628 1858 'fields' => 'ids'
1629 1859 );
1630 - if ( isset($_POST['contact_type']) && $_POST['contact_type'] != '' )
1860 + if ( '' !== $contact_type )
1631 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.
1632 1863 $args['meta_query'] = array(
1633 1864 array(
1634 1865 'key' => '_contact_types',
1635 - 'value' => ph_clean($_POST['contact_type']),
1866 + 'value' => $contact_type,
1636 1867 'compare' => 'LIKE',
1637 1868 )
1638 1869 );
1639 1870 }
1640 - if ( isset($_POST['exclude_ids']) && $_POST['exclude_ids'] != '' )
1871 + if ( '' !== $exclude_ids )
1641 1872 {
1642 - $args['post__not_in'] = explode('|', $_POST['exclude_ids']);
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 ) );
1643 1875 }
1644 1876
1645 1877 add_filter( 'posts_where', array( $this, 'search_contacts_where' ), 10, 2 );
1646 1878
@@ -1657,9 +1889,9 @@
1657 1889 $contact = new PH_Contact( get_the_ID() );
1658 1890
1659 1891 $return[] = array(
1660 1892 'ID' => get_the_ID(),
1661 - 'post_title' => get_the_title(get_the_ID()) . ( isset($_POST['contact_type']) && $_POST['contact_type'] == 'thirdparty' && $contact->company_name != '' && $contact->company_name != get_the_title(get_the_ID()) ? ' (' . $contact->company_name . ')' : '' ) ,
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 . ')' : '' ) ,
1662 1894 'address_name_number' => $contact->_address_name_number,
1663 1895 'address_street' => $contact->_address_street,
1664 1896 'address_two' => $contact->_address_two,
1665 1897 'address_three' => $contact->_address_three,
@@ -1686,10 +1918,14 @@
1686 1918 public function search_contacts_where( $where, $wp_query )
1687 1919 {
1688 1920 global $wpdb;
1689 1921
1690 - $where .= ' AND ' . $wpdb->posts . '.post_title LIKE \'%' . esc_sql( $wpdb->esc_like( ph_clean($_POST['keyword']) ) ) . '%\'';
1691 -
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 +
1692 1928 return $where;
1693 1929 }
1694 1930
1695 1931 /**
@@ -1702,9 +1938,9 @@
1702 1938 check_ajax_referer( 'search-properties', 'security' );
1703 1939
1704 1940 $return = array();
1705 1941
1706 - $keyword = ph_clean($_POST['keyword']);
1942 + $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
1707 1943
1708 1944 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1709 1945 {
1710 1946 // Get all contacts that match the name
@@ -1719,26 +1955,27 @@
1719 1955 array(
1720 1956 'relation' => 'OR',
1721 1957 array(
1722 1958 'key' => '_address_concatenated',
1723 - 'value' => ph_clean($_POST['keyword']),
1959 + 'value' => $keyword,
1724 1960 'compare' => 'LIKE'
1725 1961 ),
1726 1962 array(
1727 1963 'key' => '_reference_number',
1728 - 'value' => ph_clean($_POST['keyword']),
1964 + 'value' => $keyword,
1729 1965 'compare' => '='
1730 1966 ),
1731 1967 ),
1732 1968 );
1733 1969
1734 - if ( isset($_POST['department']) && $_POST['department'] != '' )
1970 + $department_input = isset( $_POST['department'] ) && is_string( $_POST['department'] ) ? sanitize_text_field( wp_unslash( $_POST['department'] ) ) : '';
1971 + if ( '' !== $department_input )
1735 1972 {
1736 1973 $departments_query = array(
1737 1974 'relation' => 'OR',
1738 1975 );
1739 1976
1740 - $explode_departments = explode("|", ph_clean($_POST['department']));
1977 + $explode_departments = explode("|", $department_input);
1741 1978 $new_departments = array();
1742 1979 foreach ( $explode_departments as $department )
1743 1980 {
1744 1981 $explode_department = explode("~", $department);
@@ -1773,8 +2010,9 @@
1773 2010 }
1774 2011
1775 2012 if ( !empty($meta_query) )
1776 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.
1777 2015 $args['meta_query'] = $meta_query;
1778 2016 }
1779 2017
1780 2018 $property_query = new WP_Query( $args );
@@ -1833,9 +2071,9 @@
1833 2071 check_ajax_referer( 'search-negotiators', 'security' );
1834 2072
1835 2073 $return = array();
1836 2074
1837 - $keyword = ph_clean($_POST['keyword']);
2075 + $keyword = isset( $_POST['keyword'] ) && is_string( $_POST['keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) : '';
1838 2076
1839 2077 if ( !empty( $keyword ) && strlen( $keyword ) > 2 )
1840 2078 {
1841 2079 // Get all contacts that match the name
@@ -1842,8 +2080,9 @@
1842 2080 $args = array(
1843 2081 'number' => 9999,
1844 2082 'search' => $keyword . '*',
1845 2083 'orderby' => 'display_name',
2084 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy Property Negotiator compatibility filter; existing role filters depend on this exact public hook name.
1846 2085 'role__not_in' => apply_filters( 'property_negotiator_exclude_roles', array('property_hive_contact', 'subscriber') )
1847 2086 );
1848 2087
1849 2088 $args = apply_filters( 'propertyhive_negotiators_query', $args );
@@ -1883,13 +2122,17 @@
1883 2122
1884 2123 if ( ! current_user_can( 'manage_propertyhive' ) )
1885 2124 wp_die( esc_html(__( 'You do not have permission to manage notes', 'propertyhive' )), 403 );
1886 2125
1887 - $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 + }
1888 2130
1889 2131 if ( $post_id > 0 ) {
1890 2132
1891 - $note = trim( stripslashes( $_POST['note'] ) );
2133 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Rich mention spans are converted to the established text token below, then all HTML is stripped before storage.
2134 + $note = trim( wp_unslash( $_POST['note'] ) );
1892 2135
1893 2136 $pattern = '/<span [^>]*data-post-id="(\d+)"[^>]*>([^<]*)<\/span>/i';
1894 2137 $replacement = function($matches) {
1895 2138 $post_id = $matches[1];
@@ -1899,9 +2142,9 @@
1899 2142 $note = preg_replace_callback($pattern, $replacement, $note);
1900 2143
1901 2144 $note = str_replace( array('<br>', '<br />'), "\n", $note );
1902 2145
1903 - $note = strip_tags( $note );
2146 + $note = wp_strip_all_tags( $note );
1904 2147
1905 2148 // Add note/comment to property
1906 2149 $comment = array(
1907 2150 'note_type' => 'note',
@@ -1920,13 +2163,13 @@
1920 2163 $comment = get_comment($comment_id);
1921 2164 ?>
1922 2165 <li rel="<?php echo absint( $comment_id ) ; ?>" class="note">
1923 2166 <div class="note_content">
1924 - <?php echo wpautop( wptexturize( wp_kses_post( $note ) ) ); ?>
2167 + <?php echo wp_kses_post( wpautop( wptexturize( wp_kses_post( $note ) ) ) ); ?>
1925 2168 </div>
1926 2169 <p class="meta">
1927 - <abbr class="exact-date" title="<?php echo esc_attr($comment->comment_date_gmt); ?> GMT"><?php printf( __( '%s ago', 'propertyhive' ), human_time_diff( strtotime( $comment->comment_date_gmt ), current_time( 'timestamp', 1 ) ) ); ?></abbr>
1928 - <?php if ( $comment->comment_author !== __( 'Property Hive', 'propertyhive' ) ) printf( ' ' . __( 'by %s', 'propertyhive' ), $comment->comment_author ); ?>
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 ) ); ?>
1929 2172 <a href="#" class="delete_note"><?php echo esc_html(__( 'Delete', 'propertyhive' )); ?></a>
1930 2173 </p>
1931 2174 </li>
1932 2175 <?php
@@ -1946,9 +2189,13 @@
1946 2189
1947 2190 if ( ! current_user_can( 'manage_propertyhive' ) )
1948 2191 wp_send_json_error( __( 'You do not have permission to manage notes', 'propertyhive' ), 403 );
1949 2192
1950 - $note_id = (int)$_POST['note_id'];
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 + }
1951 2198
1952 2199 if ( $note_id > 0 ) {
1953 2200 wp_delete_comment( $note_id );
1954 2201
@@ -1967,9 +2214,13 @@
1967 2214
1968 2215 if ( ! current_user_can( 'manage_propertyhive' ) )
1969 2216 wp_send_json_error( __( 'You do not have permission to manage notes', 'propertyhive' ), 403 );
1970 2217
1971 - $note_id = (int)$_POST['note_id'];
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 + }
1972 2223
1973 2224 if ( $note_id > 0 ) {
1974 2225
1975 2226 $comment = get_comment($note_id);
@@ -1974,9 +2225,9 @@
1974 2225
1975 2226 $comment = get_comment($note_id);
1976 2227 $comment_content = @unserialize($comment->comment_content, ['allowed_classes' => false]);
1977 2228
1978 - if ( $comment_content !== false )
2229 + if ( is_array( $comment_content ) )
1979 2230 {
1980 2231 if ( isset($comment_content['pinned']))
1981 2232 {
1982 2233 unset($comment_content['pinned']);
@@ -1986,9 +2237,12 @@
1986 2237 $comment_content['pinned'] = '1';
1987 2238 }
1988 2239 }
1989 2240
1990 - wp_update_comment( array('comment_ID' => $_POST['note_id'], 'comment_content' => serialize($comment_content)) );
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 ) ) ) );
1991 2245
1992 2246 wp_send_json_success();
1993 2247 }
1994 2248
@@ -2003,11 +2257,15 @@
2003 2257
2004 2258 if ( ! current_user_can( 'manage_propertyhive' ) )
2005 2259 wp_die( esc_html(__( 'You do not have permission to manage notes', 'propertyhive' )), 403 );
2006 2260
2007 - $post = get_post((int)$_POST['post_id']);
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 + }
2008 2266
2009 - $section = $_POST['section'];
2267 + $section = isset( $_POST['section'] ) && is_string( $_POST['section'] ) ? sanitize_text_field( wp_unslash( $_POST['section'] ) ) : '';
2010 2268 include( PH()->plugin_path() . '/includes/admin/views/html-display-notes.php' );
2011 2269
2012 2270 // Quit out
2013 2271 die();
@@ -2021,11 +2279,15 @@
2021 2279
2022 2280 if ( ! current_user_can( 'manage_propertyhive' ) )
2023 2281 wp_die( esc_html(__( 'You do not have permission to manage notes', 'propertyhive' )), 403 );
2024 2282
2025 - $post = get_post((int)$_POST['post_id']);
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 + }
2026 2288
2027 - $section = $_POST['section'];
2289 + $section = isset( $_POST['section'] ) && is_string( $_POST['section'] ) ? sanitize_text_field( wp_unslash( $_POST['section'] ) ) : '';
2028 2290 include( PH()->plugin_path() . '/includes/admin/views/html-display-notes.php' );
2029 2291
2030 2292 // Quit out
2031 2293 die();
@@ -2039,9 +2301,9 @@
2039 2301
2040 2302 if ( ! current_user_can( 'manage_propertyhive' ) )
2041 2303 wp_die( esc_html(__( 'You do not have permission to manage notes', 'propertyhive' )), 403 );
2042 2304
2043 - $query = sanitize_text_field($_POST['query']);
2305 + $query = isset( $_POST['query'] ) && is_string( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
2044 2306
2045 2307 $mentions = array();
2046 2308
2047 2309 // Get contacts
@@ -2095,8 +2357,9 @@
2095 2357 $args = array(
2096 2358 'post_type' => 'property',
2097 2359 'posts_per_page' => 10,
2098 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.
2099 2362 'meta_query' => array(
2100 2363 'relation' => 'OR',
2101 2364 array(
2102 2365 'key' => '_address_concatenated',
@@ -2156,9 +2419,10 @@
2156 2419 // Validate
2157 2420 $errors = array();
2158 2421 $form_controls = array();
2159 2422
2160 - 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'] ) )
2161 2425 {
2162 2426 $errors[] = __( 'Property ID is a required field and must be supplied when making an enquiry', 'propertyhive' );
2163 2427 }
2164 2428 else
@@ -2166,9 +2430,10 @@
2166 2430 //$post = get_post((int)$_POST['property_id']);
2167 2431
2168 2432 $form_controls = ph_get_property_enquiry_form_fields();
2169 2433
2170 - $form_controls = apply_filters( 'propertyhive_property_enquiry_form_fields', $form_controls, ph_clean($_POST['property_id']) );
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'] ) ) );
2171 2436 }
2172 2437
2173 2438 foreach ( $form_controls as $key => $control )
2174 2439 {
@@ -2174,14 +2439,16 @@
2174 2439 {
2175 2440 if ( isset( $control ) && isset( $control['required'] ) && $control['required'] === TRUE )
2176 2441 {
2177 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.
2178 2444 if ( ! isset( $_POST[$key] ) || ( isset( $_POST[$key] ) && empty( $_POST[$key] ) ) )
2179 2445 {
2180 2446 $errors[] = __( 'Missing required field', 'propertyhive' ) . ': ' . $key;
2181 2447 }
2182 2448 }
2183 - 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] ) ) ) )
2184 2451 {
2185 2452 $errors[] = __( 'Invalid email address provided', 'propertyhive' ) . ': ' . $key;
2186 2453 }
2187 2454 if ( in_array( $key, array('recaptcha', 'recaptcha-v3') ) )
@@ -2190,9 +2457,10 @@
2190 2457 }
2191 2458 if ( $key == 'hCaptcha' )
2192 2459 {
2193 2460 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
2194 - $response = isset( $_POST['h-captcha-response'] ) ? ph_clean($_POST['h-captcha-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'] ) ) : '';
2195 2463
2196 2464 $response = wp_remote_post(
2197 2465 'https://hcaptcha.com/siteverify',
2198 2466 array(
@@ -2227,12 +2495,13 @@
2227 2495 }
2228 2496 if ( $key == 'turnstile' )
2229 2497 {
2230 2498 $secret = isset( $control['secret'] ) ? $control['secret'] : '';
2231 - $response = isset( $_POST['cf-turnstile-response'] ) ? ph_clean($_POST['cf-turnstile-response']) : '';
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'] ) ) : '';
2232 2501
2233 2502 $response = wp_remote_post(
2234 - 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
2503 + 'https://challenges.cloudflare.com/turnstile/v0/siteverify', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Server-side CAPTCHA token verification API.
2235 2504 array(
2236 2505 'method' => 'POST',
2237 2506 'headers' => array(
2238 2507 'Content-Type' => 'application/x-www-form-urlencoded',
@@ -2269,10 +2538,12 @@
2269 2538
2270 2539 if (
2271 2540 get_option( 'propertyhive_property_enquiry_form_disclaimer', '' ) != '' &&
2272 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.
2273 2543 !isset( $_POST['disclaimer'] ) ||
2274 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.
2275 2546 isset( $_POST['disclaimer'] ) && empty( $_POST['disclaimer'] )
2276 2547 )
2277 2548 )
2278 2549 )
@@ -2314,12 +2585,19 @@
2314 2585 }
2315 2586 }*/
2316 2587
2317 2588 // Passed validation
2318 - $property_ids = array_filter( array_map( 'absint', explode( '|', sanitize_text_field( wp_unslash( $_POST['property_id'] ) ) ) ) );
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 + }
2319 2597 foreach ( $property_ids as $property_id )
2320 2598 {
2321 - if ( get_post_type($property_id) !== 'property' )
2599 + if ( get_post_type( $property_id ) !== 'property' || ! propertyhive_is_post_publicly_viewable( $property_id ) )
2322 2600 {
2323 2601 $errors[] = __( 'Invalid property supplied', 'propertyhive' );
2324 2602 break;
2325 2603 }
@@ -2410,9 +2688,9 @@
2410 2688 $message .= ( count($property_ids) > 1 ? __( 'Properties', 'propertyhive' ) : __( 'Property', 'propertyhive' ) ) . ":\n";
2411 2689 foreach ( $property_ids as $property_id )
2412 2690 {
2413 2691 $property = new PH_Property((int)$property_id);
2414 - $message .= apply_filters( 'propertyhive_property_enquiry_property_output', $property->get_formatted_full_address() . "\n" . html_entity_decode(strip_tags($property->get_formatted_price())) . "\n" . get_permalink( (int)$property_id ), (int)$property_id ) . "\n\n";
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";
2415 2693 }
2416 2694
2417 2695 unset($form_controls['action']);
2418 2696 unset($_POST['action']);
@@ -2425,11 +2703,12 @@
2425 2703 if ( isset($control['type']) && in_array($control['type'], array('html', 'recaptcha', 'recaptcha-v3', 'hCaptcha', 'turnstile')) ) { continue; }
2426 2704
2427 2705 $label = ( isset($control['label']) ) ? $control['label'] : $key;
2428 2706 $label = ( isset($control['email_label']) ) ? $control['email_label'] : $label;
2429 - $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] ) ) : '';
2430 2709
2431 - $message .= strip_tags($label) . ": " . strip_tags($value) . "\n";
2710 + $message .= wp_strip_all_tags($label) . ": " . wp_strip_all_tags($value) . "\n";
2432 2711 }
2433 2712
2434 2713 if (
2435 2714 apply_filters('propertyhive_enquiry_email_show_manage_link', true) &&
@@ -2453,13 +2732,16 @@
2453 2732 }
2454 2733 if ( $from_email_address == '' )
2455 2734 {
2456 2735 // Should never get here
2457 - $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'] ) ) : '';
2458 2738 }
2459 2739
2460 2740 $headers = array();
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.
2461 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.
2462 2744 ? sanitize_text_field( wp_unslash( $_POST['name'] ) )
2463 2745 : '';
2464 2746
2465 2747 $name = str_replace( array( "\r", "\n" ), '', $name );
@@ -2474,10 +2756,12 @@
2474 2756 {
2475 2757 $headers[] = sprintf( 'From: <%s>', $from_email_address );
2476 2758 }
2477 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.
2478 2761 if ( isset($_POST['email_address']) )
2479 2762 {
2763 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2480 2764 $reply_to = sanitize_email(wp_unslash($_POST['email_address']));
2481 2765
2482 2766 if ( is_email($reply_to) )
2483 2767 {
@@ -2518,10 +2802,12 @@
2518 2802 else
2519 2803 {
2520 2804 $title = __( 'Multiple Property Enquiry', 'propertyhive' );
2521 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.
2522 2807 if ( isset($_POST['name']) && ! empty($_POST['name']) )
2523 2808 {
2809 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2524 2810 $title .= ' ' . __( 'from', 'propertyhive' ) . ' ' . ph_clean(wp_unslash($_POST['name']));
2525 2811 }
2526 2812
2527 2813 $enquiry_post = array(
@@ -2540,24 +2826,34 @@
2540 2826 add_post_meta( $enquiry_post_id, '_source', 'website' );
2541 2827 add_post_meta( $enquiry_post_id, '_negotiator_id', '' );
2542 2828 add_post_meta( $enquiry_post_id, '_office_id', $office_id );
2543 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.
2544 2831 foreach ($_POST as $key => $value)
2545 2832 {
2546 - 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 ) )
2547 2837 {
2838 + continue;
2839 + }
2840 +
2841 + if ( $meta_key == 'property_id' )
2842 + {
2548 2843 foreach ( $property_ids as $property_id )
2549 2844 {
2550 - add_post_meta( $enquiry_post_id, $key, (int)$property_id );
2845 + add_post_meta( $enquiry_post_id, $meta_key, (int)$property_id );
2551 2846 }
2552 2847 }
2553 2848 else
2554 2849 {
2555 - add_post_meta( $enquiry_post_id, $key, sanitize_textarea_field(wp_unslash($value)) );
2850 + add_post_meta( $enquiry_post_id, $meta_key, sanitize_textarea_field(wp_unslash($value)) );
2556 2851 }
2557 2852 }
2558 2853 }
2559 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.
2560 2856 do_action('propertyhive_property_enquiry_sent', $_POST, $to, $enquiry_post_id);
2561 2857
2562 2858 // Send auto-responder
2563 2859 if ( get_option( 'propertyhive_enquiry_auto_responder', '' ) == 'yes' )
@@ -2562,8 +2858,9 @@
2562 2858 // Send auto-responder
2563 2859 if ( get_option( 'propertyhive_enquiry_auto_responder', '' ) == 'yes' )
2564 2860 {
2565 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.
2566 2863 PH()->email->send_enquiry_auto_responder( $_POST );
2567 2864 }
2568 2865 }
2569 2866 }
@@ -2581,10 +2878,12 @@
2581 2878 public function create_contact_from_enquiry()
2582 2879 {
2583 2880 global $post;
2584 2881
2585 - $enquiry_post_id = ( (isset($_POST['post_id'])) ? (int)$_POST['post_id'] : '' );
2586 - $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'] ) ) : '';
2587 2886
2588 2887 if ( ! wp_verify_nonce( $nonce, 'create-contact-from-enquiry-nonce-' . $enquiry_post_id ) )
2589 2888 {
2590 2889 // This nonce is not valid.
@@ -2656,9 +2955,9 @@
2656 2955
2657 2956 $postdata = array(
2658 2957 'post_excerpt' => '',
2659 2958 'post_content' => '',
2660 - 'post_title' => utf8_encode(wp_strip_all_tags( $name )),
2959 + 'post_title' => wp_strip_all_tags( $name ),
2661 2960 'post_status' => 'publish',
2662 2961 'post_type' => 'contact',
2663 2962 'ping_status' => 'closed',
2664 2963 'comment_status' => 'closed',
@@ -2813,15 +3112,21 @@
2813 3112 check_ajax_referer( 'contact-save-validation', 'security' );
2814 3113
2815 3114 $this->json_headers();
2816 3115
2817 - 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;
2818 3123
2819 3124 $return = array('errors' => array());
2820 3125
2821 - if ( isset($_email_address) && $_email_address != '' )
3126 + if ( '' !== $email_address_input )
2822 3127 {
2823 - $email_addresses = explode( ",", $_email_address );
3128 + $email_addresses = explode( ",", $email_address_input );
2824 3129
2825 3130 foreach ( $email_addresses as $email_address )
2826 3131 {
2827 3132 $email_address = trim( $email_address );
@@ -2836,8 +3141,9 @@
2836 3141 'post_type' => 'contact',
2837 3142 'post_status' => 'any',
2838 3143 'posts_per_page' => 1,
2839 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.
2840 3146 'meta_query' => array(
2841 3147 'relation' => 'OR',
2842 3148 array(
2843 3149 'key' => '_email_address',
@@ -2856,11 +3162,12 @@
2856 3162 'compare' => 'LIKE'
2857 3163 )
2858 3164 )
2859 3165 );
2860 - if ( isset($post_ID) && $post_ID != '' )
3166 + if ( $contact_id )
2861 3167 {
2862 - $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 );
2863 3170 }
2864 3171
2865 3172 $contact_query = new WP_Query( $args );
2866 3173
@@ -2869,9 +3176,10 @@
2869 3176 while ( $contact_query->have_posts() )
2870 3177 {
2871 3178 $contact_query->the_post();
2872 3179
2873 - $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 );
2874 3182 }
2875 3183 }
2876 3184 }
2877 3185 }
@@ -2891,9 +3199,9 @@
2891 3199 echo json_encode( $return );
2892 3200 die();
2893 3201 }
2894 3202
2895 - if ( !isset( $_POST['contact_ids'] ) || empty( $_POST['contact_ids'] ) || !isset( $_POST['primary_contact_id'] ) || empty( $_POST['primary_contact_id'] ) )
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'] ) )
2896 3204 {
2897 3205 $return = array('error' => 'Invalid parameters received');
2898 3206 echo json_encode( $return );
2899 3207 die();
@@ -2898,13 +3206,13 @@
2898 3206 echo json_encode( $return );
2899 3207 die();
2900 3208 }
2901 3209
2902 - $contacts_to_merge = array_filter( array_map( 'absint', explode( '|', $_POST['contact_ids'] ) ) );
3210 + $contacts_to_merge = array_values( array_unique( array_filter( array_map( 'absint', explode( '|', sanitize_text_field( wp_unslash( $_POST['contact_ids'] ) ) ) ) ) ) );
2903 3211
2904 3212 $primary_contact_id = absint( wp_unslash( $_POST['primary_contact_id'] ) );
2905 3213
2906 - if ( !is_array($contacts_to_merge) || !in_array( $primary_contact_id, $contacts_to_merge ) )
3214 + if ( count( $contacts_to_merge ) < 2 || !in_array( $primary_contact_id, $contacts_to_merge, true ) )
2907 3215 {
2908 3216 $return = array('error' => 'Invalid Contact IDs received');
2909 3217 echo json_encode( $return );
2910 3218 die();
@@ -2916,9 +3224,9 @@
2916 3224 echo json_encode( $return );
2917 3225 die();
2918 3226 }
2919 3227
2920 - if ( !current_user_can( 'edit_post', $primary_contact_id ) )
3228 + if ( !current_user_can( 'manage_propertyhive' ) || !current_user_can( 'edit_post', $primary_contact_id ) )
2921 3229 {
2922 3230 $return = array('error' => 'Insufficient permissions for primary contact');
2923 3231 echo json_encode( $return );
2924 3232 die();
@@ -2944,9 +3252,9 @@
2944 3252
2945 3253 // Remove primary from list
2946 3254 unset($contacts_to_merge[array_search($primary_contact_id, $contacts_to_merge)]);
2947 3255
2948 - include_once( 'includes/class-ph-admin-merge-contacts.php' );
3256 + include_once PH()->plugin_path() . '/includes/admin/class-ph-admin-merge-contacts.php';
2949 3257 $ph_admin_merge_contacts = new PH_Admin_Merge_Contacts();
2950 3258 $ph_admin_merge_contacts->do_merge( $primary_contact_id, $contacts_to_merge );
2951 3259
2952 3260 echo json_encode( array('success' => true) );
@@ -3002,8 +3310,9 @@
3002 3310 $args = array(
3003 3311 'post_type' => 'viewing',
3004 3312 'fields' => 'ids',
3005 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.
3006 3315 'meta_query' => array(
3007 3316 array(
3008 3317 'key' => '_status',
3009 3318 'value' => 'carried_out'
@@ -3033,9 +3342,9 @@
3033 3342 $return[] = array(
3034 3343 'ID' => get_the_ID(),
3035 3344 'edit_link' => get_edit_post_link( get_the_ID() ),
3036 3345 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
3037 - '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 ))),
3038 3347 'property_id' => $property_id,
3039 3348 'property_address' => $property->get_formatted_full_address(),
3040 3349 'applicant_contact_id' => $applicant_contact_ids[0],
3041 3350 'applicant_name' => get_the_title( $applicant_contact_ids[0] ),
@@ -3061,8 +3370,9 @@
3061 3370 $args = array(
3062 3371 'post_type' => 'viewing',
3063 3372 'fields' => 'ids',
3064 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.
3065 3375 'meta_query' => array(
3066 3376 array(
3067 3377 'key' => '_status',
3068 3378 'value' => 'pending'
@@ -3068,9 +3378,9 @@
3068 3378 'value' => 'pending'
3069 3379 ),
3070 3380 array(
3071 3381 'key' => '_start_date_time',
3072 - 'value' => date("Y-m-d H:i:s"),
3382 + 'value' => gmdate("Y-m-d H:i:s"),
3073 3383 'compare' => '>='
3074 3384 ),
3075 3385 array(
3076 3386 'key' => '_negotiator_id',
@@ -3096,9 +3406,9 @@
3096 3406 $return[] = array(
3097 3407 'ID' => get_the_ID(),
3098 3408 'edit_link' => get_edit_post_link( get_the_ID() ),
3099 3409 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
3100 - '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 ))),
3101 3411 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
3102 3412 'title' => 'Viewing at ' . $property->get_formatted_full_address(),
3103 3413 );
3104 3414 }
@@ -3109,8 +3419,9 @@
3109 3419 $args = array(
3110 3420 'post_type' => 'appraisal',
3111 3421 'fields' => 'ids',
3112 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.
3113 3424 'meta_query' => array(
3114 3425 array(
3115 3426 'key' => '_status',
3116 3427 'value' => 'pending'
@@ -3116,9 +3427,9 @@
3116 3427 'value' => 'pending'
3117 3428 ),
3118 3429 array(
3119 3430 'key' => '_start_date_time',
3120 - 'value' => date("Y-m-d H:i:s"),
3431 + 'value' => gmdate("Y-m-d H:i:s"),
3121 3432 'compare' => '>='
3122 3433 ),
3123 3434 array(
3124 3435 'key' => '_negotiator_id',
@@ -3143,9 +3454,9 @@
3143 3454 $return[] = array(
3144 3455 'ID' => get_the_ID(),
3145 3456 'edit_link' => get_edit_post_link( get_the_ID() ),
3146 3457 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
3147 - '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 ))),
3148 3459 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
3149 3460 'title' => 'Appraisal at ' . $appraisal->get_formatted_full_address(),
3150 3461 );
3151 3462 }
@@ -3197,9 +3508,11 @@
3197 3508 $args = array(
3198 3509 'post_type' => 'key_date',
3199 3510 'fields' => 'ids',
3200 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.
3201 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.
3202 3515 'meta_key' => '_date_due',
3203 3516 'orderby' => 'meta_value',
3204 3517 'order' => 'ASC',
3205 3518 );
@@ -3274,8 +3587,9 @@
3274 3587
3275 3588 $args = array(
3276 3589 'post_type' => 'property',
3277 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.
3278 3592 'meta_query' => array(
3279 3593 array(
3280 3594 'key' => '_on_market',
3281 3595 'value' => 'yes'
@@ -3288,8 +3602,9 @@
3288 3602 );
3289 3603
3290 3604 if ( isset($_POST['post_id']) && !empty($_POST['post_id']) )
3291 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.
3292 3607 $args['post__not_in'] = array((int)$_POST['post_id']);
3293 3608 }
3294 3609
3295 3610 $property_query = new WP_Query($args);
@@ -3307,9 +3622,13 @@
3307 3622 public function osm_geocoding_request()
3308 3623 {
3309 3624 check_ajax_referer( 'osm_geocoding_request', 'security' );
3310 3625
3311 - $this->json_headers();
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'] ) );
3312 3631
3313 3632 $lat = '';
3314 3633 $lng = '';
3315 3634 $error = '';
@@ -3322,16 +3641,21 @@
3322 3641 if ( $last_ts && ($now - $last_ts) < 1 )
3323 3642 {
3324 3643 // Too soon: tell client to retry shortly
3325 3644 $error = 'Too many geocoding requests. Please wait a second and try again.';
3326 - json_encode(array('error' => $error));
3327 - wp_die();
3645 + wp_send_json( array( 'error' => $error ) );
3328 3646 }
3329 3647
3330 3648 // Set timestamp immediately to prevent stampedes
3331 3649 set_transient( $rate_key, $now );
3332 3650
3333 - $request_url = "https://nominatim.openstreetmap.org/search?format=json&limit=1&countrycodes=" . strtolower(ph_clean($_POST['country'])) . "&addressdetails=1&q=" . urlencode(ph_clean($_POST['address']));
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' );
3334 3658
3335 3659 $response = wp_remote_get(
3336 3660 $request_url,
3337 3661 array(
@@ -3344,17 +3668,15 @@
3344 3668
3345 3669 if ( is_wp_error( $response ))
3346 3670 {
3347 3671 $error = $response->get_error_message();
3348 - echo json_encode(array('error' => $error, 'lat' => $lat, 'lng' => $lng));
3349 - die();
3672 + wp_send_json( array( 'error' => $error, 'lat' => $lat, 'lng' => $lng ) );
3350 3673 }
3351 3674
3352 3675 if ( wp_remote_retrieve_response_code($response) !== 200 )
3353 3676 {
3354 - $error = wp_remote_retrieve_response_code($response) . ' response received when geocoding address ' . ph_clean($_POST['address']) . '. Error message: ' . wp_remote_retrieve_response_message($response);
3355 - echo json_encode(array('error' => $error, 'lat' => $lat, 'lng' => $lng));
3356 - die();
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 ) );
3357 3679 }
3358 3680
3359 3681 if ( is_array( $response ) )
3360 3682 {
@@ -3367,19 +3689,17 @@
3367 3689 $lng = $json[0]['lon'];
3368 3690 }
3369 3691 else
3370 3692 {
3371 - $error = 'No co-ordinates returned for the address provided ' . ph_clean($_POST['address']) . ': ' . print_r($body, true);
3693 + $error = 'No co-ordinates returned for the address provided ' . $address . ': ' . $body;
3372 3694 }
3373 3695 }
3374 3696 else
3375 3697 {
3376 - $error = 'Failed to parse JSON response from OSM Geocoding service: ' . print_r($response, true);
3698 + $error = 'Failed to parse JSON response from OSM Geocoding service: ' . wp_json_encode( $response );
3377 3699 }
3378 3700
3379 - echo json_encode(array('error' => $error, 'lat' => $lat, 'lng' => $lng));
3380 -
3381 - die();
3701 + wp_send_json( array( 'error' => $error, 'lat' => $lat, 'lng' => $lng ) );
3382 3702 }
3383 3703
3384 3704 public function get_property_marketing_statistics_meta_box()
3385 3705 {
@@ -3385,34 +3705,41 @@
3385 3705 {
3386 3706 check_ajax_referer( 'get_property_marketing_statistics_meta_box', 'security' );
3387 3707
3388 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 + }
3389 3713
3390 - echo '<div class="propertyhive_meta_box">';
3391 -
3392 - echo '<div class="options_group">';
3393 3714
3394 - $view_statistics = get_post_meta( (int)$_POST['post_id'], '_view_statistics', TRUE );
3715 +
3716 +
3717 + $view_statistics = get_post_meta( $post_id, '_view_statistics', TRUE );
3395 3718 if ( !is_array($view_statistics) )
3396 3719 {
3397 3720 $view_statistics = array();
3398 3721 }
3399 3722
3400 - $date_from = isset($_POST['statistics_date_from']) ? ph_clean($_POST['statistics_date_from']) : date("Y-m-d", strtotime('7 days ago'));
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'));
3401 3724 $date_from = strtotime($date_from);
3402 3725
3403 - $date_to = isset($_POST['statistics_date_to']) ? ph_clean($_POST['statistics_date_to']) : date("Y-m-d");
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");
3404 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 + }
3405 3731
3732 + echo '<div class="propertyhive_meta_box"><div class="options_group">';
3406 3733 $view_statistics_output = array();
3407 3734 $total_views = 0;
3408 3735
3409 3736 for ($i = $date_from; $i <= $date_to; $i += 86400)
3410 3737 {
3411 - if ( isset($view_statistics[date("Y-m-d", $i)]) )
3738 + if ( isset($view_statistics[gmdate("Y-m-d", $i)]) )
3412 3739 {
3413 - $view_statistics_output[] = array( $i * 1000, $view_statistics[date("Y-m-d", $i)] );
3414 - $total_views += $view_statistics[date("Y-m-d", $i)];
3740 + $view_statistics_output[] = array( $i * 1000, $view_statistics[gmdate("Y-m-d", $i)] );
3741 + $total_views += $view_statistics[gmdate("Y-m-d", $i)];
3415 3742 }
3416 3743 else
3417 3744 {
3418 3745 $view_statistics_output[] = array( $i * 1000, 0 );
@@ -3437,11 +3764,12 @@
3437 3764 global $post;
3438 3765
3439 3766 check_ajax_referer( 'appraisal-details-meta-box', 'security' );
3440 3767
3441 - $post = get_post((int)$_POST['appraisal_id']);
3768 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
3769 + $post = get_post( $post_id );
3442 3770
3443 - $appraisal = new PH_Appraisal((int)$_POST['appraisal_id']);
3771 + $appraisal = new PH_Appraisal( $post_id );
3444 3772
3445 3773 echo '<div class="propertyhive_meta_box">';
3446 3774
3447 3775 echo '<div class="options_group">';
@@ -3555,9 +3883,9 @@
3555 3883 public function get_appraisal_actions()
3556 3884 {
3557 3885 check_ajax_referer( 'appraisal-actions', 'security' );
3558 3886
3559 - $post_id = (int)$_POST['appraisal_id'];
3887 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
3560 3888
3561 3889 $status = get_post_meta( $post_id, '_status', TRUE );
3562 3890 $department = get_post_meta( $post_id, '_department', TRUE );
3563 3891
@@ -3588,9 +3916,9 @@
3588 3916 $actions[] = '<a
3589 3917 href="#action_panel_appraisal_email_owner_booking_confirmation_customise"
3590 3918 class="button appraisal-action"
3591 3919 style="width:100%; margin-bottom:7px; text-align:center"
3592 - >' . ( ( $owner_booking_confirmation_sent_at == '' ) ? esc_html(__('Email ' . $owner_or_landlord . ' Booking Confirmation', 'propertyhive')) : esc_html(__('Re-Email ' . $owner_or_landlord . ' Booking Confirmation', 'propertyhive') ) ) . '</a>';
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>';
3593 3921
3594 3922 $show_customise_confirmation_meta_boxes = true;
3595 3923 }
3596 3924 else
@@ -3598,12 +3926,12 @@
3598 3926 $actions[] = '<a
3599 3927 href="#action_panel_appraisal_email_owner_booking_confirmation"
3600 3928 class="button appraisal-action"
3601 3929 style="width:100%; margin-bottom:7px; text-align:center"
3602 - >' . ( ( $owner_booking_confirmation_sent_at == '' ) ? esc_html(__('Email ' . $owner_or_landlord . ' Booking Confirmation', 'propertyhive')) : esc_html(__('Re-Email ' . $owner_or_landlord . ' Booking Confirmation', 'propertyhive') )) . '</a>';
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>';
3603 3931 }
3604 3932
3605 - $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(date("jS F", strtotime($owner_booking_confirmation_sent_at))) . '</span>' : '' ) . '</div>';
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>';
3606 3934
3607 3935 $actions[] = '<hr>';
3608 3936 }
3609 3937
@@ -3702,8 +4030,9 @@
3702 4030 $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
3703 4031
3704 4032 if ( !empty($actions) )
3705 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.
3706 4035 echo implode("", $actions);
3707 4036 }
3708 4037 else
3709 4038 {
@@ -3817,9 +4146,9 @@
3817 4146 if ( $department == 'residential-sales' )
3818 4147 {
3819 4148 echo '<div class="form-field">
3820 4149
3821 - <label for="_price">' . esc_html(__( 'Valued Price (' . $currency_symbol . ')', 'propertyhive' )) . '</label>
4150 + <label for="_price">' . esc_html(/* translators: %s: Currency symbol. */ sprintf( __( 'Valued Price (%s)', 'propertyhive' ), $currency_symbol )) . '</label>
3822 4151
3823 4152 <input type="text" id="_price" name="_price" style="width:100%;" value="' . esc_attr(get_post_meta( $post_id, '_valued_price', TRUE )) . '">
3824 4153
3825 4154 </div>';
@@ -3828,9 +4157,9 @@
3828 4157 {
3829 4158 $rent_frequency = get_post_meta( $post_id, '_valued_rent_frequency', TRUE );
3830 4159 echo '<div class="form-field">
3831 4160
3832 - <label for="_price">' . esc_html(__( 'Valued Rent (' . $currency_symbol . ')', 'propertyhive' )) . '</label>
4161 + <label for="_price">' . esc_html(/* translators: %s: Currency symbol. */ sprintf( __( 'Valued Rent (%s)', 'propertyhive' ), $currency_symbol )) . '</label>
3833 4162
3834 4163 <input type="text" id="_price" name="_price" style="width:100%;" value="' . esc_attr(get_post_meta( $post_id, '_valued_rent', TRUE )) . '">
3835 4164
3836 4165 <select id="_rent_frequency" name="_rent_frequency" class="select" style="width:100%">
@@ -3897,35 +4226,57 @@
3897 4226 public function appraisal_carried_out()
3898 4227 {
3899 4228 check_ajax_referer( 'appraisal-actions', 'security' );
3900 4229
3901 - $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 + }
3902 4234
3903 4235 $status = get_post_meta( $post_id, '_status', TRUE );
3904 4236
3905 4237 if ( $status == 'pending' )
3906 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 + }
3907 4258 update_post_meta( $post_id, '_status', 'carried_out' );
3908 4259
3909 4260 if ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-sales' )
3910 4261 {
3911 - $price = preg_replace("/[^0-9.]/", '', ph_clean($_POST['price']));
4262 + $price = preg_replace("/[^0-9.]/", '', $valuation_input['price']);
3912 4263 update_post_meta( $post_id, '_valued_price', $price );
3913 4264 update_post_meta( $post_id, '_valued_price_actual', $price );
3914 4265 }
3915 4266 elseif ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-lettings' )
3916 4267 {
3917 - $rent = preg_replace("/[^0-9.]/", '', ph_clean($_POST['rent']));
4268 + $rent = preg_replace("/[^0-9.]/", '', $valuation_input['rent']);
3918 4269 update_post_meta( $post_id, '_valued_rent', $rent );
3919 4270
3920 - 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'] );
3921 4272
3922 - switch (ph_clean($_POST['rent_frequency']))
4273 + switch ($valuation_input['rent_frequency'])
3923 4274 {
3924 4275 case "pd": { $price = ($rent * 365) / 12; break; }
3925 4276 case "pppw":
3926 4277 {
3927 - $bedrooms = get_post_meta( $postID, '_bedrooms', true );
4278 + $bedrooms = get_post_meta( $post_id, '_bedrooms', true );
3928 4279 if ( ( $bedrooms !== FALSE && $bedrooms != 0 && $bedrooms != '' ) && apply_filters( 'propertyhive_pppw_to_consider_bedrooms', true ) == true )
3929 4280 {
3930 4281 $price = (($rent * 52) / 12) * $bedrooms;
3931 4282 }
@@ -3960,16 +4311,24 @@
3960 4311 public function appraisal_cancelled()
3961 4312 {
3962 4313 check_ajax_referer( 'appraisal-actions', 'security' );
3963 4314
3964 - $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 + }
3965 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 +
3966 4325 $status = get_post_meta( $post_id, '_status', TRUE );
3967 4326
3968 4327 if ( $status == 'pending' )
3969 4328 {
3970 4329 update_post_meta( $post_id, '_status', 'cancelled' );
3971 - update_post_meta( $post_id, '_cancelled_reason', sanitize_textarea_field( $_POST['cancelled_reason'] ) );
4330 + update_post_meta( $post_id, '_cancelled_reason', wp_slash( $reason ) );
3972 4331
3973 4332 // Add note/comment to appraisal
3974 4333 $comment = array(
3975 4334 'note_type' => 'action',
@@ -3987,9 +4346,12 @@
3987 4346 public function appraisal_won()
3988 4347 {
3989 4348 check_ajax_referer( 'appraisal-actions', 'security' );
3990 4349
3991 - $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 + }
3992 4354
3993 4355 $status = get_post_meta( $post_id, '_status', TRUE );
3994 4356
3995 4357 if ( $status == 'carried_out' )
@@ -4013,16 +4375,24 @@
4013 4375 public function appraisal_lost_reason()
4014 4376 {
4015 4377 check_ajax_referer( 'appraisal-actions', 'security' );
4016 4378
4017 - $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 + }
4018 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 +
4019 4389 $status = get_post_meta( $post_id, '_status', TRUE );
4020 4390
4021 4391 if ( $status == 'carried_out' )
4022 4392 {
4023 4393 update_post_meta( $post_id, '_status', 'lost' );
4024 - update_post_meta( $post_id, '_lost_reason', sanitize_textarea_field( $_POST['lost_reason'] ) );
4394 + update_post_meta( $post_id, '_lost_reason', wp_slash( $reason ) );
4025 4395
4026 4396 // Add note/comment to appraisal
4027 4397 $comment = array(
4028 4398 'note_type' => 'action',
@@ -4040,9 +4410,9 @@
4040 4410 public function appraisal_instructed()
4041 4411 {
4042 4412 check_ajax_referer( 'appraisal-actions', 'security' );
4043 4413
4044 - $post_id = (int)$_POST['appraisal_id'];
4414 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4045 4415
4046 4416 $status = get_post_meta( $post_id, '_status', TRUE );
4047 4417
4048 4418 if ( $status == 'won' )
@@ -4267,8 +4637,9 @@
4267 4637 // get appraisals where this is the owner and where not instructed
4268 4638 $args = array(
4269 4639 'post_type' => 'appraisal',
4270 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.
4271 4642 'meta_query' => array(
4272 4643 array(
4273 4644 'key' => '_property_owner_contact_id',
4274 4645 'value' => $owner_contact_id,
@@ -4315,9 +4686,9 @@
4315 4686 public function appraisal_email_owner_booking_confirmation()
4316 4687 {
4317 4688 check_ajax_referer( 'appraisal-actions', 'security' );
4318 4689
4319 - $post_id = (int)$_POST['appraisal_id'];
4690 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4320 4691
4321 4692 $appraisal = new PH_Appraisal($post_id);
4322 4693
4323 4694 $owner_contact_id = $appraisal->property_owner_contact_id;
@@ -4404,28 +4775,29 @@
4404 4775 }
4405 4776
4406 4777 $to = implode(",", $owner_emails);
4407 4778
4408 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_appraisal_owner_booking_confirmation_email_subject', '' );
4409 - $body = isset($_POST['body']) ? sanitize_textarea_field($_POST['body']) : get_option( 'propertyhive_appraisal_owner_booking_confirmation_email_body', '' );
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', '' );
4410 4781
4411 4782 $appraisal_date_timestamp = strtotime($appraisal->start_date_time);
4412 4783
4413 4784 $subject = str_replace('[property_address]', $appraisal->get_formatted_full_address(), $subject);
4414 4785 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
4415 - $subject = str_replace('[appraisal_time]', date("H:i", $appraisal_date_timestamp), $subject);
4416 - $subject = str_replace('[appraisal_date]', date("l jS F Y", $appraisal_date_timestamp), $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);
4417 4788 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
4418 4789 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
4419 4790 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
4420 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.
4421 4793 $subject = apply_filters( 'appraisal_owner_booking_confirmation_email_subject', $subject, $post_id );
4422 4794
4423 4795 $body = str_replace('[property_address]', $appraisal->get_formatted_full_address(), $body);
4424 4796 $body = str_replace('[owner_name]', $owner_names_string, $body);
4425 4797 $body = str_replace('[owner_dear]', $owner_dears_string, $body);
4426 - $body = str_replace('[appraisal_time]', date("H:i", $appraisal_date_timestamp), $body);
4427 - $body = str_replace('[appraisal_date]', date("l jS F Y", $appraisal_date_timestamp), $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);
4428 4800 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
4429 4801 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
4430 4802 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
4431 4803
@@ -4430,8 +4802,9 @@
4430 4802 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
4431 4803
4432 4804 $body = html_entity_decode($body);
4433 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.
4434 4807 $body = apply_filters( 'appraisal_owner_booking_confirmation_email_body', $body, $post_id );
4435 4808
4436 4809 $from = '';
4437 4810 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -4473,9 +4846,9 @@
4473 4846
4474 4847 PH_Comments::insert_note( $post_id, $comment );
4475 4848 }
4476 4849
4477 - update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', date("Y-m-d H:i:s") );
4850 + update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
4478 4851
4479 4852 wp_send_json_success();
4480 4853 }
4481 4854 else
@@ -4489,9 +4862,9 @@
4489 4862 public function appraisal_revert_pending()
4490 4863 {
4491 4864 check_ajax_referer( 'appraisal-actions', 'security' );
4492 4865
4493 - $post_id = (int)$_POST['appraisal_id'];
4866 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4494 4867
4495 4868 $status = get_post_meta( $post_id, '_status', TRUE );
4496 4869
4497 4870 if ( $status == 'carried_out' || $status == 'cancelled' )
@@ -4515,9 +4888,9 @@
4515 4888 public function appraisal_revert_carried_out()
4516 4889 {
4517 4890 check_ajax_referer( 'appraisal-actions', 'security' );
4518 4891
4519 - $post_id = (int)$_POST['appraisal_id'];
4892 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4520 4893
4521 4894 $status = get_post_meta( $post_id, '_status', TRUE );
4522 4895
4523 4896 if ( $status == 'won' || $status == 'lost' )
@@ -4541,9 +4914,9 @@
4541 4914 public function appraisal_revert_won()
4542 4915 {
4543 4916 check_ajax_referer( 'appraisal-actions', 'security' );
4544 4917
4545 - $post_id = (int)$_POST['appraisal_id'];
4918 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4546 4919
4547 4920 $status = get_post_meta( $post_id, '_status', TRUE );
4548 4921
4549 4922 if ( $status == 'instructed' )
@@ -4570,10 +4943,11 @@
4570 4943 check_ajax_referer( 'book-viewing', 'security' );
4571 4944
4572 4945 $this->json_headers();
4573 4946
4574 - // TO DO: Should do validation on server side also
4575 - 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)
4576 4950 {
4577 4951 $return = array('error' => 'No property selected');
4578 4952 echo json_encode( $return );
4579 4953 die();
@@ -4578,18 +4952,26 @@
4578 4952 echo json_encode( $return );
4579 4953 die();
4580 4954 }
4581 4955
4582 - $property = new PH_Property((int)$_POST['property_id']);
4956 + $property = new PH_Property( $property_id );
4583 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 + }
4584 4966 $applicant_contact_ids = array();
4585 4967
4586 4968 // Create applicant record if required
4587 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
4969 + if (empty($booking['applicant_ids']) && !empty($booking['applicant_name']))
4588 4970 {
4589 4971 // Need to create contact/applicant
4590 4972 $contact_post = array(
4591 - 'post_title' => ph_clean($_POST['applicant_name']),
4973 + 'post_title' => $booking['applicant_name'],
4592 4974 'post_content' => '',
4593 4975 'post_type' => 'contact',
4594 4976 'post_status' => 'publish',
4595 4977 'comment_status' => 'closed',
@@ -4596,9 +4978,9 @@
4596 4978 'ping_status' => 'closed',
4597 4979 );
4598 4980
4599 4981 // Insert the post into the database
4600 - $contact_post_id = wp_insert_post( $contact_post );
4982 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
4601 4983
4602 4984 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
4603 4985 {
4604 4986 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -4607,24 +4989,24 @@
4607 4989 }
4608 4990
4609 4991 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
4610 4992
4611 - $email_address = isset($_POST['applicant_email_address']) ? sanitize_email($_POST['applicant_email_address']) : '';
4612 - $telephone_number = isset($_POST['applicant_telephone_number']) ? sanitize_text_field($_POST['applicant_telephone_number']) : '';
4993 + $email_address = sanitize_email( $booking['applicant_email_address'] );
4994 + $telephone_number = $booking['applicant_telephone_number'];
4613 4995 update_post_meta( $contact_post_id, '_email_address', $email_address );
4614 - update_post_meta( $contact_post_id, '_telephone_number', $telephone_number );
4996 + update_post_meta( $contact_post_id, '_telephone_number', wp_slash( $telephone_number ) );
4615 4997 update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean( ph_clean_telephone_number($telephone_number) ) );
4616 4998
4617 - if ( isset($_POST['applicant_address']) && !empty(sanitize_textarea_field($_POST['applicant_address'])) )
4999 + if ( '' !== $booking['applicant_address'] )
4618 5000 {
4619 - $address = ph_split_address_into_fields( sanitize_textarea_field($_POST['applicant_address']) );
5001 + $address = ph_split_address_into_fields( $booking['applicant_address'] );
4620 5002
4621 - update_post_meta( $contact_post_id, '_address_name_number', $address['address_name_number'] );
4622 - update_post_meta( $contact_post_id, '_address_street', $address['address_street'] );
4623 - update_post_meta( $contact_post_id, '_address_two', $address['address_two'] );
4624 - update_post_meta( $contact_post_id, '_address_three', $address['address_three'] );
4625 - update_post_meta( $contact_post_id, '_address_four', $address['address_four'] );
4626 - update_post_meta( $contact_post_id, '_address_postcode', $address['address_postcode'] );
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'] ) );
4627 5009 update_post_meta( $contact_post_id, '_address_country', get_option( 'propertyhive_default_country', 'GB' ) );
4628 5010 }
4629 5011
4630 5012 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
@@ -4632,20 +5014,12 @@
4632 5014
4633 5015 $applicant_contact_ids[] = $contact_post_id;
4634 5016 }
4635 5017
4636 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
5018 + if (!empty($booking['applicant_ids']) && empty($booking['applicant_name']))
4637 5019 {
4638 5020 // This is an existing contact
4639 - if ( !is_array($_POST['applicant_ids']) )
4640 - {
4641 - $_POST['applicant_ids'] = array(ph_clean($_POST['applicant_ids']));
4642 - }
4643 -
4644 - foreach ( $_POST['applicant_ids'] as $applicant_id )
4645 - {
4646 - $applicant_contact_ids[] = (int)$applicant_id;
4647 - }
5021 + $applicant_contact_ids = $booking['applicant_ids'];
4648 5022 }
4649 5023
4650 5024 $applicant_contact_ids = array_unique($applicant_contact_ids);
4651 5025
@@ -4730,11 +5104,11 @@
4730 5104 echo json_encode( $return );
4731 5105 die();
4732 5106 }
4733 5107
4734 - add_post_meta( $viewing_post_id, '_start_date_time', ph_clean($_POST['start_date']) . ' ' . ph_clean($_POST['start_time']) );
5108 + add_post_meta( $viewing_post_id, '_start_date_time', $booking['start_date'] . ' ' . $booking['start_time'] );
4735 5109 add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
4736 - add_post_meta( $viewing_post_id, '_property_id', (int)$_POST['property_id'] );
5110 + add_post_meta( $viewing_post_id, '_property_id', $property_id );
4737 5111
4738 5112 $applicant_contacts = array();
4739 5113 foreach ($applicant_contact_ids as $applicant_contact_id)
4740 5114 {
@@ -4751,11 +5125,11 @@
4751 5125 add_post_meta( $viewing_post_id, '_feedback_status', '' );
4752 5126 add_post_meta( $viewing_post_id, '_feedback', '' );
4753 5127 add_post_meta( $viewing_post_id, '_feedback_passed_on', '' );
4754 5128
4755 - if ( !empty($_POST['negotiator_ids']) )
5129 + if ( !empty($booking['negotiator_ids']) )
4756 5130 {
4757 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
5131 + foreach ( $booking['negotiator_ids'] as $negotiator_id )
4758 5132 {
4759 5133 add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
4760 5134 }
4761 5135 }
@@ -4778,10 +5152,16 @@
4778 5152 check_ajax_referer( 'book-viewing', 'security' );
4779 5153
4780 5154 $this->json_headers();
4781 5155
4782 - // TO DO: Should do validation on server side also
4783 - 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)
4784 5164 {
4785 5165 $return = array('error' => 'No contact selected');
4786 5166 echo json_encode( $return );
4787 5167 die();
@@ -4786,9 +5166,9 @@
4786 5166 echo json_encode( $return );
4787 5167 die();
4788 5168 }
4789 5169
4790 - if (empty($_POST['property_ids']))
5170 + if (empty($booking['property_ids']))
4791 5171 {
4792 5172 $return = array('error' => 'No property selected');
4793 5173 echo json_encode( $return );
4794 5174 die();
@@ -4795,9 +5175,9 @@
4795 5175 }
4796 5176
4797 5177 // Loop through contacts and create one viewing each
4798 5178 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
4799 - foreach ( $_POST['property_ids'] as $property_id )
5179 + foreach ( $booking['property_ids'] as $property_id )
4800 5180 {
4801 5181 // Insert viewing record
4802 5182 $viewing_post = array(
4803 5183 'post_title' => '',
@@ -4817,20 +5197,20 @@
4817 5197 echo json_encode( $return );
4818 5198 die();
4819 5199 }
4820 5200
4821 - 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'] );
4822 5202 add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
4823 5203 add_post_meta( $viewing_post_id, '_property_id', (int)$property_id );
4824 - 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 );
4825 5205 add_post_meta( $viewing_post_id, '_status', 'pending' );
4826 5206 add_post_meta( $viewing_post_id, '_feedback_status', '' );
4827 5207 add_post_meta( $viewing_post_id, '_feedback', '' );
4828 5208 add_post_meta( $viewing_post_id, '_feedback_passed_on', '' );
4829 5209
4830 - if ( !empty($_POST['negotiator_ids']) )
5210 + if ( !empty($booking['negotiator_ids']) )
4831 5211 {
4832 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
5212 + foreach ( $booking['negotiator_ids'] as $negotiator_id )
4833 5213 {
4834 5214 add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
4835 5215 }
4836 5216 }
@@ -4836,9 +5216,9 @@
4836 5216 }
4837 5217 }
4838 5218
4839 5219 $properties = array();
4840 - foreach ( $_POST['property_ids'] as $property_id )
5220 + foreach ( $booking['property_ids'] as $property_id )
4841 5221 {
4842 5222 $properties[] = array(
4843 5223 'ID' => (int)$property_id,
4844 5224 'post_title' => get_the_title((int)$property_id),
@@ -4864,14 +5244,16 @@
4864 5244 global $post;
4865 5245
4866 5246 check_ajax_referer( 'viewing-details-meta-box', 'security' );
4867 5247
4868 - $post = get_post((int)$_POST['viewing_id']);
5248 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
4869 5249
4870 - $viewing = new PH_Viewing((int)$_POST['viewing_id']);
5250 + $post = get_post( $post_id );
4871 5251
4872 - $readonly = isset($_POST['readonly']) ? filter_var($_POST['readonly'], FILTER_VALIDATE_BOOLEAN) : false;
5252 + $viewing = new PH_Viewing( $post_id );
4873 5253
5254 + $readonly = isset( $_POST['readonly'] ) && is_scalar( $_POST['readonly'] ) ? filter_var( wp_unslash( $_POST['readonly'] ), FILTER_VALIDATE_BOOLEAN ) : false;
5255 +
4874 5256 include( PH()->plugin_path() . '/includes/admin/views/html-viewing-details-meta-box.php' );
4875 5257
4876 5258 die();
4877 5259 }
@@ -4879,9 +5261,9 @@
4879 5261 public function get_viewing_actions()
4880 5262 {
4881 5263 check_ajax_referer( 'viewing-actions', 'security' );
4882 5264
4883 - $post_id = (int)$_POST['viewing_id'];
5265 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
4884 5266
4885 5267 include( PH()->plugin_path() . '/includes/admin/views/html-viewing-actions.php' );
4886 5268
4887 5269 die();
@@ -4890,9 +5272,13 @@
4890 5272 public function get_viewing_lightbox()
4891 5273 {
4892 5274 global $post;
4893 5275
4894 - $post_id = $_GET['post_id'];
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 + }
4895 5281
4896 5282 $post = get_post((int)$post_id);
4897 5283
4898 5284 $viewing = new PH_Viewing($post_id);
@@ -4905,9 +5291,9 @@
4905 5291 public function viewing_carried_out()
4906 5292 {
4907 5293 check_ajax_referer( 'viewing-actions', 'security' );
4908 5294
4909 - $post_id = (int)$_POST['viewing_id'];
5295 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
4910 5296
4911 5297 $status = get_post_meta( $post_id, '_status', TRUE );
4912 5298
4913 5299 if ( $status == 'pending' )
@@ -4931,9 +5317,9 @@
4931 5317 public function viewing_no_show()
4932 5318 {
4933 5319 check_ajax_referer( 'viewing-actions', 'security' );
4934 5320
4935 - $post_id = (int)$_POST['viewing_id'];
5321 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
4936 5322
4937 5323 $status = get_post_meta( $post_id, '_status', TRUE );
4938 5324
4939 5325 if ( $status == 'pending' )
@@ -4957,16 +5343,18 @@
4957 5343 public function viewing_cancelled()
4958 5344 {
4959 5345 check_ajax_referer( 'viewing-actions', 'security' );
4960 5346
4961 - $post_id = (int)$_POST['viewing_id'];
5347 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
4962 5348
5349 + $text = isset( $_POST['cancelled_reason'] ) && is_string( $_POST['cancelled_reason'] ) ? sanitize_textarea_field( wp_unslash( $_POST['cancelled_reason'] ) ) : '';
5350 +
4963 5351 $status = get_post_meta( $post_id, '_status', TRUE );
4964 5352
4965 5353 if ( $status == 'pending' )
4966 5354 {
4967 5355 update_post_meta( $post_id, '_status', 'cancelled' );
4968 - update_post_meta( $post_id, '_cancelled_reason', sanitize_textarea_field( $_POST['cancelled_reason'] ) );
5356 + update_post_meta( $post_id, '_cancelled_reason', wp_slash( $text ) );
4969 5357 update_post_meta( $post_id, '_cancelled_reason_public', isset($_POST['cancelled_reason_public']) && $_POST['cancelled_reason_public'] == 'yes' ? 'yes' : '' );
4970 5358
4971 5359 // Add note/comment to viewing
4972 5360 $comment = array(
@@ -4985,9 +5373,9 @@
4985 5373 public function viewing_email_applicant_booking_confirmation()
4986 5374 {
4987 5375 check_ajax_referer( 'viewing-actions', 'security' );
4988 5376
4989 - $post_id = (int)$_POST['viewing_id'];
5377 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
4990 5378
4991 5379 $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
4992 5380 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
4993 5381
@@ -5012,10 +5400,10 @@
5012 5400 $to = array_filter($to);
5013 5401
5014 5402 if ( !empty(implode($to)) )
5015 5403 {
5016 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_viewing_applicant_booking_confirmation_email_subject', '' );
5017 - $body = isset($_POST['body']) ? sanitize_textarea_field($_POST['body']) : get_option( 'propertyhive_viewing_applicant_booking_confirmation_email_body', '' );
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', '' );
5018 5406
5019 5407 $applicant_names = array();
5020 5408 $applicant_dears = array();
5021 5409 foreach ($applicant_contact_ids as $applicant_contact_id)
@@ -5088,21 +5476,22 @@
5088 5476 }
5089 5477
5090 5478 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
5091 5479 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
5092 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5093 - $subject = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
5094 5482 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
5095 5483 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
5096 5484 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
5097 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.
5098 5487 $subject = apply_filters( 'viewing_applicant_booking_confirmation_email_subject', $subject, $post_id, $property_id );
5099 5488
5100 5489 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
5101 5490 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
5102 5491 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
5103 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5104 - $body = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
5105 5494 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
5106 5495 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
5107 5496 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5108 5497
@@ -5107,8 +5496,9 @@
5107 5496 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5108 5497
5109 5498 $body = html_entity_decode($body);
5110 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.
5111 5501 $body = apply_filters( 'viewing_applicant_booking_confirmation_email_body', $body, $post_id, $property_id );
5112 5502
5113 5503 $from = '';
5114 5504 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -5137,9 +5527,9 @@
5137 5527
5138 5528 $attachments = array();
5139 5529 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5140 5530 {
5141 - $uploaded_files = $_FILES['attachments'];
5531 + $uploaded_files = $this->get_viewing_email_uploads();
5142 5532
5143 5533 // Handle each file upload
5144 5534 foreach ($uploaded_files['name'] as $key => $value)
5145 5535 {
@@ -5181,9 +5571,9 @@
5181 5571 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
5182 5572
5183 5573 foreach ($attachments as $temp_file)
5184 5574 {
5185 - @unlink($temp_file);
5575 + @wp_delete_file($temp_file);
5186 5576 }
5187 5577
5188 5578 if ( !$sent )
5189 5579 {
@@ -5189,9 +5579,9 @@
5189 5579 {
5190 5580 wp_send_json_error('Failed to send email');
5191 5581 }
5192 5582
5193 - update_post_meta( $post_id, '_applicant_booking_confirmation_sent_at', date("Y-m-d H:i:s") );
5583 + update_post_meta( $post_id, '_applicant_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
5194 5584
5195 5585 if ( apply_filters( 'propertyhive_log_booking_confirmation_emails', false ) === true )
5196 5586 {
5197 5587 // Add note/comment to viewing
@@ -5216,9 +5606,9 @@
5216 5606 public function viewing_email_owner_booking_confirmation()
5217 5607 {
5218 5608 check_ajax_referer( 'viewing-actions', 'security' );
5219 5609
5220 - $post_id = (int)$_POST['viewing_id'];
5610 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5221 5611
5222 5612 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5223 5613 $property_department = get_post_meta( $property_id, '_department' );
5224 5614
@@ -5329,20 +5719,21 @@
5329 5719 $property = new PH_Property((int)$property_id);
5330 5720
5331 5721 $to = implode(",", $owner_emails);
5332 5722
5333 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_viewing_owner_booking_confirmation_email_subject', '' );
5334 - $body = isset($_POST['body']) ? sanitize_textarea_field($_POST['body']) : get_option( 'propertyhive_viewing_owner_booking_confirmation_email_body', '' );
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', '' );
5335 5725
5336 5726 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
5337 5727 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
5338 5728 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
5339 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5340 - $subject = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
5341 5731 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
5342 5732 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
5343 5733 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
5344 5734
5735 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_owner_booking_confirmation_email_subject; third-party email integrations depend on the established name.
5345 5736 $subject = apply_filters( 'viewing_owner_booking_confirmation_email_subject', $subject, $post_id, $property_id );
5346 5737
5347 5738 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
5348 5739 $body = str_replace('[owner_name]', $owner_names_string, $body);
@@ -5348,10 +5739,10 @@
5348 5739 $body = str_replace('[owner_name]', $owner_names_string, $body);
5349 5740 $body = str_replace('[owner_dear]', $owner_dears_string, $body);
5350 5741 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
5351 5742 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
5352 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5353 - $body = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
5354 5745 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
5355 5746 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
5356 5747 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5357 5748
@@ -5356,8 +5747,9 @@
5356 5747 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5357 5748
5358 5749 $body = html_entity_decode($body);
5359 5750
5751 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_owner_booking_confirmation_email_body; third-party email integrations depend on the established name.
5360 5752 $body = apply_filters( 'viewing_owner_booking_confirmation_email_body', $body, $post_id, $property_id );
5361 5753
5362 5754 $from = '';
5363 5755 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -5386,9 +5778,9 @@
5386 5778
5387 5779 $attachments = array();
5388 5780 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5389 5781 {
5390 - $uploaded_files = $_FILES['attachments'];
5782 + $uploaded_files = $this->get_viewing_email_uploads();
5391 5783
5392 5784 // Handle each file upload
5393 5785 foreach ($uploaded_files['name'] as $key => $value)
5394 5786 {
@@ -5430,9 +5822,9 @@
5430 5822 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
5431 5823
5432 5824 foreach ($attachments as $temp_file)
5433 5825 {
5434 - @unlink($temp_file);
5826 + @wp_delete_file($temp_file);
5435 5827 }
5436 5828
5437 5829 if ( !$sent )
5438 5830 {
@@ -5449,9 +5841,9 @@
5449 5841
5450 5842 PH_Comments::insert_note( $post_id, $comment );
5451 5843 }
5452 5844
5453 - update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', date("Y-m-d H:i:s") );
5845 + update_post_meta( $post_id, '_owner_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
5454 5846
5455 5847 wp_send_json_success();
5456 5848 }
5457 5849 else
@@ -5465,9 +5857,9 @@
5465 5857 public function viewing_email_attending_negotiator_booking_confirmation()
5466 5858 {
5467 5859 check_ajax_referer( 'viewing-actions', 'security' );
5468 5860
5469 - $post_id = (int)$_POST['viewing_id'];
5861 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5470 5862 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5471 5863
5472 5864 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
5473 5865
@@ -5599,20 +5991,21 @@
5599 5991 }
5600 5992
5601 5993 $property = new PH_Property((int)$property_id);
5602 5994
5603 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_viewing_attending_negotiator_booking_confirmation_email_subject', '' );
5604 - $body = isset($_POST['body']) ? sanitize_textarea_field($_POST['body']) : get_option( 'propertyhive_viewing_attending_negotiator_booking_confirmation_email_body', '' );
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', '' );
5605 5997
5606 5998 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
5607 5999 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
5608 6000 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
5609 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5610 - $subject = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
5611 6003 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
5612 6004 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
5613 6005 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
5614 6006
6007 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_attending_negotiator_booking_confirmation_email_subject; third-party email integrations depend on the established name.
5615 6008 $subject = apply_filters( 'viewing_attending_negotiator_booking_confirmation_email_subject', $subject, $post_id, $property_id );
5616 6009
5617 6010 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
5618 6011 $body = str_replace('[owner_name]', $owner_names_string, $body);
@@ -5620,10 +6013,10 @@
5620 6013 $body = str_replace('[owner_details]', $owner_details, $body);
5621 6014 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
5622 6015 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
5623 6016 $body = str_replace('[applicant_details]', $applicant_details, $body);
5624 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5625 - $body = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
5626 6019 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
5627 6020 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
5628 6021 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5629 6022
@@ -5628,8 +6021,9 @@
5628 6021 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5629 6022
5630 6023 $body = html_entity_decode($body);
5631 6024
6025 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public email customization hook viewing_attending_negotiator_booking_confirmation_email_body; third-party email integrations depend on the established name.
5632 6026 $body = apply_filters( 'viewing_attending_negotiator_booking_confirmation_email_body', $body, $post_id, $property_id );
5633 6027
5634 6028 $from = '';
5635 6029 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -5658,9 +6052,9 @@
5658 6052
5659 6053 $attachments = array();
5660 6054 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5661 6055 {
5662 - $uploaded_files = $_FILES['attachments'];
6056 + $uploaded_files = $this->get_viewing_email_uploads();
5663 6057
5664 6058 // Handle each file upload
5665 6059 foreach ($uploaded_files['name'] as $key => $value)
5666 6060 {
@@ -5702,9 +6096,9 @@
5702 6096 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
5703 6097
5704 6098 foreach ($attachments as $temp_file)
5705 6099 {
5706 - @unlink($temp_file);
6100 + @wp_delete_file($temp_file);
5707 6101 }
5708 6102
5709 6103 if ( !$sent )
5710 6104 {
@@ -5721,9 +6115,9 @@
5721 6115
5722 6116 PH_Comments::insert_note( $post_id, $comment );
5723 6117 }
5724 6118
5725 - update_post_meta( $post_id, '_attending_negotiator_booking_confirmation_sent_at', date("Y-m-d H:i:s") );
6119 + update_post_meta( $post_id, '_attending_negotiator_booking_confirmation_sent_at', gmdate("Y-m-d H:i:s") );
5726 6120
5727 6121 wp_send_json_success();
5728 6122 }
5729 6123 else
@@ -5737,9 +6131,9 @@
5737 6131 public function viewing_email_applicant_cancellation_notification()
5738 6132 {
5739 6133 check_ajax_referer( 'viewing-actions', 'security' );
5740 6134
5741 - $post_id = (int)$_POST['viewing_id'];
6135 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5742 6136
5743 6137 $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
5744 6138 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5745 6139
@@ -5764,10 +6158,10 @@
5764 6158 $to = array_filter($to);
5765 6159
5766 6160 if ( !empty(implode($to)) )
5767 6161 {
5768 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_viewing_applicant_cancellation_notification_email_subject', '' );
5769 - $body = isset($_POST['body']) ? sanitize_textarea_field($_POST['body']) : get_option( 'propertyhive_viewing_applicant_cancellation_notification_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', '' );
5770 6164
5771 6165 $applicant_names = array();
5772 6166 $applicant_dears = array();
5773 6167 foreach ($applicant_contact_ids as $applicant_contact_id)
@@ -5840,21 +6234,22 @@
5840 6234 }
5841 6235
5842 6236 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
5843 6237 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
5844 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5845 - $subject = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
5846 6240 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
5847 6241 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
5848 6242 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
5849 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.
5850 6245 $subject = apply_filters( 'viewing_applicant_cancellation_notification_email_subject', $subject, $post_id, $property_id );
5851 6246
5852 6247 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
5853 6248 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
5854 6249 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
5855 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5856 - $body = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
5857 6252 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
5858 6253 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
5859 6254 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5860 6255
@@ -5869,8 +6264,9 @@
5869 6264 $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
5870 6265
5871 6266 $body = html_entity_decode($body);
5872 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.
5873 6269 $body = apply_filters( 'viewing_applicant_cancellation_notification_email_body', $body, $post_id, $property_id );
5874 6270
5875 6271 $from = '';
5876 6272 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -5899,9 +6295,9 @@
5899 6295
5900 6296 $attachments = array();
5901 6297 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5902 6298 {
5903 - $uploaded_files = $_FILES['attachments'];
6299 + $uploaded_files = $this->get_viewing_email_uploads();
5904 6300
5905 6301 // Handle each file upload
5906 6302 foreach ($uploaded_files['name'] as $key => $value)
5907 6303 {
@@ -5943,9 +6339,9 @@
5943 6339 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
5944 6340
5945 6341 foreach ($attachments as $temp_file)
5946 6342 {
5947 - @unlink($temp_file);
6343 + @wp_delete_file($temp_file);
5948 6344 }
5949 6345
5950 6346 if ( !$sent )
5951 6347 {
@@ -5951,9 +6347,9 @@
5951 6347 {
5952 6348 wp_send_json_error('Failed to send email');
5953 6349 }
5954 6350
5955 - update_post_meta( $post_id, '_applicant_cancellation_notification_sent_at', date("Y-m-d H:i:s") );
6351 + update_post_meta( $post_id, '_applicant_cancellation_notification_sent_at', gmdate("Y-m-d H:i:s") );
5956 6352
5957 6353 if ( apply_filters( 'propertyhive_log_cancellation_notification_emails', false ) === true )
5958 6354 {
5959 6355 // Add note/comment to viewing
@@ -5978,9 +6374,9 @@
5978 6374 public function viewing_email_owner_cancellation_notification()
5979 6375 {
5980 6376 check_ajax_referer( 'viewing-actions', 'security' );
5981 6377
5982 - $post_id = (int)$_POST['viewing_id'];
6378 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5983 6379
5984 6380 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5985 6381 $property_department = get_post_meta( $property_id, '_department' );
5986 6382
@@ -6091,20 +6487,21 @@
6091 6487 $property = new PH_Property((int)$property_id);
6092 6488
6093 6489 $to = implode(",", $owner_emails);
6094 6490
6095 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_viewing_owner_cancellation_notification_email_subject', '' );
6096 - $body = isset($_POST['body']) ? sanitize_textarea_field($_POST['body']) : get_option( 'propertyhive_viewing_owner_cancellation_notification_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', '' );
6097 6493
6098 6494 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
6099 6495 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
6100 6496 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
6101 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6102 - $subject = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
6103 6499 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
6104 6500 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
6105 6501 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
6106 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.
6107 6504 $subject = apply_filters( 'viewing_owner_cancellation_notification_email_subject', $subject, $post_id, $property_id );
6108 6505
6109 6506 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
6110 6507 $body = str_replace('[owner_name]', $owner_names_string, $body);
@@ -6110,10 +6507,10 @@
6110 6507 $body = str_replace('[owner_name]', $owner_names_string, $body);
6111 6508 $body = str_replace('[owner_dear]', $owner_dears_string, $body);
6112 6509 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
6113 6510 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
6114 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6115 - $body = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
6116 6513 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
6117 6514 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
6118 6515 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
6119 6516
@@ -6128,8 +6525,9 @@
6128 6525 $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
6129 6526
6130 6527 $body = html_entity_decode($body);
6131 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.
6132 6530 $body = apply_filters( 'viewing_owner_cancellation_notification_email_body', $body, $post_id, $property_id );
6133 6531
6134 6532 $from = '';
6135 6533 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -6158,9 +6556,9 @@
6158 6556
6159 6557 $attachments = array();
6160 6558 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6161 6559 {
6162 - $uploaded_files = $_FILES['attachments'];
6560 + $uploaded_files = $this->get_viewing_email_uploads();
6163 6561
6164 6562 // Handle each file upload
6165 6563 foreach ($uploaded_files['name'] as $key => $value)
6166 6564 {
@@ -6202,9 +6600,9 @@
6202 6600 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
6203 6601
6204 6602 foreach ($attachments as $temp_file)
6205 6603 {
6206 - @unlink($temp_file);
6604 + @wp_delete_file($temp_file);
6207 6605 }
6208 6606
6209 6607 if ( !$sent )
6210 6608 {
@@ -6221,9 +6619,9 @@
6221 6619
6222 6620 PH_Comments::insert_note( $post_id, $comment );
6223 6621 }
6224 6622
6225 - update_post_meta( $post_id, '_owner_cancellation_notification_sent_at', date("Y-m-d H:i:s") );
6623 + update_post_meta( $post_id, '_owner_cancellation_notification_sent_at', gmdate("Y-m-d H:i:s") );
6226 6624
6227 6625 wp_send_json_success();
6228 6626 }
6229 6627 else
@@ -6237,9 +6635,9 @@
6237 6635 public function viewing_email_attending_negotiator_cancellation_notification()
6238 6636 {
6239 6637 check_ajax_referer( 'viewing-actions', 'security' );
6240 6638
6241 - $post_id = (int)$_POST['viewing_id'];
6639 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6242 6640 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
6243 6641
6244 6642 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
6245 6643
@@ -6371,20 +6769,21 @@
6371 6769 }
6372 6770
6373 6771 $property = new PH_Property((int)$property_id);
6374 6772
6375 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_viewing_attending_negotiator_cancellation_notification_email_subject', '' );
6376 - $body = isset($_POST['body']) ? sanitize_textarea_field($_POST['body']) : get_option( 'propertyhive_viewing_attending_negotiator_cancellation_notification_email_body', '' );
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', '' );
6377 6775
6378 6776 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
6379 6777 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
6380 6778 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
6381 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6382 - $subject = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
6383 6781 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
6384 6782 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
6385 6783 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
6386 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.
6387 6786 $subject = apply_filters( 'viewing_attending_negotiator_cancellation_notification_email_subject', $subject, $post_id, $property_id );
6388 6787
6389 6788 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
6390 6789 $body = str_replace('[owner_name]', $owner_names_string, $body);
@@ -6392,10 +6791,10 @@
6392 6791 $body = str_replace('[owner_details]', $owner_details, $body);
6393 6792 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
6394 6793 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
6395 6794 $body = str_replace('[applicant_details]', $applicant_details, $body);
6396 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6397 - $body = str_replace('[viewing_date]', date("l jS F Y", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $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);
6398 6797 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
6399 6798 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
6400 6799 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
6401 6800
@@ -6410,8 +6809,9 @@
6410 6809 $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
6411 6810
6412 6811 $body = html_entity_decode($body);
6413 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.
6414 6814 $body = apply_filters( 'viewing_attending_negotiator_cancellation_notification_email_body', $body, $post_id, $property_id );
6415 6815
6416 6816 $from = '';
6417 6817 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -6440,9 +6840,9 @@
6440 6840
6441 6841 $attachments = array();
6442 6842 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6443 6843 {
6444 - $uploaded_files = $_FILES['attachments'];
6844 + $uploaded_files = $this->get_viewing_email_uploads();
6445 6845
6446 6846 // Handle each file upload
6447 6847 foreach ($uploaded_files['name'] as $key => $value)
6448 6848 {
@@ -6484,9 +6884,9 @@
6484 6884 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
6485 6885
6486 6886 foreach ($attachments as $temp_file)
6487 6887 {
6488 - @unlink($temp_file);
6888 + @wp_delete_file($temp_file);
6489 6889 }
6490 6890
6491 6891 if ( !$sent )
6492 6892 {
@@ -6503,9 +6903,9 @@
6503 6903
6504 6904 PH_Comments::insert_note( $post_id, $comment );
6505 6905 }
6506 6906
6507 - update_post_meta( $post_id, '_attending_negotiator_cancellation_notification_sent_at', date("Y-m-d H:i:s") );
6907 + update_post_meta( $post_id, '_attending_negotiator_cancellation_notification_sent_at', gmdate("Y-m-d H:i:s") );
6508 6908
6509 6909 wp_send_json_success();
6510 6910 }
6511 6911 else
@@ -6519,16 +6919,18 @@
6519 6919 public function viewing_interested_feedback()
6520 6920 {
6521 6921 check_ajax_referer( 'viewing-actions', 'security' );
6522 6922
6523 - $post_id = (int)$_POST['viewing_id'];
6923 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6524 6924
6925 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6926 +
6525 6927 $status = get_post_meta( $post_id, '_status', TRUE );
6526 6928
6527 6929 if ( $status == 'carried_out' )
6528 6930 {
6529 6931 update_post_meta( $post_id, '_feedback_status', 'interested' );
6530 - update_post_meta( $post_id, '_feedback', sanitize_textarea_field( $_POST['feedback'] ) );
6932 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
6531 6933
6532 6934 // Add note/comment to viewing
6533 6935 $comment = array(
6534 6936 'note_type' => 'action',
@@ -6546,16 +6948,18 @@
6546 6948 public function viewing_not_interested_feedback()
6547 6949 {
6548 6950 check_ajax_referer( 'viewing-actions', 'security' );
6549 6951
6550 - $post_id = (int)$_POST['viewing_id'];
6952 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6551 6953
6954 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6955 +
6552 6956 $status = get_post_meta( $post_id, '_status', TRUE );
6553 6957
6554 6958 if ( $status == 'carried_out' )
6555 6959 {
6556 6960 update_post_meta( $post_id, '_feedback_status', 'not_interested' );
6557 - update_post_meta( $post_id, '_feedback', sanitize_textarea_field( $_POST['feedback'] ) );
6961 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
6558 6962
6559 6963 // Add note/comment to viewing
6560 6964 $comment = array(
6561 6965 'note_type' => 'action',
@@ -6573,9 +6977,9 @@
6573 6977 public function viewing_feedback_not_required()
6574 6978 {
6575 6979 check_ajax_referer( 'viewing-actions', 'security' );
6576 6980
6577 - $post_id = (int)$_POST['viewing_id'];
6981 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6578 6982
6579 6983 $status = get_post_meta( $post_id, '_status', TRUE );
6580 6984
6581 6985 if ( $status == 'carried_out' )
@@ -6599,9 +7003,9 @@
6599 7003 public function viewing_revert_feedback_pending()
6600 7004 {
6601 7005 check_ajax_referer( 'viewing-actions', 'security' );
6602 7006
6603 - $post_id = (int)$_POST['viewing_id'];
7007 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6604 7008
6605 7009 $status = get_post_meta( $post_id, '_status', TRUE );
6606 7010
6607 7011 if ( $status == 'carried_out' )
@@ -6627,9 +7031,9 @@
6627 7031 public function viewing_revert_pending()
6628 7032 {
6629 7033 check_ajax_referer( 'viewing-actions', 'security' );
6630 7034
6631 - $post_id = (int)$_POST['viewing_id'];
7035 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6632 7036
6633 7037 $status = get_post_meta( $post_id, '_status', TRUE );
6634 7038
6635 7039 if ( in_array( $status, array('carried_out', 'cancelled', 'no_show') ) )
@@ -6655,9 +7059,9 @@
6655 7059 public function viewing_feedback_passed_on()
6656 7060 {
6657 7061 check_ajax_referer( 'viewing-actions', 'security' );
6658 7062
6659 - $post_id = (int)$_POST['viewing_id'];
7063 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6660 7064
6661 7065 $status = get_post_meta( $post_id, '_status', TRUE );
6662 7066
6663 7067 if ( $status == 'carried_out' )
@@ -6679,14 +7083,16 @@
6679 7083 }
6680 7084
6681 7085 public function get_property_viewings_meta_box()
6682 7086 {
6683 - $post_id = $_POST['post_id'];
7087 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
6684 7088
6685 7089 $selected_status = '';
6686 - if ( isset($_POST['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'] ) )
6687 7092 {
6688 - $selected_status = ph_clean($_POST['selected_status']);
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'] ) );
6689 7095 }
6690 7096
6691 7097 include( PH()->plugin_path() . '/includes/admin/views/html-property-viewings-meta-box.php' );
6692 7098
@@ -6697,14 +7103,16 @@
6697 7103 }
6698 7104
6699 7105 public function get_contact_viewings_meta_box()
6700 7106 {
6701 - $post_id = $_POST['post_id'];
7107 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
6702 7108
6703 7109 $selected_status = '';
6704 - if ( isset($_POST['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'] ) )
6705 7112 {
6706 - $selected_status = ph_clean($_POST['selected_status']);
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'] ) );
6707 7115 }
6708 7116
6709 7117 include( PH()->plugin_path() . '/includes/admin/views/html-contact-viewings-meta-box.php' );
6710 7118
@@ -6720,10 +7128,19 @@
6720 7128 check_ajax_referer( 'record-offer', 'security' );
6721 7129
6722 7130 $this->json_headers();
6723 7131
6724 - // TO DO: Should do validation on server side also
6725 - 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)
6726 7143 {
6727 7144 $return = array('error' => 'No property selected');
6728 7145 echo json_encode( $return );
6729 7146 die();
@@ -6728,18 +7145,18 @@
6728 7145 echo json_encode( $return );
6729 7146 die();
6730 7147 }
6731 7148
6732 - $property = new PH_Property((int)$_POST['property_id']);
7149 + $property = new PH_Property($property_id);
6733 7150
6734 7151 $applicant_contact_ids = array();
6735 7152
6736 7153 // Create applicant record if required
6737 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
7154 + if (empty($input['applicant_ids']) && !empty($input['applicant_name']))
6738 7155 {
6739 7156 // Need to create contact/applicant
6740 7157 $contact_post = array(
6741 - 'post_title' => ph_clean($_POST['applicant_name']),
7158 + 'post_title' => $input['applicant_name'],
6742 7159 'post_content' => '',
6743 7160 'post_type' => 'contact',
6744 7161 'post_status' => 'publish',
6745 7162 'comment_status' => 'closed',
@@ -6746,9 +7163,9 @@
6746 7163 'ping_status' => 'closed',
6747 7164 );
6748 7165
6749 7166 // Insert the post into the database
6750 - $contact_post_id = wp_insert_post( $contact_post );
7167 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
6751 7168
6752 7169 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
6753 7170 {
6754 7171 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -6757,24 +7174,24 @@
6757 7174 }
6758 7175
6759 7176 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
6760 7177
6761 - $email_address = isset($_POST['applicant_email_address']) ? sanitize_email($_POST['applicant_email_address']) : '';
6762 - $telephone_number = isset($_POST['applicant_telephone_number']) ? sanitize_text_field($_POST['applicant_telephone_number']) : '';
6763 - update_post_meta( $contact_post_id, '_email_address', $email_address );
6764 - update_post_meta( $contact_post_id, '_telephone_number', $telephone_number );
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 ) );
6765 7182 update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean( ph_clean_telephone_number($telephone_number) ) );
6766 7183
6767 - if ( isset($_POST['applicant_address']) && !empty(sanitize_textarea_field($_POST['applicant_address'])) )
7184 + if ( '' !== $input['applicant_address'] )
6768 7185 {
6769 - $address = ph_split_address_into_fields( sanitize_textarea_field($_POST['applicant_address']) );
7186 + $address = ph_split_address_into_fields( $input['applicant_address'] );
6770 7187
6771 - update_post_meta( $contact_post_id, '_address_name_number', $address['address_name_number'] );
6772 - update_post_meta( $contact_post_id, '_address_street', $address['address_street'] );
6773 - update_post_meta( $contact_post_id, '_address_two', $address['address_two'] );
6774 - update_post_meta( $contact_post_id, '_address_three', $address['address_three'] );
6775 - update_post_meta( $contact_post_id, '_address_four', $address['address_four'] );
6776 - update_post_meta( $contact_post_id, '_address_postcode', $address['address_postcode'] );
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'] ) );
6777 7194 update_post_meta( $contact_post_id, '_address_country', get_option( 'propertyhive_default_country', 'GB' ) );
6778 7195 }
6779 7196
6780 7197 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
@@ -6782,18 +7199,13 @@
6782 7199
6783 7200 $applicant_contact_ids[] = $contact_post_id;
6784 7201 }
6785 7202
6786 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
7203 + if (!empty($input['applicant_ids']) && empty($input['applicant_name']))
6787 7204 {
6788 7205 // This is an existing contact
6789 - if ( !is_array($_POST['applicant_ids']) )
7206 + foreach ( $input['applicant_ids'] as $applicant_id )
6790 7207 {
6791 - $_POST['applicant_ids'] = array($_POST['applicant_ids']);
6792 - }
6793 -
6794 - foreach ( $_POST['applicant_ids'] as $applicant_id )
6795 - {
6796 7208 $applicant_contact_ids[] = (int)$applicant_id;
6797 7209 }
6798 7210 }
6799 7211
@@ -6829,12 +7241,12 @@
6829 7241 echo json_encode( $return );
6830 7242 die();
6831 7243 }
6832 7244
6833 - $amount = preg_replace("/[^0-9.]/", '', $_POST['amount']);
7245 + $amount = $input['amount'];
6834 7246
6835 - add_post_meta( $offer_post_id, '_offer_date_time', ph_clean($_POST['offer_date']) . ' ' . ph_clean($_POST['offer_time']) );
6836 - 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 );
6837 7249 add_post_meta( $offer_post_id, '_applicant_contact_id', $applicant_contact_id );
6838 7250 add_post_meta( $offer_post_id, '_amount', $amount );
6839 7251 add_post_meta( $offer_post_id, '_status', 'pending' );
6840 7252
@@ -6843,11 +7255,12 @@
6843 7255 {
6844 7256 add_post_meta( $offer_post_id, '_applicant_solicitor_contact_id', (int)$applicant_solicitor_contact_id );
6845 7257 }
6846 7258
6847 - $owner_contact_ids = get_post_meta((int)$_POST['property_id'], '_owner_contact_id', TRUE);
7259 + $owner_contact_ids = get_post_meta($property_id, '_owner_contact_id', TRUE);
6848 7260 if ( !empty($owner_contact_ids) )
6849 7261 {
7262 + $owner_contact_ids = is_array( $owner_contact_ids ) ? $owner_contact_ids : array( $owner_contact_ids );
6850 7263 foreach ( $owner_contact_ids as $owner_contact_id )
6851 7264 {
6852 7265 $property_owner_solicitor_contact_id = get_post_meta( (int)$owner_contact_id, '_contact_solicitor_contact_id', TRUE );
6853 7266 if ( !empty($property_owner_solicitor_contact_id) )
@@ -6886,10 +7299,16 @@
6886 7299 check_ajax_referer( 'record-offer', 'security' );
6887 7300
6888 7301 $this->json_headers();
6889 7302
6890 - // TO DO: Should do validation on server side also
6891 - 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)
6892 7311 {
6893 7312 $return = array('error' => 'No contact selected');
6894 7313 echo json_encode( $return );
6895 7314 die();
@@ -6894,9 +7313,9 @@
6894 7313 echo json_encode( $return );
6895 7314 die();
6896 7315 }
6897 7316
6898 - if (empty($_POST['property_ids']))
7317 + if (empty($input['property_ids']))
6899 7318 {
6900 7319 $return = array('error' => 'No property selected');
6901 7320 echo json_encode( $return );
6902 7321 die();
@@ -6903,9 +7322,9 @@
6903 7322 }
6904 7323
6905 7324 // Loop through contacts and create one offer each
6906 7325 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
6907 - foreach ( $_POST['property_ids'] as $property_id )
7326 + foreach ( $input['property_ids'] as $property_id )
6908 7327 {
6909 7328 // Insert offer record
6910 7329 $offer_post = array(
6911 7330 'post_title' => '',
@@ -6925,17 +7344,17 @@
6925 7344 echo json_encode( $return );
6926 7345 die();
6927 7346 }
6928 7347
6929 - $amount = preg_replace("/[^0-9.]/", '', ph_clean($_POST['amount']));
7348 + $amount = $input['amount'];
6930 7349
6931 - 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'] );
6932 7351 add_post_meta( $offer_post_id, '_property_id', (int)$property_id );
6933 - 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 );
6934 7353 add_post_meta( $offer_post_id, '_amount', $amount );
6935 7354 add_post_meta( $offer_post_id, '_status', 'pending' );
6936 7355
6937 - $applicant_solicitor_contact_id = get_post_meta( (int)$_POST['contact_id'], '_contact_solicitor_contact_id', TRUE );
7356 + $applicant_solicitor_contact_id = get_post_meta( $contact_id, '_contact_solicitor_contact_id', TRUE );
6938 7357 if ( !empty($applicant_solicitor_contact_id) )
6939 7358 {
6940 7359 add_post_meta( $offer_post_id, '_applicant_solicitor_contact_id', (int)$applicant_solicitor_contact_id );
6941 7360 }
@@ -6942,8 +7361,9 @@
6942 7361
6943 7362 $owner_contact_ids = get_post_meta($property_id, '_owner_contact_id', TRUE);
6944 7363 if ( !empty($owner_contact_ids) )
6945 7364 {
7365 + $owner_contact_ids = is_array( $owner_contact_ids ) ? $owner_contact_ids : array( $owner_contact_ids );
6946 7366 foreach ( $owner_contact_ids as $owner_contact_id )
6947 7367 {
6948 7368 $property_owner_solicitor_contact_id = get_post_meta( (int)$owner_contact_id, '_contact_solicitor_contact_id', TRUE );
6949 7369 if ( !empty($property_owner_solicitor_contact_id) )
@@ -6954,9 +7374,9 @@
6954 7374 }
6955 7375 }
6956 7376
6957 7377 $properties = array();
6958 - foreach ( $_POST['property_ids'] as $property_id )
7378 + foreach ( $input['property_ids'] as $property_id )
6959 7379 {
6960 7380 $properties[] = array(
6961 7381 'ID' => (int)$property_id,
6962 7382 'post_title' => get_the_title((int)$property_id),
@@ -6982,12 +7402,14 @@
6982 7402 global $post;
6983 7403
6984 7404 check_ajax_referer( 'offer-details-meta-box', 'security' );
6985 7405
6986 - $post = get_post((int)$_POST['offer_id']);
7406 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
6987 7407
6988 - $offer = new PH_Offer((int)$_POST['offer_id']);
7408 + $post = get_post( $post_id );
6989 7409
7410 + $offer = new PH_Offer( $post_id );
7411 +
6990 7412 echo '<div class="propertyhive_meta_box">';
6991 7413
6992 7414 echo '<div class="options_group">';
6993 7415
@@ -6996,9 +7418,9 @@
6996 7418 echo '<p class="form-field">
6997 7419
6998 7420 <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
6999 7421
7000 - ' . esc_html(__( ucwords(str_replace("_", " ", $offer->status)), 'propertyhive' )) . '
7422 + ' . esc_html(propertyhive_get_status_label( $offer->status )) . '
7001 7423
7002 7424 </p>';
7003 7425 }
7004 7426
@@ -7004,9 +7426,9 @@
7004 7426
7005 7427 $offer_date_time = $offer->offer_date_time;
7006 7428 if ( empty($offer_date_time) )
7007 7429 {
7008 - $offer_date_time = date("Y-m-d H:i:s");
7430 + $offer_date_time = gmdate("Y-m-d H:i:s");
7009 7431 }
7010 7432
7011 7433 echo '<p class="form-field offer_date_time_field">
7012 7434
@@ -7011,18 +7433,18 @@
7011 7433 echo '<p class="form-field offer_date_time_field">
7012 7434
7013 7435 <label for="_offer_date">' . esc_html(__('Offer Date / Time', 'propertyhive')) . '</label>
7014 7436
7015 - <input type="date" class="small" name="_offer_date" id="_offer_date" value="' . esc_attr(date("Y-m-d", strtotime($offer_date_time))) . '" placeholder="">
7437 + <input type="date" class="small" name="_offer_date" id="_offer_date" value="' . esc_attr(gmdate("Y-m-d", strtotime($offer_date_time))) . '" placeholder="">
7016 7438 <select id="_offer_time_hours" name="_offer_time_hours" class="select short" style="width:55px">';
7017 7439
7018 7440 if ( empty($offer_date_time) )
7019 7441 {
7020 - $value = date("H");
7442 + $value = gmdate("H");
7021 7443 }
7022 7444 else
7023 7445 {
7024 - $value = date( "H", strtotime( $offer_date_time ) );
7446 + $value = gmdate( "H", strtotime( $offer_date_time ) );
7025 7447 }
7026 7448 for ( $i = 0; $i < 23; ++$i )
7027 7449 {
7028 7450 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
@@ -7040,9 +7462,9 @@
7040 7462 $value = '';
7041 7463 }
7042 7464 else
7043 7465 {
7044 - $value = date( "i", strtotime( $offer_date_time ) );
7466 + $value = gmdate( "i", strtotime( $offer_date_time ) );
7045 7467 }
7046 7468 for ( $i = 0; $i < 60; $i+=5 )
7047 7469 {
7048 7470 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
@@ -7079,9 +7501,9 @@
7079 7501 public function get_offer_actions()
7080 7502 {
7081 7503 check_ajax_referer( 'offer-actions', 'security' );
7082 7504
7083 - $post_id = (int)$_POST['offer_id'];
7505 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7084 7506
7085 7507 $status = get_post_meta( $post_id, '_status', TRUE );
7086 7508
7087 7509 // Success action panel
@@ -7144,9 +7566,9 @@
7144 7566 }
7145 7567 else
7146 7568 {
7147 7569 $actions[] = '<a
7148 - href="' . esc_url(wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), '1', 'create_sale' )) . '"
7570 + href="' . esc_url(wp_nonce_url( admin_url( 'post.php?post=' . $post_id . '&action=edit' ), 'propertyhive-create_sale-' . $post_id, 'create_sale' )) . '"
7149 7571 class="button button-success button-create-sale"
7150 7572 style="width:100%; margin-bottom:7px; text-align:center"
7151 7573 onclick="setTimeout(function() { jQuery(\'.button-create-sale\').attr(\'href\', \'#\'); jQuery(\'.button-create-sale\').attr(\'disabled\', \'disabled\'); jQuery(\'.button-create-sale\').html(\'Creating...\'); }, 50);"
7152 7574 >' . wp_kses_post( __('Create Sale', 'propertyhive') ) . '</a>';
@@ -7171,8 +7593,9 @@
7171 7593 $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
7172 7594
7173 7595 if ( !empty($actions) )
7174 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.
7175 7598 echo implode("", $actions);
7176 7599 }
7177 7600 else
7178 7601 {
@@ -7189,9 +7612,9 @@
7189 7612 public function offer_accepted()
7190 7613 {
7191 7614 check_ajax_referer( 'offer-actions', 'security' );
7192 7615
7193 - $post_id = (int)$_POST['offer_id'];
7616 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7194 7617
7195 7618 $status = get_post_meta( $post_id, '_status', TRUE );
7196 7619
7197 7620 if ( $status == 'pending' )
@@ -7215,9 +7638,9 @@
7215 7638 public function offer_declined()
7216 7639 {
7217 7640 check_ajax_referer( 'offer-actions', 'security' );
7218 7641
7219 - $post_id = (int)$_POST['offer_id'];
7642 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7220 7643
7221 7644 $status = get_post_meta( $post_id, '_status', TRUE );
7222 7645
7223 7646 if ( $status == 'pending' )
@@ -7241,9 +7664,9 @@
7241 7664 public function offer_withdrawn()
7242 7665 {
7243 7666 check_ajax_referer( 'offer-actions', 'security' );
7244 7667
7245 - $post_id = (int)$_POST['offer_id'];
7668 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7246 7669
7247 7670 $status = get_post_meta( $post_id, '_status', TRUE );
7248 7671
7249 7672 if ( $status == 'pending' || $status == 'accepted' )
@@ -7267,9 +7690,9 @@
7267 7690 public function offer_revert_pending()
7268 7691 {
7269 7692 check_ajax_referer( 'offer-actions', 'security' );
7270 7693
7271 - $post_id = (int)$_POST['offer_id'];
7694 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7272 7695
7273 7696 $status = get_post_meta( $post_id, '_status', TRUE );
7274 7697
7275 7698 if ( $status == 'accepted' || $status == 'declined' || $status == 'withdrawn' )
@@ -7291,14 +7714,16 @@
7291 7714 }
7292 7715
7293 7716 public function get_property_offers_meta_box()
7294 7717 {
7295 - $post_id = $_POST['post_id'];
7718 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
7296 7719
7297 7720 $selected_status = '';
7298 - if ( isset($_POST['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'] ) )
7299 7723 {
7300 - $selected_status = ph_clean($_POST['selected_status']);
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'] ) );
7301 7726 }
7302 7727
7303 7728 include( PH()->plugin_path() . '/includes/admin/views/html-property-offers-meta-box.php' );
7304 7729
@@ -7309,14 +7734,16 @@
7309 7734 }
7310 7735
7311 7736 public function get_contact_offers_meta_box()
7312 7737 {
7313 - $post_id = $_POST['post_id'];
7738 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
7314 7739
7315 7740 $selected_status = '';
7316 - if ( isset($_POST['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'] ) )
7317 7743 {
7318 - $selected_status = ph_clean($_POST['selected_status']);
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'] ) );
7319 7746 }
7320 7747
7321 7748 include( PH()->plugin_path() . '/includes/admin/views/html-contact-offers-meta-box.php' );
7322 7749
@@ -7332,12 +7759,14 @@
7332 7759 global $post;
7333 7760
7334 7761 check_ajax_referer( 'sale-details-meta-box', 'security' );
7335 7762
7336 - $post = get_post((int)$_POST['sale_id']);
7763 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7337 7764
7338 - $sale = new PH_Offer((int)$_POST['sale_id']);
7765 + $post = get_post( $post_id );
7339 7766
7767 + $sale = new PH_Offer( $post_id );
7768 +
7340 7769 echo '<div class="propertyhive_meta_box">';
7341 7770
7342 7771 echo '<div class="options_group">';
7343 7772
@@ -7346,9 +7775,9 @@
7346 7775 echo '<p class="form-field">
7347 7776
7348 7777 <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
7349 7778
7350 - ' . esc_html(__( ucwords(str_replace("_", " ", $sale->status)), 'propertyhive' )) . '
7779 + ' . esc_html(propertyhive_get_status_label( $sale->status )) . '
7351 7780
7352 7781 </p>';
7353 7782 }
7354 7783
@@ -7354,9 +7783,9 @@
7354 7783
7355 7784 $sale_date_time = $sale->sale_date_time;
7356 7785 if ( empty($sale_date_time) )
7357 7786 {
7358 - $sale_date_time = date("Y-m-d H:i:s");
7787 + $sale_date_time = gmdate("Y-m-d H:i:s");
7359 7788 }
7360 7789
7361 7790 echo '<p class="form-field sale_date_field">
7362 7791
@@ -7361,9 +7790,9 @@
7361 7790 echo '<p class="form-field sale_date_field">
7362 7791
7363 7792 <label for="_sale_date">' . esc_html(__('Sale Date', 'propertyhive')) . '</label>
7364 7793
7365 - <input type="date" class="small" name="_sale_date" id="_sale_date" value="' . esc_attr(date("Y-m-d", strtotime($sale_date_time))) . '" placeholder="">
7794 + <input type="date" class="small" name="_sale_date" id="_sale_date" value="' . esc_attr(gmdate("Y-m-d", strtotime($sale_date_time))) . '" placeholder="">
7366 7795
7367 7796 </p>';
7368 7797
7369 7798 $args = array(
@@ -7390,9 +7819,9 @@
7390 7819 public function get_sale_actions()
7391 7820 {
7392 7821 check_ajax_referer( 'sale-actions', 'security' );
7393 7822
7394 - $post_id = (int)$_POST['sale_id'];
7823 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7395 7824
7396 7825 $status = get_post_meta( $post_id, '_status', TRUE );
7397 7826
7398 7827 // Success action panel
@@ -7401,9 +7830,9 @@
7401 7830 <div class="options_group" style="padding-top:8px;">
7402 7831
7403 7832 <div id="success_actions"></div>
7404 7833
7405 - <a class="button action-cancel" style="width:100%;" href="#">' . __( 'Back To Actions', 'propertyhive' ) . '</a>
7834 + <a class="button action-cancel" style="width:100%;" href="#">' . esc_html__( 'Back To Actions', 'propertyhive' ) . '</a>
7406 7835
7407 7836 </div>
7408 7837
7409 7838 </div>';
@@ -7454,8 +7883,9 @@
7454 7883 $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
7455 7884
7456 7885 if ( !empty($actions) )
7457 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.
7458 7888 echo implode("", $actions);
7459 7889 }
7460 7890 else
7461 7891 {
@@ -7472,9 +7902,9 @@
7472 7902 public function sale_exchanged()
7473 7903 {
7474 7904 check_ajax_referer( 'sale-actions', 'security' );
7475 7905
7476 - $post_id = (int)$_POST['sale_id'];
7906 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7477 7907
7478 7908 $status = get_post_meta( $post_id, '_status', TRUE );
7479 7909
7480 7910 if ( $status == 'current' )
@@ -7498,9 +7928,9 @@
7498 7928 public function sale_completed()
7499 7929 {
7500 7930 check_ajax_referer( 'sale-actions', 'security' );
7501 7931
7502 - $post_id = (int)$_POST['sale_id'];
7932 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7503 7933
7504 7934 $status = get_post_meta( $post_id, '_status', TRUE );
7505 7935
7506 7936 if ( $status == 'exchanged' )
@@ -7524,9 +7954,9 @@
7524 7954 public function sale_fallen_through()
7525 7955 {
7526 7956 check_ajax_referer( 'sale-actions', 'security' );
7527 7957
7528 - $post_id = (int)$_POST['sale_id'];
7958 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7529 7959
7530 7960 $status = get_post_meta( $post_id, '_status', TRUE );
7531 7961
7532 7962 if ( $status == 'current' || $status == 'exchanged' )
@@ -7548,14 +7978,16 @@
7548 7978 }
7549 7979
7550 7980 public function get_property_sales_meta_box()
7551 7981 {
7552 - $post_id = (int)$_POST['post_id'];
7982 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
7553 7983
7554 7984 $selected_status = '';
7555 - if ( isset($_POST['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'] ) )
7556 7987 {
7557 - $selected_status = ph_clean($_POST['selected_status']);
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'] ) );
7558 7990 }
7559 7991
7560 7992 include( PH()->plugin_path() . '/includes/admin/views/html-property-sales-meta-box.php' );
7561 7993
@@ -7566,14 +7998,16 @@
7566 7998 }
7567 7999
7568 8000 public function get_contact_sales_meta_box()
7569 8001 {
7570 - $post_id = (int)$_POST['post_id'];
8002 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
7571 8003
7572 8004 $selected_status = '';
7573 - if ( isset($_POST['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'] ) )
7574 8007 {
7575 - $selected_status = ph_clean($_POST['selected_status']);
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'] ) );
7576 8010 }
7577 8011
7578 8012 include( PH()->plugin_path() . '/includes/admin/views/html-contact-sales-meta-box.php' );
7579 8013
@@ -7584,14 +8018,16 @@
7584 8018 }
7585 8019
7586 8020 public function get_property_enquiries_meta_box()
7587 8021 {
7588 - $post_id = (int)$_POST['post_id'];
8022 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
7589 8023
7590 8024 $selected_status = '';
7591 - if ( isset($_POST['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'] ) )
7592 8027 {
7593 - $selected_status = ph_clean($_POST['selected_status']);
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'] ) );
7594 8030 }
7595 8031
7596 8032 include( PH()->plugin_path() . '/includes/admin/views/html-property-enquiries-meta-box.php' );
7597 8033
@@ -7602,14 +8038,16 @@
7602 8038 }
7603 8039
7604 8040 public function get_contact_enquiries_meta_box()
7605 8041 {
7606 - $post_id = (int)$_POST['post_id'];
8042 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
7607 8043
7608 8044 $selected_status = '';
7609 - if ( isset($_POST['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'] ) )
7610 8047 {
7611 - $selected_status = ph_clean($_POST['selected_status']);
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'] ) );
7612 8050 }
7613 8051
7614 8052 include( PH()->plugin_path() . '/includes/admin/views/html-contact-enquiries-meta-box.php' );
7615 8053
@@ -7621,77 +8059,75 @@
7621 8059
7622 8060 /**
7623 8061 * Add new management key date via ajax
7624 8062 */
7625 - public function add_key_date()
7626 - {
7627 - $parent_post_id = (int)$_POST['post_id'];
7628 -
7629 - if ( $parent_post_id > 0 ) {
7630 - $date_description = wp_kses_post( trim( stripslashes( $_POST['key_date_description'] ) ) );
7631 - $date_type_id = ph_clean( stripslashes( $_POST['key_date_type'] ) );
7632 - $date_due = ph_clean($_POST['key_date_due']) . ' ' . ph_clean($_POST['key_date_hours']) . ':' . ph_clean($_POST['key_date_minutes']);
7633 - $date_notes = sanitize_textarea_field($_POST['key_date_notes']);
7634 -
7635 - $parent_post_type = get_post_type( $parent_post_id );
7636 -
7637 - // Insert key date record
7638 - $key_date_post = array(
7639 - 'post_title' => $date_description,
7640 - 'post_content' => '',
7641 - 'post_type' => 'key_date',
7642 - 'post_status' => 'publish',
7643 - 'comment_status' => 'closed',
7644 - 'ping_status' => 'closed',
7645 - );
7646 -
7647 - // Insert the post into the database
7648 - $key_date_post_id = wp_insert_post( $key_date_post );
7649 -
7650 - if ( is_wp_error($key_date_post_id) || $key_date_post_id == 0 )
7651 - {
7652 - $return = array('error' => 'Failed to create key date post. Please try again');
7653 - echo json_encode( $return );
7654 - die();
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 );
7655 8077 }
7656 -
7657 - add_post_meta( $key_date_post_id, '_date_due', $date_due );
7658 - add_post_meta( $key_date_post_id, '_key_date_status', 'pending' );
7659 - add_post_meta( $key_date_post_id, '_key_date_type_id', $date_type_id );
7660 - add_post_meta( $key_date_post_id, '_key_date_notes', $date_notes );
7661 -
7662 - switch ( $parent_post_type )
7663 - {
7664 - case 'property' :
7665 - {
7666 - add_post_meta( $key_date_post_id, '_property_id', $parent_post_id );
7667 - break;
7668 - }
7669 - case 'tenancy' :
7670 - {
7671 - add_post_meta( $key_date_post_id, '_tenancy_id', $parent_post_id );
7672 -
7673 - $parent_property_id = get_post_meta( $parent_post_id, '_property_id', true );
7674 - add_post_meta( $key_date_post_id, '_property_id', $parent_property_id );
7675 - break;
7676 - }
7677 - }
8078 + $details[$field] = sanitize_text_field( wp_unslash( $_POST[$field] ) );
7678 8079 }
7679 - die();
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 ) );
7680 8111 }
7681 8112
7682 8113 public function get_management_dates_grid()
7683 8114 {
7684 - $post_id = (int)$_POST['post_id'];
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' ) );
7685 8117
7686 - if ( isset($_POST['selected_type_id']) )
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'] ) )
7687 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.
7688 8122 $selected_type_id = (int)$_POST['selected_type_id'];
7689 8123 }
7690 8124
7691 - if ( isset($_POST['selected_status']) )
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'] ) )
7692 8127 {
7693 - $selected_status = ph_clean($_POST['selected_status']);
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'] ) );
7694 8130 }
7695 8131
7696 8132 include( PH()->plugin_path() . '/includes/admin/views/html-management-dates-meta-box.php' );
7697 8133
@@ -7700,9 +8136,10 @@
7700 8136 }
7701 8137
7702 8138 public function get_key_dates_quick_edit_row()
7703 8139 {
7704 - $post_id = (int)$_POST['post_id'];
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' ) );
7705 8142
7706 8143 include( PH()->plugin_path() . '/includes/admin/views/html-key-dates-quick-edit.php' );
7707 8144
7708 8145 // Quit out
@@ -7710,9 +8147,10 @@
7710 8147 }
7711 8148
7712 8149 public function check_key_date_recurrence()
7713 8150 {
7714 - $post_id = (int)$_POST['post_id'];
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' );
7715 8153
7716 8154 $next_key_date = '';
7717 8155
7718 8156 $key_date = new PH_Key_Date(get_post($post_id));
@@ -7772,26 +8210,43 @@
7772 8210
7773 8211 if ( ! current_user_can( 'manage_propertyhive' ) )
7774 8212 wp_send_json_error( __( 'You do not have permission to manage key dates', 'propertyhive' ), 403 );
7775 8213
7776 - $key_date_post_id = (int)$_POST['post_id'];
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 );
8222 + }
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 + }
7777 8232
7778 8233 $args = array(
7779 8234 'ID' => $key_date_post_id,
7780 - 'post_title' => ph_clean($_POST['description']),
8235 + 'post_title' => $date_input['description'],
7781 8236 );
7782 - wp_update_post( $args );
8237 + wp_update_post( wp_slash( $args ) );
7783 8238
7784 - update_post_meta( $key_date_post_id, '_date_due', ph_clean($_POST['due_date_time']) );
7785 - update_post_meta( $key_date_post_id, '_key_date_status', ph_clean($_POST['status']) );
7786 - update_post_meta( $key_date_post_id, '_key_date_type_id', (int)$_POST['type'] );
7787 - update_post_meta( $key_date_post_id, '_key_date_notes', sanitize_textarea_field($_POST['notes'] ));
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'] ));
7788 8243
7789 - if ( isset($_POST['next_key_date']) )
8244 + if ( null !== $next_key_date )
7790 8245 {
7791 8246 // Insert next key date record
7792 8247 $next_key_date_post = array(
7793 - 'post_title' => ph_clean($_POST['description']),
8248 + 'post_title' => $date_input['description'],
7794 8249 'post_content' => '',
7795 8250 'post_type' => 'key_date',
7796 8251 'post_status' => 'publish',
7797 8252 'comment_status' => 'closed',
@@ -7798,9 +8253,9 @@
7798 8253 'ping_status' => 'closed',
7799 8254 );
7800 8255
7801 8256 // Insert the post into the database
7802 - $next_key_date_post_id = wp_insert_post( $next_key_date_post );
8257 + $next_key_date_post_id = wp_insert_post( wp_slash( $next_key_date_post ) );
7803 8258
7804 8259 if ( is_wp_error($next_key_date_post_id) || $next_key_date_post_id == 0 )
7805 8260 {
7806 8261 $return = array('error' => 'Failed to create next key date post. Please try again');
@@ -7807,11 +8262,11 @@
7807 8262 echo json_encode( $return );
7808 8263 die();
7809 8264 }
7810 8265
7811 - add_post_meta( $next_key_date_post_id, '_date_due', ph_clean($_POST['next_key_date']) );
8266 + add_post_meta( $next_key_date_post_id, '_date_due', $next_key_date );
7812 8267 add_post_meta( $next_key_date_post_id, '_key_date_status', 'pending' );
7813 - add_post_meta( $next_key_date_post_id, '_key_date_type_id', (int)$_POST['type'] );
8268 + add_post_meta( $next_key_date_post_id, '_key_date_type_id', absint( $date_input['type'] ) );
7814 8269
7815 8270 if ( metadata_exists('post', $key_date_post_id, '_property_id') ) {
7816 8271 add_post_meta( $next_key_date_post_id, '_property_id', get_post_meta($key_date_post_id, '_property_id', true) );
7817 8272 }
@@ -7832,9 +8287,12 @@
7832 8287
7833 8288 if ( ! current_user_can( 'manage_propertyhive' ) )
7834 8289 wp_send_json_error( __( 'You do not have permission to manage key dates', 'propertyhive' ), 403 );
7835 8290
7836 - $date_post_id = (int)$_POST['date_post_id'];
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 + }
7837 8295
7838 8296 wp_delete_post($date_post_id, TRUE);
7839 8297
7840 8298 $return = array('success' => true);
@@ -7844,13 +8302,15 @@
7844 8302 }
7845 8303
7846 8304 public function get_property_tenancies_grid()
7847 8305 {
7848 - $post_id = (int)$_POST['post_id'];
8306 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
7849 8307
7850 - if ( isset($_POST['selected_status']) )
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'] ) )
7851 8310 {
7852 - $selected_status = ph_clean($_POST['selected_status']);
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'] ) );
7853 8313 }
7854 8314
7855 8315 include( PH()->plugin_path() . '/includes/admin/views/html-property-tenancies-meta-box.php' );
7856 8316
@@ -7859,13 +8319,15 @@
7859 8319 }
7860 8320
7861 8321 public function get_contact_tenancies_grid()
7862 8322 {
7863 - $post_id = (int)$_POST['post_id'];
8323 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
7864 8324
7865 - if ( isset($_POST['selected_status']) )
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'] ) )
7866 8327 {
7867 - $selected_status = ph_clean($_POST['selected_status']);
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'] ) );
7868 8330 }
7869 8331
7870 8332 include( PH()->plugin_path() . '/includes/admin/views/html-contact-tenancies-meta-box.php' );
7871 8333
@@ -7874,18 +8336,22 @@
7874 8336 }
7875 8337
7876 8338 public function get_contact_solicitor()
7877 8339 {
7878 - switch( get_post_type((int)$_POST['post_id']) )
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 ) )
7879 8343 {
7880 8344 case 'contact':
7881 8345 {
7882 - $contact_post_ids = array( (int)$_POST['post_id'] );
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 );
7883 8348 break;
7884 8349 }
7885 8350 case 'property':
7886 8351 {
7887 - $owner_contact_ids = get_post_meta((int)$_POST['post_id'], '_owner_contact_id', TRUE);
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);
7888 8354 if ( !empty( $owner_contact_ids ) )
7889 8355 {
7890 8356 if ( !is_array($owner_contact_ids) )
7891 8357 {
@@ -7927,9 +8393,9 @@
7927 8393 }
7928 8394
7929 8395 public function activate_pro_feature()
7930 8396 {
7931 - if ( !wp_verify_nonce( $_POST['_ajax_nonce'], "updates" ) )
8397 + if ( ! isset( $_POST['_ajax_nonce'] ) || ! is_string( $_POST['_ajax_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ajax_nonce'] ) ), 'updates' ) )
7932 8398 {
7933 8399 $return = array(
7934 8400 'errorMessage' => 'Invalid nonce provided'
7935 8401 );
@@ -7935,18 +8401,18 @@
7935 8401 );
7936 8402 wp_send_json_error($return);
7937 8403 }
7938 8404
7939 - if ( ! current_user_can( 'install_plugins' ) )
8405 + if ( ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'install_plugins' ) )
7940 8406 {
7941 8407 $return = array(
7942 - 'errorMessage' => __( 'Sorry, you are not allowed to manage plugins on this site.' )
8408 + 'errorMessage' => __( 'Sorry, you are not allowed to manage plugins on this site.', 'propertyhive' )
7943 8409 );
7944 8410 wp_send_json_error( $return );
7945 8411 }
7946 8412
7947 8413 // check plugin status
7948 - $slug = ph_clean($_POST['slug']);
8414 + $slug = isset( $_POST['slug'] ) && is_string( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
7949 8415
7950 8416 $feature = get_ph_pro_feature( $slug );
7951 8417
7952 8418 if ( $feature === false )
@@ -8109,40 +8575,39 @@
8109 8575 );
8110 8576 wp_send_json_error($return);
8111 8577 }
8112 8578
8113 - $tmpfname = WP_PLUGIN_DIR . '/' . $slug . '.zip';
8114 -
8115 - $handle = @fopen($tmpfname, "w");
8116 - if ( $handle === false )
8117 - {
8118 - $return = array(
8119 - 'errorMessage' => 'Failed to write plugin contents to temp file: ' . $tmpfname
8120 - );
8121 - wp_send_json_error($return);
8579 + $tmpfname = wp_tempnam( $slug . '.zip' );
8580 + if ( ! $tmpfname ) {
8581 + wp_send_json_error( array( 'errorMessage' => __( 'Unable to create a temporary download file.', 'propertyhive' ) ) );
8122 8582 }
8123 - fwrite($handle, $zip_contents);
8124 - fclose($handle);
8125 8583
8126 - global $wp_filesystem;
8127 -
8128 8584 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
8129 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 + }
8130 8591
8592 + global $wp_filesystem;
8131 8593 $wp_filesystem = new WP_Filesystem_Direct( false );
8132 8594
8133 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.
8134 8597 define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ) );
8135 8598 }
8136 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.
8137 8601 define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 | 0755 ) );
8138 8602 }
8139 8603
8140 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.
8141 8606 $unzipped = unzip_file( $tmpfname, WP_PLUGIN_DIR );
8142 8607 if ( is_wp_error( $unzipped ) )
8143 8608 {
8144 - @unlink($tmpfname);
8609 + @wp_delete_file($tmpfname);
8145 8610
8146 8611 $return = array(
8147 8612 'errorMessage' => $unzipped->get_error_message()
8148 8613 );
@@ -8148,9 +8613,9 @@
8148 8613 );
8149 8614 wp_send_json_error($return);
8150 8615 }
8151 8616
8152 - @unlink($tmpfname);
8617 + @wp_delete_file($tmpfname);
8153 8618
8154 8619 // Need to sort out cache for activate plugin to work
8155 8620 // Taken from WordPress.org docs
8156 8621 $cache_plugins = wp_cache_get( 'plugins', 'plugins' );
@@ -8199,9 +8664,9 @@
8199 8664 }
8200 8665
8201 8666 public function deactivate_pro_feature()
8202 8667 {
8203 - if ( !wp_verify_nonce( $_POST['_ajax_nonce'], "updates" ) )
8668 + if ( ! isset( $_POST['_ajax_nonce'] ) || ! is_string( $_POST['_ajax_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_ajax_nonce'] ) ), 'updates' ) )
8204 8669 {
8205 8670 $return = array(
8206 8671 'errorMessage' => 'Invalid nonce provided'
8207 8672 );
@@ -8207,22 +8672,22 @@
8207 8672 );
8208 8673 wp_send_json_error($return);
8209 8674 }
8210 8675
8211 - if ( ! current_user_can( 'install_plugins' ) )
8676 + if ( ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'install_plugins' ) )
8212 8677 {
8213 8678 $return = array(
8214 - 'errorMessage' => __( 'Sorry, you are not allowed to manage plugins on this site.' )
8679 + 'errorMessage' => __( 'Sorry, you are not allowed to manage plugins on this site.', 'propertyhive' )
8215 8680 );
8216 8681 wp_send_json_error( $return );
8217 8682 }
8218 8683
8219 8684 // check plugin is active
8220 - $slug = ph_clean($_POST['slug']);
8685 + $slug = isset( $_POST['slug'] ) && is_string( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
8221 8686
8222 8687 $feature = get_ph_pro_feature( $slug );
8223 8688
8224 - if ( !is_plugin_active( $feature['wordpress_plugin_file'] ) )
8689 + if ( false === $feature || ! is_plugin_active( $feature['wordpress_plugin_file'] ) )
8225 8690 {
8226 8691 $return = array(
8227 8692 'errorMessage' => 'Plugin not active'
8228 8693 );