﻿id	summary	reporter	owner	description	type	status	priority	milestone	component	version	severity	resolution	keywords	cc	focuses
65171	wp_check_post_lock_window filter values below 120s break post lock detection in backgrounded tabs	katag9k		"== 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`."	defect (bug)	new	normal	Awaiting Review	Editor		normal		has-patch		ui, javascript
