Plugin Directory

Changeset 3265459


Ignore:
Timestamp:
04/02/2025 07:12:39 AM (9 months ago)
Author:
carlbensy16
Message:

Release version 1.0.3 – updated plugin, fixed assets

Location:
just-duplicate
Files:
15 added
8 edited

Legend:

Unmodified
Added
Removed
  • just-duplicate/trunk/README.txt

    r3252339 r3265459  
    77Support URI: https://justthere.co.uk/plugins/just-duplicate/support
    88Feature Request URI: https://justthere.co.uk/plugins/just-duplicate/feature-request
    9 Donate link: https://justthere.co.uk/donate
     9Donate link: https://justthere.co.uk/plugins/support-us/
    1010Requires at least: 6.0
    1111Tested up to: 6.7
    12 Stable tag: 1.0.2
    13 Version: 1.0.2
     12Stable tag: 1.0.3
     13Version: 1.0.3
    1414Requires PHP: 7.4
    1515License: GNU General Public License v3.0 or later
    1616License URI: http://www.gnu.org/licenses/gpl.html
    17 Tags: post duplicator, page duplicator, WooCommerce product duplicator, duplicator
     17Tags: duplicate posts, duplicate pages, duplicate WooCommerce products, WordPress duplicator, content duplicator, post duplicator, page duplicator, WooCommerce duplicator, batch duplication, role-based access control
    1818
    19 Easily duplicate pages, posts, custom post types, and WooCommerce products with Just Duplicate. A simple, fast, and customizable solution for WordPress content duplication.
     19Easily duplicate WordPress pages, posts, custom post types, and WooCommerce products with one click.
    2020
    2121== Description ==
    22 Just Duplicate makes duplicating WordPress content effortless. Whether you're managing a blog, an online store, or a custom content-driven website, this plugin allows you to quickly duplicate pages, posts, custom post types, menus, media, and WooCommerce products with one click.
     22**Just Duplicate** is the ultimate WordPress plugin for duplicating content effortlessly. Whether you're managing a blog, an e-commerce store, or a custom content-driven website, this plugin empowers you to duplicate pages, posts, custom post types, menus, media, and WooCommerce products with just one click.
    2323
    24 Built for efficiency, Just Duplicate saves time by eliminating repetitive content creation tasks. With role-based access control, you can restrict duplication permissions, ensuring only authorized users can duplicate content. Additionally, the plugin allows batch duplication for handling multiple items at once.
     24Designed for speed and efficiency, **Just Duplicate** eliminates repetitive tasks, saving you time and effort. With advanced features like role-based access control, batch duplication, and customizable settings, you can tailor the duplication process to your needs. The plugin is lightweight, fast, and fully compatible with popular page builders like Elementor, Divi, and Gutenberg.
    2525
    26 Customize duplication settings to include post meta, taxonomies, attachments, and custom fields. Apply default prefixes and suffixes to duplicated content to keep everything organized.
    27 
    28 Compatible with major page builders like Elementor, Divi, and Gutenberg, Just Duplicate works seamlessly with any WordPress setup.
    29 
    30 👉 Simplify content management with Just Duplicate – the easiest way to duplicate WordPress content.
     26👉 **Simplify your WordPress content management with Just Duplicate – the easiest and fastest way to duplicate WordPress content.**
    3127
    3228**Key Features:**
    33 ✅ **One-Click Duplication** – Easily duplicate posts, pages, custom post types, and WooCommerce products. 
    34 🚀 **Batch Duplication** – Duplicate multiple items at once to streamline workflow
     29✅ **One-Click Duplication** – Quickly duplicate posts, pages, custom post types, and WooCommerce products. 
     30🚀 **Batch Duplication** – Duplicate multiple items simultaneously to boost productivity
    3531⚙️ **Customizable Duplication Settings** – Choose what to copy: meta fields, taxonomies, attachments, featured images, and more. 
    36 🔑 **Role-Based Access Control** – Restrict duplication permissions to specific user roles
    37 📂 **Automatic Prefix/Suffix** – Add default text to duplicated items to differentiate them easily
    38 🛠️ **Seamless Compatibility** – Works with major page builders (Elementor, Divi, Gutenberg) and themes. 
    39 📢 **No Bloat, Just Speed** – Lightweight and optimized for fast performance. 
     32🔑 **Role-Based Access Control** – Restrict duplication permissions to specific user roles for better security
     33📂 **Automatic Prefix/Suffix** – Add default text to duplicated items for easy identification
     34🛠️ **Seamless Compatibility** – Works flawlessly with Elementor, Divi, Gutenberg, and other major page builders and themes. 
     35📢 **Lightweight and Optimized** – No bloat, just speed and performance. 
    4036
    41 For more information about Just Duplicate, visit [our website](https://justthere.co.uk/plugins/just-duplicate).
     37**Why Choose Just Duplicate?**
     38- Save time by automating repetitive content creation tasks.
     39- Maintain consistency across your website with accurate duplication.
     40- Enhance workflow with advanced features like batch duplication and role-based access control.
     41
     42For more details, visit [our website](https://justthere.co.uk/plugins/just-duplicate).
    4243
    4344== Installation ==
     
    5657
    5758= How do I duplicate a post or page? =
    58 Once activated, hover over any post or page in your WordPress dashboard. You'll see a "Duplicate" option. Click it, and a new draft will be created instantly.
     59Hover over any post or page in your WordPress dashboard. You'll see a "Duplicate" option. Click it, and a new draft will be created instantly.
    5960
    6061= Can I duplicate WooCommerce products? =
     
    7778
    7879== Screenshots ==
    79 1. Plugin settings page 
    80 2. Duplicate button on a post/page 
    81 3. Role-based access settings 
     801. Duplicate button on page tab.
     812. Preview duplication button on page tab.
     823. Actual preview of duplication.
     834. Duplication report log.
     845. Just Duplicate general settings.
    8285
    8386== Changelog ==
     87
     88= 1.0.3 =
     89- Add: Media duplication functionality with AJAX support.
     90- Add: Navigation menu duplication with parent-child relationship handling.
     91- Add: Preview modal for duplication with enhanced UI/UX.
     92- Add: Localization support for admin scripts.
     93- Add: Scheduled duplication feature for posts/pages.
     94- Add: Rollback functionality to undo the last duplicated post.
     95- Add: Duplication logging for media and menus.
     96- Add: Admin notice with a rollback link after duplication.
     97- Add: Transient storage for tracking the last duplicated post ID.
     98- Add: Scheduled Duplication feature to duplicate posts/pages at a specific time.
     99- Add: Rollback functionality to undo the last duplicated post.
     100- Add: Admin notice with a rollback link after duplication.
     101- Add: Transient storage for tracking the last duplicated post ID.
     102- Fix: Compatibility with Elementor and other page builders.
     103- Fix: Nonce verification for AJAX actions.
     104- Fix: Improved error handling for duplication failures.
     105- Fix: Page builder compatibility.
     106- Update: Enhanced admin settings page with tabbed navigation.
     107- Update: Improved duplication logic for better performance and reliability.
     108- Update: Admin styles for a modern and consistent look.
     109- Update: Documentation links and support resources.
     110- Update: Improved admin notices for better user feedback.
     111- Update: Enhanced logging and reporting features.
     112- Improvement: Enhanced duplication logic for better performance and reliability.
     113- Improvement: Added nonce verification for rollback actions.
    84114
    85115= 1.0.2 =
     
    129159
    130160== Upgrade Notice ==
    131 = 1.0.2 =
     161= 1.0. =
    132162- No Major Changes (Safe Update)
    133163
  • just-duplicate/trunk/assets/css/admin-style.css

    r3252339 r3265459  
    1616    --jd-border-radius: 3px;
    1717    --jd-text-color: #23282d;              /* Default admin text colour */
     18    --jd-font-family: 'Arial', sans-serif; /* Modern font */
     19    --jd-box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow */
     20    --jd-hover-color: #005a8c; /* Darker blue for hover effects */
     21}
     22
     23/* Apply modern font */
     24body {
     25    font-family: var(--jd-font-family);
    1826}
    1927
     
    2533    border-radius: var(--jd-border-radius);
    2634    color: var(--jd-text-color);
     35    box-shadow: var(--jd-box-shadow);
    2736}
    2837
     
    5160    transition: background var(--jd-transition);
    5261    color: var(--jd-text-color);
     62    border-radius: var(--jd-border-radius) var(--jd-border-radius) 0 0;
    5363}
    5464
    5565.jd-tabs .jd-tab:hover {
    5666    background: var(--jd-tab-bg-hover);
     67    color: var(--jd-hover-color);
    5768}
    5869
     
    6071    background: var(--jd-tab-bg-active);
    6172    border-bottom: 1px solid var(--jd-tab-bg-active);
     73    color: var(--jd-hover-color);
    6274}
    6375
     
    98110    max-height: 80vh;              /* Limit height and allow scroll if content is long */
    99111    overflow-y: auto;
    100     box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
     112    box-shadow: var(--jd-box-shadow);
    101113    border-radius: var(--jd-border-radius);
    102114    position: relative;            /* For positioning the close button */
    103115    color: var(--jd-text-color);
     116    transition: opacity var(--jd-transition), transform var(--jd-transition);
     117    transform: scale(0.95); /* Slightly shrink modal initially */
     118    opacity: 0; /* Initially hidden */
     119}
     120
     121.preview-modal.active {
     122    transform: scale(1); /* Scale to full size */
     123    opacity: 1; /* Fully visible */
    104124}
    105125
     
    117137
    118138.preview-modal-close:hover {
    119     color: #0073aa;  /* Default WP admin blue */
     139    color: var(--jd-hover-color);
    120140}
    121141
     
    138158    padding: 8px 16px;
    139159    border-radius: var(--jd-border-radius);
    140     transition: background-color var(--jd-transition);
     160    transition: background-color var(--jd-transition), transform var(--jd-transition);
    141161    cursor: pointer;
    142162}
    143163
    144164.preview-modal .button:hover {
    145     background-color: #006799;
     165    background-color: var(--jd-hover-color);
     166    transform: translateY(-2px); /* Slight lift effect */
    146167}
    147168
  • just-duplicate/trunk/assets/js/admin-script.js

    r3237393 r3265459  
    2727                                        '<span class="preview-modal-close">&times;</span>' +
    2828                                        '<h2>' + response.data.title + '</h2>' +
    29                                         '<p><strong>Author:</strong> ' + response.data.author + '</p>' +
    30                                         '<p><strong>Date:</strong> ' + response.data.date + '</p>' +
     29                                        '<p><strong>' + JustDuplicateL10n.author + ':</strong> ' + response.data.author + '</p>' +
     30                                        '<p><strong>' + JustDuplicateL10n.date + ':</strong> ' + response.data.date + '</p>' +
    3131                                        '<div class="preview-content">' + response.data.content + '</div>' +
    3232                                        '<div class="preview-actions">' +
    33                                             '<button class="button confirm-duplicate" data-duplicate-url="' + response.data.duplicate_url + '">Confirm Duplicate</button>' +
    34                                             '<button class="button cancel-preview">Cancel</button>' +
     33                                            '<button class="button confirm-duplicate" data-duplicate-url="' + response.data.duplicate_url + '">' + JustDuplicateL10n.confirmDuplicate + '</button>' +
     34                                            '<button class="button cancel-preview">' + JustDuplicateL10n.cancel + '</button>' +
    3535                                        '</div>' +
    3636                                    '</div>' +
     
    3838                $('body').append(modalHtml);
    3939            } else {
    40                 alert('Error: ' + response.data);
     40                alert(JustDuplicateL10n.error + ': ' + response.data);
    4141            }
    4242        }).fail(function(xhr, status, error) {
    43             console.error('AJAX request failed:', status, error);
    44             alert('AJAX error: ' + error);
     43            console.error(JustDuplicateL10n.ajaxError + ':', status, error);
     44            alert(JustDuplicateL10n.ajaxError + ': ' + error);
    4545        });
    4646    });
  • just-duplicate/trunk/includes/admin/class-admin-settings.php

    r3252339 r3265459  
    3838        'duplicate_comments'       => true,
    3939        'duplicate_featured_image' => true,
     40        'custom_title'             => '',
     41        'custom_slug'              => '',
     42        'custom_post_status'       => 'draft',
    4043    ];
    4144
     
    183186            __( 'Duplicate Featured Image', 'just-duplicate' ),
    184187            [ __CLASS__, 'render_duplicate_featured_image_field' ],
     188            self::OPTION_KEY,
     189            'JUST_DUPLICATE_general'
     190        );
     191
     192        // Schedule Duplication field.
     193        add_settings_field(
     194            'schedule_duplication',
     195            __( 'Schedule Duplication', 'just-duplicate' ),
     196            [ __CLASS__, 'render_schedule_duplication_field' ],
     197            self::OPTION_KEY,
     198            'JUST_DUPLICATE_general'
     199        );
     200
     201        // Custom Title field.
     202        add_settings_field(
     203            'custom_title',
     204            __( 'Custom Title', 'just-duplicate' ),
     205            [ __CLASS__, 'render_custom_title_field' ],
     206            self::OPTION_KEY,
     207            'JUST_DUPLICATE_general'
     208        );
     209
     210        // Custom Slug field.
     211        add_settings_field(
     212            'custom_slug',
     213            __( 'Custom Slug', 'just-duplicate' ),
     214            [ __CLASS__, 'render_custom_slug_field' ],
     215            self::OPTION_KEY,
     216            'JUST_DUPLICATE_general'
     217        );
     218
     219        // Custom Post Status field.
     220        add_settings_field(
     221            'custom_post_status',
     222            __( 'Custom Post Status', 'just-duplicate' ),
     223            [ __CLASS__, 'render_custom_post_status_field' ],
    185224            self::OPTION_KEY,
    186225            'JUST_DUPLICATE_general'
     
    206245            'duplicate_comments'       => isset( $settings['duplicate_comments'] ) ? (bool) $settings['duplicate_comments'] : true,
    207246            'duplicate_featured_image' => isset( $settings['duplicate_featured_image'] ) ? (bool) $settings['duplicate_featured_image'] : true,
     247            'custom_title'             => sanitize_text_field( $settings['custom_title'] ?? '' ),
     248            'custom_slug'              => sanitize_title( $settings['custom_slug'] ?? '' ),
     249            'custom_post_status'       => in_array( $settings['custom_post_status'], [ 'draft', 'publish', 'pending' ], true ) ? $settings['custom_post_status'] : 'draft',
     250            'schedule_duplication'     => isset( $settings['schedule_duplication'] ) ? sanitize_text_field( $settings['schedule_duplication'] ) : '',
    208251        ];
    209252    }
     
    355398            <input type="checkbox" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[duplicate_featured_image]" value="1" <?php checked( $settings['duplicate_featured_image'] ?? true, true ); ?> />
    356399            <label><?php esc_html_e( 'Duplicate featured image.', 'just-duplicate' ); ?></label>
     400        </p>
     401        <?php
     402    }
     403
     404    /**
     405     * Render the "Schedule Duplication" field.
     406     *
     407     * @return void
     408     */
     409    public static function render_schedule_duplication_field(): void {
     410        ?>
     411        <p>
     412            <input type="datetime-local" name="schedule_duplication" />
     413            <span class="description"><?php esc_html_e( 'Set a date and time to schedule duplication.', 'just-duplicate' ); ?></span>
     414        </p>
     415        <?php
     416    }
     417
     418    /**
     419     * Render the "Custom Title" field.
     420     *
     421     * @return void
     422     */
     423    public static function render_custom_title_field(): void {
     424        $settings = get_option( self::OPTION_KEY, [] );
     425        ?>
     426        <p>
     427            <input type="text" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[custom_title]" value="<?php echo esc_attr( $settings['custom_title'] ?? '' ); ?>" />
     428            <span class="description"><?php esc_html_e( 'Set a custom title for duplicated items (optional).', 'just-duplicate' ); ?></span>
     429        </p>
     430        <?php
     431    }
     432
     433    /**
     434     * Render the "Custom Slug" field.
     435     *
     436     * @return void
     437     */
     438    public static function render_custom_slug_field(): void {
     439        $settings = get_option( self::OPTION_KEY, [] );
     440        ?>
     441        <p>
     442            <input type="text" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[custom_slug]" value="<?php echo esc_attr( $settings['custom_slug'] ?? '' ); ?>" />
     443            <span class="description"><?php esc_html_e( 'Set a custom slug for duplicated items (optional).', 'just-duplicate' ); ?></span>
     444        </p>
     445        <?php
     446    }
     447
     448    /**
     449     * Render the "Custom Post Status" field.
     450     *
     451     * @return void
     452     */
     453    public static function render_custom_post_status_field(): void {
     454        $settings = get_option( self::OPTION_KEY, [] );
     455        ?>
     456        <p>
     457            <select name="<?php echo esc_attr( self::OPTION_KEY ); ?>[custom_post_status]">
     458                <option value="draft" <?php selected( $settings['custom_post_status'] ?? 'draft', 'draft' ); ?>><?php esc_html_e( 'Draft', 'just-duplicate' ); ?></option>
     459                <option value="publish" <?php selected( $settings['custom_post_status'] ?? 'draft', 'publish' ); ?>><?php esc_html_e( 'Publish', 'just-duplicate' ); ?></option>
     460                <option value="pending" <?php selected( $settings['custom_post_status'] ?? 'draft', 'pending' ); ?>><?php esc_html_e( 'Pending', 'just-duplicate' ); ?></option>
     461            </select>
     462            <span class="description"><?php esc_html_e( 'Set the post status for duplicated items.', 'just-duplicate' ); ?></span>
    357463        </p>
    358464        <?php
     
    409515            ? (string) $settings['default_suffix']
    410516            : ' (Copy)';
     517        $custom_title   = isset( $settings['custom_title'] ) ? (string) $settings['custom_title'] : '';
     518        $custom_slug    = isset( $settings['custom_slug'] ) ? (string) $settings['custom_slug'] : '';
     519        $custom_status  = isset( $settings['custom_post_status'] ) ? (string) $settings['custom_post_status'] : 'draft';
     520
    411521        $new_post = [
    412             'post_title'   => $default_prefix . $post->post_title . $default_suffix,
     522            'post_title'   => $custom_title ?: $default_prefix . $post->post_title . $default_suffix,
     523            'post_name'    => $custom_slug ?: sanitize_title( $default_prefix . $post->post_title . $default_suffix ),
    413524            'post_content' => $post->post_content,
    414             'post_status'  => 'draft',
     525            'post_status'  => $custom_status,
    415526            'post_type'    => $post->post_type,
    416527            'post_author'  => get_current_user_id(),
     
    486597            <ul class="jd-tabs">
    487598                <li class="jd-tab active" data-tab="general-tab"><?php esc_html_e( 'General', 'just-duplicate' ); ?></li>
    488                 <li class="jd-tab" data-tab="advanced-tab"><?php esc_html_e( 'Advanced', 'just-duplicate' ); ?></li>
    489599                <li class="jd-tab" data-tab="help-tab"><?php esc_html_e( 'Help & Support', 'just-duplicate' ); ?></li>
    490600                <li class="jd-tab" data-tab="report-tab"><?php esc_html_e( 'Report', 'just-duplicate' ); ?></li>
     
    499609                    ?>
    500610                </form>
    501             </div>
    502             <div class="jd-tab-content" id="advanced-tab">
    503                 <h2><?php esc_html_e( 'Advanced Settings', 'just-duplicate' ); ?></h2>
    504                 <p><?php esc_html_e( 'Coming Soon...', 'just-duplicate' ); ?></p>
    505611            </div>
    506612            <div class="jd-tab-content" id="help-tab">
     
    547653                </li>
    548654                <li>
    549                     <a href="https://justthere.co.uk/donate" target="_blank">
     655                    <a href="https://justthere.co.uk/plugins/support-us/" target="_blank">
    550656                        <?php esc_html_e( 'Buy Us a Coffee', 'just-duplicate' ); ?>
    551657                    </a>
  • just-duplicate/trunk/includes/admin/class-menu-duplicator.php

    r3252339 r3265459  
    3232            return;
    3333        }
    34         // Build the base duplication URL using a constant nonce.
    3534        $duplicate_base = wp_nonce_url(
    3635            admin_url( 'admin-post.php?action=duplicate_menu' ),
     
    4039        <script type="text/javascript">
    4140        jQuery(document).ready(function($) {
    42             console.log("Menu Duplicator hook fired");
    43             // Create the Duplicate Menu button.
     41            console.log("<?php echo esc_js(__('Menu Duplicator hook fired', 'just-duplicate')); ?>");
    4442            var duplicateButton = $('<a>', {
    45                 text: '<?php esc_html_e( "Duplicate Menu", "just-duplicate" ); ?>',
     43                text: '<?php echo esc_js(__('Duplicate Menu', 'just-duplicate')); ?>',
    4644                href: '#',
    4745                class: 'button button-secondary'
     
    4947            duplicateButton.on('click', function(e) {
    5048                e.preventDefault();
    51                 // Retrieve the current menu ID from the hidden input with id "menu".
    5249                var menuId = $('#menu').val();
    53                 console.log("Selected menu ID:", menuId);
     50                console.log("<?php echo esc_js(__('Selected menu ID:', 'just-duplicate')); ?>", menuId);
    5451                if (!menuId || menuId == 0) {
    55                     alert('<?php echo esc_js( __( "Please select a menu first.", "just-duplicate" ) ); ?>');
     52                    alert('<?php echo esc_js(__('Please select a menu first.', 'just-duplicate')); ?>');
    5653                    return;
    5754                }
    58                 // Build the final duplication URL by appending the menu ID.
    5955                var duplicateUrl = '<?php echo esc_url( $duplicate_base ); ?>' + '&menu=' + menuId;
    60                 console.log("Duplicate URL:", duplicateUrl);
     56                console.log("<?php echo esc_js(__('Duplicate URL:', 'just-duplicate')); ?>", duplicateUrl);
    6157                window.location.href = duplicateUrl;
    6258            });
    63             // Append the button to the container.
    6459            var target = $('#nav-menu-footer .major-publishing-actions');
    6560            if (target.length) {
    6661                target.append(duplicateButton);
    67                 console.log("Duplicate button appended to .major-publishing-actions");
     62                console.log("<?php echo esc_js(__('Duplicate button appended to .major-publishing-actions', 'just-duplicate')); ?>");
    6863            } else {
    6964                $('#nav-menu-footer').append(duplicateButton);
    70                 console.log("Duplicate button appended to #nav-menu-footer");
     65                console.log("<?php echo esc_js(__('Duplicate button appended to #nav-menu-footer', 'just-duplicate')); ?>");
    7166            }
    7267        });
     
    7974     */
    8075    public static function handle_duplicate_menu(): void {
    81         if ( ! isset( $_GET['_wpnonce'] ) || ! isset( $_GET['menu'] ) ) {
    82             wp_die( esc_html( __( 'Missing required parameters.', 'just-duplicate' ) ) );
     76        if ( ! isset( $_GET['_wpnonce'], $_GET['menu'] ) ) {
     77            wp_die( esc_html__( 'Missing required parameters.', 'just-duplicate' ) );
    8378        }
    84         $menu_id = intval( $_GET['menu'] );
     79
     80        $menu_id = absint( $_GET['menu'] );
    8581        $nonce   = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) );
    8682
    8783        if ( ! wp_verify_nonce( $nonce, 'duplicate_menu_action' ) ) {
    88             wp_die( esc_html( __( 'Nonce verification failed.', 'just-duplicate' ) ) );
     84            wp_die( esc_html__( 'Nonce verification failed.', 'just-duplicate' ) );
    8985        }
     86
    9087        if ( ! $menu_id ) {
    91             wp_die( esc_html( __( 'No menu specified.', 'just-duplicate' ) ) );
     88            wp_die( esc_html__( 'No menu specified.', 'just-duplicate' ) );
    9289        }
     90
    9391        $new_menu_id = self::duplicate_menu( $menu_id );
    9492        if ( is_wp_error( $new_menu_id ) ) {
  • just-duplicate/trunk/includes/class-duplicate-handler.php

    r3252339 r3265459  
    1313class Duplicate_Handler {
    1414
     15    private static $last_duplicated_post_id;
     16
    1517    /**
    1618     * Initialize the duplication handler.
     
    2729        // Handle the duplication action.
    2830        add_action( 'admin_action_duplicate_post', [ __CLASS__, 'process_duplication' ] );
     31
     32        // Hook to display the rollback notice.
     33        add_action( 'admin_notices', [ __CLASS__, 'add_rollback_notice' ] );
     34
     35        // Hook to handle the rollback action.
     36        add_action( 'admin_action_rollback_duplicate', [ __CLASS__, 'handle_rollback_action' ] );
    2937    }
    3038
     
    7684
    7785    /**
    78      * Process the duplication of a post or page.
    79      *
    80      * Validates permissions, duplicates the post along with its meta, taxonomies, and attachments (if enabled),
    81      * and then conditionally redirects based on the "redirect_after_duplicate" setting.
    82      *
    83      * @return void
     86     * Process the duplication of a post or page with role-based access control.
    8487     */
    8588    public static function process_duplication(): void {
    86         // Verify required parameters.
     89        // Verify nonce.
    8790        if ( ! isset( $_GET['_wpnonce'], $_GET['post'] ) ) {
    8891            wp_die( esc_html__( 'Missing required parameters.', 'just-duplicate' ) );
     
    9093
    9194        $post_id = absint( $_GET['post'] );
    92         $nonce   = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) );
     95        $nonce   = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
    9396
    9497        if ( ! wp_verify_nonce( $nonce, 'duplicate_post_' . $post_id ) ) {
    95             wp_die( esc_html__( 'You do not have permission to duplicate this item.', 'just-duplicate' ) );
     98            wp_die( esc_html__( 'Nonce verification failed.', 'just-duplicate' ) );
    9699        }
    97100
     
    101104        }
    102105
    103         // Additional permission check.
    104         if ( ! current_user_can( 'edit_posts', $post_id ) ) {
    105             wp_die( esc_html__( 'You do not have sufficient permissions to duplicate this post.', 'just-duplicate' ) );
    106         }
    107 
    108         // Prepare the duplicated post.
    109         $new_post_data = [
    110             'post_title'   => sanitize_text_field( $post->post_title ) . ' (Copy)',
    111             'post_content' => wp_kses_post( $post->post_content ),
    112             'post_status'  => 'draft',
    113             'post_type'    => sanitize_text_field( $post->post_type ),
     106        // Role-based access control.
     107        $current_user_id = get_current_user_id();
     108        if ( ! ( current_user_can( 'manage_options' ) || current_user_can( 'edit_others_posts' ) ||
     109                 ( current_user_can( 'edit_posts' ) && $current_user_id === $post->post_author ) ) ) {
     110            wp_die( esc_html__( 'You do not have permission to duplicate this post.', 'just-duplicate' ) );
     111        }
     112
     113        // Duplicate the post.
     114        $new_post_id = self::duplicate_post( $post_id );
     115
     116        if ( $new_post_id ) {
     117            // Redirect back to the referring page.
     118            $referer = wp_get_referer();
     119            $redirect_url = $referer ? $referer : admin_url( 'edit.php' );
     120            wp_redirect( $redirect_url );
     121            exit;
     122        } else {
     123            wp_die( esc_html__( 'Failed to duplicate the post.', 'just-duplicate' ) );
     124        }
     125    }
     126
     127    /**
     128     * Duplicate a post or page.
     129     *
     130     * @param int $post_id The ID of the post to duplicate.
     131     * @return int|null The ID of the duplicated post, or null on failure.
     132     */
     133    public static function duplicate_post( int $post_id ): ?int {
     134        $post = get_post( $post_id );
     135        if ( ! $post ) {
     136            return null;
     137        }
     138
     139        // Trigger the before duplicate hook.
     140        do_action( 'just_duplicate_before_duplicate', $post_id );
     141
     142        $settings = get_option( 'JUST_DUPLICATE_settings', [] );
     143
     144        // Prepare the new post data.
     145        $new_post = [
     146            'post_title'   => $settings['custom_title'] ?: $post->post_title . ' (Copy)',
     147            'post_name'    => $settings['custom_slug'] ?: '',
     148            'post_content' => $post->post_content,
     149            'post_status'  => $settings['custom_post_status'] ?? 'draft',
     150            'post_type'    => $post->post_type,
    114151            'post_author'  => get_current_user_id(),
    115             'post_excerpt' => sanitize_text_field( $post->post_excerpt ),
    116             'post_parent'  => absint( $post->post_parent ),
     152            'post_excerpt' => $post->post_excerpt,
     153            'post_parent'  => $post->post_parent,
    117154        ];
    118155
    119         // Insert the duplicated post.
    120         $new_post_id = wp_insert_post( $new_post_data );
     156        // Insert the new post.
     157        $new_post_id = wp_insert_post( $new_post );
    121158        if ( is_wp_error( $new_post_id ) || ! $new_post_id ) {
    122             wp_die( esc_html__( 'Failed to duplicate the post.', 'just-duplicate' ) );
    123         }
    124 
    125         // Log the duplication action.
    126         Duplicate_Logger::log( $post_id, $new_post_id, 'post' );
    127 
    128         // Retrieve selective duplication options.
    129         $options = self::get_selective_duplication_options();
    130 
    131         // Conditionally copy post meta.
    132         if ( $options['duplicate_post_meta'] ) {
    133             self::copy_post_meta( $post_id, $new_post_id );
    134         }
    135         // Conditionally copy taxonomies.
    136         if ( $options['duplicate_taxonomies'] ) {
    137             self::copy_post_taxonomies( $post_id, $new_post_id );
    138         }
    139         // Conditionally duplicate attachments (e.g., featured image).
    140         if ( $options['duplicate_attachments'] ) {
    141             $thumb_id = get_post_thumbnail_id( $post_id );
    142             if ( $thumb_id ) {
    143                 $new_thumb_id = self::duplicate_attachment( $thumb_id, $new_post_id );
    144                 if ( $new_thumb_id ) {
    145                     set_post_thumbnail( $new_post_id, $new_thumb_id );
    146                 }
    147             }
    148         }
    149         // Conditionally copy custom fields.
    150         if ( $options['duplicate_custom_fields'] ) {
    151             self::copy_custom_fields( $post_id, $new_post_id );
    152         }
    153         // Conditionally copy custom taxonomies.
    154         if ( $options['duplicate_custom_taxonomies'] ) {
    155             self::copy_custom_taxonomies( $post_id, $new_post_id );
    156         }
    157         // Conditionally copy comments.
    158         if ( $options['duplicate_comments'] ) {
    159             self::copy_comments( $post_id, $new_post_id );
    160         }
    161         // Conditionally copy featured image.
    162         if ( $options['duplicate_featured_image'] ) {
    163             $thumb_id = get_post_thumbnail_id( $post_id );
    164             if ( $thumb_id ) {
    165                 $new_thumb_id = self::duplicate_attachment( $thumb_id, $new_post_id );
    166                 if ( $new_thumb_id ) {
    167                     set_post_thumbnail( $new_post_id, $new_thumb_id );
    168                 }
    169             }
    170         }
    171 
    172         // Check the "redirect_after_duplicate" setting.
    173         if ( ! empty( $settings['redirect_after_duplicate'] ) ) {
    174             // Redirect to the edit screen of the new post.
    175             wp_redirect( admin_url( 'post.php?action=edit&post=' . $new_post_id ) );
     159            return null;
     160        }
     161
     162        // Copy metadata, taxonomies, and other related data.
     163        self::copy_post_meta( $post_id, $new_post_id );
     164        self::copy_post_taxonomies( $post_id, $new_post_id );
     165
     166        // If the original post has a featured image, duplicate it.
     167        $thumb_id = get_post_thumbnail_id( $post_id );
     168        if ( $thumb_id ) {
     169            $new_thumb_id = self::duplicate_attachment( $thumb_id, $new_post_id );
     170            if ( $new_thumb_id ) {
     171                set_post_thumbnail( $new_post_id, $new_thumb_id );
     172            }
     173        }
     174
     175        // Store the last duplicated post ID.
     176        self::$last_duplicated_post_id = $new_post_id;
     177
     178        // Store the last duplicated post ID in a transient.
     179        if ( $new_post_id ) {
     180            set_transient( 'just_duplicate_last_post_id', $new_post_id, HOUR_IN_SECONDS );
     181        }
     182
     183        // Trigger the after duplicate hook.
     184        do_action( 'just_duplicate_after_duplicate', $post_id, $new_post_id );
     185
     186        return $new_post_id;
     187    }
     188
     189    /**
     190     * Get the last duplicated post ID.
     191     *
     192     * @return int|null The ID of the last duplicated post, or null if none.
     193     */
     194    public static function get_last_duplicated_post_id(): ?int {
     195        return self::$last_duplicated_post_id;
     196    }
     197
     198    /**
     199     * Rollback the last duplicated post.
     200     *
     201     * @return void
     202     */
     203    public static function rollback_last_duplicate(): void {
     204        $last_post_id = get_transient( 'just_duplicate_last_post_id' );
     205
     206        if ( $last_post_id ) {
     207            wp_delete_post( $last_post_id, true );
     208            delete_transient( 'just_duplicate_last_post_id' );
     209            add_action( 'admin_notices', function () {
     210                echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'The last duplicated post has been rolled back.', 'just-duplicate' ) . '</p></div>';
     211            } );
    176212        } else {
    177             // Redirect back to the referring page (fallback to the posts list if no referer).
    178             $redirect_url = wp_get_referer() ? wp_get_referer() : admin_url( 'edit.php' );
    179             // Optionally, add a query parameter to indicate duplication success.
    180             $redirect_url = add_query_arg( 'duplicated', $new_post_id, $redirect_url );
    181             wp_redirect( $redirect_url );
    182         }
     213            add_action( 'admin_notices', function () {
     214                echo '<div class="notice notice-error is-dismissible"><p>' . esc_html__( 'No duplicated post found to rollback.', 'just-duplicate' ) . '</p></div>';
     215            } );
     216        }
     217    }
     218
     219    /**
     220     * Add an admin notice with a rollback link after duplication.
     221     */
     222    public static function add_rollback_notice(): void {
     223        $last_post_id = get_transient( 'just_duplicate_last_post_id' );
     224
     225        if ( $last_post_id ) {
     226            $rollback_url = add_query_arg(
     227                [
     228                    'action' => 'rollback_duplicate',
     229                    '_wpnonce' => wp_create_nonce( 'rollback_duplicate' ),
     230                ],
     231                admin_url( 'admin.php' )
     232            );
     233
     234            echo '<div class="notice notice-info is-dismissible"><p>' .
     235                esc_html__( 'A post has been duplicated. ', 'just-duplicate' ) .
     236                '<a href="' . esc_url( $rollback_url ) . '">' . esc_html__( 'Undo this action.', 'just-duplicate' ) . '</a>' .
     237                '</p></div>';
     238        }
     239    }
     240
     241    /**
     242     * Handle the rollback action.
     243     */
     244    public static function handle_rollback_action(): void {
     245        if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'rollback_duplicate' ) ) {
     246            wp_die( esc_html__( 'Nonce verification failed.', 'just-duplicate' ) );
     247        }
     248
     249        self::rollback_last_duplicate();
     250        wp_redirect( admin_url( 'edit.php' ) );
    183251        exit;
    184252    }
     
    206274                continue;
    207275            }
     276
     277            // Only copy Elementor-specific meta keys if the original post was edited in Elementor.
     278            if ( in_array( $key, [ '_elementor_edit_mode', '_elementor_data', '_elementor_template_type', '_elementor_version' ], true ) ) {
     279                $is_elementor = get_post_meta( $old_post_id, '_elementor_edit_mode', true );
     280                if ( ! $is_elementor ) {
     281                    continue;
     282                }
     283            }
     284
    208285            foreach ( $values as $value ) {
    209286                add_post_meta( $new_post_id, $key, maybe_unserialize( $value ) );
     
    323400     */
    324401    public static function preview_duplicate(): void {
     402        // Verify the AJAX nonce.
     403        check_ajax_referer( 'preview_duplicate_post', '_wpnonce' );
     404
    325405        // Check permissions.
    326406        if ( ! current_user_can( 'edit_posts' ) ) {
    327             wp_send_json_error( __( 'Permission denied.', 'just-duplicate' ) );
     407            wp_send_json_error( esc_html__( 'Permission denied.', 'just-duplicate' ) );
    328408        }
    329409       
    330410        $post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
    331         $nonce   = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
    332 
    333         if ( ! wp_verify_nonce( $nonce, 'preview_duplicate_post_' . $post_id ) ) {
    334             wp_send_json_error( __( 'Nonce verification failed.', 'just-duplicate' ) );
    335         }
    336411
    337412        $post = get_post( $post_id );
    338413        if ( ! $post ) {
    339             wp_send_json_error( __( 'Post not found.', 'just-duplicate' ) );
     414            wp_send_json_error( esc_html__( 'Post not found.', 'just-duplicate' ) );
    340415        }
    341416
     
    350425        $preview_data = [
    351426            'post_id'      => $post->ID,
    352             'title'        => $default_prefix . $post->post_title . $default_suffix,
    353             'content'      => apply_filters( 'the_content', $post->post_content ),
    354             'excerpt'      => $post->post_excerpt,
    355             'author'       => get_the_author_meta( 'display_name', $post->post_author ),
    356             'date'         => $post->post_date,
    357             'duplicate_url'=> wp_nonce_url(
    358                                 add_query_arg(
    359                                     [
    360                                         'action' => 'duplicate_post',
    361                                         'post'   => $post->ID,
    362                                     ],
    363                                     admin_url( 'admin.php' )
    364                                 ),
    365                                 'duplicate_post_' . $post->ID
    366                               ),
     427            'title'        => esc_html( $default_prefix . $post->post_title . $default_suffix ),
     428            'content'      => wp_kses_post( apply_filters( 'the_content', $post->post_content ) ),
     429            'excerpt'      => esc_html( $post->post_excerpt ),
     430            'author'       => esc_html( get_the_author_meta( 'display_name', $post->post_author ) ),
     431            'date'         => esc_html( $post->post_date ),
     432            'duplicate_url'=> esc_url( wp_nonce_url(
     433                add_query_arg(
     434                    [
     435                        'action' => 'duplicate_post',
     436                        'post'   => $post->ID,
     437                    ],
     438                    admin_url( 'admin.php' )
     439                ),
     440                'duplicate_post_' . $post->ID
     441            ) ),
    367442        ];
    368443
  • just-duplicate/trunk/includes/class-just-duplicate-loader.php

    r3252339 r3265459  
    7979        }
    8080
     81        // Load Scheduled Duplicator.
     82        $scheduled_duplicator_path = JUST_DUPLICATE_PATH . 'includes/class-scheduled-duplicator.php';
     83        if ( file_exists( $scheduled_duplicator_path ) ) {
     84            require_once $scheduled_duplicator_path;
     85            if ( class_exists( 'Just_Duplicate\Scheduled_Duplicator' ) ) {
     86                Scheduled_Duplicator::init();
     87            }
     88        }
     89
    8190        // Load admin-specific components if in admin area.
    8291        if ( is_admin() ) {
     
    99108            }
    100109        }
     110
    101111        // Load Menu Duplicator.
    102112        $menu_duplicator_path = JUST_DUPLICATE_PATH . 'includes/admin/class-menu-duplicator.php';
     
    107117            }
    108118        }
     119
    109120        // Load Media Duplicator.
    110121        $media_duplicator_path = JUST_DUPLICATE_PATH . 'includes/admin/class-media-duplicator.php';
     
    115126            }
    116127        }
     128
     129        // Enqueue admin script with localization.
     130        add_action('admin_enqueue_scripts', function() {
     131            wp_localize_script(
     132                'just-duplicate-admin-script',
     133                'JustDuplicateL10n',
     134                [
     135                    'author' => __('Author', 'just-duplicate'),
     136                    'date' => __('Date', 'just-duplicate'),
     137                    'confirmDuplicate' => __('Confirm Duplicate', 'just-duplicate'),
     138                    'cancel' => __('Cancel', 'just-duplicate'),
     139                    'error' => __('Error', 'just-duplicate'),
     140                    'ajaxError' => __('AJAX error', 'just-duplicate'),
     141                ]
     142            );
     143        });
    117144    }
    118145}
  • just-duplicate/trunk/just-duplicate.php

    r3252339 r3265459  
    44 * Plugin URI: https://wordpress.org/plugins/just-duplicate
    55 * Description: A powerful plugin to duplicate pages, posts, custom post types, WooCommerce products, menus, and more. Supports batch duplication, customizable options, and compatibility with major plugins and themes.
    6  * Version: 1.0.2
     6 * Version: 1.0.3
    77 * Requires at least: 5.0
    88 * Tested up to: 6.7
     
    1010 * Author: Just There
    1111 * Author URI: https://justthere.co.uk/
    12  * Support Us: https://justthere.co.uk/donate
     12 * Support Us: https://justthere.co.uk/plugins/support-us/
    1313 * License: GPLv3
    1414 * License URI: https://www.gnu.org/licenses/gpl-3.0.html
     
    2121
    2222// Define plugin constants.
    23 define( 'JUST_DUPLICATE_VERSION', '1.0.2' );
     23define( 'JUST_DUPLICATE_VERSION', '1.0.3' );
    2424define( 'JUST_DUPLICATE_PATH', plugin_dir_path( __FILE__ ) );
    2525define( 'JUST_DUPLICATE_URL', plugin_dir_url( __FILE__ ) );
     
    9393}
    9494add_action( 'admin_enqueue_scripts', 'just_duplicate_enqueue_admin_assets' );
     95
     96/**
     97 * Add a "Support Us" link to the plugin's action links on the plugins page.
     98 *
     99 * @param array $links Existing plugin action links.
     100 * @return array Modified plugin action links.
     101 */
     102function just_duplicate_add_support_us_link( array $links ): array {
     103    $support_link = '<a href="https://justthere.co.uk/plugins/support-us/" style="color: red;" target="_blank">' . esc_html__( 'Support Us', 'just-duplicate' ) . '</a>';
     104    array_unshift( $links, $support_link ); // Add the link to the beginning of the array.
     105    return $links;
     106}
     107add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'just_duplicate_add_support_us_link' );
Note: See TracChangeset for help on using the changeset viewer.