| @@ -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 | |