Changeset 3265459
- Timestamp:
- 04/02/2025 07:12:39 AM (9 months ago)
- Location:
- just-duplicate
- Files:
-
- 15 added
- 8 edited
-
assets/screenshot-1.png (added)
-
assets/screenshot-2.png (added)
-
assets/screenshot-3.png (added)
-
assets/screenshot-4.png (added)
-
assets/screenshot-5.png (added)
-
trunk/README.txt (modified) (4 diffs)
-
trunk/assets/css/admin-style.css (modified) (7 diffs)
-
trunk/assets/images/banner-772x250.png (added)
-
trunk/assets/images/icon-512x512.png.png (added)
-
trunk/assets/images/screenshot-1.png (added)
-
trunk/assets/images/screenshot-2.png (added)
-
trunk/assets/images/screenshot-3.png (added)
-
trunk/assets/images/screenshot-4.png (added)
-
trunk/assets/images/screenshot-5.png (added)
-
trunk/assets/js/admin-script.js (modified) (2 diffs)
-
trunk/assets/js/gutenberg-duplicate.js (added)
-
trunk/includes/admin/class-admin-settings.php (modified) (8 diffs)
-
trunk/includes/admin/class-menu-duplicator.php (modified) (4 diffs)
-
trunk/includes/class-admin-settings.php (added)
-
trunk/includes/class-duplicate-handler.php (modified) (8 diffs)
-
trunk/includes/class-just-duplicate-loader.php (modified) (4 diffs)
-
trunk/includes/class-scheduled-duplicator.php (added)
-
trunk/just-duplicate.php (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
just-duplicate/trunk/README.txt
r3252339 r3265459 7 7 Support URI: https://justthere.co.uk/plugins/just-duplicate/support 8 8 Feature Request URI: https://justthere.co.uk/plugins/just-duplicate/feature-request 9 Donate link: https://justthere.co.uk/ donate9 Donate link: https://justthere.co.uk/plugins/support-us/ 10 10 Requires at least: 6.0 11 11 Tested up to: 6.7 12 Stable tag: 1.0. 213 Version: 1.0. 212 Stable tag: 1.0.3 13 Version: 1.0.3 14 14 Requires PHP: 7.4 15 15 License: GNU General Public License v3.0 or later 16 16 License URI: http://www.gnu.org/licenses/gpl.html 17 Tags: post duplicator, page duplicator, WooCommerce product duplicator, duplicator17 Tags: duplicate posts, duplicate pages, duplicate WooCommerce products, WordPress duplicator, content duplicator, post duplicator, page duplicator, WooCommerce duplicator, batch duplication, role-based access control 18 18 19 Easily duplicate pages, posts, custom post types, and WooCommerce products with Just Duplicate. A simple, fast, and customizable solution for WordPress content duplication.19 Easily duplicate WordPress pages, posts, custom post types, and WooCommerce products with one click. 20 20 21 21 == 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 withone 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. 23 23 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.24 Designed 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. 25 25 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.** 31 27 32 28 **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. 35 31 ⚙️ **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 fastperformance.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. 40 36 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 42 For more details, visit [our website](https://justthere.co.uk/plugins/just-duplicate). 42 43 43 44 == Installation == … … 56 57 57 58 = 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.59 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. 59 60 60 61 = Can I duplicate WooCommerce products? = … … 77 78 78 79 == Screenshots == 79 1. Plugin settings page 80 2. Duplicate button on a post/page 81 3. Role-based access settings 80 1. Duplicate button on page tab. 81 2. Preview duplication button on page tab. 82 3. Actual preview of duplication. 83 4. Duplication report log. 84 5. Just Duplicate general settings. 82 85 83 86 == 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. 84 114 85 115 = 1.0.2 = … … 129 159 130 160 == Upgrade Notice == 131 = 1.0. 2=161 = 1.0. = 132 162 - No Major Changes (Safe Update) 133 163 -
just-duplicate/trunk/assets/css/admin-style.css
r3252339 r3265459 16 16 --jd-border-radius: 3px; 17 17 --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 */ 24 body { 25 font-family: var(--jd-font-family); 18 26 } 19 27 … … 25 33 border-radius: var(--jd-border-radius); 26 34 color: var(--jd-text-color); 35 box-shadow: var(--jd-box-shadow); 27 36 } 28 37 … … 51 60 transition: background var(--jd-transition); 52 61 color: var(--jd-text-color); 62 border-radius: var(--jd-border-radius) var(--jd-border-radius) 0 0; 53 63 } 54 64 55 65 .jd-tabs .jd-tab:hover { 56 66 background: var(--jd-tab-bg-hover); 67 color: var(--jd-hover-color); 57 68 } 58 69 … … 60 71 background: var(--jd-tab-bg-active); 61 72 border-bottom: 1px solid var(--jd-tab-bg-active); 73 color: var(--jd-hover-color); 62 74 } 63 75 … … 98 110 max-height: 80vh; /* Limit height and allow scroll if content is long */ 99 111 overflow-y: auto; 100 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);112 box-shadow: var(--jd-box-shadow); 101 113 border-radius: var(--jd-border-radius); 102 114 position: relative; /* For positioning the close button */ 103 115 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 */ 104 124 } 105 125 … … 117 137 118 138 .preview-modal-close:hover { 119 color: #0073aa; /* Default WP admin blue */139 color: var(--jd-hover-color); 120 140 } 121 141 … … 138 158 padding: 8px 16px; 139 159 border-radius: var(--jd-border-radius); 140 transition: background-color var(--jd-transition) ;160 transition: background-color var(--jd-transition), transform var(--jd-transition); 141 161 cursor: pointer; 142 162 } 143 163 144 164 .preview-modal .button:hover { 145 background-color: #006799; 165 background-color: var(--jd-hover-color); 166 transform: translateY(-2px); /* Slight lift effect */ 146 167 } 147 168 -
just-duplicate/trunk/assets/js/admin-script.js
r3237393 r3265459 27 27 '<span class="preview-modal-close">×</span>' + 28 28 '<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>' + 31 31 '<div class="preview-content">' + response.data.content + '</div>' + 32 32 '<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>' + 35 35 '</div>' + 36 36 '</div>' + … … 38 38 $('body').append(modalHtml); 39 39 } else { 40 alert( 'Error: ' + response.data);40 alert(JustDuplicateL10n.error + ': ' + response.data); 41 41 } 42 42 }).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); 45 45 }); 46 46 }); -
just-duplicate/trunk/includes/admin/class-admin-settings.php
r3252339 r3265459 38 38 'duplicate_comments' => true, 39 39 'duplicate_featured_image' => true, 40 'custom_title' => '', 41 'custom_slug' => '', 42 'custom_post_status' => 'draft', 40 43 ]; 41 44 … … 183 186 __( 'Duplicate Featured Image', 'just-duplicate' ), 184 187 [ __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' ], 185 224 self::OPTION_KEY, 186 225 'JUST_DUPLICATE_general' … … 206 245 'duplicate_comments' => isset( $settings['duplicate_comments'] ) ? (bool) $settings['duplicate_comments'] : true, 207 246 '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'] ) : '', 208 251 ]; 209 252 } … … 355 398 <input type="checkbox" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[duplicate_featured_image]" value="1" <?php checked( $settings['duplicate_featured_image'] ?? true, true ); ?> /> 356 399 <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> 357 463 </p> 358 464 <?php … … 409 515 ? (string) $settings['default_suffix'] 410 516 : ' (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 411 521 $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 ), 413 524 'post_content' => $post->post_content, 414 'post_status' => 'draft',525 'post_status' => $custom_status, 415 526 'post_type' => $post->post_type, 416 527 'post_author' => get_current_user_id(), … … 486 597 <ul class="jd-tabs"> 487 598 <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>489 599 <li class="jd-tab" data-tab="help-tab"><?php esc_html_e( 'Help & Support', 'just-duplicate' ); ?></li> 490 600 <li class="jd-tab" data-tab="report-tab"><?php esc_html_e( 'Report', 'just-duplicate' ); ?></li> … … 499 609 ?> 500 610 </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>505 611 </div> 506 612 <div class="jd-tab-content" id="help-tab"> … … 547 653 </li> 548 654 <li> 549 <a href="https://justthere.co.uk/ donate" target="_blank">655 <a href="https://justthere.co.uk/plugins/support-us/" target="_blank"> 550 656 <?php esc_html_e( 'Buy Us a Coffee', 'just-duplicate' ); ?> 551 657 </a> -
just-duplicate/trunk/includes/admin/class-menu-duplicator.php
r3252339 r3265459 32 32 return; 33 33 } 34 // Build the base duplication URL using a constant nonce.35 34 $duplicate_base = wp_nonce_url( 36 35 admin_url( 'admin-post.php?action=duplicate_menu' ), … … 40 39 <script type="text/javascript"> 41 40 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')); ?>"); 44 42 var duplicateButton = $('<a>', { 45 text: '<?php e sc_html_e( "Duplicate Menu", "just-duplicate"); ?>',43 text: '<?php echo esc_js(__('Duplicate Menu', 'just-duplicate')); ?>', 46 44 href: '#', 47 45 class: 'button button-secondary' … … 49 47 duplicateButton.on('click', function(e) { 50 48 e.preventDefault(); 51 // Retrieve the current menu ID from the hidden input with id "menu".52 49 var menuId = $('#menu').val(); 53 console.log(" Selected menu ID:", menuId);50 console.log("<?php echo esc_js(__('Selected menu ID:', 'just-duplicate')); ?>", menuId); 54 51 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')); ?>'); 56 53 return; 57 54 } 58 // Build the final duplication URL by appending the menu ID.59 55 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); 61 57 window.location.href = duplicateUrl; 62 58 }); 63 // Append the button to the container.64 59 var target = $('#nav-menu-footer .major-publishing-actions'); 65 60 if (target.length) { 66 61 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')); ?>"); 68 63 } else { 69 64 $('#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')); ?>"); 71 66 } 72 67 }); … … 79 74 */ 80 75 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' ) ); 83 78 } 84 $menu_id = intval( $_GET['menu'] ); 79 80 $menu_id = absint( $_GET['menu'] ); 85 81 $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ); 86 82 87 83 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' ) ); 89 85 } 86 90 87 if ( ! $menu_id ) { 91 wp_die( esc_html ( __( 'No menu specified.', 'just-duplicate' )) );88 wp_die( esc_html__( 'No menu specified.', 'just-duplicate' ) ); 92 89 } 90 93 91 $new_menu_id = self::duplicate_menu( $menu_id ); 94 92 if ( is_wp_error( $new_menu_id ) ) { -
just-duplicate/trunk/includes/class-duplicate-handler.php
r3252339 r3265459 13 13 class Duplicate_Handler { 14 14 15 private static $last_duplicated_post_id; 16 15 17 /** 16 18 * Initialize the duplication handler. … … 27 29 // Handle the duplication action. 28 30 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' ] ); 29 37 } 30 38 … … 76 84 77 85 /** 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. 84 87 */ 85 88 public static function process_duplication(): void { 86 // Verify required parameters.89 // Verify nonce. 87 90 if ( ! isset( $_GET['_wpnonce'], $_GET['post'] ) ) { 88 91 wp_die( esc_html__( 'Missing required parameters.', 'just-duplicate' ) ); … … 90 93 91 94 $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'] ) ) : ''; 93 96 94 97 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' ) ); 96 99 } 97 100 … … 101 104 } 102 105 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, 114 151 '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, 117 154 ]; 118 155 119 // Insert the duplicatedpost.120 $new_post_id = wp_insert_post( $new_post _data);156 // Insert the new post. 157 $new_post_id = wp_insert_post( $new_post ); 121 158 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 } ); 176 212 } 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' ) ); 183 251 exit; 184 252 } … … 206 274 continue; 207 275 } 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 208 285 foreach ( $values as $value ) { 209 286 add_post_meta( $new_post_id, $key, maybe_unserialize( $value ) ); … … 323 400 */ 324 401 public static function preview_duplicate(): void { 402 // Verify the AJAX nonce. 403 check_ajax_referer( 'preview_duplicate_post', '_wpnonce' ); 404 325 405 // Check permissions. 326 406 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' ) ); 328 408 } 329 409 330 410 $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 }336 411 337 412 $post = get_post( $post_id ); 338 413 if ( ! $post ) { 339 wp_send_json_error( __( 'Post not found.', 'just-duplicate' ) );414 wp_send_json_error( esc_html__( 'Post not found.', 'just-duplicate' ) ); 340 415 } 341 416 … … 350 425 $preview_data = [ 351 426 '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->ID366 ),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 ) ), 367 442 ]; 368 443 -
just-duplicate/trunk/includes/class-just-duplicate-loader.php
r3252339 r3265459 79 79 } 80 80 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 81 90 // Load admin-specific components if in admin area. 82 91 if ( is_admin() ) { … … 99 108 } 100 109 } 110 101 111 // Load Menu Duplicator. 102 112 $menu_duplicator_path = JUST_DUPLICATE_PATH . 'includes/admin/class-menu-duplicator.php'; … … 107 117 } 108 118 } 119 109 120 // Load Media Duplicator. 110 121 $media_duplicator_path = JUST_DUPLICATE_PATH . 'includes/admin/class-media-duplicator.php'; … … 115 126 } 116 127 } 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 }); 117 144 } 118 145 } -
just-duplicate/trunk/just-duplicate.php
r3252339 r3265459 4 4 * Plugin URI: https://wordpress.org/plugins/just-duplicate 5 5 * 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. 26 * Version: 1.0.3 7 7 * Requires at least: 5.0 8 8 * Tested up to: 6.7 … … 10 10 * Author: Just There 11 11 * Author URI: https://justthere.co.uk/ 12 * Support Us: https://justthere.co.uk/ donate12 * Support Us: https://justthere.co.uk/plugins/support-us/ 13 13 * License: GPLv3 14 14 * License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 21 21 22 22 // Define plugin constants. 23 define( 'JUST_DUPLICATE_VERSION', '1.0. 2' );23 define( 'JUST_DUPLICATE_VERSION', '1.0.3' ); 24 24 define( 'JUST_DUPLICATE_PATH', plugin_dir_path( __FILE__ ) ); 25 25 define( 'JUST_DUPLICATE_URL', plugin_dir_url( __FILE__ ) ); … … 93 93 } 94 94 add_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 */ 102 function 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 } 107 add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'just_duplicate_add_support_us_link' );
Note: See TracChangeset
for help on using the changeset viewer.