PluginProbe
Property Hive / 2.3.0
Property Hive v2.3.0
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 1.4.62 All 260 releases
← All changes | propertyhive.php +203 -74 2.2.32.3.0 View file →
@@ -2,16 +2,17 @@
2 2 /**
3 3 * Plugin Name: Property Hive
4 4 * Plugin URI: https://wordpress.org/plugins/propertyhive/
5 5 * Description: Property Hive has everything you need to build estate agency websites
6 - * Version: 2.2.3
6 + * Version: 2.3.0
7 7 * Author: PropertyHive
8 8 * Author URI: https://wp-property-hive.com
9 + * License: GPLv3
10 + * License URI: https://www.gnu.org/licenses/gpl-3.0.html
9 11 * Requires at least: 5.6
10 - * Tested up to: 6.9
12 + * Tested up to: 7.1
11 13 *
12 14 * Text Domain: propertyhive
13 - * Domain Path: /i18n/languages/
14 15 *
15 16 * @package PropertyHive
16 17 * @category Core
17 18 * @author PropertyHive
@@ -26,9 +27,9 @@
26 27 /**
27 28 * Main PropertyHive Class
28 29 *
29 30 * @class PropertyHive
30 - * @version 2.2.3
31 + * @version 2.3.0
31 32 */
32 33 final class PropertyHive {
33 34
34 35 /**
@@ -33,9 +34,9 @@
33 34
34 35 /**
35 36 * @var string
36 37 */
37 - public $version = '2.2.3';
38 + public $version = '2.3.0';
38 39
39 40 /**
40 41 * @var PropertyHive The single instance of the class
41 42 */
@@ -98,9 +99,9 @@
98 99 *
99 100 * @since 1.0.0
100 101 */
101 102 public function __clone() {
102 - _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'propertyhive' ), '1.0.0' );
103 + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'propertyhive' ), '1.0.0' );
103 104 }
104 105
105 106 /**
106 107 * Unserializing instances of this class is forbidden.
@@ -107,9 +108,9 @@
107 108 *
108 109 * @since 1.0.0
109 110 */
110 111 public function __wakeup() {
111 - _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'propertyhive' ), '1.0.0' );
112 + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'propertyhive' ), '1.0.0' );
112 113 }
113 114
114 115 /**
115 116 * PropertyHive Constructor.
@@ -158,10 +159,13 @@
158 159 $page_ids = array_filter( array( ph_get_page_id( 'my_account' ) ) );
159 160
160 161 if ( !empty($page_ids) && is_page( $page_ids ) )
161 162 {
163 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress/cache-plugin integration constant DONOTCACHEPAGE; cache integrations depend on this established global constant name.
162 164 if ( !defined('DONOTCACHEPAGE') ) { define('DONOTCACHEPAGE', TRUE); }
165 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress/cache-plugin integration constant DONOTCACHEOBJECT; cache integrations depend on this established global constant name.
163 166 if ( !defined('DONOTCACHEOBJECT') ) { define('DONOTCACHEOBJECT', TRUE); }
167 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress/cache-plugin integration constant DONOTCACHEDB; cache integrations depend on this established global constant name.
164 168 if ( !defined('DONOTCACHEDB') ) { define('DONOTCACHEDB', TRUE); }
165 169 }
166 170 }
167 171
@@ -170,9 +174,9 @@
170 174 $custom_departments = ph_get_custom_departments();
171 175
172 176 foreach ( $custom_departments as $key => $custom_department )
173 177 {
174 - $departments[$key] = __( $custom_department['name'], 'propertyhive' );
178 + $departments[$key] = $custom_department['name'];
175 179 }
176 180
177 181 return $departments;
178 182 }
@@ -186,11 +190,13 @@
186 190 if ( !$post = get_post($post_id) ) {
187 191 return false;
188 192 }
189 193
194 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Recount immediately after comment changes, excluding internal CRM notes; a cached count would be stale at this mutation boundary.
190 195 $new = (int) $wpdb->get_var( $wpdb->prepare("SELECT COUNT(*)
191 196 FROM $wpdb->comments
192 197 WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type != 'propertyhive_note' ", $post_id) );
198 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Core comment_count needs the recalculated non-note count; clean_post_cache immediately below invalidates the affected post.
193 199 $wpdb->update( $wpdb->posts, array('comment_count' => $new), array('ID' => $post_id) );
194 200
195 201 clean_post_cache( $post );
196 202 }
@@ -251,12 +257,15 @@
251 257 * Define PH Constants
252 258 */
253 259 private function define_constants()
254 260 {
261 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Legacy public PH_* constants are consumed by existing themes and add-ons.
255 262 define( 'PH_PLUGIN_FILE', __FILE__ );
263 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Preserve the existing public version constant used by add-ons.
256 264 define( 'PH_VERSION', $this->version );
257 265
258 266 if ( ! defined( 'PH_TEMPLATE_PATH' ) ) {
267 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Preserve the existing theme-overridable template path constant.
259 268 define( 'PH_TEMPLATE_PATH', $this->template_path() );
260 269 }
261 270 }
262 271
@@ -279,9 +288,10 @@
279 288 if ( defined( 'DOING_AJAX' ) ) {
280 289 $this->ajax_includes();
281 290 }
282 291
283 - if ( ! is_admin() || defined( 'DOING_AJAX' ) || (isset($_GET['action']) && $_GET['action'] == 'elementor') ) {
292 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only Elementor editor detection selects frontend includes; it does not change saved data.
293 + if ( ! is_admin() || defined( 'DOING_AJAX' ) || ( isset( $_GET['action'] ) && is_string( $_GET['action'] ) && 'elementor' === $_GET['action'] ) ) {
284 294 $this->frontend_includes();
285 295 }
286 296
287 297 include_once( 'includes/ph-form-functions.php' ); // Form Renderers
@@ -309,8 +319,10 @@
309 319 include_once( 'includes/class-ph-rank-math.php' ); // Rank Math
310 320 include_once( 'includes/class-ph-aioseo.php' ); // All In One SEO
311 321 include_once( 'includes/class-ph-duplicate-post.php' ); // Duplicate Post
312 322
323 + include_once( 'includes/class-ph-search-analytics.php' ); // Search Analytics
324 +
313 325 include_once( 'includes/class-ph-additional-fields.php' ); // Additional Fields
314 326 include_once( 'includes/class-ph-text-substitution.php' ); // Text Substitution
315 327
316 328 include_once( 'includes/ph-pro-feature-functions.php' ); // Pro Features
@@ -357,71 +369,174 @@
357 369 include_once( 'includes/widgets/class-ph-widget-properties.php' );*/
358 370 }
359 371
360 372 /**
361 - * Unsubscribe contact if ph_unsubscribe param set in query string. Might be a better place for this
373 + * Contacts may store several comma-separated mailbox addresses.
362 374 */
363 - public function unsubscribe_contact() {
364 - if ( isset($_GET['ph_unsubscribe']) && !empty($_GET['ph_unsubscribe']) )
365 - {
366 - $ph_unsubscribe = sanitize_text_field(base64_decode($_GET['ph_unsubscribe']));
367 - if ( $ph_unsubscribe === FALSE )
368 - {
369 - die("Invalid token passed 1");
370 - return false;
375 + private function contact_unsubscribe_recipients( $email ) {
376 + if ( ! is_string( $email ) || '' === $email ) {
377 + return array();
378 + }
379 + $recipients = array_map( 'trim', explode( ',', $email ) );
380 + foreach ( $recipients as $recipient ) {
381 + if ( ! is_email( $recipient ) ) {
382 + return array();
371 383 }
384 + }
385 + return array_values( array_unique( $recipients ) );
386 + }
372 387
373 - $explode_ph_unsubscribe = explode("|", $ph_unsubscribe);
374 - if ( count($explode_ph_unsubscribe) != 2 )
375 - {
376 - die("Invalid token passed 2");
377 - return false;
378 - }
388 + /**
389 + * Build a durable, email-bound unsubscribe link for an existing contact.
390 + */
391 + public function get_contact_unsubscribe_url( $contact_id ) {
392 + if ( ( ! is_int( $contact_id ) && ! is_string( $contact_id ) ) || ! ctype_digit( (string) $contact_id ) ) {
393 + return '';
394 + }
395 + $contact_id = (int) $contact_id;
396 + $email = get_post_meta( $contact_id, '_email_address', true );
397 + if ( ! $contact_id || 'contact' !== get_post_type( $contact_id ) || ! $this->contact_unsubscribe_recipients( $email ) ) {
398 + return '';
399 + }
400 + return $this->contact_unsubscribe_token_url( 'v2|' . $contact_id, $email );
401 + }
379 402
380 - $contact_id = $explode_ph_unsubscribe[0];
381 - if ( FALSE === get_post_status( $contact_id ) )
382 - {
383 - die("Invalid token passed 3");
403 + /**
404 + * Sign the purpose/version, payload and current email with the site's secret.
405 + */
406 + private function contact_unsubscribe_token_url( $payload, $email ) {
407 + $signature = hash_hmac( 'sha256', 'propertyhive-unsubscribe|' . $payload . '|' . $email, wp_salt( 'auth' ) );
408 + return add_query_arg( 'ph_unsubscribe', rawurlencode( base64_encode( $payload . '|' . $signature ) ), site_url( '/' ) );
409 + }
410 +
411 + /**
412 + * Atomically limit mailbox-verification mail, including concurrent requests.
413 + */
414 + private function contact_unsubscribe_mail_slot( $contact_id ) {
415 + global $wpdb;
416 + $key = 'propertyhive_unsubscribe_cooldown_' . $contact_id;
417 + $now = time();
418 + $previous = get_option( $key, false );
419 + if ( false !== $previous ) {
420 + if ( ! is_numeric( $previous ) || (int) $previous > $now ) {
384 421 return false;
385 422 }
386 - $contact_email = get_post_meta( $contact_id, '_email_address', TRUE );
423 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Compare-and-delete the expired lock atomically: delete_option could remove a newer request's lock. Invalidate the option cache immediately below.
424 + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s", $key, (string) $previous ) );
425 + wp_cache_delete( $key, 'options' );
426 + }
427 + // INSERT IGNORE must not overwrite another request's newly acquired lock (add_option can update duplicate rows).
428 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- The unique option_name index is the cross-request lock; clear positive and negative option caches immediately after the atomic insert.
429 + $acquired = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, %s)", $key, (string) ( $now + 5 * MINUTE_IN_SECONDS ), 'no' ) );
430 + wp_cache_delete( $key, 'options' );
431 + wp_cache_delete( 'notoptions', 'options' );
432 + return 1 === $acquired;
433 + }
387 434
388 - if (md5($contact_email) != $explode_ph_unsubscribe[1])
389 - {
390 - die("Invalid token passed 4");
391 - return false;
392 - }
435 + private function contact_unsubscribe_result( $message, $status = 200 ) {
436 + wp_die( esc_html( $message ), esc_html__( 'Unsubscribe', 'propertyhive' ), array( 'response' => (int) $status ) );
437 + }
393 438
394 - // TODO: Make sure not already unsubscribed
439 + /**
440 + * Signed links authorize unsubscribe; old links first require mailbox proof.
441 + */
442 + public function unsubscribe_contact() {
443 + if ( ! isset( $_GET['ph_unsubscribe'] ) ) {
444 + return;
445 + }
446 + $invalid = __( 'This unsubscribe link is invalid or has expired.', 'propertyhive' );
447 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.NonceVerification.Recommended -- Bound the raw token's size before decoding; no value is used until signature/mailbox verification below.
448 + if ( ! is_string( $_GET['ph_unsubscribe'] ) || strlen( $_GET['ph_unsubscribe'] ) > 512 ) {
449 + $this->contact_unsubscribe_result( $invalid, 400 );
450 + return;
451 + }
452 + $encoded_token = sanitize_text_field( wp_unslash( $_GET['ph_unsubscribe'] ) );
453 + $decoded = base64_decode( $encoded_token, true );
454 + $parts = false !== $decoded ? explode( '|', $decoded ) : array();
455 + $legacy = count( $parts ) === 2;
456 + $version = $legacy ? 'legacy' : ( isset( $parts[0] ) ? $parts[0] : '' );
457 + $id_part = $legacy ? $parts[0] : ( isset( $parts[1] ) ? $parts[1] : '' );
458 + $contact_id = ctype_digit( $id_part ) ? (int) $id_part : 0;
459 + $email = $contact_id ? get_post_meta( $contact_id, '_email_address', true ) : '';
460 + if ( ! $contact_id || 'contact' !== get_post_type( $contact_id ) || ! $this->contact_unsubscribe_recipients( $email ) ) {
461 + $this->contact_unsubscribe_result( $invalid, 400 );
462 + return;
463 + }
395 464
396 - // We've got this far. We received a valid token and email address
397 - $forbidden_contact_methods = get_post_meta( $contact_id, '_forbidden_contact_methods', TRUE );
398 - if (!is_array($forbidden_contact_methods))
399 - {
400 - $forbidden_contact_methods = array();
465 + if ( $legacy ) {
466 + if ( ! hash_equals( md5( $email ), $parts[1] ) ) {
467 + $this->contact_unsubscribe_result( $invalid, 400 );
468 + return;
401 469 }
402 - $forbidden_contact_methods[] = 'email';
403 - update_post_meta( $contact_id, '_forbidden_contact_methods', array_unique($forbidden_contact_methods) );
470 + $nonce_action = 'propertyhive-unsubscribe-request-' . $contact_id;
471 + if ( isset( $_POST['propertyhive_unsubscribe_confirm'] ) ) {
472 + if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' !== $_SERVER['REQUEST_METHOD'] || ! is_string( $_POST['propertyhive_unsubscribe_confirm'] ) || '1' !== $_POST['propertyhive_unsubscribe_confirm'] || ! isset( $_POST['_wpnonce'] ) || ! is_string( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), $nonce_action ) ) {
473 + $this->contact_unsubscribe_result( $invalid, 400 );
474 + return;
475 + }
476 + if ( $this->contact_unsubscribe_mail_slot( $contact_id ) ) {
477 + // Reserve the cooldown before sending, including delivery failures.
478 + $nonce = wp_generate_password( 32, false, false );
479 + $expires = time() + HOUR_IN_SECONDS;
480 + $verify_key = 'propertyhive_unsubscribe_verify_' . $contact_id . '_' . hash( 'sha256', $nonce );
481 + set_transient( $verify_key, 1, HOUR_IN_SECONDS );
482 + $url = $this->contact_unsubscribe_token_url( 'v3|' . $contact_id . '|' . $expires . '|' . $nonce, $email );
483 + $sent = wp_mail(
484 + $this->contact_unsubscribe_recipients( $email ),
485 + __( 'Confirm your unsubscribe request', 'propertyhive' ),
486 + /* translators: %s: Mailbox verification URL. */
487 + sprintf( __( "To confirm your unsubscribe request, open this link within one hour:\n\n%s\n\nIf you did not request this, you can ignore this email.", 'propertyhive' ), $url )
488 + );
489 + if ( ! $sent ) {
490 + delete_transient( $verify_key );
491 + }
492 + }
493 + $this->contact_unsubscribe_result( __( 'Please check your inbox for a confirmation link. If you recently requested one, please allow a few minutes before trying again.', 'propertyhive' ) );
494 + return;
495 + }
496 + $legacy_url = add_query_arg( 'ph_unsubscribe', rawurlencode( $encoded_token ), site_url( '/' ) );
497 + $form = '<p>' . esc_html__( 'This older unsubscribe link requires email confirmation. Request a confirmation link to continue.', 'propertyhive' ) . '</p>';
498 + $form .= '<form method="post" action="' . esc_url( $legacy_url ) . '"><input type="hidden" name="propertyhive_unsubscribe_confirm" value="1"><input type="hidden" name="_wpnonce" value="' . esc_attr( wp_create_nonce( $nonce_action ) ) . '"><button type="submit">' . esc_html__( 'Send confirmation link', 'propertyhive' ) . '</button></form>';
499 + wp_die( wp_kses( $form, array( 'p' => array(), 'form' => array( 'method' => true, 'action' => true ), 'input' => array( 'type' => true, 'name' => true, 'value' => true ), 'button' => array( 'type' => true ) ) ), esc_html__( 'Unsubscribe', 'propertyhive' ), array( 'response' => 200 ) );
500 + return;
501 + }
404 502
405 - // Write note to applicant
406 - $comment = array(
407 - 'note_type' => 'unsubscribe'
408 - );
503 + $is_verification = 'v3' === $version && count( $parts ) === 5;
504 + if ( ! ( 'v2' === $version && count( $parts ) === 3 ) && ! $is_verification ) {
505 + $this->contact_unsubscribe_result( $invalid, 400 );
506 + return;
507 + }
508 + $signature = array_pop( $parts );
509 + $expected = hash_hmac( 'sha256', 'propertyhive-unsubscribe|' . implode( '|', $parts ) . '|' . $email, wp_salt( 'auth' ) );
510 + if ( ! hash_equals( $expected, $signature ) || ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) {
511 + $this->contact_unsubscribe_result( $invalid, 400 );
512 + return;
513 + }
514 + if ( $is_verification ) {
515 + $verify_key = 'propertyhive_unsubscribe_verify_' . $contact_id . '_' . hash( 'sha256', $parts[3] );
516 + if ( ! ctype_digit( $parts[2] ) || (int) $parts[2] <= time() || ! get_transient( $verify_key ) || ! delete_transient( $verify_key ) ) {
517 + $this->contact_unsubscribe_result( $invalid, 400 );
518 + return;
519 + }
520 + }
409 521
410 - $data = array(
411 - 'comment_post_ID' => $contact_id,
412 - 'comment_author' => 'Property Hive',
522 + $methods = get_post_meta( $contact_id, '_forbidden_contact_methods', true );
523 + $methods = is_array( $methods ) ? $methods : array();
524 + if ( ! in_array( 'email', $methods, true ) ) {
525 + $methods[] = 'email';
526 + update_post_meta( $contact_id, '_forbidden_contact_methods', wp_slash( array_unique( $methods ) ) );
527 + wp_insert_comment( array(
528 + 'comment_post_ID' => $contact_id,
529 + 'comment_author' => 'Property Hive',
413 530 'comment_author_email' => 'propertyhive@noreply.com',
414 - 'comment_author_url' => '',
415 - 'comment_date' => date("Y-m-d H:i:s"),
416 - 'comment_content' => serialize($comment),
417 - 'comment_approved' => 1,
418 - 'comment_type' => 'propertyhive_note',
419 - );
420 - wp_insert_comment( $data );
421 -
422 - die("You have been unsubscribed successfully. Please allow up to 24 hours for this to take effect.");
531 + 'comment_author_url' => '',
532 + 'comment_date' => gmdate( 'Y-m-d H:i:s' ),
533 + 'comment_content' => serialize( array( 'note_type' => 'unsubscribe' ) ),
534 + 'comment_approved' => 1,
535 + 'comment_type' => 'propertyhive_note',
536 + ) );
423 537 }
538 + $this->contact_unsubscribe_result( __( 'You have been unsubscribed successfully. Please allow up to 24 hours for this to take effect.', 'propertyhive' ) );
424 539 }
425 540
426 541 /**
427 542 * Init PropertyHive when WordPress Initialises.
@@ -427,8 +542,9 @@
427 542 * Init PropertyHive when WordPress Initialises.
428 543 */
429 544 public function init() {
430 545 // Before init action
546 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public Property Hive extension hook before_propertyhive_init; changing the established name would detach installed callbacks.
431 547 do_action( 'before_propertyhive_init' );
432 548
433 549 // Set up localisation
434 550 $this->load_plugin_textdomain();
@@ -469,8 +585,9 @@
469 585 *
470 586 * Note: the first-loaded translation file overrides any following ones if the same translation is present
471 587 */
472 588 public function load_plugin_textdomain() {
589 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core hook plugin_locale; renaming it would break the core hook contract.
473 590 $locale = apply_filters( 'plugin_locale', get_locale(), 'propertyhive' );
474 591
475 592 // Admin Locale
476 593 if ( is_admin() ) {
@@ -479,8 +596,9 @@
479 596 }
480 597
481 598 // Global + Frontend Locale
482 599 load_textdomain( 'propertyhive', WP_LANG_DIR . "/propertyhive/propertyhive-$locale.mo" );
600 + // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Preserve bundled i18n/languages translations and the plugin_locale override on supported WordPress versions; WordPress.org language packs alone do not cover this legacy custom path.
483 601 load_plugin_textdomain( 'propertyhive', false, plugin_basename( dirname( __FILE__ ) ) . "/i18n/languages" );
484 602 }
485 603
486 604 /**
@@ -487,27 +605,36 @@
487 605 * Ensure theme and server variable compatibility and setup image sizes..
488 606 */
489 607 public function setup_environment() {
490 608
491 - // IIS
492 - if ( ! isset($_SERVER['REQUEST_URI'] ) ) {
493 - $_SERVER['REQUEST_URI'] = substr( $_SERVER['PHP_SELF'], 1 );
494 - if ( isset( $_SERVER['QUERY_STRING'] ) ) {
495 - $_SERVER['REQUEST_URI'].='?'.$_SERVER['QUERY_STRING'];
609 + // IIS fallback must preserve encoded URLs and query syntax for WordPress routing.
610 + if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
611 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This is server-to-server URI compatibility state, not output; validate string shape and remove CR/LF while preserving URL encodings and query delimiters.
612 + $php_self = isset( $_SERVER['PHP_SELF'] ) && is_string( $_SERVER['PHP_SELF'] ) ? str_replace( array( "\r", "\n" ), '', wp_unslash( $_SERVER['PHP_SELF'] ) ) : '';
613 + $_SERVER['REQUEST_URI'] = substr( $php_self, 1 );
614 + if ( isset( $_SERVER['QUERY_STRING'] ) && is_string( $_SERVER['QUERY_STRING'] ) ) {
615 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Preserve the existing query string exactly apart from CR/LF; URL output escaping belongs at its eventual output boundary.
616 + $_SERVER['REQUEST_URI'] .= '?' . str_replace( array( "\r", "\n" ), '', wp_unslash( $_SERVER['QUERY_STRING'] ) );
496 617 }
497 618 }
498 -
499 - // NGINX Proxy
500 - if ( ! isset( $_SERVER['REMOTE_ADDR'] ) && isset( $_SERVER['HTTP_REMOTE_ADDR'] ) ) {
501 - $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_REMOTE_ADDR'];
619 +
620 + // Legacy NGINX proxy compatibility; only copy syntactically valid IP addresses.
621 + if ( ! isset( $_SERVER['REMOTE_ADDR'] ) && isset( $_SERVER['HTTP_REMOTE_ADDR'] ) && is_string( $_SERVER['HTTP_REMOTE_ADDR'] ) ) {
622 + $remote_address = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REMOTE_ADDR'] ) );
623 + if ( filter_var( $remote_address, FILTER_VALIDATE_IP ) ) {
624 + $_SERVER['REMOTE_ADDR'] = $remote_address;
625 + }
502 626 }
503 -
504 - if ( ! isset( $_SERVER['HTTPS'] ) && ! empty( $_SERVER['HTTP_HTTPS'] ) ) {
505 - $_SERVER['HTTPS'] = $_SERVER['HTTP_HTTPS'];
627 +
628 + if ( ! isset( $_SERVER['HTTPS'] ) && isset( $_SERVER['HTTP_HTTPS'] ) && is_string( $_SERVER['HTTP_HTTPS'] ) ) {
629 + $https = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HTTPS'] ) );
630 + if ( '' !== $https && '0' !== $https ) {
631 + $_SERVER['HTTPS'] = $https;
632 + }
506 633 }
507 -
508 - // Support for hosts which don't use HTTPS, and use HTTP_X_FORWARDED_PROTO
509 - if ( ! isset( $_SERVER['HTTPS'] ) && ! empty( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' ) {
634 +
635 + // Support hosts which use HTTP_X_FORWARDED_PROTO instead of HTTPS.
636 + if ( ! isset( $_SERVER['HTTPS'] ) && isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
510 637 $_SERVER['HTTPS'] = '1';
511 638 }
512 639 }
513 640
@@ -536,8 +663,9 @@
536 663 *
537 664 * @return string
538 665 */
539 666 public function template_path() {
667 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy public template path filter; themes and extensions can customize the plugin template directory through this exact hook.
540 668 return apply_filters( 'PH_TEMPLATE_PATH', 'propertyhive/' );
541 669 }
542 670
543 671 /**
@@ -557,9 +685,9 @@
557 685 * @return string
558 686 */
559 687 public function api_request_url( $request, $ssl = null ) {
560 688 if ( is_null( $ssl ) ) {
561 - $scheme = parse_url( get_option( 'home' ), PHP_URL_SCHEME );
689 + $scheme = wp_parse_url( get_option( 'home' ), PHP_URL_SCHEME );
562 690 } elseif ( $ssl ) {
563 691 $scheme = 'https';
564 692 } else {
565 693 $scheme = 'http';
@@ -581,8 +709,9 @@
581 709 *
582 710 * @since 1.0.0
583 711 * @return PropertyHive
584 712 */
713 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public global helper PH; the established callable name is part of the plugin/extension API and must remain stable.
585 714 function PH() {
586 715 return PropertyHive::instance();
587 716 }
588 717