--- a/post-duplicator/assets/build/gutenbergButton.asset.php
+++ b/post-duplicator/assets/build/gutenbergButton.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-primitives'), 'version' => 'eb9b987d1d4ef9337299');
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-primitives'), 'version' => '4a582df916423a2c3f5e');
--- a/post-duplicator/assets/build/postDuplicator.asset.php
+++ b/post-duplicator/assets/build/postDuplicator.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '7cfbb7eda2177715b9a5');
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '9e5dd04797fa5b464171');
--- a/post-duplicator/includes/api.php
+++ b/post-duplicator/includes/api.php
@@ -277,7 +277,7 @@
'date' => $post->post_date,
'author' => $author_name,
'authorId' => $post->post_author,
- 'parent' => $post->post_parent || 0,
+ 'parent' => (int) $post->post_parent,
'parentPost' => $parent_post,
'featuredImage' => $featured_image,
) );
@@ -537,13 +537,14 @@
return true;
}
+
/**
* Duplicate a post
*/
function duplicate_post( $request ) {
$data = $request->get_json_params();
- // Get access to the database
+ // Get access to the database (used for meta insertion when copying validated keys from original)
global $wpdb;
// Get and validate the original id
@@ -644,9 +645,27 @@
}
}
- // Set the parent - check for selectedParentId first, otherwise keep original parent
- if ( isset( $settings['selectedParentId'] ) ) {
- $duplicate['post_parent'] = intval( $settings['selectedParentId'] );
+ // Set the parent from request data.
+ // Validate parent is the same post type to avoid invalid cross-type assignments.
+ if ( array_key_exists( 'selectedParentId', $data ) ) {
+ $selected_parent = $data['selectedParentId'];
+
+ // Explicit "No Parent" selection.
+ if ( null === $selected_parent || '' === $selected_parent || 0 === $selected_parent || '0' === $selected_parent ) {
+ $duplicate['post_parent'] = 0;
+ } else {
+ $selected_parent_id = absint( $selected_parent );
+ if ( $selected_parent_id > 0 ) {
+ $selected_parent_post = get_post( $selected_parent_id );
+ if (
+ $selected_parent_post &&
+ $selected_parent_post->post_type === $duplicate['post_type'] &&
+ is_post_type_hierarchical( $duplicate['post_type'] )
+ ) {
+ $duplicate['post_parent'] = $selected_parent_id;
+ }
+ }
+ }
}
// Set the post date
@@ -837,15 +856,16 @@
if ( $include_custom_meta === true ) {
$excluded_meta_keys = get_excluded_meta_keys();
+ $original_custom_fields = get_post_custom( $original_id );
$cloned_meta_data = [];
// Use provided custom meta data if available, otherwise fetch from original post
if ( isset( $settings['customMetaData'] ) && is_array( $settings['customMetaData'] ) ) {
- // Use provided custom meta data
- $original_custom_fields = get_post_custom( $original_id );
-
+ // Security: Only copy keys that exist on the original post (blocks key injection).
+ // For protected meta (e.g. _wp_page_template): use original values only (blocks value injection).
+ // For non-protected meta: allow user-edited values from the request.
foreach ( $settings['customMetaData'] as $meta_item ) {
- if ( ! isset( $meta_item['key'] ) || ! isset( $meta_item['value'] ) ) {
+ if ( ! isset( $meta_item['key'] ) ) {
continue;
}
$meta_key = $meta_item['key'];
@@ -855,48 +875,53 @@
continue; // Skip invalid meta keys
}
+ // Security: Only allow keys that exist on the original post (blocks injection)
+ if ( ! array_key_exists( $meta_key, $original_custom_fields ) ) {
+ continue;
+ }
+
// Skip excluded meta keys
if ( in_array( $meta_key, $excluded_meta_keys, true ) ) {
continue;
}
- if ( ! array_key_exists( $meta_key, $cloned_meta_data ) ) {
- $cloned_meta_data[$meta_key] = [];
- }
-
- // before add the meta value check if the original value is a serialized array or object or json string and if so, format the new value accordingly
- $original_value = isset( $original_custom_fields[$meta_key] ) ? $original_custom_fields[$meta_key][0] : false;
-
- // Get the new meta value and decode JSON string if it is a JSON string
- $meta_value = $meta_item['value'];
- if ( is_string( $meta_value ) && is_json_string( $meta_value ) ) {
- $meta_value = json_decode( $meta_value, true );
- }
-
- // Format the new meta value accordingly
- if ( is_array( $meta_value ) ) {
- if ( $original_value ) {
- if ( is_serialized( $original_value ) ) {
- $meta_value = maybe_serialize( $meta_value );
- } elseif ( is_json_string( $original_value ) ) {
- $meta_value = wp_json_encode( $meta_value );
- }
- } else {
- // if $meta_value is array or object, serialize it
- if ( is_array( $meta_value ) || is_object( $meta_value ) ) {
+ if ( is_protected_meta( $meta_key, 'post' ) ) {
+ // Protected meta: use original values only to prevent value injection
+ $cloned_meta_data[ $meta_key ] = $original_custom_fields[ $meta_key ];
+ } else {
+ // Non-protected meta: allow user-edited values from the request
+ if ( ! array_key_exists( $meta_key, $cloned_meta_data ) ) {
+ $cloned_meta_data[ $meta_key ] = [];
+ }
+ $original_value = isset( $original_custom_fields[ $meta_key ] ) ? $original_custom_fields[ $meta_key ][0] : false;
+ $meta_value = isset( $meta_item['value'] ) ? $meta_item['value'] : '';
+ // Decode JSON string if needed
+ if ( is_string( $meta_value ) && is_json_string( $meta_value ) ) {
+ $meta_value = json_decode( $meta_value, true );
+ }
+ // Format value for storage (serialize vs json based on original format)
+ if ( is_array( $meta_value ) ) {
+ if ( $original_value ) {
+ if ( is_serialized( $original_value ) ) {
+ $meta_value = maybe_serialize( $meta_value );
+ } elseif ( is_json_string( $original_value ) ) {
+ $meta_value = wp_json_encode( $meta_value );
+ }
+ } else {
$meta_value = maybe_serialize( $meta_value );
}
}
+ $cloned_meta_data[ $meta_key ][] = $meta_value;
}
- $cloned_meta_data[$meta_key][] = $meta_value;
}
} else {
// Fall back to original behavior: duplicate all custom fields
- $cloned_meta_data = get_post_custom( $original_id );
+ $cloned_meta_data = $original_custom_fields;
}
// Insert the cloned meta data into the database
+ // Uses $wpdb->insert for keys validated against original (including protected meta like ACF fields)
foreach( $cloned_meta_data as $key => $value ) {
// Skip excluded meta keys
@@ -920,7 +945,7 @@
'%s',
'%s',
);
- $result = $wpdb->insert( $wpdb->prefix . 'postmeta', $data, $formats );
+ $wpdb->insert( $wpdb->prefix . 'postmeta', $data, $formats );
}
}
}
--- a/post-duplicator/includes/integrations/simple-custom-post-order.php
+++ b/post-duplicator/includes/integrations/simple-custom-post-order.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * Integration with Simple Custom Post Order (SCPO) plugin
+ *
+ * SCPO manages post order via the menu_order field and runs a "refresh" on every
+ * admin page load that reassigns sequential menu_order values when it detects
+ * gaps or duplicates. Post Duplicator copies menu_order from the original,
+ * creating duplicate values that trigger SCPO's refresh and cause unpredictable
+ * reordering of all posts.
+ *
+ * This integration assigns the duplicate a unique menu_order (max+1) when SCPO
+ * is active, preventing the refresh from running and preserving the user's order.
+ *
+ * @package MtphrPostDuplicatorIntegrations
+ */
+
+namespace MtphrPostDuplicatorIntegrations;
+
+// Only load when Simple Custom Post Order is active
+if ( ! class_exists( 'SCPO_Engine' ) ) {
+ return;
+}
+
+add_action( 'mtphr_post_duplicator_created', __NAMESPACE__ . 'set_duplicate_menu_order_for_scpo', 10, 3 );
+
+/**
+ * Set the duplicate's menu_order to a unique value when SCPO is managing this post type.
+ *
+ * SCPO's refresh() runs on admin_init and reassigns menu_order when cnt !== max.
+ * By assigning max+1, we keep the sequence intact so SCPO skips the refresh.
+ *
+ * @param int $original_id Original post ID
+ * @param int $duplicate_id Duplicated post ID
+ * @param array $settings Duplication settings
+ */
+function set_duplicate_menu_order_for_scpo( $original_id, $duplicate_id, $settings ) {
+ $duplicate_id = absint( $duplicate_id );
+ if ( ! $duplicate_id ) {
+ return;
+ }
+
+ $post_type = get_post_type( $duplicate_id );
+ if ( ! $post_type ) {
+ return;
+ }
+ $post_type = sanitize_key( $post_type );
+
+ $scpo_options = get_option( 'scporder_options', [] );
+ $objects = isset( $scpo_options['objects'] ) && is_array( $scpo_options['objects'] )
+ ? $scpo_options['objects']
+ : [];
+
+ if ( ! in_array( $post_type, $objects, true ) ) {
+ return;
+ }
+
+ global $wpdb;
+
+ $max = $wpdb->get_var(
+ $wpdb->prepare(
+ "SELECT MAX(menu_order) FROM $wpdb->posts
+ WHERE post_type = %s AND post_status IN ('publish', 'pending', 'draft', 'private', 'future')",
+ $post_type
+ )
+ );
+
+ $new_menu_order = (int) $max + 1;
+
+ $wpdb->update(
+ $wpdb->posts,
+ [ 'menu_order' => $new_menu_order ],
+ [ 'ID' => $duplicate_id ],
+ [ '%d' ],
+ [ '%d' ]
+ );
+
+ clean_post_cache( $duplicate_id );
+}
--- a/post-duplicator/m4c-postduplicator.php
+++ b/post-duplicator/m4c-postduplicator.php
@@ -3,7 +3,7 @@
Plugin Name: Post Duplicator
Plugin URI: https://www.metaphorcreations.com/post-duplicator/
Description: Creates functionality to duplicate any and all post types, including taxonomies & custom fields
-Version: 3.0.8
+Version: 3.0.9
Author: Meta4Creations
Author URI: https://www.metaphorcreations.com/
License: GPL-2.0+
@@ -36,7 +36,7 @@
// Plugin version.
if ( ! defined( 'MTPHR_POST_DUPLICATOR_VERSION' ) ) {
- define( 'MTPHR_POST_DUPLICATOR_VERSION', '3.0.8' );
+ define( 'MTPHR_POST_DUPLICATOR_VERSION', '3.0.9' );
}
// Plugin Folder Path.
@@ -94,6 +94,7 @@
add_action( 'plugins_loaded', function () {
require_once( MTPHR_POST_DUPLICATOR_DIR.'includes/integrations/the-events-calendar.php' );
require_once( MTPHR_POST_DUPLICATOR_DIR.'includes/integrations/wp-nested-pages.php' );
+ require_once( MTPHR_POST_DUPLICATOR_DIR.'includes/integrations/simple-custom-post-order.php' );
}, 20 );