From 5ec1029ca71f56a961f28b67ce458b7fcae64493 Mon Sep 17 00:00:00 2001 From: Nate Allen Date: Wed, 29 Nov 2023 16:35:20 -0500 Subject: [PATCH 01/12] Get attachment IDs for each post being exported Currently, attachmets are only exported if you choose to export with the 'all' option. This change will get the attachment IDs of all of the posts being exported when the 'all' option isn't used. --- src/wp-admin/includes/export.php | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/wp-admin/includes/export.php b/src/wp-admin/includes/export.php index 9610ac8c9f521..cefa6aeb10f9b 100644 --- a/src/wp-admin/includes/export.php +++ b/src/wp-admin/includes/export.php @@ -144,6 +144,40 @@ function export_wp( $args = array() ) { // Grab a snapshot of post IDs, just in case it changes during the export. $post_ids = $wpdb->get_col( "SELECT ID FROM {$wpdb->posts} $join WHERE $where" ); + // Get IDs for the attachments of each post, unless all content is already being exported. + if ( 'all' !== $args['content'] ) { + foreach ( array_chunk( $post_ids, 20 ) as $chunk ) { + $posts_in = array_map( 'absint', $chunk ); + $placeholders = array_fill( 0, count( $posts_in ), '%d' ); + + // Prepare the SQL statement for attachment ids + $attachment_ids = $wpdb->get_col( + $wpdb->prepare( + " + SELECT ID + FROM $wpdb->posts + WHERE post_parent IN (" . implode( ',', $placeholders ) . ") AND post_type = 'attachment' + ", + $posts_in + ) + ); + + $thumbnails_ids = $wpdb->get_col( + $wpdb->prepare( + " + SELECT meta_value + FROM {$wpdb->postmeta} + WHERE {$wpdb->postmeta}.post_id IN (" . implode( ',', $placeholders ) . ") + AND {$wpdb->postmeta}.meta_key = '_thumbnail_id' + ", + $posts_in + ) + ); + + $post_ids = array_unique( array_merge( $post_ids, $attachment_ids, $thumbnails_ids ) ); + } + } + /* * Get the requested terms ready, empty unless posts filtered by category * or all content. From 52942343c782e50f5a0b84035e3eb8544d4a7a15 Mon Sep 17 00:00:00 2001 From: Nate Allen Date: Wed, 29 Nov 2023 16:36:05 -0500 Subject: [PATCH 02/12] Add unit test to ensure that attachments are included --- tests/phpunit/tests/export/attachments.php | 66 ++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/phpunit/tests/export/attachments.php diff --git a/tests/phpunit/tests/export/attachments.php b/tests/phpunit/tests/export/attachments.php new file mode 100644 index 0000000000000..8182b32fd5c34 --- /dev/null +++ b/tests/phpunit/tests/export/attachments.php @@ -0,0 +1,66 @@ +post->create_and_get( + array( + 'post_title' => 'Test Post 1', + 'post_author' => $factory->user->create( array( 'role' => 'editor' ) ), + ) + ); + + $post_2 = self::factory()->post->create_and_get( + array( + 'post_title' => 'Test Post 2', + 'post_author' => $factory->user->create( array( 'role' => 'editor' ) ), + ) + ); + + $file = DIR_TESTDATA . '/images/test-image.jpg'; + + self::$attachment_1 = $factory->attachment->create_upload_object( $file ); + + set_post_thumbnail( self::$post_1->ID, self::$attachment_1 ); + set_post_thumbnail( $post_2, $factory->attachment->create_upload_object( $file ) ); + } + + /** + * Tests the export function to ensure that attachments are included. + * + * Runs in a separate process to prevent "headers already sent" error. + * + * This test does not preserve global state to prevent the exception + * "Serialization of 'Closure' is not allowed" when running in + * a separate process. + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_export_includes_attachments_for_specific_author() { + require_once ABSPATH . 'wp-admin/includes/export.php'; + + ob_start(); + export_wp( + array( + 'content' => 'post', + 'author' => self::$post_1->post_author, + ) + ); + $xml = simplexml_load_string( ob_get_clean() ); + + $this->assertNotEmpty( $xml->channel->item->title ); + $this->assertEquals( self::$post_1->ID, (int) $xml->channel->item[0]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$attachment_1, (int) $xml->channel->item[1]->children( 'wp', true )->post_id ); + + // Test that the post and attachment by the other author are not included by asserting that there are only two items. + $this->assertCount( 2, $xml->channel->item ); + } +} \ No newline at end of file From 380156ea81079289ed7c18049b04a218ad31d083 Mon Sep 17 00:00:00 2001 From: Nate Allen Date: Wed, 29 Nov 2023 16:46:12 -0500 Subject: [PATCH 03/12] Add newline at end of file --- tests/phpunit/tests/export/attachments.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/export/attachments.php b/tests/phpunit/tests/export/attachments.php index 8182b32fd5c34..a3caeb0c46578 100644 --- a/tests/phpunit/tests/export/attachments.php +++ b/tests/phpunit/tests/export/attachments.php @@ -63,4 +63,4 @@ public function test_export_includes_attachments_for_specific_author() { // Test that the post and attachment by the other author are not included by asserting that there are only two items. $this->assertCount( 2, $xml->channel->item ); } -} \ No newline at end of file +} From 9be7e691f79b7f912f84c348cb3e814d9a370e38 Mon Sep 17 00:00:00 2001 From: Nate Allen Date: Tue, 13 Feb 2024 16:00:13 -0500 Subject: [PATCH 04/12] Update condition to skip if export is 'all' or 'attachment' --- src/wp-admin/includes/export.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/export.php b/src/wp-admin/includes/export.php index cefa6aeb10f9b..02b23723e04de 100644 --- a/src/wp-admin/includes/export.php +++ b/src/wp-admin/includes/export.php @@ -145,7 +145,7 @@ function export_wp( $args = array() ) { $post_ids = $wpdb->get_col( "SELECT ID FROM {$wpdb->posts} $join WHERE $where" ); // Get IDs for the attachments of each post, unless all content is already being exported. - if ( 'all' !== $args['content'] ) { + if ( ! in_array( $args['content'], array( 'all', 'attachment' ), true ) ) { foreach ( array_chunk( $post_ids, 20 ) as $chunk ) { $posts_in = array_map( 'absint', $chunk ); $placeholders = array_fill( 0, count( $posts_in ), '%d' ); From 0fa10ae716fb3d254ea477b745bba6f7c21b34e1 Mon Sep 17 00:00:00 2001 From: Nate Allen Date: Thu, 15 Feb 2024 13:55:29 -0500 Subject: [PATCH 05/12] Use while loop and array_splice to chunk the data This is more consistent to how it is being done elsewhere in the file and is a bit more memory efficient as well. --- src/wp-admin/includes/export.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/export.php b/src/wp-admin/includes/export.php index 02b23723e04de..303c3c7984118 100644 --- a/src/wp-admin/includes/export.php +++ b/src/wp-admin/includes/export.php @@ -146,8 +146,8 @@ function export_wp( $args = array() ) { // Get IDs for the attachments of each post, unless all content is already being exported. if ( ! in_array( $args['content'], array( 'all', 'attachment' ), true ) ) { - foreach ( array_chunk( $post_ids, 20 ) as $chunk ) { - $posts_in = array_map( 'absint', $chunk ); + while ( $next_posts = array_splice( $post_ids, 0, 20 ) ) { + $posts_in = array_map( 'absint', $next_posts ); $placeholders = array_fill( 0, count( $posts_in ), '%d' ); // Prepare the SQL statement for attachment ids From 1084e7882f9c4bd830cf2c352465fd00fb71cd4e Mon Sep 17 00:00:00 2001 From: Nate Allen Date: Thu, 15 Feb 2024 13:57:14 -0500 Subject: [PATCH 06/12] Create placeholders to be used in multiple places --- src/wp-admin/includes/export.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/export.php b/src/wp-admin/includes/export.php index 303c3c7984118..e992cd3fa1104 100644 --- a/src/wp-admin/includes/export.php +++ b/src/wp-admin/includes/export.php @@ -150,13 +150,16 @@ function export_wp( $args = array() ) { $posts_in = array_map( 'absint', $next_posts ); $placeholders = array_fill( 0, count( $posts_in ), '%d' ); + // Create a string for the placeholders. + $in_placeholder = implode( ',', $placeholders ); + // Prepare the SQL statement for attachment ids $attachment_ids = $wpdb->get_col( $wpdb->prepare( " SELECT ID FROM $wpdb->posts - WHERE post_parent IN (" . implode( ',', $placeholders ) . ") AND post_type = 'attachment' + WHERE post_parent IN ($in_placeholder) AND post_type = 'attachment' ", $posts_in ) @@ -167,7 +170,7 @@ function export_wp( $args = array() ) { " SELECT meta_value FROM {$wpdb->postmeta} - WHERE {$wpdb->postmeta}.post_id IN (" . implode( ',', $placeholders ) . ") + WHERE {$wpdb->postmeta}.post_id IN ($in_placeholder) AND {$wpdb->postmeta}.meta_key = '_thumbnail_id' ", $posts_in From 462c84e05107c31f33aed433b6d3358169b2ff97 Mon Sep 17 00:00:00 2001 From: Nate Allen Date: Thu, 15 Feb 2024 14:32:59 -0500 Subject: [PATCH 07/12] Create a copy of post_ids to avoid modifying the original --- src/wp-admin/includes/export.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/export.php b/src/wp-admin/includes/export.php index e992cd3fa1104..6d5b1a424f513 100644 --- a/src/wp-admin/includes/export.php +++ b/src/wp-admin/includes/export.php @@ -146,7 +146,13 @@ function export_wp( $args = array() ) { // Get IDs for the attachments of each post, unless all content is already being exported. if ( ! in_array( $args['content'], array( 'all', 'attachment' ), true ) ) { - while ( $next_posts = array_splice( $post_ids, 0, 20 ) ) { + // Array to hold all additional IDs (attachments and thumbnails) + $additional_ids = array(); + + // Create a copy of the post IDs array to avoid modifying the original array. + $processing_ids = $post_ids; + + while ( $next_posts = array_splice( $processing_ids, 0, 20 ) ) { $posts_in = array_map( 'absint', $next_posts ); $placeholders = array_fill( 0, count( $posts_in ), '%d' ); @@ -177,8 +183,11 @@ function export_wp( $args = array() ) { ) ); - $post_ids = array_unique( array_merge( $post_ids, $attachment_ids, $thumbnails_ids ) ); + $additional_ids = array_merge( $additional_ids, $attachment_ids, $thumbnails_ids ); } + + // Merge the additional IDs back with the original post IDs after processing all posts + $post_ids = array_unique( array_merge( $post_ids, $additional_ids ) ); } /* From 8b67134388492729dd5527d0fc54eead6018cd61 Mon Sep 17 00:00:00 2001 From: Nate Allen Date: Thu, 15 Feb 2024 17:24:49 -0500 Subject: [PATCH 08/12] Add additional unit tests --- tests/phpunit/tests/export/attachments.php | 155 +++++++++++++++++++-- 1 file changed, 146 insertions(+), 9 deletions(-) diff --git a/tests/phpunit/tests/export/attachments.php b/tests/phpunit/tests/export/attachments.php index a3caeb0c46578..25a8f4c68cea0 100644 --- a/tests/phpunit/tests/export/attachments.php +++ b/tests/phpunit/tests/export/attachments.php @@ -7,33 +7,62 @@ */ class Test_Export_Includes_Attachments extends WP_UnitTestCase { protected static $post_1; + protected static $post_2; + protected static $page_1; + protected static $page_2; protected static $attachment_1; + protected static $attachment_2; + protected static $attachment_3; + protected static $attachment_4; public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { - self::$post_1 = self::factory()->post->create_and_get( + self::$post_1 = $factory->post->create_and_get( array( 'post_title' => 'Test Post 1', + 'post_type' => 'post', 'post_author' => $factory->user->create( array( 'role' => 'editor' ) ), ) ); - $post_2 = self::factory()->post->create_and_get( + self::$post_2 = self::factory()->post->create_and_get( array( 'post_title' => 'Test Post 2', + 'post_type' => 'post', + 'post_author' => $factory->user->create( array( 'role' => 'editor' ) ), + ) + ); + + self::$page_1 = $factory->post->create_and_get( + array( + 'post_title' => 'Test Page 1', + 'post_type' => 'page', + 'post_author' => $factory->user->create( array( 'role' => 'editor' ) ), + ) + ); + + self::$page_2 = self::factory()->post->create_and_get( + array( + 'post_title' => 'Test Page 2', + 'post_type' => 'page', 'post_author' => $factory->user->create( array( 'role' => 'editor' ) ), ) ); $file = DIR_TESTDATA . '/images/test-image.jpg'; - self::$attachment_1 = $factory->attachment->create_upload_object( $file ); + self::$attachment_1 = $factory->attachment->create_upload_object( $file, self::$post_1->ID ); + self::$attachment_2 = $factory->attachment->create_upload_object( $file, self::$post_2->ID ); + self::$attachment_3 = $factory->attachment->create_upload_object( $file, self::$page_1->ID ); + self::$attachment_4 = $factory->attachment->create_upload_object( $file, self::$page_2->ID ); set_post_thumbnail( self::$post_1->ID, self::$attachment_1 ); - set_post_thumbnail( $post_2, $factory->attachment->create_upload_object( $file ) ); + set_post_thumbnail( self::$post_2->ID, self::$attachment_2 ); + set_post_thumbnail( self::$page_1->ID, self::$attachment_3 ); + set_post_thumbnail( self::$page_2->ID, self::$attachment_4 ); } /** - * Tests the export function to ensure that attachments are included. + * Tests the export function to ensure that attachments are included when exporting all content. * * Runs in a separate process to prevent "headers already sent" error. * @@ -44,7 +73,117 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { * @runInSeparateProcess * @preserveGlobalState disabled */ - public function test_export_includes_attachments_for_specific_author() { + public function test_export_includes_attachments_all_content() { + require_once ABSPATH . 'wp-admin/includes/export.php'; + + ob_start(); + export_wp( array( 'content' => 'all' ) ); + $xml = simplexml_load_string( ob_get_clean() ); + + $this->assertNotEmpty( $xml->channel->item->title ); + $this->assertEquals( self::$post_1->ID, (int) $xml->channel->item[0]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$post_2->ID, (int) $xml->channel->item[1]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$page_1->ID, (int) $xml->channel->item[2]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$page_2->ID, (int) $xml->channel->item[3]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$attachment_1, (int) $xml->channel->item[4]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$attachment_2, (int) $xml->channel->item[5]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$attachment_3, (int) $xml->channel->item[6]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$attachment_4, (int) $xml->channel->item[7]->children( 'wp', true )->post_id ); + $this->assertCount( 8, $xml->channel->item ); // Expect 4 items: 2 pages, 2 posts and 4 attachments + } + + /** + * Tests the export function to ensure that attachments are included when exporting all posts. + * + * Runs in a separate process to prevent "headers already sent" error. + * + * This test does not preserve global state to prevent the exception + * "Serialization of 'Closure' is not allowed" when running in + * a separate process. + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_export_includes_attachments_for_all_posts() { + require_once ABSPATH . 'wp-admin/includes/export.php'; + + ob_start(); + export_wp( array( 'content' => 'post' ) ); + $xml = simplexml_load_string( ob_get_clean() ); + + $this->assertNotEmpty( $xml->channel->item->title ); + $this->assertEquals( self::$post_1->ID, (int) $xml->channel->item[0]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$post_2->ID, (int) $xml->channel->item[1]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$attachment_1, (int) $xml->channel->item[2]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$attachment_2, (int) $xml->channel->item[3]->children( 'wp', true )->post_id ); + $this->assertCount( 4, $xml->channel->item ); // Expect 4 items: 2 posts and 2 attachments + } + + /** + * Tests the export function to ensure that attachments are included when exporting all pages. + * + * Runs in a separate process to prevent "headers already sent" error. + * + * This test does not preserve global state to prevent the exception + * "Serialization of 'Closure' is not allowed" when running in + * a separate process. + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_export_includes_attachments_for_all_pages() { + require_once ABSPATH . 'wp-admin/includes/export.php'; + + ob_start(); + export_wp( array( 'content' => 'page' ) ); + $xml = simplexml_load_string( ob_get_clean() ); + + $this->assertNotEmpty( $xml->channel->item->title ); + $this->assertEquals( self::$page_1->ID, (int) $xml->channel->item[0]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$page_2->ID, (int) $xml->channel->item[1]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$attachment_3, (int) $xml->channel->item[2]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$attachment_4, (int) $xml->channel->item[3]->children( 'wp', true )->post_id ); + $this->assertCount( 4, $xml->channel->item ); // Expect 4 items: 2 pages and 2 attachments + } + + /** + * Tests the export function to ensure that attachments are included when exporting pages by a specific author. + * + * Runs in a separate process to prevent "headers already sent" error. + * + * This test does not preserve global state to prevent the exception + * "Serialization of 'Closure' is not allowed" when running in + * a separate process. + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_export_includes_attachments_for_specific_author_pages() { + require_once ABSPATH . 'wp-admin/includes/export.php'; + + ob_start(); + export_wp( array( 'content' => 'page', 'author' => self::$page_1->post_author ) ); + $xml = simplexml_load_string( ob_get_clean() ); + + $this->assertNotEmpty( $xml->channel->item->title ); + $this->assertEquals( self::$page_1->ID, (int) $xml->channel->item[0]->children( 'wp', true )->post_id ); + $this->assertEquals( self::$attachment_3, (int) $xml->channel->item[1]->children( 'wp', true )->post_id ); + $this->assertCount( 2, $xml->channel->item ); // Expect 2 items: 1 page and 1 attachment + } + + /** + * Tests the export function to ensure that attachments are included when exporting posts by a specific author. + * + * Runs in a separate process to prevent "headers already sent" error. + * + * This test does not preserve global state to prevent the exception + * "Serialization of 'Closure' is not allowed" when running in + * a separate process. + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_export_includes_attachments_for_specific_author_posts() { require_once ABSPATH . 'wp-admin/includes/export.php'; ob_start(); @@ -59,8 +198,6 @@ public function test_export_includes_attachments_for_specific_author() { $this->assertNotEmpty( $xml->channel->item->title ); $this->assertEquals( self::$post_1->ID, (int) $xml->channel->item[0]->children( 'wp', true )->post_id ); $this->assertEquals( self::$attachment_1, (int) $xml->channel->item[1]->children( 'wp', true )->post_id ); - - // Test that the post and attachment by the other author are not included by asserting that there are only two items. - $this->assertCount( 2, $xml->channel->item ); + $this->assertCount( 2, $xml->channel->item ); // Expect 2 items: 1 post and 1 attachment } } From 382c7ba3b9d4cfa0a382038a889e54de554fdd8d Mon Sep 17 00:00:00 2001 From: Nate Allen Date: Thu, 15 Feb 2024 17:34:35 -0500 Subject: [PATCH 09/12] Each array value should start on a new line --- tests/phpunit/tests/export/attachments.php | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/export/attachments.php b/tests/phpunit/tests/export/attachments.php index 25a8f4c68cea0..05999e6d1ba7a 100644 --- a/tests/phpunit/tests/export/attachments.php +++ b/tests/phpunit/tests/export/attachments.php @@ -108,7 +108,11 @@ public function test_export_includes_attachments_for_all_posts() { require_once ABSPATH . 'wp-admin/includes/export.php'; ob_start(); - export_wp( array( 'content' => 'post' ) ); + export_wp( + array( + 'content' => 'post', + ) + ); $xml = simplexml_load_string( ob_get_clean() ); $this->assertNotEmpty( $xml->channel->item->title ); @@ -135,7 +139,11 @@ public function test_export_includes_attachments_for_all_pages() { require_once ABSPATH . 'wp-admin/includes/export.php'; ob_start(); - export_wp( array( 'content' => 'page' ) ); + export_wp( + array( + 'content' => 'page', + ) + ); $xml = simplexml_load_string( ob_get_clean() ); $this->assertNotEmpty( $xml->channel->item->title ); @@ -162,7 +170,12 @@ public function test_export_includes_attachments_for_specific_author_pages() { require_once ABSPATH . 'wp-admin/includes/export.php'; ob_start(); - export_wp( array( 'content' => 'page', 'author' => self::$page_1->post_author ) ); + export_wp( + array( + 'content' => 'page', + 'author' => self::$page_1->post_author, + ) + ); $xml = simplexml_load_string( ob_get_clean() ); $this->assertNotEmpty( $xml->channel->item->title ); From d91af2727a34752a03f0c779e57e3cb4701536c0 Mon Sep 17 00:00:00 2001 From: Nate Allen Date: Mon, 19 Feb 2024 12:29:29 -0500 Subject: [PATCH 10/12] Add full stop at end of comments --- src/wp-admin/includes/export.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/export.php b/src/wp-admin/includes/export.php index 6d5b1a424f513..cc0e7904329cf 100644 --- a/src/wp-admin/includes/export.php +++ b/src/wp-admin/includes/export.php @@ -146,7 +146,7 @@ function export_wp( $args = array() ) { // Get IDs for the attachments of each post, unless all content is already being exported. if ( ! in_array( $args['content'], array( 'all', 'attachment' ), true ) ) { - // Array to hold all additional IDs (attachments and thumbnails) + // Array to hold all additional IDs (attachments and thumbnails). $additional_ids = array(); // Create a copy of the post IDs array to avoid modifying the original array. @@ -159,7 +159,7 @@ function export_wp( $args = array() ) { // Create a string for the placeholders. $in_placeholder = implode( ',', $placeholders ); - // Prepare the SQL statement for attachment ids + // Prepare the SQL statement for attachment ids. $attachment_ids = $wpdb->get_col( $wpdb->prepare( " From 6c9d28d204579317db9a9c1614bb31a67a589452 Mon Sep 17 00:00:00 2001 From: hellofromtonya Date: Tue, 20 Feb 2024 09:14:57 -0600 Subject: [PATCH 11/12] Test improvements. Changes the test class to cover the export_wp() function, i.e. the function under test. 1. Relocates to tests/admin/. 2. Renames the test file to function's name in camelCase. 3. Renames the test class to follow Tests_[GroupName]_FunctionName. 4. Changes the groups to admin and export. 5. Renames tests to test_should_[what_it_should_do] format. 6. Moves the ticket annotation to the test methods. 7. Adds covers annotation. 8. Adds test failure reason when there are multiple assertions (i.e. to help identify which assertion failed). 9. Uses assertSame() as part of an effort in Core to reduce assertEquals() usage. 10. Switch to dataProvider. 11. Update docblocks. --- tests/phpunit/tests/admin/exportWp.php | 293 +++++++++++++++++++++ tests/phpunit/tests/export/attachments.php | 216 --------------- 2 files changed, 293 insertions(+), 216 deletions(-) create mode 100644 tests/phpunit/tests/admin/exportWp.php delete mode 100644 tests/phpunit/tests/export/attachments.php diff --git a/tests/phpunit/tests/admin/exportWp.php b/tests/phpunit/tests/admin/exportWp.php new file mode 100644 index 0000000000000..ebb60018c0e7a --- /dev/null +++ b/tests/phpunit/tests/admin/exportWp.php @@ -0,0 +1,293 @@ + array(), + 'attachment for post 1' => array(), + 'post 2' => array(), + 'attachment for post 2' => array(), + 'page 1' => array(), + 'attachment for page 1' => array(), + 'page 2' => array(), + 'attachment for page 2' => array(), + ); + + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + require_once ABSPATH . 'wp-admin/includes/export.php'; + $file = DIR_TESTDATA . '/images/test-image.jpg'; + + $dataset = array( + 'post 1' => array( + 'post_title' => 'Test Post 1', + 'post_type' => 'post', + ), + 'post 2' => array( + 'post_title' => 'Test Post 2', + 'post_type' => 'post', + ), + 'page 1' => array( + 'post_title' => 'Test Page 1', + 'post_type' => 'page', + ), + 'page 2' => array( + 'post_title' => 'Test Page 2', + 'post_type' => 'page', + ), + ); + + $xml_item_index = -1; + + foreach ( $dataset as $post_key => $post_data ) { + $attachment_key = "attachment for $post_key"; + $post_data['post_author'] = $factory->user->create( array( 'role' => 'editor' ) ); + + $post_id = $factory->post->create( $post_data ); + $attachment_id = $factory->attachment->create_upload_object( $file, $post_id ); + set_post_thumbnail( $post_id, $attachment_id ); + + self::$post_ids[ $post_key ] = array( + 'post_id' => $post_id, + 'post_author' => $post_data['post_author'], + 'xml_item_index' => ++$xml_item_index, + ); + self::$post_ids[ $attachment_key ] = array( + 'post_id' => $attachment_id, + 'post_author' => $post_data['post_author'], + 'xml_item_index' => ++$xml_item_index, + ); + } + } + + /** + * @dataProvider data_should_include_attachments + * + * @ticket 17379 + * + * @param array $args Arguments to pass to export_wp(). + * @param array $expected { + * The expected data. + * + * @type array $items { + * The expected XML items count assertion arguments. + * + * @type int $number_of_items The expected number of XML items. + * @type string $message The assertion failure message. + * } + * @type array $ids A list of self::$post_ids keys. + */ + public function test_should_include_attachments( array $args, array $expected ) { + $this->populate_args_post_authors( $args, $expected['ids'] ); + + $xml = $this->get_the_export( $args ); + + $expected_number_of_items = $expected['items']['number_of_items']; + $this->assertCount( $expected_number_of_items, $xml->channel->item, $expected['items']['message'] ); + + // Test each XML item's post ID to valid the post, page, and attachment (when appropriate) were exported. + foreach ( $expected['ids'] as $post_ids_key ) { + $xml_item = $this->get_xml_item( $xml, $post_ids_key, $expected_number_of_items ); + + $this->assertSame( + $this->get_expected_id( $post_ids_key ), + (int) $xml_item->post_id, + "In the XML, the {$post_ids_key}'s ID should match the expected content" + ); + } + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_include_attachments() { + return array( + 'for all content' => array( + 'args' => array( + 'content' => 'all', + ), + 'expected' => array( + 'items' => array( + 'number_of_items' => 8, + 'message' => 'The number of items should be 8 = 2 pages, 2 posts and 4 attachments', + ), + 'ids' => array( + 'post 1', + 'post 2', + 'page 1', + 'page 2', + 'attachment for page 1', + 'attachment for post 2', + 'attachment for page 1', + 'attachment for page 2', + ), + ), + ), + 'for all posts' => array( + 'args' => array( + 'content' => 'post', + ), + 'expected' => array( + 'items' => array( + 'number_of_items' => 4, + 'message' => 'The number of items should be 4 = 2 posts and 2 attachments', + ), + 'ids' => array( + 'post 1', + 'post 2', + 'attachment for post 1', + 'attachment for post 2', + ), + ), + ), + 'for all pages' => array( + 'args' => array( + 'content' => 'page', + ), + 'expected' => array( + 'items' => array( + 'number_of_items' => 4, + 'message' => 'The number of items should be 4 = 2 pages and 2 attachments', + ), + 'ids' => array( + 'page 1', + 'attachment for page 1', + 'page 2', + 'attachment for page 2', + ), + ), + ), + 'for specific author posts' => array( + 'args' => array( + 'content' => 'post', + 'author' => '', // The test will populate the author's ID. + ), + 'expected' => array( + 'items' => array( + 'number_of_items' => 2, + 'message' => 'The number of items should be 2 = 1 post and 1 attachment', + ), + 'ids' => array( + 'post 1', + 'attachment for post 1', + ), + ), + ), + 'for specific author pages' => array( + 'args' => array( + 'content' => 'page', + 'author' => '', // The test will populate the author's ID. + ), + 'expected' => array( + 'items' => array( + 'number_of_items' => 2, + 'message' => 'The number of items should be 2 = 1 page and 1 attachment', + ), + 'ids' => array( + 'page 2', + 'attachment for page 2', + ), + ), + ), + ); + } + + /** + * Gets the export results. + * + * @since 6.5.0 + * + * @param array $args Arguments to pass to export_wp(). + * @return SimpleXMLElement|false Returns the XML object on success, otherwise false is returned. + */ + private function get_the_export( $args ) { + ob_start(); + export_wp( $args ); + $results = ob_get_clean(); + + return simplexml_load_string( $results ); + } + + /** + * Gets the expected ID. + * + * @since 6.5.0 + * + * @param string $post_ids_key The key to lookup in the $post_ids static property. + * @return int Expected ID. + */ + private function get_expected_id( $post_ids_key ) { + $post_info = self::$post_ids[ $post_ids_key ]; + + return $post_info['post_id']; + } + + /** + * Gets the XML item for the given post or attachment in the self::$post_ids. + * + * @since 6.5.0 + * + * @param SimpleXMLElement $xml XML object. + * @param string $post_ids_key The key to lookup in the $post_ids static property. + * @param int $number_of_items The number of expected XML items. + * @return SimpleXMLElement The XML item. + */ + private function get_xml_item( $xml, $post_ids_key, $number_of_items ) { + $post_info = self::$post_ids[ $post_ids_key ]; + + if ( $post_info['xml_item_index'] < $number_of_items ) { + $xml_item_index = $post_info['xml_item_index']; + } elseif ( 2 === $number_of_items ) { + $xml_item_index = 0 === $post_info['xml_item_index'] % 2 ? 0 : 1; + } else { + $xml_item_index = $post_info['xml_item_index'] - $number_of_items; + } + + return $xml->channel->item[ $xml_item_index ]->children( 'wp', true ); + } + + /** + * Populates the post author in the given args. + * + * @since 6.5.0 + * + * @param array $args Passed by reference. export_wp() arguments to process. + */ + private function populate_args_post_authors( array &$args, $expected_ids ) { + if ( ! isset( $args['author'] ) ) { + return; + } + $post_ids_key = $expected_ids[0]; + $args['author'] = self::$post_ids[ $post_ids_key ]['post_author']; + } +} diff --git a/tests/phpunit/tests/export/attachments.php b/tests/phpunit/tests/export/attachments.php deleted file mode 100644 index 05999e6d1ba7a..0000000000000 --- a/tests/phpunit/tests/export/attachments.php +++ /dev/null @@ -1,216 +0,0 @@ -post->create_and_get( - array( - 'post_title' => 'Test Post 1', - 'post_type' => 'post', - 'post_author' => $factory->user->create( array( 'role' => 'editor' ) ), - ) - ); - - self::$post_2 = self::factory()->post->create_and_get( - array( - 'post_title' => 'Test Post 2', - 'post_type' => 'post', - 'post_author' => $factory->user->create( array( 'role' => 'editor' ) ), - ) - ); - - self::$page_1 = $factory->post->create_and_get( - array( - 'post_title' => 'Test Page 1', - 'post_type' => 'page', - 'post_author' => $factory->user->create( array( 'role' => 'editor' ) ), - ) - ); - - self::$page_2 = self::factory()->post->create_and_get( - array( - 'post_title' => 'Test Page 2', - 'post_type' => 'page', - 'post_author' => $factory->user->create( array( 'role' => 'editor' ) ), - ) - ); - - $file = DIR_TESTDATA . '/images/test-image.jpg'; - - self::$attachment_1 = $factory->attachment->create_upload_object( $file, self::$post_1->ID ); - self::$attachment_2 = $factory->attachment->create_upload_object( $file, self::$post_2->ID ); - self::$attachment_3 = $factory->attachment->create_upload_object( $file, self::$page_1->ID ); - self::$attachment_4 = $factory->attachment->create_upload_object( $file, self::$page_2->ID ); - - set_post_thumbnail( self::$post_1->ID, self::$attachment_1 ); - set_post_thumbnail( self::$post_2->ID, self::$attachment_2 ); - set_post_thumbnail( self::$page_1->ID, self::$attachment_3 ); - set_post_thumbnail( self::$page_2->ID, self::$attachment_4 ); - } - - /** - * Tests the export function to ensure that attachments are included when exporting all content. - * - * Runs in a separate process to prevent "headers already sent" error. - * - * This test does not preserve global state to prevent the exception - * "Serialization of 'Closure' is not allowed" when running in - * a separate process. - * - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function test_export_includes_attachments_all_content() { - require_once ABSPATH . 'wp-admin/includes/export.php'; - - ob_start(); - export_wp( array( 'content' => 'all' ) ); - $xml = simplexml_load_string( ob_get_clean() ); - - $this->assertNotEmpty( $xml->channel->item->title ); - $this->assertEquals( self::$post_1->ID, (int) $xml->channel->item[0]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$post_2->ID, (int) $xml->channel->item[1]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$page_1->ID, (int) $xml->channel->item[2]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$page_2->ID, (int) $xml->channel->item[3]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$attachment_1, (int) $xml->channel->item[4]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$attachment_2, (int) $xml->channel->item[5]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$attachment_3, (int) $xml->channel->item[6]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$attachment_4, (int) $xml->channel->item[7]->children( 'wp', true )->post_id ); - $this->assertCount( 8, $xml->channel->item ); // Expect 4 items: 2 pages, 2 posts and 4 attachments - } - - /** - * Tests the export function to ensure that attachments are included when exporting all posts. - * - * Runs in a separate process to prevent "headers already sent" error. - * - * This test does not preserve global state to prevent the exception - * "Serialization of 'Closure' is not allowed" when running in - * a separate process. - * - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function test_export_includes_attachments_for_all_posts() { - require_once ABSPATH . 'wp-admin/includes/export.php'; - - ob_start(); - export_wp( - array( - 'content' => 'post', - ) - ); - $xml = simplexml_load_string( ob_get_clean() ); - - $this->assertNotEmpty( $xml->channel->item->title ); - $this->assertEquals( self::$post_1->ID, (int) $xml->channel->item[0]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$post_2->ID, (int) $xml->channel->item[1]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$attachment_1, (int) $xml->channel->item[2]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$attachment_2, (int) $xml->channel->item[3]->children( 'wp', true )->post_id ); - $this->assertCount( 4, $xml->channel->item ); // Expect 4 items: 2 posts and 2 attachments - } - - /** - * Tests the export function to ensure that attachments are included when exporting all pages. - * - * Runs in a separate process to prevent "headers already sent" error. - * - * This test does not preserve global state to prevent the exception - * "Serialization of 'Closure' is not allowed" when running in - * a separate process. - * - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function test_export_includes_attachments_for_all_pages() { - require_once ABSPATH . 'wp-admin/includes/export.php'; - - ob_start(); - export_wp( - array( - 'content' => 'page', - ) - ); - $xml = simplexml_load_string( ob_get_clean() ); - - $this->assertNotEmpty( $xml->channel->item->title ); - $this->assertEquals( self::$page_1->ID, (int) $xml->channel->item[0]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$page_2->ID, (int) $xml->channel->item[1]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$attachment_3, (int) $xml->channel->item[2]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$attachment_4, (int) $xml->channel->item[3]->children( 'wp', true )->post_id ); - $this->assertCount( 4, $xml->channel->item ); // Expect 4 items: 2 pages and 2 attachments - } - - /** - * Tests the export function to ensure that attachments are included when exporting pages by a specific author. - * - * Runs in a separate process to prevent "headers already sent" error. - * - * This test does not preserve global state to prevent the exception - * "Serialization of 'Closure' is not allowed" when running in - * a separate process. - * - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function test_export_includes_attachments_for_specific_author_pages() { - require_once ABSPATH . 'wp-admin/includes/export.php'; - - ob_start(); - export_wp( - array( - 'content' => 'page', - 'author' => self::$page_1->post_author, - ) - ); - $xml = simplexml_load_string( ob_get_clean() ); - - $this->assertNotEmpty( $xml->channel->item->title ); - $this->assertEquals( self::$page_1->ID, (int) $xml->channel->item[0]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$attachment_3, (int) $xml->channel->item[1]->children( 'wp', true )->post_id ); - $this->assertCount( 2, $xml->channel->item ); // Expect 2 items: 1 page and 1 attachment - } - - /** - * Tests the export function to ensure that attachments are included when exporting posts by a specific author. - * - * Runs in a separate process to prevent "headers already sent" error. - * - * This test does not preserve global state to prevent the exception - * "Serialization of 'Closure' is not allowed" when running in - * a separate process. - * - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function test_export_includes_attachments_for_specific_author_posts() { - require_once ABSPATH . 'wp-admin/includes/export.php'; - - ob_start(); - export_wp( - array( - 'content' => 'post', - 'author' => self::$post_1->post_author, - ) - ); - $xml = simplexml_load_string( ob_get_clean() ); - - $this->assertNotEmpty( $xml->channel->item->title ); - $this->assertEquals( self::$post_1->ID, (int) $xml->channel->item[0]->children( 'wp', true )->post_id ); - $this->assertEquals( self::$attachment_1, (int) $xml->channel->item[1]->children( 'wp', true )->post_id ); - $this->assertCount( 2, $xml->channel->item ); // Expect 2 items: 1 post and 1 attachment - } -} From 5f7c6bf276d6aefd34a60c07c172baba0e88ea35 Mon Sep 17 00:00:00 2001 From: hellofromtonya Date: Wed, 21 Feb 2024 11:22:20 -0600 Subject: [PATCH 12/12] Remove {} around $wpdb->postmeta --- src/wp-admin/includes/export.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/includes/export.php b/src/wp-admin/includes/export.php index cc0e7904329cf..d05f98f73437d 100644 --- a/src/wp-admin/includes/export.php +++ b/src/wp-admin/includes/export.php @@ -175,9 +175,9 @@ function export_wp( $args = array() ) { $wpdb->prepare( " SELECT meta_value - FROM {$wpdb->postmeta} - WHERE {$wpdb->postmeta}.post_id IN ($in_placeholder) - AND {$wpdb->postmeta}.meta_key = '_thumbnail_id' + FROM $wpdb->postmeta + WHERE $wpdb->postmeta.post_id IN ($in_placeholder) + AND $wpdb->postmeta.meta_key = '_thumbnail_id' ", $posts_in )