diff --git src/wp-includes/class-wp-oembed-controller.php src/wp-includes/class-wp-oembed-controller.php index 7ee7950b07..9efeb54ed0 100644 --- src/wp-includes/class-wp-oembed-controller.php +++ src/wp-includes/class-wp-oembed-controller.php @@ -173,12 +173,22 @@ final class WP_oEmbed_Controller { $args['height'] = $args['maxheight']; } + // Short-circuit process for URLs belonging to the current site. + $data = get_oembed_response_data_for_url( $url, $args ); + + if ( $data ) { + return $data; + } + $data = _wp_oembed_get_object()->get_data( $url, $args ); if ( false === $data ) { return new WP_Error( 'oembed_invalid_url', get_status_header_desc( 404 ), array( 'status' => 404 ) ); } + /** This filter is documented in wp-includes/class-oembed.php */ + $data->html = apply_filters( 'oembed_result', _wp_oembed_get_object()->data2html( (object) $data, $url ), $url, $args ); + /** * Filters the oEmbed TTL value (time to live). * diff --git src/wp-includes/embed.php src/wp-includes/embed.php index 176988057b..efd991104a 100644 --- src/wp-includes/embed.php +++ src/wp-includes/embed.php @@ -555,6 +555,71 @@ function get_oembed_response_data( $post, $width ) { return apply_filters( 'oembed_response_data', $data, $post, $width, $height ); } + +/** + * Retrieves the oEmbed response data for a given URL. + * + * @since 5.0.0 + * + * @param string $url The URL that should be inspected for discovery `` tags. + * @param array $args oEmbed remote get arguments. + * @return object|false oEmbed response data if the URL does belong to the current site. False otherwise. + */ +function get_oembed_response_data_for_url( $url, $args ) { + $switched_blog = false; + + if ( is_multisite() ) { + $url_parts = wp_parse_args( wp_parse_url( $url ), array( + 'host' => '', + 'path' => '/', + ) ); + + $qv = array( 'domain' => $url_parts['host'], 'path' => '/' ); + + // In case of subdirectory configs, set the path. + if ( ! is_subdomain_install() ) { + $path = explode( '/', ltrim( $url_parts['path'], '/' ) ); + $path = reset( $path ); + + if ( $path ) { + $qv['path'] = get_network()->path . $path . '/'; + } + } + + $sites = get_sites( $qv ); + $site = reset( $sites ); + + if ( $site && (int) $site->blog_id !== get_current_blog_id() ) { + switch_to_blog( $site->blog_id ); + $switched_blog = true; + } + } + + $post_id = url_to_postid( $url ); + + /** This filter is documented in wp-includes/class-wp-oembed-controller.php */ + $post_id = apply_filters( 'oembed_request_post_id', $post_id, $url ); + + if ( ! $post_id ) { + if ( $switched_blog ) { + restore_current_blog(); + } + + return false; + } + + $width = isset( $args['width'] ) ? $args['width'] : 0; + + $data = get_oembed_response_data( $post_id, $width ); + + if ( $switched_blog ) { + restore_current_blog(); + } + + return $data ? (object) $data : false; +} + + /** * Filters the oEmbed response data to return an iframe embed code. * @@ -1071,60 +1136,11 @@ function the_embed_site_title() { * Null if the URL does not belong to the current site. */ function wp_filter_pre_oembed_result( $result, $url, $args ) { - $switched_blog = false; - - if ( is_multisite() ) { - $url_parts = wp_parse_args( wp_parse_url( $url ), array( - 'host' => '', - 'path' => '/', - ) ); - - $qv = array( 'domain' => $url_parts['host'], 'path' => '/' ); - - // In case of subdirectory configs, set the path. - if ( ! is_subdomain_install() ) { - $path = explode( '/', ltrim( $url_parts['path'], '/' ) ); - $path = reset( $path ); - - if ( $path ) { - $qv['path'] = get_network()->path . $path . '/'; - } - } - - $sites = get_sites( $qv ); - $site = reset( $sites ); - - if ( $site && (int) $site->blog_id !== get_current_blog_id() ) { - switch_to_blog( $site->blog_id ); - $switched_blog = true; - } - } - - $post_id = url_to_postid( $url ); - - /** This filter is documented in wp-includes/class-wp-oembed-controller.php */ - $post_id = apply_filters( 'oembed_request_post_id', $post_id, $url ); - - if ( ! $post_id ) { - if ( $switched_blog ) { - restore_current_blog(); - } + $data = get_oembed_response_data_for_url( $url, $args ); - return $result; + if ( $data ) { + return _wp_oembed_get_object()->data2html( $data, $url ); } - $width = isset( $args['width'] ) ? $args['width'] : 0; - - $data = get_oembed_response_data( $post_id, $width ); - $data = _wp_oembed_get_object()->data2html( (object) $data, $url ); - - if ( $switched_blog ) { - restore_current_blog(); - } - - if ( ! $data ) { - return $result; - } - - return $data; + return $result; } diff --git tests/phpunit/tests/oembed/controller.php tests/phpunit/tests/oembed/controller.php index 9f9aa6dd46..2d6afaf76c 100644 --- tests/phpunit/tests/oembed/controller.php +++ tests/phpunit/tests/oembed/controller.php @@ -14,6 +14,7 @@ class Test_oEmbed_Controller extends WP_UnitTestCase { protected static $subscriber; const YOUTUBE_VIDEO_ID = 'OQSNhk5ICTI'; const INVALID_OEMBED_URL = 'https://www.notreallyanoembedprovider.com/watch?v=awesome-cat-video'; + const UNTRUSTED_PROVIDER_URL = 'https://www.untrustedprovider.com'; public static function wpSetUpBeforeClass( $factory ) { self::$subscriber = $factory->user->create( array( @@ -71,7 +72,8 @@ class Test_oEmbed_Controller extends WP_UnitTestCase { unset( $preempt, $r ); $parsed_url = wp_parse_url( $url ); - parse_str( $parsed_url['query'], $query_params ); + $query = isset( $parsed_url['query'] ) ? $parsed_url['query'] : ''; + parse_str( $query, $query_params ); $this->request_count += 1; // Mock request to YouTube Embed. @@ -80,7 +82,7 @@ class Test_oEmbed_Controller extends WP_UnitTestCase { 'response' => array( 'code' => 200, ), - 'body' => wp_json_encode( + 'body' => wp_json_encode( array( 'version' => '1.0', 'type' => 'video', @@ -90,20 +92,48 @@ class Test_oEmbed_Controller extends WP_UnitTestCase { 'width' => $query_params['maxwidth'], 'thumbnail_height' => $query_params['maxheight'], 'height' => $query_params['maxheight'], - 'html' => '', + 'html' => 'Unfiltered', 'author_name' => 'Yosemitebear62', 'thumbnail_url' => 'https://i.ytimg.com/vi/' . self::YOUTUBE_VIDEO_ID . '/hqdefault.jpg', 'title' => 'Yosemitebear Mountain Double Rainbow 1-8-10', ) ), ); - } else { + } + + if ( $url === self::UNTRUSTED_PROVIDER_URL ) { return array( 'response' => array( - 'code' => 404, + 'code' => 200, ), + 'body' => '', ); } + + if ( ! empty( $query_params['url'] ) && false !== strpos( $query_params['url'], self::UNTRUSTED_PROVIDER_URL ) ) { + return array( + 'response' => array( + 'code' => 200, + ), + 'body' => wp_json_encode( + array( + 'version' => '1.0', + 'type' => 'rich', + 'provider_name' => 'Untrusted', + 'provider_url' => self::UNTRUSTED_PROVIDER_URL, + 'html' => 'FilteredUnfiltered', + 'author_name' => 'Untrusted Embed Author', + 'title' => 'Untrusted Embed', + ) + ), + ); + } + + return array( + 'response' => array( + 'code' => 404, + ), + ); } function test_wp_oembed_ensure_format() { @@ -510,7 +540,7 @@ class Test_oEmbed_Controller extends WP_UnitTestCase { $data = $response->get_data(); $this->assertNotEmpty( $data ); - $this->assertTrue( is_object( $data ) ); + $this->assertInternalType( 'object', $data ); $this->assertEquals( 'YouTube', $data->provider_name ); $this->assertEquals( 'https://i.ytimg.com/vi/' . self::YOUTUBE_VIDEO_ID . '/hqdefault.jpg', $data->thumbnail_url ); $this->assertEquals( $data->width, $request['maxwidth'] ); @@ -552,4 +582,137 @@ class Test_oEmbed_Controller extends WP_UnitTestCase { $data = $response->get_data(); $this->assertEquals( $data['code'], 'rest_invalid_param' ); } + + /** + * @ticket 45142 + */ + function test_proxy_with_internal_url() { + wp_set_current_user( self::$editor ); + + $user = self::factory()->user->create_and_get( array( + 'display_name' => 'John Doe', + ) ); + $post = self::factory()->post->create_and_get( array( + 'post_author' => $user->ID, + 'post_title' => 'Hello World', + ) ); + + $request = new WP_REST_Request( 'GET', '/oembed/1.0/proxy' ); + $request->set_param( 'url', get_permalink( $post->ID ) ); + $request->set_param( 'maxwidth', 400 ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $data = (array) $data; + + $this->assertNotEmpty( $data ); + + $this->assertArrayHasKey( 'version', $data ); + $this->assertArrayHasKey( 'provider_name', $data ); + $this->assertArrayHasKey( 'provider_url', $data ); + $this->assertArrayHasKey( 'author_name', $data ); + $this->assertArrayHasKey( 'author_url', $data ); + $this->assertArrayHasKey( 'title', $data ); + $this->assertArrayHasKey( 'type', $data ); + $this->assertArrayHasKey( 'width', $data ); + + $this->assertEquals( '1.0', $data['version'] ); + $this->assertEquals( get_bloginfo( 'name' ), $data['provider_name'] ); + $this->assertEquals( get_home_url(), $data['provider_url'] ); + $this->assertEquals( $user->display_name, $data['author_name'] ); + $this->assertEquals( get_author_posts_url( $user->ID, $user->user_nicename ), $data['author_url'] ); + $this->assertEquals( $post->post_title, $data['title'] ); + $this->assertEquals( 'rich', $data['type'] ); + $this->assertTrue( $data['width'] <= $request['maxwidth'] ); + } + + /** + * @ticket 45142 + */ + function test_proxy_with_static_front_page_url() { + wp_set_current_user( self::$editor ); + + $post = self::factory()->post->create_and_get( array( + 'post_title' => 'Front page', + 'post_type' => 'page', + 'post_author' => 0, + ) ); + + update_option( 'show_on_front', 'page' ); + update_option( 'page_on_front', $post->ID ); + + $request = new WP_REST_Request( 'GET', '/oembed/1.0/proxy' ); + $request->set_param( 'url', home_url() ); + $request->set_param( 'maxwidth', 400 ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertInternalType( 'object', $data ); + + $data = (array) $data; + + $this->assertNotEmpty( $data ); + + $this->assertArrayHasKey( 'version', $data ); + $this->assertArrayHasKey( 'provider_name', $data ); + $this->assertArrayHasKey( 'provider_url', $data ); + $this->assertArrayHasKey( 'author_name', $data ); + $this->assertArrayHasKey( 'author_url', $data ); + $this->assertArrayHasKey( 'title', $data ); + $this->assertArrayHasKey( 'type', $data ); + $this->assertArrayHasKey( 'width', $data ); + + $this->assertEquals( '1.0', $data['version'] ); + $this->assertEquals( get_bloginfo( 'name' ), $data['provider_name'] ); + $this->assertEquals( get_home_url(), $data['provider_url'] ); + $this->assertEquals( get_bloginfo( 'name' ), $data['author_name'] ); + $this->assertEquals( get_home_url(), $data['author_url'] ); + $this->assertEquals( $post->post_title, $data['title'] ); + $this->assertEquals( 'rich', $data['type'] ); + $this->assertTrue( $data['width'] <= $request['maxwidth'] ); + + update_option( 'show_on_front', 'posts' ); + } + + /** + * @ticket 45142 + */ + public function test_proxy_filters_result_of_untrusted_oembed_provider() { + wp_set_current_user( self::$editor ); + + $request = new WP_REST_Request( 'GET', '/oembed/1.0/proxy' ); + $request->set_param( 'url', self::UNTRUSTED_PROVIDER_URL ); + $request->set_param( 'maxwidth', 456 ); + $request->set_param( 'maxheight', 789 ); + $request->set_param( '_wpnonce', wp_create_nonce( 'wp_rest' ) ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertInternalType( 'object', $data ); + + $this->markTestIncomplete( 'Need to test that the resulting HTML is passed through the oembed_dataparse filter' ); + } + + /** + * @ticket 45142 + */ + public function test_proxy_does_not_filter_result_of_trusted_oembed_provider() { + wp_set_current_user( self::$editor ); + + $request = new WP_REST_Request( 'GET', '/oembed/1.0/proxy' ); + $request->set_param( 'url', 'https://www.youtube.com/watch?v=' . self::YOUTUBE_VIDEO_ID ); + $request->set_param( 'maxwidth', 456 ); + $request->set_param( 'maxheight', 789 ); + $request->set_param( '_wpnonce', wp_create_nonce( 'wp_rest' ) ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertInternalType( 'object', $data ); + + $this->assertStringStartsWith( 'Unfiltered', $data->html ); + } }