diff --git a/src/js/_enqueues/wp/autosave.js b/src/js/_enqueues/wp/autosave.js index 9f46c3161483e..099a2b8c091e2 100644 --- a/src/js/_enqueues/wp/autosave.js +++ b/src/js/_enqueues/wp/autosave.js @@ -115,6 +115,10 @@ window.autosave = function() { data.auto_draft = '1'; } + if ( $( '#active_post_lock' ).length ) { + data.active_post_lock = $( '#active_post_lock' ).val() || ''; + } + return data; } diff --git a/src/wp-admin/includes/misc.php b/src/wp-admin/includes/misc.php index f021aedb8a5fb..6c6cf4096bc07 100644 --- a/src/wp-admin/includes/misc.php +++ b/src/wp-admin/includes/misc.php @@ -1201,9 +1201,8 @@ function wp_refresh_post_lock( $response, $data, $screen_id ) { return $response; } - $user_id = wp_check_post_lock( $post_id ); + $user_id = wp_check_post_lock( $post_id, $received['lock'] ?? '' ); $user = get_userdata( $user_id ); - if ( $user ) { $error = array( 'name' => $user->display_name, diff --git a/src/wp-admin/includes/post.php b/src/wp-admin/includes/post.php index d32f5d6889e58..a3094707590f6 100644 --- a/src/wp-admin/includes/post.php +++ b/src/wp-admin/includes/post.php @@ -1708,11 +1708,14 @@ function _wp_post_thumbnail_html( $thumbnail_id = null, $post = null ) { * * @since 2.5.0 * - * @param int|WP_Post $post ID or object of the post to check for editing. + * @param int|WP_Post $post ID or object of the post to check for editing. + * @param string $active_lock Optional. The active lock held by the current editing session. + * When provided, the current user's stale lock is treated as locked. * @return int|false ID of the user with lock. False if the post does not exist, post is not locked, - * the user with lock does not exist, or the post is locked by current user. + * the user with lock does not exist, or the post is locked by the current session. */ -function wp_check_post_lock( $post ) { +function wp_check_post_lock( $post, $active_lock = '' ) { + $post = get_post( $post ); if ( ! $post ) { @@ -1725,9 +1728,10 @@ function wp_check_post_lock( $post ) { return false; } - $lock = explode( ':', $lock ); - $time = $lock[0]; - $user = isset( $lock[1] ) ? (int) $lock[1] : (int) get_post_meta( $post->ID, '_edit_last', true ); + $lock_value = $lock; + $lock = explode( ':', $lock ); + $time = $lock[0]; + $user = isset( $lock[1] ) ? (int) $lock[1] : (int) get_post_meta( $post->ID, '_edit_last', true ); if ( ! get_userdata( $user ) ) { return false; @@ -1736,13 +1740,18 @@ function wp_check_post_lock( $post ) { /** This filter is documented in wp-admin/includes/ajax-actions.php */ $time_window = apply_filters( 'wp_check_post_lock_window', 150 ); - if ( $time && $time > time() - $time_window && get_current_user_id() !== $user ) { - return $user; + if ( $time && $time > time() - $time_window ) { + if ( get_current_user_id() !== $user ) { + return $user; + } + + if ( $active_lock && $active_lock !== $lock_value ) { + return $user; + } } return false; } - /** * Marks the post as currently being edited by the current user. * @@ -2093,7 +2102,9 @@ function post_preview() { $is_autosave = false; - if ( ! wp_check_post_lock( $post->ID ) && get_current_user_id() === (int) $post->post_author + $active_post_lock = $post_data['active_post_lock'] ?? ''; + + if ( ! wp_check_post_lock( $post->ID, $active_post_lock ) && get_current_user_id() === (int) $post->post_author && ( 'draft' === $post->post_status || 'auto-draft' === $post->post_status ) ) { $saved_post_id = edit_post(); @@ -2168,7 +2179,9 @@ function wp_autosave( $post_data ) { $post_data['post_category'] = explode( ',', $post_data['catslist'] ); } - if ( ! wp_check_post_lock( $post->ID ) && get_current_user_id() === (int) $post->post_author + $active_post_lock = $post_data['active_post_lock'] ?? ''; + + if ( ! wp_check_post_lock( $post->ID, $active_post_lock ) && get_current_user_id() === (int) $post->post_author && ( 'auto-draft' === $post->post_status || 'draft' === $post->post_status ) ) { // Drafts and auto-drafts are just overwritten by autosave for the same user if the post is not locked. diff --git a/tests/phpunit/tests/admin/includesPost.php b/tests/phpunit/tests/admin/includesPost.php index d9d39d8da727d..a598fb994790a 100644 --- a/tests/phpunit/tests/admin/includesPost.php +++ b/tests/phpunit/tests/admin/includesPost.php @@ -132,6 +132,7 @@ public function test__wp_translate_postdata_cap_checks_editor() { * @ticket 25272 */ public function test_edit_post_auto_draft() { + wp_set_current_user( self::$editor_id ); $post = self::factory()->post->create_and_get( array( 'post_status' => 'auto-draft' ) ); $this->assertSame( 'auto-draft', $post->post_status ); @@ -145,6 +146,31 @@ public function test_edit_post_auto_draft() { $this->assertSame( 'draft', get_post( $post->ID )->post_status ); } + /** + * @ticket 28288 + */ + public function test_wp_check_post_lock_allows_the_current_session() { + wp_set_current_user( self::$editor_id ); + + $lock = implode( ':', wp_set_post_lock( self::$post_id ) ); + + $this->assertFalse( wp_check_post_lock( self::$post_id, $lock ) ); + } + + /** + * @ticket 28288 + */ + public function test_wp_check_post_lock_detects_a_stale_same_user_lock() { + wp_set_current_user( self::$editor_id ); + + $stale_lock = ( time() - 1 ) . ':' . self::$editor_id; + $fresh_lock = time() . ':' . self::$editor_id; + + update_post_meta( self::$post_id, '_edit_lock', $fresh_lock ); + + $this->assertNotSame( $stale_lock, $fresh_lock ); + $this->assertSame( self::$editor_id, wp_check_post_lock( self::$post_id, $stale_lock ) ); + } /** * @ticket 30615 */ diff --git a/tests/phpunit/tests/ajax/wpAjaxHeartbeat.php b/tests/phpunit/tests/ajax/wpAjaxHeartbeat.php index 6759d9f0db466..93b1dd4694c20 100644 --- a/tests/phpunit/tests/ajax/wpAjaxHeartbeat.php +++ b/tests/phpunit/tests/ajax/wpAjaxHeartbeat.php @@ -86,6 +86,7 @@ public function test_autosave_post() { * Tests autosaving a locked post. */ public function test_autosave_locked_post() { + // Lock the post to another user. wp_set_current_user( self::$editor_id ); wp_set_post_lock( self::$post_id ); @@ -133,6 +134,53 @@ public function test_autosave_locked_post() { $this->assertStringContainsString( $md5, $autosave->post_content ); } + /** + * Tests autosaving a post from a stale lock held by the same user. + * + * @ticket 28288 + */ + public function test_autosave_stale_same_user_lock_creates_an_autosave() { + wp_set_current_user( self::$admin_id ); + + $stale_lock = ( time() - 1 ) . ':' . self::$admin_id; + $fresh_lock = time() . ':' . self::$admin_id; + $md5 = md5( uniqid() ); + $_POST = array( + 'action' => 'heartbeat', + '_nonce' => wp_create_nonce( 'heartbeat-nonce' ), + 'data' => array( + 'wp_autosave' => array( + 'post_id' => self::$post_id, + '_wpnonce' => wp_create_nonce( 'update-post_' . self::$post_id ), + 'post_content' => self::$post->post_content . PHP_EOL . $md5, + 'post_type' => 'post', + 'active_post_lock' => $stale_lock, + ), + ), + ); + + update_post_meta( self::$post_id, '_edit_lock', $fresh_lock ); + + $this->assertNotSame( $stale_lock, $fresh_lock ); + + try { + $this->_handleAjax( 'heartbeat' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + $response = json_decode( $this->_last_response, true ); + + $this->assertNotEmpty( $response['wp_autosave'] ); + $this->assertTrue( $response['wp_autosave']['success'] ); + + $post = get_post( self::$post_id ); + $this->assertStringNotContainsString( $md5, $post->post_content ); + + $autosave = wp_get_post_autosave( self::$post_id, get_current_user_id() ); + $this->assertNotEmpty( $autosave ); + $this->assertStringContainsString( $md5, $autosave->post_content ); + } /** * Tests with an invalid nonce. */