Digital Sovereignty for everyone

How it works

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_by and _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_content refuses any update to a locked post: title, content, excerpt, status, parent, slug, and revision restores.
  • pre_trash_post and pre_delete_post refuse trashing and deleting it.
  • add_post_metadata, update_post_metadata and delete_post_metadata refuse 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_key changes 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 ): a WP_Error (code lh_post_locking_locked, status 403) explaining that the post is locked.
  • is_locked( $post ), get_lock_details( $post_id ) and blocks_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 check edit_post first.
  • Direct database writes bypass everything. Anything that writes through $wpdb instead 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 the lh_post_locking_bypass_lock filter.
  • 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.

If you like this plugin please consider making a donation to further development

Install LHero.org

lhero.org

Open it straight from your home screen, full screen and quicker to load.

  1. Tap the Share button . If you cannot see it, tap ••• first.
  2. Scroll down and tap Add to Home Screen.
  3. Tap Add.
  1. Tap the browser menu â‹®.
  2. Tap Install app or Add to Home screen.
  1. In the menu bar, choose File, then Add to Dock.

Use your browser's menu to install this site or add it to your home screen.