diff --git a/3rd-party/3rd-party.php b/3rd-party/3rd-party.php index 9d5462747315..cdf08f273e27 100644 --- a/3rd-party/3rd-party.php +++ b/3rd-party/3rd-party.php @@ -10,6 +10,7 @@ require_once( JETPACK__PLUGIN_DIR . '3rd-party/bitly.php' ); require_once( JETPACK__PLUGIN_DIR . '3rd-party/bbpress.php' ); require_once( JETPACK__PLUGIN_DIR . '3rd-party/woocommerce.php' ); +require_once( JETPACK__PLUGIN_DIR . '3rd-party/domain-mapping.php' ); // We can't load this conditionally since polldaddy add the call in class constuctor. require_once( JETPACK__PLUGIN_DIR . '3rd-party/polldaddy.php' ); diff --git a/3rd-party/domain-mapping.php b/3rd-party/domain-mapping.php new file mode 100644 index 000000000000..6079ac325908 --- /dev/null +++ b/3rd-party/domain-mapping.php @@ -0,0 +1,113 @@ +function_exists( 'domain_mapping_siteurl' ) ) { + return false; + } + + add_filter( 'jetpack_sync_home_url', 'domain_mapping_siteurl' ); + add_filter( 'jetpack_sync_site_url', 'domain_mapping_siteurl' ); + + return true; + } + + /** + * This method will test for a class and method known to be used in WPMU Dev's domain mapping plugin. If the + * method exists, then we'll hook the swap_to_mapped_url() to our Jetpack sync filters for home_url and site_url. + * + * @return bool + */ + function hook_wpmu_dev_domain_mapping() { + if ( ! $this->class_exists( 'domain_map' ) || ! $this->method_exists( 'domain_map', 'utils' ) ) { + return false; + } + + $utils = $this->get_domain_mapping_utils_instance(); + add_filter( 'jetpack_sync_home_url', array( $utils, 'swap_to_mapped_url' ) ); + add_filter( 'jetpack_sync_site_url', array( $utils, 'swap_to_mapped_url' ) ); + + return true; + } + + /* + * Utility Methods + * + * These methods are very minimal, and in most cases, simply pass on arguments. Why create them you ask? + * So that we can test. + */ + + public function method_exists( $class, $method ) { + return method_exists( $class, $method ); + } + + public function class_exists( $class ) { + return class_exists( $class ); + } + + public function function_exists( $function ) { + return function_exists( $function ); + } + + public function get_domain_mapping_utils_instance() { + return domain_map::utils(); + } +} + +Jetpack_3rd_Party_Domain_Mapping::init(); diff --git a/class.jetpack-xmlrpc-server.php b/class.jetpack-xmlrpc-server.php index 61f1be7c3556..54e311b8da21 100644 --- a/class.jetpack-xmlrpc-server.php +++ b/class.jetpack-xmlrpc-server.php @@ -340,9 +340,10 @@ function sync_object( $args ) { * @return array */ function validate_urls_for_idc_mitigation() { + require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-functions.php'; return array( - 'home' => get_home_url(), - 'siteurl' => get_site_url(), + 'home' => Jetpack_Sync_Functions::home_url(), + 'siteurl' => Jetpack_Sync_Functions::site_url(), ); } diff --git a/class.jetpack.php b/class.jetpack.php index 2544348a5ae1..6a7a497b2d64 100644 --- a/class.jetpack.php +++ b/class.jetpack.php @@ -5797,9 +5797,10 @@ public static function normalize_url_protocol_agnostic( $url ) { * @return array Array of the local urls, wpcom urls, and error code */ public static function get_sync_error_idc_option( $response = array() ) { + require_once JETPACK__PLUGIN_DIR . 'sync/class.jetpack-sync-functions.php'; $local_options = array( - 'home' => get_home_url(), - 'siteurl' => get_site_url(), + 'home' => Jetpack_Sync_Functions::home_url(), + 'siteurl' => Jetpack_Sync_Functions::site_url(), ); $options = array_merge( $local_options, $response ); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c25dbf8962a4..2f27db1b85b2 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -80,6 +80,9 @@ tests/php/test_class.jetpack-jitm.php + + tests/php/3rd-party + diff --git a/sync/class.jetpack-sync-actions.php b/sync/class.jetpack-sync-actions.php index 05a507783448..a01d819abdd0 100644 --- a/sync/class.jetpack-sync-actions.php +++ b/sync/class.jetpack-sync-actions.php @@ -113,6 +113,7 @@ static function set_is_importing_true() { } static function send_data( $data, $codec_name, $sent_timestamp, $queue_id, $checkout_duration, $preprocess_duration ) { + require_once dirname( __FILE__ ) . '/class.jetpack-sync-functions.php'; Jetpack::load_xml_rpc_client(); $query_args = array( @@ -120,8 +121,8 @@ static function send_data( $data, $codec_name, $sent_timestamp, $queue_id, $chec 'codec' => $codec_name, // send the name of the codec used to encode the data 'timestamp' => $sent_timestamp, // send current server time so we can compensate for clock differences 'queue' => $queue_id, // sync or full_sync - 'home' => get_home_url(), // Send home url option to check for Identity Crisis server-side - 'siteurl' => get_site_url(), // Send siteurl option to check for Identity Crisis server-side + 'home' => Jetpack_Sync_Functions::home_url(), // Send home url option to check for Identity Crisis server-side + 'siteurl' => Jetpack_Sync_Functions::site_url(), // Send siteurl option to check for Identity Crisis server-side 'cd' => sprintf( '%.4f', $checkout_duration), // Time spent retrieving queue items from the DB 'pd' => sprintf( '%.4f', $preprocess_duration), // Time spent converting queue items into data to send ); diff --git a/sync/class.jetpack-sync-functions.php b/sync/class.jetpack-sync-functions.php index 596af3573976..f8d76def4e75 100644 --- a/sync/class.jetpack-sync-functions.php +++ b/sync/class.jetpack-sync-functions.php @@ -144,18 +144,53 @@ public static function file_system_write_access() { return false; } + /** + * Helper function that is used when getting home or siteurl values. Decides + * whether to get the raw or filtered value. + * + * @return string + */ + public static function get_raw_or_filtered_url( $url_type ) { + if ( + ! Jetpack_Constants::is_defined( 'JETPACK_SYNC_USE_RAW_URL' ) || + Jetpack_Constants::get_constant( 'JETPACK_SYNC_USE_RAW_URL' ) + ) { + $url = self::get_raw_url( $url_type ); + } else { + $url_function = ( 'home' == $url_type ) + ? 'home_url' + : 'site_url'; + $url = self::normalize_www_in_url( $url_type, $url_function ); + $url = self::get_protocol_normalized_url( $url_function, $url ); + } + + return $url; + } + public static function home_url() { - return self::get_protocol_normalized_url( - 'home_url', - self::normalize_www_in_url( 'home', 'home_url' ) - ); + $url = self::get_raw_or_filtered_url( 'home' ); + + /** + * Allows overriding of the home_url value that is synced back to WordPress.com. + * + * @since 5.2 + * + * @param string $home_url + */ + return esc_url_raw( apply_filters( 'jetpack_sync_home_url', $url ) ); } public static function site_url() { - return self::get_protocol_normalized_url( - 'site_url', - self::normalize_www_in_url( 'siteurl', 'site_url' ) - ); + $url = self::get_raw_or_filtered_url( 'siteurl' ); + + /** + * Allows overriding of the site_url value that is synced back to WordPress.com. + * + * @since 5.2 + * + * @param string $site_url + */ + return esc_url_raw( apply_filters( 'jetpack_sync_site_url', $url ) ); } public static function main_network_site_url() { @@ -184,6 +219,23 @@ public static function get_protocol_normalized_url( $callable, $new_value ) { return set_url_scheme( $new_value, $forced_scheme ); } + public static function get_raw_url( $option_name ) { + $value = null; + $constant = ( 'home' == $option_name ) + ? 'WP_HOME' + : 'WP_SITEURL'; + + if ( Jetpack_Constants::is_defined( $constant ) ) { + $value = Jetpack_Constants::get_constant( $constant ); + } else { + // Let's get the option from the database so that we can bypass filters. This will help + // ensure that we get more uniform values. + $value = Jetpack_Options::get_raw_option( $option_name ); + } + + return $value; + } + public static function normalize_www_in_url( $option, $url_function ) { $url = wp_parse_url( call_user_func( $url_function ) ); $option_url = wp_parse_url( get_option( $option ) ); diff --git a/sync/class.jetpack-sync-module-callables.php b/sync/class.jetpack-sync-module-callables.php index b056521332b0..917345b3f61d 100644 --- a/sync/class.jetpack-sync-module-callables.php +++ b/sync/class.jetpack-sync-module-callables.php @@ -145,7 +145,7 @@ public function maybe_sync_callables() { return; } - $callable_checksums = (array) get_option( self::CALLABLES_CHECKSUM_OPTION_NAME, array() ); + $callable_checksums = (array) Jetpack_Options::get_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, array() ); // only send the callables that have changed foreach ( $callables as $name => $value ) { @@ -166,7 +166,7 @@ public function maybe_sync_callables() { $callable_checksums[ $name ] = $checksum; } } - update_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callable_checksums ); + Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callable_checksums ); } public function expand_callables( $args ) { diff --git a/tests/php/3rd-party/test_class.jetpack-domain-mapping.php b/tests/php/3rd-party/test_class.jetpack-domain-mapping.php new file mode 100644 index 000000000000..3e53b86331ba --- /dev/null +++ b/tests/php/3rd-party/test_class.jetpack-domain-mapping.php @@ -0,0 +1,152 @@ +get_jetpack_sync_filters() as $filter ) { + remove_all_filters( $filter ); + } + } + + function test_domain_mapping_should_not_try_to_hook_when_sunrise_disable() { + $stub = $this->getMockBuilder( 'MockDomainMapping' ) + ->setMethods( array( 'hook_wordpress_mu_domain_mapping', 'hook_wpmu_dev_domain_mapping' ) ) + ->disableOriginalConstructor() + ->getMock(); + + // Both of these methods should not be called + $stub->expects( $this->exactly( 0 ) ) + ->method( 'hook_wordpress_mu_domain_mapping' ) + ->will( $this->returnValue( false ) ); + + $stub->expects( $this->exactly( 0 ) ) + ->method( 'hook_wpmu_dev_domain_mapping' ) + ->will( $this->returnValue( false ) ); + + $stub->attempt_to_hook_domain_mapping_plugins(); + } + + function test_domain_mapping_should_stop_search_after_hooking_once() { + Jetpack_Constants::set_constant( 'SUNRISE', true ); + + $stub = $this->getMockBuilder( 'MockDomainMapping' ) + ->setMethods( array( 'hook_wordpress_mu_domain_mapping', 'hook_wpmu_dev_domain_mapping' ) ) + ->disableOriginalConstructor() + ->getMock(); + + // The first method in the array should be the only one called. + $stub->expects( $this->exactly( 1 ) ) + ->method( 'hook_wordpress_mu_domain_mapping' ) + ->will( $this->returnValue( true ) ); + + $stub->expects( $this->exactly( 0 ) ) + ->method( 'hook_wpmu_dev_domain_mapping' ) + ->will( $this->returnValue( false ) ); + + $stub->attempt_to_hook_domain_mapping_plugins(); + } + + function test_domain_mapping_mu_domain_mapping_not_hooked_when_function_not_exists() { + Jetpack_Constants::set_constant( 'SUNRISE_LOADED', true ); + + $stub = $this->getMockBuilder( 'MockDomainMapping' ) + ->setMethods( array( 'function_exists' ) ) + ->disableOriginalConstructor() + ->getMock(); + + $stub->expects( $this->once() ) + ->method( 'function_exists' ) + ->will( $this->returnValue( false ) ); + + $this->assertFalse( $stub->hook_wordpress_mu_domain_mapping() ); + + foreach ( $this->get_jetpack_sync_filters() as $filter ) { + $this->assertFalse( $this->filter_has_hook( $filter ) ); + } + } + + function test_domain_mapping_mu_domain_mapping_hooked_when_function_exists() { + Jetpack_Constants::set_constant( 'SUNRISE_LOADED', true ); + + $stub = $this->getMockBuilder( 'MockDomainMapping' ) + ->setMethods( array( 'function_exists' ) ) + ->disableOriginalConstructor() + ->getMock(); + + $stub->expects( $this->once() ) + ->method( 'function_exists' ) + ->will( $this->returnValue( true ) ); + + $this->assertTrue( $stub->hook_wordpress_mu_domain_mapping() ); + + foreach ( $this->get_jetpack_sync_filters() as $filter ) { + $this->assertTrue( $this->filter_has_hook( $filter ) ); + } + } + + function test_domain_mapping_wpmu_dev_domain_mapping_not_hooked_when_functions_not_exist() { + $stub = $this->getMockBuilder( 'MockDomainMapping' ) + ->setMethods( array( 'class_exists', 'method_exists' ) ) + ->disableOriginalConstructor() + ->getMock(); + + $stub->expects( $this->once() ) + ->method( 'class_exists' ) + ->will( $this->returnValue( false ) ); + + $stub->expects( $this->exactly( 0 ) ) + ->method( 'method_exists' ) + ->will( $this->returnValue( false ) ); + + $this->assertFalse( $stub->hook_wpmu_dev_domain_mapping() ); + + foreach ( $this->get_jetpack_sync_filters() as $filter ) { + $this->assertFalse( $this->filter_has_hook( $filter ) ); + } + } + + function test_domain_mapping_wpmu_dev_domain_mapping_hooked_when_functions_exist() { + $stub = $this->getMockBuilder( 'MockDomainMapping' ) + ->setMethods( array( 'class_exists', 'method_exists', 'get_domain_mapping_utils_instance' ) ) + ->disableOriginalConstructor() + ->getMock(); + + $stub->expects( $this->once() ) + ->method( 'class_exists' ) + ->will( $this->returnValue( true ) ); + + $stub->expects( $this->once() ) + ->method( 'method_exists' ) + ->will( $this->returnValue( true ) ); + + $stub->expects( $this->once() ) + ->method( 'get_domain_mapping_utils_instance' ) + ->will( $this->returnValue( new stdClass() ) ); + + $this->assertTrue( $stub->hook_wpmu_dev_domain_mapping() ); + + foreach ( $this->get_jetpack_sync_filters() as $filter ) { + $this->assertTrue( $this->filter_has_hook( $filter ) ); + } + } + + function filter_has_hook( $hook ) { + global $wp_filter; + return isset( $wp_filter[ $hook ] ) && ! empty( $wp_filter[ $hook ] ); + } + + function get_jetpack_sync_filters() { + return array( + 'jetpack_sync_home_url', + 'jetpack_sync_site_url', + ); + } +} diff --git a/tests/php/sync/test_class.jetpack-sync-callables.php b/tests/php/sync/test_class.jetpack-sync-callables.php index 95694bdc9f76..55e38362a531 100644 --- a/tests/php/sync/test_class.jetpack-sync-callables.php +++ b/tests/php/sync/test_class.jetpack-sync-callables.php @@ -204,9 +204,8 @@ function test_sync_jetpack_sync_unlock_sync_callable_action_allows_syncing_siteu $this->server_replica_storage->reset(); - // We set the filters here to simulate how setting the WP_HOME and WP_SITEURL constant works. - add_filter( 'option_home', array( $this, 'return_https_site_com_blog' ) ); - add_filter( 'option_siteurl', array( $this, 'return_https_site_com_blog' ) ); + update_option( 'home', $this->return_https_site_com_blog() ); + update_option( 'siteurl', $this->return_https_site_com_blog() ); /** * Used to signal that the callables await transient should be cleared. Clearing the await transient is useful @@ -228,8 +227,9 @@ function test_sync_jetpack_sync_unlock_sync_callable_action_allows_syncing_siteu // Cleanup unset( $_SERVER['HTTPS'] ); - remove_filter( 'option_home', array( $this, 'return_https_site_com_blog' ) ); - remove_filter( 'option_siteurl', array( $this, 'return_https_site_com_blog' ) ); + + update_option( 'home', $original_home_option ); + update_option( 'siteurl', $original_siteurl_option ); } function test_home_site_urls_synced_while_migrate_for_idc_set() { @@ -270,25 +270,6 @@ function test_home_site_urls_synced_while_migrate_for_idc_set() { Jetpack_Options::delete_option( 'migrate_for_idc' ); } - function test_scheme_switching_does_not_cause_sync() { - $this->setSyncClientDefaults(); - delete_transient( Jetpack_Sync_Module_Callables::CALLABLES_AWAIT_TRANSIENT_NAME ); - delete_option( Jetpack_Sync_Module_Callables::CALLABLES_CHECKSUM_OPTION_NAME ); - $_SERVER['HTTPS'] = 'off'; - $home_url = home_url(); - $this->sender->do_sync(); - - $this->assertEquals( $home_url, $this->server_replica_storage->get_callable( 'home_url' ) ); - - // this sets is_ssl() to return true. - $_SERVER['HTTPS'] = 'on'; - delete_transient( Jetpack_Sync_Module_Callables::CALLABLES_AWAIT_TRANSIENT_NAME ); - $this->sender->do_sync(); - - unset( $_SERVER['HTTPS'] ); - $this->assertEquals( $home_url, $this->server_replica_storage->get_callable( 'home_url' ) ); - } - function return_example_com() { return 'http://example.com'; } @@ -517,7 +498,60 @@ function test_sanitize_sync_taxonomies_method() { $sanitized = Jetpack_Sync_Functions::sanitize_taxonomy( (object) array( 'rest_controller_class' => 'WP_REST_Terms_Controller' ) ); $this->assertEquals( $sanitized->rest_controller_class, 'WP_REST_Terms_Controller' ); +} + function test_get_raw_url_by_option_bypasses_filters() { + add_filter( 'option_home', array( $this, '__return_filtered_url' ) ); + $this->assertTrue( 'http://filteredurl.com' !== Jetpack_Sync_Functions::get_raw_url( 'home' ) ); + remove_filter( 'option_home', array( $this, '__return_filtered_url' ) ); + } + + function test_get_raw_url_by_constant_bypasses_filters() { + Jetpack_Constants::set_constant( 'WP_HOME', 'http://constanturl.com' ); + Jetpack_Constants::set_constant( 'WP_SITEURL', 'http://constanturl.com' ); + add_filter( 'option_home', array( $this, '__return_filtered_url' ) ); + add_filter( 'option_siteurl', array( $this, '__return_filtered_url' ) ); + + $this->assertEquals( 'http://constanturl.com', Jetpack_Sync_Functions::get_raw_url( 'home' ) ); + $this->assertEquals( 'http://constanturl.com', Jetpack_Sync_Functions::get_raw_url( 'siteurl' ) ); + + remove_filter( 'option_home', array( $this, '__return_filtered_url' ) ); + remove_filter( 'option_siteurl', array( $this, '__return_filtered_url' ) ); + Jetpack_Constants::clear_constants(); + } + + function test_get_raw_url_returns_with_http_if_is_ssl() { + $home_option = get_option( 'home' ); + + // Test without https first + $this->assertEquals( $home_option, Jetpack_Sync_Functions::get_raw_url( 'home' ) ); + + // Now, with https + $_SERVER['HTTPS'] = 'on'; + $this->assertEquals( + set_url_scheme( $home_option, 'http' ), + Jetpack_Sync_Functions::get_raw_url( 'home' ) + ); + unset( $_SERVER['HTTPS'] ); + } + + function test_user_can_stop_raw_urls() { + add_filter( 'option_home', array( $this, '__return_filtered_url' ) ); + add_filter( 'option_siteurl', array( $this, '__return_filtered_url' ) ); + + // Test with constant first + $this->assertTrue( 'http://filteredurl.com' !== Jetpack_Sync_Functions::home_url() ); + + // Now, without, which should return the filtered URL + Jetpack_Constants::set_constant( 'JETPACK_SYNC_USE_RAW_URL', false ); + $this->assertEquals( $this->__return_filtered_url(), Jetpack_Sync_Functions::home_url() ); + Jetpack_Constants::clear_constants(); + + remove_filter( 'option_home', array( $this, '__return_filtered_url' ) ); + remove_filter( 'option_siteurl', array( $this, '__return_filtered_url' ) ); + } + function __return_filtered_url() { + return 'http://filteredurl.com'; } function add_www_subdomain_to_siteurl( $url ) { diff --git a/tests/php/test_class.jetpack.php b/tests/php/test_class.jetpack.php index bdf5590d69c9..bf364c15d732 100644 --- a/tests/php/test_class.jetpack.php +++ b/tests/php/test_class.jetpack.php @@ -323,14 +323,6 @@ function test_idc_optin_default() { } } - function test_idc_optin_false_when_sunrise() { - Jetpack_Constants::set_constant( 'SUNRISE', true ); - - $this->assertFalse( Jetpack::sync_idc_optin() ); - - Jetpack_Constants::clear_constants(); - } - function test_idc_optin_filter_overrides_development_version() { add_filter( 'jetpack_development_version', '__return_true' ); add_filter( 'jetpack_sync_idc_optin', '__return_false' );