<?php
/**
* SimplePostDuplicator - Adds a Duplicate option to Posts & Pages [SnipSnip.pro]
*
* HARDENED 2026-08-07: the capability check was the blanket `edit_posts`, which every
* Contributor holds. Because the handler then copies the source post's content and
* re-authors the duplicate to the requesting user, any Contributor could duplicate —
* and thereby read — another author's private or draft post. Both the handler and the
* row-action link now check `edit_post` against the SPECIFIC post ID, which is the
* per-object capability WordPress maps through map_meta_cap.
*
* Also: the duplicate link is no longer offered for posts the user cannot read, and the
* nonce action is now a fixed string rather than basename(__FILE__) (which is the
* evaluating file, not this snippet, and is not stable across runners).
*/
if (!class_exists('SimplePostDuplicator')) {
class SimplePostDuplicator {
const NONCE_ACTION = 'simple_post_duplicator_duplicate';
public function __construct() {
add_action('admin_action_simple_post_duplicator_duplicate_post_as_draft', array($this, 'duplicatePostAsDraft'));
add_filter('post_row_actions', array($this, 'addDuplicatePostLink'), 10, 2);
add_filter('page_row_actions', array($this, 'addDuplicatePostLink'), 10, 2);
}
/**
* Function for post duplication. Duplicates appear as drafts. Users are redirected to the edit screen.
*/
public function duplicatePostAsDraft() {
if (!(isset($_GET['post']) || isset($_POST['post']) || (isset($_REQUEST['action']) && 'simple_post_duplicator_duplicate_post_as_draft' === $_REQUEST['action']))) {
wp_die('No post to duplicate has been supplied!');
}
// Nonce verification
if (!isset($_GET['duplicate_nonce']) || !wp_verify_nonce($_GET['duplicate_nonce'], self::NONCE_ACTION)) {
wp_die('Invalid security token.');
}
// Get the original post ID
$post_id = (isset($_GET['post'])) ? absint($_GET['post']) : absint($_POST['post']);
// Get the original post data
$post = get_post($post_id);
if (!$post) {
wp_die('You do not have permission to duplicate this item.');
}
// Permission must be checked against THIS post, not the blanket edit_posts
// capability — otherwise a Contributor can duplicate anyone's private drafts.
if (!current_user_can('edit_post', $post_id)) {
wp_die('You do not have permission to duplicate this item.');
}
// The duplicate is re-authored to the current user, so they must also be
// allowed to create posts of this type.
$post_type_object = get_post_type_object($post->post_type);
if (!$post_type_object || !current_user_can($post_type_object->cap->create_posts)) {
wp_die('You do not have permission to duplicate this item.');
}
// Prepare the new post data
$args = array(
'comment_status' => $post->comment_status,
'ping_status' => $post->ping_status,
'post_author' => get_current_user_id(),
'post_content' => $post->post_content,
'post_excerpt' => $post->post_excerpt,
'post_name' => $post->post_name,
'post_parent' => $post->post_parent,
'post_password' => $post->post_password,
'post_status' => 'draft',
'post_title' => $post->post_title,
'post_type' => $post->post_type,
'to_ping' => $post->to_ping,
'menu_order' => $post->menu_order
);
// Insert the new post
$new_post_id = wp_insert_post($args);
if ($new_post_id) {
// Duplicate the taxonomy terms
$taxonomies = get_object_taxonomies($post->post_type);
foreach ($taxonomies as $taxonomy) {
$post_terms = wp_get_object_terms($post_id, $taxonomy, array('fields' => 'slugs'));
wp_set_object_terms($new_post_id, $post_terms, $taxonomy, false);
}
// Duplicate the post meta
global $wpdb;
$post_meta_infos = $wpdb->get_results($wpdb->prepare("SELECT meta_key, meta_value FROM $wpdb->postmeta WHERE post_id = %d", $post_id));
foreach ($post_meta_infos as $meta_info) {
$meta_key = $meta_info->meta_key;
if ($meta_key === '_wp_old_slug') {
continue;
}
$meta_value = $meta_info->meta_value;
update_post_meta($new_post_id, $meta_key, $meta_value);
}
// Redirect to the edit post screen for the new draft
wp_redirect(admin_url('post.php?action=edit&post=' . $new_post_id));
exit;
} else {
wp_die('Post creation failed, could not duplicate the original post.');
}
}
/**
* Add the duplicate link to the action list for post_row_actions
*/
public function addDuplicatePostLink($actions, $post) {
if (isset($post->ID) && current_user_can('edit_post', $post->ID)) {
$duplicate_url = wp_nonce_url(admin_url('admin.php?action=simple_post_duplicator_duplicate_post_as_draft&post=' . $post->ID), self::NONCE_ACTION, 'duplicate_nonce');
$actions['duplicate'] = '<a href="' . esc_url($duplicate_url) . '" title="Duplicate this item" rel="permalink">Duplicate</a>';
}
return $actions;
}
}
// Instantiate the class
new SimplePostDuplicator();
}
Leave a Reply