Changeset 3384189
- Timestamp:
- 10/24/2025 06:32:18 PM (7 weeks ago)
- Location:
- just-duplicate/trunk
- Files:
-
- 2 added
- 3 deleted
- 7 edited
-
README.txt (modified) (4 diffs)
-
assets/images/icon-128x128.gif (added)
-
assets/images/icon-128x128.png.png (deleted)
-
assets/images/icon-256x256.gif (added)
-
assets/images/icon-256x256.png.png (deleted)
-
assets/images/icon-512x512.png.png (deleted)
-
assets/js/admin-script.js (modified) (1 diff)
-
assets/js/gutenberg-duplicate.js (modified) (1 diff)
-
includes/admin/class-admin-settings.php (modified) (34 diffs)
-
includes/class-duplicate-handler.php (modified) (20 diffs)
-
includes/class-duplicate-logger.php (modified) (2 diffs)
-
just-duplicate.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
just-duplicate/trunk/README.txt
r3297660 r3384189 10 10 Requires at least: 6.0 11 11 Tested up to: 6.8 12 Stable tag: 1.0. 413 Version: 1.0. 412 Stable tag: 1.0.6 13 Version: 1.0.6 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: duplicate posts, duplicate pages, duplicate WooCommerce products, WordPress duplicator, content duplicator, post duplicator, page duplicator, WooCommerce duplicator, batch duplication, role-based access control17 Tags: Duplicate Page, Duplicate Post, Page Duplicator, Post Duplicator, Duplicate Custom Posts 18 18 19 19 Easily duplicate WordPress pages, posts, custom post types, and WooCommerce products with one click. … … 54 54 3. Go to **Plugins > Installed Plugins**, find "Just Duplicate", and click **Activate**. 55 55 56 == How to Use == 57 58 - In the Posts/Pages list: hover a row to see two links — "Clone" (duplicates and returns to the list) and "New Draft" (opens the editor for the copy). 59 - In the Classic editor: find the buttons in the Publish box. 60 - In the Block editor (Gutenberg): open the sidebar panel to find "Clone" and "New Draft" buttons. 61 - In the Admin Bar (optional): use the Duplicate menu for quick actions. 62 - Bulk actions: Select items in the list table, choose "Duplicate" from Bulk actions, and apply. 63 64 == Settings == 65 66 Go to Settings → Just Duplicate to configure: 67 68 - Where to show links: Row actions, Submitbox (edit screen), Admin Bar, and Bulk actions. 69 - Who can duplicate: Allow specific roles and limit to selected post types. 70 - What to copy: Author, date, password, excerpt, template, post format, sticky, menu order (with optional increment), taxonomies, and meta. 71 - Blacklists: Exclude specific meta keys or taxonomies using CSV with wildcard support (e.g., _edit_*). 72 - Attachments: Reference original media or duplicate media files and remap references. 73 - Children: Copy one level of child posts to preserve simple hierarchies. 74 - Redirect after duplicate: Go back to list, open editor, or view the original. 75 - Title/slug patterns: Continue using your existing title/slug patterns without breaking changes. 76 77 Advanced: 78 79 - Original surfacing: Add an "Original" column in the list table, a "Copy" post state label, and/or an "Original" meta box on the edit screen. 80 - Rewrite & Republish: Create a working copy of a post; when published, its changes replace the original content while preserving the original ID/URL. 81 56 82 == Frequently Asked Questions == 57 83 … … 85 111 86 112 == Changelog == 113 114 = 1.0.6 = 115 - Granular settings for what to copy (author, date, password, excerpt, template, format, sticky, menu order with increment, children) 116 - Post type and role allowlists; link location toggles (row, submitbox, admin bar, bulk) 117 - Gutenberg sidebar panel with Clone/New Draft actions (nonce-protected) 118 - Admin Bar “Duplicate” menu with Clone and New Draft (toggleable) 119 - Meta and taxonomy blacklists with * wildcard 120 - Attachment handling: reference originals or duplicate media (best-effort remap) 121 - Redirect preferences after duplication: list/editor/original 122 - Original surfacing: optional list column, post state label, and meta box 123 - Bulk duplicate across all enabled post types with success notice 124 - Rewrite & Republish flow: create working copy and apply changes back to original on publish 125 - Nonces and capability checks across all actions 126 - Backward compatibility preserved for existing options and hooks 127 - Report tab now includes a Clear Log button (clears plugin log and transient) 128 129 = 1.0.5 = 130 - Update: Improve image assets. 131 - Update: README.txt. 132 - Improvement: Code Improvement 87 133 88 134 = 1.0.4 = … … 176 222 177 223 == Copyright == 224 Copyright © 2013 - 2025 Just There 178 225 Just Duplicate is built with ❤️ by Just There. -
just-duplicate/trunk/assets/js/admin-script.js
r3265459 r3384189 14 14 $('#' + tabId).addClass('active'); 15 15 }); 16 17 // If a tab hint is provided via query (?jd_tab=report) or hash (#report-tab), activate it on load. 18 (function activateInitialTab() { 19 function getQueryParam(name) { 20 var results = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.href); 21 return results ? decodeURIComponent(results[1].replace(/\+/g, ' ')) : null; 22 } 23 var hint = getQueryParam('jd_tab'); 24 var hash = window.location.hash ? window.location.hash.replace('#', '') : ''; 25 var target = null; 26 if (hint === 'report') { target = 'report-tab'; } 27 else if (hint === 'general') { target = 'general-tab'; } 28 else if (hint === 'help') { target = 'help-tab'; } 29 else if (hash) { target = hash; } 30 if (target && $('#' + target).length) { 31 $('.jd-tabs .jd-tab').removeClass('active'); 32 $('.jd-tab-content').removeClass('active'); 33 $('.jd-tabs .jd-tab[data-tab="' + target + '"]').addClass('active'); 34 $('#' + target).addClass('active'); 35 } 36 })(); 16 37 17 38 // --- Preview Duplicate Handling --- -
just-duplicate/trunk/assets/js/gutenberg-duplicate.js
r3265459 r3384189 2 2 const { registerPlugin } = wp.plugins; 3 3 const { PluginPostStatusInfo } = wp.editPost; 4 const { createElement } = wp.element; 4 const { createElement, Fragment } = wp.element; 5 const { Button } = wp.components; 5 6 6 7 registerPlugin('just-duplicate', { 7 8 render: () => { 9 const onClone = () => { window.location.href = JustDuplicate.cloneUrl; }; 10 const onNewDraft = () => { window.location.href = JustDuplicate.draftUrl; }; 11 const onRewrite = () => { window.location.href = JustDuplicate.rewriteUrl; }; 12 8 13 return createElement( 9 14 PluginPostStatusInfo, 10 15 null, 11 createElement( 12 'a', 13 { 14 href: JustDuplicate.url, 15 className: 'button', 16 }, 17 'Duplicate This' 16 createElement('div', { className: 'jd-gutenberg-actions' }, 17 createElement(Button, { onClick: onClone, isSecondary: true, style: { marginRight: '6px' } }, 'Clone'), 18 createElement(Button, { onClick: onNewDraft, isPrimary: true, style: { marginRight: '6px' } }, 'New Draft'), 19 createElement(Button, { onClick: onRewrite, isTertiary: true }, 'Rewrite & Republish') 18 20 ) 19 21 ); -
just-duplicate/trunk/includes/admin/class-admin-settings.php
r3297655 r3384189 7 7 exit; // Exit if accessed directly. 8 8 } 9 10 // Lightweight namespaced shims to forward WordPress global functions; this calms static analysis 11 // while keeping runtime behavior identical under WordPress. 12 if ( ! function_exists( __NAMESPACE__ . '\\add_action' ) ) { 13 function add_action( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) { return \function_exists('\\add_action') ? \call_user_func_array('\\add_action', \func_get_args()) : null; } 14 } 15 if ( ! function_exists( __NAMESPACE__ . '\\add_filter' ) ) { 16 function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) { return \function_exists('\\add_filter') ? \call_user_func_array('\\add_filter', \func_get_args()) : null; } 17 } 18 if ( ! function_exists( __NAMESPACE__ . '\\add_menu_page' ) ) { 19 function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $callback = '', $icon_url = '', $position = null ) { return \function_exists('\\add_menu_page') ? \call_user_func_array('\\add_menu_page', \func_get_args()) : null; } 20 } 21 if ( ! function_exists( __NAMESPACE__ . '\\add_meta_box' ) ) { 22 function add_meta_box( $id, $title, $callback, $screen, $context = 'advanced', $priority = 'default', $callback_args = null ) { return \function_exists('\\add_meta_box') ? \call_user_func_array('\\add_meta_box', \func_get_args()) : null; } 23 } 24 if ( ! function_exists( __NAMESPACE__ . '\\add_settings_field' ) ) { 25 function add_settings_field( $id, $title, $callback, $page, $section = 'default', $args = [] ) { return \function_exists('\\add_settings_field') ? \call_user_func_array('\\add_settings_field', \func_get_args()) : null; } 26 } 27 if ( ! function_exists( __NAMESPACE__ . '\\add_settings_section' ) ) { 28 function add_settings_section( $id, $title, $callback, $page ) { return \function_exists('\\add_settings_section') ? \call_user_func_array('\\add_settings_section', \func_get_args()) : null; } 29 } 30 if ( ! function_exists( __NAMESPACE__ . '\\register_setting' ) ) { 31 function register_setting( $option_group, $option_name, $args = [] ) { return \function_exists('\\register_setting') ? \call_user_func_array('\\register_setting', \func_get_args()) : null; } 32 } 33 if ( ! function_exists( __NAMESPACE__ . '\\__' ) ) { 34 function __( $text, $domain = 'default' ) { return \function_exists('\\__') ? \call_user_func_array('\\__', \func_get_args()) : $text; } 35 } 36 if ( ! function_exists( __NAMESPACE__ . '\\esc_html__' ) ) { 37 function esc_html__( $text, $domain = 'default' ) { return \function_exists('\\esc_html__') ? \call_user_func_array('\\esc_html__', \func_get_args()) : $text; } 38 } 39 if ( ! function_exists( __NAMESPACE__ . '\\esc_html' ) ) { 40 function esc_html( $text ) { return \function_exists('\\esc_html') ? \call_user_func_array('\\esc_html', \func_get_args()) : $text; } 41 } 42 if ( ! function_exists( __NAMESPACE__ . '\\esc_attr' ) ) { 43 function esc_attr( $text ) { return \function_exists('\\esc_attr') ? \call_user_func_array('\\esc_attr', \func_get_args()) : (string) $text; } 44 } 45 if ( ! function_exists( __NAMESPACE__ . '\\esc_url' ) ) { 46 function esc_url( $url ) { return \function_exists('\\esc_url') ? \call_user_func_array('\\esc_url', \func_get_args()) : (string) $url; } 47 } 48 if ( ! function_exists( __NAMESPACE__ . '\\selected' ) ) { 49 function selected( $selected, $current = true, $echo = true ) { return \function_exists('\\selected') ? \call_user_func_array('\\selected', \func_get_args()) : ''; } 50 } 51 if ( ! function_exists( __NAMESPACE__ . '\\checked' ) ) { 52 function checked( $checked, $current = true, $echo = true ) { return \function_exists('\\checked') ? \call_user_func_array('\\checked', \func_get_args()) : ''; } 53 } 54 if ( ! function_exists( __NAMESPACE__ . '\\submit_button' ) ) { 55 function submit_button( $text = null, $type = 'primary', $name = 'submit', $wrap = true, $other_attributes = null ) { return \function_exists('\\submit_button') ? \call_user_func_array('\\submit_button', \func_get_args()) : null; } 56 } 57 if ( ! function_exists( __NAMESPACE__ . '\\admin_url' ) ) { 58 function admin_url( $path = '', $scheme = 'admin' ) { return \function_exists('\\admin_url') ? \call_user_func_array('\\admin_url', \func_get_args()) : (string) $path; } 59 } 60 if ( ! function_exists( __NAMESPACE__ . '\\get_option' ) ) { 61 function get_option( $option, $default = false ) { return \function_exists('\\get_option') ? \call_user_func_array('\\get_option', \func_get_args()) : $default; } 62 } 63 if ( ! function_exists( __NAMESPACE__ . '\\get_post_types' ) ) { 64 function get_post_types( $args = [], $output = 'names', $operator = 'and' ) { return \function_exists('\\get_post_types') ? \call_user_func_array('\\get_post_types', \func_get_args()) : []; } 65 } 66 if ( ! function_exists( __NAMESPACE__ . '\\get_post_type' ) ) { 67 function get_post_type( $post = null ) { return \function_exists('\\get_post_type') ? \call_user_func_array('\\get_post_type', \func_get_args()) : null; } 68 } 69 if ( ! function_exists( __NAMESPACE__ . '\\get_post_type_object' ) ) { 70 function get_post_type_object( $post_type ) { return \function_exists('\\get_post_type_object') ? \call_user_func_array('\\get_post_type_object', \func_get_args()) : null; } 71 } 72 if ( ! function_exists( __NAMESPACE__ . '\\is_post_type_hierarchical' ) ) { 73 function is_post_type_hierarchical( $post_type ) { return \function_exists('\\is_post_type_hierarchical') ? \call_user_func_array('\\is_post_type_hierarchical', \func_get_args()) : false; } 74 } 75 if ( ! function_exists( __NAMESPACE__ . '\\get_current_screen' ) ) { 76 function get_current_screen() { return \function_exists('\\get_current_screen') ? \call_user_func_array('\\get_current_screen', \func_get_args()) : null; } 77 } 78 if ( ! function_exists( __NAMESPACE__ . '\\wp_enqueue_script' ) ) { 79 function wp_enqueue_script( $handle, $src = '', $deps = [], $ver = false, $in_footer = false ) { return \function_exists('\\wp_enqueue_script') ? \call_user_func_array('\\wp_enqueue_script', \func_get_args()) : null; } 80 } 81 if ( ! function_exists( __NAMESPACE__ . '\\wp_enqueue_style' ) ) { 82 function wp_enqueue_style( $handle, $src = '', $deps = [], $ver = false, $media = 'all' ) { return \function_exists('\\wp_enqueue_style') ? \call_user_func_array('\\wp_enqueue_style', \func_get_args()) : null; } 83 } 84 if ( ! function_exists( __NAMESPACE__ . '\\wp_localize_script' ) ) { 85 function wp_localize_script( $handle, $object_name, $l10n ) { return \function_exists('\\wp_localize_script') ? \call_user_func_array('\\wp_localize_script', \func_get_args()) : null; } 86 } 87 if ( ! function_exists( __NAMESPACE__ . '\\wp_nonce_field' ) ) { 88 function wp_nonce_field( $action = -1, $name = '_wpnonce', $referer = true, $echo = true ) { return \function_exists('\\wp_nonce_field') ? \call_user_func_array('\\wp_nonce_field', \func_get_args()) : null; } 89 } 90 if ( ! function_exists( __NAMESPACE__ . '\\wp_nonce_url' ) ) { 91 function wp_nonce_url( $actionurl, $action = -1, $name = '_wpnonce' ) { return \function_exists('\\wp_nonce_url') ? \call_user_func_array('\\wp_nonce_url', \func_get_args()) : (string) $actionurl; } 92 } 93 if ( ! function_exists( __NAMESPACE__ . '\\wp_unslash' ) ) { 94 function wp_unslash( $value ) { return \function_exists('\\wp_unslash') ? \call_user_func_array('\\wp_unslash', \func_get_args()) : $value; } 95 } 96 if ( ! function_exists( __NAMESPACE__ . '\\wp_create_nonce' ) ) { 97 function wp_create_nonce( $action = -1 ) { return \function_exists('\\wp_create_nonce') ? \call_user_func_array('\\wp_create_nonce', \func_get_args()) : ''; } 98 } 99 if ( ! function_exists( __NAMESPACE__ . '\\sanitize_text_field' ) ) { 100 function sanitize_text_field( $str ) { return \function_exists('\\sanitize_text_field') ? \call_user_func_array('\\sanitize_text_field', \func_get_args()) : (string) $str; } 101 } 102 if ( ! function_exists( __NAMESPACE__ . '\\get_the_title' ) ) { 103 function get_the_title( $post = 0 ) { return \function_exists('\\get_the_title') ? \call_user_func_array('\\get_the_title', \func_get_args()) : ''; } 104 } 105 106 9 107 10 108 /** … … 28 126 */ 29 127 private const DEFAULT_SETTINGS = [ 128 // Back-compat legacy fields 30 129 'redirect_after_duplicate' => false, 31 130 'default_prefix' => '', … … 43 142 'custom_title_pattern' => '', 44 143 'custom_slug_pattern' => '', 144 145 // New fields 146 'enabled_post_types' => [], 147 'enabled_roles' => [ 'administrator', 'editor' ], 148 'show_row_actions' => true, 149 'show_submitbox' => true, 150 'show_admin_bar' => false, 151 'show_bulk' => true, 152 'copy_author' => false, 153 'copy_date' => false, 154 'copy_password' => true, 155 'copy_excerpt' => true, 156 'copy_template' => true, 157 'copy_post_format' => true, 158 'copy_sticky' => false, 159 'copy_menu_order' => true, 160 'increase_menu_order_by' => 0, 161 'copy_children' => false, 162 'media_attachment_handling'=> 'reference', 163 'meta_blacklist' => '_edit_lock,_edit_last', 164 'tax_blacklist' => '', 165 'redirect_after' => 'list', 166 'copy_exact_slug' => false, 167 'status_same_as_original' => false, 168 // Original surfacing 169 'show_original_column' => false, 170 'show_copy_post_state' => false, 171 'show_original_metabox' => false, 45 172 ]; 46 173 … … 53 180 */ 54 181 public static function init(): void { 55 add_action( 'admin_menu', [ __CLASS__, 'add_settings_page' ] ); 56 add_action( 'admin_init', [ __CLASS__, 'register_settings' ] ); 57 add_filter( 'bulk_actions-edit-post', [ __CLASS__, 'register_bulk_action' ] ); 58 add_filter( 'bulk_actions-edit-page', [ __CLASS__, 'register_bulk_action' ] ); 59 add_action( 'handle_bulk_actions-edit-post', [ __CLASS__, 'handle_bulk_action' ], 10, 3 ); 60 add_action( 'handle_bulk_actions-edit-page', [ __CLASS__, 'handle_bulk_action' ], 10, 3 ); 182 \add_action( 'admin_menu', [ __CLASS__, 'add_settings_page' ] ); 183 \add_action( 'admin_init', [ __CLASS__, 'register_settings' ] ); 184 185 // Clear logs action 186 \add_action( 'admin_post_jd_clear_log', [ __CLASS__, 'handle_clear_log' ] ); 187 188 // Dynamic bulk registration for enabled CPTs 189 \add_action( 'current_screen', [ __CLASS__, 'register_bulk_for_screen' ] ); 190 191 // Submitbox buttons 192 \add_action( 'post_submitbox_misc_actions', [ __CLASS__, 'render_submitbox_buttons' ] ); 193 194 // Admin Bar menu 195 \add_action( 'admin_bar_menu', [ __CLASS__, 'admin_bar_menu' ], 100 ); 196 197 // Gutenberg panel 198 \add_action( 'enqueue_block_editor_assets', [ __CLASS__, 'enqueue_gutenberg_assets' ] ); 199 200 // List table column and post state 201 \add_filter( 'display_post_states', [ __CLASS__, 'maybe_add_post_state' ], 10, 2 ); 202 \add_filter( 'manage_posts_columns', [ __CLASS__, 'add_original_column' ] ); 203 \add_filter( 'manage_pages_columns', [ __CLASS__, 'add_original_column' ] ); 204 \add_action( 'manage_posts_custom_column', [ __CLASS__, 'render_original_column' ], 10, 2 ); 205 \add_action( 'manage_pages_custom_column', [ __CLASS__, 'render_original_column' ], 10, 2 ); 206 \add_action( 'add_meta_boxes', [ __CLASS__, 'register_original_metabox' ] ); 61 207 } 62 208 … … 67 213 */ 68 214 public static function add_settings_page(): void { 69 add_menu_page(70 __( 'Just Duplicate', 'just-duplicate' ),71 __( 'Just Duplicate', 'just-duplicate' ),215 \add_menu_page( 216 \__( 'Just Duplicate', 'just-duplicate' ), 217 \__( 'Just Duplicate', 'just-duplicate' ), 72 218 'manage_options', 73 219 'just-duplicate-settings', … … 83 229 */ 84 230 public static function register_settings(): void { 85 register_setting(231 \register_setting( 86 232 self::OPTION_KEY, // Option group name. 87 233 self::OPTION_KEY, // Option name. 88 234 [ 89 235 'type' => 'array', 90 'description' => __( 'Settings for Just Duplicate', 'just-duplicate' ),236 'description' => \__( 'Settings for Just Duplicate', 'just-duplicate' ), 91 237 'sanitize_callback' => [ __CLASS__, 'sanitize_settings' ], // Explicitly defined callback 92 238 'show_in_rest' => false, … … 95 241 ); 96 242 97 add_settings_section( 98 'JUST_DUPLICATE_general', 99 __( 'General Settings', 'just-duplicate' ), 100 '__return_false', 101 self::OPTION_KEY 102 ); 103 104 // Redirect After Duplication field. 105 add_settings_field( 243 \add_settings_section( 'JUST_DUPLICATE_general', \__( 'General Settings', 'just-duplicate' ), '__return_false', self::OPTION_KEY ); 244 \add_settings_section( 'JUST_DUPLICATE_links', \__( 'Link Locations', 'just-duplicate' ), '__return_false', self::OPTION_KEY ); 245 \add_settings_section( 'JUST_DUPLICATE_access', \__( 'Access Control', 'just-duplicate' ), '__return_false', self::OPTION_KEY ); 246 \add_settings_section( 'JUST_DUPLICATE_copy', \__( 'What to Copy', 'just-duplicate' ), '__return_false', self::OPTION_KEY ); 247 \add_settings_section( 'JUST_DUPLICATE_behavior', \__( 'Behavior', 'just-duplicate' ), '__return_false', self::OPTION_KEY ); 248 249 // Redirect After Duplication (legacy checkbox kept for BC). 250 \add_settings_field( 106 251 'redirect_after_duplicate', 107 __( 'Redirect After Duplication', 'just-duplicate' ),252 \__( 'Redirect After Duplication', 'just-duplicate' ), 108 253 [ __CLASS__, 'render_redirect_field' ], 109 254 self::OPTION_KEY, … … 112 257 113 258 // Default Prefix field. 114 add_settings_field(259 \add_settings_field( 115 260 'default_prefix', 116 __( 'Default Prefix', 'just-duplicate' ),261 \__( 'Default Prefix', 'just-duplicate' ), 117 262 [ __CLASS__, 'render_prefix_field' ], 118 263 self::OPTION_KEY, … … 121 266 122 267 // Default Suffix field. 123 add_settings_field(268 \add_settings_field( 124 269 'default_suffix', 125 __( 'Default Suffix', 'just-duplicate' ),270 \__( 'Default Suffix', 'just-duplicate' ), 126 271 [ __CLASS__, 'render_suffix_field' ], 127 272 self::OPTION_KEY, … … 130 275 131 276 // Duplicate Post Meta field. 132 add_settings_field(277 \add_settings_field( 133 278 'duplicate_post_meta', 134 __( 'Duplicate Post Meta', 'just-duplicate' ),279 \__( 'Duplicate Post Meta', 'just-duplicate' ), 135 280 [ __CLASS__, 'render_duplicate_post_meta_field' ], 136 281 self::OPTION_KEY, … … 139 284 140 285 // Duplicate Taxonomies field. 141 add_settings_field(286 \add_settings_field( 142 287 'duplicate_taxonomies', 143 __( 'Duplicate Taxonomies', 'just-duplicate' ),288 \__( 'Duplicate Taxonomies', 'just-duplicate' ), 144 289 [ __CLASS__, 'render_duplicate_taxonomies_field' ], 145 290 self::OPTION_KEY, … … 148 293 149 294 // Duplicate Attachments field. 150 add_settings_field(295 \add_settings_field( 151 296 'duplicate_attachments', 152 __( 'Duplicate Attachments', 'just-duplicate' ),297 \__( 'Duplicate Attachments', 'just-duplicate' ), 153 298 [ __CLASS__, 'render_duplicate_attachments_field' ], 154 299 self::OPTION_KEY, … … 157 302 158 303 // Duplicate Custom Fields field. 159 add_settings_field(304 \add_settings_field( 160 305 'duplicate_custom_fields', 161 __( 'Duplicate Custom Fields', 'just-duplicate' ),306 \__( 'Duplicate Custom Fields', 'just-duplicate' ), 162 307 [ __CLASS__, 'render_duplicate_custom_fields_field' ], 163 308 self::OPTION_KEY, … … 166 311 167 312 // Duplicate Custom Taxonomies field. 168 add_settings_field(313 \add_settings_field( 169 314 'duplicate_custom_taxonomies', 170 __( 'Duplicate Custom Taxonomies', 'just-duplicate' ),315 \__( 'Duplicate Custom Taxonomies', 'just-duplicate' ), 171 316 [ __CLASS__, 'render_duplicate_custom_taxonomies_field' ], 172 317 self::OPTION_KEY, … … 175 320 176 321 // Duplicate Comments field. 177 add_settings_field(322 \add_settings_field( 178 323 'duplicate_comments', 179 __( 'Duplicate Comments', 'just-duplicate' ),324 \__( 'Duplicate Comments', 'just-duplicate' ), 180 325 [ __CLASS__, 'render_duplicate_comments_field' ], 181 326 self::OPTION_KEY, … … 184 329 185 330 // Duplicate Featured Image field. 186 add_settings_field(331 \add_settings_field( 187 332 'duplicate_featured_image', 188 __( 'Duplicate Featured Image', 'just-duplicate' ),333 \__( 'Duplicate Featured Image', 'just-duplicate' ), 189 334 [ __CLASS__, 'render_duplicate_featured_image_field' ], 190 335 self::OPTION_KEY, … … 193 338 194 339 // Schedule Duplication field. 195 add_settings_field(340 \add_settings_field( 196 341 'schedule_duplication', 197 __( 'Schedule Duplication', 'just-duplicate' ),342 \__( 'Schedule Duplication', 'just-duplicate' ), 198 343 [ __CLASS__, 'render_schedule_duplication_field' ], 199 344 self::OPTION_KEY, … … 202 347 203 348 // Custom Title field. 204 add_settings_field(349 \add_settings_field( 205 350 'custom_title', 206 __( 'Custom Title', 'just-duplicate' ),351 \__( 'Custom Title', 'just-duplicate' ), 207 352 [ __CLASS__, 'render_custom_title_field' ], 208 353 self::OPTION_KEY, … … 211 356 212 357 // Custom Slug field. 213 add_settings_field(358 \add_settings_field( 214 359 'custom_slug', 215 __( 'Custom Slug', 'just-duplicate' ),360 \__( 'Custom Slug', 'just-duplicate' ), 216 361 [ __CLASS__, 'render_custom_slug_field' ], 217 362 self::OPTION_KEY, … … 220 365 221 366 // Custom Post Status field. 222 add_settings_field(367 \add_settings_field( 223 368 'custom_post_status', 224 __( 'Duplicated Post Status', 'just-duplicate' ),369 \__( 'Duplicated Post Status', 'just-duplicate' ), 225 370 [ __CLASS__, 'render_custom_post_status_field' ], 226 371 self::OPTION_KEY, … … 229 374 230 375 // Duplicate ACF Meta field. 231 add_settings_field(376 \add_settings_field( 232 377 'duplicate_acf_meta', 233 __( 'Duplicate ACF Meta', 'just-duplicate' ),378 \__( 'Duplicate ACF Meta', 'just-duplicate' ), 234 379 [ __CLASS__, 'render_duplicate_acf_meta_field' ], 235 380 self::OPTION_KEY, … … 238 383 239 384 // Duplicate SEO Meta field. 240 add_settings_field(385 \add_settings_field( 241 386 'duplicate_seo_meta', 242 __( 'Duplicate SEO Meta', 'just-duplicate' ),387 \__( 'Duplicate SEO Meta', 'just-duplicate' ), 243 388 [ __CLASS__, 'render_duplicate_seo_meta_field' ], 244 389 self::OPTION_KEY, … … 247 392 248 393 // Add fields for custom title and slug patterns. 249 add_settings_field(394 \add_settings_field( 250 395 'custom_title_pattern', 251 __( 'Custom Title Pattern', 'just-duplicate' ),396 \__( 'Custom Title Pattern', 'just-duplicate' ), 252 397 [ __CLASS__, 'render_custom_title_pattern_field' ], 253 398 self::OPTION_KEY, … … 255 400 ); 256 401 257 add_settings_field(402 \add_settings_field( 258 403 'custom_slug_pattern', 259 __( 'Custom Slug Pattern', 'just-duplicate' ),404 \__( 'Custom Slug Pattern', 'just-duplicate' ), 260 405 [ __CLASS__, 'render_custom_slug_pattern_field' ], 261 406 self::OPTION_KEY, … … 264 409 265 410 // Add a setting for media attachment handling. 266 add_settings_field(411 \add_settings_field( 267 412 'media_attachment_handling', 268 __( 'Media Attachment Handling', 'just-duplicate' ),413 \__( 'Media Attachment Handling', 'just-duplicate' ), 269 414 [ __CLASS__, 'render_media_attachment_handling_field' ], 270 415 self::OPTION_KEY, 271 416 'JUST_DUPLICATE_general' 272 417 ); 418 419 // Link locations 420 \add_settings_field( 'show_row_actions', \__( 'Show in Row Actions', 'just-duplicate' ), [ __CLASS__, 'render_show_row_actions_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_links' ); 421 \add_settings_field( 'show_submitbox', \__( 'Show in Edit Screen', 'just-duplicate' ), [ __CLASS__, 'render_show_submitbox_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_links' ); 422 \add_settings_field( 'show_admin_bar', \__( 'Show in Admin Bar', 'just-duplicate' ), [ __CLASS__, 'render_show_admin_bar_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_links' ); 423 \add_settings_field( 'show_bulk', \__( 'Enable Bulk Action', 'just-duplicate' ), [ __CLASS__, 'render_show_bulk_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_links' ); 424 425 // Access control 426 \add_settings_field( 'enabled_post_types', \__( 'Enabled Post Types', 'just-duplicate' ), [ __CLASS__, 'render_enabled_post_types_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_access' ); 427 \add_settings_field( 'enabled_roles', \__( 'Enabled Roles', 'just-duplicate' ), [ __CLASS__, 'render_enabled_roles_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_access' ); 428 429 // What to copy 430 \add_settings_field( 'copy_author', \__( 'Copy Author', 'just-duplicate' ), [ __CLASS__, 'render_checkbox_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_copy', [ 'key' => 'copy_author' ] ); 431 \add_settings_field( 'copy_date', \__( 'Copy Date', 'just-duplicate' ), [ __CLASS__, 'render_checkbox_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_copy', [ 'key' => 'copy_date' ] ); 432 \add_settings_field( 'copy_password', \__( 'Copy Password', 'just-duplicate' ), [ __CLASS__, 'render_checkbox_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_copy', [ 'key' => 'copy_password' ] ); 433 \add_settings_field( 'copy_excerpt', \__( 'Copy Excerpt', 'just-duplicate' ), [ __CLASS__, 'render_checkbox_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_copy', [ 'key' => 'copy_excerpt' ] ); 434 \add_settings_field( 'copy_template', \__( 'Copy Page Template', 'just-duplicate' ), [ __CLASS__, 'render_checkbox_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_copy', [ 'key' => 'copy_template' ] ); 435 \add_settings_field( 'copy_post_format', \__( 'Copy Post Format', 'just-duplicate' ), [ __CLASS__, 'render_checkbox_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_copy', [ 'key' => 'copy_post_format' ] ); 436 \add_settings_field( 'copy_sticky', \__( 'Copy Sticky', 'just-duplicate' ), [ __CLASS__, 'render_checkbox_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_copy', [ 'key' => 'copy_sticky' ] ); 437 \add_settings_field( 'copy_menu_order', \__( 'Copy Menu Order', 'just-duplicate' ), [ __CLASS__, 'render_checkbox_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_copy', [ 'key' => 'copy_menu_order' ] ); 438 \add_settings_field( 'increase_menu_order_by', \__( 'Increase Menu Order by', 'just-duplicate' ), [ __CLASS__, 'render_number_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_copy', [ 'key' => 'increase_menu_order_by' ] ); 439 \add_settings_field( 'copy_children', \__( 'Copy Children (one level)', 'just-duplicate' ), [ __CLASS__, 'render_checkbox_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_copy', [ 'key' => 'copy_children' ] ); 440 \add_settings_field( 'meta_blacklist', \__( 'Meta Blacklist (CSV, * supported)', 'just-duplicate' ), [ __CLASS__, 'render_text_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_copy', [ 'key' => 'meta_blacklist' ] ); 441 \add_settings_field( 'tax_blacklist', \__( 'Taxonomy Blacklist (CSV, * supported)', 'just-duplicate' ), [ __CLASS__, 'render_text_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_copy', [ 'key' => 'tax_blacklist' ] ); 442 443 // Behavior 444 \add_settings_field( 'redirect_after', \__( 'Redirect After Action', 'just-duplicate' ), [ __CLASS__, 'render_redirect_after_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_behavior' ); 445 \add_settings_field( 'copy_exact_slug', \__( 'Copy Exact Slug', 'just-duplicate' ), [ __CLASS__, 'render_checkbox_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_behavior', [ 'key' => 'copy_exact_slug' ] ); 446 \add_settings_field( 'status_same_as_original', \__( 'Status: Same as original', 'just-duplicate' ), [ __CLASS__, 'render_checkbox_field' ], self::OPTION_KEY, 'JUST_DUPLICATE_behavior', [ 'key' => 'status_same_as_original' ] ); 273 447 } 274 448 … … 280 454 */ 281 455 public static function sanitize_settings( array $settings ): array { 456 // Populate dynamic post types default if empty 457 $show_ui_types = array_keys( \get_post_types( [ 'show_ui' => true ], 'names' ) ); 458 $roles = \wp_roles(); 459 $all_roles = is_object( $roles ) ? array_keys( $roles->roles ) : [ 'administrator', 'editor' ]; 460 282 461 return [ 283 462 'redirect_after_duplicate' => isset( $settings['redirect_after_duplicate'] ) ? (bool) $settings['redirect_after_duplicate'] : false, 284 'default_prefix' => sanitize_text_field( $settings['default_prefix'] ?? '' ),463 'default_prefix' => \sanitize_text_field( $settings['default_prefix'] ?? '' ), 285 464 'default_suffix' => sanitize_text_field( $settings['default_suffix'] ?? '' ), 286 465 'duplicate_post_meta' => isset( $settings['duplicate_post_meta'] ) ? (bool) $settings['duplicate_post_meta'] : true, … … 299 478 'custom_title_pattern' => sanitize_text_field( $settings['custom_title_pattern'] ?? '' ), 300 479 'custom_slug_pattern' => sanitize_text_field( $settings['custom_slug_pattern'] ?? '' ), 301 'media_attachment_handling' => $settings['media_attachment_handling'] ?? 'duplicate', 480 'media_attachment_handling' => in_array( ( $settings['media_attachment_handling'] ?? 'reference' ), [ 'reference','duplicate' ], true ) ? $settings['media_attachment_handling'] : 'reference', 481 482 // New settings 483 'enabled_post_types' => array_values( array_intersect( (array) ( $settings['enabled_post_types'] ?? $show_ui_types ), $show_ui_types ) ), 484 'enabled_roles' => array_values( array_intersect( (array) ( $settings['enabled_roles'] ?? [ 'administrator','editor' ] ), $all_roles ) ), 485 'show_row_actions' => isset( $settings['show_row_actions'] ) ? (bool) $settings['show_row_actions'] : true, 486 'show_submitbox' => isset( $settings['show_submitbox'] ) ? (bool) $settings['show_submitbox'] : true, 487 'show_admin_bar' => isset( $settings['show_admin_bar'] ) ? (bool) $settings['show_admin_bar'] : false, 488 'show_bulk' => isset( $settings['show_bulk'] ) ? (bool) $settings['show_bulk'] : true, 489 'copy_author' => isset( $settings['copy_author'] ) ? (bool) $settings['copy_author'] : false, 490 'copy_date' => isset( $settings['copy_date'] ) ? (bool) $settings['copy_date'] : false, 491 'copy_password' => isset( $settings['copy_password'] ) ? (bool) $settings['copy_password'] : true, 492 'copy_excerpt' => isset( $settings['copy_excerpt'] ) ? (bool) $settings['copy_excerpt'] : true, 493 'copy_template' => isset( $settings['copy_template'] ) ? (bool) $settings['copy_template'] : true, 494 'copy_post_format' => isset( $settings['copy_post_format'] ) ? (bool) $settings['copy_post_format'] : true, 495 'copy_sticky' => isset( $settings['copy_sticky'] ) ? (bool) $settings['copy_sticky'] : false, 496 'copy_menu_order' => isset( $settings['copy_menu_order'] ) ? (bool) $settings['copy_menu_order'] : true, 497 'increase_menu_order_by' => isset( $settings['increase_menu_order_by'] ) ? (int) $settings['increase_menu_order_by'] : 0, 498 'copy_children' => isset( $settings['copy_children'] ) ? (bool) $settings['copy_children'] : false, 499 'meta_blacklist' => sanitize_text_field( $settings['meta_blacklist'] ?? '_edit_lock,_edit_last' ), 500 'tax_blacklist' => sanitize_text_field( $settings['tax_blacklist'] ?? '' ), 501 'redirect_after' => in_array( ( $settings['redirect_after'] ?? 'list' ), [ 'list','editor','original' ], true ) ? $settings['redirect_after'] : 'list', 502 'copy_exact_slug' => isset( $settings['copy_exact_slug'] ) ? (bool) $settings['copy_exact_slug'] : false, 503 'status_same_as_original' => isset( $settings['status_same_as_original'] ) ? (bool) $settings['status_same_as_original'] : false, 504 'show_original_column' => isset( $settings['show_original_column'] ) ? (bool) $settings['show_original_column'] : false, 505 'show_copy_post_state' => isset( $settings['show_copy_post_state'] ) ? (bool) $settings['show_copy_post_state'] : false, 506 'show_original_metabox' => isset( $settings['show_original_metabox'] ) ? (bool) $settings['show_original_metabox'] : false, 302 507 ]; 303 508 } … … 314 519 <input type="checkbox" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[redirect_after_duplicate]" value="1" <?php checked( $settings['redirect_after_duplicate'] ?? false, true ); ?> /> 315 520 <label><?php esc_html_e( 'Redirect to the edit screen of the duplicated item.', 'just-duplicate' ); ?></label> 521 </p> 522 <?php 523 } 524 525 public static function render_redirect_after_field(): void { 526 $settings = get_option( self::OPTION_KEY, [] ); 527 $val = $settings['redirect_after'] ?? 'list'; 528 ?> 529 <p> 530 <label><input type="radio" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[redirect_after]" value="list" <?php checked( $val, 'list' ); ?> /> <?php esc_html_e( 'List (default)', 'just-duplicate' ); ?></label><br /> 531 <label><input type="radio" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[redirect_after]" value="editor" <?php checked( $val, 'editor' ); ?> /> <?php esc_html_e( 'Editor', 'just-duplicate' ); ?></label><br /> 532 <label><input type="radio" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[redirect_after]" value="original" <?php checked( $val, 'original' ); ?> /> <?php esc_html_e( 'Original', 'just-duplicate' ); ?></label> 316 533 </p> 317 534 <?php … … 586 803 <p> 587 804 <select name="<?php echo esc_attr( self::OPTION_KEY ); ?>[media_attachment_handling]"> 588 <option value=" duplicate" <?php selected( $settings['media_attachment_handling'] ?? 'duplicate', 'duplicate' ); ?>><?php esc_html_e( 'Duplicate Media', 'just-duplicate' ); ?></option>589 <option value=" reference" <?php selected( $settings['media_attachment_handling'] ?? 'duplicate', 'reference' ); ?>><?php esc_html_e( 'Reference Originals', 'just-duplicate' ); ?></option>805 <option value="reference" <?php selected( $settings['media_attachment_handling'] ?? 'reference', 'reference' ); ?>><?php esc_html_e( 'Reference Originals', 'just-duplicate' ); ?></option> 806 <option value="duplicate" <?php selected( $settings['media_attachment_handling'] ?? 'reference', 'duplicate' ); ?>><?php esc_html_e( 'Duplicate Media', 'just-duplicate' ); ?></option> 590 807 </select> 591 808 <span class="description"><?php esc_html_e( 'Choose whether to duplicate attached media or reference the originals.', 'just-duplicate' ); ?></span> 592 809 </p> 593 810 <?php 811 } 812 813 // Link location fields 814 public static function render_show_row_actions_field(): void { self::render_checkbox_field_common( 'show_row_actions', __( 'Show Clone/New Draft in list rows', 'just-duplicate' ) ); } 815 public static function render_show_submitbox_field(): void { self::render_checkbox_field_common( 'show_submitbox', __( 'Show buttons in edit screen', 'just-duplicate' ) ); } 816 public static function render_show_admin_bar_field(): void { self::render_checkbox_field_common( 'show_admin_bar', __( 'Show in admin bar', 'just-duplicate' ) ); } 817 public static function render_show_bulk_field(): void { self::render_checkbox_field_common( 'show_bulk', __( 'Enable bulk action', 'just-duplicate' ) ); } 818 819 private static function render_checkbox_field_common( string $key, string $label ): void { 820 $settings = get_option( self::OPTION_KEY, [] ); 821 ?> 822 <p> 823 <input type="checkbox" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[<?php echo esc_attr( $key ); ?>]" value="1" <?php checked( $settings[ $key ] ?? false, true ); ?> /> 824 <label><?php echo esc_html( $label ); ?></label> 825 </p> 826 <?php 827 } 828 829 // Generic checkbox/number/text 830 public static function render_checkbox_field( array $args ): void { 831 $key = $args['key'] ?? ''; 832 $settings = get_option( self::OPTION_KEY, [] ); 833 ?> 834 <p><input type="checkbox" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[<?php echo esc_attr( $key ); ?>]" value="1" <?php checked( $settings[ $key ] ?? false, true ); ?> /></p> 835 <?php 836 } 837 public static function render_number_field( array $args ): void { 838 $key = $args['key'] ?? ''; 839 $settings = get_option( self::OPTION_KEY, [] ); 840 ?> 841 <p><input type="number" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[<?php echo esc_attr( $key ); ?>]" value="<?php echo esc_attr( (string) ( $settings[ $key ] ?? '0' ) ); ?>" /></p> 842 <?php 843 } 844 public static function render_text_field( array $args ): void { 845 $key = $args['key'] ?? ''; 846 $settings = get_option( self::OPTION_KEY, [] ); 847 ?> 848 <p><input type="text" class="regular-text" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[<?php echo esc_attr( $key ); ?>]" value="<?php echo esc_attr( (string) ( $settings[ $key ] ?? '' ) ); ?>" /></p> 849 <?php 850 } 851 852 // Enabled post types and roles 853 public static function render_enabled_post_types_field(): void { 854 $settings = get_option( self::OPTION_KEY, [] ); 855 $selected = (array) ( $settings['enabled_post_types'] ?? [] ); 856 $types = get_post_types( [ 'show_ui' => true ], 'objects' ); 857 echo '<select multiple name="' . esc_attr( self::OPTION_KEY ) . '[enabled_post_types][]" size="8" style="min-width: 280px;">'; 858 foreach ( $types as $type => $obj ) { 859 printf( '<option value="%1$s" %2$s>%3$s</option>', esc_attr( $type ), selected( in_array( $type, $selected, true ), true, false ), esc_html( $obj->labels->name ) ); 860 } 861 echo '</select>'; 862 } 863 public static function render_enabled_roles_field(): void { 864 $settings = get_option( self::OPTION_KEY, [] ); 865 $selected = (array) ( $settings['enabled_roles'] ?? [ 'administrator','editor' ] ); 866 $roles = wp_roles(); 867 $all = is_object( $roles ) ? $roles->roles : []; 868 echo '<select multiple name="' . esc_attr( self::OPTION_KEY ) . '[enabled_roles][]" size="8" style="min-width: 280px;">'; 869 foreach ( $all as $role_key => $role_obj ) { 870 printf( '<option value="%1$s" %2$s>%3$s</option>', esc_attr( $role_key ), selected( in_array( $role_key, $selected, true ), true, false ), esc_html( $role_obj['name'] ) ); 871 } 872 echo '</select>'; 873 } 874 875 // Dynamic bulk registration per CPT 876 public static function register_bulk_for_screen( \WP_Screen $screen ): void { 877 $settings = get_option( self::OPTION_KEY, [] ); 878 if ( empty( $settings['show_bulk'] ) ) { return; } 879 if ( strpos( $screen->id, 'edit-' ) === 0 ) { 880 $post_type = substr( $screen->id, 5 ); 881 if ( ! self::is_post_type_enabled( $post_type ) ) { return; } 882 add_filter( 'bulk_actions-edit-' . $post_type, [ __CLASS__, 'register_bulk_action' ] ); 883 add_filter( 'handle_bulk_actions-edit-' . $post_type, [ \Just_Duplicate\Duplicate_Handler::class, 'handle_bulk_action' ], 10, 3 ); 884 } 594 885 } 595 886 … … 620 911 } 621 912 foreach ( $post_ids as $post_id ) { 622 self::duplicate_post( (int) $post_id);913 \Just_Duplicate\Duplicate_Handler::duplicate_post( (int) $post_id, [] ); 623 914 } 624 915 return add_query_arg( 'bulk_duplicated', count( $post_ids ), $redirect_url ); … … 685 976 } 686 977 978 // Submitbox UI 979 public static function render_submitbox_buttons(): void { 980 $settings = get_option( self::OPTION_KEY, [] ); 981 if ( empty( $settings['show_submitbox'] ) ) { return; } 982 global $post; 983 if ( ! $post ) { return; } 984 if ( ! self::is_post_type_enabled( $post->post_type ) || ! current_user_can( 'edit_post', $post->ID ) || ! self::current_user_role_allowed() ) { return; } 985 $nonce = wp_create_nonce( 'just_dupe_' . $post->ID ); 986 $clone = admin_url( 'admin.php?action=jd_clone&post=' . $post->ID . '&_wpnonce=' . $nonce ); 987 $draft = admin_url( 'admin.php?action=jd_new_draft&post=' . $post->ID . '&_wpnonce=' . $nonce ); 988 $rewrite = admin_url( 'admin.php?action=jd_rewrite&post=' . $post->ID . '&_wpnonce=' . $nonce ); 989 echo '<div class="misc-pub-section">'; 990 echo '<a class="button" href="' . esc_url( $clone ) . '">' . esc_html__( 'Clone', 'just-duplicate' ) . '</a> '; 991 echo '<a class="button button-primary" href="' . esc_url( $draft ) . '">' . esc_html__( 'New Draft', 'just-duplicate' ) . '</a> '; 992 echo '<a class="button" href="' . esc_url( $rewrite ) . '">' . esc_html__( 'Rewrite & Republish', 'just-duplicate' ) . '</a>'; 993 echo '</div>'; 994 } 995 996 // Admin Bar 997 public static function admin_bar_menu( \WP_Admin_Bar $admin_bar ): void { 998 $settings = get_option( self::OPTION_KEY, [] ); 999 if ( empty( $settings['show_admin_bar'] ) ) { return; } 1000 global $pagenow, $post; 1001 if ( ! in_array( $pagenow, [ 'post.php', 'post-new.php' ], true ) || ! $post ) { return; } 1002 if ( ! self::is_post_type_enabled( $post->post_type ) || ! current_user_can( 'edit_post', $post->ID ) || ! self::current_user_role_allowed() ) { return; } 1003 $nonce = wp_create_nonce( 'just_dupe_' . $post->ID ); 1004 $parent_id = 'just-duplicate'; 1005 $admin_bar->add_node( [ 'id' => $parent_id, 'title' => __( 'Duplicate', 'just-duplicate' ), 'href' => false ] ); 1006 $admin_bar->add_node( [ 'id' => 'jd-clone', 'parent' => $parent_id, 'title' => __( 'Clone', 'just-duplicate' ), 'href' => admin_url( 'admin.php?action=jd_clone&post=' . $post->ID . '&_wpnonce=' . $nonce ) ] ); 1007 $admin_bar->add_node( [ 'id' => 'jd-new-draft', 'parent' => $parent_id, 'title' => __( 'New Draft', 'just-duplicate' ), 'href' => admin_url( 'admin.php?action=jd_new_draft&post=' . $post->ID . '&_wpnonce=' . $nonce ) ] ); 1008 $admin_bar->add_node( [ 'id' => 'jd-rewrite', 'parent' => $parent_id, 'title' => __( 'Rewrite & Republish', 'just-duplicate' ), 'href' => admin_url( 'admin.php?action=jd_rewrite&post=' . $post->ID . '&_wpnonce=' . $nonce ) ] ); 1009 } 1010 1011 // Gutenberg assets 1012 public static function enqueue_gutenberg_assets(): void { 1013 $settings = get_option( self::OPTION_KEY, [] ); 1014 if ( empty( $settings['show_submitbox'] ) ) { return; } 1015 global $post; 1016 if ( ! $post ) { return; } 1017 if ( ! self::is_post_type_enabled( $post->post_type ) || ! current_user_can( 'edit_post', $post->ID ) || ! self::current_user_role_allowed() ) { return; } 1018 $nonce = wp_create_nonce( 'just_dupe_' . $post->ID ); 1019 wp_enqueue_script( 'just-duplicate-gutenberg', JUST_DUPLICATE_URL . 'assets/js/gutenberg-duplicate.js', [ 'wp-edit-post', 'wp-plugins', 'wp-element', 'wp-components' ], JUST_DUPLICATE_VERSION, true ); 1020 wp_localize_script( 'just-duplicate-gutenberg', 'JustDuplicate', [ 1021 'cloneUrl' => admin_url( 'admin.php?action=jd_clone&post=' . $post->ID . '&_wpnonce=' . $nonce ), 1022 'draftUrl' => admin_url( 'admin.php?action=jd_new_draft&post=' . $post->ID . '&_wpnonce=' . $nonce ), 1023 'rewriteUrl' => admin_url( 'admin.php?action=jd_rewrite&post=' . $post->ID . '&_wpnonce=' . $nonce ), 1024 ] ); 1025 } 1026 1027 // List table columns and post state 1028 public static function add_original_column( array $columns ): array { 1029 $settings = get_option( self::OPTION_KEY, [] ); 1030 if ( empty( $settings['show_original_column'] ) ) { return $columns; } 1031 $columns['jd_original'] = __( 'Original', 'just-duplicate' ); 1032 return $columns; 1033 } 1034 public static function render_original_column( string $column, int $post_id ): void { 1035 if ( 'jd_original' !== $column ) { return; } 1036 $orig = (int) get_post_meta( $post_id, '_jd_original_post', true ); 1037 if ( $orig ) { 1038 $link = get_edit_post_link( $orig, '' ); 1039 echo $link ? '<a href="' . esc_url( $link ) . '">#' . esc_html( (string) $orig ) . '</a>' : '#' . esc_html( (string) $orig ); 1040 } 1041 } 1042 public static function maybe_add_post_state( array $states, \WP_Post $post ): array { 1043 $settings = get_option( self::OPTION_KEY, [] ); 1044 if ( empty( $settings['show_copy_post_state'] ) ) { return $states; } 1045 $orig = (int) get_post_meta( $post->ID, '_jd_original_post', true ); 1046 if ( $orig ) { 1047 $states['jd_copy'] = __( 'Copy', 'just-duplicate' ); 1048 } 1049 return $states; 1050 } 1051 public static function register_original_metabox(): void { 1052 $settings = get_option( self::OPTION_KEY, [] ); 1053 if ( empty( $settings['show_original_metabox'] ) ) { return; } 1054 add_meta_box( 'jd_original_meta', __( 'Original', 'just-duplicate' ), [ __CLASS__, 'render_original_metabox' ], null, 'side', 'default' ); 1055 } 1056 public static function render_original_metabox( \WP_Post $post ): void { 1057 $orig = (int) get_post_meta( $post->ID, '_jd_original_post', true ); 1058 if ( $orig ) { 1059 $link = get_edit_post_link( $orig, '' ); 1060 echo '<p>' . esc_html__( 'Original:', 'just-duplicate' ) . ' ' . ( $link ? '<a href="' . esc_url( $link ) . '">#' . esc_html( (string) $orig ) . '</a>' : '#' . esc_html( (string) $orig ) ) . '</p>'; 1061 } else { 1062 echo '<p>' . esc_html__( 'No original reference found.', 'just-duplicate' ) . '</p>'; 1063 } 1064 } 1065 1066 private static function is_post_type_enabled( string $post_type ): bool { 1067 $settings = get_option( self::OPTION_KEY, [] ); 1068 $enabled = (array) ( $settings['enabled_post_types'] ?? [] ); 1069 return empty( $enabled ) || in_array( $post_type, $enabled, true ); 1070 } 1071 private static function current_user_role_allowed(): bool { 1072 $settings = get_option( self::OPTION_KEY, [] ); 1073 $allowed = (array) ( $settings['enabled_roles'] ?? [ 'administrator','editor' ] ); 1074 $user = wp_get_current_user(); 1075 if ( empty( $user->roles ) ) { return false; } 1076 foreach ( $user->roles as $role ) { 1077 if ( in_array( $role, $allowed, true ) ) { return true; } 1078 } 1079 return false; 1080 } 1081 687 1082 /** 688 1083 * Copy metadata from the original post to the duplicated post. … … 765 1160 <div class="wrap just-duplicate-wrap"> 766 1161 <h1><?php esc_html_e( 'Just Duplicate Settings', 'just-duplicate' ); ?></h1> 1162 <?php if ( isset( $_GET['jd_log_cleared'] ) && '1' === $_GET['jd_log_cleared'] ) : ?> 1163 <div class="notice notice-success is-dismissible"><p><?php echo esc_html__( 'Duplication log cleared.', 'just-duplicate' ); ?></p></div> 1164 <?php endif; ?> 767 1165 <!-- Tabs Navigation --> 768 1166 <ul class="jd-tabs"> … … 852 1250 ?> 853 1251 <h2><?php esc_html_e( 'Duplication Report', 'just-duplicate' ); ?></h2> 1252 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="margin-bottom:12px;"> 1253 <input type="hidden" name="action" value="jd_clear_log" /> 1254 <?php wp_nonce_field( 'jd_clear_log_action', '_jdnonce' ); ?> 1255 <button type="submit" class="button button-secondary" onclick="return confirm('<?php echo esc_js( __( 'This will permanently clear the duplication log and related transients. Are you sure?', 'just-duplicate' ) ); ?>');"> 1256 <?php esc_html_e( 'Clear Log', 'just-duplicate' ); ?> 1257 </button> 1258 </form> 854 1259 <table class="wp-list-table widefat fixed striped"> 855 1260 <thead> … … 878 1283 879 1284 /** 1285 * Handle the Clear Log admin action. 1286 * 1287 * @return void 1288 */ 1289 public static function handle_clear_log(): void { 1290 if ( ! current_user_can( 'manage_options' ) ) { 1291 wp_die( esc_html__( 'You do not have permission to perform this action.', 'just-duplicate' ) ); 1292 } 1293 $nonce = isset( $_POST['_jdnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_jdnonce'] ) ) : ''; 1294 if ( ! wp_verify_nonce( $nonce, 'jd_clear_log_action' ) ) { 1295 wp_die( esc_html__( 'Nonce verification failed.', 'just-duplicate' ) ); 1296 } 1297 1298 // Clear option-based log. 1299 \Just_Duplicate\Duplicate_Logger::clear(); 1300 // Clear transient tracking the last duplicated post id. 1301 \delete_transient( 'just_duplicate_last_post_id' ); 1302 1303 /** 1304 * Fires after Just Duplicate logs are cleared. 1305 * Allows extensions to purge their own options/transients. 1306 */ 1307 \do_action( 'just_duplicate_logs_cleared' ); 1308 1309 // Redirect back to settings with a success flag; try to re-open the Report tab. 1310 $url = \add_query_arg( [ 1311 'page' => 'just-duplicate-settings', 1312 'jd_log_cleared' => '1', 1313 'jd_tab' => 'report', 1314 ], admin_url( 'admin.php' ) ); 1315 \wp_safe_redirect( $url ); 1316 exit; 1317 } 1318 1319 /** 880 1320 * Handle the preview duplication AJAX request. 881 1321 * -
just-duplicate/trunk/includes/class-duplicate-handler.php
r3297655 r3384189 7 7 exit; // Exit if accessed directly. 8 8 } 9 10 // Import WordPress global functions/constants for static analysis and clarity. 11 use const \HOUR_IN_SECONDS; 12 use function \absint; 13 use function \add_action; 14 use function \add_filter; 15 use function \add_query_arg; 16 use function \admin_url; 17 use function \apply_filters; 18 use function \check_ajax_referer; 19 use function \current_user_can; 20 use function \date_i18n; 21 use function \delete_post_meta; 22 use function \delete_transient; 23 use function \do_action; 24 use function \esc_html; 25 use function \esc_html__; 26 use function \esc_url; 27 use function \get_attached_file; 28 use function \get_post_types; 29 use function \get_children; 30 use function \get_comments; 31 use function \get_current_user_id; 32 use function \get_edit_post_link; 33 use function \get_object_taxonomies; 34 use function \get_post; 35 use function \get_post_field; 36 use function \get_post_format; 37 use function \get_post_meta; 38 use function \get_post_thumbnail_id; 39 use function \get_post_type; 40 use function \get_the_author_meta; 41 use function \get_transient; 42 use function \is_wp_error; 43 use function \maybe_unserialize; 44 use function \sanitize_text_field; 45 use function \sanitize_title; 46 use function \set_post_format; 47 use function \set_post_thumbnail; 48 use function \set_transient; 49 use function \stick_post; 50 use function \update_post_meta; 51 use function \wp_check_filetype; 52 use function \wp_create_nonce; 53 use function \wp_die; 54 use function \wp_generate_attachment_metadata; 55 use function \wp_get_current_user; 56 use function \wp_get_object_terms; 57 use function \wp_insert_attachment; 58 use function \wp_insert_comment; 59 use function \wp_insert_post; 60 use function \wp_kses_post; 61 use function \wp_nonce_url; 62 use function \wp_redirect; 63 use function \wp_safe_redirect; 64 use function \wp_send_json_error; 65 use function \wp_send_json_success; 66 use function \wp_set_object_terms; 67 use function \wp_trash_post; 68 use function \wp_unslash; 69 use function \wp_update_attachment_metadata; 70 use function \wp_update_post; 71 use function \wp_upload_dir; 72 use function \wp_verify_nonce; 9 73 10 74 /** … … 16 80 17 81 /** 82 * Cached settings for this request. 83 * 84 * @var array|null 85 */ 86 private static ?array $settings = null; 87 88 /** 89 * Allowed action slugs. 90 */ 91 private const ACTION_CLONE = 'jd_clone'; 92 private const ACTION_NEW_DRAFT = 'jd_new_draft'; 93 private const ACTION_REWRITE = 'jd_rewrite'; 94 private const NONCE_ACTION_PREFIX = 'just_dupe_'; 95 96 /** 18 97 * Initialize the duplication handler. 19 98 * … … 23 102 */ 24 103 public static function init(): void { 25 // Add duplicate action links for posts and pages. 26 add_filter( 'post_row_actions', [ __CLASS__, 'add_duplicate_link' ], 10, 2 ); 27 add_filter( 'page_row_actions', [ __CLASS__, 'add_duplicate_link' ], 10, 2 ); 28 29 // Handle the duplication action. 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' ] ); 104 // Add row action links for all admin-visible post types (filtered later by allowlist). 105 $post_types = \get_post_types( [ 'show_ui' => true ], 'names' ); 106 if ( \is_array( $post_types ) ) { 107 foreach ( $post_types as $post_type ) { 108 \add_filter( $post_type . '_row_actions', [ __CLASS__, 'add_row_action_links' ], 10, 2 ); 109 // Bulk actions per CPT handled elsewhere in Admin Settings for registration; keep handler here as fallback. 110 \add_filter( 'handle_bulk_actions-edit-' . $post_type, [ __CLASS__, 'handle_bulk_action' ], 10, 3 ); 111 } 112 } 113 114 // Handle the duplicate actions. 115 \add_action( 'admin_action_' . self::ACTION_CLONE, [ __CLASS__, 'process_clone' ] ); 116 \add_action( 'admin_action_' . self::ACTION_NEW_DRAFT, [ __CLASS__, 'process_new_draft' ] ); 117 \add_action( 'admin_action_duplicate_post', [ __CLASS__, 'process_duplication_legacy' ] ); 118 119 // Rewrite & Republish entry and finalize on publish. 120 \add_action( 'admin_action_' . self::ACTION_REWRITE, [ __CLASS__, 'process_rewrite' ] ); 121 \add_action( 'transition_post_status', [ __CLASS__, 'maybe_finalize_rewrite' ], 10, 3 ); 122 123 // Hook to display notices. 124 \add_action( 'admin_notices', [ __CLASS__, 'render_admin_notices' ] ); 125 \add_action( 'admin_notices', [ __CLASS__, 'add_rollback_notice' ] ); 126 127 // Rollback action. 128 \add_action( 'admin_action_rollback_duplicate', [ __CLASS__, 'handle_rollback_action' ] ); 37 129 } 38 130 … … 44 136 * @return array Modified array of row action links. 45 137 */ 46 public static function add_duplicate_link( array $actions, \WP_Post $post ): array { 47 if ( current_user_can( 'edit_posts', $post->ID ) ) { 48 // Build duplicate URL. 49 $duplicate_url = wp_nonce_url( 50 add_query_arg( 51 [ 52 'action' => 'duplicate_post', 53 'post' => $post->ID, 54 ], 55 admin_url( 'admin.php' ) 56 ), 57 'duplicate_post_' . $post->ID 58 ); 59 // Build preview URL for AJAX preview. 60 $preview_url = wp_nonce_url( 61 add_query_arg( 62 [ 63 'action' => 'preview_duplicate_post', 64 'post' => $post->ID, 65 ], 66 admin_url( 'admin-ajax.php' ) 67 ), 68 'preview_duplicate_post_' . $post->ID 69 ); 70 $actions['duplicate'] = sprintf( 71 '<a href="%s">%s</a>', 72 esc_url( $duplicate_url ), 73 esc_html__( 'Duplicate', 'just-duplicate' ) 74 ); 75 $actions['preview_duplicate'] = sprintf( 76 '<a href="#" class="preview-duplicate" data-preview-url="%s">%s</a>', 77 esc_url( $preview_url ), 78 esc_html__( 'Preview Duplicate', 'just-duplicate' ) 79 ); 80 } 81 138 public static function add_row_action_links( array $actions, $post ): array { 139 $settings = self::get_settings(); 140 if ( empty( $settings['show_row_actions'] ) ) { 141 return $actions; 142 } 143 if ( ! self::is_post_type_enabled( $post->post_type ) ) { 144 return $actions; 145 } 146 if ( ! \current_user_can( 'edit_post', $post->ID ) || ! self::current_user_role_allowed() ) { 147 return $actions; 148 } 149 150 $nonce = \wp_create_nonce( self::NONCE_ACTION_PREFIX . $post->ID ); 151 $clone_url = \add_query_arg( [ 'action' => self::ACTION_CLONE, 'post' => $post->ID, '_wpnonce' => $nonce ], \admin_url( 'admin.php' ) ); 152 $draft_url = \add_query_arg( [ 'action' => self::ACTION_NEW_DRAFT, 'post' => $post->ID, '_wpnonce' => $nonce ], \admin_url( 'admin.php' ) ); 153 $rewrite_url = \add_query_arg( [ 'action' => self::ACTION_REWRITE, 'post' => $post->ID, '_wpnonce' => $nonce ], \admin_url( 'admin.php' ) ); 154 155 $actions['jd_clone'] = '<a href="' . \esc_url( $clone_url ) . '">' . \esc_html__( 'Clone', 'just-duplicate' ) . '</a>'; 156 $actions['jd_new_draft'] = '<a href="' . \esc_url( $draft_url ) . '">' . \esc_html__( 'New Draft', 'just-duplicate' ) . '</a>'; 157 $actions['jd_rewrite'] = '<a href="' . \esc_url( $rewrite_url ) . '">' . \esc_html__( 'Rewrite & Republish', 'just-duplicate' ) . '</a>'; 82 158 return $actions; 83 159 } … … 86 162 * Process the duplication of a post or page with role-based access control. 87 163 */ 88 public static function process_duplication (): void {164 public static function process_duplication_legacy(): void { 89 165 // Verify nonce. 90 166 if ( ! isset( $_GET['_wpnonce'], $_GET['post'] ) ) { 91 wp_die(esc_html__( 'Missing required parameters.', 'just-duplicate' ) );92 } 93 94 $post_id = absint( $_GET['post'] );95 $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field(wp_unslash( $_GET['_wpnonce'] ) ) : '';96 97 if ( ! wp_verify_nonce( $nonce, 'duplicate_post_' . $post_id ) ) {98 wp_die(esc_html__( 'Nonce verification failed.', 'just-duplicate' ) );99 } 100 101 $post = get_post( $post_id );167 \wp_die( \esc_html__( 'Missing required parameters.', 'just-duplicate' ) ); 168 } 169 170 $post_id = \absint( $_GET['post'] ); 171 $nonce = isset( $_GET['_wpnonce'] ) ? \sanitize_text_field( \wp_unslash( $_GET['_wpnonce'] ) ) : ''; 172 173 if ( ! \wp_verify_nonce( $nonce, 'duplicate_post_' . $post_id ) ) { 174 \wp_die( \esc_html__( 'Nonce verification failed.', 'just-duplicate' ) ); 175 } 176 177 $post = \get_post( $post_id ); 102 178 if ( ! $post ) { 103 wp_die(esc_html__( 'The post you are trying to duplicate does not exist.', 'just-duplicate' ) );179 \wp_die( \esc_html__( 'The post you are trying to duplicate does not exist.', 'just-duplicate' ) ); 104 180 } 105 181 106 182 // 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' ) );183 $current_user_id = \get_current_user_id(); 184 if ( ! ( \current_user_can( 'manage_options' ) || \current_user_can( 'edit_others_posts' ) || 185 ( \current_user_can( 'edit_posts' ) && $current_user_id === $post->post_author ) ) ) { 186 \wp_die( \esc_html__( 'You do not have permission to duplicate this post.', 'just-duplicate' ) ); 111 187 } 112 188 113 189 // Duplicate the post. 114 $new_post_id = self::duplicate_post( $post_id );190 $new_post_id = self::duplicate_post( $post_id, [] ); 115 191 116 192 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 ); 193 // Legacy behavior: redirect to editor. 194 $edit_url = \add_query_arg( [ 'post' => $new_post_id, 'action' => 'edit' ], \admin_url( 'post.php' ) ); 195 \wp_redirect( $edit_url ); 121 196 exit; 122 197 } else { … … 126 201 127 202 /** 203 * Process the Clone action: duplicate and return to list (per settings). 204 */ 205 public static function process_clone(): void { 206 self::process_action_request( self::ACTION_CLONE ); 207 } 208 209 /** 210 * Process the New Draft action: duplicate and open editor. 211 */ 212 public static function process_new_draft(): void { 213 self::process_action_request( self::ACTION_NEW_DRAFT ); 214 } 215 216 /** 217 * Common action request processor. 218 */ 219 private static function process_action_request( string $action ): void { 220 if ( ! isset( $_GET['_wpnonce'], $_GET['post'] ) ) { 221 \wp_die( \esc_html__( 'Missing required parameters.', 'just-duplicate' ) ); 222 } 223 $post_id = \absint( $_GET['post'] ); 224 $nonce = \sanitize_text_field( \wp_unslash( $_GET['_wpnonce'] ) ); 225 if ( ! \wp_verify_nonce( $nonce, self::NONCE_ACTION_PREFIX . $post_id ) ) { 226 \wp_die( \esc_html__( 'Nonce verification failed.', 'just-duplicate' ) ); 227 } 228 $post = \get_post( $post_id ); 229 if ( ! $post ) { 230 \wp_die( \esc_html__( 'The post you are trying to duplicate does not exist.', 'just-duplicate' ) ); 231 } 232 if ( ! \current_user_can( 'edit_post', $post_id ) || ! self::current_user_role_allowed() || ! self::is_post_type_enabled( $post->post_type ) ) { 233 \wp_die( \esc_html__( 'You do not have permission to duplicate this post.', 'just-duplicate' ) ); 234 } 235 236 $args = [ 'open_editor' => ( $action === self::ACTION_NEW_DRAFT ) ]; 237 $new_post_id = self::duplicate_post( $post_id, $args ); 238 if ( is_wp_error( $new_post_id ) || ! $new_post_id ) { 239 \wp_die( esc_html__( 'Failed to duplicate the post.', 'just-duplicate' ) ); 240 } 241 242 // Build redirect per settings and action. 243 $settings = self::get_settings(); 244 $redirect_pref = isset( $settings['redirect_after'] ) ? $settings['redirect_after'] : 'list'; 245 246 $redirect = \admin_url( 'edit.php?post_type=' . urlencode( \get_post_type( $post_id ) ) ); 247 if ( $redirect_pref === 'original' ) { 248 $redirect = \get_edit_post_link( $post_id, '' ); 249 } 250 if ( $action === self::ACTION_NEW_DRAFT || $redirect_pref === 'editor' ) { 251 $redirect = \add_query_arg( [ 'post' => $new_post_id, 'action' => 'edit' ], \admin_url( 'post.php' ) ); 252 } 253 // Add notice flag. 254 $redirect = \add_query_arg( [ 'jd_duplicated' => 1, 'jd_new' => $new_post_id, 'jd_from' => $post_id ], $redirect ); 255 \wp_safe_redirect( $redirect ); 256 exit; 257 } 258 259 /** 128 260 * Duplicate a post or page. 129 261 * … … 131 263 * @return int|null The ID of the duplicated post, or null on failure. 132 264 */ 133 public static function duplicate_post( int $post_id ): ?int{134 $post = get_post( $post_id );265 public static function duplicate_post( int $post_id, array $args = [] ) { 266 $post = \get_post( $post_id ); 135 267 if ( ! $post ) { 136 268 return null; … … 138 270 139 271 // Trigger the before duplicate hook. 140 do_action( 'just_duplicate_before_duplicate', $post_id ); 141 142 $settings = get_option( 'JUST_DUPLICATE_settings', [] ); 143 $title_pattern = $settings['custom_title_pattern'] ?? '%original% (Copy)'; 144 $slug_pattern = $settings['custom_slug_pattern'] ?? '%original%-copy'; 145 $post_status = $settings['custom_post_status'] ?? 'draft'; 272 \do_action( 'just_duplicate_before_duplicate', $post_id ); 273 274 $settings = self::get_settings(); 275 $title_pattern = $settings['custom_title_pattern'] ?? '%original% (Copy)'; 276 $slug_pattern = $settings['custom_slug_pattern'] ?? '%original%-copy'; 277 $status_setting = $settings['custom_post_status'] ?? 'draft'; // legacy field 278 $copy_status_same = isset( $settings['status_same_as_original'] ) ? (bool) $settings['status_same_as_original'] : false; 279 $post_status = $copy_status_same ? $post->post_status : $status_setting; 280 281 $copy_author = ! empty( $settings['copy_author'] ); 282 $copy_date = ! empty( $settings['copy_date'] ); 283 $copy_password = ! array_key_exists( 'copy_password', $settings ) || ! empty( $settings['copy_password'] ); // default true 284 $copy_excerpt = isset( $settings['copy_excerpt'] ) ? (bool) $settings['copy_excerpt'] : true; 285 $copy_template = ! empty( $settings['copy_template'] ); 286 $copy_format = ! empty( $settings['copy_post_format'] ); 287 $copy_sticky = ! empty( $settings['copy_sticky'] ); 288 $copy_menu = isset( $settings['copy_menu_order'] ) ? (bool) $settings['copy_menu_order'] : true; 289 $menu_inc = isset( $settings['increase_menu_order_by'] ) ? (int) $settings['increase_menu_order_by'] : 0; 290 $copy_children = ! empty( $settings['copy_children'] ); 291 $copy_tax = isset( $settings['duplicate_taxonomies'] ) ? (bool) $settings['duplicate_taxonomies'] : true; 292 $copy_meta = isset( $settings['duplicate_post_meta'] ) ? (bool) $settings['duplicate_post_meta'] : true; 293 $media_handling = $settings['media_attachment_handling'] ?? 'reference'; 294 $tax_blacklist = self::parse_csv_patterns( $settings['tax_blacklist'] ?? '' ); 295 $meta_blacklist = self::parse_csv_patterns( $settings['meta_blacklist'] ?? '_edit_lock,_edit_last' ); 296 $copy_exact_slug = ! empty( $settings['copy_exact_slug'] ); 146 297 147 298 // Prepare the new post data. 299 $new_title = \str_replace( ['%original%', '%date%', '%timestamp%'], [$post->post_title, \date_i18n( 'Y-m-d' ), \time()], $title_pattern ); 300 $new_slug = $copy_exact_slug ? $post->post_name : \str_replace( ['%original%', '%date%', '%timestamp%'], [\sanitize_title( $post->post_title ), \date_i18n( 'Y-m-d' ), \time()], $slug_pattern ); 301 148 302 $new_post = [ 149 'post_title' => str_replace( 150 ['%original%', '%date%', '%timestamp%'], 151 [$post->post_title, date_i18n( 'Y-m-d' ), time()], 152 $title_pattern 153 ), 154 'post_name' => str_replace( 155 ['%original%', '%date%', '%timestamp%'], 156 [sanitize_title( $post->post_title ), date_i18n( 'Y-m-d' ), time()], 157 $slug_pattern 158 ), 303 'post_title' => $new_title, 304 'post_name' => $new_slug, 159 305 'post_content' => $post->post_content, 160 306 'post_status' => $post_status, 161 307 'post_type' => $post->post_type, 162 'post_author' => get_current_user_id(),163 'post_excerpt' => $ post->post_excerpt,308 'post_author' => $copy_author ? (int) $post->post_author : (int) \get_current_user_id(), 309 'post_excerpt' => $copy_excerpt ? $post->post_excerpt : '', 164 310 'post_parent' => $post->post_parent, 165 311 ]; 166 312 313 if ( $copy_password ) { 314 $new_post['post_password'] = $post->post_password; 315 } 316 if ( $copy_date ) { 317 $new_post['post_date'] = $post->post_date; 318 $new_post['post_date_gmt'] = $post->post_date_gmt; 319 } 320 if ( $copy_menu ) { 321 $new_post['menu_order'] = (int) $post->menu_order + $menu_inc; 322 } 323 167 324 // Insert the new post. 168 $new_post_id = wp_insert_post( $new_post);169 if ( is_wp_error( $new_post_id ) || ! $new_post_id ) {325 $new_post_id = \wp_insert_post( \wp_slash( $new_post ) ); 326 if ( \is_wp_error( $new_post_id ) || ! $new_post_id ) { 170 327 return null; 171 328 } 172 329 173 // Copy metadata, taxonomies, and other related data. 174 self::copy_post_meta( $post_id, $new_post_id ); 175 self::copy_post_taxonomies( $post_id, $new_post_id ); 176 177 // If the original post has a featured image, duplicate it. 178 $settings = get_option( 'JUST_DUPLICATE_settings', [] ); 179 $media_handling = $settings['media_attachment_handling'] ?? 'duplicate'; 180 181 if ( 'duplicate' === $media_handling ) { 182 $thumb_id = get_post_thumbnail_id( $post_id ); 183 if ( $thumb_id ) { 330 // Save original reference. 331 \update_post_meta( $new_post_id, '_jd_original_post', $post_id ); 332 333 // Copy taxonomies unless blacklisted per taxonomy. 334 if ( $copy_tax ) { 335 self::copy_post_taxonomies_filtered( $post_id, $new_post_id, $tax_blacklist ); 336 } 337 338 // Copy meta with blacklist and preserve known exclusions. 339 if ( $copy_meta ) { 340 self::copy_post_meta_filtered( $post_id, $new_post_id, $meta_blacklist ); 341 } 342 343 // Page template and format. 344 if ( $copy_template ) { 345 $template = \get_post_meta( $post_id, '_wp_page_template', true ); 346 if ( $template ) { \update_post_meta( $new_post_id, '_wp_page_template', $template ); } 347 } 348 if ( $copy_format && \function_exists( 'set_post_format' ) ) { 349 $format = \get_post_format( $post_id ); 350 if ( $format ) { \set_post_format( $new_post_id, $format ); } 351 } 352 if ( $copy_sticky && 'post' === $post->post_type ) { 353 \stick_post( $new_post_id ); 354 } 355 356 // Featured image and attachments handling. 357 $thumb_id = \get_post_thumbnail_id( $post_id ); 358 $attachment_map = []; 359 if ( $thumb_id ) { 360 if ( 'duplicate' === $media_handling ) { 184 361 $new_thumb_id = self::duplicate_attachment( $thumb_id, $new_post_id ); 185 362 if ( $new_thumb_id ) { 186 set_post_thumbnail( $new_post_id, $new_thumb_id ); 363 $attachment_map[ (int) $thumb_id ] = (int) $new_thumb_id; 364 \set_post_thumbnail( $new_post_id, $new_thumb_id ); 187 365 } 188 } 189 } elseif ( 'reference' === $media_handling ) { 190 $thumb_id = get_post_thumbnail_id( $post_id ); 191 if ( $thumb_id ) { 192 set_post_thumbnail( $new_post_id, $thumb_id ); 366 } else { 367 \set_post_thumbnail( $new_post_id, $thumb_id ); 368 } 369 } 370 371 // Best-effort remap of attachment IDs in content/meta if we duplicated any. 372 if ( ! empty( $attachment_map ) ) { 373 $updated_content = self::remap_ids_in_content( \get_post_field( 'post_content', $new_post_id ), $attachment_map ); 374 if ( $updated_content !== null ) { 375 \wp_update_post( [ 'ID' => $new_post_id, 'post_content' => $updated_content ] ); 376 } 377 // WooCommerce gallery meta 378 $wc_gallery = \get_post_meta( $new_post_id, '_product_image_gallery', true ); 379 if ( is_string( $wc_gallery ) && $wc_gallery !== '' ) { 380 $ids = array_map( 'intval', array_map( 'trim', explode( ',', $wc_gallery ) ) ); 381 $ids = array_map( function( $i ) use ( $attachment_map ) { return $attachment_map[ $i ] ?? $i; }, $ids ); 382 \update_post_meta( $new_post_id, '_product_image_gallery', implode( ',', $ids ) ); 193 383 } 194 384 } … … 199 389 // Store the last duplicated post ID in a transient. 200 390 if ( $new_post_id ) { 201 set_transient( 'just_duplicate_last_post_id', $new_post_id,HOUR_IN_SECONDS );391 \set_transient( 'just_duplicate_last_post_id', $new_post_id, \HOUR_IN_SECONDS ); 202 392 } 203 393 204 394 // Trigger the after duplicate hook. 205 do_action( 'just_duplicate_after_duplicate', $post_id, $new_post_id ); 395 \do_action( 'just_duplicate_after_duplicate', $post_id, $new_post_id ); 396 397 // Children copy (one level deep). 398 if ( $copy_children ) { 399 $children = \get_children( [ 'post_parent' => $post_id, 'post_type' => $post->post_type, 'post_status' => 'any' ] ); 400 foreach ( $children as $child ) { 401 self::duplicate_post( (int) $child->ID, $args ); 402 // maintain hierarchy 403 $created = self::$last_duplicated_post_id; 404 if ( $created ) { 405 \wp_update_post( [ 'ID' => $created, 'post_parent' => $new_post_id ] ); 406 } 407 } 408 } 206 409 207 410 return $new_post_id; … … 223 426 */ 224 427 public static function rollback_last_duplicate(): void { 225 $last_post_id = get_transient( 'just_duplicate_last_post_id' );428 $last_post_id = \get_transient( 'just_duplicate_last_post_id' ); 226 429 227 430 if ( $last_post_id ) { 228 wp_delete_post( $last_post_id, true );229 delete_transient( 'just_duplicate_last_post_id' );230 add_action( 'admin_notices', function () {231 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'The last duplicated post has been rolled back.', 'just-duplicate' ) . '</p></div>';431 \wp_delete_post( $last_post_id, true ); 432 \delete_transient( 'just_duplicate_last_post_id' ); 433 \add_action( 'admin_notices', function () { 434 echo '<div class="notice notice-success is-dismissible"><p>' . \esc_html__( 'The last duplicated post has been rolled back.', 'just-duplicate' ) . '</p></div>'; 232 435 } ); 233 436 } else { 234 add_action( 'admin_notices', function () {235 echo '<div class="notice notice-error is-dismissible"><p>' . esc_html__( 'No duplicated post found to rollback.', 'just-duplicate' ) . '</p></div>';437 \add_action( 'admin_notices', function () { 438 echo '<div class="notice notice-error is-dismissible"><p>' . \esc_html__( 'No duplicated post found to rollback.', 'just-duplicate' ) . '</p></div>'; 236 439 } ); 237 440 } … … 242 445 */ 243 446 public static function add_rollback_notice(): void { 244 $last_post_id = get_transient( 'just_duplicate_last_post_id' );447 $last_post_id = \get_transient( 'just_duplicate_last_post_id' ); 245 448 246 449 if ( $last_post_id ) { 247 $rollback_url = add_query_arg(450 $rollback_url = \add_query_arg( 248 451 [ 249 452 'action' => 'rollback_duplicate', 250 '_wpnonce' => wp_create_nonce( 'rollback_duplicate' ),453 '_wpnonce' => \wp_create_nonce( 'rollback_duplicate' ), 251 454 ], 252 admin_url( 'admin.php' )455 \admin_url( 'admin.php' ) 253 456 ); 254 457 255 458 echo '<div class="notice notice-info is-dismissible"><p>' . 256 esc_html__( 'A post has been duplicated. ', 'just-duplicate' ) .257 '<a href="' . esc_url( $rollback_url ) . '">' .esc_html__( 'Undo this action.', 'just-duplicate' ) . '</a>' .459 \esc_html__( 'A post has been duplicated. ', 'just-duplicate' ) . 460 '<a href="' . \esc_url( $rollback_url ) . '">' . \esc_html__( 'Undo this action.', 'just-duplicate' ) . '</a>' . 258 461 '</p></div>'; 259 462 } … … 361 564 * @return void 362 565 */ 363 private static function copy_post_taxonomies( int $old_post_id, int $new_post_id ): void { 364 $post_type = get_post_type( $old_post_id ); 365 $taxonomies = get_object_taxonomies( $post_type ); 366 566 private static function copy_post_taxonomies_filtered( int $old_post_id, int $new_post_id, array $tax_blacklist ): void { 567 $post_type = \get_post_type( $old_post_id ); 568 $taxonomies = \get_object_taxonomies( $post_type ); 367 569 foreach ( $taxonomies as $taxonomy ) { 368 $terms = wp_get_object_terms( $old_post_id, $taxonomy, [ 'fields' => 'ids' ] ); 369 if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) { 370 wp_set_object_terms( $new_post_id, $terms, $taxonomy ); 570 if ( self::is_any_pattern_match( $taxonomy, $tax_blacklist ) ) { 571 continue; 572 } 573 $terms = \wp_get_object_terms( $old_post_id, $taxonomy, [ 'fields' => 'ids' ] ); 574 if ( ! \is_wp_error( $terms ) && ! empty( $terms ) ) { 575 \wp_set_object_terms( $new_post_id, $terms, $taxonomy ); 371 576 } 372 577 } … … 380 585 * @return void 381 586 */ 382 private static function copy_ custom_fields( int $old_post_id, int $new_post_id): void {383 $meta_data = get_post_meta( $old_post_id );384 587 private static function copy_post_meta_filtered( int $old_post_id, int $new_post_id, array $meta_blacklist ): void { 588 $meta_data = \get_post_meta( $old_post_id ); 589 $settings = self::get_settings(); 385 590 foreach ( $meta_data as $key => $values ) { 591 // preserve blacklist and core exclusions 592 if ( in_array( $key, [ '_edit_lock', '_edit_last' ], true ) ) { continue; } 593 if ( self::is_any_pattern_match( (string) $key, $meta_blacklist ) ) { continue; } 594 // Respect ACF and SEO toggles if present 595 if ( strpos( (string) $key, 'acf_' ) === 0 && empty( $settings['duplicate_acf_meta'] ) ) { continue; } 596 if ( strpos( (string) $key, '_yoast_' ) === 0 && empty( $settings['duplicate_seo_meta'] ) ) { continue; } 597 386 598 foreach ( $values as $value ) { 387 add_post_meta( $new_post_id, $key,maybe_unserialize( $value ) );599 \add_post_meta( $new_post_id, $key, \maybe_unserialize( $value ) ); 388 600 } 389 601 } … … 398 610 */ 399 611 private static function copy_custom_taxonomies( int $old_post_id, int $new_post_id ): void { 400 $taxonomies = get_object_taxonomies(get_post_type( $old_post_id ) );612 $taxonomies = \get_object_taxonomies( \get_post_type( $old_post_id ) ); 401 613 402 614 foreach ( $taxonomies as $taxonomy ) { 403 $terms = wp_get_object_terms( $old_post_id, $taxonomy, [ 'fields' => 'slugs' ] );404 if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {405 wp_set_object_terms( $new_post_id, $terms, $taxonomy );615 $terms = \wp_get_object_terms( $old_post_id, $taxonomy, [ 'fields' => 'slugs' ] ); 616 if ( ! \is_wp_error( $terms ) && ! empty( $terms ) ) { 617 \wp_set_object_terms( $new_post_id, $terms, $taxonomy ); 406 618 } 407 619 } … … 418 630 public static function preview_duplicate(): void { 419 631 // Verify the AJAX nonce. 420 check_ajax_referer( 'preview_duplicate_post', '_wpnonce' );632 \check_ajax_referer( 'preview_duplicate_post', '_wpnonce' ); 421 633 422 634 // Check permissions. 423 if ( ! current_user_can( 'edit_posts' ) ) {424 wp_send_json_error(esc_html__( 'Permission denied.', 'just-duplicate' ) );635 if ( ! \current_user_can( 'edit_posts' ) ) { 636 \wp_send_json_error( \esc_html__( 'Permission denied.', 'just-duplicate' ) ); 425 637 } 426 638 427 $post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;428 429 $post = get_post( $post_id );639 $post_id = isset( $_GET['post'] ) ? \absint( $_GET['post'] ) : 0; 640 641 $post = \get_post( $post_id ); 430 642 if ( ! $post ) { 431 wp_send_json_error(esc_html__( 'Post not found.', 'just-duplicate' ) );643 \wp_send_json_error( \esc_html__( 'Post not found.', 'just-duplicate' ) ); 432 644 } 433 645 434 646 // Retrieve plugin settings. 435 $settings =get_option( 'JUST_DUPLICATE_settings', [] );647 $settings = \get_option( 'JUST_DUPLICATE_settings', [] ); 436 648 $default_prefix = isset( $settings['default_prefix'] ) ? (string) $settings['default_prefix'] : ''; 437 649 $default_suffix = ( isset( $settings['default_suffix'] ) && '' !== $settings['default_suffix'] ) … … 442 654 $preview_data = [ 443 655 'post_id' => $post->ID, 444 'title' => esc_html( $default_prefix . $post->post_title . $default_suffix ),445 'content' => wp_kses_post(apply_filters( 'the_content', $post->post_content ) ),446 'excerpt' => esc_html( $post->post_excerpt ),447 'author' => esc_html(get_the_author_meta( 'display_name', $post->post_author ) ),448 'date' => esc_html( $post->post_date ),449 'duplicate_url'=> esc_url(wp_nonce_url(450 add_query_arg(656 'title' => \esc_html( $default_prefix . $post->post_title . $default_suffix ), 657 'content' => \wp_kses_post( \apply_filters( 'the_content', $post->post_content ) ), 658 'excerpt' => \esc_html( $post->post_excerpt ), 659 'author' => \esc_html( \get_the_author_meta( 'display_name', $post->post_author ) ), 660 'date' => \esc_html( $post->post_date ), 661 'duplicate_url'=> \esc_url( \wp_nonce_url( 662 \add_query_arg( 451 663 [ 452 664 'action' => 'duplicate_post', 453 665 'post' => $post->ID, 454 666 ], 455 admin_url( 'admin.php' )667 \admin_url( 'admin.php' ) 456 668 ), 457 669 'duplicate_post_' . $post->ID … … 459 671 ]; 460 672 461 wp_send_json_success( $preview_data );673 \wp_send_json_success( $preview_data ); 462 674 } 463 675 … … 468 680 */ 469 681 private static function get_selective_duplication_options(): array { 470 $settings =get_option( 'JUST_DUPLICATE_settings', [] );682 $settings = \get_option( 'JUST_DUPLICATE_settings', [] ); 471 683 return [ 472 684 'duplicate_post_meta' => ! empty( $settings['duplicate_post_meta'] ), … … 488 700 */ 489 701 private static function copy_comments( int $old_post_id, int $new_post_id ): void { 490 $comments =get_comments( [ 'post_id' => $old_post_id ] );702 $comments = \get_comments( [ 'post_id' => $old_post_id ] ); 491 703 foreach ( $comments as $comment ) { 492 704 $new_comment = [ … … 500 712 'user_id' => $comment->user_id, 501 713 ]; 502 wp_insert_comment( $new_comment ); 503 } 714 \wp_insert_comment( $new_comment ); 715 } 716 } 717 718 /** 719 * Handle bulk action fallback; other registration handled in Admin Settings. 720 */ 721 public static function handle_bulk_action( string $redirect_url, string $action, array $post_ids ): string { 722 if ( 'duplicate' !== $action && self::ACTION_CLONE !== $action ) { 723 return $redirect_url; 724 } 725 $count = 0; 726 foreach ( $post_ids as $pid ) { 727 if ( \current_user_can( 'edit_post', (int) $pid ) && self::is_post_type_enabled( \get_post_type( (int) $pid ) ) && self::current_user_role_allowed() ) { 728 $res = self::duplicate_post( (int) $pid, [] ); 729 if ( $res ) { $count++; } 730 } 731 } 732 return \add_query_arg( 'bulk_duplicated', $count, $redirect_url ); 733 } 734 735 /** 736 * Process Rewrite & Republish creation. 737 */ 738 public static function process_rewrite(): void { 739 if ( ! isset( $_GET['_wpnonce'], $_GET['post'] ) ) { \wp_die( \esc_html__( 'Missing required parameters.', 'just-duplicate' ) ); } 740 $post_id = \absint( $_GET['post'] ); 741 $nonce = \sanitize_text_field( \wp_unslash( $_GET['_wpnonce'] ) ); 742 if ( ! \wp_verify_nonce( $nonce, self::NONCE_ACTION_PREFIX . $post_id ) ) { \wp_die( \esc_html__( 'Nonce verification failed.', 'just-duplicate' ) ); } 743 $post = \get_post( $post_id ); 744 if ( ! $post || ! \current_user_can( 'edit_post', $post_id ) ) { \wp_die( \esc_html__( 'Permission denied.', 'just-duplicate' ) ); } 745 746 $working_id = self::duplicate_post( $post_id, [ 'open_editor' => true ] ); 747 if ( ! $working_id ) { wp_die( esc_html__( 'Failed to create working copy.', 'just-duplicate' ) ); } 748 \update_post_meta( $working_id, '_jd_rewrite_of', $post_id ); 749 // Force draft 750 \wp_update_post( [ 'ID' => $working_id, 'post_status' => 'draft' ] ); 751 752 \wp_safe_redirect( \add_query_arg( [ 'post' => $working_id, 'action' => 'edit' ], \admin_url( 'post.php' ) ) ); 753 exit; 754 } 755 756 /** 757 * On publish, if this is a working copy, copy content/meta/terms back to original and trash the working copy. 758 */ 759 public static function maybe_finalize_rewrite( $new_status, $old_status, $post ): void { 760 if ( 'publish' !== $new_status ) { return; } 761 $original_id = (int) \get_post_meta( $post->ID, '_jd_rewrite_of', true ); 762 if ( ! $original_id ) { return; } 763 764 // Permissions 765 if ( ! \current_user_can( 'edit_post', $original_id ) ) { return; } 766 767 // Copy content/meta/terms into original while preserving ID/URL and status/date. 768 $orig = \get_post( $original_id ); 769 if ( ! $orig ) { return; } 770 771 // Content and excerpt 772 \wp_update_post( [ 773 'ID' => $original_id, 774 'post_content' => $post->post_content, 775 'post_excerpt' => $post->post_excerpt, 776 'post_title' => $post->post_title, // keep edited title 777 ] ); 778 779 // Taxonomies 780 $taxes = \get_object_taxonomies( $orig->post_type ); 781 foreach ( $taxes as $tax ) { 782 $terms = \wp_get_object_terms( $post->ID, $tax, [ 'fields' => 'ids' ] ); 783 if ( ! \is_wp_error( $terms ) ) { 784 \wp_set_object_terms( $original_id, $terms, $tax ); 785 } 786 } 787 788 // Meta: copy all except internals and our own markers 789 $meta = \get_post_meta( $post->ID ); 790 foreach ( $meta as $k => $vals ) { 791 if ( in_array( $k, [ '_edit_lock', '_edit_last', '_jd_rewrite_of', '_jd_original_post' ], true ) ) { continue; } 792 \delete_post_meta( $original_id, $k ); 793 foreach ( $vals as $v ) { \add_post_meta( $original_id, $k, \maybe_unserialize( $v ) ); } 794 } 795 796 // Store a revision of original (WP does automatically on update if revisions enabled) 797 798 // Trash working copy 799 \wp_trash_post( $post->ID ); 800 801 // Add notice 802 \add_filter( 'redirect_post_location', function( $location ) use ( $original_id ) { 803 return \add_query_arg( [ 'jd_rewrite_applied' => 1, 'post' => $original_id, 'action' => 'edit' ], $location ); 804 } ); 805 } 806 807 /** 808 * Admin notices renderer for duplication events and bulk counts. 809 */ 810 public static function render_admin_notices(): void { 811 if ( isset( $_GET['jd_duplicated'] ) && isset( $_GET['jd_new'] ) ) { 812 $new_id = absint( $_GET['jd_new'] ); 813 echo '<div class="notice notice-success is-dismissible"><p>' . \esc_html__( 'Post duplicated.', 'just-duplicate' ) . ' ' . sprintf( '<a href="%s">%s</a>', \esc_url( \get_edit_post_link( $new_id, '' ) ), \esc_html__( 'Edit new copy', 'just-duplicate' ) ) . '</p></div>'; 814 } 815 if ( isset( $_GET['bulk_duplicated'] ) ) { 816 $count = absint( $_GET['bulk_duplicated'] ); 817 echo '<div class="notice notice-success is-dismissible"><p>' . sprintf( \esc_html__( 'Duplicated %d items.', 'just-duplicate' ), $count ) . '</p></div>'; 818 } 819 if ( isset( $_GET['jd_rewrite_applied'] ) ) { 820 echo '<div class="notice notice-success is-dismissible"><p>' . \esc_html__( 'Rewrite changes applied to original.', 'just-duplicate' ) . '</p></div>'; 821 } 822 } 823 824 /* ========================= Helper methods ========================= */ 825 826 private static function get_settings(): array { 827 if ( self::$settings !== null ) { return self::$settings; } 828 $defaults = [ 829 'enabled_post_types' => array_keys( \get_post_types( [ 'show_ui' => true ], 'names' ) ), 830 'enabled_roles' => [ 'administrator', 'editor' ], 831 'show_row_actions' => true, 832 'show_submitbox' => true, 833 'show_admin_bar' => false, 834 'show_bulk' => true, 835 'copy_author' => false, 836 'copy_date' => false, 837 'copy_password' => true, 838 'copy_excerpt' => true, 839 'copy_template' => true, 840 'copy_post_format' => true, 841 'copy_sticky' => false, 842 'copy_menu_order' => true, 843 'increase_menu_order_by' => 0, 844 'copy_children' => false, 845 'media_attachment_handling' => 'reference', 846 'meta_blacklist' => '_edit_lock,_edit_last', 847 'tax_blacklist' => '', 848 'redirect_after' => 'list', 849 ]; 850 $settings = \get_option( 'JUST_DUPLICATE_settings', [] ); 851 self::$settings = \wp_parse_args( $settings, $defaults ); 852 return self::$settings; 853 } 854 855 private static function is_post_type_enabled( string $post_type ): bool { 856 $settings = self::get_settings(); 857 $enabled = is_array( $settings['enabled_post_types'] ?? null ) ? $settings['enabled_post_types'] : []; 858 return in_array( $post_type, $enabled, true ); 859 } 860 861 private static function current_user_role_allowed(): bool { 862 $settings = self::get_settings(); 863 $allowed = is_array( $settings['enabled_roles'] ?? null ) ? $settings['enabled_roles'] : [ 'administrator', 'editor' ]; 864 $user = \wp_get_current_user(); 865 if ( empty( $user->roles ) ) { return false; } 866 foreach ( $user->roles as $role ) { 867 if ( in_array( $role, $allowed, true ) ) { return true; } 868 } 869 return false; 870 } 871 872 private static function parse_csv_patterns( string $csv ): array { 873 $csv = trim( $csv ); 874 if ( $csv === '' ) { return []; } 875 $parts = array_map( 'trim', explode( ',', $csv ) ); 876 return array_filter( $parts, static function( $v ) { return $v !== ''; } ); 877 } 878 879 private static function is_any_pattern_match( string $value, array $patterns ): bool { 880 foreach ( $patterns as $pattern ) { 881 if ( self::wildcard_match( $pattern, $value ) ) { return true; } 882 } 883 return false; 884 } 885 886 private static function wildcard_match( string $pattern, string $string ): bool { 887 // Convert simple * wildcard to regex. 888 $regex = '/^' . str_replace( ['\\*','\\?'], ['.*','.' ], preg_quote( $pattern, '/' ) ) . '$/i'; 889 return (bool) preg_match( $regex, $string ); 890 } 891 892 /** 893 * Best-effort remap of attachment IDs in content (shortcodes and common block attributes). 894 */ 895 private static function remap_ids_in_content( string $content, array $map ): ?string { 896 if ( empty( $map ) || $content === '' ) { return $content; } 897 // Gallery shortcode ids="1,2" 898 $content = preg_replace_callback( '/(\bid\s*=\s*")(\d+)(")/i', function( $m ) use ( $map ) { 899 $id = (int) $m[2]; 900 return $m[1] . ( $map[ $id ] ?? $id ) . $m[3]; 901 }, $content ); 902 $content = preg_replace_callback( '/(\bids\s*=\s*")([0-9,\s]+)(")/i', function( $m ) use ( $map ) { 903 $ids = array_map( 'intval', array_map( 'trim', explode( ',', $m[2] ) ) ); 904 $ids = array_map( function( $i ) use ( $map ) { return $map[ $i ] ?? $i; }, $ids ); 905 return $m[1] . implode( ',', $ids ) . $m[3]; 906 }, $content ); 907 // JSON-like "id": 123 908 $content = preg_replace_callback( '/("id"\s*:\s*)(\d+)/i', function( $m ) use ( $map ) { 909 $id = (int) $m[2]; 910 return $m[1] . ( $map[ $id ] ?? $id ); 911 }, $content ); 912 return $content; 504 913 } 505 914 } -
just-duplicate/trunk/includes/class-duplicate-logger.php
r3252339 r3384189 3 3 4 4 namespace Just_Duplicate; 5 6 // Lightweight shims for WordPress global functions to satisfy static analysis in namespaced code. 7 function get_option( $option, $default = false ) { return \function_exists('\\get_option') ? \call_user_func_array('\\get_option', \func_get_args()) : $default; } 8 function update_option( $option, $value, $autoload = null ) { return \function_exists('\\update_option') ? \call_user_func_array('\\update_option', \func_get_args()) : false; } 9 function delete_option( $option ) { return \function_exists('\\delete_option') ? \call_user_func_array('\\delete_option', \func_get_args()) : false; } 10 function get_current_user_id() { return \function_exists('\\get_current_user_id') ? \call_user_func_array('\\get_current_user_id', \func_get_args()) : 0; } 11 function current_time( $type, $gmt = 0 ) { return \function_exists('\\current_time') ? \call_user_func_array('\\current_time', \func_get_args()) : date('Y-m-d H:i:s'); } 5 12 6 13 if ( ! defined( 'ABSPATH' ) ) { … … 41 48 return get_option( 'JUST_DUPLICATE_log', [] ); 42 49 } 50 51 /** 52 * Clear the duplication log stored in wp_options. 53 * 54 * @return bool True on success, false on failure. 55 */ 56 public static function clear(): bool { 57 return (bool) delete_option( 'JUST_DUPLICATE_log' ); 58 } 43 59 } -
just-duplicate/trunk/just-duplicate.php
r3297655 r3384189 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. 46 * Version: 1.0.6 7 7 * Requires at least: 5.0 8 * Tested up to: 6. 78 * Tested up to: 6.8 9 9 * Requires PHP: 7.0 10 10 * Author: Just There … … 21 21 22 22 // Define plugin constants. 23 define( 'JUST_DUPLICATE_VERSION', '1.0. 4' );23 define( 'JUST_DUPLICATE_VERSION', '1.0.6' ); 24 24 define( 'JUST_DUPLICATE_PATH', plugin_dir_path( __FILE__ ) ); 25 25 define( 'JUST_DUPLICATE_URL', plugin_dir_url( __FILE__ ) );
Note: See TracChangeset
for help on using the changeset viewer.