How it works
- In the posts/pages/CPT list table, select one or more applicable items and choose Copy to draft from the Bulk Actions dropdown (top or bottom), then Apply.
- Each selected item you have permission to edit is copied to a draft (title prefixed “Copy of …”). Items you can’t edit, or that aren’t an applicable post type, are silently skipped and reported in an admin notice.
- One item selected → you’re redirected straight to the edit screen for the new draft.
- Two or more items selected → you stay on the list table, with an admin notice reporting how many were copied (and how many were skipped, if any).
- Post meta and taxonomy terms are copied across.
_wp_old_date,_wp_old_slug, and ActivityPub’s federation-state meta (activitypub_status,activitypub_deleted_at) are excluded so new drafts don’t inherit stale redirect data or a false “already federated” flag. - A template tag,
lh_copy_to_draft( $post_id = null ), echoes a “Copy” link for use in themes (single-post copy, independent of the bulk action). If no$post_idis passed it falls back to the current post in the loop. - The same single-post copy link is also added to the “more” action modal via the
lh_more_modal_template_list_endfilter, if a theme/plugin provides that hook.
Permissions
The copy action is gated on WordPress’s own edit_post capability check for each specific post being copied — anyone who can already edit a given post can duplicate it. There is no separate admin settings screen, so install_plugins is not used here.
Security
- Bulk action (list table): WordPress core verifies the bulk-action nonce (
bulk-{plural}) beforehandle_bulk_actions-edit-{post_type}is ever called, so no additional nonce handling is required in the plugin. - Single-post link (template tag / “more” modal): the link is signed with
wp_nonce_url()using the actionlh_copy_to_draft-{post_id}, and the handler (admin_action_lh_copy_to_draft_save_as_new_post) verifies that nonce withcheck_admin_referer()before performing the capability check and copy.
Admin interfaces
No settings page. A Copy to draft option is added to the Bulk Actions dropdown on the list table screen for each applicable post type, and an admin notice reports the result after running it.
Database tables
None. Uses core wp_posts, wp_postmeta, and term relationship tables only, via wp_insert_post(), add_post_meta(), and wp_set_post_terms().
Cron jobs
None.
Extensibility for plugins with data outside postmeta
Some plugins store post-related data in their own custom tables rather than postmeta (e.g. an events plugin storing start/end dates for indexed queries). This plugin has no knowledge of any such table, and deliberately stays that way — but it provides a hook so other plugins can carry their own data across on copy:
do_action( 'lh_copy_to_draft_after_duplicate_post', $new_post_id, $post_object, $custom_overrides );
Fired after core fields, meta, and taxonomy terms have all been copied. $custom_overrides is an opaque array supplied by the caller — this plugin never inspects or validates its contents; it’s entirely up to the consuming plugin to interpret it and act.
If a consuming plugin’s data is also mirrored into postmeta, it can exclude that meta key from the default copy via lh_copy_to_draft_meta_defaults_filter (set the key to false), so it isn’t copied as a stale value before the consuming plugin re-derives the correct one in its own hook callback.