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 +1107 -586 2.2.22.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,11 +2419,12 @@
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 - $errors[] = __( 'Property ID is a required field and must be supplied when making an enquiry', 'propertyhive' ) . ': ' . $key;
2426 + $errors[] = __( 'Property ID is a required field and must be supplied when making an enquiry', 'propertyhive' );
2163 2427 }
2164 2428 else
2165 2429 {
2166 2430 //$post = get_post((int)$_POST['property_id']);
@@ -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 )
@@ -2278,9 +2549,61 @@
2278 2549 )
2279 2550 {
2280 2551 $errors[] = __( 'Missing required field', 'propertyhive' ) . ': disclaimer';
2281 2552 }
2282 -
2553 +
2554 + // Check only expected fields are received
2555 + /*$allowed_keys = array_keys($form_controls);
2556 + $allowed_keys[] = 'action';
2557 + $allowed_keys[] = 'utm_source';
2558 + $allowed_keys[] = 'utm_medium';
2559 + $allowed_keys[] = 'utm_term';
2560 + $allowed_keys[] = 'utm_content';
2561 + $allowed_keys[] = 'utm_campaign';
2562 + $allowed_keys[] = 'gclid';
2563 + $allowed_keys[] = 'fbclid';
2564 + $allowed_keys[] = 'property_id';
2565 + $allowed_keys[] = 'disclaimer';
2566 + $allowed_keys[] = 'g-recaptcha-response';
2567 + $allowed_keys[] = 'h-captcha-response';
2568 + $allowed_keys[] = 'cf-turnstile-response';
2569 +
2570 + $allowed_keys = apply_filters(
2571 + 'propertyhive_property_enquiry_allowed_keys',
2572 + $allowed_keys
2573 + );
2574 +
2575 + foreach ( $_POST as $key => $value )
2576 + {
2577 + if ( !in_array($key, $allowed_keys) )
2578 + {
2579 + // Unexpected field
2580 + $errors[] = sprintf(
2581 + esc_html__( 'Unexpected field %s received', 'propertyhive' ),
2582 + esc_html( $key )
2583 + );
2584 + break;
2585 + }
2586 + }*/
2587 +
2588 + // Passed validation
2589 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2590 + $property_ids = isset( $_POST['property_id'] ) && is_string( $_POST['property_id'] ) ? array_values( array_filter( array_map( 'absint', explode( '|', sanitize_text_field( wp_unslash( $_POST['property_id'] ) ) ) ) ) ) : array();
2591 + if ( empty( $property_ids ) ) {
2592 + $errors[] = __( 'Invalid property supplied', 'propertyhive' );
2593 + }
2594 + if ( count( $property_ids ) > 100 ) {
2595 + $errors[] = __( 'Too many properties supplied.', 'propertyhive' );
2596 + }
2597 + foreach ( $property_ids as $property_id )
2598 + {
2599 + if ( get_post_type( $property_id ) !== 'property' || ! propertyhive_is_post_publicly_viewable( $property_id ) )
2600 + {
2601 + $errors[] = __( 'Invalid property supplied', 'propertyhive' );
2602 + break;
2603 + }
2604 + }
2605 +
2283 2606 if ( !empty($errors) )
2284 2607 {
2285 2608 // Failed validation
2286 2609
@@ -2289,11 +2612,8 @@
2289 2612 $return['errors'] = $errors;
2290 2613 }
2291 2614 else
2292 2615 {
2293 - // Passed validation
2294 - $property_ids = explode("|", ph_clean($_POST['property_id']));
2295 -
2296 2616 // Get recipient email address
2297 2617 $to = '';
2298 2618
2299 2619 // Try and get office's email address first, else fallback to admin email
@@ -2368,9 +2688,9 @@
2368 2688 $message .= ( count($property_ids) > 1 ? __( 'Properties', 'propertyhive' ) : __( 'Property', 'propertyhive' ) ) . ":\n";
2369 2689 foreach ( $property_ids as $property_id )
2370 2690 {
2371 2691 $property = new PH_Property((int)$property_id);
2372 - $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";
2373 2693 }
2374 2694
2375 2695 unset($form_controls['action']);
2376 2696 unset($_POST['action']);
@@ -2383,11 +2703,12 @@
2383 2703 if ( isset($control['type']) && in_array($control['type'], array('html', 'recaptcha', 'recaptcha-v3', 'hCaptcha', 'turnstile')) ) { continue; }
2384 2704
2385 2705 $label = ( isset($control['label']) ) ? $control['label'] : $key;
2386 2706 $label = ( isset($control['email_label']) ) ? $control['email_label'] : $label;
2387 - $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] ) ) : '';
2388 2709
2389 - $message .= strip_tags($label) . ": " . strip_tags($value) . "\n";
2710 + $message .= wp_strip_all_tags($label) . ": " . wp_strip_all_tags($value) . "\n";
2390 2711 }
2391 2712
2392 2713 if (
2393 2714 apply_filters('propertyhive_enquiry_email_show_manage_link', true) &&
@@ -2411,23 +2732,42 @@
2411 2732 }
2412 2733 if ( $from_email_address == '' )
2413 2734 {
2414 2735 // Should never get here
2415 - $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'] ) ) : '';
2416 2738 }
2417 2739
2418 2740 $headers = array();
2419 - if ( isset($_POST['name']) && ! empty($_POST['name']) )
2741 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2742 + $name = isset( $_POST['name'] )
2743 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2744 + ? sanitize_text_field( wp_unslash( $_POST['name'] ) )
2745 + : '';
2746 +
2747 + $name = str_replace( array( "\r", "\n" ), '', $name );
2748 +
2749 + $from_email_address = sanitize_email( $from_email_address );
2750 +
2751 + if ( $name !== '' )
2420 2752 {
2421 - $headers[] = 'From: ' . html_entity_decode(ph_clean( $_POST['name'] )) . ' <' . sanitize_email( $from_email_address ) . '>';
2753 + $headers[] = sprintf( 'From: %s <%s>', $name, $from_email_address );
2422 2754 }
2423 2755 else
2424 2756 {
2425 - $headers[] = 'From: <' . sanitize_email( $from_email_address ) . '>';
2757 + $headers[] = sprintf( 'From: <%s>', $from_email_address );
2426 2758 }
2427 - if ( isset($_POST['email_address']) && sanitize_email( $_POST['email_address'] ) != '' )
2759 +
2760 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2761 + if ( isset($_POST['email_address']) )
2428 2762 {
2429 - $headers[] = 'Reply-To: ' . sanitize_email( $_POST['email_address'] );
2763 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2764 + $reply_to = sanitize_email(wp_unslash($_POST['email_address']));
2765 +
2766 + if ( is_email($reply_to) )
2767 + {
2768 + $headers[] = 'Reply-To: ' . $reply_to;
2769 + }
2430 2770 }
2431 2771
2432 2772 $to = apply_filters( 'propertyhive_property_enquiry_to', $to, $property_ids );
2433 2773 $subject = apply_filters( 'propertyhive_property_enquiry_subject', $subject, $property_ids );
@@ -2462,11 +2802,13 @@
2462 2802 else
2463 2803 {
2464 2804 $title = __( 'Multiple Property Enquiry', 'propertyhive' );
2465 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.
2466 2807 if ( isset($_POST['name']) && ! empty($_POST['name']) )
2467 2808 {
2468 - $title .= __( ' from ', 'propertyhive' ) . ph_clean($_POST['name']);
2809 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public enquiry submission accepts guest data without using account authority; published-property validation and configured CAPTCHA checks precede delivery/storage. Existing third-party forms share this public contract.
2810 + $title .= ' ' . __( 'from', 'propertyhive' ) . ' ' . ph_clean(wp_unslash($_POST['name']));
2469 2811 }
2470 2812
2471 2813 $enquiry_post = array(
2472 2814 'post_title' => $title,
@@ -2484,24 +2826,34 @@
2484 2826 add_post_meta( $enquiry_post_id, '_source', 'website' );
2485 2827 add_post_meta( $enquiry_post_id, '_negotiator_id', '' );
2486 2828 add_post_meta( $enquiry_post_id, '_office_id', $office_id );
2487 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.
2488 2831 foreach ($_POST as $key => $value)
2489 2832 {
2490 - 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 ) )
2491 2837 {
2838 + continue;
2839 + }
2840 +
2841 + if ( $meta_key == 'property_id' )
2842 + {
2492 2843 foreach ( $property_ids as $property_id )
2493 2844 {
2494 - add_post_meta( $enquiry_post_id, $key, (int)$property_id );
2845 + add_post_meta( $enquiry_post_id, $meta_key, (int)$property_id );
2495 2846 }
2496 2847 }
2497 2848 else
2498 2849 {
2499 - add_post_meta( $enquiry_post_id, $key, sanitize_textarea_field($value) );
2850 + add_post_meta( $enquiry_post_id, $meta_key, sanitize_textarea_field(wp_unslash($value)) );
2500 2851 }
2501 2852 }
2502 2853 }
2503 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.
2504 2856 do_action('propertyhive_property_enquiry_sent', $_POST, $to, $enquiry_post_id);
2505 2857
2506 2858 // Send auto-responder
2507 2859 if ( get_option( 'propertyhive_enquiry_auto_responder', '' ) == 'yes' )
@@ -2506,8 +2858,9 @@
2506 2858 // Send auto-responder
2507 2859 if ( get_option( 'propertyhive_enquiry_auto_responder', '' ) == 'yes' )
2508 2860 {
2509 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.
2510 2863 PH()->email->send_enquiry_auto_responder( $_POST );
2511 2864 }
2512 2865 }
2513 2866 }
@@ -2525,10 +2878,12 @@
2525 2878 public function create_contact_from_enquiry()
2526 2879 {
2527 2880 global $post;
2528 2881
2529 - $enquiry_post_id = ( (isset($_POST['post_id'])) ? (int)$_POST['post_id'] : '' );
2530 - $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'] ) ) : '';
2531 2886
2532 2887 if ( ! wp_verify_nonce( $nonce, 'create-contact-from-enquiry-nonce-' . $enquiry_post_id ) )
2533 2888 {
2534 2889 // This nonce is not valid.
@@ -2600,9 +2955,9 @@
2600 2955
2601 2956 $postdata = array(
2602 2957 'post_excerpt' => '',
2603 2958 'post_content' => '',
2604 - 'post_title' => utf8_encode(wp_strip_all_tags( $name )),
2959 + 'post_title' => wp_strip_all_tags( $name ),
2605 2960 'post_status' => 'publish',
2606 2961 'post_type' => 'contact',
2607 2962 'ping_status' => 'closed',
2608 2963 'comment_status' => 'closed',
@@ -2757,15 +3112,21 @@
2757 3112 check_ajax_referer( 'contact-save-validation', 'security' );
2758 3113
2759 3114 $this->json_headers();
2760 3115
2761 - 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;
2762 3123
2763 3124 $return = array('errors' => array());
2764 3125
2765 - if ( isset($_email_address) && $_email_address != '' )
3126 + if ( '' !== $email_address_input )
2766 3127 {
2767 - $email_addresses = explode( ",", $_email_address );
3128 + $email_addresses = explode( ",", $email_address_input );
2768 3129
2769 3130 foreach ( $email_addresses as $email_address )
2770 3131 {
2771 3132 $email_address = trim( $email_address );
@@ -2780,8 +3141,9 @@
2780 3141 'post_type' => 'contact',
2781 3142 'post_status' => 'any',
2782 3143 'posts_per_page' => 1,
2783 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.
2784 3146 'meta_query' => array(
2785 3147 'relation' => 'OR',
2786 3148 array(
2787 3149 'key' => '_email_address',
@@ -2800,11 +3162,12 @@
2800 3162 'compare' => 'LIKE'
2801 3163 )
2802 3164 )
2803 3165 );
2804 - if ( isset($post_ID) && $post_ID != '' )
3166 + if ( $contact_id )
2805 3167 {
2806 - $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 );
2807 3170 }
2808 3171
2809 3172 $contact_query = new WP_Query( $args );
2810 3173
@@ -2813,9 +3176,10 @@
2813 3176 while ( $contact_query->have_posts() )
2814 3177 {
2815 3178 $contact_query->the_post();
2816 3179
2817 - $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 );
2818 3182 }
2819 3183 }
2820 3184 }
2821 3185 }
@@ -2835,9 +3199,9 @@
2835 3199 echo json_encode( $return );
2836 3200 die();
2837 3201 }
2838 3202
2839 - 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'] ) )
2840 3204 {
2841 3205 $return = array('error' => 'Invalid parameters received');
2842 3206 echo json_encode( $return );
2843 3207 die();
@@ -2842,13 +3206,13 @@
2842 3206 echo json_encode( $return );
2843 3207 die();
2844 3208 }
2845 3209
2846 - $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'] ) ) ) ) ) ) );
2847 3211
2848 3212 $primary_contact_id = absint( wp_unslash( $_POST['primary_contact_id'] ) );
2849 3213
2850 - 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 ) )
2851 3215 {
2852 3216 $return = array('error' => 'Invalid Contact IDs received');
2853 3217 echo json_encode( $return );
2854 3218 die();
@@ -2860,9 +3224,9 @@
2860 3224 echo json_encode( $return );
2861 3225 die();
2862 3226 }
2863 3227
2864 - if ( !current_user_can( 'edit_post', $primary_contact_id ) )
3228 + if ( !current_user_can( 'manage_propertyhive' ) || !current_user_can( 'edit_post', $primary_contact_id ) )
2865 3229 {
2866 3230 $return = array('error' => 'Insufficient permissions for primary contact');
2867 3231 echo json_encode( $return );
2868 3232 die();
@@ -2888,9 +3252,9 @@
2888 3252
2889 3253 // Remove primary from list
2890 3254 unset($contacts_to_merge[array_search($primary_contact_id, $contacts_to_merge)]);
2891 3255
2892 - include_once( 'includes/class-ph-admin-merge-contacts.php' );
3256 + include_once PH()->plugin_path() . '/includes/admin/class-ph-admin-merge-contacts.php';
2893 3257 $ph_admin_merge_contacts = new PH_Admin_Merge_Contacts();
2894 3258 $ph_admin_merge_contacts->do_merge( $primary_contact_id, $contacts_to_merge );
2895 3259
2896 3260 echo json_encode( array('success' => true) );
@@ -2946,8 +3310,9 @@
2946 3310 $args = array(
2947 3311 'post_type' => 'viewing',
2948 3312 'fields' => 'ids',
2949 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.
2950 3315 'meta_query' => array(
2951 3316 array(
2952 3317 'key' => '_status',
2953 3318 'value' => 'carried_out'
@@ -2977,9 +3342,9 @@
2977 3342 $return[] = array(
2978 3343 'ID' => get_the_ID(),
2979 3344 'edit_link' => get_edit_post_link( get_the_ID() ),
2980 3345 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
2981 - '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 ))),
2982 3347 'property_id' => $property_id,
2983 3348 'property_address' => $property->get_formatted_full_address(),
2984 3349 'applicant_contact_id' => $applicant_contact_ids[0],
2985 3350 'applicant_name' => get_the_title( $applicant_contact_ids[0] ),
@@ -3005,8 +3370,9 @@
3005 3370 $args = array(
3006 3371 'post_type' => 'viewing',
3007 3372 'fields' => 'ids',
3008 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.
3009 3375 'meta_query' => array(
3010 3376 array(
3011 3377 'key' => '_status',
3012 3378 'value' => 'pending'
@@ -3012,9 +3378,9 @@
3012 3378 'value' => 'pending'
3013 3379 ),
3014 3380 array(
3015 3381 'key' => '_start_date_time',
3016 - 'value' => date("Y-m-d H:i:s"),
3382 + 'value' => gmdate("Y-m-d H:i:s"),
3017 3383 'compare' => '>='
3018 3384 ),
3019 3385 array(
3020 3386 'key' => '_negotiator_id',
@@ -3040,9 +3406,9 @@
3040 3406 $return[] = array(
3041 3407 'ID' => get_the_ID(),
3042 3408 'edit_link' => get_edit_post_link( get_the_ID() ),
3043 3409 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
3044 - '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 ))),
3045 3411 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
3046 3412 'title' => 'Viewing at ' . $property->get_formatted_full_address(),
3047 3413 );
3048 3414 }
@@ -3053,8 +3419,9 @@
3053 3419 $args = array(
3054 3420 'post_type' => 'appraisal',
3055 3421 'fields' => 'ids',
3056 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.
3057 3424 'meta_query' => array(
3058 3425 array(
3059 3426 'key' => '_status',
3060 3427 'value' => 'pending'
@@ -3060,9 +3427,9 @@
3060 3427 'value' => 'pending'
3061 3428 ),
3062 3429 array(
3063 3430 'key' => '_start_date_time',
3064 - 'value' => date("Y-m-d H:i:s"),
3431 + 'value' => gmdate("Y-m-d H:i:s"),
3065 3432 'compare' => '>='
3066 3433 ),
3067 3434 array(
3068 3435 'key' => '_negotiator_id',
@@ -3087,9 +3454,9 @@
3087 3454 $return[] = array(
3088 3455 'ID' => get_the_ID(),
3089 3456 'edit_link' => get_edit_post_link( get_the_ID() ),
3090 3457 'start_date_time' => get_post_meta( get_the_ID(), '_start_date_time', TRUE ),
3091 - '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 ))),
3092 3459 'start_date_time_timestamp' => strtotime(get_post_meta( get_the_ID(), '_start_date_time', TRUE )),
3093 3460 'title' => 'Appraisal at ' . $appraisal->get_formatted_full_address(),
3094 3461 );
3095 3462 }
@@ -3141,9 +3508,11 @@
3141 3508 $args = array(
3142 3509 'post_type' => 'key_date',
3143 3510 'fields' => 'ids',
3144 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.
3145 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.
3146 3515 'meta_key' => '_date_due',
3147 3516 'orderby' => 'meta_value',
3148 3517 'order' => 'ASC',
3149 3518 );
@@ -3218,8 +3587,9 @@
3218 3587
3219 3588 $args = array(
3220 3589 'post_type' => 'property',
3221 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.
3222 3592 'meta_query' => array(
3223 3593 array(
3224 3594 'key' => '_on_market',
3225 3595 'value' => 'yes'
@@ -3232,8 +3602,9 @@
3232 3602 );
3233 3603
3234 3604 if ( isset($_POST['post_id']) && !empty($_POST['post_id']) )
3235 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.
3236 3607 $args['post__not_in'] = array((int)$_POST['post_id']);
3237 3608 }
3238 3609
3239 3610 $property_query = new WP_Query($args);
@@ -3251,9 +3622,13 @@
3251 3622 public function osm_geocoding_request()
3252 3623 {
3253 3624 check_ajax_referer( 'osm_geocoding_request', 'security' );
3254 3625
3255 - $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'] ) );
3256 3631
3257 3632 $lat = '';
3258 3633 $lng = '';
3259 3634 $error = '';
@@ -3266,16 +3641,21 @@
3266 3641 if ( $last_ts && ($now - $last_ts) < 1 )
3267 3642 {
3268 3643 // Too soon: tell client to retry shortly
3269 3644 $error = 'Too many geocoding requests. Please wait a second and try again.';
3270 - json_encode(array('error' => $error));
3271 - wp_die();
3645 + wp_send_json( array( 'error' => $error ) );
3272 3646 }
3273 3647
3274 3648 // Set timestamp immediately to prevent stampedes
3275 3649 set_transient( $rate_key, $now );
3276 3650
3277 - $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' );
3278 3658
3279 3659 $response = wp_remote_get(
3280 3660 $request_url,
3281 3661 array(
@@ -3288,17 +3668,15 @@
3288 3668
3289 3669 if ( is_wp_error( $response ))
3290 3670 {
3291 3671 $error = $response->get_error_message();
3292 - echo json_encode(array('error' => $error, 'lat' => $lat, 'lng' => $lng));
3293 - die();
3672 + wp_send_json( array( 'error' => $error, 'lat' => $lat, 'lng' => $lng ) );
3294 3673 }
3295 3674
3296 3675 if ( wp_remote_retrieve_response_code($response) !== 200 )
3297 3676 {
3298 - $error = wp_remote_retrieve_response_code($response) . ' response received when geocoding address ' . ph_clean($_POST['address']) . '. Error message: ' . wp_remote_retrieve_response_message($response);
3299 - echo json_encode(array('error' => $error, 'lat' => $lat, 'lng' => $lng));
3300 - 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 ) );
3301 3679 }
3302 3680
3303 3681 if ( is_array( $response ) )
3304 3682 {
@@ -3311,19 +3689,17 @@
3311 3689 $lng = $json[0]['lon'];
3312 3690 }
3313 3691 else
3314 3692 {
3315 - $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;
3316 3694 }
3317 3695 }
3318 3696 else
3319 3697 {
3320 - $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 );
3321 3699 }
3322 3700
3323 - echo json_encode(array('error' => $error, 'lat' => $lat, 'lng' => $lng));
3324 -
3325 - die();
3701 + wp_send_json( array( 'error' => $error, 'lat' => $lat, 'lng' => $lng ) );
3326 3702 }
3327 3703
3328 3704 public function get_property_marketing_statistics_meta_box()
3329 3705 {
@@ -3329,34 +3705,41 @@
3329 3705 {
3330 3706 check_ajax_referer( 'get_property_marketing_statistics_meta_box', 'security' );
3331 3707
3332 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 + }
3333 3713
3334 - echo '<div class="propertyhive_meta_box">';
3335 -
3336 - echo '<div class="options_group">';
3337 3714
3338 - $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 );
3339 3718 if ( !is_array($view_statistics) )
3340 3719 {
3341 3720 $view_statistics = array();
3342 3721 }
3343 3722
3344 - $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'));
3345 3724 $date_from = strtotime($date_from);
3346 3725
3347 - $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");
3348 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 + }
3349 3731
3732 + echo '<div class="propertyhive_meta_box"><div class="options_group">';
3350 3733 $view_statistics_output = array();
3351 3734 $total_views = 0;
3352 3735
3353 3736 for ($i = $date_from; $i <= $date_to; $i += 86400)
3354 3737 {
3355 - if ( isset($view_statistics[date("Y-m-d", $i)]) )
3738 + if ( isset($view_statistics[gmdate("Y-m-d", $i)]) )
3356 3739 {
3357 - $view_statistics_output[] = array( $i * 1000, $view_statistics[date("Y-m-d", $i)] );
3358 - $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)];
3359 3742 }
3360 3743 else
3361 3744 {
3362 3745 $view_statistics_output[] = array( $i * 1000, 0 );
@@ -3381,11 +3764,12 @@
3381 3764 global $post;
3382 3765
3383 3766 check_ajax_referer( 'appraisal-details-meta-box', 'security' );
3384 3767
3385 - $post = get_post((int)$_POST['appraisal_id']);
3768 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
3769 + $post = get_post( $post_id );
3386 3770
3387 - $appraisal = new PH_Appraisal((int)$_POST['appraisal_id']);
3771 + $appraisal = new PH_Appraisal( $post_id );
3388 3772
3389 3773 echo '<div class="propertyhive_meta_box">';
3390 3774
3391 3775 echo '<div class="options_group">';
@@ -3499,9 +3883,9 @@
3499 3883 public function get_appraisal_actions()
3500 3884 {
3501 3885 check_ajax_referer( 'appraisal-actions', 'security' );
3502 3886
3503 - $post_id = (int)$_POST['appraisal_id'];
3887 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
3504 3888
3505 3889 $status = get_post_meta( $post_id, '_status', TRUE );
3506 3890 $department = get_post_meta( $post_id, '_department', TRUE );
3507 3891
@@ -3532,9 +3916,9 @@
3532 3916 $actions[] = '<a
3533 3917 href="#action_panel_appraisal_email_owner_booking_confirmation_customise"
3534 3918 class="button appraisal-action"
3535 3919 style="width:100%; margin-bottom:7px; text-align:center"
3536 - >' . ( ( $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>';
3537 3921
3538 3922 $show_customise_confirmation_meta_boxes = true;
3539 3923 }
3540 3924 else
@@ -3542,12 +3926,12 @@
3542 3926 $actions[] = '<a
3543 3927 href="#action_panel_appraisal_email_owner_booking_confirmation"
3544 3928 class="button appraisal-action"
3545 3929 style="width:100%; margin-bottom:7px; text-align:center"
3546 - >' . ( ( $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>';
3547 3931 }
3548 3932
3549 - $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>';
3550 3934
3551 3935 $actions[] = '<hr>';
3552 3936 }
3553 3937
@@ -3646,8 +4030,9 @@
3646 4030 $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
3647 4031
3648 4032 if ( !empty($actions) )
3649 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.
3650 4035 echo implode("", $actions);
3651 4036 }
3652 4037 else
3653 4038 {
@@ -3761,9 +4146,9 @@
3761 4146 if ( $department == 'residential-sales' )
3762 4147 {
3763 4148 echo '<div class="form-field">
3764 4149
3765 - <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>
3766 4151
3767 4152 <input type="text" id="_price" name="_price" style="width:100%;" value="' . esc_attr(get_post_meta( $post_id, '_valued_price', TRUE )) . '">
3768 4153
3769 4154 </div>';
@@ -3772,9 +4157,9 @@
3772 4157 {
3773 4158 $rent_frequency = get_post_meta( $post_id, '_valued_rent_frequency', TRUE );
3774 4159 echo '<div class="form-field">
3775 4160
3776 - <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>
3777 4162
3778 4163 <input type="text" id="_price" name="_price" style="width:100%;" value="' . esc_attr(get_post_meta( $post_id, '_valued_rent', TRUE )) . '">
3779 4164
3780 4165 <select id="_rent_frequency" name="_rent_frequency" class="select" style="width:100%">
@@ -3841,35 +4226,57 @@
3841 4226 public function appraisal_carried_out()
3842 4227 {
3843 4228 check_ajax_referer( 'appraisal-actions', 'security' );
3844 4229
3845 - $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 + }
3846 4234
3847 4235 $status = get_post_meta( $post_id, '_status', TRUE );
3848 4236
3849 4237 if ( $status == 'pending' )
3850 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 + }
3851 4258 update_post_meta( $post_id, '_status', 'carried_out' );
3852 4259
3853 4260 if ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-sales' )
3854 4261 {
3855 - $price = preg_replace("/[^0-9.]/", '', ph_clean($_POST['price']));
4262 + $price = preg_replace("/[^0-9.]/", '', $valuation_input['price']);
3856 4263 update_post_meta( $post_id, '_valued_price', $price );
3857 4264 update_post_meta( $post_id, '_valued_price_actual', $price );
3858 4265 }
3859 4266 elseif ( get_post_meta( $post_id, '_department', TRUE ) == 'residential-lettings' )
3860 4267 {
3861 - $rent = preg_replace("/[^0-9.]/", '', ph_clean($_POST['rent']));
4268 + $rent = preg_replace("/[^0-9.]/", '', $valuation_input['rent']);
3862 4269 update_post_meta( $post_id, '_valued_rent', $rent );
3863 4270
3864 - 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'] );
3865 4272
3866 - switch (ph_clean($_POST['rent_frequency']))
4273 + switch ($valuation_input['rent_frequency'])
3867 4274 {
3868 4275 case "pd": { $price = ($rent * 365) / 12; break; }
3869 4276 case "pppw":
3870 4277 {
3871 - $bedrooms = get_post_meta( $postID, '_bedrooms', true );
4278 + $bedrooms = get_post_meta( $post_id, '_bedrooms', true );
3872 4279 if ( ( $bedrooms !== FALSE && $bedrooms != 0 && $bedrooms != '' ) && apply_filters( 'propertyhive_pppw_to_consider_bedrooms', true ) == true )
3873 4280 {
3874 4281 $price = (($rent * 52) / 12) * $bedrooms;
3875 4282 }
@@ -3904,16 +4311,24 @@
3904 4311 public function appraisal_cancelled()
3905 4312 {
3906 4313 check_ajax_referer( 'appraisal-actions', 'security' );
3907 4314
3908 - $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 + }
3909 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 +
3910 4325 $status = get_post_meta( $post_id, '_status', TRUE );
3911 4326
3912 4327 if ( $status == 'pending' )
3913 4328 {
3914 4329 update_post_meta( $post_id, '_status', 'cancelled' );
3915 - update_post_meta( $post_id, '_cancelled_reason', sanitize_textarea_field( $_POST['cancelled_reason'] ) );
4330 + update_post_meta( $post_id, '_cancelled_reason', wp_slash( $reason ) );
3916 4331
3917 4332 // Add note/comment to appraisal
3918 4333 $comment = array(
3919 4334 'note_type' => 'action',
@@ -3931,9 +4346,12 @@
3931 4346 public function appraisal_won()
3932 4347 {
3933 4348 check_ajax_referer( 'appraisal-actions', 'security' );
3934 4349
3935 - $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 + }
3936 4354
3937 4355 $status = get_post_meta( $post_id, '_status', TRUE );
3938 4356
3939 4357 if ( $status == 'carried_out' )
@@ -3957,16 +4375,24 @@
3957 4375 public function appraisal_lost_reason()
3958 4376 {
3959 4377 check_ajax_referer( 'appraisal-actions', 'security' );
3960 4378
3961 - $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 + }
3962 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 +
3963 4389 $status = get_post_meta( $post_id, '_status', TRUE );
3964 4390
3965 4391 if ( $status == 'carried_out' )
3966 4392 {
3967 4393 update_post_meta( $post_id, '_status', 'lost' );
3968 - update_post_meta( $post_id, '_lost_reason', sanitize_textarea_field( $_POST['lost_reason'] ) );
4394 + update_post_meta( $post_id, '_lost_reason', wp_slash( $reason ) );
3969 4395
3970 4396 // Add note/comment to appraisal
3971 4397 $comment = array(
3972 4398 'note_type' => 'action',
@@ -3984,9 +4410,9 @@
3984 4410 public function appraisal_instructed()
3985 4411 {
3986 4412 check_ajax_referer( 'appraisal-actions', 'security' );
3987 4413
3988 - $post_id = (int)$_POST['appraisal_id'];
4414 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
3989 4415
3990 4416 $status = get_post_meta( $post_id, '_status', TRUE );
3991 4417
3992 4418 if ( $status == 'won' )
@@ -4211,8 +4637,9 @@
4211 4637 // get appraisals where this is the owner and where not instructed
4212 4638 $args = array(
4213 4639 'post_type' => 'appraisal',
4214 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.
4215 4642 'meta_query' => array(
4216 4643 array(
4217 4644 'key' => '_property_owner_contact_id',
4218 4645 'value' => $owner_contact_id,
@@ -4259,9 +4686,9 @@
4259 4686 public function appraisal_email_owner_booking_confirmation()
4260 4687 {
4261 4688 check_ajax_referer( 'appraisal-actions', 'security' );
4262 4689
4263 - $post_id = (int)$_POST['appraisal_id'];
4690 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4264 4691
4265 4692 $appraisal = new PH_Appraisal($post_id);
4266 4693
4267 4694 $owner_contact_id = $appraisal->property_owner_contact_id;
@@ -4348,28 +4775,29 @@
4348 4775 }
4349 4776
4350 4777 $to = implode(",", $owner_emails);
4351 4778
4352 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_appraisal_owner_booking_confirmation_email_subject', '' );
4353 - $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', '' );
4354 4781
4355 4782 $appraisal_date_timestamp = strtotime($appraisal->start_date_time);
4356 4783
4357 4784 $subject = str_replace('[property_address]', $appraisal->get_formatted_full_address(), $subject);
4358 4785 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
4359 - $subject = str_replace('[appraisal_time]', date("H:i", $appraisal_date_timestamp), $subject);
4360 - $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);
4361 4788 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
4362 4789 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
4363 4790 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
4364 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.
4365 4793 $subject = apply_filters( 'appraisal_owner_booking_confirmation_email_subject', $subject, $post_id );
4366 4794
4367 4795 $body = str_replace('[property_address]', $appraisal->get_formatted_full_address(), $body);
4368 4796 $body = str_replace('[owner_name]', $owner_names_string, $body);
4369 4797 $body = str_replace('[owner_dear]', $owner_dears_string, $body);
4370 - $body = str_replace('[appraisal_time]', date("H:i", $appraisal_date_timestamp), $body);
4371 - $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);
4372 4800 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
4373 4801 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
4374 4802 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
4375 4803
@@ -4374,8 +4802,9 @@
4374 4802 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
4375 4803
4376 4804 $body = html_entity_decode($body);
4377 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.
4378 4807 $body = apply_filters( 'appraisal_owner_booking_confirmation_email_body', $body, $post_id );
4379 4808
4380 4809 $from = '';
4381 4810 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -4417,9 +4846,9 @@
4417 4846
4418 4847 PH_Comments::insert_note( $post_id, $comment );
4419 4848 }
4420 4849
4421 - 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") );
4422 4851
4423 4852 wp_send_json_success();
4424 4853 }
4425 4854 else
@@ -4433,9 +4862,9 @@
4433 4862 public function appraisal_revert_pending()
4434 4863 {
4435 4864 check_ajax_referer( 'appraisal-actions', 'security' );
4436 4865
4437 - $post_id = (int)$_POST['appraisal_id'];
4866 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4438 4867
4439 4868 $status = get_post_meta( $post_id, '_status', TRUE );
4440 4869
4441 4870 if ( $status == 'carried_out' || $status == 'cancelled' )
@@ -4459,9 +4888,9 @@
4459 4888 public function appraisal_revert_carried_out()
4460 4889 {
4461 4890 check_ajax_referer( 'appraisal-actions', 'security' );
4462 4891
4463 - $post_id = (int)$_POST['appraisal_id'];
4892 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4464 4893
4465 4894 $status = get_post_meta( $post_id, '_status', TRUE );
4466 4895
4467 4896 if ( $status == 'won' || $status == 'lost' )
@@ -4485,9 +4914,9 @@
4485 4914 public function appraisal_revert_won()
4486 4915 {
4487 4916 check_ajax_referer( 'appraisal-actions', 'security' );
4488 4917
4489 - $post_id = (int)$_POST['appraisal_id'];
4918 + $post_id = $this->get_authorized_record_id( 'appraisal_id', 'appraisal' );
4490 4919
4491 4920 $status = get_post_meta( $post_id, '_status', TRUE );
4492 4921
4493 4922 if ( $status == 'instructed' )
@@ -4514,10 +4943,11 @@
4514 4943 check_ajax_referer( 'book-viewing', 'security' );
4515 4944
4516 4945 $this->json_headers();
4517 4946
4518 - // TO DO: Should do validation on server side also
4519 - 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)
4520 4950 {
4521 4951 $return = array('error' => 'No property selected');
4522 4952 echo json_encode( $return );
4523 4953 die();
@@ -4522,18 +4952,26 @@
4522 4952 echo json_encode( $return );
4523 4953 die();
4524 4954 }
4525 4955
4526 - $property = new PH_Property((int)$_POST['property_id']);
4956 + $property = new PH_Property( $property_id );
4527 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 + }
4528 4966 $applicant_contact_ids = array();
4529 4967
4530 4968 // Create applicant record if required
4531 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
4969 + if (empty($booking['applicant_ids']) && !empty($booking['applicant_name']))
4532 4970 {
4533 4971 // Need to create contact/applicant
4534 4972 $contact_post = array(
4535 - 'post_title' => ph_clean($_POST['applicant_name']),
4973 + 'post_title' => $booking['applicant_name'],
4536 4974 'post_content' => '',
4537 4975 'post_type' => 'contact',
4538 4976 'post_status' => 'publish',
4539 4977 'comment_status' => 'closed',
@@ -4540,9 +4978,9 @@
4540 4978 'ping_status' => 'closed',
4541 4979 );
4542 4980
4543 4981 // Insert the post into the database
4544 - $contact_post_id = wp_insert_post( $contact_post );
4982 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
4545 4983
4546 4984 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
4547 4985 {
4548 4986 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -4551,24 +4989,24 @@
4551 4989 }
4552 4990
4553 4991 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
4554 4992
4555 - $email_address = isset($_POST['applicant_email_address']) ? sanitize_email($_POST['applicant_email_address']) : '';
4556 - $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'];
4557 4995 update_post_meta( $contact_post_id, '_email_address', $email_address );
4558 - update_post_meta( $contact_post_id, '_telephone_number', $telephone_number );
4996 + update_post_meta( $contact_post_id, '_telephone_number', wp_slash( $telephone_number ) );
4559 4997 update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean( ph_clean_telephone_number($telephone_number) ) );
4560 4998
4561 - if ( isset($_POST['applicant_address']) && !empty(sanitize_textarea_field($_POST['applicant_address'])) )
4999 + if ( '' !== $booking['applicant_address'] )
4562 5000 {
4563 - $address = ph_split_address_into_fields( sanitize_textarea_field($_POST['applicant_address']) );
5001 + $address = ph_split_address_into_fields( $booking['applicant_address'] );
4564 5002
4565 - update_post_meta( $contact_post_id, '_address_name_number', $address['address_name_number'] );
4566 - update_post_meta( $contact_post_id, '_address_street', $address['address_street'] );
4567 - update_post_meta( $contact_post_id, '_address_two', $address['address_two'] );
4568 - update_post_meta( $contact_post_id, '_address_three', $address['address_three'] );
4569 - update_post_meta( $contact_post_id, '_address_four', $address['address_four'] );
4570 - 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'] ) );
4571 5009 update_post_meta( $contact_post_id, '_address_country', get_option( 'propertyhive_default_country', 'GB' ) );
4572 5010 }
4573 5011
4574 5012 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
@@ -4576,20 +5014,12 @@
4576 5014
4577 5015 $applicant_contact_ids[] = $contact_post_id;
4578 5016 }
4579 5017
4580 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
5018 + if (!empty($booking['applicant_ids']) && empty($booking['applicant_name']))
4581 5019 {
4582 5020 // This is an existing contact
4583 - if ( !is_array($_POST['applicant_ids']) )
4584 - {
4585 - $_POST['applicant_ids'] = array(ph_clean($_POST['applicant_ids']));
4586 - }
4587 -
4588 - foreach ( $_POST['applicant_ids'] as $applicant_id )
4589 - {
4590 - $applicant_contact_ids[] = (int)$applicant_id;
4591 - }
5021 + $applicant_contact_ids = $booking['applicant_ids'];
4592 5022 }
4593 5023
4594 5024 $applicant_contact_ids = array_unique($applicant_contact_ids);
4595 5025
@@ -4674,11 +5104,11 @@
4674 5104 echo json_encode( $return );
4675 5105 die();
4676 5106 }
4677 5107
4678 - 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'] );
4679 5109 add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
4680 - add_post_meta( $viewing_post_id, '_property_id', (int)$_POST['property_id'] );
5110 + add_post_meta( $viewing_post_id, '_property_id', $property_id );
4681 5111
4682 5112 $applicant_contacts = array();
4683 5113 foreach ($applicant_contact_ids as $applicant_contact_id)
4684 5114 {
@@ -4695,11 +5125,11 @@
4695 5125 add_post_meta( $viewing_post_id, '_feedback_status', '' );
4696 5126 add_post_meta( $viewing_post_id, '_feedback', '' );
4697 5127 add_post_meta( $viewing_post_id, '_feedback_passed_on', '' );
4698 5128
4699 - if ( !empty($_POST['negotiator_ids']) )
5129 + if ( !empty($booking['negotiator_ids']) )
4700 5130 {
4701 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
5131 + foreach ( $booking['negotiator_ids'] as $negotiator_id )
4702 5132 {
4703 5133 add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
4704 5134 }
4705 5135 }
@@ -4722,10 +5152,16 @@
4722 5152 check_ajax_referer( 'book-viewing', 'security' );
4723 5153
4724 5154 $this->json_headers();
4725 5155
4726 - // TO DO: Should do validation on server side also
4727 - 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)
4728 5164 {
4729 5165 $return = array('error' => 'No contact selected');
4730 5166 echo json_encode( $return );
4731 5167 die();
@@ -4730,9 +5166,9 @@
4730 5166 echo json_encode( $return );
4731 5167 die();
4732 5168 }
4733 5169
4734 - if (empty($_POST['property_ids']))
5170 + if (empty($booking['property_ids']))
4735 5171 {
4736 5172 $return = array('error' => 'No property selected');
4737 5173 echo json_encode( $return );
4738 5174 die();
@@ -4739,9 +5175,9 @@
4739 5175 }
4740 5176
4741 5177 // Loop through contacts and create one viewing each
4742 5178 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
4743 - foreach ( $_POST['property_ids'] as $property_id )
5179 + foreach ( $booking['property_ids'] as $property_id )
4744 5180 {
4745 5181 // Insert viewing record
4746 5182 $viewing_post = array(
4747 5183 'post_title' => '',
@@ -4761,20 +5197,20 @@
4761 5197 echo json_encode( $return );
4762 5198 die();
4763 5199 }
4764 5200
4765 - 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'] );
4766 5202 add_post_meta( $viewing_post_id, '_duration', 30 * 60 ); // Stored in seconds. Default to 30 mins
4767 5203 add_post_meta( $viewing_post_id, '_property_id', (int)$property_id );
4768 - 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 );
4769 5205 add_post_meta( $viewing_post_id, '_status', 'pending' );
4770 5206 add_post_meta( $viewing_post_id, '_feedback_status', '' );
4771 5207 add_post_meta( $viewing_post_id, '_feedback', '' );
4772 5208 add_post_meta( $viewing_post_id, '_feedback_passed_on', '' );
4773 5209
4774 - if ( !empty($_POST['negotiator_ids']) )
5210 + if ( !empty($booking['negotiator_ids']) )
4775 5211 {
4776 - foreach ( $_POST['negotiator_ids'] as $negotiator_id )
5212 + foreach ( $booking['negotiator_ids'] as $negotiator_id )
4777 5213 {
4778 5214 add_post_meta( $viewing_post_id, '_negotiator_id', (int)$negotiator_id );
4779 5215 }
4780 5216 }
@@ -4780,9 +5216,9 @@
4780 5216 }
4781 5217 }
4782 5218
4783 5219 $properties = array();
4784 - foreach ( $_POST['property_ids'] as $property_id )
5220 + foreach ( $booking['property_ids'] as $property_id )
4785 5221 {
4786 5222 $properties[] = array(
4787 5223 'ID' => (int)$property_id,
4788 5224 'post_title' => get_the_title((int)$property_id),
@@ -4808,14 +5244,16 @@
4808 5244 global $post;
4809 5245
4810 5246 check_ajax_referer( 'viewing-details-meta-box', 'security' );
4811 5247
4812 - $post = get_post((int)$_POST['viewing_id']);
5248 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
4813 5249
4814 - $viewing = new PH_Viewing((int)$_POST['viewing_id']);
5250 + $post = get_post( $post_id );
4815 5251
4816 - $readonly = isset($_POST['readonly']) ? filter_var($_POST['readonly'], FILTER_VALIDATE_BOOLEAN) : false;
5252 + $viewing = new PH_Viewing( $post_id );
4817 5253
5254 + $readonly = isset( $_POST['readonly'] ) && is_scalar( $_POST['readonly'] ) ? filter_var( wp_unslash( $_POST['readonly'] ), FILTER_VALIDATE_BOOLEAN ) : false;
5255 +
4818 5256 include( PH()->plugin_path() . '/includes/admin/views/html-viewing-details-meta-box.php' );
4819 5257
4820 5258 die();
4821 5259 }
@@ -4823,9 +5261,9 @@
4823 5261 public function get_viewing_actions()
4824 5262 {
4825 5263 check_ajax_referer( 'viewing-actions', 'security' );
4826 5264
4827 - $post_id = (int)$_POST['viewing_id'];
5265 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
4828 5266
4829 5267 include( PH()->plugin_path() . '/includes/admin/views/html-viewing-actions.php' );
4830 5268
4831 5269 die();
@@ -4834,9 +5272,13 @@
4834 5272 public function get_viewing_lightbox()
4835 5273 {
4836 5274 global $post;
4837 5275
4838 - $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 + }
4839 5281
4840 5282 $post = get_post((int)$post_id);
4841 5283
4842 5284 $viewing = new PH_Viewing($post_id);
@@ -4849,9 +5291,9 @@
4849 5291 public function viewing_carried_out()
4850 5292 {
4851 5293 check_ajax_referer( 'viewing-actions', 'security' );
4852 5294
4853 - $post_id = (int)$_POST['viewing_id'];
5295 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
4854 5296
4855 5297 $status = get_post_meta( $post_id, '_status', TRUE );
4856 5298
4857 5299 if ( $status == 'pending' )
@@ -4875,9 +5317,9 @@
4875 5317 public function viewing_no_show()
4876 5318 {
4877 5319 check_ajax_referer( 'viewing-actions', 'security' );
4878 5320
4879 - $post_id = (int)$_POST['viewing_id'];
5321 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
4880 5322
4881 5323 $status = get_post_meta( $post_id, '_status', TRUE );
4882 5324
4883 5325 if ( $status == 'pending' )
@@ -4901,16 +5343,18 @@
4901 5343 public function viewing_cancelled()
4902 5344 {
4903 5345 check_ajax_referer( 'viewing-actions', 'security' );
4904 5346
4905 - $post_id = (int)$_POST['viewing_id'];
5347 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
4906 5348
5349 + $text = isset( $_POST['cancelled_reason'] ) && is_string( $_POST['cancelled_reason'] ) ? sanitize_textarea_field( wp_unslash( $_POST['cancelled_reason'] ) ) : '';
5350 +
4907 5351 $status = get_post_meta( $post_id, '_status', TRUE );
4908 5352
4909 5353 if ( $status == 'pending' )
4910 5354 {
4911 5355 update_post_meta( $post_id, '_status', 'cancelled' );
4912 - update_post_meta( $post_id, '_cancelled_reason', sanitize_textarea_field( $_POST['cancelled_reason'] ) );
5356 + update_post_meta( $post_id, '_cancelled_reason', wp_slash( $text ) );
4913 5357 update_post_meta( $post_id, '_cancelled_reason_public', isset($_POST['cancelled_reason_public']) && $_POST['cancelled_reason_public'] == 'yes' ? 'yes' : '' );
4914 5358
4915 5359 // Add note/comment to viewing
4916 5360 $comment = array(
@@ -4929,9 +5373,9 @@
4929 5373 public function viewing_email_applicant_booking_confirmation()
4930 5374 {
4931 5375 check_ajax_referer( 'viewing-actions', 'security' );
4932 5376
4933 - $post_id = (int)$_POST['viewing_id'];
5377 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
4934 5378
4935 5379 $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
4936 5380 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
4937 5381
@@ -4956,10 +5400,10 @@
4956 5400 $to = array_filter($to);
4957 5401
4958 5402 if ( !empty(implode($to)) )
4959 5403 {
4960 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_viewing_applicant_booking_confirmation_email_subject', '' );
4961 - $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', '' );
4962 5406
4963 5407 $applicant_names = array();
4964 5408 $applicant_dears = array();
4965 5409 foreach ($applicant_contact_ids as $applicant_contact_id)
@@ -5032,21 +5476,22 @@
5032 5476 }
5033 5477
5034 5478 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
5035 5479 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
5036 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5037 - $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);
5038 5482 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
5039 5483 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
5040 5484 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
5041 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.
5042 5487 $subject = apply_filters( 'viewing_applicant_booking_confirmation_email_subject', $subject, $post_id, $property_id );
5043 5488
5044 5489 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
5045 5490 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
5046 5491 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
5047 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5048 - $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);
5049 5494 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
5050 5495 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
5051 5496 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5052 5497
@@ -5051,8 +5496,9 @@
5051 5496 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5052 5497
5053 5498 $body = html_entity_decode($body);
5054 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.
5055 5501 $body = apply_filters( 'viewing_applicant_booking_confirmation_email_body', $body, $post_id, $property_id );
5056 5502
5057 5503 $from = '';
5058 5504 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -5081,9 +5527,9 @@
5081 5527
5082 5528 $attachments = array();
5083 5529 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5084 5530 {
5085 - $uploaded_files = $_FILES['attachments'];
5531 + $uploaded_files = $this->get_viewing_email_uploads();
5086 5532
5087 5533 // Handle each file upload
5088 5534 foreach ($uploaded_files['name'] as $key => $value)
5089 5535 {
@@ -5125,9 +5571,9 @@
5125 5571 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
5126 5572
5127 5573 foreach ($attachments as $temp_file)
5128 5574 {
5129 - @unlink($temp_file);
5575 + @wp_delete_file($temp_file);
5130 5576 }
5131 5577
5132 5578 if ( !$sent )
5133 5579 {
@@ -5133,9 +5579,9 @@
5133 5579 {
5134 5580 wp_send_json_error('Failed to send email');
5135 5581 }
5136 5582
5137 - 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") );
5138 5584
5139 5585 if ( apply_filters( 'propertyhive_log_booking_confirmation_emails', false ) === true )
5140 5586 {
5141 5587 // Add note/comment to viewing
@@ -5160,9 +5606,9 @@
5160 5606 public function viewing_email_owner_booking_confirmation()
5161 5607 {
5162 5608 check_ajax_referer( 'viewing-actions', 'security' );
5163 5609
5164 - $post_id = (int)$_POST['viewing_id'];
5610 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5165 5611
5166 5612 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5167 5613 $property_department = get_post_meta( $property_id, '_department' );
5168 5614
@@ -5273,20 +5719,21 @@
5273 5719 $property = new PH_Property((int)$property_id);
5274 5720
5275 5721 $to = implode(",", $owner_emails);
5276 5722
5277 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_viewing_owner_booking_confirmation_email_subject', '' );
5278 - $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', '' );
5279 5725
5280 5726 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
5281 5727 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
5282 5728 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
5283 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5284 - $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);
5285 5731 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
5286 5732 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
5287 5733 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
5288 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.
5289 5736 $subject = apply_filters( 'viewing_owner_booking_confirmation_email_subject', $subject, $post_id, $property_id );
5290 5737
5291 5738 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
5292 5739 $body = str_replace('[owner_name]', $owner_names_string, $body);
@@ -5292,10 +5739,10 @@
5292 5739 $body = str_replace('[owner_name]', $owner_names_string, $body);
5293 5740 $body = str_replace('[owner_dear]', $owner_dears_string, $body);
5294 5741 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
5295 5742 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
5296 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5297 - $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);
5298 5745 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
5299 5746 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
5300 5747 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5301 5748
@@ -5300,8 +5747,9 @@
5300 5747 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5301 5748
5302 5749 $body = html_entity_decode($body);
5303 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.
5304 5752 $body = apply_filters( 'viewing_owner_booking_confirmation_email_body', $body, $post_id, $property_id );
5305 5753
5306 5754 $from = '';
5307 5755 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -5330,9 +5778,9 @@
5330 5778
5331 5779 $attachments = array();
5332 5780 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5333 5781 {
5334 - $uploaded_files = $_FILES['attachments'];
5782 + $uploaded_files = $this->get_viewing_email_uploads();
5335 5783
5336 5784 // Handle each file upload
5337 5785 foreach ($uploaded_files['name'] as $key => $value)
5338 5786 {
@@ -5374,9 +5822,9 @@
5374 5822 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
5375 5823
5376 5824 foreach ($attachments as $temp_file)
5377 5825 {
5378 - @unlink($temp_file);
5826 + @wp_delete_file($temp_file);
5379 5827 }
5380 5828
5381 5829 if ( !$sent )
5382 5830 {
@@ -5393,9 +5841,9 @@
5393 5841
5394 5842 PH_Comments::insert_note( $post_id, $comment );
5395 5843 }
5396 5844
5397 - 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") );
5398 5846
5399 5847 wp_send_json_success();
5400 5848 }
5401 5849 else
@@ -5409,9 +5857,9 @@
5409 5857 public function viewing_email_attending_negotiator_booking_confirmation()
5410 5858 {
5411 5859 check_ajax_referer( 'viewing-actions', 'security' );
5412 5860
5413 - $post_id = (int)$_POST['viewing_id'];
5861 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5414 5862 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5415 5863
5416 5864 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
5417 5865
@@ -5543,20 +5991,21 @@
5543 5991 }
5544 5992
5545 5993 $property = new PH_Property((int)$property_id);
5546 5994
5547 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_viewing_attending_negotiator_booking_confirmation_email_subject', '' );
5548 - $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', '' );
5549 5997
5550 5998 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
5551 5999 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
5552 6000 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
5553 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5554 - $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);
5555 6003 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
5556 6004 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
5557 6005 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
5558 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.
5559 6008 $subject = apply_filters( 'viewing_attending_negotiator_booking_confirmation_email_subject', $subject, $post_id, $property_id );
5560 6009
5561 6010 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
5562 6011 $body = str_replace('[owner_name]', $owner_names_string, $body);
@@ -5564,10 +6013,10 @@
5564 6013 $body = str_replace('[owner_details]', $owner_details, $body);
5565 6014 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
5566 6015 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
5567 6016 $body = str_replace('[applicant_details]', $applicant_details, $body);
5568 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5569 - $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);
5570 6019 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
5571 6020 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
5572 6021 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5573 6022
@@ -5572,8 +6021,9 @@
5572 6021 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5573 6022
5574 6023 $body = html_entity_decode($body);
5575 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.
5576 6026 $body = apply_filters( 'viewing_attending_negotiator_booking_confirmation_email_body', $body, $post_id, $property_id );
5577 6027
5578 6028 $from = '';
5579 6029 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -5602,9 +6052,9 @@
5602 6052
5603 6053 $attachments = array();
5604 6054 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5605 6055 {
5606 - $uploaded_files = $_FILES['attachments'];
6056 + $uploaded_files = $this->get_viewing_email_uploads();
5607 6057
5608 6058 // Handle each file upload
5609 6059 foreach ($uploaded_files['name'] as $key => $value)
5610 6060 {
@@ -5646,9 +6096,9 @@
5646 6096 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
5647 6097
5648 6098 foreach ($attachments as $temp_file)
5649 6099 {
5650 - @unlink($temp_file);
6100 + @wp_delete_file($temp_file);
5651 6101 }
5652 6102
5653 6103 if ( !$sent )
5654 6104 {
@@ -5665,9 +6115,9 @@
5665 6115
5666 6116 PH_Comments::insert_note( $post_id, $comment );
5667 6117 }
5668 6118
5669 - 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") );
5670 6120
5671 6121 wp_send_json_success();
5672 6122 }
5673 6123 else
@@ -5681,9 +6131,9 @@
5681 6131 public function viewing_email_applicant_cancellation_notification()
5682 6132 {
5683 6133 check_ajax_referer( 'viewing-actions', 'security' );
5684 6134
5685 - $post_id = (int)$_POST['viewing_id'];
6135 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5686 6136
5687 6137 $applicant_contact_ids = get_post_meta( $post_id, '_applicant_contact_id' );
5688 6138 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5689 6139
@@ -5708,10 +6158,10 @@
5708 6158 $to = array_filter($to);
5709 6159
5710 6160 if ( !empty(implode($to)) )
5711 6161 {
5712 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_viewing_applicant_cancellation_notification_email_subject', '' );
5713 - $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', '' );
5714 6164
5715 6165 $applicant_names = array();
5716 6166 $applicant_dears = array();
5717 6167 foreach ($applicant_contact_ids as $applicant_contact_id)
@@ -5784,21 +6234,22 @@
5784 6234 }
5785 6235
5786 6236 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
5787 6237 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
5788 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
5789 - $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);
5790 6240 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
5791 6241 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
5792 6242 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
5793 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.
5794 6245 $subject = apply_filters( 'viewing_applicant_cancellation_notification_email_subject', $subject, $post_id, $property_id );
5795 6246
5796 6247 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
5797 6248 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
5798 6249 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
5799 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
5800 - $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);
5801 6252 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
5802 6253 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
5803 6254 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
5804 6255
@@ -5813,8 +6264,9 @@
5813 6264 $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
5814 6265
5815 6266 $body = html_entity_decode($body);
5816 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.
5817 6269 $body = apply_filters( 'viewing_applicant_cancellation_notification_email_body', $body, $post_id, $property_id );
5818 6270
5819 6271 $from = '';
5820 6272 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -5843,9 +6295,9 @@
5843 6295
5844 6296 $attachments = array();
5845 6297 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
5846 6298 {
5847 - $uploaded_files = $_FILES['attachments'];
6299 + $uploaded_files = $this->get_viewing_email_uploads();
5848 6300
5849 6301 // Handle each file upload
5850 6302 foreach ($uploaded_files['name'] as $key => $value)
5851 6303 {
@@ -5887,9 +6339,9 @@
5887 6339 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
5888 6340
5889 6341 foreach ($attachments as $temp_file)
5890 6342 {
5891 - @unlink($temp_file);
6343 + @wp_delete_file($temp_file);
5892 6344 }
5893 6345
5894 6346 if ( !$sent )
5895 6347 {
@@ -5895,9 +6347,9 @@
5895 6347 {
5896 6348 wp_send_json_error('Failed to send email');
5897 6349 }
5898 6350
5899 - 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") );
5900 6352
5901 6353 if ( apply_filters( 'propertyhive_log_cancellation_notification_emails', false ) === true )
5902 6354 {
5903 6355 // Add note/comment to viewing
@@ -5922,9 +6374,9 @@
5922 6374 public function viewing_email_owner_cancellation_notification()
5923 6375 {
5924 6376 check_ajax_referer( 'viewing-actions', 'security' );
5925 6377
5926 - $post_id = (int)$_POST['viewing_id'];
6378 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
5927 6379
5928 6380 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
5929 6381 $property_department = get_post_meta( $property_id, '_department' );
5930 6382
@@ -6035,20 +6487,21 @@
6035 6487 $property = new PH_Property((int)$property_id);
6036 6488
6037 6489 $to = implode(",", $owner_emails);
6038 6490
6039 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_viewing_owner_cancellation_notification_email_subject', '' );
6040 - $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', '' );
6041 6493
6042 6494 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
6043 6495 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
6044 6496 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
6045 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6046 - $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);
6047 6499 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
6048 6500 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
6049 6501 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
6050 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.
6051 6504 $subject = apply_filters( 'viewing_owner_cancellation_notification_email_subject', $subject, $post_id, $property_id );
6052 6505
6053 6506 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
6054 6507 $body = str_replace('[owner_name]', $owner_names_string, $body);
@@ -6054,10 +6507,10 @@
6054 6507 $body = str_replace('[owner_name]', $owner_names_string, $body);
6055 6508 $body = str_replace('[owner_dear]', $owner_dears_string, $body);
6056 6509 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
6057 6510 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
6058 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6059 - $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);
6060 6513 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
6061 6514 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
6062 6515 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
6063 6516
@@ -6072,8 +6525,9 @@
6072 6525 $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
6073 6526
6074 6527 $body = html_entity_decode($body);
6075 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.
6076 6530 $body = apply_filters( 'viewing_owner_cancellation_notification_email_body', $body, $post_id, $property_id );
6077 6531
6078 6532 $from = '';
6079 6533 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -6102,9 +6556,9 @@
6102 6556
6103 6557 $attachments = array();
6104 6558 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6105 6559 {
6106 - $uploaded_files = $_FILES['attachments'];
6560 + $uploaded_files = $this->get_viewing_email_uploads();
6107 6561
6108 6562 // Handle each file upload
6109 6563 foreach ($uploaded_files['name'] as $key => $value)
6110 6564 {
@@ -6146,9 +6600,9 @@
6146 6600 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
6147 6601
6148 6602 foreach ($attachments as $temp_file)
6149 6603 {
6150 - @unlink($temp_file);
6604 + @wp_delete_file($temp_file);
6151 6605 }
6152 6606
6153 6607 if ( !$sent )
6154 6608 {
@@ -6165,9 +6619,9 @@
6165 6619
6166 6620 PH_Comments::insert_note( $post_id, $comment );
6167 6621 }
6168 6622
6169 - 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") );
6170 6624
6171 6625 wp_send_json_success();
6172 6626 }
6173 6627 else
@@ -6181,9 +6635,9 @@
6181 6635 public function viewing_email_attending_negotiator_cancellation_notification()
6182 6636 {
6183 6637 check_ajax_referer( 'viewing-actions', 'security' );
6184 6638
6185 - $post_id = (int)$_POST['viewing_id'];
6639 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6186 6640 $property_id = get_post_meta( $post_id, '_property_id', TRUE );
6187 6641
6188 6642 $negotiator_ids = get_post_meta( $post_id, '_negotiator_id' );
6189 6643
@@ -6315,20 +6769,21 @@
6315 6769 }
6316 6770
6317 6771 $property = new PH_Property((int)$property_id);
6318 6772
6319 - $subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : get_option( 'propertyhive_viewing_attending_negotiator_cancellation_notification_email_subject', '' );
6320 - $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', '' );
6321 6775
6322 6776 $subject = str_replace('[property_address]', $property->get_formatted_full_address(), $subject);
6323 6777 $subject = str_replace('[owner_name]', $owner_names_string, $subject);
6324 6778 $subject = str_replace('[applicant_name]', $applicant_names_string, $subject);
6325 - $subject = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $subject);
6326 - $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);
6327 6781 $subject = str_replace('[negotiator_name]', $negotiator_names_string, $subject);
6328 6782 $subject = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $subject);
6329 6783 $subject = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $subject);
6330 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.
6331 6786 $subject = apply_filters( 'viewing_attending_negotiator_cancellation_notification_email_subject', $subject, $post_id, $property_id );
6332 6787
6333 6788 $body = str_replace('[property_address]', $property->get_formatted_full_address(), $body);
6334 6789 $body = str_replace('[owner_name]', $owner_names_string, $body);
@@ -6336,10 +6791,10 @@
6336 6791 $body = str_replace('[owner_details]', $owner_details, $body);
6337 6792 $body = str_replace('[applicant_name]', $applicant_names_string, $body);
6338 6793 $body = str_replace('[applicant_dear]', $applicant_dears_string, $body);
6339 6794 $body = str_replace('[applicant_details]', $applicant_details, $body);
6340 - $body = str_replace('[viewing_time]', date("H:i", strtotime(get_post_meta( $post_id, '_start_date_time', true ))), $body);
6341 - $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);
6342 6797 $body = str_replace('[negotiator_name]', $negotiator_names_string, $body);
6343 6798 $body = str_replace('[negotiator_email_address]', $negotiator_email_addresses_string, $body);
6344 6799 $body = str_replace('[negotiator_telephone_number]', $negotiator_telephone_numbers_string, $body);
6345 6800
@@ -6354,8 +6809,9 @@
6354 6809 $body = str_replace('[cancelled_reason]', $cancelled_reason, $body);
6355 6810
6356 6811 $body = html_entity_decode($body);
6357 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.
6358 6814 $body = apply_filters( 'viewing_attending_negotiator_cancellation_notification_email_body', $body, $post_id, $property_id );
6359 6815
6360 6816 $from = '';
6361 6817 $from_setting = get_option( 'propertyhive_confirmations_default_from', '' );
@@ -6384,9 +6840,9 @@
6384 6840
6385 6841 $attachments = array();
6386 6842 if ( isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0]) )
6387 6843 {
6388 - $uploaded_files = $_FILES['attachments'];
6844 + $uploaded_files = $this->get_viewing_email_uploads();
6389 6845
6390 6846 // Handle each file upload
6391 6847 foreach ($uploaded_files['name'] as $key => $value)
6392 6848 {
@@ -6428,9 +6884,9 @@
6428 6884 $sent = wp_mail($to, $subject, $body, $headers, $attachments);
6429 6885
6430 6886 foreach ($attachments as $temp_file)
6431 6887 {
6432 - @unlink($temp_file);
6888 + @wp_delete_file($temp_file);
6433 6889 }
6434 6890
6435 6891 if ( !$sent )
6436 6892 {
@@ -6447,9 +6903,9 @@
6447 6903
6448 6904 PH_Comments::insert_note( $post_id, $comment );
6449 6905 }
6450 6906
6451 - 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") );
6452 6908
6453 6909 wp_send_json_success();
6454 6910 }
6455 6911 else
@@ -6463,16 +6919,18 @@
6463 6919 public function viewing_interested_feedback()
6464 6920 {
6465 6921 check_ajax_referer( 'viewing-actions', 'security' );
6466 6922
6467 - $post_id = (int)$_POST['viewing_id'];
6923 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6468 6924
6925 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6926 +
6469 6927 $status = get_post_meta( $post_id, '_status', TRUE );
6470 6928
6471 6929 if ( $status == 'carried_out' )
6472 6930 {
6473 6931 update_post_meta( $post_id, '_feedback_status', 'interested' );
6474 - update_post_meta( $post_id, '_feedback', sanitize_textarea_field( $_POST['feedback'] ) );
6932 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
6475 6933
6476 6934 // Add note/comment to viewing
6477 6935 $comment = array(
6478 6936 'note_type' => 'action',
@@ -6490,16 +6948,18 @@
6490 6948 public function viewing_not_interested_feedback()
6491 6949 {
6492 6950 check_ajax_referer( 'viewing-actions', 'security' );
6493 6951
6494 - $post_id = (int)$_POST['viewing_id'];
6952 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6495 6953
6954 + $text = isset( $_POST['feedback'] ) && is_string( $_POST['feedback'] ) ? sanitize_textarea_field( wp_unslash( $_POST['feedback'] ) ) : '';
6955 +
6496 6956 $status = get_post_meta( $post_id, '_status', TRUE );
6497 6957
6498 6958 if ( $status == 'carried_out' )
6499 6959 {
6500 6960 update_post_meta( $post_id, '_feedback_status', 'not_interested' );
6501 - update_post_meta( $post_id, '_feedback', sanitize_textarea_field( $_POST['feedback'] ) );
6961 + update_post_meta( $post_id, '_feedback', wp_slash( $text ) );
6502 6962
6503 6963 // Add note/comment to viewing
6504 6964 $comment = array(
6505 6965 'note_type' => 'action',
@@ -6517,9 +6977,9 @@
6517 6977 public function viewing_feedback_not_required()
6518 6978 {
6519 6979 check_ajax_referer( 'viewing-actions', 'security' );
6520 6980
6521 - $post_id = (int)$_POST['viewing_id'];
6981 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6522 6982
6523 6983 $status = get_post_meta( $post_id, '_status', TRUE );
6524 6984
6525 6985 if ( $status == 'carried_out' )
@@ -6543,9 +7003,9 @@
6543 7003 public function viewing_revert_feedback_pending()
6544 7004 {
6545 7005 check_ajax_referer( 'viewing-actions', 'security' );
6546 7006
6547 - $post_id = (int)$_POST['viewing_id'];
7007 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6548 7008
6549 7009 $status = get_post_meta( $post_id, '_status', TRUE );
6550 7010
6551 7011 if ( $status == 'carried_out' )
@@ -6571,9 +7031,9 @@
6571 7031 public function viewing_revert_pending()
6572 7032 {
6573 7033 check_ajax_referer( 'viewing-actions', 'security' );
6574 7034
6575 - $post_id = (int)$_POST['viewing_id'];
7035 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6576 7036
6577 7037 $status = get_post_meta( $post_id, '_status', TRUE );
6578 7038
6579 7039 if ( in_array( $status, array('carried_out', 'cancelled', 'no_show') ) )
@@ -6599,9 +7059,9 @@
6599 7059 public function viewing_feedback_passed_on()
6600 7060 {
6601 7061 check_ajax_referer( 'viewing-actions', 'security' );
6602 7062
6603 - $post_id = (int)$_POST['viewing_id'];
7063 + $post_id = $this->get_authorized_record_id( 'viewing_id', 'viewing' );
6604 7064
6605 7065 $status = get_post_meta( $post_id, '_status', TRUE );
6606 7066
6607 7067 if ( $status == 'carried_out' )
@@ -6623,14 +7083,16 @@
6623 7083 }
6624 7084
6625 7085 public function get_property_viewings_meta_box()
6626 7086 {
6627 - $post_id = $_POST['post_id'];
7087 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
6628 7088
6629 7089 $selected_status = '';
6630 - 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'] ) )
6631 7092 {
6632 - $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'] ) );
6633 7095 }
6634 7096
6635 7097 include( PH()->plugin_path() . '/includes/admin/views/html-property-viewings-meta-box.php' );
6636 7098
@@ -6641,14 +7103,16 @@
6641 7103 }
6642 7104
6643 7105 public function get_contact_viewings_meta_box()
6644 7106 {
6645 - $post_id = $_POST['post_id'];
7107 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
6646 7108
6647 7109 $selected_status = '';
6648 - 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'] ) )
6649 7112 {
6650 - $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'] ) );
6651 7115 }
6652 7116
6653 7117 include( PH()->plugin_path() . '/includes/admin/views/html-contact-viewings-meta-box.php' );
6654 7118
@@ -6664,10 +7128,19 @@
6664 7128 check_ajax_referer( 'record-offer', 'security' );
6665 7129
6666 7130 $this->json_headers();
6667 7131
6668 - // TO DO: Should do validation on server side also
6669 - 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)
6670 7143 {
6671 7144 $return = array('error' => 'No property selected');
6672 7145 echo json_encode( $return );
6673 7146 die();
@@ -6672,18 +7145,18 @@
6672 7145 echo json_encode( $return );
6673 7146 die();
6674 7147 }
6675 7148
6676 - $property = new PH_Property((int)$_POST['property_id']);
7149 + $property = new PH_Property($property_id);
6677 7150
6678 7151 $applicant_contact_ids = array();
6679 7152
6680 7153 // Create applicant record if required
6681 - if (empty($_POST['applicant_ids']) && !empty($_POST['applicant_name']))
7154 + if (empty($input['applicant_ids']) && !empty($input['applicant_name']))
6682 7155 {
6683 7156 // Need to create contact/applicant
6684 7157 $contact_post = array(
6685 - 'post_title' => ph_clean($_POST['applicant_name']),
7158 + 'post_title' => $input['applicant_name'],
6686 7159 'post_content' => '',
6687 7160 'post_type' => 'contact',
6688 7161 'post_status' => 'publish',
6689 7162 'comment_status' => 'closed',
@@ -6690,9 +7163,9 @@
6690 7163 'ping_status' => 'closed',
6691 7164 );
6692 7165
6693 7166 // Insert the post into the database
6694 - $contact_post_id = wp_insert_post( $contact_post );
7167 + $contact_post_id = wp_insert_post( wp_slash( $contact_post ) );
6695 7168
6696 7169 if ( is_wp_error($contact_post_id) || $contact_post_id == 0 )
6697 7170 {
6698 7171 $return = array('error' => 'Failed to create contact post. Please try again');
@@ -6701,24 +7174,24 @@
6701 7174 }
6702 7175
6703 7176 update_post_meta( $contact_post_id, '_contact_types', array('applicant') );
6704 7177
6705 - $email_address = isset($_POST['applicant_email_address']) ? sanitize_email($_POST['applicant_email_address']) : '';
6706 - $telephone_number = isset($_POST['applicant_telephone_number']) ? sanitize_text_field($_POST['applicant_telephone_number']) : '';
6707 - update_post_meta( $contact_post_id, '_email_address', $email_address );
6708 - 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 ) );
6709 7182 update_post_meta( $contact_post_id, '_telephone_number_clean', ph_clean( ph_clean_telephone_number($telephone_number) ) );
6710 7183
6711 - if ( isset($_POST['applicant_address']) && !empty(sanitize_textarea_field($_POST['applicant_address'])) )
7184 + if ( '' !== $input['applicant_address'] )
6712 7185 {
6713 - $address = ph_split_address_into_fields( sanitize_textarea_field($_POST['applicant_address']) );
7186 + $address = ph_split_address_into_fields( $input['applicant_address'] );
6714 7187
6715 - update_post_meta( $contact_post_id, '_address_name_number', $address['address_name_number'] );
6716 - update_post_meta( $contact_post_id, '_address_street', $address['address_street'] );
6717 - update_post_meta( $contact_post_id, '_address_two', $address['address_two'] );
6718 - update_post_meta( $contact_post_id, '_address_three', $address['address_three'] );
6719 - update_post_meta( $contact_post_id, '_address_four', $address['address_four'] );
6720 - 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'] ) );
6721 7194 update_post_meta( $contact_post_id, '_address_country', get_option( 'propertyhive_default_country', 'GB' ) );
6722 7195 }
6723 7196
6724 7197 update_post_meta( $contact_post_id, '_applicant_profiles', 1 );
@@ -6726,18 +7199,13 @@
6726 7199
6727 7200 $applicant_contact_ids[] = $contact_post_id;
6728 7201 }
6729 7202
6730 - if (!empty($_POST['applicant_ids']) && empty($_POST['applicant_name']))
7203 + if (!empty($input['applicant_ids']) && empty($input['applicant_name']))
6731 7204 {
6732 7205 // This is an existing contact
6733 - if ( !is_array($_POST['applicant_ids']) )
7206 + foreach ( $input['applicant_ids'] as $applicant_id )
6734 7207 {
6735 - $_POST['applicant_ids'] = array($_POST['applicant_ids']);
6736 - }
6737 -
6738 - foreach ( $_POST['applicant_ids'] as $applicant_id )
6739 - {
6740 7208 $applicant_contact_ids[] = (int)$applicant_id;
6741 7209 }
6742 7210 }
6743 7211
@@ -6773,12 +7241,12 @@
6773 7241 echo json_encode( $return );
6774 7242 die();
6775 7243 }
6776 7244
6777 - $amount = preg_replace("/[^0-9.]/", '', $_POST['amount']);
7245 + $amount = $input['amount'];
6778 7246
6779 - add_post_meta( $offer_post_id, '_offer_date_time', ph_clean($_POST['offer_date']) . ' ' . ph_clean($_POST['offer_time']) );
6780 - 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 );
6781 7249 add_post_meta( $offer_post_id, '_applicant_contact_id', $applicant_contact_id );
6782 7250 add_post_meta( $offer_post_id, '_amount', $amount );
6783 7251 add_post_meta( $offer_post_id, '_status', 'pending' );
6784 7252
@@ -6787,11 +7255,12 @@
6787 7255 {
6788 7256 add_post_meta( $offer_post_id, '_applicant_solicitor_contact_id', (int)$applicant_solicitor_contact_id );
6789 7257 }
6790 7258
6791 - $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);
6792 7260 if ( !empty($owner_contact_ids) )
6793 7261 {
7262 + $owner_contact_ids = is_array( $owner_contact_ids ) ? $owner_contact_ids : array( $owner_contact_ids );
6794 7263 foreach ( $owner_contact_ids as $owner_contact_id )
6795 7264 {
6796 7265 $property_owner_solicitor_contact_id = get_post_meta( (int)$owner_contact_id, '_contact_solicitor_contact_id', TRUE );
6797 7266 if ( !empty($property_owner_solicitor_contact_id) )
@@ -6830,10 +7299,16 @@
6830 7299 check_ajax_referer( 'record-offer', 'security' );
6831 7300
6832 7301 $this->json_headers();
6833 7302
6834 - // TO DO: Should do validation on server side also
6835 - 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)
6836 7311 {
6837 7312 $return = array('error' => 'No contact selected');
6838 7313 echo json_encode( $return );
6839 7314 die();
@@ -6838,9 +7313,9 @@
6838 7313 echo json_encode( $return );
6839 7314 die();
6840 7315 }
6841 7316
6842 - if (empty($_POST['property_ids']))
7317 + if (empty($input['property_ids']))
6843 7318 {
6844 7319 $return = array('error' => 'No property selected');
6845 7320 echo json_encode( $return );
6846 7321 die();
@@ -6847,9 +7322,9 @@
6847 7322 }
6848 7323
6849 7324 // Loop through contacts and create one offer each
6850 7325 // At the moment it's a 1-to-1 relationship, but might support multiple in the future
6851 - foreach ( $_POST['property_ids'] as $property_id )
7326 + foreach ( $input['property_ids'] as $property_id )
6852 7327 {
6853 7328 // Insert offer record
6854 7329 $offer_post = array(
6855 7330 'post_title' => '',
@@ -6869,17 +7344,17 @@
6869 7344 echo json_encode( $return );
6870 7345 die();
6871 7346 }
6872 7347
6873 - $amount = preg_replace("/[^0-9.]/", '', ph_clean($_POST['amount']));
7348 + $amount = $input['amount'];
6874 7349
6875 - 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'] );
6876 7351 add_post_meta( $offer_post_id, '_property_id', (int)$property_id );
6877 - 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 );
6878 7353 add_post_meta( $offer_post_id, '_amount', $amount );
6879 7354 add_post_meta( $offer_post_id, '_status', 'pending' );
6880 7355
6881 - $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 );
6882 7357 if ( !empty($applicant_solicitor_contact_id) )
6883 7358 {
6884 7359 add_post_meta( $offer_post_id, '_applicant_solicitor_contact_id', (int)$applicant_solicitor_contact_id );
6885 7360 }
@@ -6886,8 +7361,9 @@
6886 7361
6887 7362 $owner_contact_ids = get_post_meta($property_id, '_owner_contact_id', TRUE);
6888 7363 if ( !empty($owner_contact_ids) )
6889 7364 {
7365 + $owner_contact_ids = is_array( $owner_contact_ids ) ? $owner_contact_ids : array( $owner_contact_ids );
6890 7366 foreach ( $owner_contact_ids as $owner_contact_id )
6891 7367 {
6892 7368 $property_owner_solicitor_contact_id = get_post_meta( (int)$owner_contact_id, '_contact_solicitor_contact_id', TRUE );
6893 7369 if ( !empty($property_owner_solicitor_contact_id) )
@@ -6898,9 +7374,9 @@
6898 7374 }
6899 7375 }
6900 7376
6901 7377 $properties = array();
6902 - foreach ( $_POST['property_ids'] as $property_id )
7378 + foreach ( $input['property_ids'] as $property_id )
6903 7379 {
6904 7380 $properties[] = array(
6905 7381 'ID' => (int)$property_id,
6906 7382 'post_title' => get_the_title((int)$property_id),
@@ -6926,12 +7402,14 @@
6926 7402 global $post;
6927 7403
6928 7404 check_ajax_referer( 'offer-details-meta-box', 'security' );
6929 7405
6930 - $post = get_post((int)$_POST['offer_id']);
7406 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
6931 7407
6932 - $offer = new PH_Offer((int)$_POST['offer_id']);
7408 + $post = get_post( $post_id );
6933 7409
7410 + $offer = new PH_Offer( $post_id );
7411 +
6934 7412 echo '<div class="propertyhive_meta_box">';
6935 7413
6936 7414 echo '<div class="options_group">';
6937 7415
@@ -6940,9 +7418,9 @@
6940 7418 echo '<p class="form-field">
6941 7419
6942 7420 <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
6943 7421
6944 - ' . esc_html(__( ucwords(str_replace("_", " ", $offer->status)), 'propertyhive' )) . '
7422 + ' . esc_html(propertyhive_get_status_label( $offer->status )) . '
6945 7423
6946 7424 </p>';
6947 7425 }
6948 7426
@@ -6948,9 +7426,9 @@
6948 7426
6949 7427 $offer_date_time = $offer->offer_date_time;
6950 7428 if ( empty($offer_date_time) )
6951 7429 {
6952 - $offer_date_time = date("Y-m-d H:i:s");
7430 + $offer_date_time = gmdate("Y-m-d H:i:s");
6953 7431 }
6954 7432
6955 7433 echo '<p class="form-field offer_date_time_field">
6956 7434
@@ -6955,18 +7433,18 @@
6955 7433 echo '<p class="form-field offer_date_time_field">
6956 7434
6957 7435 <label for="_offer_date">' . esc_html(__('Offer Date / Time', 'propertyhive')) . '</label>
6958 7436
6959 - <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="">
6960 7438 <select id="_offer_time_hours" name="_offer_time_hours" class="select short" style="width:55px">';
6961 7439
6962 7440 if ( empty($offer_date_time) )
6963 7441 {
6964 - $value = date("H");
7442 + $value = gmdate("H");
6965 7443 }
6966 7444 else
6967 7445 {
6968 - $value = date( "H", strtotime( $offer_date_time ) );
7446 + $value = gmdate( "H", strtotime( $offer_date_time ) );
6969 7447 }
6970 7448 for ( $i = 0; $i < 23; ++$i )
6971 7449 {
6972 7450 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
@@ -6984,9 +7462,9 @@
6984 7462 $value = '';
6985 7463 }
6986 7464 else
6987 7465 {
6988 - $value = date( "i", strtotime( $offer_date_time ) );
7466 + $value = gmdate( "i", strtotime( $offer_date_time ) );
6989 7467 }
6990 7468 for ( $i = 0; $i < 60; $i+=5 )
6991 7469 {
6992 7470 $j = str_pad($i, 2, '0', STR_PAD_LEFT);
@@ -7023,9 +7501,9 @@
7023 7501 public function get_offer_actions()
7024 7502 {
7025 7503 check_ajax_referer( 'offer-actions', 'security' );
7026 7504
7027 - $post_id = (int)$_POST['offer_id'];
7505 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7028 7506
7029 7507 $status = get_post_meta( $post_id, '_status', TRUE );
7030 7508
7031 7509 // Success action panel
@@ -7088,9 +7566,9 @@
7088 7566 }
7089 7567 else
7090 7568 {
7091 7569 $actions[] = '<a
7092 - 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' )) . '"
7093 7571 class="button button-success button-create-sale"
7094 7572 style="width:100%; margin-bottom:7px; text-align:center"
7095 7573 onclick="setTimeout(function() { jQuery(\'.button-create-sale\').attr(\'href\', \'#\'); jQuery(\'.button-create-sale\').attr(\'disabled\', \'disabled\'); jQuery(\'.button-create-sale\').html(\'Creating...\'); }, 50);"
7096 7574 >' . wp_kses_post( __('Create Sale', 'propertyhive') ) . '</a>';
@@ -7115,8 +7593,9 @@
7115 7593 $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
7116 7594
7117 7595 if ( !empty($actions) )
7118 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.
7119 7598 echo implode("", $actions);
7120 7599 }
7121 7600 else
7122 7601 {
@@ -7133,9 +7612,9 @@
7133 7612 public function offer_accepted()
7134 7613 {
7135 7614 check_ajax_referer( 'offer-actions', 'security' );
7136 7615
7137 - $post_id = (int)$_POST['offer_id'];
7616 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7138 7617
7139 7618 $status = get_post_meta( $post_id, '_status', TRUE );
7140 7619
7141 7620 if ( $status == 'pending' )
@@ -7159,9 +7638,9 @@
7159 7638 public function offer_declined()
7160 7639 {
7161 7640 check_ajax_referer( 'offer-actions', 'security' );
7162 7641
7163 - $post_id = (int)$_POST['offer_id'];
7642 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7164 7643
7165 7644 $status = get_post_meta( $post_id, '_status', TRUE );
7166 7645
7167 7646 if ( $status == 'pending' )
@@ -7185,9 +7664,9 @@
7185 7664 public function offer_withdrawn()
7186 7665 {
7187 7666 check_ajax_referer( 'offer-actions', 'security' );
7188 7667
7189 - $post_id = (int)$_POST['offer_id'];
7668 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7190 7669
7191 7670 $status = get_post_meta( $post_id, '_status', TRUE );
7192 7671
7193 7672 if ( $status == 'pending' || $status == 'accepted' )
@@ -7211,9 +7690,9 @@
7211 7690 public function offer_revert_pending()
7212 7691 {
7213 7692 check_ajax_referer( 'offer-actions', 'security' );
7214 7693
7215 - $post_id = (int)$_POST['offer_id'];
7694 + $post_id = $this->get_authorized_record_id( 'offer_id', 'offer' );
7216 7695
7217 7696 $status = get_post_meta( $post_id, '_status', TRUE );
7218 7697
7219 7698 if ( $status == 'accepted' || $status == 'declined' || $status == 'withdrawn' )
@@ -7235,14 +7714,16 @@
7235 7714 }
7236 7715
7237 7716 public function get_property_offers_meta_box()
7238 7717 {
7239 - $post_id = $_POST['post_id'];
7718 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
7240 7719
7241 7720 $selected_status = '';
7242 - 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'] ) )
7243 7723 {
7244 - $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'] ) );
7245 7726 }
7246 7727
7247 7728 include( PH()->plugin_path() . '/includes/admin/views/html-property-offers-meta-box.php' );
7248 7729
@@ -7253,14 +7734,16 @@
7253 7734 }
7254 7735
7255 7736 public function get_contact_offers_meta_box()
7256 7737 {
7257 - $post_id = $_POST['post_id'];
7738 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
7258 7739
7259 7740 $selected_status = '';
7260 - 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'] ) )
7261 7743 {
7262 - $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'] ) );
7263 7746 }
7264 7747
7265 7748 include( PH()->plugin_path() . '/includes/admin/views/html-contact-offers-meta-box.php' );
7266 7749
@@ -7276,12 +7759,14 @@
7276 7759 global $post;
7277 7760
7278 7761 check_ajax_referer( 'sale-details-meta-box', 'security' );
7279 7762
7280 - $post = get_post((int)$_POST['sale_id']);
7763 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7281 7764
7282 - $sale = new PH_Offer((int)$_POST['sale_id']);
7765 + $post = get_post( $post_id );
7283 7766
7767 + $sale = new PH_Offer( $post_id );
7768 +
7284 7769 echo '<div class="propertyhive_meta_box">';
7285 7770
7286 7771 echo '<div class="options_group">';
7287 7772
@@ -7290,9 +7775,9 @@
7290 7775 echo '<p class="form-field">
7291 7776
7292 7777 <label for="">' . esc_html(__('Status', 'propertyhive')) . '</label>
7293 7778
7294 - ' . esc_html(__( ucwords(str_replace("_", " ", $sale->status)), 'propertyhive' )) . '
7779 + ' . esc_html(propertyhive_get_status_label( $sale->status )) . '
7295 7780
7296 7781 </p>';
7297 7782 }
7298 7783
@@ -7298,9 +7783,9 @@
7298 7783
7299 7784 $sale_date_time = $sale->sale_date_time;
7300 7785 if ( empty($sale_date_time) )
7301 7786 {
7302 - $sale_date_time = date("Y-m-d H:i:s");
7787 + $sale_date_time = gmdate("Y-m-d H:i:s");
7303 7788 }
7304 7789
7305 7790 echo '<p class="form-field sale_date_field">
7306 7791
@@ -7305,9 +7790,9 @@
7305 7790 echo '<p class="form-field sale_date_field">
7306 7791
7307 7792 <label for="_sale_date">' . esc_html(__('Sale Date', 'propertyhive')) . '</label>
7308 7793
7309 - <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="">
7310 7795
7311 7796 </p>';
7312 7797
7313 7798 $args = array(
@@ -7334,9 +7819,9 @@
7334 7819 public function get_sale_actions()
7335 7820 {
7336 7821 check_ajax_referer( 'sale-actions', 'security' );
7337 7822
7338 - $post_id = (int)$_POST['sale_id'];
7823 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7339 7824
7340 7825 $status = get_post_meta( $post_id, '_status', TRUE );
7341 7826
7342 7827 // Success action panel
@@ -7345,9 +7830,9 @@
7345 7830 <div class="options_group" style="padding-top:8px;">
7346 7831
7347 7832 <div id="success_actions"></div>
7348 7833
7349 - <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>
7350 7835
7351 7836 </div>
7352 7837
7353 7838 </div>';
@@ -7398,8 +7883,9 @@
7398 7883 $actions = apply_filters( 'propertyhive_admin_post_actions', $actions, $post_id );
7399 7884
7400 7885 if ( !empty($actions) )
7401 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.
7402 7888 echo implode("", $actions);
7403 7889 }
7404 7890 else
7405 7891 {
@@ -7416,9 +7902,9 @@
7416 7902 public function sale_exchanged()
7417 7903 {
7418 7904 check_ajax_referer( 'sale-actions', 'security' );
7419 7905
7420 - $post_id = (int)$_POST['sale_id'];
7906 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7421 7907
7422 7908 $status = get_post_meta( $post_id, '_status', TRUE );
7423 7909
7424 7910 if ( $status == 'current' )
@@ -7442,9 +7928,9 @@
7442 7928 public function sale_completed()
7443 7929 {
7444 7930 check_ajax_referer( 'sale-actions', 'security' );
7445 7931
7446 - $post_id = (int)$_POST['sale_id'];
7932 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7447 7933
7448 7934 $status = get_post_meta( $post_id, '_status', TRUE );
7449 7935
7450 7936 if ( $status == 'exchanged' )
@@ -7468,9 +7954,9 @@
7468 7954 public function sale_fallen_through()
7469 7955 {
7470 7956 check_ajax_referer( 'sale-actions', 'security' );
7471 7957
7472 - $post_id = (int)$_POST['sale_id'];
7958 + $post_id = $this->get_authorized_record_id( 'sale_id', 'sale' );
7473 7959
7474 7960 $status = get_post_meta( $post_id, '_status', TRUE );
7475 7961
7476 7962 if ( $status == 'current' || $status == 'exchanged' )
@@ -7492,14 +7978,16 @@
7492 7978 }
7493 7979
7494 7980 public function get_property_sales_meta_box()
7495 7981 {
7496 - $post_id = (int)$_POST['post_id'];
7982 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
7497 7983
7498 7984 $selected_status = '';
7499 - 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'] ) )
7500 7987 {
7501 - $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'] ) );
7502 7990 }
7503 7991
7504 7992 include( PH()->plugin_path() . '/includes/admin/views/html-property-sales-meta-box.php' );
7505 7993
@@ -7510,14 +7998,16 @@
7510 7998 }
7511 7999
7512 8000 public function get_contact_sales_meta_box()
7513 8001 {
7514 - $post_id = (int)$_POST['post_id'];
8002 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
7515 8003
7516 8004 $selected_status = '';
7517 - 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'] ) )
7518 8007 {
7519 - $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'] ) );
7520 8010 }
7521 8011
7522 8012 include( PH()->plugin_path() . '/includes/admin/views/html-contact-sales-meta-box.php' );
7523 8013
@@ -7528,14 +8018,16 @@
7528 8018 }
7529 8019
7530 8020 public function get_property_enquiries_meta_box()
7531 8021 {
7532 - $post_id = (int)$_POST['post_id'];
8022 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
7533 8023
7534 8024 $selected_status = '';
7535 - 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'] ) )
7536 8027 {
7537 - $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'] ) );
7538 8030 }
7539 8031
7540 8032 include( PH()->plugin_path() . '/includes/admin/views/html-property-enquiries-meta-box.php' );
7541 8033
@@ -7546,14 +8038,16 @@
7546 8038 }
7547 8039
7548 8040 public function get_contact_enquiries_meta_box()
7549 8041 {
7550 - $post_id = (int)$_POST['post_id'];
8042 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
7551 8043
7552 8044 $selected_status = '';
7553 - 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'] ) )
7554 8047 {
7555 - $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'] ) );
7556 8050 }
7557 8051
7558 8052 include( PH()->plugin_path() . '/includes/admin/views/html-contact-enquiries-meta-box.php' );
7559 8053
@@ -7565,77 +8059,75 @@
7565 8059
7566 8060 /**
7567 8061 * Add new management key date via ajax
7568 8062 */
7569 - public function add_key_date()
7570 - {
7571 - $parent_post_id = (int)$_POST['post_id'];
7572 -
7573 - if ( $parent_post_id > 0 ) {
7574 - $date_description = wp_kses_post( trim( stripslashes( $_POST['key_date_description'] ) ) );
7575 - $date_type_id = ph_clean( stripslashes( $_POST['key_date_type'] ) );
7576 - $date_due = ph_clean($_POST['key_date_due']) . ' ' . ph_clean($_POST['key_date_hours']) . ':' . ph_clean($_POST['key_date_minutes']);
7577 - $date_notes = sanitize_textarea_field($_POST['key_date_notes']);
7578 -
7579 - $parent_post_type = get_post_type( $parent_post_id );
7580 -
7581 - // Insert key date record
7582 - $key_date_post = array(
7583 - 'post_title' => $date_description,
7584 - 'post_content' => '',
7585 - 'post_type' => 'key_date',
7586 - 'post_status' => 'publish',
7587 - 'comment_status' => 'closed',
7588 - 'ping_status' => 'closed',
7589 - );
7590 -
7591 - // Insert the post into the database
7592 - $key_date_post_id = wp_insert_post( $key_date_post );
7593 -
7594 - if ( is_wp_error($key_date_post_id) || $key_date_post_id == 0 )
7595 - {
7596 - $return = array('error' => 'Failed to create key date post. Please try again');
7597 - echo json_encode( $return );
7598 - 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 );
7599 8077 }
7600 -
7601 - add_post_meta( $key_date_post_id, '_date_due', $date_due );
7602 - add_post_meta( $key_date_post_id, '_key_date_status', 'pending' );
7603 - add_post_meta( $key_date_post_id, '_key_date_type_id', $date_type_id );
7604 - add_post_meta( $key_date_post_id, '_key_date_notes', $date_notes );
7605 -
7606 - switch ( $parent_post_type )
7607 - {
7608 - case 'property' :
7609 - {
7610 - add_post_meta( $key_date_post_id, '_property_id', $parent_post_id );
7611 - break;
7612 - }
7613 - case 'tenancy' :
7614 - {
7615 - add_post_meta( $key_date_post_id, '_tenancy_id', $parent_post_id );
7616 -
7617 - $parent_property_id = get_post_meta( $parent_post_id, '_property_id', true );
7618 - add_post_meta( $key_date_post_id, '_property_id', $parent_property_id );
7619 - break;
7620 - }
7621 - }
8078 + $details[$field] = sanitize_text_field( wp_unslash( $_POST[$field] ) );
7622 8079 }
7623 - 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 ) );
7624 8111 }
7625 8112
7626 8113 public function get_management_dates_grid()
7627 8114 {
7628 - $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' ) );
7629 8117
7630 - 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'] ) )
7631 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.
7632 8122 $selected_type_id = (int)$_POST['selected_type_id'];
7633 8123 }
7634 8124
7635 - 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'] ) )
7636 8127 {
7637 - $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'] ) );
7638 8130 }
7639 8131
7640 8132 include( PH()->plugin_path() . '/includes/admin/views/html-management-dates-meta-box.php' );
7641 8133
@@ -7644,9 +8136,10 @@
7644 8136 }
7645 8137
7646 8138 public function get_key_dates_quick_edit_row()
7647 8139 {
7648 - $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' ) );
7649 8142
7650 8143 include( PH()->plugin_path() . '/includes/admin/views/html-key-dates-quick-edit.php' );
7651 8144
7652 8145 // Quit out
@@ -7654,9 +8147,10 @@
7654 8147 }
7655 8148
7656 8149 public function check_key_date_recurrence()
7657 8150 {
7658 - $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' );
7659 8153
7660 8154 $next_key_date = '';
7661 8155
7662 8156 $key_date = new PH_Key_Date(get_post($post_id));
@@ -7716,26 +8210,43 @@
7716 8210
7717 8211 if ( ! current_user_can( 'manage_propertyhive' ) )
7718 8212 wp_send_json_error( __( 'You do not have permission to manage key dates', 'propertyhive' ), 403 );
7719 8213
7720 - $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 + }
7721 8232
7722 8233 $args = array(
7723 8234 'ID' => $key_date_post_id,
7724 - 'post_title' => ph_clean($_POST['description']),
8235 + 'post_title' => $date_input['description'],
7725 8236 );
7726 - wp_update_post( $args );
8237 + wp_update_post( wp_slash( $args ) );
7727 8238
7728 - update_post_meta( $key_date_post_id, '_date_due', ph_clean($_POST['due_date_time']) );
7729 - update_post_meta( $key_date_post_id, '_key_date_status', ph_clean($_POST['status']) );
7730 - update_post_meta( $key_date_post_id, '_key_date_type_id', (int)$_POST['type'] );
7731 - 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'] ));
7732 8243
7733 - if ( isset($_POST['next_key_date']) )
8244 + if ( null !== $next_key_date )
7734 8245 {
7735 8246 // Insert next key date record
7736 8247 $next_key_date_post = array(
7737 - 'post_title' => ph_clean($_POST['description']),
8248 + 'post_title' => $date_input['description'],
7738 8249 'post_content' => '',
7739 8250 'post_type' => 'key_date',
7740 8251 'post_status' => 'publish',
7741 8252 'comment_status' => 'closed',
@@ -7742,9 +8253,9 @@
7742 8253 'ping_status' => 'closed',
7743 8254 );
7744 8255
7745 8256 // Insert the post into the database
7746 - $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 ) );
7747 8258
7748 8259 if ( is_wp_error($next_key_date_post_id) || $next_key_date_post_id == 0 )
7749 8260 {
7750 8261 $return = array('error' => 'Failed to create next key date post. Please try again');
@@ -7751,11 +8262,11 @@
7751 8262 echo json_encode( $return );
7752 8263 die();
7753 8264 }
7754 8265
7755 - 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 );
7756 8267 add_post_meta( $next_key_date_post_id, '_key_date_status', 'pending' );
7757 - 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'] ) );
7758 8269
7759 8270 if ( metadata_exists('post', $key_date_post_id, '_property_id') ) {
7760 8271 add_post_meta( $next_key_date_post_id, '_property_id', get_post_meta($key_date_post_id, '_property_id', true) );
7761 8272 }
@@ -7776,9 +8287,12 @@
7776 8287
7777 8288 if ( ! current_user_can( 'manage_propertyhive' ) )
7778 8289 wp_send_json_error( __( 'You do not have permission to manage key dates', 'propertyhive' ), 403 );
7779 8290
7780 - $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 + }
7781 8295
7782 8296 wp_delete_post($date_post_id, TRUE);
7783 8297
7784 8298 $return = array('success' => true);
@@ -7788,13 +8302,15 @@
7788 8302 }
7789 8303
7790 8304 public function get_property_tenancies_grid()
7791 8305 {
7792 - $post_id = (int)$_POST['post_id'];
8306 + $post_id = $this->get_authorized_record_id( 'post_id', 'property' );
7793 8307
7794 - 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'] ) )
7795 8310 {
7796 - $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'] ) );
7797 8313 }
7798 8314
7799 8315 include( PH()->plugin_path() . '/includes/admin/views/html-property-tenancies-meta-box.php' );
7800 8316
@@ -7803,13 +8319,15 @@
7803 8319 }
7804 8320
7805 8321 public function get_contact_tenancies_grid()
7806 8322 {
7807 - $post_id = (int)$_POST['post_id'];
8323 + $post_id = $this->get_authorized_record_id( 'post_id', 'contact' );
7808 8324
7809 - 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'] ) )
7810 8327 {
7811 - $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'] ) );
7812 8330 }
7813 8331
7814 8332 include( PH()->plugin_path() . '/includes/admin/views/html-contact-tenancies-meta-box.php' );
7815 8333
@@ -7818,18 +8336,22 @@
7818 8336 }
7819 8337
7820 8338 public function get_contact_solicitor()
7821 8339 {
7822 - 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 ) )
7823 8343 {
7824 8344 case 'contact':
7825 8345 {
7826 - $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 );
7827 8348 break;
7828 8349 }
7829 8350 case 'property':
7830 8351 {
7831 - $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);
7832 8354 if ( !empty( $owner_contact_ids ) )
7833 8355 {
7834 8356 if ( !is_array($owner_contact_ids) )
7835 8357 {
@@ -7871,9 +8393,9 @@
7871 8393 }
7872 8394
7873 8395 public function activate_pro_feature()
7874 8396 {
7875 - 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' ) )
7876 8398 {
7877 8399 $return = array(
7878 8400 'errorMessage' => 'Invalid nonce provided'
7879 8401 );
@@ -7879,18 +8401,18 @@
7879 8401 );
7880 8402 wp_send_json_error($return);
7881 8403 }
7882 8404
7883 - if ( ! current_user_can( 'install_plugins' ) )
8405 + if ( ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'install_plugins' ) )
7884 8406 {
7885 8407 $return = array(
7886 - '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' )
7887 8409 );
7888 8410 wp_send_json_error( $return );
7889 8411 }
7890 8412
7891 8413 // check plugin status
7892 - $slug = ph_clean($_POST['slug']);
8414 + $slug = isset( $_POST['slug'] ) && is_string( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
7893 8415
7894 8416 $feature = get_ph_pro_feature( $slug );
7895 8417
7896 8418 if ( $feature === false )
@@ -8053,40 +8575,39 @@
8053 8575 );
8054 8576 wp_send_json_error($return);
8055 8577 }
8056 8578
8057 - $tmpfname = WP_PLUGIN_DIR . '/' . $slug . '.zip';
8058 -
8059 - $handle = @fopen($tmpfname, "w");
8060 - if ( $handle === false )
8061 - {
8062 - $return = array(
8063 - 'errorMessage' => 'Failed to write plugin contents to temp file: ' . $tmpfname
8064 - );
8065 - 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' ) ) );
8066 8582 }
8067 - fwrite($handle, $zip_contents);
8068 - fclose($handle);
8069 8583
8070 - global $wp_filesystem;
8071 -
8072 8584 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
8073 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 + }
8074 8591
8592 + global $wp_filesystem;
8075 8593 $wp_filesystem = new WP_Filesystem_Direct( false );
8076 8594
8077 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.
8078 8597 define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ) );
8079 8598 }
8080 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.
8081 8601 define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 | 0755 ) );
8082 8602 }
8083 8603
8084 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.
8085 8606 $unzipped = unzip_file( $tmpfname, WP_PLUGIN_DIR );
8086 8607 if ( is_wp_error( $unzipped ) )
8087 8608 {
8088 - @unlink($tmpfname);
8609 + @wp_delete_file($tmpfname);
8089 8610
8090 8611 $return = array(
8091 8612 'errorMessage' => $unzipped->get_error_message()
8092 8613 );
@@ -8092,9 +8613,9 @@
8092 8613 );
8093 8614 wp_send_json_error($return);
8094 8615 }
8095 8616
8096 - @unlink($tmpfname);
8617 + @wp_delete_file($tmpfname);
8097 8618
8098 8619 // Need to sort out cache for activate plugin to work
8099 8620 // Taken from WordPress.org docs
8100 8621 $cache_plugins = wp_cache_get( 'plugins', 'plugins' );
@@ -8143,9 +8664,9 @@
8143 8664 }
8144 8665
8145 8666 public function deactivate_pro_feature()
8146 8667 {
8147 - 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' ) )
8148 8669 {
8149 8670 $return = array(
8150 8671 'errorMessage' => 'Invalid nonce provided'
8151 8672 );
@@ -8151,22 +8672,22 @@
8151 8672 );
8152 8673 wp_send_json_error($return);
8153 8674 }
8154 8675
8155 - if ( ! current_user_can( 'install_plugins' ) )
8676 + if ( ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'install_plugins' ) )
8156 8677 {
8157 8678 $return = array(
8158 - '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' )
8159 8680 );
8160 8681 wp_send_json_error( $return );
8161 8682 }
8162 8683
8163 8684 // check plugin is active
8164 - $slug = ph_clean($_POST['slug']);
8685 + $slug = isset( $_POST['slug'] ) && is_string( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
8165 8686
8166 8687 $feature = get_ph_pro_feature( $slug );
8167 8688
8168 - if ( !is_plugin_active( $feature['wordpress_plugin_file'] ) )
8689 + if ( false === $feature || ! is_plugin_active( $feature['wordpress_plugin_file'] ) )
8169 8690 {
8170 8691 $return = array(
8171 8692 'errorMessage' => 'Plugin not active'
8172 8693 );