From 48e280db32ca49eb5dfbc253d9af1c265c2d857e Mon Sep 17 00:00:00 2001 From: Boone Gorges Date: Thu, 19 Sep 2019 01:48:54 +0000 Subject: [PATCH] Improve `do_enclose()` logic on post publish. Removing the direct SQL query in `do_all_pings()` improves filterability. As part of this change, the signature of `do_enclose()` is changed so that a null `$content` parameter can be passed, with the `$content` then inferred from the `$post` passed in the second parameter. In addition, the second parameter was modified so that a post ID or a `WP_Post` object can be provided. These changes make it possible to trigger enclosure checks with a post ID alone (as in `do_all_pings()`) and also brings the function signature in line with `do_trackbacks()` and `pingback()`. Props dshanske, spacedmonkey, janw.oostendorp, mrmadhat, birgire. See #36824. git-svn-id: https://develop.svn.wordpress.org/trunk@46175 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/comment.php | 18 +- src/wp-includes/functions.php | 28 +- tests/phpunit/tests/functions/doEnclose.php | 305 ++++++++++++++++++++ 3 files changed, 339 insertions(+), 12 deletions(-) create mode 100644 tests/phpunit/tests/functions/doEnclose.php diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 4b192ea02e..304536b9d2 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -2639,10 +2639,20 @@ function do_all_pings() { pingback( $ping->post_content, $ping->ID ); } - // Do Enclosures - while ( $enclosure = $wpdb->get_row( "SELECT ID, post_content, meta_id FROM {$wpdb->posts}, {$wpdb->postmeta} WHERE {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id AND {$wpdb->postmeta}.meta_key = '_encloseme' LIMIT 1" ) ) { - delete_metadata_by_mid( 'post', $enclosure->meta_id ); - do_enclose( $enclosure->post_content, $enclosure->ID ); + // Do enclosures. + $enclosures = get_posts( + array( + 'post_type' => get_post_types(), + 'suppress_filters' => false, + 'nopaging' => true, + 'meta_key' => '_encloseme', + 'fields' => 'ids', + ) + ); + + foreach ( $enclosure as $enclosure ) { + delete_post_meta( $enclosure, '_encloseme' ); + do_enclose( null, $enclosure->ID ); } // Do Trackbacks diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 035347e1ed..97d8941478 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -799,27 +799,39 @@ function wp_extract_urls( $content ) { * pingbacks and trackbacks. * * @since 1.5.0 + * @since 5.3.0 The `$content` parameter was made optional, and the `$post` parameter was + * updated to accept a post ID or a WP_Post object. * * @global wpdb $wpdb WordPress database abstraction object. * - * @param string $content Post Content. - * @param int $post_ID Post ID. + * @param string $content Post content. If `null`, the `post_content` field from `$post` is used. + * @param int|WP_Post $post Post ID or post object. + * @return null|bool Returns false if post is not found. */ -function do_enclose( $content, $post_ID ) { +function do_enclose( $content = null, $post ) { global $wpdb; //TODO: Tidy this ghetto code up and make the debug code optional include_once( ABSPATH . WPINC . '/class-IXR.php' ); + $post = get_post( $post ); + if ( ! $post ) { + return false; + } + + if ( null === $content ) { + $content = $post->post_content; + } + $post_links = array(); - $pung = get_enclosed( $post_ID ); + $pung = get_enclosed( $post->ID ); $post_links_temp = wp_extract_urls( $content ); foreach ( $pung as $link_test ) { if ( ! in_array( $link_test, $post_links_temp ) ) { // link no longer in post - $mids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE post_id = %d AND meta_key = 'enclosure' AND meta_value LIKE %s", $post_ID, $wpdb->esc_like( $link_test ) . '%' ) ); + $mids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE post_id = %d AND meta_key = 'enclosure' AND meta_value LIKE %s", $post->ID, $wpdb->esc_like( $link_test ) . '%' ) ); foreach ( $mids as $mid ) { delete_metadata_by_mid( 'post', $mid ); } @@ -851,10 +863,10 @@ function do_enclose( $content, $post_ID ) { * @param array $post_links An array of enclosure links. * @param int $post_ID Post ID. */ - $post_links = apply_filters( 'enclosure_links', $post_links, $post_ID ); + $post_links = apply_filters( 'enclosure_links', $post_links, $post->ID ); foreach ( (array) $post_links as $url ) { - if ( $url != '' && ! $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE post_id = %d AND meta_key = 'enclosure' AND meta_value LIKE %s", $post_ID, $wpdb->esc_like( $url ) . '%' ) ) ) { + if ( $url != '' && ! $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE post_id = %d AND meta_key = 'enclosure' AND meta_value LIKE %s", $post->ID, $wpdb->esc_like( $url ) . '%' ) ) ) { $headers = wp_get_http_headers( $url ); if ( $headers ) { @@ -878,7 +890,7 @@ function do_enclose( $content, $post_ID ) { } if ( in_array( substr( $type, 0, strpos( $type, '/' ) ), $allowed_types ) ) { - add_post_meta( $post_ID, 'enclosure', "$url\n$len\n$mime\n" ); + add_post_meta( $post->ID, 'enclosure', "$url\n$len\n$mime\n" ); } } } diff --git a/tests/phpunit/tests/functions/doEnclose.php b/tests/phpunit/tests/functions/doEnclose.php new file mode 100644 index 0000000000..196fe0c30a --- /dev/null +++ b/tests/phpunit/tests/functions/doEnclose.php @@ -0,0 +1,305 @@ +post->create(); + + do_enclose( $content, $post_id ); + + $actual = $this->get_enclosed_by_post_id( $post_id ); + $this->assertSame( $expected, $actual ); + } + + /** + * Test the function with an implicit content input. + * + * @since 5.3.0 + * + * @dataProvider data_test_do_enclose + */ + public function test_function_with_implicit_content_input( $content, $expected ) { + $post_id = self::factory()->post->create( + array( + 'post_content' => $content, + ) + ); + + do_enclose( null, $post_id ); + + $actual = $this->get_enclosed_by_post_id( $post_id ); + $this->assertSame( $expected, $actual ); + } + + /** + * Dataprovider for `test_function_with_explicit_content_input()` + * and `test_function_with_implicit_content_input()`. + * + * @since 5.3.0 + * + * @return array { + * @type array { + * @type string Post content. + * @type string Expected values. + * } + * } + */ + public function data_test_do_enclose() { + return array( + 'null' => array( + 'content' => null, + 'expected' => '', + ), + 'empty' => array( + 'content' => '', + 'expected' => '', + ), + 'single-bare-movie' => array( + 'content' => 'movie.mp4', + 'expected' => '', + ), + 'single-bare-audio' => array( + 'content' => 'audio.ogg', + 'expected' => '', + ), + 'single-relative-movie' => array( + 'content' => '/movie.mp4', + 'expected' => "/movie.mp4\n123\nvideo/mp4\n", + ), + 'single-relative-audio' => array( + 'content' => '/audio.ogg', + 'expected' => "/audio.ogg\n321\naudio/ogg\n", + ), + 'single-unknown' => array( + 'content' => 'https://example.com/wp-content/uploads/2018/06/file.unknown', + 'expected' => '', + ), + 'single-movie' => array( + 'content' => 'https://example.com/wp-content/uploads/2018/06/movie.mp4', + 'expected' => "https://example.com/wp-content/uploads/2018/06/movie.mp4\n123\nvideo/mp4\n", + ), + 'single-audio' => array( + 'content' => 'https://example.com/wp-content/uploads/2018/06/audio.ogg', + 'expected' => "https://example.com/wp-content/uploads/2018/06/audio.ogg\n321\naudio/ogg\n", + ), + 'single-movie-query' => array( + 'content' => 'https://example.com/wp-content/uploads/2018/06/movie.mp4?test=1', + 'expected' => "https://example.com/wp-content/uploads/2018/06/movie.mp4?test=1\n123\nvideo/mp4\n", + ), + 'multi' => array( + 'content' => "https://example.com/wp-content/uploads/2018/06/audio.ogg\n" . + 'https://example.com/wp-content/uploads/2018/06/movie.mp4', + 'expected' => "https://example.com/wp-content/uploads/2018/06/audio.ogg\n321\naudio/ogg\n" . + "https://example.com/wp-content/uploads/2018/06/movie.mp4\n123\nvideo/mp4\n", + ), + ); + } + + /** + * The function should return false when the post ID input is invalid. + * + * @since 5.3.0 + */ + public function test_function_should_return_false_when_invalid_post_id() { + $post_id = null; + $result = do_enclose( null, $post_id ); + $this->assertFalse( $result ); + } + + /** + * The function should delete an enclosed link when it's no longer in the post content. + * + * @since 5.3.0 + */ + public function test_function_should_delete_enclosed_link_when_no_longer_in_post_content() { + $data = $this->data_test_do_enclose(); + + // Create a post with a single movie link. + $post_id = self::factory()->post->create( + array( + 'post_content' => $data['single-movie']['content'], + ) + ); + + do_enclose( null, $post_id ); + + $actual = $this->get_enclosed_by_post_id( $post_id ); + $this->assertSame( $data['single-movie']['expected'], $actual ); + + // Replace the movie link with an audio link. + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => $data['single-audio']['content'], + ) + ); + + do_enclose( null, $post_id ); + + $actual = $this->get_enclosed_by_post_id( $post_id ); + $this->assertSame( $data['single-audio']['expected'], $actual ); + } + + /** + * The function should support a post object input. + * + * @since 5.3.0 + */ + public function test_function_should_support_post_object_input() { + $data = $this->data_test_do_enclose(); + + $post_object = self::factory()->post->create_and_get( + array( + 'post_content' => $data['multi']['content'], + ) + ); + + do_enclose( null, $post_object ); + + $actual = $this->get_enclosed_by_post_id( $post_object->ID ); + $this->assertSame( $data['multi']['expected'], $actual ); + } + + /** + * The enclosure links should be filterable with the `enclosure_links` filter. + * + * @since 5.3.0 + */ + public function test_function_enclosure_links_should_be_filterable() { + $data = $this->data_test_do_enclose(); + + $post_id = self::factory()->post->create( + array( + 'post_content' => $data['multi']['content'], + ) + ); + + add_filter( 'enclosure_links', array( $this, 'filter_enclosure_links' ), 10, 2 ); + do_enclose( null, $post_id ); + remove_filter( 'enclosure_links', array( $this, 'filter_enclosure_links' ) ); + + $actual = $this->get_enclosed_by_post_id( $post_id ); + $expected = str_replace( 'example.org', sprintf( 'example-%d.org', $post_id ), $data['multi']['expected'] ); + $this->assertSame( $expected, $actual ); + } + + /** + * A callback to filter the list of enclosure links. + * + * @since 5.3.0 + * + * @param array $post_links An array of enclosure links. + * @param int $post_id Post ID. + * @return array $post_links An array of enclosure links. + */ + public function filter_enclosure_links( $enclosure_links, $post_id ) { + // Replace the link host to contain the post ID, to test both filter input arguments. + foreach ( $enclosure_links as &$link ) { + $link = str_replace( 'example.org', sprintf( 'example-%d.org', $post_id ), $link ); + } + return $enclosure_links; + } + + /** + * Helper function to get all enclosure data for a given post. + * + * @since 5.3.0 + * + * @param int $post_id Post ID. + * @return string All enclosure data for the given post. + */ + protected function get_enclosed_by_post_id( $post_id ) { + return join( (array) get_post_meta( $post_id, 'enclosure', false ), '' ); + } + + /** + * Fake the HTTP request response. + * + * @since 5.3.0 + * + * @param bool $false False. + * @param array $arguments Request arguments. + * @param string $url Request URL. + * + * @return array Header. + */ + public function fake_http_request( $false, $arguments, $url ) { + + // Video and audio headers. + $fake_headers = array( + 'mp4' => array( + 'headers' => array( + 'content-length' => 123, + 'content-type' => 'video/mp4', + ), + ), + 'ogg' => array( + 'headers' => array( + 'content-length' => 321, + 'content-type' => 'audio/ogg', + ), + ), + ); + + $path = parse_url( $url, PHP_URL_PATH ); + + if ( false !== $path ) { + $extension = pathinfo( $path, PATHINFO_EXTENSION ); + if ( isset( $fake_headers[ $extension ] ) ) { + return $fake_headers[ $extension ]; + } + } + + // Fallback header. + return array( + 'headers' => array( + 'content-length' => 0, + 'content-type' => '', + ), + ); + } + +}