A lock is a flag stored in post meta, beside the post status rather than instead of it. A locked post can be published, private, draft or any custom status, and front-end queries, sitemaps and feeds treat it exactly as they would an unlocked post.
Storage
_lh_post_locking_locked: present (as 1) only while the post is locked, and deleted on unlock, so finding locked posts is a query on an indexed meta key._lh_post_locking_locked_byand_lh_post_locking_locked_at: who locked the post and when (GMT), cleared on unlock.
The flag is registered for the REST API so the block editor can read and save it. Only users with the lock capability can write it.
Enforcement
Enforcement has two layers. Both apply to anyone without the lock capability (manage_options by default).
Capabilities. A map_meta_cap filter adds do_not_allow to edit_post, edit_page, delete_post, delete_page and publish_post on a locked post. Edit and Trash links disappear, and the editor and REST API refuse changes with a proper permission error. This layer only works when the caller asks for one of those per-post capabilities.
Write path. Since 0.3.0, a backstop refuses the write itself, whatever route it takes (wp-admin, REST, MCP abilities, cron or another plugin), as long as it goes through the WordPress APIs. This catches callers that only check a type-level capability such as edit_posts.
wp_insert_post_empty_contentrefuses any update to a locked post: title, content, excerpt, status, parent, slug, and revision restores.pre_trash_postandpre_delete_postrefuse trashing and deleting it.add_post_metadata,update_post_metadataanddelete_post_metadatarefuse changes to its public (non-underscore) custom fields. Underscore-prefixed meta stays writable, so internal bookkeeping such as edit locks, caches and the lock flag itself keeps working;lh_post_locking_is_frozen_meta_keychanges the rule.
Comment moderation is deliberately left alone. Core checks edit_comment through edit_post, so the plugin recomputes it without the lock, and editors can still approve, edit or remove comments on a locked post.
For callers
A write ability that acts on one post should check edit_post or delete_post on that post before writing, so it can give a proper “locked” error; the backstop is the safety net. When the backstop refuses an update, core reports the misleading “Content, title, and excerpt are empty.” Callers can turn that into a clear message with these static methods on LH_Post_Locking_Plugin:
last_refused_post(): the ID of the last post whose write was refused in this request, or 0.locked_error( $post_id ): aWP_Error(codelh_post_locking_locked, status 403) explaining that the post is locked.is_locked( $post ),get_lock_details( $post_id )andblocks_write( $post_id ).run_bypassing_lock( $callback ): runs a callback with enforcement switched off, for legitimate background updaters.
Known limits
- Terms are not frozen. Core has no filter that can veto
wp_set_object_terms(), so categories, tags and other terms on a locked post can still be changed by code that does not checkedit_postfirst. - Direct database writes bypass everything. Anything that writes through
$wpdbinstead of the WordPress APIs is not seen. - Signed-out contexts are refused. Cron and other requests with no user fail the lock capability check, so they cannot change a locked post. That is the intended default for a frozen post. A legitimate background updater uses
run_bypassing_lock()or thelh_post_locking_bypass_lockfilter. - Scheduled publishing still works. WordPress publishes a scheduled post with a direct status change, not an update, so a locked future post is still published on time. This is intended.
- Comment counts and pings are also updated directly by core and are unaffected.
Interface
- Block editor: a Lock editing checkbox in the post status panel, under Status and Visibility.
- Classic editor: the same checkbox in the Publish box.
- List tables: a Locked post state after the title, a Locked view with a count, and Lock editing and Unlock editing bulk actions.
- MCP: get-lock, set-lock and list-locked (see Abilities).
Users without the lock capability see none of the editor or list-table controls.
Lockable post types
Every post type with an admin screen and custom-fields support, except attachments. Custom-fields support is needed for the meta to reach the block editor.
Copies
A post copied with LH Copy to Draft starts unlocked, with no lock history, and no unlock event fires for it.