diff --git a/src/wp-admin/edit.php b/src/wp-admin/edit.php index 35aca175d4..68786f0f8c 100644 --- a/src/wp-admin/edit.php +++ b/src/wp-admin/edit.php @@ -134,6 +134,11 @@ if ( $doaction ) { break; case 'untrash': $untrashed = 0; + + if ( isset( $_GET['doaction'] ) && ( 'undo' === $_GET['doaction'] ) ) { + add_filter( 'wp_untrash_post_status', 'wp_untrash_post_set_previous_status', 10, 3 ); + } + foreach ( (array) $post_ids as $post_id ) { if ( ! current_user_can( 'delete_post', $post_id ) ) { wp_die( __( 'Sorry, you are not allowed to restore this item from the Trash.' ) ); @@ -146,6 +151,9 @@ if ( $doaction ) { $untrashed++; } $sendback = add_query_arg( 'untrashed', $untrashed, $sendback ); + + remove_filter( 'wp_untrash_post_status', 'wp_untrash_post_set_previous_status', 10, 3 ); + break; case 'delete': $deleted = 0; @@ -419,6 +427,18 @@ foreach ( $bulk_counts as $message => $count ) { $ids = preg_replace( '/[^0-9,]/', '', $_REQUEST['ids'] ); $messages[] = '<a href="' . esc_url( wp_nonce_url( "edit.php?post_type=$post_type&doaction=undo&action=untrash&ids=$ids", 'bulk-posts' ) ) . '">' . __( 'Undo' ) . '</a>'; } + + if ( 'untrashed' === $message && isset( $_REQUEST['ids'] ) ) { + $ids = explode( ',', $_REQUEST['ids'] ); + + if ( 1 === count( $ids ) && current_user_can( 'edit_post', $ids[0] ) ) { + $messages[] = sprintf( + '<a href="%1$s">%2$s</a>', + esc_url( get_edit_post_link( $ids[0] ) ), + esc_html( get_post_type_object( get_post_type( $ids[0] ) )->labels->edit_item ) + ); + } + } } if ( $messages ) { diff --git a/src/wp-admin/post.php b/src/wp-admin/post.php index 759e4b6b93..c4a0ab17e5 100644 --- a/src/wp-admin/post.php +++ b/src/wp-admin/post.php @@ -291,7 +291,14 @@ switch ( $action ) { wp_die( __( 'Error in restoring the item from Trash.' ) ); } - wp_redirect( add_query_arg( 'untrashed', 1, $sendback ) ); + $sendback = add_query_arg( + array( + 'untrashed' => 1, + 'ids' => $post_id, + ), + $sendback + ); + wp_redirect( $sendback ); exit; case 'delete': diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 3f6864368c..f46fc6ea65 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -1193,6 +1193,7 @@ function wp_removable_query_args() { 'error', 'hotkeys_highlight_first', 'hotkeys_highlight_last', + 'ids', 'locked', 'message', 'same', diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 293461e9ed..71161f4cab 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -3233,11 +3233,13 @@ function wp_trash_post( $post_id = 0 ) { } /** - * Restore a post or page from the Trash. + * Restores a post from the Trash. * * @since 2.9.0 + * @since 5.6.0 An untrashed post is now returned to 'draft' status by default, except for + * attachments which are returned to their original 'inherit' status. * - * @param int $post_id Optional. Post ID. Default is ID of the global $post. + * @param int $post_id Optional. Post ID. Default is ID of the global `$post`. * @return WP_Post|false|null Post data on success, false or null on failure. */ function wp_untrash_post( $post_id = 0 ) { @@ -3247,19 +3249,25 @@ function wp_untrash_post( $post_id = 0 ) { return $post; } + $post_id = $post->ID; + if ( 'trash' !== $post->post_status ) { return false; } + $previous_status = get_post_meta( $post_id, '_wp_trash_meta_status', true ); + /** * Filters whether a post untrashing should take place. * * @since 4.9.0 + * @since 5.6.0 The `$previous_status` parameter was added. * - * @param bool|null $untrash Whether to go forward with untrashing. - * @param WP_Post $post Post object. + * @param bool|null $untrash Whether to go forward with untrashing. + * @param WP_Post $post Post object. + * @param string $previous_status The status of the post at the point where it was trashed. */ - $check = apply_filters( 'pre_untrash_post', null, $post ); + $check = apply_filters( 'pre_untrash_post', null, $post, $previous_status ); if ( null !== $check ) { return $check; } @@ -3268,12 +3276,31 @@ function wp_untrash_post( $post_id = 0 ) { * Fires before a post is restored from the Trash. * * @since 2.9.0 + * @since 5.6.0 The `$previous_status` parameter was added. * - * @param int $post_id Post ID. + * @param int $post_id Post ID. + * @param string $previous_status The status of the post at the point where it was trashed. */ - do_action( 'untrash_post', $post_id ); + do_action( 'untrash_post', $post_id, $previous_status ); - $post_status = get_post_meta( $post_id, '_wp_trash_meta_status', true ); + $new_status = ( 'attachment' === $post->post_type ) ? 'inherit' : 'draft'; + + /** + * Filters the status that a post gets assigned when it is restored from the trash (untrashed). + * + * By default posts that are restored will be assigned a status of 'draft'. Return the value of `$previous_status` + * in order to assign the status that the post had before it was trashed. The `wp_untrash_post_set_previous_status()` + * function is available for this. + * + * Prior to WordPress 5.6.0, restored posts were always assigned their original status. + * + * @since 5.6.0 + * + * @param string $new_status The new status of the post being restored. + * @param int $post_id The ID of the post being restored. + * @param string $previous_status The status of the post at the point where it was trashed. + */ + $post_status = apply_filters( 'wp_untrash_post_status', $new_status, $post_id, $previous_status ); delete_post_meta( $post_id, '_wp_trash_meta_status' ); delete_post_meta( $post_id, '_wp_trash_meta_time' ); @@ -3295,10 +3322,12 @@ function wp_untrash_post( $post_id = 0 ) { * Fires after a post is restored from the Trash. * * @since 2.9.0 + * @since 5.6.0 The `$previous_status` parameter was added. * - * @param int $post_id Post ID. + * @param int $post_id Post ID. + * @param string $previous_status The status of the post at the point where it was trashed. */ - do_action( 'untrashed_post', $post_id ); + do_action( 'untrashed_post', $post_id, $previous_status ); return $post; } @@ -7513,3 +7542,19 @@ function wp_get_original_image_url( $attachment_id ) { */ return apply_filters( 'wp_get_original_image_url', $original_image_url, $attachment_id ); } + +/** + * Filter callback which sets the status of an untrashed post to its previous status. + * + * This can be used as a callback on the `wp_untrash_post_status` filter. + * + * @since 5.6.0 + * + * @param string $new_status The new status of the post being restored. + * @param int $post_id The ID of the post being restored. + * @param string $previous_status The status of the post at the point where it was trashed. + * @return string The new status of the post. + */ +function wp_untrash_post_set_previous_status( $new_status, $post_id, $previous_status ) { + return $previous_status; +} diff --git a/tests/phpunit/tests/post/wpInsertPost.php b/tests/phpunit/tests/post/wpInsertPost.php index 4c9f7a6390..a3640bf244 100644 --- a/tests/phpunit/tests/post/wpInsertPost.php +++ b/tests/phpunit/tests/post/wpInsertPost.php @@ -159,11 +159,57 @@ class Tests_WPInsertPost extends WP_UnitTestCase { ); wp_untrash_post( $about_page_id ); + wp_update_post( + array( + 'ID' => $about_page_id, + 'post_status' => 'publish', + ) + ); $this->assertSame( 'about', get_post( $another_about_page_id )->post_name ); $this->assertSame( 'about-2', get_post( $about_page_id )->post_name ); } + /** + * @ticket 23022 + * @dataProvider data_various_post_statuses + */ + function test_untrashing_a_post_should_always_restore_it_to_draft_status( $post_status ) { + $page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_status' => $post_status, + ) + ); + + wp_trash_post( $page_id ); + wp_untrash_post( $page_id ); + + $this->assertSame( 'draft', get_post( $page_id )->post_status ); + } + + /** + * @ticket 23022 + * @dataProvider data_various_post_statuses + */ + function test_wp_untrash_post_status_filter_restores_post_to_correct_status( $post_status ) { + add_filter( 'wp_untrash_post_status', 'wp_untrash_post_set_previous_status', 10, 3 ); + + $page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_status' => $post_status, + ) + ); + + wp_trash_post( $page_id ); + wp_untrash_post( $page_id ); + + remove_filter( 'wp_untrash_post_status', 'wp_untrash_post_set_previous_status', 10, 3 ); + + $this->assertSame( $post_status, get_post( $page_id )->post_status ); + } + /** * Data for testing the ability for users to set the post slug. * @@ -183,6 +229,28 @@ class Tests_WPInsertPost extends WP_UnitTestCase { ); } + /** + * Data for testing post statuses. + * + * @return array Array of test arguments. + */ + function data_various_post_statuses() { + return array( + array( + 'draft', + ), + array( + 'pending', + ), + array( + 'private', + ), + array( + 'publish', + ), + ); + } + /** * Test contributor making changes to the pending post slug. *