From 6e74a9bf236b8b4bdb84781dc69de2c178a2c847 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 30 Sep 2017 01:14:34 +0000 Subject: [PATCH] Embeds: Cache oEmbeds in an `oembed_cache` custom post type instead of postmeta when there is no global `$post`. Add processing of embeds to rich Text widget. Props swissspidy, westonruter, ocean90, johnbillion. See #40854, #39994, #40935. Fixes #34115. git-svn-id: https://develop.svn.wordpress.org/trunk@41651 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-embed.php | 188 +++++++++++++------ src/wp-includes/post.php | 17 +- tests/phpunit/tests/external-http/oembed.php | 6 +- tests/phpunit/tests/oembed/WpEmbed.php | 95 ++++++++-- 4 files changed, 230 insertions(+), 76 deletions(-) diff --git a/src/wp-includes/class-wp-embed.php b/src/wp-includes/class-wp-embed.php index 3ed15de46e..c40ae2db59 100644 --- a/src/wp-includes/class-wp-embed.php +++ b/src/wp-includes/class-wp-embed.php @@ -30,12 +30,14 @@ class WP_Embed { public function __construct() { // Hack to get the [embed] shortcode to run before wpautop() add_filter( 'the_content', array( $this, 'run_shortcode' ), 8 ); + add_filter( 'widget_text_content', array( $this, 'run_shortcode' ), 8 ); // Shortcode placeholder for strip_shortcodes() add_shortcode( 'embed', '__return_false' ); // Attempts to embed all URLs in a post add_filter( 'the_content', array( $this, 'autoembed' ), 8 ); + add_filter( 'widget_text_content', array( $this, 'autoembed' ), 8 ); // After a post is saved, cache oEmbed items via Ajax add_action( 'edit_form_advanced', array( $this, 'maybe_run_ajax_cache' ) ); @@ -185,88 +187,119 @@ class WP_Embed { } $post_ID = ( ! empty( $post->ID ) ) ? $post->ID : null; - if ( ! empty( $this->post_ID ) ) // Potentially set by WP_Embed::cache_oembed() + + // Potentially set by WP_Embed::cache_oembed(). + if ( ! empty( $this->post_ID ) ) { $post_ID = $this->post_ID; + } + + // Check for a cached result (stored as custom post or in the post meta). + $key_suffix = md5( $url . serialize( $attr ) ); + $cachekey = '_oembed_' . $key_suffix; + $cachekey_time = '_oembed_time_' . $key_suffix; + + /** + * Filters the oEmbed TTL value (time to live). + * + * @since 4.0.0 + * + * @param int $time Time to live (in seconds). + * @param string $url The attempted embed URL. + * @param array $attr An array of shortcode attributes. + * @param int $post_ID Post ID. + */ + $ttl = apply_filters( 'oembed_ttl', DAY_IN_SECONDS, $url, $attr, $post_ID ); + + $cache = ''; + $cache_time = 0; + + $cached_post_id = $this->find_oembed_post_id( $key_suffix ); - // Unknown URL format. Let oEmbed have a go. if ( $post_ID ) { - - // Check for a cached result (stored in the post meta) - $key_suffix = md5( $url . serialize( $attr ) ); - $cachekey = '_oembed_' . $key_suffix; - $cachekey_time = '_oembed_time_' . $key_suffix; - - /** - * Filters the oEmbed TTL value (time to live). - * - * @since 4.0.0 - * - * @param int $time Time to live (in seconds). - * @param string $url The attempted embed URL. - * @param array $attr An array of shortcode attributes. - * @param int $post_ID Post ID. - */ - $ttl = apply_filters( 'oembed_ttl', DAY_IN_SECONDS, $url, $attr, $post_ID ); - $cache = get_post_meta( $post_ID, $cachekey, true ); $cache_time = get_post_meta( $post_ID, $cachekey_time, true ); if ( ! $cache_time ) { $cache_time = 0; } + } elseif ( $cached_post_id ) { + $cached_post = get_post( $cached_post_id ); - $cached_recently = ( time() - $cache_time ) < $ttl; + $cache = $cached_post->post_content; + $cache_time = strtotime( $cached_post->post_modified_gmt ); + } - if ( $this->usecache || $cached_recently ) { - // Failures are cached. Serve one if we're using the cache. - if ( '{{unknown}}' === $cache ) - return $this->maybe_make_link( $url ); + $cached_recently = ( time() - $cache_time ) < $ttl; - if ( ! empty( $cache ) ) { - /** - * Filters the cached oEmbed HTML. - * - * @since 2.9.0 - * - * @see WP_Embed::shortcode() - * - * @param mixed $cache The cached HTML result, stored in post meta. - * @param string $url The attempted embed URL. - * @param array $attr An array of shortcode attributes. - * @param int $post_ID Post ID. - */ - return apply_filters( 'embed_oembed_html', $cache, $url, $attr, $post_ID ); - } + if ( $this->usecache || $cached_recently ) { + // Failures are cached. Serve one if we're using the cache. + if ( '{{unknown}}' === $cache ) { + return $this->maybe_make_link( $url ); } - /** - * Filters whether to inspect the given URL for discoverable link tags. - * - * @since 2.9.0 - * @since 4.4.0 The default value changed to true. - * - * @see WP_oEmbed::discover() - * - * @param bool $enable Whether to enable `` tag discovery. Default true. - */ - $attr['discover'] = ( apply_filters( 'embed_oembed_discover', true ) ); + if ( ! empty( $cache ) ) { + /** + * Filters the cached oEmbed HTML. + * + * @since 2.9.0 + * + * @see WP_Embed::shortcode() + * + * @param mixed $cache The cached HTML result, stored in post meta. + * @param string $url The attempted embed URL. + * @param array $attr An array of shortcode attributes. + * @param int $post_ID Post ID. + */ + return apply_filters( 'embed_oembed_html', $cache, $url, $attr, $post_ID ); + } + } - // Use oEmbed to get the HTML - $html = wp_oembed_get( $url, $attr ); + /** + * Filters whether to inspect the given URL for discoverable link tags. + * + * @since 2.9.0 + * @since 4.4.0 The default value changed to true. + * + * @see WP_oEmbed::discover() + * + * @param bool $enable Whether to enable `` tag discovery. Default true. + */ + $attr['discover'] = apply_filters( 'embed_oembed_discover', true ); - // Maybe cache the result + // Use oEmbed to get the HTML. + $html = wp_oembed_get( $url, $attr ); + + if ( $post_ID ) { if ( $html ) { update_post_meta( $post_ID, $cachekey, $html ); update_post_meta( $post_ID, $cachekey_time, time() ); } elseif ( ! $cache ) { update_post_meta( $post_ID, $cachekey, '{{unknown}}' ); } + } else { + $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ); - // If there was a result, return it - if ( $html ) { - /** This filter is documented in wp-includes/class-wp-embed.php */ - return apply_filters( 'embed_oembed_html', $html, $url, $attr, $post_ID ); + if ( $has_kses ) { + // Prevent KSES from corrupting JSON in post_content. + kses_remove_filters(); } + + wp_insert_post( wp_slash( array( + 'post_name' => $key_suffix, + 'post_content' => $html ? $html : '{{unknown}}', + 'post_status' => 'publish', + 'post_type' => 'oembed_cache', + ) ) ); + + if ( $has_kses ) { + kses_init_filters(); + } + } + + // If there was a result, return it. + if ( $html ) { + /** This filter is documented in wp-includes/class-wp-embed.php */ + return apply_filters( 'embed_oembed_html', $html, $url, $attr, $post_ID ); } // Still unknown @@ -382,4 +415,43 @@ class WP_Embed { */ return apply_filters( 'embed_maybe_make_link', $output, $url ); } + + /** + * Find the oEmbed cache post ID for a given cache key. + * + * @since 4.9.0 + * + * @param string $cache_key oEmbed cache key. + * @return int|null Post ID on success, null on failure. + */ + public function find_oembed_post_id( $cache_key ) { + $cache_group = 'oembed_cache_post'; + $oembed_post_id = wp_cache_get( $cache_key, $cache_group ); + + if ( $oembed_post_id && 'oembed_cache' === get_post_type( $oembed_post_id ) ) { + return $oembed_post_id; + } + + $oembed_post_query = new WP_Query( array( + 'post_type' => 'oembed_cache', + 'post_status' => 'publish', + 'name' => $cache_key, + 'posts_per_page' => 1, + 'no_found_rows' => true, + 'cache_results' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'lazy_load_term_meta' => false, + ) ); + + if ( ! empty( $oembed_post_query->posts ) ) { + // Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed. + $oembed_post_id = $oembed_post_query->posts[0]->ID; + wp_cache_set( $cache_key, $oembed_post_id, $cache_group ); + + return $oembed_post_id; + } + + return null; + } } diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 0ed4a3efb2..e6f1340293 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -195,6 +195,21 @@ function create_initial_post_types() { ), ) ); + register_post_type( 'oembed_cache', array( + 'labels' => array( + 'name' => __( 'oEmbed Responses' ), + 'singular_name' => __( 'oEmbed Response' ), + ), + 'public' => false, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'delete_with_user' => false, + 'can_export' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'supports' => array(), + ) ); + register_post_status( 'publish', array( 'label' => _x( 'Published', 'post status' ), 'public' => true, @@ -3003,7 +3018,7 @@ function wp_get_recent_posts( $args = array(), $output = ARRAY_A ) { * @type string $guid Global Unique ID for referencing the post. Default empty. * @type array $post_category Array of category names, slugs, or IDs. * Defaults to value of the 'default_category' option. - * @type array $tags_input Array of tag names, slugs, or IDs. Default empty. + * @type array $tags_input Array of tag names, slugs, or IDs. Default empty. * @type array $tax_input Array of taxonomy terms keyed by their taxonomy name. Default empty. * @type array $meta_input Array of post meta values keyed by their post meta key. Default empty. * } diff --git a/tests/phpunit/tests/external-http/oembed.php b/tests/phpunit/tests/external-http/oembed.php index 3f227393d4..1e4648d732 100644 --- a/tests/phpunit/tests/external-http/oembed.php +++ b/tests/phpunit/tests/external-http/oembed.php @@ -35,12 +35,12 @@ class Tests_External_HTTP_OEmbed extends WP_UnitTestCase { function test_youtube_embed_url() { global $wp_embed; $out = $wp_embed->autoembed( 'https://www.youtube.com/embed/QcIy9NiNbmo' ); - $this->assertContains( 'https://youtube.com/watch?v=QcIy9NiNbmo', $out ); + $this->assertContains( 'https://www.youtube.com/embed/QcIy9NiNbmo?feature=oembed', $out ); } function test_youtube_v_url() { global $wp_embed; $out = $wp_embed->autoembed( 'https://www.youtube.com/v/QcIy9NiNbmo' ); - $this->assertContains( 'https://youtube.com/watch?v=QcIy9NiNbmo', $out ); + $this->assertContains( 'https://www.youtube.com/embed/QcIy9NiNbmo?feature=oembed', $out ); } -} \ No newline at end of file +} diff --git a/tests/phpunit/tests/oembed/WpEmbed.php b/tests/phpunit/tests/oembed/WpEmbed.php index 9a46e1f3af..877a79e4b1 100644 --- a/tests/phpunit/tests/oembed/WpEmbed.php +++ b/tests/phpunit/tests/oembed/WpEmbed.php @@ -161,48 +161,115 @@ class Tests_WP_Embed extends WP_UnitTestCase { $this->assertNotEmpty( get_post_meta( $post_id, $cachekey_time, true ) ); } - public function test_shortcode_should_cache_data_in_post_meta_for_known_post() { - $GLOBALS['post'] = $this->factory()->post->create_and_get(); + public function test_shortcode_should_get_cached_data_from_post_meta_for_known_post() { + global $post; + + $post = $this->factory()->post->create_and_get(); $url = 'https://example.com/'; $expected = 'Embedded content'; $key_suffix = md5( $url . serialize( wp_embed_defaults( $url ) ) ); $cachekey = '_oembed_' . $key_suffix; - $cachekey_time = '_oembed_time_' . $key_suffix; + + add_post_meta( $post->ID, $cachekey, $expected ); add_filter( 'pre_oembed_result', array( $this, '_pre_oembed_result_callback' ) ); $actual = $this->wp_embed->shortcode( array(), $url ); remove_filter( 'pre_oembed_result', array( $this, '_pre_oembed_result_callback' ) ); - $this->assertEquals( $expected, $actual ); + $actual_2 = $this->wp_embed->shortcode( array(), $url ); - $this->assertEquals( $expected, get_post_meta( $GLOBALS['post']->ID, $cachekey, true ) ); - $this->assertNotEmpty( get_post_meta( $GLOBALS['post']->ID, $cachekey_time, true ) ); + $cached = get_post_meta( $post->ID, $cachekey, true ); + + // Cleanup. + unset( $post ); - // Result should be cached. - $actual = $this->wp_embed->shortcode( array(), $url ); $this->assertEquals( $expected, $actual ); + $this->assertEquals( $expected, $actual_2 ); + $this->assertEquals( $expected, $cached ); } - public function test_shortcode_should_cache_failure_in_post_meta_for_known_post() { - $GLOBALS['post'] = $this->factory()->post->create_and_get(); + public function test_shortcode_should_get_cached_failure_from_post_meta_for_known_post() { + global $post; + + $post = $this->factory()->post->create_and_get(); $url = 'https://example.com/'; $expected = '' . esc_html( $url ) . ''; $key_suffix = md5( $url . serialize( wp_embed_defaults( $url ) ) ); $cachekey = '_oembed_' . $key_suffix; $cachekey_time = '_oembed_time_' . $key_suffix; + add_post_meta( $post->ID, $cachekey, '{{unknown}}' ); + add_post_meta( $post->ID, $cachekey_time, 0 ); + add_filter( 'pre_oembed_result', '__return_empty_string' ); $actual = $this->wp_embed->shortcode( array(), $url ); remove_filter( 'pre_oembed_result', '__return_empty_string' ); - $this->assertEquals( $expected, $actual ); + // Result should be cached. + $actual_2 = $this->wp_embed->shortcode( array(), $url ); - $this->assertEquals( '{{unknown}}', get_post_meta( $GLOBALS['post']->ID, $cachekey, true ) ); - $this->assertEmpty( get_post_meta( $GLOBALS['post']->ID, $cachekey_time, true ) ); + $cached = get_post_meta( $post->ID, $cachekey, true ); + $cached_time = get_post_meta( $post->ID, $cachekey_time, true ); + + // Cleanup. + unset( $post ); + + $this->assertEquals( $expected, $actual ); + $this->assertEquals( '{{unknown}}', $cached ); + $this->assertEmpty( $cached_time ); + $this->assertEquals( $expected, $actual_2 ); + } + + /** + * @ticket 34115 + */ + public function test_shortcode_should_cache_data_in_custom_post() { + $url = 'https://example.com/'; + $expected = 'Embedded content'; + $key_suffix = md5( $url . serialize( wp_embed_defaults( $url ) ) ); + + add_filter( 'pre_oembed_result', array( $this, '_pre_oembed_result_callback' ) ); + $actual = $this->wp_embed->shortcode( array(), $url ); + remove_filter( 'pre_oembed_result', array( $this, '_pre_oembed_result_callback' ) ); + + $oembed_post_id = $this->wp_embed->find_oembed_post_id( $key_suffix ); + $post_content = get_post( $oembed_post_id )->post_content; // Result should be cached. - $actual = $this->wp_embed->shortcode( array(), $url ); + $actual_2 = $this->wp_embed->shortcode( array(), $url ); + + wp_delete_post( $oembed_post_id ); + + $this->assertNotNull( $oembed_post_id ); + $this->assertEquals( $expected, $post_content ); $this->assertEquals( $expected, $actual ); + $this->assertEquals( $expected, $actual_2 ); + } + + /** + * @ticket 34115 + */ + public function test_shortcode_should_cache_failure_in_custom_post() { + $url = 'https://example.com/'; + $expected = '' . esc_html( $url ) . ''; + $key_suffix = md5( $url . serialize( wp_embed_defaults( $url ) ) ); + + add_filter( 'pre_oembed_result', '__return_empty_string' ); + $actual = $this->wp_embed->shortcode( array(), $url ); + remove_filter( 'pre_oembed_result', '__return_empty_string' ); + + $oembed_post_id = $this->wp_embed->find_oembed_post_id( $key_suffix ); + $post_content = get_post( $oembed_post_id )->post_content; + + // Result should be cached. + $actual_2 = $this->wp_embed->shortcode( array(), $url ); + + wp_delete_post( $oembed_post_id ); + + $this->assertEquals( $expected, $actual ); + $this->assertEquals( $expected, $actual_2 ); + $this->assertNotNull( $oembed_post_id ); + $this->assertEquals( '{{unknown}}', $post_content ); } public function test_shortcode_should_get_url_from_src_attribute() {