@prefix sioc: <http://rdfs.org/sioc/ns#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix content: <http://purl.org/rss/1.0/modules/content/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://lhero.org/?post_type=lh-portfolio&#038;p=146681>
  a sioc:Post ;
  dc:title "How it works" ;
  dcterms:identifier 146681 ;
  dc:modified "2026-08-31T22:14:36Z"^^xsd:dateTime ;
  dc:created "2026-08-31T22:14:36Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-copy-to-draft/how-it-works/> ;
  sioc:has_creator <https://lhero.org/author/1/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded """<h2>How it works</h2>
<ol>
<li>In the posts/pages/CPT list table, select one or more applicable items and choose <strong>Copy to draft</strong> from the Bulk Actions dropdown (top or bottom), then Apply.</li>
<li>Each selected item you have permission to edit is copied to a draft (title prefixed &#8220;Copy of &#8230;&#8221;). Items you can&#8217;t edit, or that aren&#8217;t an applicable post type, are silently skipped and reported in an admin notice.</li>
<li><strong>One item selected</strong> &#8594; you&#8217;re redirected straight to the edit screen for the new draft.</li>
<li><strong>Two or more items selected</strong> &#8594; you stay on the list table, with an admin notice reporting how many were copied (and how many were skipped, if any).</li>
<li>Post meta and taxonomy terms are copied across. <code>_wp_old_date</code>, <code>_wp_old_slug</code>, and ActivityPub&#8217;s federation-state meta (<code>activitypub_status</code>, <code>activitypub_deleted_at</code>) are excluded so new drafts don&#8217;t inherit stale redirect data or a false &#8220;already federated&#8221; flag.</li>
<li>A template tag, <code>lh_copy_to_draft( $post_id = null )</code>, echoes a &#8220;Copy&#8221; link for use in themes (single-post copy, independent of the bulk action). If no <code>$post_id</code> is passed it falls back to the current post in the loop.</li>
<li>The same single-post copy link is also added to the &#8220;more&#8221; action modal via the <code>lh_more_modal_template_list_end</code> filter, if a theme/plugin provides that hook.</li>
</ol>
<h2>Permissions</h2>
<p>The copy action is gated on WordPress&#8217;s own <code>edit_post</code> capability check for each specific post being copied &#8212; anyone who can already edit a given post can duplicate it. There is no separate admin settings screen, so <code>install_plugins</code> is not used here.</p>
<h2>Security</h2>
<ul>
<li><strong>Bulk action</strong> (list table): WordPress core verifies the bulk-action nonce (<code>bulk-{plural}</code>) before <code>handle_bulk_actions-edit-{post_type}</code> is ever called, so no additional nonce handling is required in the plugin.</li>
<li><strong>Single-post link</strong> (template tag / &#8220;more&#8221; modal): the link is signed with <code>wp_nonce_url()</code> using the action <code>lh_copy_to_draft-{post_id}</code>, and the handler (<code>admin_action_lh_copy_to_draft_save_as_new_post</code>) verifies that nonce with <code>check_admin_referer()</code> before performing the capability check and copy.</li>
</ul>
<h2>Admin interfaces</h2>
<p>No settings page. A <strong>Copy to draft</strong> 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.</p>
<h2>Database tables</h2>
<p>None. Uses core <code>wp_posts</code>, <code>wp_postmeta</code>, and term relationship tables only, via <code>wp_insert_post()</code>, <code>add_post_meta()</code>, and <code>wp_set_post_terms()</code>.</p>
<h2>Cron jobs</h2>
<p>None.</p>
<h2>Extensibility for plugins with data outside postmeta</h2>
<p>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 &#8212; but it provides a hook so other plugins can carry their own data across on copy:</p>
<pre><code>do_action( 'lh_copy_to_draft_after_duplicate_post', $new_post_id, $post_object, $custom_overrides );</code></pre>
<p>Fired after core fields, meta, and taxonomy terms have all been copied. <code>$custom_overrides</code> is an opaque array supplied by the caller &#8212; this plugin never inspects or validates its contents; it&#8217;s entirely up to the consuming plugin to interpret it and act.</p>
<p>If a consuming plugin&#8217;s data is also mirrored into postmeta, it can exclude that meta key from the default copy via <code>lh_copy_to_draft_meta_defaults_filter</code> (set the key to <code>false</code>), so it isn&#8217;t copied as a stale value before the consuming plugin re-derives the correct one in its own hook callback.</p>
"""^^rdf:XMLLiteral ;
  sioc:content """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 &#8220;Copy of &#8230;&#8221;). Items you can&#8217;t edit, or that aren&#8217;t an applicable post type, are silently skipped and reported in an admin notice.
One item selected &#8594; you&#8217;re redirected straight to the edit screen for the new draft.
Two or more items selected &#8594; 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&#8217;s federation-state meta (activitypub_status, activitypub_deleted_at) are excluded so new drafts don&#8217;t inherit stale redirect data or a false &#8220;already federated&#8221; flag.
A template tag, lh_copy_to_draft( $post_id = null ), echoes a &#8220;Copy&#8221; link for use in themes (single-post copy, independent of the bulk action). If no $post_id is passed it falls back to the current post in the loop.
The same single-post copy link is also added to the &#8220;more&#8221; action modal via the lh_more_modal_template_list_end filter, if a theme/plugin provides that hook.

Permissions
The copy action is gated on WordPress&#8217;s own edit_post capability check for each specific post being copied &#8212; 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}) before handle_bulk_actions-edit-{post_type} is ever called, so no additional nonce handling is required in the plugin.
Single-post link (template tag / &#8220;more&#8221; modal): the link is signed with wp_nonce_url() using the action lh_copy_to_draft-{post_id}, and the handler (admin_action_lh_copy_to_draft_save_as_new_post) verifies that nonce with check_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 &#8212; 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 &#8212; this plugin never inspects or validates its contents; it&#8217;s entirely up to the consuming plugin to interpret it and act.
If a consuming plugin&#8217;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&#8217;t copied as a stale value before the consuming plugin re-derives the correct one in its own hook callback.
""" ;
  sioc:topic <https://lhero.org/lh_portfolio-type/doc-section/>, <https://lhero.org/?taxonomy=author&term=cap-1> .

<https://lhero.org/author/1/#account> rdfs:seeAlso <https://lhero.org/author/1/?feed=lhrdf&format=turtle> .
<https://lhero.org/lh_portfolio-type/doc-section/> rdfs:seeAlso <https://lhero.org/lh_portfolio-type/doc-section/?feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
