Duplicate Posts & Pages without a Plugin! The Simple Post Duplicator!

Duplicate posts pages without a plugin the simple post duplicator

Duplicating posts or pages in WordPress used to be simple. Yea, you can install a plugin, and there are some good ones. Years ago I used to use a great free one until it got bought by a bigger company and became bloated and tried to upsell me on other plugins. So, I wrote this code. And now… everything is simple again!

Duplicate posts pages without a plugin the simple post duplicator

What it does…

This is a WordPress code snippet that allows users to duplicate posts and pages seamlessly. It enables the creation of duplicate drafts, ensuring that the original content remains untouched (including taxonomy terms and post meta). This straightforward solution avoids the complexities of plugins while providing an efficient post duplication feature.

Why it does it…

This code was created as an alternative to bloated plugins that compromise simplicity and attempt to sell additional features. This Simple Post Duplicator code snippet aims to bring back the ease of duplicating posts and pages in WordPress without unnecessary complexities or upsells. It offers a lightweight and reliable solution for users who prefer a straightforward approach.

How it does it…

Orig: This code snippet gives you a PHP class (which means you can use the Code Snippets Pro ❤️ plugin or just copy/paste the code into your theme’s functions.php file) and will only run the code if the class doesn’t already exist.

This works on posts, pages and even custom post types (CPTs). It consists of two main methods:

  1. duplicatePostAsDraft(): This method handles the post duplication process. It verifies the user’s permission, checks for the presence of a post to duplicate, performs nonce verification for security, creates a duplicate draft post with identical content, taxonomy terms, and post meta information, and redirects the user to the new draft’s edit screen.
  2. addDuplicatePostLink(): This method adds a “Duplicate” link to the action list of individual posts in the WordPress admin panel. It ensures that only users with the appropriate permissions can see and use the link. Upon clicking the “Duplicate” link, the user is directed to the duplicate post’s edit screen.

Ahhh… concise and well-structured code to ensure a hassle-free post duplication experience! You’re welcome.

See the code…

<?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();
}

See the code on github…

Leave a Reply

Your email address will not be published. Required fields are marked *