/home/silvwabw/public_html/wp-content/plugins/post-duplicator/includes/api.php
<?php
namespace Mtphr\PostDuplicator;
add_action( 'rest_api_init', __NAMESPACE__ . '\register_routes' );
/**
* Register rest routes
*/
function register_routes() {
register_rest_route( 'post-duplicator/v1', 'duplicate-post', array(
'methods' => 'POST',
'permission_callback' => __NAMESPACE__ . '\duplicate_post_permissions',
'callback' => __NAMESPACE__ . '\duplicate_post',
) );
register_rest_route( 'post-duplicator/v1', 'post-data/(?P<id>\d+)', array(
'methods' => 'GET',
'permission_callback' => __NAMESPACE__ . '\get_post_data_permissions',
'callback' => __NAMESPACE__ . '\get_post_data',
'args' => array(
'id' => array(
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
),
),
) );
register_rest_route( 'post-duplicator/v1', 'post-full-data/(?P<id>\d+)', array(
'methods' => 'GET',
'permission_callback' => __NAMESPACE__ . '\get_post_data_permissions',
'callback' => __NAMESPACE__ . '\get_post_full_data',
'args' => array(
'id' => array(
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
),
),
) );
register_rest_route( 'post-duplicator/v1', 'parent-posts', array(
'methods' => 'GET',
'permission_callback' => __NAMESPACE__ . '\get_parent_posts_permissions',
'callback' => __NAMESPACE__ . '\get_parent_posts',
'args' => array(
'post_type' => array(
'validate_callback' => function( $param ) {
// Validate that it's a valid post type slug
return post_type_exists( sanitize_key( $param ) );
},
'sanitize_callback' => 'sanitize_key',
),
'exclude_id' => array(
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
),
),
) );
}
/**
* Permission check for getting post data
*/
function get_post_data_permissions( $request ) {
$post_id = $request->get_param( 'id' );
if ( ! $post_id ) {
return new \WP_Error( 'no_post_id', esc_html__( 'No post ID provided.', 'post-duplicator' ), array( 'status' => 403 ) );
}
$post = get_post( $post_id );
if ( ! $post ) {
return new \WP_Error( 'post_not_found', esc_html__( 'Post not found.', 'post-duplicator' ), array( 'status' => 404 ) );
}
if ( ! user_can_duplicate( $post ) ) {
return new \WP_Error( 'no_permission', esc_html__( 'User does not have permission to view this post.', 'post-duplicator' ), array( 'status' => 403 ) );
}
return true;
}
/**
* Get taxonomy and custom meta data for a post
*/
function get_post_data( $request ) {
$post_id = $request->get_param( 'id' );
$post = get_post( $post_id );
if ( ! $post ) {
return new \WP_Error( 'post_not_found', esc_html__( 'Post not found.', 'post-duplicator' ), array( 'status' => 404 ) );
}
// Get taxonomies
$taxonomies_data = array();
$taxonomies = get_object_taxonomies( $post->post_type );
$disabled_taxonomies = array( 'post_translations', 'post_format' );
foreach ( $taxonomies as $taxonomy_slug ) {
if ( in_array( $taxonomy_slug, $disabled_taxonomies ) ) {
continue;
}
$taxonomy = get_taxonomy( $taxonomy_slug );
if ( ! $taxonomy ) {
continue;
}
// Get terms currently assigned to the post
$assigned_term_ids = wp_get_post_terms( $post_id, $taxonomy_slug, array( 'fields' => 'ids' ) );
// Get ALL available terms for this taxonomy
$all_terms = get_terms( array(
'taxonomy' => $taxonomy_slug,
'hide_empty' => false,
) );
$terms_data = array();
if ( ! is_wp_error( $all_terms ) ) {
foreach ( $all_terms as $term ) {
$terms_data[] = array(
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
);
}
}
$taxonomies_data[] = array(
'slug' => $taxonomy_slug,
'label' => $taxonomy->labels->name,
'hierarchical' => $taxonomy->hierarchical,
'terms' => $terms_data,
'assignedTermIds' => $assigned_term_ids,
);
}
// Get custom meta fields
$custom_meta_data = array();
$custom_fields = get_post_custom( $post_id );
$excluded_meta_keys = get_excluded_meta_keys();
foreach ( $custom_fields as $key => $values ) {
// Skip excluded meta keys
if ( in_array( $key, $excluded_meta_keys, true ) ) {
continue;
}
// Check if meta is enabled via filter (defaults to true for all meta keys, including those starting with "_")
if ( ! apply_filters( "mtphr_post_duplicator_meta_{$key}_enabled", true ) ) {
continue;
}
foreach ( $values as $value ) {
// Detect data type
$type = 'string';
$is_serialized = false;
$original_value = $value;
// Check if serialized
if ( is_serialized( $value ) ) {
$is_serialized = true;
$unserialized = maybe_unserialize( $value );
if ( is_array( $unserialized ) ) {
$type = 'array';
$value = wp_json_encode( $unserialized, JSON_PRETTY_PRINT );
} elseif ( is_object( $unserialized ) ) {
$type = 'object';
$value = wp_json_encode( $unserialized, JSON_PRETTY_PRINT );
} else {
$type = 'string';
}
} elseif ( is_numeric( $value ) ) {
// Check if it's a number (int or float)
if ( strpos( $value, '.' ) !== false ) {
$type = 'number';
} else {
$type = 'number';
}
} elseif ( $value === 'true' || $value === 'false' || $value === '1' || $value === '0' || $value === '' ) {
// Could be boolean, but WordPress stores as string
$type = 'string';
}
// Try to detect JSON
if ( ! $is_serialized ) {
$json_decoded = json_decode( $value, true );
if ( json_last_error() === JSON_ERROR_NONE && ( is_array( $json_decoded ) || is_object( $json_decoded ) ) ) {
$type = is_array( $json_decoded ) ? 'array' : 'object';
$value = wp_json_encode( $json_decoded, JSON_PRETTY_PRINT );
}
}
$custom_meta_data[] = array(
'key' => $key,
'value' => $value,
'type' => $type,
'isSerialized' => $is_serialized,
'originalValue' => $original_value,
);
}
}
return rest_ensure_response( array(
'taxonomies' => $taxonomies_data,
'customMeta' => $custom_meta_data,
) );
}
/**
* Get full post data including title, slug, date, author, parent, featured image
* Works for all post types regardless of show_in_rest setting
*/
function get_post_full_data( $request ) {
$post_id = $request->get_param( 'id' );
$post = get_post( $post_id );
if ( ! $post ) {
return new \WP_Error( 'post_not_found', esc_html__( 'Post not found.', 'post-duplicator' ), array( 'status' => 404 ) );
}
// Get author name
$author_name = 'Unknown Author';
if ( $post->post_author && $post->post_author > 0 ) {
$author = get_userdata( $post->post_author );
if ( $author ) {
$author_name = $author->display_name;
}
}
// Get featured image data
$featured_image = null;
$featured_media_id = get_post_thumbnail_id( $post_id );
if ( $featured_media_id && $featured_media_id > 0 ) {
$attachment = get_post( $featured_media_id );
if ( $attachment && wp_attachment_is_image( $featured_media_id ) ) {
$image_url = wp_get_attachment_image_url( $featured_media_id, 'full' );
$thumbnail_url = wp_get_attachment_image_url( $featured_media_id, 'thumbnail' );
$alt_text = get_post_meta( $featured_media_id, '_wp_attachment_image_alt', true );
$featured_image = array(
'id' => $featured_media_id,
'url' => $image_url ? $image_url : '',
'thumbnail' => $thumbnail_url ? $thumbnail_url : $image_url,
'alt' => $alt_text ? $alt_text : '',
);
}
}
// Get parent post data if available
$parent_post = null;
if ( $post->post_parent && $post->post_parent > 0 ) {
$parent = get_post( $post->post_parent );
if ( $parent ) {
$parent_post = array(
'id' => $parent->ID,
'title' => $parent->post_title,
);
}
}
return rest_ensure_response( array(
'id' => $post->ID,
'title' => $post->post_title,
'type' => $post->post_type,
'status' => $post->post_status,
'slug' => $post->post_name,
'date' => $post->post_date,
'author' => $author_name,
'authorId' => $post->post_author,
'parent' => $post->post_parent || 0,
'parentPost' => $parent_post,
'featuredImage' => $featured_image,
) );
}
/**
* Permission check for getting parent posts
*/
function get_parent_posts_permissions( $request ) {
// User must be logged in and have edit capabilities
if ( ! is_user_logged_in() ) {
return new \WP_Error( 'not_logged_in', esc_html__( 'You must be logged in to access this endpoint.', 'post-duplicator' ), array( 'status' => 401 ) );
}
// Check if user has edit capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
return new \WP_Error( 'no_permission', esc_html__( 'You do not have permission to access this endpoint.', 'post-duplicator' ), array( 'status' => 403 ) );
}
return true;
}
/**
* Get available parent posts for a post type (hierarchical)
*/
function get_parent_posts( $request ) {
$post_type = $request->get_param( 'post_type' );
$exclude_id = $request->get_param( 'exclude_id' );
// Validate and sanitize post type
if ( ! $post_type || ! post_type_exists( $post_type ) ) {
// Default to 'page' if no valid post type specified
$post_type = 'page';
} else {
$post_type = sanitize_key( $post_type );
}
// Validate and sanitize exclude_id
if ( $exclude_id ) {
$exclude_id = absint( $exclude_id );
}
// Get posts that can be parents (same post type, published or draft)
$args = array(
'post_type' => $post_type,
'post_status' => array( 'publish', 'draft', 'private' ),
'posts_per_page' => -1,
'orderby' => 'menu_order title',
'order' => 'ASC',
);
// Exclude the current post (can't be its own parent)
if ( $exclude_id && $exclude_id > 0 ) {
$args['post__not_in'] = array( $exclude_id );
}
$posts = get_posts( $args );
// Build a map of posts by ID
$posts_map = array();
foreach ( $posts as $post ) {
$posts_map[ $post->ID ] = array(
'id' => $post->ID,
'title' => $post->post_title,
'parent' => $post->post_parent,
'children' => array(),
);
}
// Build hierarchical structure and collect descendants of excluded post
$excluded_descendants = array();
if ( $exclude_id && $exclude_id > 0 ) {
$exclude_id_int = $exclude_id;
// Recursively find all descendants of the excluded post from the full post list
// We need to query all posts to find descendants, not just the ones we're showing
$all_posts_args = array(
'post_type' => $post_type,
'post_status' => array( 'publish', 'draft', 'private' ),
'posts_per_page' => -1,
'fields' => 'ids',
);
$all_post_ids = get_posts( $all_posts_args );
$find_descendants = function( $parent_id ) use ( &$find_descendants, $all_post_ids, $post_type ) {
$descendants = array();
foreach ( $all_post_ids as $post_id ) {
$post = get_post( $post_id );
if ( $post && $post->post_parent == $parent_id ) {
$descendants[] = $post_id;
// Recursively get descendants of this child
$descendants = array_merge( $descendants, $find_descendants( $post_id ) );
}
}
return $descendants;
};
$excluded_descendants = $find_descendants( $exclude_id_int );
$excluded_descendants[] = $exclude_id_int; // Include the post itself
}
$root_posts = array();
foreach ( $posts_map as $post_id => $post_data ) {
// Skip if this post is a descendant of the excluded post
if ( in_array( $post_id, $excluded_descendants ) ) {
continue;
}
if ( $post_data['parent'] == 0 ) {
// Root level post
$root_posts[] = $post_id;
} else {
// Child post - add to parent's children array only if parent is not excluded
if ( isset( $posts_map[ $post_data['parent'] ] ) && ! in_array( $post_data['parent'], $excluded_descendants ) ) {
$posts_map[ $post_data['parent'] ]['children'][] = $post_id;
} else {
// Parent not in our list (different post type or excluded), treat as root
$root_posts[] = $post_id;
}
}
}
// Recursive function to build flat list with hierarchy info
$hierarchical_list = array();
$build_list = function( $post_ids, $level = 0 ) use ( &$build_list, &$hierarchical_list, &$posts_map ) {
foreach ( $post_ids as $post_id ) {
if ( ! isset( $posts_map[ $post_id ] ) ) {
continue;
}
$post_data = $posts_map[ $post_id ];
$hierarchical_list[] = array(
'id' => $post_data['id'],
'title' => $post_data['title'],
'level' => $level,
'parent' => $post_data['parent'],
);
// Recursively add children
if ( ! empty( $post_data['children'] ) ) {
$build_list( $post_data['children'], $level + 1 );
}
}
};
// Build the hierarchical list starting from root posts
$build_list( $root_posts );
return rest_ensure_response( $hierarchical_list );
}
/**
* Check if a meta value contains HTML and should be sanitized with wp_kses_post
*
* @param string $meta_value The meta value to check
* @param string $meta_key The meta key
* @param int $post_id The post ID (optional, for ACF field detection)
* @return bool True if the value contains HTML
*/
function meta_value_contains_html( $meta_value, $meta_key = '', $post_id = 0 ) {
// Check if value is empty or not a string
if ( empty( $meta_value ) || ! is_string( $meta_value ) ) {
return false;
}
// First, check if value contains HTML tags (most reliable method)
// Use trim to handle whitespace-only differences
$stripped = strip_tags( trim( $meta_value ) );
$trimmed_original = trim( $meta_value );
if ( $stripped !== $trimmed_original && strlen( $trimmed_original ) > strlen( $stripped ) ) {
// Contains HTML tags - the stripped version is shorter, meaning tags were removed
return true;
}
// Check if ACF is active and this might be an ACF WYSIWYG field
if ( function_exists( 'acf_get_field' ) && $post_id > 0 && ! empty( $meta_key ) ) {
// For ACF flexible content fields, check if there's a corresponding field key
// Pattern: fieldname_0_subfieldname -> _fieldname_0_subfieldname contains field key
$field_key_meta = '_' . $meta_key;
$field_key = get_post_meta( $post_id, $field_key_meta, true );
if ( ! empty( $field_key ) && strpos( $field_key, 'field_' ) === 0 ) {
// This is an ACF field, check its type
$field = acf_get_field( $field_key );
if ( $field && isset( $field['type'] ) ) {
// Check for WYSIWYG and other HTML-capable field types
$html_field_types = array( 'wysiwyg', 'textarea', 'oembed', 'url' );
if ( in_array( $field['type'], $html_field_types, true ) ) {
return true;
}
}
}
// Also check for common ACF field name patterns that indicate HTML content
// Patterns like: *_editor_*, *_wysiwyg_*, *_html_*, *_content_*
$html_patterns = array( 'editor', 'wysiwyg', 'html', 'content', 'description', 'text' );
foreach ( $html_patterns as $pattern ) {
if ( stripos( $meta_key, $pattern ) !== false ) {
// Check if this is actually an ACF field by looking for the field key
$field_key_meta = '_' . $meta_key;
$field_key = get_post_meta( $post_id, $field_key_meta, true );
if ( ! empty( $field_key ) && strpos( $field_key, 'field_' ) === 0 ) {
return true;
}
}
}
}
return false;
}
/**
* Sanitize meta value appropriately based on content type
*
* @param string $meta_value The meta value to sanitize
* @param string $meta_key The meta key
* @param int $post_id The post ID (optional, for ACF field detection)
* @return string Sanitized meta value
*/
function sanitize_meta_value( $meta_value, $meta_key = '', $post_id = 0 ) {
// Check if value contains HTML
if ( meta_value_contains_html( $meta_value, $meta_key, $post_id ) ) {
// Use wp_kses_post to preserve HTML but sanitize it
// wp_kses_post sanitizes HTML while preserving allowed tags
// $wpdb->insert() handles escaping automatically via prepared statements
return wp_kses_post( $meta_value );
}
// Default to sanitize_text_field for plain text
return sanitize_text_field( $meta_value );
}
/**
* Duplicate a post
*/
function duplicate_post_permissions( $request ) {
$data = $request->get_json_params();
$original_id = isset( $data['original_id'] ) ? $data['original_id'] : false;
if ( ! $original_id ) {
return new \WP_Error( 'no_original_id', esc_html__( 'No original id passed.', 'post-duplicator' ), array( 'status' => 403 ) );
}
// Validate original_id is a positive integer
$original_id = absint( $original_id );
if ( ! $original_id || $original_id <= 0 ) {
return new \WP_Error( 'invalid_original_id', esc_html__( 'Invalid original id.', 'post-duplicator' ), array( 'status' => 403 ) );
}
$post = get_post( $original_id );
if ( ! $post ) {
return new \WP_Error( 'post_not_found', esc_html__( 'Post not found.', 'post-duplicator' ), array( 'status' => 404 ) );
}
if ( ! user_can_duplicate( $post ) ) {
return new \WP_Error( 'no_permission', esc_html__( 'User does not have permission to duplicate post.', 'post-duplicator' ), array( 'status' => 403 ) );
}
return true;
}
/**
* Duplicate a post
*/
function duplicate_post( $request ) {
$data = $request->get_json_params();
// Get access to the database
global $wpdb;
// Get and validate the original id
$original_id = isset( $data['original_id'] ) ? absint( $data['original_id'] ) : 0;
if ( ! $original_id || $original_id <= 0 ) {
return new \WP_Error( 'invalid_original_id', esc_html__( 'Invalid original id.', 'post-duplicator' ), array( 'status' => 400 ) );
}
// Get the original post object
$orig = get_post( $original_id );
if ( ! $orig ) {
return new \WP_Error( 'post_not_found', esc_html__( 'Original post not found.', 'post-duplicator' ), array( 'status' => 404 ) );
}
// Get default settings
$default_settings = get_option_value();
// Merge with any override settings from the request
// Remove original_id from data to get only settings
$override_settings = $data;
unset( $override_settings['original_id'] );
// Merge: override settings take precedence
$settings = array_merge( $default_settings, $override_settings );
// Create an empty array and populate only the fields we want
// This ensures we don't carry over any unwanted data
$duplicate = array();
// Copy basic post fields explicitly
$duplicate['post_author'] = $orig->post_author;
$duplicate['post_content'] = $orig->post_content;
$duplicate['post_title'] = $orig->post_title;
$duplicate['post_excerpt'] = $orig->post_excerpt;
$duplicate['post_status'] = $orig->post_status;
$duplicate['comment_status'] = $orig->comment_status;
$duplicate['ping_status'] = $orig->ping_status;
$duplicate['post_password'] = $orig->post_password;
$duplicate['post_name'] = $orig->post_name;
$duplicate['to_ping'] = $orig->to_ping;
$duplicate['pinged'] = $orig->pinged;
$duplicate['post_content_filtered'] = $orig->post_content_filtered;
$duplicate['post_parent'] = $orig->post_parent;
$duplicate['menu_order'] = $orig->menu_order;
$duplicate['post_type'] = $orig->post_type;
$duplicate['post_mime_type'] = $orig->post_mime_type;
// Modify the title
// If fullTitle is provided (user edited the full title), use it
// Otherwise, append the suffix
if ( isset( $settings['fullTitle'] ) && ! empty( $settings['fullTitle'] ) ) {
$duplicate['post_title'] = sanitize_text_field( $settings['fullTitle'] );
} else {
$appended = isset( $settings['title'] ) ? sanitize_text_field( $settings['title'] ) : esc_html__( 'Copy', 'post-duplicator' );
$duplicate['post_title'] = $duplicate['post_title'] . ' ' . $appended;
}
// Modify the slug
// If fullSlug is provided (user edited the full slug), use it
// Otherwise, append the suffix
if ( isset( $settings['fullSlug'] ) && ! empty( $settings['fullSlug'] ) ) {
$duplicate['post_name'] = sanitize_title( $settings['fullSlug'] );
} else {
$duplicate['post_name'] = sanitize_title( $duplicate['post_name'] . '-' . $settings['slug'] );
}
// Set the status - validate against allowed statuses
if( $settings['status'] != 'same' ) {
$allowed_statuses = array( 'draft', 'publish', 'pending', 'private', 'future' );
$requested_status = sanitize_text_field( $settings['status'] );
if ( in_array( $requested_status, $allowed_statuses, true ) ) {
$duplicate['post_status'] = $requested_status;
} else {
// Invalid status, default to draft
$duplicate['post_status'] = 'draft';
}
}
// Check if a user has publish get_post_type_capabilities. If not, make sure they can't _publish
if ( ! current_user_can( 'publish_posts' ) ) {
// Force the post status to pending
if ( 'publish' == $duplicate['post_status'] ) {
$duplicate['post_status'] = 'pending';
}
}
// Set the type - validate against allowed post types
if( $settings['type'] != 'same' ) {
$requested_type = sanitize_key( $settings['type'] );
// Validate that the post type exists and user has permission to create it
if ( post_type_exists( $requested_type ) && current_user_can( get_post_type_object( $requested_type )->cap->create_posts ) ) {
$duplicate['post_type'] = $requested_type;
} else {
// Invalid post type or no permission, keep original type
$duplicate['post_type'] = $orig->post_type;
}
}
// Set the parent - check for selectedParentId first, otherwise keep original parent
if ( isset( $settings['selectedParentId'] ) ) {
$duplicate['post_parent'] = intval( $settings['selectedParentId'] );
}
// Set the post date
if ( $settings['timestamp'] == 'duplicate' ) {
$timestamp = strtotime($orig->post_date);
$timestamp_gmt = strtotime($orig->post_date_gmt);
} elseif ( $settings['timestamp'] == 'custom' && isset( $settings['customDate'] ) && ! empty( $settings['customDate'] ) ) {
// Use custom date if provided
$custom_date = $settings['customDate'];
try {
// Parse the ISO date string (e.g., "2024-01-15T10:30:00.000Z")
// JavaScript's toISOString() returns UTC time
// Convert ISO format to WordPress date format (Y-m-d H:i:s)
$date_obj = new \DateTime( $custom_date, new \DateTimeZone( 'UTC' ) );
$gmt_date = $date_obj->format( 'Y-m-d H:i:s' );
// Convert GMT date to local timezone using WordPress function
$local_date = get_date_from_gmt( $gmt_date );
$timestamp = strtotime( $local_date );
$timestamp_gmt = strtotime( $gmt_date );
} catch ( \Exception $e ) {
// If date parsing fails, fall back to current time
$timestamp = current_time('timestamp',0);
$timestamp_gmt = current_time('timestamp',1);
}
} else {
$timestamp = current_time('timestamp',0);
$timestamp_gmt = current_time('timestamp',1);
}
if( isset( $settings['time_offset'] ) && $settings['time_offset'] ) {
$offset = intval($settings['time_offset_seconds']+$settings['time_offset_minutes']*60+$settings['time_offset_hours']*3600+$settings['time_offset_days']*86400);
if( $settings['time_offset_direction'] == 'newer' ) {
$timestamp = intval($timestamp+$offset);
$timestamp_gmt = intval($timestamp_gmt+$offset);
} else {
$timestamp = intval($timestamp-$offset);
$timestamp_gmt = intval($timestamp_gmt-$offset);
}
}
$duplicate['post_date'] = date('Y-m-d H:i:s', $timestamp);
$duplicate['post_date_gmt'] = date('Y-m-d H:i:s', $timestamp_gmt);
$duplicate['post_modified'] = date('Y-m-d H:i:s', current_time('timestamp',0));
$duplicate['post_modified_gmt'] = date('Y-m-d H:i:s', current_time('timestamp',1));
// Set author - check for selectedAuthorId first, then fall back to post_author setting
// Handle "No Author" case (null or empty selectedAuthorId)
if ( isset( $settings['selectedAuthorId'] ) ) {
if ( $settings['selectedAuthorId'] === null || $settings['selectedAuthorId'] === '' || $settings['selectedAuthorId'] === 0 ) {
// "No Author" - set to 0 for post types that don't support authors
$duplicate['post_author'] = 0;
} else {
$duplicate['post_author'] = intval( $settings['selectedAuthorId'] );
}
} elseif ( 'current_user' == $settings['post_author'] ) {
$duplicate['post_author'] = get_current_user_id();
}
// Sanitize post content
add_filter( 'wp_kses_allowed_html', __NAMESPACE__ . '\additional_kses', 10, 2 );
$duplicate['post_content'] = wp_slash( wp_kses_post( $duplicate['post_content'] ) );
remove_filter( 'wp_kses_allowed_html', __NAMESPACE__ . '\additional_kses', 10, 2 );
// Insert the post into the database
$duplicate_id = wp_insert_post( $duplicate );
// Handle featured image
if ( isset( $settings['featuredImageId'] ) ) {
// If featuredImageId is null or 0, remove the featured image
if ( empty( $settings['featuredImageId'] ) ) {
delete_post_thumbnail( $duplicate_id );
} else {
// Set the featured image
$thumbnail_id = intval( $settings['featuredImageId'] );
// Verify the attachment exists and is an image
$attachment = get_post( $thumbnail_id );
if ( $attachment && wp_attachment_is_image( $thumbnail_id ) ) {
set_post_thumbnail( $duplicate_id, $thumbnail_id );
}
}
} else {
// Default behavior: copy featured image from original post if it exists
$original_thumbnail_id = get_post_thumbnail_id( $original_id );
if ( $original_thumbnail_id ) {
set_post_thumbnail( $duplicate_id, $original_thumbnail_id );
}
}
// check which terms are connected to the duplicate post right here and now
$duplicate_terms = wp_get_post_terms( $duplicate_id, get_object_taxonomies( $duplicate['post_type'] ) );
// Handle taxonomies
// Default to true for backward compatibility
$include_taxonomies = false;
if ( isset( $settings['includeTaxonomies'] ) && false !== $settings['includeTaxonomies'] ) {
$tax_value = $settings['includeTaxonomies'];
// Handle both boolean and string boolean values from JSON
if ( is_bool( $tax_value ) ) {
$include_taxonomies = $tax_value;
} elseif ( is_string( $tax_value ) ) {
// Handle string booleans - explicitly check for false strings
$include_taxonomies = ! ( $tax_value === 'false' || $tax_value === '0' || $tax_value === '' );
} elseif ( $tax_value === 0 || $tax_value === '0' ) {
// Explicitly handle 0/false values
$include_taxonomies = false;
} else {
// For any other value, cast to bool
$include_taxonomies = (bool) $tax_value;
}
}
// Only duplicate taxonomies if explicitly enabled
// Use strict comparison to ensure false/0 values are respected
if ( $include_taxonomies === true ) {
// Use provided taxonomy data if available, otherwise fetch from original post
if ( isset( $settings['taxonomyData'] ) && is_array( $settings['taxonomyData'] ) && ! empty( $settings['taxonomyData'] ) ) {
// Use provided taxonomy data
foreach ( $settings['taxonomyData'] as $taxonomy_slug => $term_ids ) {
if ( ! is_array( $term_ids ) ) {
continue;
}
// Validate taxonomy slug
$taxonomy_slug = sanitize_key( $taxonomy_slug );
if ( ! taxonomy_exists( $taxonomy_slug ) ) {
continue; // Skip invalid taxonomy
}
// Verify taxonomy is registered for this post type
if ( ! is_object_in_taxonomy( $duplicate['post_type'], $taxonomy_slug ) ) {
continue; // Skip taxonomies not registered for this post type
}
// Convert term IDs to integers and filter out invalid values
$term_ids = array_map( 'absint', $term_ids );
$term_ids = array_filter( $term_ids );
// Verify all term IDs exist and belong to the correct taxonomy
$valid_term_ids = array();
foreach ( $term_ids as $term_id ) {
$term = get_term( $term_id, $taxonomy_slug );
if ( $term && ! is_wp_error( $term ) ) {
$valid_term_ids[] = $term_id;
}
}
if ( ! empty( $valid_term_ids ) ) {
wp_set_object_terms( $duplicate_id, $valid_term_ids, $taxonomy_slug );
}
}
} elseif ( ! isset( $settings['taxonomyData'] ) ) {
// Only fall back to original behavior if taxonomyData was not provided at all
// This means the user didn't customize, so use default behavior
$taxonomies = get_object_taxonomies( $duplicate['post_type'] );
$disabled_taxonomies = ['post_translations'];
foreach( $taxonomies as $taxonomy ) {
if ( in_array( $taxonomy, $disabled_taxonomies ) ) {
continue;
}
$terms = wp_get_post_terms( $original_id, $taxonomy, array('fields' => 'names') );
wp_set_object_terms( $duplicate_id, $terms, $taxonomy );
}
}
// If includeTaxonomies is false, do nothing - taxonomies are not duplicated
}
// Handle custom meta fields
// Default to true for backward compatibility
$include_custom_meta = false;
if ( isset( $settings['includeCustomMeta'] ) && false !== $settings['includeCustomMeta'] ) {
// Handle both boolean and string boolean values from JSON
if ( is_bool( $settings['includeCustomMeta'] ) ) {
$include_custom_meta = $settings['includeCustomMeta'];
} elseif ( is_string( $settings['includeCustomMeta'] ) ) {
// Handle string booleans (shouldn't happen with proper JSON, but be safe)
$include_custom_meta = ( $settings['includeCustomMeta'] === 'true' || $settings['includeCustomMeta'] === '1' );
} elseif ( $settings['includeCustomMeta'] === 0 || $settings['includeCustomMeta'] === '0' ) {
// Explicitly handle 0/false values
$include_custom_meta = false;
} else {
$include_custom_meta = (bool) $settings['includeCustomMeta'];
}
}
// Only duplicate custom meta if explicitly enabled
if ( $include_custom_meta === true ) {
// 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
$excluded_meta_keys = get_excluded_meta_keys();
foreach ( $settings['customMetaData'] as $meta_item ) {
if ( ! isset( $meta_item['key'] ) || ! isset( $meta_item['value'] ) ) {
continue;
}
$meta_key = sanitize_key( $meta_item['key'] );
// Validate meta key is not empty and follows WordPress naming conventions
if ( empty( $meta_key ) || strlen( $meta_key ) > 255 ) {
continue; // Skip invalid meta keys
}
// Skip excluded meta keys
if ( in_array( $meta_key, $excluded_meta_keys, true ) ) {
continue;
}
// Prefer originalValue if available - it contains the raw value from the database
// This ensures we preserve HTML that might have been processed/stripped in the frontend
$meta_value = $meta_item['value'];
$use_original = false;
if ( isset( $meta_item['originalValue'] ) && ! empty( $meta_item['originalValue'] ) && is_string( $meta_item['originalValue'] ) ) {
// Check if this is an ACF WYSIWYG field - if so, always use originalValue
if ( function_exists( 'acf_get_field' ) && $original_id > 0 ) {
$field_key_meta = '_' . $meta_key;
$field_key = get_post_meta( $original_id, $field_key_meta, true );
if ( ! empty( $field_key ) && strpos( $field_key, 'field_' ) === 0 ) {
$field = acf_get_field( $field_key );
if ( $field && isset( $field['type'] ) && $field['type'] === 'wysiwyg' ) {
// This is a WYSIWYG field, always use originalValue
$use_original = true;
}
}
}
// Also check if originalValue contains HTML
if ( ! $use_original && meta_value_contains_html( $meta_item['originalValue'], $meta_key, $original_id ) ) {
$use_original = true;
}
if ( $use_original ) {
$meta_value = $meta_item['originalValue'];
}
}
// Handle data type preservation
if ( isset( $meta_item['type'] ) && isset( $meta_item['isSerialized'] ) ) {
if ( $meta_item['isSerialized'] ) {
// It was originally serialized, so serialize it again
// Try to decode JSON first if it's a JSON string (from the modal)
$json_decoded = json_decode( $meta_value, true );
if ( json_last_error() === JSON_ERROR_NONE ) {
$meta_value = maybe_serialize( $json_decoded );
} else {
// Already a string, serialize it
$meta_value = maybe_serialize( $meta_value );
}
} elseif ( in_array( $meta_item['type'], array( 'array', 'object' ) ) ) {
// It was originally a JSON string (not serialized), so keep it as JSON
// Validate it's valid JSON - if valid, keep as-is to preserve exact format
// Only re-encode if necessary (e.g., if user modified it in the modal)
$json_decoded = json_decode( $meta_value, true );
if ( json_last_error() === JSON_ERROR_NONE ) {
// Valid JSON - re-encode to ensure it's properly formatted
// Use wp_json_encode which handles WordPress-specific encoding
$meta_value = wp_json_encode( $json_decoded );
} else {
// Invalid JSON, sanitize appropriately (may contain HTML)
$meta_value = sanitize_meta_value( $meta_value, $meta_key, $original_id );
}
} elseif ( $meta_item['type'] === 'number' ) {
// Preserve as number (WordPress will store as string anyway, but we can validate)
$meta_value = is_numeric( $meta_value ) ? $meta_value : sanitize_text_field( $meta_value );
} else {
// String type - check if it contains HTML (e.g., ACF WYSIWYG fields)
$meta_value = sanitize_meta_value( $meta_value, $meta_key, $original_id );
}
} else {
// Fallback: sanitize appropriately (may contain HTML)
$meta_value = sanitize_meta_value( $meta_value, $meta_key, $original_id );
}
// Apply filters
if ( ! apply_filters( "mtphr_post_duplicator_meta_{$meta_key}_enabled", true ) ) {
continue;
}
$meta_value = apply_filters( "mtphr_post_duplicator_meta_value", $meta_value, $meta_key, $duplicate_id, $duplicate['post_type'] );
$data = array(
'post_id' => intval( $duplicate_id ),
'meta_key' => $meta_key,
'meta_value' => $meta_value,
);
$formats = array(
'%d',
'%s',
'%s',
);
$result = $wpdb->insert( $wpdb->prefix.'postmeta', $data, $formats );
}
} else {
// Fall back to original behavior: duplicate all custom fields
$custom_fields = get_post_custom( $original_id );
$excluded_meta_keys = get_excluded_meta_keys();
foreach ( $custom_fields as $key => $value ) {
// Skip excluded meta keys
if ( in_array( $key, $excluded_meta_keys, true ) ) {
continue;
}
if( is_array($value) && count($value) > 0 ) {
foreach( $value as $i=>$v ) {
if ( ! apply_filters( "mtphr_post_duplicator_meta_{$key}_enabled", true ) ) {
continue;
}
// Sanitize meta value appropriately (may contain HTML for ACF WYSIWYG fields)
$meta_value = sanitize_meta_value( $v, $key, $original_id );
$meta_value = apply_filters( "mtphr_post_duplicator_meta_value", $meta_value, $key, $duplicate_id, $duplicate['post_type'] );
$data = array(
'post_id' => intval( $duplicate_id ),
'meta_key' => sanitize_text_field( $key ),
'meta_value' => $meta_value,
);
$formats = array(
'%d',
'%s',
'%s',
);
$result = $wpdb->insert( $wpdb->prefix.'postmeta', $data, $formats );
}
}
}
}
}
// Add an action for others to do custom stuff
do_action( 'mtphr_post_duplicator_created', $original_id, $duplicate_id, $settings );
$other_data = array(
'duplicate_post' => $duplicate,
'duplicate_terms' => $duplicate_terms,
);
return rest_ensure_response( [
'duplicate_id' => $duplicate_id,
'other_data' => $other_data,
] , 200 );
}
/**
* Add custom allowed kses
*/
function additional_kses( $allowed_tags ) {
// Allow the center tag with its attributes
$allowed_tags['center'] = array(
'align' => true,
'class' => true,
'id' => true,
'style' => true,
);
return $allowed_tags;
}