/
home
/
faureele
/
auvergne
/
wp-content
/
plugins
/
post-duplicator
/
includes
/
Upload File
HOME
<?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', ), ), ) ); register_rest_route( 'post-duplicator/v1', 'users', array( 'methods' => 'GET', 'permission_callback' => __NAMESPACE__ . '\get_users_permissions', 'callback' => __NAMESPACE__ . '\get_users', ) ); register_rest_route( 'post-duplicator/v1', 'search-terms', array( 'methods' => 'GET', 'permission_callback' => __NAMESPACE__ . '\search_terms_permissions', 'callback' => __NAMESPACE__ . '\search_terms', 'args' => array( 'taxonomy' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_key', 'validate_callback' => function( $param ) { return taxonomy_exists( sanitize_key( $param ) ); }, ), 'search' => array( 'required' => false, 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ), 'number' => array( 'required' => false, 'type' => 'integer', 'default' => 20, '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; } // Only return terms currently assigned to the post. Full taxonomies are // searched on demand via the search-terms endpoint so large term sets // are not loaded into the duplication modal. $assigned_terms = wp_get_post_terms( $post_id, $taxonomy_slug, array( 'update_term_meta_cache' => false, ) ); $assigned_term_ids = array(); $terms_data = array(); if ( ! is_wp_error( $assigned_terms ) ) { foreach ( $assigned_terms as $term ) { $assigned_term_ids[] = (int) $term->term_id; $terms_data[] = array( 'id' => (int) $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; $trimmed_value = trim( $value ); // Never decode serialized objects; keep them as raw strings. if ( preg_match( '/^(?:O|C):\d+:/', $trimmed_value ) ) { $type = 'string'; } else { $unserialized = @unserialize( $trimmed_value, array( 'allowed_classes' => false ) ); $is_valid_unserialized = ( false !== $unserialized || 'b:0;' === $trimmed_value ); if ( $is_valid_unserialized && is_array( $unserialized ) ) { $type = 'array'; $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, ) ); } /** * Permission check for searching taxonomy terms */ function search_terms_permissions( $request ) { 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 ) ); } 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 ) ); } $taxonomy_slug = sanitize_key( $request->get_param( 'taxonomy' ) ); $taxonomy = get_taxonomy( $taxonomy_slug ); if ( ! $taxonomy ) { return new \WP_Error( 'invalid_taxonomy', esc_html__( 'Invalid taxonomy.', 'post-duplicator' ), array( 'status' => 400 ) ); } if ( ! current_user_can( $taxonomy->cap->assign_terms ) ) { return new \WP_Error( 'no_permission', esc_html__( 'You do not have permission to assign terms in this taxonomy.', 'post-duplicator' ), array( 'status' => 403 ) ); } return true; } /** * Search taxonomy terms for the duplication modal */ function search_terms( $request ) { $taxonomy_slug = sanitize_key( $request->get_param( 'taxonomy' ) ); $search = sanitize_text_field( $request->get_param( 'search' ) ); $number = absint( $request->get_param( 'number' ) ); if ( $number < 1 ) { $number = 20; } elseif ( $number > 50 ) { $number = 50; } $args = array( 'taxonomy' => $taxonomy_slug, 'hide_empty' => false, 'number' => $number, 'orderby' => 'name', 'order' => 'ASC', 'update_term_meta_cache' => false, ); if ( '' !== $search ) { $args['search'] = $search; } $terms = get_terms( $args ); $terms_data = array(); if ( ! is_wp_error( $terms ) ) { foreach ( $terms as $term ) { $terms_data[] = array( 'id' => (int) $term->term_id, 'name' => $term->name, 'slug' => $term->slug, ); } } return rest_ensure_response( $terms_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' => (int) $post->post_parent, '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 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 ) ); } if ( ! is_post_type_duplication_enabled( $post->post_type ) ) { return new \WP_Error( 'type_disabled', esc_html__( 'Duplication is disabled for this post type.', 'post-duplicator' ), array( 'status' => 403 ) ); } return true; } /** * Core duplication engine. * * Performs the actual post duplication using the provided settings array. * Both the REST API endpoint and the PHP fallback handler use this function * so that all duplication logic lives in one place. * * @param WP_Post $orig The original post object. * @param array $settings Merged settings (plugin defaults + any caller overrides). * @return array|\WP_Error Array with duplicate_id, permalink, duplicate, duplicate_terms * on success; WP_Error on failure. */ function perform_duplication( $orig, $settings ) { $original_id = $orig->ID; $duplicate = array(); $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; // Explicitly omit guid — WordPress generates a fresh one from get_permalink() // after insertion. An empty string here guarantees the original's guid is // never carried over, including for non-public post types where get_permalink() // may otherwise return an ambiguous fallback URL. $duplicate['guid'] = ''; // Modify the title 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 ( 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 { $duplicate['post_status'] = 'draft'; } } // Restrict publish-gated statuses for users without publish capability if ( ! current_user_can( 'publish_posts' ) ) { if ( in_array( $duplicate['post_status'], array( 'publish', 'private', 'future' ), true ) ) { $duplicate['post_status'] = 'pending'; } } // Set the type - validate against allowed post types if ( $settings['type'] != 'same' ) { $requested_type = sanitize_key( $settings['type'] ); if ( post_type_exists( $requested_type ) && current_user_can( get_post_type_object( $requested_type )->cap->create_posts ) ) { $duplicate['post_type'] = $requested_type; } else { $duplicate['post_type'] = $orig->post_type; } } // Set the parent - only when explicitly provided in settings if ( array_key_exists( 'selectedParentId', $settings ) ) { $selected_parent = $settings['selectedParentId']; 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 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'] ) ) { $custom_date = $settings['customDate']; try { $date_obj = new \DateTime( $custom_date, new \DateTimeZone( 'UTC' ) ); $gmt_date = $date_obj->format( 'Y-m-d H:i:s' ); $local_date = get_date_from_gmt( $gmt_date ); $timestamp = strtotime( $local_date ); $timestamp_gmt = strtotime( $gmt_date ); } catch ( \Exception $e ) { $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 if ( isset( $settings['selectedAuthorId'] ) ) { $requested_author = intval( $settings['selectedAuthorId'] ); if ( $settings['selectedAuthorId'] === null || $settings['selectedAuthorId'] === '' || $requested_author === 0 ) { $duplicate['post_author'] = 0; } elseif ( $requested_author !== get_current_user_id() ) { if ( current_user_can( 'edit_others_posts' ) ) { $duplicate['post_author'] = $requested_author; } else { $duplicate['post_author'] = get_current_user_id(); } } else { $duplicate['post_author'] = $requested_author; } } 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 ); if ( is_wp_error( $duplicate_id ) || ! $duplicate_id ) { return new \WP_Error( 'insert_failed', esc_html__( 'Failed to create duplicate post.', 'post-duplicator' ) ); } // Handle featured image if ( isset( $settings['featuredImageId'] ) ) { if ( empty( $settings['featuredImageId'] ) ) { delete_post_thumbnail( $duplicate_id ); } else { $thumbnail_id = intval( $settings['featuredImageId'] ); $attachment = get_post( $thumbnail_id ); if ( $attachment && wp_attachment_is_image( $thumbnail_id ) ) { set_post_thumbnail( $duplicate_id, $thumbnail_id ); } } } else { $original_thumbnail_id = get_post_thumbnail_id( $original_id ); if ( $original_thumbnail_id ) { set_post_thumbnail( $duplicate_id, $original_thumbnail_id ); } } $duplicate_terms = wp_get_post_terms( $duplicate_id, get_object_taxonomies( $duplicate['post_type'] ) ); // Handle taxonomies $include_taxonomies = false; if ( isset( $settings['includeTaxonomies'] ) && false !== $settings['includeTaxonomies'] ) { $tax_value = $settings['includeTaxonomies']; if ( is_bool( $tax_value ) ) { $include_taxonomies = $tax_value; } elseif ( is_string( $tax_value ) ) { $include_taxonomies = ! ( $tax_value === 'false' || $tax_value === '0' || $tax_value === '' ); } elseif ( $tax_value === 0 || $tax_value === '0' ) { $include_taxonomies = false; } else { $include_taxonomies = (bool) $tax_value; } } if ( $include_taxonomies === true ) { if ( isset( $settings['taxonomyData'] ) && is_array( $settings['taxonomyData'] ) && ! empty( $settings['taxonomyData'] ) ) { foreach ( $settings['taxonomyData'] as $taxonomy_slug => $term_ids ) { if ( ! is_array( $term_ids ) ) { continue; } $taxonomy_slug = sanitize_key( $taxonomy_slug ); if ( ! taxonomy_exists( $taxonomy_slug ) ) { continue; } if ( ! is_object_in_taxonomy( $duplicate['post_type'], $taxonomy_slug ) ) { continue; } $term_ids = array_map( 'absint', $term_ids ); $term_ids = array_filter( $term_ids ); $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'] ) ) { $taxonomies = get_object_taxonomies( $duplicate['post_type'] ); $disabled_taxonomies = array( '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 ); } } } // Handle custom meta fields $include_custom_meta = false; if ( isset( $settings['includeCustomMeta'] ) && false !== $settings['includeCustomMeta'] ) { if ( is_bool( $settings['includeCustomMeta'] ) ) { $include_custom_meta = $settings['includeCustomMeta']; } elseif ( is_string( $settings['includeCustomMeta'] ) ) { $include_custom_meta = ( $settings['includeCustomMeta'] === 'true' || $settings['includeCustomMeta'] === '1' ); } elseif ( $settings['includeCustomMeta'] === 0 || $settings['includeCustomMeta'] === '0' ) { $include_custom_meta = false; } else { $include_custom_meta = (bool) $settings['includeCustomMeta']; } } if ( $include_custom_meta === true ) { $excluded_meta_keys = get_excluded_meta_keys(); $original_custom_fields = get_post_custom( $original_id ); $cloned_meta_data = array(); if ( isset( $settings['customMetaData'] ) && is_array( $settings['customMetaData'] ) ) { // Security: only copy keys that exist on the original post (blocks key injection). // For protected meta: use original values only (blocks value injection). foreach ( $settings['customMetaData'] as $meta_item ) { if ( ! isset( $meta_item['key'] ) ) { continue; } $meta_key = $meta_item['key']; if ( empty( $meta_key ) || strlen( $meta_key ) > 255 ) { continue; } if ( ! array_key_exists( $meta_key, $original_custom_fields ) ) { continue; } if ( in_array( $meta_key, $excluded_meta_keys, true ) ) { continue; } if ( is_protected_meta( $meta_key, 'post' ) ) { // Decode raw stored values so add_post_meta() can re-serialize them // safely without double serialization corrupting the result. $cloned_meta_data[ $meta_key ] = array_map( 'maybe_unserialize', $original_custom_fields[ $meta_key ] ); } else { if ( ! array_key_exists( $meta_key, $cloned_meta_data ) ) { $cloned_meta_data[ $meta_key ] = array(); } $original_value = isset( $original_custom_fields[ $meta_key ] ) ? $original_custom_fields[ $meta_key ][0] : false; $meta_value = isset( $meta_item['value'] ) ? $meta_item['value'] : ''; if ( is_string( $meta_value ) && is_json_string( $meta_value ) ) { $meta_value = json_decode( $meta_value, true ); } if ( is_array( $meta_value ) ) { // Preserve JSON encoding when the original meta was stored as JSON. // For arrays destined for PHP serialization, leave as an array; // add_post_meta() below will serialize via maybe_serialize(). if ( $original_value && is_string( $original_value ) && is_json_string( $original_value ) ) { $meta_value = wp_json_encode( $meta_value ); } } $cloned_meta_data[ $meta_key ][] = $meta_value; } } } else { // No per-key user input: copy all original meta. Decode raw stored values // so add_post_meta() can re-serialize them safely below. foreach ( $original_custom_fields as $meta_key => $values ) { if ( ! is_array( $values ) ) { continue; } $cloned_meta_data[ $meta_key ] = array_map( 'maybe_unserialize', $values ); } } foreach ( $cloned_meta_data as $key => $value ) { if ( in_array( $key, $excluded_meta_keys, true ) ) { continue; } if ( ! is_array( $value ) || count( $value ) === 0 ) { continue; } foreach ( $value as $i => $v ) { if ( ! apply_filters( "mtphr_post_duplicator_meta_{$key}_enabled", true ) ) { continue; } $meta_value = apply_filters( "mtphr_post_duplicator_meta_value", $v, $key, $duplicate_id, $duplicate['post_type'] ); // Use add_post_meta() (instead of a direct $wpdb->insert()) so WordPress // core's maybe_serialize() applies double-serialization protection. A // user-supplied raw serialized object string is then stored as an escaped // serialized string and is NOT instantiated as a PHP object when later // read back via get_post_meta(). Mitigates PHP Object Injection (CWE-502). add_post_meta( $duplicate_id, $key, wp_slash( $meta_value ) ); } } } do_action( 'mtphr_post_duplicator_created', $original_id, $duplicate_id, $settings ); return array( 'duplicate_id' => $duplicate_id, 'permalink' => get_permalink( $duplicate_id ), 'duplicate' => $duplicate, 'duplicate_terms' => $duplicate_terms, ); } /** * Duplicate a post (REST API callback) */ function duplicate_post( $request ) { $data = $request->get_json_params(); $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 ) ); } $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 ) ); } $override_settings = $data; unset( $override_settings['original_id'] ); $settings = array_merge( get_option_value(), $override_settings ); $result = perform_duplication( $orig, $settings ); if ( is_wp_error( $result ) ) { return $result; } return rest_ensure_response( array( 'duplicate_id' => $result['duplicate_id'], 'permalink' => $result['permalink'], 'other_data' => array( 'duplicate_post' => $result['duplicate'], 'duplicate_terms' => $result['duplicate_terms'], ), ), 200 ); } /** * Permission check for getting users */ function get_users_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 users with edit capabilities * * Note: This is now only called when the duplicate modal opens (via REST API), * not on every admin page load, so memory usage is acceptable even with full user objects. */ function get_users( $request ) { // Get users with edit_posts capability // Use global namespace \get_users() to avoid conflict with this function name $users = \get_users( [ 'capability__in' => ['edit_posts'], 'orderby' => 'display_name', 'order' => 'ASC', ] ); $user_options = []; foreach ( $users as $user ) { $user_options[] = [ 'value' => (string) $user->ID, 'label' => $user->display_name . ' (' . $user->user_login . ')', ]; } return rest_ensure_response( $user_options ); } /** * 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; }