From 367eea36ccbe1094eaf45673e62b9eadf2bde6a2 Mon Sep 17 00:00:00 2001 From: Mainul Hassan Date: Tue, 12 May 2026 19:15:16 +0600 Subject: [PATCH 1/4] Release the post lock when switching user from the post editing screen --- user-switching.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/user-switching.php b/user-switching.php index 225822f..cf3d726 100644 --- a/user-switching.php +++ b/user-switching.php @@ -73,6 +73,7 @@ public function init_hooks(): void { add_filter( 'ms_user_row_actions', [ $this, 'filter_user_row_actions' ], 10, 2 ); add_filter( 'login_message', [ $this, 'filter_login_message' ], 1 ); add_filter( 'removable_query_args', [ $this, 'filter_removable_query_args' ] ); + add_filter( 'wp_check_post_lock_window', [ $this, 'filter_post_lock_window_after_switch' ] ); add_action( 'wp_meta', [ $this, 'action_wp_meta' ] ); add_filter( 'plugin_row_meta', [ $this, 'filter_plugin_row_meta' ], 10, 2 ); add_action( 'wp_footer', [ $this, 'action_wp_footer' ] ); @@ -975,6 +976,24 @@ public function filter_removable_query_args( array $args ): array { ] ); } + /** + * Treats the post lock as expired on the post-edit screen immediately after a user switch. + * + * @param int $window The post lock window duration in seconds. + * @return int The filtered post lock window duration in seconds. + */ + public function filter_post_lock_window_after_switch( int $window ): int { + if ( empty( $_GET['user_switched'] ) ) { + return $window; + } + + if ( ( $_GET['action'] ?? '' ) !== 'edit' ) { + return $window; + } + + return 0; + } + /** * Returns the switch to or switch back URL for a given user. * From 0cfcf341e0837fb8843f5d7c11fb157c95c68d0a Mon Sep 17 00:00:00 2001 From: Mainul Hassan Date: Tue, 12 May 2026 21:47:34 +0600 Subject: [PATCH 2/4] Add integration tests for releasing the post lock after switching --- tests/integration/PostLockTest.php | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/integration/PostLockTest.php diff --git a/tests/integration/PostLockTest.php b/tests/integration/PostLockTest.php new file mode 100644 index 0000000..eae45fe --- /dev/null +++ b/tests/integration/PostLockTest.php @@ -0,0 +1,57 @@ + 'true', + 'action' => 'edit', + ]; + + self::assertSame( 0, apply_filters( 'wp_check_post_lock_window', 150 ) ); + } + + public function testPostLockWindowIsUnchangedWithoutUserSwitched(): void { + $_GET = [ + 'action' => 'edit', + ]; + + self::assertSame( 150, apply_filters( 'wp_check_post_lock_window', 150 ) ); + } + + public function testPostLockWindowIsUnchangedWhenActionIsNotEdit(): void { + $_GET = [ + 'user_switched' => 'true', + ]; + + self::assertSame( 150, apply_filters( 'wp_check_post_lock_window', 150 ) ); + } + + public function testStalePostLockIsTreatedAsExpiredAfterSwitch(): void { + $current = self::$testers['admin']; + $previous = self::$users['editor']; + + $post = self::factory()->post->create_and_get(); + self::assertInstanceOf( 'WP_Post', $post ); + + wp_set_current_user( $current->ID ); + + // Simulate a fresh lock left behind by the user being switched away from. + update_post_meta( $post->ID, '_edit_lock', time() . ':' . $previous->ID ); + + // Without the after-switch query args, the lock is considered active. + $_GET = []; + self::assertSame( $previous->ID, wp_check_post_lock( $post->ID ) ); + + // With the after-switch query args, the lock is treated as expired. + $_GET = [ + 'user_switched' => 'true', + 'action' => 'edit', + ]; + self::assertFalse( wp_check_post_lock( $post->ID ) ); + } +} From 51ab8376ccfb259c1f7bf35646ab17adb376c9e0 Mon Sep 17 00:00:00 2001 From: Mainul Hassan Date: Wed, 13 May 2026 00:10:09 +0600 Subject: [PATCH 3/4] Only release the post lock after switching back --- tests/integration/PostLockTest.php | 18 +++++++++--------- user-switching.php | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/integration/PostLockTest.php b/tests/integration/PostLockTest.php index eae45fe..9862fc4 100644 --- a/tests/integration/PostLockTest.php +++ b/tests/integration/PostLockTest.php @@ -3,19 +3,19 @@ namespace UserSwitching\Tests; /** - * @covers \user_switching::filter_post_lock_window_after_switch + * @covers \user_switching::filter_post_lock_window_after_switch_back */ final class PostLockTest extends Test { - public function testPostLockWindowIsZeroedOnPostEditScreenAfterSwitch(): void { + public function testPostLockWindowIsZeroedOnPostEditScreenAfterSwitchBack(): void { $_GET = [ - 'user_switched' => 'true', + 'switched_back' => 'true', 'action' => 'edit', ]; self::assertSame( 0, apply_filters( 'wp_check_post_lock_window', 150 ) ); } - public function testPostLockWindowIsUnchangedWithoutUserSwitched(): void { + public function testPostLockWindowIsUnchangedWithoutSwitchedBack(): void { $_GET = [ 'action' => 'edit', ]; @@ -25,13 +25,13 @@ public function testPostLockWindowIsUnchangedWithoutUserSwitched(): void { public function testPostLockWindowIsUnchangedWhenActionIsNotEdit(): void { $_GET = [ - 'user_switched' => 'true', + 'switched_back' => 'true', ]; self::assertSame( 150, apply_filters( 'wp_check_post_lock_window', 150 ) ); } - public function testStalePostLockIsTreatedAsExpiredAfterSwitch(): void { + public function testStalePostLockIsTreatedAsExpiredAfterSwitchBack(): void { $current = self::$testers['admin']; $previous = self::$users['editor']; @@ -43,13 +43,13 @@ public function testStalePostLockIsTreatedAsExpiredAfterSwitch(): void { // Simulate a fresh lock left behind by the user being switched away from. update_post_meta( $post->ID, '_edit_lock', time() . ':' . $previous->ID ); - // Without the after-switch query args, the lock is considered active. + // Without the after-switch-back query args, the lock is considered active. $_GET = []; self::assertSame( $previous->ID, wp_check_post_lock( $post->ID ) ); - // With the after-switch query args, the lock is treated as expired. + // With the after-switch-back query args, the lock is treated as expired. $_GET = [ - 'user_switched' => 'true', + 'switched_back' => 'true', 'action' => 'edit', ]; self::assertFalse( wp_check_post_lock( $post->ID ) ); diff --git a/user-switching.php b/user-switching.php index cf3d726..99f869d 100644 --- a/user-switching.php +++ b/user-switching.php @@ -73,7 +73,7 @@ public function init_hooks(): void { add_filter( 'ms_user_row_actions', [ $this, 'filter_user_row_actions' ], 10, 2 ); add_filter( 'login_message', [ $this, 'filter_login_message' ], 1 ); add_filter( 'removable_query_args', [ $this, 'filter_removable_query_args' ] ); - add_filter( 'wp_check_post_lock_window', [ $this, 'filter_post_lock_window_after_switch' ] ); + add_filter( 'wp_check_post_lock_window', [ $this, 'filter_post_lock_window_after_switch_back' ] ); add_action( 'wp_meta', [ $this, 'action_wp_meta' ] ); add_filter( 'plugin_row_meta', [ $this, 'filter_plugin_row_meta' ], 10, 2 ); add_action( 'wp_footer', [ $this, 'action_wp_footer' ] ); @@ -977,13 +977,13 @@ public function filter_removable_query_args( array $args ): array { } /** - * Treats the post lock as expired on the post-edit screen immediately after a user switch. + * Treats the post lock as expired on the post-edit screen immediately after switching back. * * @param int $window The post lock window duration in seconds. * @return int The filtered post lock window duration in seconds. */ - public function filter_post_lock_window_after_switch( int $window ): int { - if ( empty( $_GET['user_switched'] ) ) { + public function filter_post_lock_window_after_switch_back( int $window ): int { + if ( empty( $_GET['switched_back'] ) ) { return $window; } From ce0b086ec961b297f49077326ce078ddf2b0fd0d Mon Sep 17 00:00:00 2001 From: Mainul Hassan Date: Wed, 13 May 2026 00:10:38 +0600 Subject: [PATCH 4/4] Add acceptance test for releasing the post lock after switching back --- tests/acceptance/PostLock.spec.ts | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/acceptance/PostLock.spec.ts diff --git a/tests/acceptance/PostLock.spec.ts b/tests/acceptance/PostLock.spec.ts new file mode 100644 index 0000000..5abb21a --- /dev/null +++ b/tests/acceptance/PostLock.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from './utils/test-setup'; + +test.describe( 'Post Lock', () => { + let sharedPostId: string; + let editorId: string; + + test.beforeAll( async ( { globalUtils } ) => { + await globalUtils.installWordPress(); + sharedPostId = globalUtils.runWPCLICommand( 'post create --post_title="Test Post" --post_status=publish --porcelain' ); + editorId = globalUtils.runWPCLICommand( 'user create editor editor@example.com --role=editor --user_pass=password --porcelain' ); + } ); + + test( 'Switch back from a post editing screen releases the post lock', { + annotation: { + type: 'user-story', + description: 'As a user who has switched accounts to edit a post, I want to switch back without seeing the post locked modal, in order to continue editing' + } + }, async ( { + page, + admin, + userSwitching, + globalUtils, + } ) => { + await userSwitching.loginViaPage( 'admin', 'password' ); + + // Simulate a fresh lock left behind by the editor user. + const setStaleLock = () => { + const now = Math.floor( Date.now() / 1000 ); + globalUtils.runWPCLICommand( `post meta update ${sharedPostId} _edit_lock "${now}:${editorId}"` ); + }; + + // Without the after-switch-back query args, the lock modal should be visible. + setStaleLock(); + await admin.visitAdminPage( 'post.php', `post=${sharedPostId}&action=edit` ); + await expect( page.getByText( 'This post is already being edited' ) ).toBeVisible(); + + // With the after-switch-back query args, the lock modal should not be visible. + setStaleLock(); + await admin.visitAdminPage( 'post.php', `post=${sharedPostId}&action=edit&user_switched=true&switched_back=true` ); + await expect( page.getByText( 'This post is already being edited' ) ).not.toBeVisible(); + } ); +} );