options = $options; } /** * Check ordinary-clone permission for a specific post. * * @param \WP_User $user User attempting the clone. * @param \WP_Post $post Post to be cloned. * * @return bool */ public function can_clone( $user, $post ): bool { return $this->can_copy( $user, $post, 'clone_posts', 'clone_roles' ); } /** * Create an independent draft copy. * * @param int $source_id Source post ID. * * @return int|\WP_Error Clone post ID, or a WP_Error on failure. */ public function clone_post( int $source_id ) { return $this->copy_post( $source_id, [ 'title_suffix' => ' ' . __( '(Clone)', 'admin-optimizer' ), 'extra_meta' => [], 'filter' => 'adminoptim_clone_post_meta', 'action' => 'adminoptim_after_clone', ] ); } /** * Check a feature capability/role and WordPress post capability. * * @param \WP_User $user User attempting the copy. * @param \WP_Post $post Post being copied. * @param string $capability Capability that grants the feature regardless of role. * @param string $roles_option Options key holding the roles allowed to use the feature. * * @return bool */ protected function can_copy( $user, $post, string $capability, string $roles_option ): bool { if ( ! $user instanceof \WP_User || ! $post instanceof \WP_Post || ! user_can( $user, 'edit_post', $post->ID ) ) { return false; } $roles = isset( $this->options[ $roles_option ] ) ? (array) $this->options[ $roles_option ] : [ 'administrator' ]; $feature_cap = $user->has_cap( $capability ) || (bool) array_intersect( $user->roles, $roles ); if ( ! $feature_cap ) { return false; } return empty( $post->post_password ) || (int) $post->post_author === (int) $user->ID; } /** * Copy a post, all metadata values, and taxonomy relationships. * * @param int $source_id Source post ID. * @param array $profile Copy profile. * @return int|\WP_Error */ protected function copy_post( int $source_id, array $profile ) { $source = get_post( $source_id ); if ( ! $source instanceof \WP_Post ) { return new \WP_Error( 'adminoptim_invalid_source', __( 'The source post does not exist.', 'admin-optimizer' ) ); } $post_data = [ 'post_title' => $source->post_title . ( $profile['title_suffix'] ?? '' ), 'post_content' => $source->post_content, 'post_author' => $source->post_author, 'post_content_filtered' => $source->post_content_filtered, 'post_excerpt' => $source->post_excerpt, 'post_type' => $source->post_type, 'post_parent' => $source->post_parent, 'menu_order' => $source->menu_order, 'comment_status' => $source->comment_status, 'ping_status' => $source->ping_status, 'post_status' => 'draft', ]; $post_data = apply_filters( 'adminoptim_content_refresher_clone_post_data', $post_data, $source, $profile ); $clone_id = wp_insert_post( wp_slash( $post_data ), true ); if ( is_wp_error( $clone_id ) ) { return $clone_id; } $metadata = $this->get_copyable_metadata( $source_id ); $filter = isset( $profile['filter'] ) ? (string) $profile['filter'] : ''; if ( $filter ) { $metadata = apply_filters( $filter, $metadata, $source_id, $clone_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- $filter is always a hardcoded, prefixed literal from the profile array (e.g. 'adminoptim_clone_post_meta'); phpcs can't resolve the indirection. } foreach ( (array) ( $profile['extra_meta'] ?? [] ) as $key => $values ) { $metadata[ $key ] = is_array( $values ) ? $values : [ $values ]; } $this->write_metadata( $clone_id, $metadata ); $terms_result = $this->copy_taxonomies( $clone_id, $source ); if ( is_wp_error( $terms_result ) ) { wp_trash_post( $clone_id ); return $terms_result; } $action = isset( $profile['action'] ) ? (string) $profile['action'] : ''; if ( $action ) { do_action( $action, $clone_id, $source_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- $filter is always a hardcoded, prefixed literal from the profile array (e.g. 'adminoptim_clone_post_meta'); phpcs can't resolve the indirection. } return $clone_id; } /** * Return metadata while excluding refresh control fields. * * @param int $post_id Post ID to read metadata from. * * @return array */ protected function get_copyable_metadata( int $post_id ): array { $metadata = get_post_meta( $post_id ); if ( ! is_array( $metadata ) ) { return []; } foreach ( array_keys( $metadata ) as $key ) { if ( 0 === strpos( (string) $key, '_republish_' ) ) { unset( $metadata[ $key ] ); } } return $metadata; } /** * Write all values for each metadata key. * * @param int $post_id Post ID to write metadata to. * @param array $metadata Metadata keyed by meta key, each value an array of raw meta values. * @param bool $replace Whether to delete existing values for each key before writing. * * @return void */ protected function write_metadata( int $post_id, array $metadata, bool $replace = false ): void { foreach ( $metadata as $key => $values ) { $key = (string) $key; if ( $replace ) { delete_post_meta( $post_id, $key ); } foreach ( (array) $values as $value ) { add_post_meta( $post_id, $key, maybe_unserialize( $value ) ); } } } /** * Copy taxonomy terms by term ID to avoid slug ambiguity. * * @param int $destination_id Post ID to assign terms to. * @param \WP_Post $source Source post to copy terms from. * * @return true|\WP_Error */ protected function copy_taxonomies( int $destination_id, \WP_Post $source ) { foreach ( get_object_taxonomies( $source->post_type ) as $taxonomy ) { $term_ids = wp_get_object_terms( $source->ID, $taxonomy, [ 'fields' => 'ids' ] ); if ( is_wp_error( $term_ids ) ) { return $term_ids; } $result = wp_set_object_terms( $destination_id, $term_ids, $taxonomy, false ); if ( is_wp_error( $result ) ) { return $result; } } return true; } }