Make WordPress Core

Opened 6 weeks ago

Last modified 5 weeks ago

#65171 new defect (bug)

wp_check_post_lock_window filter values below 120s break post lock detection in backgrounded tabs

Reported by: katag9k's profile katag9k Owned by:
Milestone: Awaiting Review Priority: normal
Severity: normal Version:
Component: Editor Keywords: has-patch needs-unit-tests needs-testing
Focuses: ui, javascript Cc:

Description

Summary

When wp_check_post_lock_window is filtered to a value lower than the maximum heartbeat interval (120s), post lock detection silently breaks for users editing in backgrounded tabs. A second editor opening the same post sees no "Currently being edited" modal, walks into the editor, and on the next heartbeat takes over the lock — silently overwriting any unsaved changes the first editor had typed.

Steps to reproduce

Add the following to a theme or plugin:

  add_filter( 'wp_check_post_lock_window', function() { return 30; } );
  1. User A opens /wp-admin/post.php?post=N&action=edit. _edit_lock is set with the current timestamp.
  2. User A switches to a different tab/window so the editor tab is backgrounded. heartbeat.js overrides interval to 120000ms.
  3. After 30 seconds, the lock is considered expired by wp_check_post_lock() (uses the filtered window).
  4. User A's lock is not refreshed until 120s elapse (next backgrounded heartbeat).
  5. Between t=30s and t=120s, user B opens the same post. wp_check_post_lock() returns false. No takeover modal appears. B walks into the editor.
  6. B's first heartbeat claims the lock. A's next heartbeat receives lock_error and A is shown the takeover modal — losing any unsaved changes.

I reproduced this end-to-end by logging both heartbeat traffic and wp_check_post_lock_window calls. The 120s gap between A's heartbeats is exactly what heartbeat.js:512 produces; the lack of modal for B is exactly what post.php produces when the lock has aged past 30s.

Code paths

The defect is two pieces of core that have no shared awareness:

src/wp-admin/includes/post.php, wp_check_post_lock():

  $time_window = apply_filters( 'wp_check_post_lock_window', 150 );

  if ( $time && $time > time() - $time_window && get_current_user_id() !== $user ) {
      return $user;
  }

The filter has no documented minimum value.

src/js/_enqueues/wp/heartbeat.js, scheduleNextTick():

  if ( ! settings.hasFocus ) {
      interval = 120000; // 120 seconds. Post locks expire after 150 seconds.
  }

The 120000 constant is hardcoded; the comment confirms the design implicitly assumes the lock window is always 150s. There is no mechanism for a customised lock window to reach the JS layer.

src/wp-includes/general-template.php, wp_heartbeat_settings(), does not expose the lock window — it only sets ajaxurl and nonce.

Change History (4)

This ticket was mentioned in PR #11732 on WordPress/wordpress-develop by @sukhendu2002.


6 weeks ago
#1

  • Keywords has-patch added; needs-patch removed

#2 @adamsilverstein
5 weeks ago

  • Keywords needs-unit-tests needs-testing added

@sukhendu2002 - the patch looks good. It would be nice to add some unit tests, ideally reproducing the issue / fixed by the patch.

@katag9k thanks for opening the ticket. Can you check if the patch from @sukhendu2002 fixes the issue for you?

@adamsilverstein commented on PR #11732:


5 weeks ago
#3

Thanks for working on this @Sukhendu2002 — nice fix.

To help land this, I drafted PHPUnit coverage for the new post_lock_window setting in wp_heartbeat_set_suspension() on top of your branch (rebased on current trunk so the test file added in r60520 / 7938204 is present):

https://github.com/adamsilverstein/wordpress-develop/tree/trac-65171-tests

The single test commit is:
https://github.com/adamsilverstein/wordpress-develop/commit/2139778d90

It adds three cases to Tests_Admin_Includes_Misc_WpHeartbeatSetSuspension_Test:

  • post_lock_window is exposed as 150 on post.php and post-new.php.
  • post_lock_window is omitted on non-post screens (index.php, edit.php).
  • post_lock_window honors the wp_check_post_lock_window filter (verified with a custom value of 60).

Locally: OK (9 tests, 11 assertions).

Feel free to fold these into this PR if you'd like — happy to push to your branch directly if that's easier. Let me know.

@sukhendu2002 commented on PR #11732:


5 weeks ago
#4

Thanks @adamsilverstein! I've cherry-picked your test commit into my branch. All tests passing. Appreciate the coverage!

Note: See TracTickets for help on using tickets.