$is_valid ); if ( ! $is_valid && $sync_error ) { // Since the option exists, and did not validate, delete it. Jetpack_Options::delete_option( 'sync_error_idc' ); } return $is_valid; } /** * Reverses WP.com URLs stored in sync_error_idc option. * * @param array $sync_error error option containing reversed URLs. * @return array */ public static function reverse_wpcom_urls_for_idc( $sync_error ) { if ( isset( $sync_error['reversed_url'] ) ) { if ( array_key_exists( 'wpcom_siteurl', $sync_error ) ) { $sync_error['wpcom_siteurl'] = strrev( $sync_error['wpcom_siteurl'] ); } if ( array_key_exists( 'wpcom_home', $sync_error ) ) { $sync_error['wpcom_home'] = strrev( $sync_error['wpcom_home'] ); } } return $sync_error; } /** * Normalizes a url by doing three things: * - Strips protocol * - Strips www * - Adds a trailing slash * * @param string $url URL to parse. * * @return WP_Error|string * @since 0.2.0 * @since-jetpack 4.4.0 */ public static function normalize_url_protocol_agnostic( $url ) { $parsed_url = wp_parse_url( trailingslashit( esc_url_raw( $url ) ) ); if ( ! $parsed_url || empty( $parsed_url['host'] ) || empty( $parsed_url['path'] ) ) { return new WP_Error( 'cannot_parse_url', sprintf( /* translators: %s: URL to parse. */ esc_html__( 'Cannot parse URL %s', 'jetpack-idc' ), $url ) ); } // Strip www and protocols. $url = preg_replace( '/^www\./i', '', $parsed_url['host'] . $parsed_url['path'] ); return $url; } /** * Gets the value that is to be saved in the jetpack_sync_error_idc option. * * @param array $response HTTP response. * * @return array Array of the local urls, wpcom urls, and error code. * @since 0.2.0 * @since-jetpack 4.4.0 * @since-jetpack 5.4.0 Add transient since home/siteurl retrieved directly from DB. */ public static function get_sync_error_idc_option( $response = array() ) { // Since the local options will hit the database directly, store the values // in a transient to allow for autoloading and caching on subsequent views. $local_options = get_transient( 'jetpack_idc_local' ); if ( false === $local_options ) { $local_options = array( 'home' => Urls::home_url(), 'siteurl' => Urls::site_url(), ); set_transient( 'jetpack_idc_local', $local_options, MINUTE_IN_SECONDS ); } $options = array_merge( $local_options, $response ); $returned_values = array(); foreach ( $options as $key => $option ) { if ( 'error_code' === $key ) { $returned_values[ $key ] = $option; continue; } $normalized_url = self::normalize_url_protocol_agnostic( $option ); if ( is_wp_error( $normalized_url ) ) { continue; } $returned_values[ $key ] = $normalized_url; } // We need to protect WPCOM URLs from search & replace by reversing them. See https://wp.me/pf5801-3R // Add 'reversed_url' key for backward compatibility if ( array_key_exists( 'wpcom_home', $returned_values ) && array_key_exists( 'wpcom_siteurl', $returned_values ) ) { $returned_values['reversed_url'] = true; $returned_values = self::reverse_wpcom_urls_for_idc( $returned_values ); } return $returned_values; } /** * Returns the value of the jetpack_should_handle_idc filter or constant. * If set to true, the site will be put into staging mode. * * This method uses both the current jetpack_should_handle_idc filter * and constant to determine whether an IDC should be handled. * * @return bool * @since 0.2.6 */ public static function should_handle_idc() { if ( Constants::is_defined( 'JETPACK_SHOULD_HANDLE_IDC' ) ) { $default = Constants::get_constant( 'JETPACK_SHOULD_HANDLE_IDC' ); } else { $default = ! Constants::is_defined( 'SUNRISE' ) && ! is_multisite(); } /** * Allows sites to opt in for IDC mitigation which blocks the site from syncing to WordPress.com when the home * URL or site URL do not match what WordPress.com expects. The default value is either true, or the value of * JETPACK_SHOULD_HANDLE_IDC constant if set. * * @param bool $default Whether the site is opted in to IDC mitigation. * * @since 0.2.6 */ return (bool) apply_filters( 'jetpack_should_handle_idc', $default ); } /** * Whether the site is undergoing identity crisis. * * @return bool */ public static function has_identity_crisis() { return false !== static::check_identity_crisis() && ! static::$is_safe_mode_confirmed; } /** * Whether an admin has confirmed safe mode. * Unlike `static::$is_safe_mode_confirmed` this function always returns the actual flag value. * * @return bool */ public static function safe_mode_is_confirmed() { return Jetpack_Options::get_option( 'safe_mode_confirmed' ); } /** * Returns the mismatched URLs. * * @return array|bool The mismatched urls, or false if the site is not connected, offline, in safe mode, or the IDC error is not valid. */ public static function get_mismatched_urls() { if ( ! static::has_identity_crisis() ) { return false; } $data = static::check_identity_crisis(); if ( ! $data || ! isset( $data['error_code'] ) || ! isset( $data['wpcom_home'] ) || ! isset( $data['home'] ) || ! isset( $data['wpcom_siteurl'] ) || ! isset( $data['siteurl'] ) ) { // The jetpack_sync_error_idc option is missing a key. return false; } if ( 'jetpack_site_url_mismatch' === $data['error_code'] ) { return array( 'wpcom_url' => $data['wpcom_siteurl'], 'current_url' => $data['siteurl'], ); } return array( 'wpcom_url' => $data['wpcom_home'], 'current_url' => $data['home'], ); } /** * Try to detect $_SERVER['HTTP_HOST'] being used within WP_SITEURL or WP_HOME definitions inside of wp-config. * * If `HTTP_HOST` usage is found, it's possbile (though not certain) that site URLs are dynamic. * * When a site URL is dynamic, it can lead to a Jetpack IDC. If potentially dynamic usage is detected, * helpful support info will be shown on the IDC UI about setting a static site/home URL. * * @return bool True if potentially dynamic site urls were detected in wp-config, false otherwise. */ public static function detect_possible_dynamic_site_url() { $transient_key = 'jetpack_idc_possible_dynamic_site_url_detected'; $transient_val = get_transient( $transient_key ); if ( false !== $transient_val ) { return (bool) $transient_val; } $path = self::locate_wp_config(); $wp_config = $path ? file_get_contents( $path ) : false; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents if ( $wp_config ) { $matched = preg_match( '/define ?\( ?[\'"](?:WP_SITEURL|WP_HOME).+(?:HTTP_HOST).+\);/', $wp_config ); if ( $matched ) { set_transient( $transient_key, 1, HOUR_IN_SECONDS ); return true; } } set_transient( $transient_key, 0, HOUR_IN_SECONDS ); return false; } /** * Gets path to WordPress configuration. * Source: https://github.com/wp-cli/wp-cli/blob/master/php/utils.php * * @return string */ public static function locate_wp_config() { static $path; if ( null === $path ) { $path = false; if ( getenv( 'WP_CONFIG_PATH' ) && file_exists( getenv( 'WP_CONFIG_PATH' ) ) ) { $path = getenv( 'WP_CONFIG_PATH' ); } elseif ( file_exists( ABSPATH . 'wp-config.php' ) ) { $path = ABSPATH . 'wp-config.php'; } elseif ( file_exists( dirname( ABSPATH ) . '/wp-config.php' ) && ! file_exists( dirname( ABSPATH ) . '/wp-settings.php' ) ) { $path = dirname( ABSPATH ) . '/wp-config.php'; } if ( $path ) { $path = realpath( $path ); } } return $path; } /** * Adds `url_secret` to the `jetpack.idcUrlValidation` URL validation endpoint. * Adds `url_secret_error` in case of an error. * * @param array $response The endpoint response that we're modifying. * * @return array * * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag -- The exception is being caught, false positive. */ public static function add_secret_to_url_validation_response( array $response ) { try { $secret = new URL_Secret(); $secret->create(); if ( $secret->exists() ) { $response['url_secret'] = $secret->get_secret(); } } catch ( Exception $e ) { $response['url_secret_error'] = new WP_Error( 'unable_to_create_url_secret', $e->getMessage() ); } return $response; } /** * Check if URL is an IP. * * @param string $hostname The hostname to check. * @return bool */ public static function url_is_ip( $hostname = null ) { if ( ! $hostname ) { $hostname = wp_parse_url( Urls::site_url(), PHP_URL_HOST ); } $is_ip = filter_var( $hostname, FILTER_VALIDATE_IP ) !== false ? $hostname : false; return $is_ip; } /** * Add IDC-related data to the registration query. * * @param array $params The existing query params. * * @return array */ public static function register_request_body( array $params ) { $persistent_blog_id = get_option( static::PERSISTENT_BLOG_ID_OPTION_NAME ); if ( $persistent_blog_id ) { $params['persistent_blog_id'] = $persistent_blog_id; $params['url_secret'] = URL_Secret::create_secret( 'registration_request_url_secret_failed' ); } return $params; } /** * Set the necessary options when site gets registered. * * @param int $blog_id The blog ID. * * @return void */ public static function site_registered( $blog_id ) { update_option( static::PERSISTENT_BLOG_ID_OPTION_NAME, (int) $blog_id, false ); } /** * Check if we need to update the ip_requester option. * * @param string $hostname The hostname to check. * * @return void */ public static function maybe_update_ip_requester( $hostname ) { // Check if transient exists $transient_key = ip2long( $hostname ); if ( $transient_key && ! get_transient( 'jetpack_idc_ip_requester_' . $transient_key ) ) { self::set_ip_requester_for_idc( $hostname, $transient_key ); } } /** * If URL is an IP, add the IP value to the ip_requester option with its expiry value. * * @param string $hostname The hostname to check. * @param int $transient_key The transient key. */ public static function set_ip_requester_for_idc( $hostname, $transient_key ) { // Check if option exists $data = Jetpack_Options::get_option( 'identity_crisis_ip_requester' ); $ip_requester = array( 'ip' => $hostname, 'expires_at' => time() + 360, ); // If not set, initialize it if ( empty( $data ) ) { $data = array( $ip_requester ); } else { $updated_data = array(); $updated_value = false; // Remove expired values and update existing IP foreach ( $data as $item ) { if ( time() > $item['expires_at'] ) { continue; // Skip expired IP } if ( $item['ip'] === $hostname ) { $item['expires_at'] = time() + 360; $updated_value = true; } $updated_data[] = $item; } if ( ! $updated_value || empty( $updated_data ) ) { $updated_data[] = $ip_requester; } $data = $updated_data; } self::update_ip_requester( $data, $transient_key ); } /** * Update the ip_requester option and set a transient to expire in 5 minutes. * * @param array $data The data to be updated. * @param int $transient_key The transient key. * * @return void */ public static function update_ip_requester( $data, $transient_key ) { // Update the option $updated = Jetpack_Options::update_option( 'identity_crisis_ip_requester', $data ); // Set a transient to expire in 5 minutes if ( $updated ) { $transient_name = 'jetpack_idc_ip_requester_' . $transient_key; set_transient( $transient_name, $data, 300 ); } } /** * Adds `ip_requester` to the `jetpack.idcUrlValidation` URL validation endpoint. * * @param array $response The enpoint response that we're modifying. * * @return array */ public static function add_ip_requester_to_url_validation_response( array $response ) { $requesters = Jetpack_Options::get_option( 'identity_crisis_ip_requester' ); if ( $requesters ) { // Loop through the requesters and add the IP to the response if it's not expired $i = 0; foreach ( $requesters as $ip ) { if ( $ip['expires_at'] > time() ) { $response['ip_requester'][] = $ip['ip']; } // Limit the response to five IPs $i = ++$i; if ( $i === 5 ) { break; } } } return $response; } }