[wp-trac] [WordPress Trac] #65171: wp_check_post_lock_window filter values below 120s break post lock detection in backgrounded tabs

WordPress Trac noreply at wordpress.org
Tue May 5 21:12:28 UTC 2026


#65171: wp_check_post_lock_window filter values below 120s break post lock
detection in backgrounded tabs
----------------------------+-----------------------------
 Reporter:  katag9k         |      Owner:  (none)
     Type:  defect (bug)    |     Status:  new
 Priority:  normal          |  Milestone:  Awaiting Review
Component:  Editor          |    Version:
 Severity:  normal          |   Keywords:  needs-patch
  Focuses:  ui, javascript  |
----------------------------+-----------------------------
 == 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`.

-- 
Ticket URL: <https://core.trac.wordpress.org/ticket/65171>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list