Файловый менеджер - Редактировать - /var/www/xthruster/html/wp-content/uploads/flags/src.tar
Назад
ui/metabox.php 0000755 00000005437 14717635165 0007364 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\UI; use WP_Post; use Yoast\WP\Duplicate_Post\Permissions_Helper; use Yoast\WP\Duplicate_Post\Utils; /** * Duplicate Post class to manage the metabox. */ class Metabox { /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Initializes the class. * * @param Permissions_Helper $permissions_helper The permissions helper. */ public function __construct( Permissions_Helper $permissions_helper ) { $this->permissions_helper = $permissions_helper; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { if ( \intval( \get_option( 'duplicate_post_show_original_meta_box' ) ) === 1 ) { \add_action( 'add_meta_boxes', [ $this, 'add_custom_metabox' ], 10, 2 ); } } /** * Adds a metabox to Edit screen. * * @param string $post_type The post type. * @param WP_Post $post The current post object. * * @return void */ public function add_custom_metabox( $post_type, $post ) { $enabled_post_types = $this->permissions_helper->get_enabled_post_types(); if ( \in_array( $post_type, $enabled_post_types, true ) && $post instanceof WP_Post ) { $original_item = Utils::get_original( $post ); if ( $original_item instanceof WP_Post ) { \add_meta_box( 'duplicate_post_show_original', \__( 'Duplicate Post', 'duplicate-post' ), [ $this, 'custom_metabox_html' ], $post_type, 'side', 'default', [ 'original' => $original_item ] ); } } } /** * Outputs the HTML for the metabox. * * @param WP_Post $post The current post. * @param array $metabox The array containing the metabox data. * * @return void */ public function custom_metabox_html( $post, $metabox ) { $original_item = $metabox['args']['original']; if ( ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { ?> <p> <input type="checkbox" name="duplicate_post_remove_original" id="duplicate-post-remove-original" value="duplicate_post_remove_original" aria-describedby="duplicate-post-remove-original-description"> <label for="duplicate-post-remove-original"> <?php \esc_html_e( 'Delete reference to original item.', 'duplicate-post' ); ?> </label> </p> <?php } ?> <p id="duplicate-post-remove-original-description"> <?php \printf( \wp_kses( /* translators: %s: post title */ \__( 'The original item this was copied from is: <span class="duplicate_post_original_item_title_span">%s</span>', 'duplicate-post' ), [ 'span' => [ 'class' => [], ], ] ), Utils::get_edit_or_view_link( $original_item ) // phpcs:ignore WordPress.Security.EscapeOutput ); ?> </p> <?php } } ui/asset-manager.php 0000755 00000010253 14717635165 0010444 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\UI; use Yoast\WP\Duplicate_Post\Utils; /** * Duplicate Post class to manage assets. */ class Asset_Manager { /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { \add_action( 'init', [ $this, 'register_styles' ] ); \add_action( 'init', [ $this, 'register_scripts' ] ); } /** * Registers the styles. * * @return void */ public function register_styles() { \wp_register_style( 'duplicate-post', \plugins_url( '/css/duplicate-post.css', \DUPLICATE_POST_FILE ), [], \DUPLICATE_POST_CURRENT_VERSION ); \wp_register_style( 'duplicate-post-options', \plugins_url( '/css/duplicate-post-options.css', \DUPLICATE_POST_FILE ), [], \DUPLICATE_POST_CURRENT_VERSION ); } /** * Registers the scripts. * * @return void */ public function register_scripts() { $flattened_version = Utils::flatten_version( \DUPLICATE_POST_CURRENT_VERSION ); \wp_register_script( 'duplicate_post_edit_script', \plugins_url( \sprintf( 'js/dist/duplicate-post-edit-%s.js', $flattened_version ), \DUPLICATE_POST_FILE ), [ 'wp-components', 'wp-element', 'wp-i18n', ], \DUPLICATE_POST_CURRENT_VERSION, true ); \wp_register_script( 'duplicate_post_strings', \plugins_url( \sprintf( 'js/dist/duplicate-post-strings-%s.js', $flattened_version ), \DUPLICATE_POST_FILE ), [ 'wp-components', 'wp-element', 'wp-i18n', ], \DUPLICATE_POST_CURRENT_VERSION, true ); \wp_register_script( 'duplicate_post_quick_edit_script', \plugins_url( \sprintf( 'js/dist/duplicate-post-quick-edit-%s.js', $flattened_version ), \DUPLICATE_POST_FILE ), [ 'jquery' ], \DUPLICATE_POST_CURRENT_VERSION, true ); \wp_register_script( 'duplicate_post_options_script', \plugins_url( \sprintf( 'js/dist/duplicate-post-options-%s.js', $flattened_version ), \DUPLICATE_POST_FILE ), [ 'jquery' ], \DUPLICATE_POST_CURRENT_VERSION, true ); } /** * Enqueues the styles. * * @return void */ public function enqueue_styles() { \wp_enqueue_style( 'duplicate-post' ); } /** * Enqueues the styles for the options page. * * @return void */ public function enqueue_options_styles() { \wp_enqueue_style( 'duplicate-post-options' ); } /** * Enqueues the script for the Block editor and passes object via localization. * * @param array $data_object The object to pass to the script. * * @return void */ public function enqueue_edit_script( $data_object = [] ) { $handle = 'duplicate_post_edit_script'; \wp_enqueue_script( $handle ); \wp_add_inline_script( $handle, 'let duplicatePostNotices = {};', 'before' ); \wp_localize_script( $handle, 'duplicatePost', $data_object ); } /** * Enqueues the script for the Javascript strings and passes object via localization. * * @param array $data_object The object to pass to the script. * * @return void */ public function enqueue_strings_script( $data_object = [] ) { $handle = 'duplicate_post_strings'; \wp_enqueue_script( $handle ); \wp_localize_script( $handle, 'duplicatePostStrings', $data_object ); } /** * Enqueues the script for the Quick Edit. * * @return void */ public function enqueue_quick_edit_script() { \wp_enqueue_script( 'duplicate_post_quick_edit_script' ); } /** * Enqueues the script for the Options page. * * @return void */ public function enqueue_options_script() { \wp_enqueue_script( 'duplicate_post_options_script' ); } /** * Enqueues the script for the Elementor plugin. * * @param array $data_object The object to pass to the script. * * @return void */ public function enqueue_elementor_script( $data_object = [] ) { $flattened_version = Utils::flatten_version( \DUPLICATE_POST_CURRENT_VERSION ); $handle = 'duplicate_post_elementor_script'; \wp_register_script( $handle, \plugins_url( \sprintf( 'js/dist/duplicate-post-elementor-%s.js', $flattened_version ), \DUPLICATE_POST_FILE ), [ 'jquery' ], \DUPLICATE_POST_CURRENT_VERSION, true ); \wp_enqueue_script( $handle ); \wp_localize_script( $handle, 'duplicatePost', $data_object ); } } ui/newsletter.php 0000755 00000010146 14717635165 0010112 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\UI; /** * Newsletter class. */ class Newsletter { /** * Renders the newsletter signup form. * * @return string The HTML of the newsletter signup form (escaped). */ public static function newsletter_signup_form() { $newsletter_form_response = self::newsletter_handle_form(); $copy = \sprintf( /* translators: 1: Yoast */ \esc_html__( 'If you want to stay up to date about all the exciting developments around Duplicate Post, subscribe to the %1$s newsletter!', 'duplicate-post' ), 'Yoast' ); $email_label = \esc_html__( 'Email address', 'duplicate-post' ); $copy_privacy_policy = \sprintf( /* translators: %1$s and %2$s are replaced by opening and closing anchor tags. */ \esc_html__( 'Yoast respects your privacy. Read %1$sour privacy policy%2$s on how we handle your personal information.', 'duplicate-post' ), '<a href="https://yoa.st/4jf" target="_blank">', '</a>' ); $response_html = ''; if ( \is_array( $newsletter_form_response ) ) { $response_status = $newsletter_form_response['status']; $response_message = $newsletter_form_response['message']; $response_html = '<div class="newsletter-response-' . $response_status . ' clear" id="newsletter-response" style="margin-top: 6px;">' . $response_message . '</div>'; } $html = ' <!-- Begin Newsletter Signup Form --> <form method="post" id="newsletter-subscribe-form" name="newsletter-subscribe-form" novalidate> ' . \wp_nonce_field( 'newsletter', 'newsletter_nonce' ) . ' <p>' . $copy . '</p> <div class="newsletter-field-group" style="display: flex; flex-direction: column"> <label for="newsletter-email" style="margin: 0 0 4px 0;"><strong>' . $email_label . '</strong></label> <div> <input type="email" value="" name="EMAIL" class="required email" id="newsletter-email" style="margin-right: 4px;"> <input type="submit" value="' . \esc_attr__( 'Subscribe', 'duplicate-post' ) . '" name="subscribe" id="newsletter-subscribe" class="button"> </div> <p style="font-size: 10px;">' . $copy_privacy_policy . '</p> </div> ' . $response_html . ' </form> <!--End Newsletter Signup Form--> '; return $html; } /** * Handles and validates Newsletter form. * * @return array|null */ private static function newsletter_handle_form() { //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Already sanitized. if ( isset( $_POST['newsletter_nonce'] ) && ! \wp_verify_nonce( \wp_unslash( $_POST['newsletter_nonce'] ), 'newsletter' ) ) { return [ 'status' => 'error', 'message' => \esc_html__( 'Something went wrong. Please try again later.', 'duplicate-post' ), ]; } $email = null; if ( isset( $_POST['EMAIL'] ) ) { $email = \sanitize_email( \wp_unslash( $_POST['EMAIL'] ) ); } if ( $email === null ) { return null; } if ( ! \is_email( $email ) ) { return [ 'status' => 'error', 'message' => \esc_html__( 'Please enter valid e-mail address.', 'duplicate-post' ), ]; } return self::newsletter_subscribe_to_mailblue( $email ); } /** * Handles subscription request and provides feedback response. * * @param string $email Subscriber email. * * @return array Feedback response. */ private static function newsletter_subscribe_to_mailblue( $email ) { $response = \wp_remote_post( 'https://my.yoast.com/api/Mailing-list/subscribe', [ 'method' => 'POST', 'body' => [ 'customerDetails' => [ 'email' => $email, 'firstName' => '', ], 'list' => 'Yoast newsletter', ], ] ); $wp_remote_retrieve_response_code = \wp_remote_retrieve_response_code( $response ); if ( $wp_remote_retrieve_response_code <= 200 || $wp_remote_retrieve_response_code >= 300 ) { return [ 'status' => 'error', 'message' => \esc_html__( 'Something went wrong. Please try again later.', 'duplicate-post' ), ]; } return [ 'status' => 'success', 'message' => \esc_html__( 'You have successfully subscribed to the newsletter. Please check your inbox.', 'duplicate-post' ), ]; } } ui/column.php 0000755 00000011413 14717635165 0007211 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\UI; use WP_Post; use Yoast\WP\Duplicate_Post\Permissions_Helper; use Yoast\WP\Duplicate_Post\Utils; /** * Duplicate Post class to manage the custom column + quick edit. */ class Column { /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Holds the asset manager. * * @var Asset_Manager */ protected $asset_manager; /** * Initializes the class. * * @param Permissions_Helper $permissions_helper The permissions helper. * @param Asset_Manager $asset_manager The asset manager. */ public function __construct( Permissions_Helper $permissions_helper, Asset_Manager $asset_manager ) { $this->permissions_helper = $permissions_helper; $this->asset_manager = $asset_manager; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { if ( \intval( \get_option( 'duplicate_post_show_original_column' ) ) === 1 ) { $enabled_post_types = $this->permissions_helper->get_enabled_post_types(); if ( \count( $enabled_post_types ) ) { foreach ( $enabled_post_types as $enabled_post_type ) { \add_filter( "manage_{$enabled_post_type}_posts_columns", [ $this, 'add_original_column' ] ); \add_action( "manage_{$enabled_post_type}_posts_custom_column", [ $this, 'show_original_item' ], 10, 2 ); } \add_action( 'quick_edit_custom_box', [ $this, 'quick_edit_remove_original' ], 10, 2 ); \add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] ); \add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_styles' ] ); } } } /** * Adds Original item column to the post list. * * @param array $post_columns The post columns array. * * @return array The updated array. */ public function add_original_column( $post_columns ) { if ( \is_array( $post_columns ) ) { $post_columns['duplicate_post_original_item'] = \__( 'Original item', 'duplicate-post' ); } return $post_columns; } /** * Sets the text to be displayed in the Original item column for the current post. * * @param string $column_name The name for the current column. * @param int $post_id The ID for the current post. * * @return void */ public function show_original_item( $column_name, $post_id ) { if ( $column_name === 'duplicate_post_original_item' ) { $column_content = '-'; $data_attr = ' data-no-original="1"'; $original_item = Utils::get_original( $post_id ); if ( $original_item ) { $post = \get_post( $post_id ); $data_attr = ''; if ( $post instanceof WP_Post && $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { $data_attr = ' data-copy-is-for-rewrite-and-republish="1"'; } $column_content = Utils::get_edit_or_view_link( $original_item ); } echo \sprintf( '<span class="duplicate_post_original_link"%s>%s</span>', $data_attr, // phpcs:ignore WordPress.Security.EscapeOutput $column_content // phpcs:ignore WordPress.Security.EscapeOutput ); } } /** * Adds original item checkbox + edit link in the Quick Edit. * * @param string $column_name The name for the current column. * * @return void */ public function quick_edit_remove_original( $column_name ) { if ( $column_name !== 'duplicate_post_original_item' ) { return; } \printf( '<fieldset class="inline-edit-col-left" id="duplicate_post_quick_edit_fieldset"> <div class="inline-edit-col"> <input type="checkbox" name="duplicate_post_remove_original" id="duplicate-post-remove-original" value="duplicate_post_remove_original" aria-describedby="duplicate-post-remove-original-description"> <label for="duplicate-post-remove-original"> <span class="checkbox-title">%s</span> </label> <span id="duplicate-post-remove-original-description" class="checkbox-title">%s</span> </div> </fieldset>', \esc_html__( 'Delete reference to original item.', 'duplicate-post' ), \wp_kses( \__( 'The original item this was copied from is: <span class="duplicate_post_original_item_title_span"></span>', 'duplicate-post' ), [ 'span' => [ 'class' => [], ], ] ) ); } /** * Enqueues the Javascript file to inject column data into the Quick Edit. * * @param string $hook The current admin page. * * @return void */ public function admin_enqueue_scripts( $hook ) { if ( $hook === 'edit.php' ) { $this->asset_manager->enqueue_quick_edit_script(); } } /** * Enqueues the CSS file to for the Quick edit * * @param string $hook The current admin page. * * @return void */ public function admin_enqueue_styles( $hook ) { if ( $hook === 'edit.php' ) { $this->asset_manager->enqueue_styles(); } } } ui/link-builder.php 0000755 00000005443 14717635165 0010303 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\UI; use WP_Post; /** * Duplicate Post link builder. */ class Link_Builder { /** * Builds URL for duplication action for the Rewrite & Republish feature. * * @param int|WP_Post $post The post object or ID. * @param string $context The context in which the URL will be used. * * @return string The URL for the link. */ public function build_rewrite_and_republish_link( $post, $context = 'display' ) { return $this->build_link( $post, $context, 'duplicate_post_rewrite' ); } /** * Builds URL for the "Clone" action. * * @param int|WP_Post $post The post object or ID. * @param string $context The context in which the URL will be used. * * @return string The URL for the link. */ public function build_clone_link( $post, $context = 'display' ) { return $this->build_link( $post, $context, 'duplicate_post_clone' ); } /** * Builds URL for the "Copy to a new draft" action. * * @param int|WP_Post $post The post object or ID. * @param string $context The context in which the URL will be used. * * @return string The URL for the link. */ public function build_new_draft_link( $post, $context = 'display' ) { return $this->build_link( $post, $context, 'duplicate_post_new_draft' ); } /** * Builds URL for the "Check Changes" action. * * @param int|WP_Post $post The post object or ID. * @param string $context The context in which the URL will be used. * * @return string The URL for the link. */ public function build_check_link( $post, $context = 'display' ) { return $this->build_link( $post, $context, 'duplicate_post_check_changes' ); } /** * Builds URL for duplication action. * * @param int|WP_Post $post The post object or ID. * @param string $context The context in which the URL will be used. * @param string $action_name The action for the URL. * * @return string The URL for the link. */ public function build_link( $post, $context, $action_name ) { $post = \get_post( $post ); if ( ! $post instanceof WP_Post ) { return ''; } if ( $context === 'display' ) { $action = '?action=' . $action_name . '&post=' . $post->ID; } else { $action = '?action=' . $action_name . '&post=' . $post->ID; } return \wp_nonce_url( /** * Filter on the URL of the clone link * * @param string $url The URL of the clone link. * @param int $ID The ID of the post * @param string $context The context in which the URL is used. * @param string $action_name The action name. * * @return string */ \apply_filters( 'duplicate_post_get_clone_post_link', \admin_url( 'admin.php' . $action ), $post->ID, $context, $action_name ), $action_name . '_' . $post->ID ); } } ui/post-states.php 0000755 00000003716 14717635165 0010211 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\UI; use WP_Post; use Yoast\WP\Duplicate_Post\Permissions_Helper; use Yoast\WP\Duplicate_Post\Utils; /** * Duplicate Post class to manage the post states display. */ class Post_States { /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Initializes the class. * * @param Permissions_Helper $permissions_helper The Permissions helper object. */ public function __construct( Permissions_Helper $permissions_helper ) { $this->permissions_helper = $permissions_helper; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { \add_filter( 'display_post_states', [ $this, 'show_original_in_post_states' ], 10, 2 ); } /** * Shows link to original post in the post states. * * @param array $post_states The array of post states. * @param WP_Post $post The current post. * * @return array The updated post states array. */ public function show_original_in_post_states( $post_states, $post ) { if ( ! $post instanceof WP_Post || ! \is_array( $post_states ) ) { return $post_states; } $original_item = Utils::get_original( $post ); if ( ! $original_item ) { return $post_states; } if ( $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { /* translators: %s: Original item link (to view or edit) or title. */ $post_states['duplicate_post_original_item'] = \sprintf( \esc_html__( 'Rewrite & Republish of %s', 'duplicate-post' ), Utils::get_edit_or_view_link( $original_item ) ); return $post_states; } if ( \intval( \get_option( 'duplicate_post_show_original_in_post_states' ) ) === 1 ) { /* translators: %s: Original item link (to view or edit) or title. */ $post_states['duplicate_post_original_item'] = \sprintf( \__( 'Original: %s', 'duplicate-post' ), Utils::get_edit_or_view_link( $original_item ) ); } return $post_states; } } ui/row-actions.php 0000755 00000011077 14717635165 0010167 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\UI; use WP_Post; use Yoast\WP\Duplicate_Post\Permissions_Helper; use Yoast\WP\Duplicate_Post\Utils; /** * Duplicate Post class to manage the row actions. */ class Row_Actions { /** * Holds the object to create the action link to duplicate. * * @var Link_Builder */ protected $link_builder; /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Initializes the class. * * @param Link_Builder $link_builder The link builder. * @param Permissions_Helper $permissions_helper The permissions helper. */ public function __construct( Link_Builder $link_builder, Permissions_Helper $permissions_helper ) { $this->link_builder = $link_builder; $this->permissions_helper = $permissions_helper; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { if ( \intval( Utils::get_option( 'duplicate_post_show_link_in', 'row' ) ) === 0 ) { return; } if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'clone' ) ) === 1 ) { \add_filter( 'post_row_actions', [ $this, 'add_clone_action_link' ], 10, 2 ); \add_filter( 'page_row_actions', [ $this, 'add_clone_action_link' ], 10, 2 ); } if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'new_draft' ) ) === 1 ) { \add_filter( 'post_row_actions', [ $this, 'add_new_draft_action_link' ], 10, 2 ); \add_filter( 'page_row_actions', [ $this, 'add_new_draft_action_link' ], 10, 2 ); } if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'rewrite_republish' ) ) === 1 ) { \add_filter( 'post_row_actions', [ $this, 'add_rewrite_and_republish_action_link' ], 10, 2 ); \add_filter( 'page_row_actions', [ $this, 'add_rewrite_and_republish_action_link' ], 10, 2 ); } } /** * Hooks in the `post_row_actions` and `page_row_actions` filters to add a 'Clone' link. * * @param array $actions The array of actions from the filter. * @param WP_Post $post The post object. * * @return array The updated array of actions. */ public function add_clone_action_link( $actions, $post ) { if ( ! $post instanceof WP_Post || ! $this->permissions_helper->should_links_be_displayed( $post ) || ! \is_array( $actions ) ) { return $actions; } $title = \_draft_or_post_title( $post ); $actions['clone'] = '<a href="' . $this->link_builder->build_clone_link( $post->ID ) . '" aria-label="' . \esc_attr( /* translators: %s: Post title. */ \sprintf( \__( 'Clone “%s”', 'duplicate-post' ), $title ) ) . '">' . \esc_html_x( 'Clone', 'verb', 'duplicate-post' ) . '</a>'; return $actions; } /** * Hooks in the `post_row_actions` and `page_row_actions` filters to add a 'New Draft' link. * * @param array $actions The array of actions from the filter. * @param WP_Post $post The post object. * * @return array The updated array of actions. */ public function add_new_draft_action_link( $actions, $post ) { if ( ! $post instanceof WP_Post || ! $this->permissions_helper->should_links_be_displayed( $post ) || ! \is_array( $actions ) ) { return $actions; } $title = \_draft_or_post_title( $post ); $actions['edit_as_new_draft'] = '<a href="' . $this->link_builder->build_new_draft_link( $post->ID ) . '" aria-label="' . \esc_attr( /* translators: %s: Post title. */ \sprintf( \__( 'New draft of “%s”', 'duplicate-post' ), $title ) ) . '">' . \esc_html__( 'New Draft', 'duplicate-post' ) . '</a>'; return $actions; } /** * Hooks in the `post_row_actions` and `page_row_actions` filters to add a 'Rewrite & Republish' link. * * @param array $actions The array of actions from the filter. * @param WP_Post $post The post object. * * @return array The updated array of actions. */ public function add_rewrite_and_republish_action_link( $actions, $post ) { if ( ! $post instanceof WP_Post || ! $this->permissions_helper->should_rewrite_and_republish_be_allowed( $post ) || ! $this->permissions_helper->should_links_be_displayed( $post ) || ! \is_array( $actions ) ) { return $actions; } $title = \_draft_or_post_title( $post ); $actions['rewrite'] = '<a href="' . $this->link_builder->build_rewrite_and_republish_link( $post->ID ) . '" aria-label="' . \esc_attr( /* translators: %s: Post title. */ \sprintf( \__( 'Rewrite & Republish “%s”', 'duplicate-post' ), $title ) ) . '">' . \esc_html_x( 'Rewrite & Republish', 'verb', 'duplicate-post' ) . '</a>'; return $actions; } } ui/user-interface.php 0000755 00000005542 14717635165 0010636 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\UI; use Yoast\WP\Duplicate_Post\Permissions_Helper; /** * Duplicate Post user interface. */ class User_Interface { /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Holds the object to manage the row actions for the post. * * @var Row_Actions */ protected $row_actions; /** * Holds the object to manage the classic editor UI. * * @var Classic_Editor */ protected $classic_editor; /** * Holds the object to manage the block editor UI. * * @var Block_Editor */ protected $block_editor; /** * Holds the object to manage the admin bar links. * * @var Admin_Bar */ protected $admin_bar; /** * Holds the object to manage the bulk actions dropdown. * * @var Bulk_Actions */ protected $bulk_actions; /** * Post states object. * * @var Post_States */ protected $post_states; /** * Metabox object. * * @var Metabox */ protected $metabox; /** * Newsletter object. * * @var Newsletter */ protected $newsletter; /** * Column object. * * @var Column */ protected $column; /** * Holds the object to create the action link to duplicate. * * @var Link_Builder */ protected $link_builder; /** * Holds the object to create the action link to duplicate. * * @var Asset_Manager */ protected $asset_manager; /** * Initializes the class. * * @param Permissions_Helper $permissions_helper The permissions helper object. */ public function __construct( Permissions_Helper $permissions_helper ) { $this->permissions_helper = $permissions_helper; $this->link_builder = new Link_Builder(); $this->asset_manager = new Asset_Manager(); $this->asset_manager->register_hooks(); $this->admin_bar = new Admin_Bar( $this->link_builder, $this->permissions_helper, $this->asset_manager ); $this->block_editor = new Block_Editor( $this->link_builder, $this->permissions_helper, $this->asset_manager ); $this->bulk_actions = new Bulk_Actions( $this->permissions_helper ); $this->column = new Column( $this->permissions_helper, $this->asset_manager ); $this->metabox = new Metabox( $this->permissions_helper ); $this->newsletter = new Newsletter(); $this->post_states = new Post_States( $this->permissions_helper ); $this->classic_editor = new Classic_Editor( $this->link_builder, $this->permissions_helper, $this->asset_manager ); $this->row_actions = new Row_Actions( $this->link_builder, $this->permissions_helper ); $this->admin_bar->register_hooks(); $this->block_editor->register_hooks(); $this->bulk_actions->register_hooks(); $this->column->register_hooks(); $this->metabox->register_hooks(); $this->post_states->register_hooks(); $this->classic_editor->register_hooks(); $this->row_actions->register_hooks(); } } ui/admin-bar.php 0000755 00000011315 14717635165 0007547 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\UI; use WP_Post; use Yoast\WP\Duplicate_Post\Permissions_Helper; use Yoast\WP\Duplicate_Post\Utils; /** * Duplicate Post class to manage the admin bar. */ class Admin_Bar { /** * Holds the object to create the action link to duplicate. * * @var Link_Builder */ protected $link_builder; /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Holds the asset manager. * * @var Asset_Manager */ protected $asset_manager; /** * Initializes the class. * * @param Link_Builder $link_builder The link builder. * @param Permissions_Helper $permissions_helper The permissions helper. * @param Asset_Manager $asset_manager The asset manager. */ public function __construct( Link_Builder $link_builder, Permissions_Helper $permissions_helper, Asset_Manager $asset_manager ) { $this->link_builder = $link_builder; $this->permissions_helper = $permissions_helper; $this->asset_manager = $asset_manager; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { if ( \intval( Utils::get_option( 'duplicate_post_show_link_in', 'adminbar' ) ) === 1 ) { \add_action( 'wp_before_admin_bar_render', [ $this, 'admin_bar_render' ] ); \add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_styles' ] ); \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_styles' ] ); } } /** * Shows Rewrite & Republish link in the Toolbar. * * @global \WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance. * * @return void */ public function admin_bar_render() { global $wp_admin_bar; if ( ! \is_admin_bar_showing() ) { return; } $post = $this->get_current_post(); if ( ! $post ) { return; } $show_new_draft = ( \intval( Utils::get_option( 'duplicate_post_show_link', 'new_draft' ) ) === 1 ); $show_rewrite_and_republish = ( \intval( Utils::get_option( 'duplicate_post_show_link', 'rewrite_republish' ) ) === 1 ) && $this->permissions_helper->should_rewrite_and_republish_be_allowed( $post ); if ( $show_new_draft && $show_rewrite_and_republish ) { $wp_admin_bar->add_menu( [ 'id' => 'duplicate-post', 'title' => '<span class="ab-icon"></span><span class="ab-label">' . \__( 'Duplicate Post', 'duplicate-post' ) . '</span>', 'href' => $this->link_builder->build_new_draft_link( $post ), ] ); $wp_admin_bar->add_menu( [ 'id' => 'new-draft', 'parent' => 'duplicate-post', 'title' => \__( 'Copy to a new draft', 'duplicate-post' ), 'href' => $this->link_builder->build_new_draft_link( $post ), ] ); $wp_admin_bar->add_menu( [ 'id' => 'rewrite-republish', 'parent' => 'duplicate-post', 'title' => \__( 'Rewrite & Republish', 'duplicate-post' ), 'href' => $this->link_builder->build_rewrite_and_republish_link( $post ), ] ); } else { if ( $show_new_draft ) { $wp_admin_bar->add_menu( [ 'id' => 'new-draft', 'title' => '<span class="ab-icon"></span><span class="ab-label">' . \__( 'Copy to a new draft', 'duplicate-post' ) . '</span>', 'href' => $this->link_builder->build_new_draft_link( $post ), ] ); } if ( $show_rewrite_and_republish ) { $wp_admin_bar->add_menu( [ 'id' => 'rewrite-republish', 'title' => '<span class="ab-icon"></span><span class="ab-label">' . \__( 'Rewrite & Republish', 'duplicate-post' ) . '</span>', 'href' => $this->link_builder->build_rewrite_and_republish_link( $post ), ] ); } } } /** * Links stylesheet for Toolbar link. * * @global \WP_Query $wp_the_query. * * @return void */ public function enqueue_styles() { if ( ! \is_admin_bar_showing() ) { return; } $post = $this->get_current_post(); if ( ! $post ) { return; } $this->asset_manager->enqueue_styles(); } /** * Returns the current post object (both if it's displayed or being edited). * * @global \WP_Query $wp_the_query * * @return false|WP_Post The Post object, false if we are not on a post. */ public function get_current_post() { global $wp_the_query; if ( \is_admin() ) { $post = \get_post(); } else { $post = $wp_the_query->get_queried_object(); } if ( empty( $post ) || ! $post instanceof WP_Post ) { return false; } if ( ( ! $this->permissions_helper->is_edit_post_screen() && ! \is_singular( $post->post_type ) ) || ! $this->permissions_helper->post_type_has_admin_bar( $post->post_type ) ) { return false; } if ( ! $this->permissions_helper->should_links_be_displayed( $post ) ) { return false; } return $post; } } ui/block-editor.php 0000755 00000017567 14717635165 0010312 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\UI; use WP_Post; use Yoast\WP\Duplicate_Post\Permissions_Helper; use Yoast\WP\Duplicate_Post\Utils; /** * Duplicate Post class to manage the block editor UI. */ class Block_Editor { /** * Holds the object to create the action link to duplicate. * * @var Link_Builder */ protected $link_builder; /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Holds the asset manager. * * @var Asset_Manager */ protected $asset_manager; /** * Initializes the class. * * @param Link_Builder $link_builder The link builder. * @param Permissions_Helper $permissions_helper The permissions helper. * @param Asset_Manager $asset_manager The asset manager. */ public function __construct( Link_Builder $link_builder, Permissions_Helper $permissions_helper, Asset_Manager $asset_manager ) { $this->link_builder = $link_builder; $this->permissions_helper = $permissions_helper; $this->asset_manager = $asset_manager; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { \add_action( 'elementor/editor/after_enqueue_styles', [ $this, 'hide_elementor_post_status' ] ); \add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue_elementor_script' ], 9 ); \add_action( 'admin_enqueue_scripts', [ $this, 'should_previously_used_keyword_assessment_run' ], 9 ); \add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_block_editor_scripts' ] ); \add_filter( 'wpseo_link_suggestions_indexables', [ $this, 'remove_original_from_wpseo_link_suggestions' ], 10, 3 ); } /** * Enqueues the necessary Elementor script for the current post. * * @return void */ public function enqueue_elementor_script() { $post = \get_post(); if ( ! $post instanceof WP_Post ) { return; } $edit_js_object = $this->generate_js_object( $post ); $this->asset_manager->enqueue_elementor_script( $edit_js_object ); } /** * Hides the post status control if we're working on a Rewrite and Republish post. * * @return void */ public function hide_elementor_post_status() { $post = \get_post(); if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { return; } \wp_add_inline_style( 'elementor-editor', '.elementor-control-post_status { display: none !important; }' ); } /** * Disables the Yoast SEO PreviouslyUsedKeyword assessment for Rewrite & Republish original and duplicate posts. * * @return void */ public function should_previously_used_keyword_assessment_run() { if ( $this->permissions_helper->is_edit_post_screen() || $this->permissions_helper->is_new_post_screen() ) { $post = \get_post(); if ( $post instanceof WP_Post && ( $this->permissions_helper->is_rewrite_and_republish_copy( $post ) || $this->permissions_helper->has_rewrite_and_republish_copy( $post ) ) ) { \add_filter( 'wpseo_previously_used_keyword_active', '__return_false' ); } } } /** * Enqueues the necessary JavaScript code for the block editor. * * @return void */ public function enqueue_block_editor_scripts() { if ( ! $this->permissions_helper->is_edit_post_screen() && ! $this->permissions_helper->is_new_post_screen() ) { return; } $post = \get_post(); if ( ! $post instanceof WP_Post ) { return; } $edit_js_object = $this->generate_js_object( $post ); $this->asset_manager->enqueue_edit_script( $edit_js_object ); if ( $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { $string_js_object = [ 'checkLink' => $this->get_check_permalink(), ]; $this->asset_manager->enqueue_strings_script( $string_js_object ); } } /** * Generates a New Draft permalink for the current post. * * @return string The permalink. Returns empty if the post can't be copied. */ public function get_new_draft_permalink() { $post = \get_post(); if ( ! $post instanceof WP_Post || ! $this->permissions_helper->should_links_be_displayed( $post ) ) { return ''; } return $this->link_builder->build_new_draft_link( $post ); } /** * Generates a Rewrite & Republish permalink for the current post. * * @return string The permalink. Returns empty if the post cannot be copied for Rewrite & Republish. */ public function get_rewrite_republish_permalink() { $post = \get_post(); if ( ! $post instanceof WP_Post || $this->permissions_helper->is_rewrite_and_republish_copy( $post ) || $this->permissions_helper->has_rewrite_and_republish_copy( $post ) || ! $this->permissions_helper->should_links_be_displayed( $post ) ) { return ''; } return $this->link_builder->build_rewrite_and_republish_link( $post ); } /** * Generates a Check Changes permalink for the current post, if it's intended for Rewrite & Republish. * * @return string The permalink. Returns empty if the post does not exist or it's not a Rewrite & Republish copy. */ public function get_check_permalink() { $post = \get_post(); if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { return ''; } return $this->link_builder->build_check_link( $post ); } /** * Generates a URL to the original post edit screen. * * @return string The URL. Empty if the copy post doesn't have an original. */ public function get_original_post_edit_url() { $post = \get_post(); if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { return ''; } $original_post_id = Utils::get_original_post_id( $post->ID ); if ( ! $original_post_id ) { return ''; } return \add_query_arg( [ 'dprepublished' => 1, 'dpcopy' => $post->ID, 'dpnonce' => \wp_create_nonce( 'dp-republish' ), ], \admin_url( 'post.php?action=edit&post=' . $original_post_id ) ); } /** * Generates an array of data to be passed as a localization object to JavaScript. * * @param WP_Post $post The current post object. * * @return array The data to pass to JavaScript. */ protected function generate_js_object( WP_Post $post ) { $is_rewrite_and_republish_copy = $this->permissions_helper->is_rewrite_and_republish_copy( $post ); return [ 'newDraftLink' => $this->get_new_draft_permalink(), 'rewriteAndRepublishLink' => $this->get_rewrite_republish_permalink(), 'showLinks' => Utils::get_option( 'duplicate_post_show_link' ), 'showLinksIn' => Utils::get_option( 'duplicate_post_show_link_in' ), 'rewriting' => ( $is_rewrite_and_republish_copy ) ? 1 : 0, 'originalEditURL' => $this->get_original_post_edit_url(), ]; } /** * Filters the Yoast SEO Premium link suggestions. * * Removes the original post from the Yoast SEO Premium link suggestions * displayed on the Rewrite & Republish copy. * * @param array $suggestions An array of suggestion indexables that can be filtered. * @param int $object_id The object id for the current indexable. * @param string $object_type The object type for the current indexable. * * @return array The filtered array of suggestion indexables. */ public function remove_original_from_wpseo_link_suggestions( $suggestions, $object_id, $object_type ) { if ( $object_type !== 'post' ) { return $suggestions; } // WordPress get_post already checks if the passed ID is valid and returns null if it's not. $post = \get_post( $object_id ); if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { return $suggestions; } $original_post_id = Utils::get_original_post_id( $post->ID ); return \array_filter( $suggestions, static function ( $suggestion ) use ( $original_post_id ) { return $suggestion->object_id !== $original_post_id; } ); } } ui/bulk-actions.php 0000755 00000004251 14717635165 0010311 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\UI; use Yoast\WP\Duplicate_Post\Permissions_Helper; use Yoast\WP\Duplicate_Post\Utils; /** * Duplicate Post class to manage the bulk actions menu. */ class Bulk_Actions { /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Initializes the class. * * @param Permissions_Helper $permissions_helper The permissions helper. */ public function __construct( Permissions_Helper $permissions_helper ) { $this->permissions_helper = $permissions_helper; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { if ( \intval( Utils::get_option( 'duplicate_post_show_link_in', 'bulkactions' ) ) === 0 ) { return; } \add_action( 'admin_init', [ $this, 'add_bulk_filters' ] ); } /** * Hooks the function to add the Rewrite & Republish option in the bulk actions for the selected post types. * * @return void */ public function add_bulk_filters() { if ( ! $this->permissions_helper->is_current_user_allowed_to_copy() ) { return; } $duplicate_post_types_enabled = $this->permissions_helper->get_enabled_post_types(); foreach ( $duplicate_post_types_enabled as $duplicate_post_type_enabled ) { \add_filter( "bulk_actions-edit-{$duplicate_post_type_enabled}", [ $this, 'register_bulk_action' ] ); } } /** * Adds 'Rewrite & Republish' to the bulk action dropdown. * * @param array $bulk_actions The bulk actions array. * * @return array The bulk actions array. */ public function register_bulk_action( $bulk_actions ) { $is_draft_or_trash = isset( $_REQUEST['post_status'] ) && \in_array( $_REQUEST['post_status'], [ 'draft', 'trash' ], true ); if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'clone' ) ) === 1 ) { $bulk_actions['duplicate_post_bulk_clone'] = \esc_html__( 'Clone', 'duplicate-post' ); } if ( ! $is_draft_or_trash && \intval( Utils::get_option( 'duplicate_post_show_link', 'rewrite_republish' ) ) === 1 ) { $bulk_actions['duplicate_post_bulk_rewrite_republish'] = \esc_html__( 'Rewrite & Republish', 'duplicate-post' ); } return $bulk_actions; } } ui/classic-editor.php 0000755 00000026401 14717635165 0010624 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\UI; use WP_Post; use Yoast\WP\Duplicate_Post\Permissions_Helper; use Yoast\WP\Duplicate_Post\Utils; /** * Duplicate Post class to manage the classic editor UI. */ class Classic_Editor { /** * Holds the object to create the action link to duplicate. * * @var Link_Builder */ protected $link_builder; /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Holds the asset manager. * * @var Asset_Manager */ protected $asset_manager; /** * Initializes the class. * * @param Link_Builder $link_builder The link builder. * @param Permissions_Helper $permissions_helper The permissions helper. * @param Asset_Manager $asset_manager The asset manager. */ public function __construct( Link_Builder $link_builder, Permissions_Helper $permissions_helper, Asset_Manager $asset_manager ) { $this->link_builder = $link_builder; $this->permissions_helper = $permissions_helper; $this->asset_manager = $asset_manager; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { \add_action( 'post_submitbox_misc_actions', [ $this, 'add_check_changes_link' ], 90 ); if ( \intval( Utils::get_option( 'duplicate_post_show_link_in', 'submitbox' ) ) === 1 ) { if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'new_draft' ) ) === 1 ) { \add_action( 'post_submitbox_start', [ $this, 'add_new_draft_post_button' ] ); } if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'rewrite_republish' ) ) === 1 ) { \add_action( 'post_submitbox_start', [ $this, 'add_rewrite_and_republish_post_button' ] ); } } \add_action( 'load-post.php', [ $this, 'hook_translations' ] ); \add_filter( 'post_updated_messages', [ $this, 'change_scheduled_notice_classic_editor' ] ); \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_classic_editor_scripts' ] ); if ( \intval( Utils::get_option( 'duplicate_post_show_link_in', 'submitbox' ) ) === 1 ) { if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'new_draft' ) ) === 1 || \intval( Utils::get_option( 'duplicate_post_show_link', 'rewrite_republish' ) ) === 1 ) { \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_classic_editor_styles' ] ); } } // Remove slug editing from Classic Editor. \add_action( 'add_meta_boxes', [ $this, 'remove_slug_meta_box' ], 10, 2 ); \add_filter( 'get_sample_permalink_html', [ $this, 'remove_sample_permalink_slug_editor' ], 10, 5 ); } /** * Hooks the functions to change the translations. * * @return void */ public function hook_translations() { \add_filter( 'gettext', [ $this, 'change_republish_strings_classic_editor' ], 10, 3 ); \add_filter( 'gettext_with_context', [ $this, 'change_schedule_strings_classic_editor' ], 10, 4 ); } /** * Enqueues the necessary JavaScript code for the Classic editor. * * @return void */ public function enqueue_classic_editor_scripts() { if ( $this->permissions_helper->is_classic_editor() && isset( $_GET['post'] ) ) { $id = \intval( \wp_unslash( $_GET['post'] ) ); $post = \get_post( $id ); if ( ! \is_null( $post ) && $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { $this->asset_manager->enqueue_strings_script(); } } } /** * Enqueues the necessary styles for the Classic editor. * * @return void */ public function enqueue_classic_editor_styles() { if ( $this->permissions_helper->is_classic_editor() && isset( $_GET['post'] ) ) { $id = \intval( \wp_unslash( $_GET['post'] ) ); $post = \get_post( $id ); if ( ! \is_null( $post ) && $this->permissions_helper->should_links_be_displayed( $post ) ) { $this->asset_manager->enqueue_styles(); } } } /** * Adds a button in the post/page edit screen to create a clone * * @param WP_Post|null $post The post object that's being edited. * * @return void */ public function add_new_draft_post_button( $post = null ) { if ( \is_null( $post ) ) { if ( isset( $_GET['post'] ) ) { $id = \intval( \wp_unslash( $_GET['post'] ) ); $post = \get_post( $id ); } } if ( $post instanceof WP_Post && $this->permissions_helper->should_links_be_displayed( $post ) ) { ?> <div id="duplicate-action"> <a class="submitduplicate duplication" href="<?php echo \esc_url( $this->link_builder->build_new_draft_link( $post ) ); ?>"><?php \esc_html_e( 'Copy to a new draft', 'duplicate-post' ); ?> </a> </div> <?php } } /** * Adds a button in the post/page edit screen to create a clone for Rewrite & Republish. * * @param WP_Post|null $post The post object that's being edited. * * @return void */ public function add_rewrite_and_republish_post_button( $post = null ) { if ( \is_null( $post ) ) { if ( isset( $_GET['post'] ) ) { $id = \intval( \wp_unslash( $_GET['post'] ) ); $post = \get_post( $id ); } } if ( $post instanceof WP_Post && $this->permissions_helper->should_rewrite_and_republish_be_allowed( $post ) && $this->permissions_helper->should_links_be_displayed( $post ) ) { ?> <div id="rewrite-republish-action"> <a class="submitduplicate duplication" href="<?php echo \esc_url( $this->link_builder->build_rewrite_and_republish_link( $post ) ); ?>"><?php \esc_html_e( 'Rewrite & Republish', 'duplicate-post' ); ?> </a> </div> <?php } } /** * Adds a message in the post/page edit screen to create a clone for Rewrite & Republish. * * @param WP_Post|null $post The post object that's being edited. * * @return void */ public function add_check_changes_link( $post = null ) { if ( \is_null( $post ) ) { if ( isset( $_GET['post'] ) ) { $id = \intval( \wp_unslash( $_GET['post'] ) ); $post = \get_post( $id ); } } if ( $post instanceof WP_Post && $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { ?> <div id="check-changes-action" class="misc-pub-section"> <?php \esc_html_e( 'Do you want to compare your changes with the original version before merging? Please save any changes first.', 'duplicate-post' ); ?> <br><br> <a class='button' href=<?php echo \esc_url( $this->link_builder->build_check_link( $post ) ); ?>> <?php \esc_html_e( 'Compare', 'duplicate-post' ); ?> </a> </div> <?php } } /** * Changes the 'Publish' copies in the submitbox to 'Republish' if a post is intended for republishing. * * @param string $translation The translated text. * @param string $text The text to translate. * @param string $domain The translation domain. * * @return string The to-be-used copy of the text. */ public function change_republish_strings_classic_editor( $translation, $text, $domain ) { if ( $domain !== 'default' ) { return $translation; } if ( $text === 'Publish' && $this->should_change_rewrite_republish_copy( \get_post() ) ) { return \__( 'Republish', 'duplicate-post' ); } elseif ( $text === 'Publish on: %s' && $this->should_change_rewrite_republish_copy( \get_post() ) ) { /* translators: %s: Date on which the post is to be republished. */ return \__( 'Republish on: %s', 'duplicate-post' ); } return $translation; } /** * Changes the 'Schedule' copy in the submitbox to 'Schedule republish' if a post is intended for republishing. * * @param string $translation The translated text. * @param string $text The text to translate. * @param string $context The translation context. * @param string $domain The translation domain. * * @return string The to-be-used copy of the text. */ public function change_schedule_strings_classic_editor( $translation, $text, $context, $domain ) { if ( $domain !== 'default' || $context !== 'post action/button label' ) { return $translation; } if ( $text === 'Schedule' && $this->should_change_rewrite_republish_copy( \get_post() ) ) { return \__( 'Schedule republish', 'duplicate-post' ); } return $translation; } /** * Changes the post-scheduled notice when a post or page intended for republishing is scheduled. * * @param array[] $messages Post updated messaged. * * @return array[] The to-be-used messages. */ public function change_scheduled_notice_classic_editor( $messages ) { $post = \get_post(); if ( ! $this->should_change_rewrite_republish_copy( $post ) ) { return $messages; } $permalink = \get_permalink( $post->ID ); $scheduled_date = \get_the_time( \get_option( 'date_format' ), $post ); $scheduled_time = \get_the_time( \get_option( 'time_format' ), $post ); if ( $post->post_type === 'post' ) { $messages['post'][9] = \sprintf( /* translators: 1: The post title with a link to the frontend page, 2: The scheduled date and time. */ \esc_html__( 'This rewritten post %1$s is now scheduled to replace the original post. It will be published on %2$s.', 'duplicate-post' ), '<a href="' . $permalink . '">' . $post->post_title . '</a>', '<strong>' . $scheduled_date . ' ' . $scheduled_time . '</strong>' ); return $messages; } if ( $post->post_type === 'page' ) { $messages['page'][9] = \sprintf( /* translators: 1: The page title with a link to the frontend page, 2: The scheduled date and time. */ \esc_html__( 'This rewritten page %1$s is now scheduled to replace the original page. It will be published on %2$s.', 'duplicate-post' ), '<a href="' . $permalink . '">' . $post->post_title . '</a>', '<strong>' . $scheduled_date . ' ' . $scheduled_time . '</strong>' ); } return $messages; } /** * Determines if the Rewrite & Republish copies for the post should be used. * * @param WP_Post $post The current post object. * * @return bool True if the Rewrite & Republish copies should be used. */ public function should_change_rewrite_republish_copy( $post ) { global $pagenow; if ( ! \in_array( $pagenow, [ 'post.php', 'post-new.php' ], true ) ) { return false; } if ( ! $post instanceof WP_Post ) { return false; } return $this->permissions_helper->is_rewrite_and_republish_copy( $post ); } /** * Removes the slug meta box in the Classic Editor when the post is a Rewrite & Republish copy. * * @param string $post_type Post type. * @param WP_Post $post Post object. * * @return void */ public function remove_slug_meta_box( $post_type, $post ) { if ( $post instanceof WP_Post && $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { \remove_meta_box( 'slugdiv', $post_type, 'normal' ); } } /** * Removes the sample permalink slug editor in the Classic Editor when the post is a Rewrite & Republish copy. * * @param string $html Sample permalink HTML markup. * @param int $post_id Post ID. * @param string $new_title New sample permalink title. * @param string $new_slug New sample permalink slug. * @param WP_Post $post Post object. * * @return string The filtered HTML of the sample permalink slug editor. */ public function remove_sample_permalink_slug_editor( $html, $post_id, $new_title, $new_slug, $post ) { if ( ! $post instanceof WP_Post ) { return $html; } if ( $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { return ''; } return $html; } } post-duplicator.php 0000755 00000030044 14717635165 0010431 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post; use WP_Error; use WP_Post; /** * Duplicate Post class to create copies. * * @since 4.0 */ class Post_Duplicator { /** * Returns an array with the default option values. * * @return array The default options values. */ public function get_default_options() { return [ 'copy_title' => true, 'copy_date' => false, 'copy_status' => false, 'copy_name' => false, 'copy_excerpt' => true, 'copy_content' => true, 'copy_thumbnail' => true, 'copy_template' => true, 'copy_format' => true, 'copy_author' => false, 'copy_password' => false, 'copy_attachments' => false, 'copy_children' => false, 'copy_comments' => false, 'copy_menu_order' => true, 'title_prefix' => '', 'title_suffix' => '', 'increase_menu_order_by' => null, 'parent_id' => null, 'meta_excludelist' => [], 'taxonomies_excludelist' => [], 'use_filters' => true, ]; } /** * Creates a copy of a post object, accordingly to an options array. * * @param WP_Post $post The original post object. * @param array $options The options overriding the default ones. * * @return int|WP_Error The copy ID, or a WP_Error object on failure. */ public function create_duplicate( WP_Post $post, array $options = [] ) { $defaults = $this->get_default_options(); $options = \wp_parse_args( $options, $defaults ); $title = ''; $new_post_status = $post->post_status; if ( $post->post_type !== 'attachment' ) { $title = $this->generate_copy_title( $post, $options ); $new_post_status = $this->generate_copy_status( $post, $options ); } $new_post_author_id = $this->generate_copy_author( $post, $options ); $menu_order = 0; if ( $options['copy_menu_order'] ) { $menu_order = $post->menu_order; } if ( ! empty( $options['increase_menu_order_by'] ) && \is_numeric( $options['increase_menu_order_by'] ) ) { $menu_order += \intval( $options['increase_menu_order_by'] ); } $new_post = [ 'post_author' => $new_post_author_id, 'post_content' => ( $options['copy_content'] ) ? $post->post_content : '', 'post_content_filtered' => ( $options['copy_content'] ) ? $post->post_content_filtered : '', 'post_title' => $title, 'post_excerpt' => ( $options['copy_excerpt'] ) ? $post->post_excerpt : '', 'post_status' => $new_post_status, 'post_type' => $post->post_type, 'comment_status' => $post->comment_status, 'ping_status' => $post->ping_status, 'post_password' => ( $options['copy_password'] ) ? $post->post_password : '', 'post_name' => ( $options['copy_name'] ) ? $post->post_name : '', 'post_parent' => empty( $options['parent_id'] ) ? $post->post_parent : $options['parent_id'], 'menu_order' => $menu_order, 'post_mime_type' => $post->post_mime_type, ]; if ( $options['copy_date'] ) { $new_post_date = $post->post_date; $new_post['post_date'] = $new_post_date; $new_post['post_date_gmt'] = \get_gmt_from_date( $new_post_date ); \add_filter( 'wp_insert_post_data', [ $this, 'set_modified' ], 1, 1 ); } if ( $options['use_filters'] ) { /** * Filter new post values. * * @param array $new_post New post values. * @param WP_Post $post Original post object. * * @return array */ $new_post = \apply_filters( 'duplicate_post_new_post', $new_post, $post ); } $new_post_id = \wp_insert_post( \wp_slash( $new_post ), true ); if ( $options['copy_date'] ) { \remove_filter( 'wp_insert_post_data', [ $this, 'set_modified' ], 1 ); } if ( ! \is_wp_error( $new_post_id ) ) { \delete_post_meta( $new_post_id, '_dp_original' ); \add_post_meta( $new_post_id, '_dp_original', $post->ID ); } return $new_post_id; } /** * Modifies the post data to set the modified date to now. * * This is needed for the Block editor when a post is copied with its date, * so that the current publish date is shown instead of "Immediately". * * @param array $data The array of post data. * * @return array The updated array of post data. */ public function set_modified( $data ) { $data['post_modified'] = \current_time( 'mysql' ); $data['post_modified_gmt'] = \current_time( 'mysql', 1 ); return $data; } /** * Wraps the function to create a copy for the Rewrite & Republish feature. * * @param WP_Post $post The original post object. * * @return int|WP_Error The copy ID, or a WP_Error object on failure. */ public function create_duplicate_for_rewrite_and_republish( WP_Post $post ) { $options = [ 'copy_title' => true, 'copy_date' => true, 'copy_name' => false, 'copy_content' => true, 'copy_excerpt' => true, 'copy_author' => true, 'copy_menu_order' => true, 'use_filters' => false, ]; $defaults = $this->get_default_options(); $options = \wp_parse_args( $options, $defaults ); $new_post_id = $this->create_duplicate( $post, $options ); if ( ! \is_wp_error( $new_post_id ) ) { $this->copy_post_taxonomies( $new_post_id, $post, $options ); $this->copy_post_meta_info( $new_post_id, $post, $options ); \update_post_meta( $new_post_id, '_dp_is_rewrite_republish_copy', 1 ); \update_post_meta( $post->ID, '_dp_has_rewrite_republish_copy', $new_post_id ); \update_post_meta( $new_post_id, '_dp_creation_date_gmt', \current_time( 'mysql', 1 ) ); } return $new_post_id; } /** * Copies the taxonomies of a post to another post. * * @param int $new_id New post ID. * @param WP_Post $post The original post object. * @param array $options The options array. * * @return void */ public function copy_post_taxonomies( $new_id, $post, $options ) { // Clear default category (added by wp_insert_post). \wp_set_object_terms( $new_id, null, 'category' ); $post_taxonomies = \get_object_taxonomies( $post->post_type ); // Several plugins just add support to post-formats but don't register post_format taxonomy. if ( \post_type_supports( $post->post_type, 'post-formats' ) && ! \in_array( 'post_format', $post_taxonomies, true ) ) { $post_taxonomies[] = 'post_format'; } $taxonomies_excludelist = $options['taxonomies_excludelist']; if ( ! \is_array( $taxonomies_excludelist ) ) { $taxonomies_excludelist = []; } if ( ! $options['copy_format'] ) { $taxonomies_excludelist[] = 'post_format'; } if ( $options['use_filters'] ) { /** * Filters the taxonomy excludelist when copying a post. * * @param array $taxonomies_excludelist The taxonomy excludelist from the options. * * @return array */ $taxonomies_excludelist = \apply_filters( 'duplicate_post_taxonomies_excludelist_filter', $taxonomies_excludelist ); } $post_taxonomies = \array_diff( $post_taxonomies, $taxonomies_excludelist ); foreach ( $post_taxonomies as $taxonomy ) { $post_terms = \wp_get_object_terms( $post->ID, $taxonomy, [ 'orderby' => 'term_order' ] ); $terms = []; $num_terms = \count( $post_terms ); for ( $i = 0; $i < $num_terms; $i++ ) { $terms[] = $post_terms[ $i ]->slug; } \wp_set_object_terms( $new_id, $terms, $taxonomy ); } } /** * Copies the meta information of a post to another post. * * @param int $new_id The new post ID. * @param WP_Post $post The original post object. * @param array $options The options array. * * @return void */ public function copy_post_meta_info( $new_id, $post, $options ) { $post_meta_keys = \get_post_custom_keys( $post->ID ); if ( empty( $post_meta_keys ) ) { return; } $meta_excludelist = $options['meta_excludelist']; if ( ! \is_array( $meta_excludelist ) ) { $meta_excludelist = []; } $meta_excludelist = \array_merge( $meta_excludelist, Utils::get_default_filtered_meta_names() ); if ( ! $options['copy_template'] ) { $meta_excludelist[] = '_wp_page_template'; } if ( ! $options['copy_thumbnail'] ) { $meta_excludelist[] = '_thumbnail_id'; } if ( $options['use_filters'] ) { /** * Filters the meta fields excludelist when copying a post. * * @param array $meta_excludelist The meta fields excludelist from the options. * * @return array */ $meta_excludelist = \apply_filters( 'duplicate_post_excludelist_filter', $meta_excludelist ); } $meta_excludelist_string = '(' . \implode( ')|(', $meta_excludelist ) . ')'; if ( \strpos( $meta_excludelist_string, '*' ) !== false ) { $meta_excludelist_string = \str_replace( [ '*' ], [ '[a-zA-Z0-9_]*' ], $meta_excludelist_string ); $meta_keys = []; foreach ( $post_meta_keys as $meta_key ) { if ( ! \preg_match( '#^' . $meta_excludelist_string . '$#', $meta_key ) ) { $meta_keys[] = $meta_key; } } } else { $meta_keys = \array_diff( $post_meta_keys, $meta_excludelist ); } if ( $options['use_filters'] ) { /** * Filters the list of meta fields names when copying a post. * * @param array $meta_keys The list of meta fields name, with the ones in the excludelist already removed. * * @return array */ $meta_keys = \apply_filters( 'duplicate_post_meta_keys_filter', $meta_keys ); } foreach ( $meta_keys as $meta_key ) { $meta_values = \get_post_custom_values( $meta_key, $post->ID ); // Clear existing meta data so that add_post_meta() works properly with non-unique keys. \delete_post_meta( $new_id, $meta_key ); foreach ( $meta_values as $meta_value ) { $meta_value = \maybe_unserialize( $meta_value ); \add_post_meta( $new_id, $meta_key, Utils::recursively_slash_strings( $meta_value ) ); } } } /** * Generates and returns the title for the copy. * * @param WP_Post $post The original post object. * @param array $options The options array. * * @return string The calculated title for the copy. */ public function generate_copy_title( WP_Post $post, array $options ) { $prefix = \sanitize_text_field( $options['title_prefix'] ); $suffix = \sanitize_text_field( $options['title_suffix'] ); if ( $options['copy_title'] ) { $title = $post->post_title; if ( ! empty( $prefix ) ) { $prefix .= ' '; } if ( ! empty( $suffix ) ) { $suffix = ' ' . $suffix; } } else { $title = ''; } return \trim( $prefix . $title . $suffix ); } /** * Generates and returns the status for the copy. * * @param WP_Post $post The original post object. * @param array $options The options array. * * @return string The calculated status for the copy. */ public function generate_copy_status( WP_Post $post, array $options ) { $new_post_status = 'draft'; if ( $options['copy_status'] ) { $new_post_status = $post->post_status; if ( $new_post_status === 'publish' || $new_post_status === 'future' ) { // Check if the user has the right capability. if ( \is_post_type_hierarchical( $post->post_type ) ) { if ( ! \current_user_can( 'publish_pages' ) ) { $new_post_status = 'pending'; } } elseif ( ! \current_user_can( 'publish_posts' ) ) { $new_post_status = 'pending'; } } } return $new_post_status; } /** * Generates and returns the author ID for the copy. * * @param WP_Post $post The original post object. * @param array $options The options array. * * @return int|string The calculated author ID for the copy. */ public function generate_copy_author( WP_Post $post, array $options ) { $new_post_author = \wp_get_current_user(); $new_post_author_id = $new_post_author->ID; if ( $options['copy_author'] ) { // Check if the user has the right capability. if ( \is_post_type_hierarchical( $post->post_type ) ) { if ( \current_user_can( 'edit_others_pages' ) ) { $new_post_author_id = $post->post_author; } } elseif ( \current_user_can( 'edit_others_posts' ) ) { $new_post_author_id = $post->post_author; } } return $new_post_author_id; } } watchers/copied-post-watcher.php 0000755 00000006255 14717635165 0013010 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Watchers; use WP_Post; use Yoast\WP\Duplicate_Post\Permissions_Helper; /** * Duplicate Post class to watch if the current post has a Rewrite & Republish copy. */ class Copied_Post_Watcher { /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Initializes the class. * * @param Permissions_Helper $permissions_helper The Permissions helper object. */ public function __construct( $permissions_helper ) { $this->permissions_helper = $permissions_helper; $this->register_hooks(); } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { \add_action( 'admin_notices', [ $this, 'add_admin_notice' ] ); \add_action( 'enqueue_block_editor_assets', [ $this, 'add_block_editor_notice' ], 11 ); } /** * Generates the translated text for the notice. * * @param WP_Post $post The current post object. * * @return string The translated text for the notice. */ public function get_notice_text( $post ) { if ( $this->permissions_helper->has_trashed_rewrite_and_republish_copy( $post ) ) { return \__( 'You can only make one Rewrite & Republish duplicate at a time, and a duplicate of this post already exists in the trash. Permanently delete it if you want to make a new duplicate.', 'duplicate-post' ); } $scheduled_copy = $this->permissions_helper->has_scheduled_rewrite_and_republish_copy( $post ); if ( ! $scheduled_copy ) { return \__( 'A duplicate of this post was made. Please note that any changes you make to this post will be replaced when the duplicated version is republished.', 'duplicate-post' ); } return \sprintf( /* translators: %1$s: scheduled date of the copy, %2$s: scheduled time of the copy. */ \__( 'A duplicate of this post was made, which is scheduled to replace this post on %1$s at %2$s.', 'duplicate-post' ), \get_the_time( \get_option( 'date_format' ), $scheduled_copy ), \get_the_time( \get_option( 'time_format' ), $scheduled_copy ) ); } /** * Shows a notice on the Classic editor. * * @return void */ public function add_admin_notice() { if ( ! $this->permissions_helper->is_classic_editor() ) { return; } $post = \get_post(); if ( ! $post instanceof WP_Post ) { return; } if ( $this->permissions_helper->has_rewrite_and_republish_copy( $post ) ) { print '<div id="message" class="notice notice-warning is-dismissible fade"><p>' . \esc_html( $this->get_notice_text( $post ) ) . '</p></div>'; } } /** * Shows a notice on the Block editor. * * @return void */ public function add_block_editor_notice() { $post = \get_post(); if ( ! $post instanceof WP_Post ) { return; } if ( $this->permissions_helper->has_rewrite_and_republish_copy( $post ) ) { $notice = [ 'text' => \wp_slash( $this->get_notice_text( $post ) ), 'status' => 'warning', 'isDismissible' => true, ]; \wp_add_inline_script( 'duplicate_post_edit_script', "duplicatePostNotices.has_rewrite_and_republish_notice = '" . \wp_json_encode( $notice ) . "';", 'before' ); } } } watchers/republished-post-watcher.php 0000755 00000005150 14717635165 0014044 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Watchers; use Yoast\WP\Duplicate_Post\Permissions_Helper; /** * Duplicate Post class to watch if the post has been republished for Rewrite & Republish. * * @since 4.0 */ class Republished_Post_Watcher { /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Initializes the class. * * @param Permissions_Helper $permissions_helper The Permissions helper object. */ public function __construct( Permissions_Helper $permissions_helper ) { $this->permissions_helper = $permissions_helper; $this->register_hooks(); } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { \add_filter( 'removable_query_args', [ $this, 'add_removable_query_args' ] ); \add_action( 'admin_notices', [ $this, 'add_admin_notice' ] ); \add_action( 'enqueue_block_editor_assets', [ $this, 'add_block_editor_notice' ], 11 ); } /** * Adds vars to the removable query args. * * @param array $removable_query_args Array of query args keys. * * @return array The updated array of query args keys. */ public function add_removable_query_args( $removable_query_args ) { if ( \is_array( $removable_query_args ) ) { $removable_query_args[] = 'dprepublished'; $removable_query_args[] = 'dpcopy'; $removable_query_args[] = 'dpnonce'; } return $removable_query_args; } /** * Generates the translated text for the republished notice. * * @return string The translated text for the republished notice. */ public function get_notice_text() { return \__( 'Your original post has been replaced with the rewritten post. You are now viewing the (rewritten) original post.', 'duplicate-post' ); } /** * Shows a notice on the Classic editor. * * @return void */ public function add_admin_notice() { if ( ! $this->permissions_helper->is_classic_editor() ) { return; } if ( ! empty( $_REQUEST['dprepublished'] ) ) { echo '<div id="message" class="notice notice-success is-dismissible"><p>' . \esc_html( $this->get_notice_text() ) . '</p></div>'; } } /** * Shows a notice on the Block editor. * * @return void */ public function add_block_editor_notice() { if ( ! empty( $_REQUEST['dprepublished'] ) ) { $notice = [ 'text' => \wp_slash( $this->get_notice_text() ), 'status' => 'success', 'isDismissible' => true, ]; \wp_add_inline_script( 'duplicate_post_edit_script', "duplicatePostNotices.republished_notice = '" . \wp_json_encode( $notice ) . "';", 'before' ); } } } watchers/watchers.php 0000755 00000003012 14717635165 0010733 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Watchers; use Yoast\WP\Duplicate_Post\Permissions_Helper; /** * Duplicate Post user interface. */ class Watchers { /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Holds the original post watcher. * * @var Original_Post_Watcher */ protected $original_post_watcher; /** * Holds the copied post watcher. * * @var Copied_Post_Watcher */ protected $copied_post_watcher; /** * Holds the bulk actions watcher. * * @var Bulk_Actions_Watcher */ protected $bulk_actions_watcher; /** * Holds the link actions watcher. * * @var Link_Actions_Watcher */ protected $link_actions_watcher; /** * Holds the republished post watcher. * * @var Republished_Post_Watcher */ protected $republished_post_watcher; /** * Initializes the class. * * @param Permissions_Helper $permissions_helper The permissions helper object. */ public function __construct( Permissions_Helper $permissions_helper ) { $this->permissions_helper = $permissions_helper; $this->copied_post_watcher = new Copied_Post_Watcher( $this->permissions_helper ); $this->original_post_watcher = new Original_Post_Watcher( $this->permissions_helper ); $this->bulk_actions_watcher = new Bulk_Actions_Watcher(); $this->link_actions_watcher = new Link_Actions_Watcher( $this->permissions_helper ); $this->republished_post_watcher = new Republished_Post_Watcher( $this->permissions_helper ); } } watchers/link-actions-watcher.php 0000755 00000006707 14717635165 0013157 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Watchers; use Yoast\WP\Duplicate_Post\Permissions_Helper; /** * Duplicate Post class to watch for the link actions and show notices. */ class Link_Actions_Watcher { /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Initializes the class. * * @param Permissions_Helper $permissions_helper The Permissions helper object. */ public function __construct( Permissions_Helper $permissions_helper ) { $this->permissions_helper = $permissions_helper; $this->register_hooks(); } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { \add_filter( 'removable_query_args', [ $this, 'add_removable_query_args' ], 10, 1 ); \add_action( 'admin_notices', [ $this, 'add_clone_admin_notice' ] ); \add_action( 'admin_notices', [ $this, 'add_rewrite_and_republish_admin_notice' ] ); \add_action( 'enqueue_block_editor_assets', [ $this, 'add_rewrite_and_republish_block_editor_notice' ] ); } /** * Adds vars to the removable query args. * * @param array $removable_query_args Array of query args keys. * * @return array The updated array of query args keys. */ public function add_removable_query_args( $removable_query_args ) { if ( \is_array( $removable_query_args ) ) { $removable_query_args[] = 'cloned'; $removable_query_args[] = 'rewriting'; } return $removable_query_args; } /** * Shows a notice after the Clone link action has succeeded. * * @return void */ public function add_clone_admin_notice() { if ( ! empty( $_REQUEST['cloned'] ) ) { if ( ! $this->permissions_helper->is_classic_editor() ) { return; } $copied_posts = \intval( $_REQUEST['cloned'] ); \printf( '<div id="message" class="notice notice-success fade"><p>' . \esc_html( /* translators: %s: Number of posts copied. */ \_n( '%s item copied.', '%s items copied.', $copied_posts, 'duplicate-post' ) ) . '</p></div>', \esc_html( $copied_posts ) ); } } /** * Shows a notice in Classic editor after the Rewrite & Republish action via link has succeeded. * * @return void */ public function add_rewrite_and_republish_admin_notice() { if ( ! empty( $_REQUEST['rewriting'] ) ) { if ( ! $this->permissions_helper->is_classic_editor() ) { return; } print '<div id="message" class="notice notice-warning is-dismissible fade"><p>' . \esc_html__( 'You can now start rewriting your post in this duplicate of the original post. If you click "Republish", your changes will be merged into the original post and you’ll be redirected there.', 'duplicate-post' ) . '</p></div>'; } } /** * Shows a notice on the Block editor after the Rewrite & Republish action via link has succeeded. * * @return void */ public function add_rewrite_and_republish_block_editor_notice() { if ( ! empty( $_REQUEST['rewriting'] ) ) { $notice = [ 'text' => \wp_slash( \__( 'You can now start rewriting your post in this duplicate of the original post. If you click "Republish", this rewritten post will replace the original post.', 'duplicate-post' ) ), 'status' => 'warning', 'isDismissible' => true, ]; \wp_add_inline_script( 'duplicate_post_edit_script', "duplicatePostNotices.rewriting_notice = '" . \wp_json_encode( $notice ) . "';", 'before' ); } } } watchers/original-post-watcher.php 0000755 00000004414 14717635165 0013344 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Watchers; use WP_Post; use Yoast\WP\Duplicate_Post\Permissions_Helper; /** * Duplicate Post Original post watcher class. * * Watches the original post for changes. * * @since 4.0 */ class Original_Post_Watcher { /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Initializes the class. * * @param Permissions_Helper $permissions_helper The Permissions helper object. */ public function __construct( Permissions_Helper $permissions_helper ) { $this->permissions_helper = $permissions_helper; $this->register_hooks(); } /** * Registers the hooks. * * @return void */ public function register_hooks() { \add_action( 'admin_notices', [ $this, 'add_admin_notice' ] ); \add_action( 'enqueue_block_editor_assets', [ $this, 'add_block_editor_notice' ], 11 ); } /** * Generates the translated text for the notice. * * @return string The translated text for the notice. */ public function get_notice_text() { return \__( 'The original post has been edited in the meantime. If you click "Republish", this rewritten post will replace the original post.', 'duplicate-post' ); } /** * Shows a notice on the Classic editor. * * @return void */ public function add_admin_notice() { if ( ! $this->permissions_helper->is_classic_editor() ) { return; } $post = \get_post(); if ( ! $post instanceof WP_Post ) { return; } if ( $this->permissions_helper->has_original_changed( $post ) ) { print '<div id="message" class="notice notice-warning is-dismissible fade"><p>' . \esc_html( $this->get_notice_text() ) . '</p></div>'; } } /** * Shows a notice on the Block editor. * * @return void */ public function add_block_editor_notice() { $post = \get_post(); if ( ! $post instanceof WP_Post ) { return; } if ( $this->permissions_helper->has_original_changed( $post ) ) { $notice = [ 'text' => \wp_slash( $this->get_notice_text() ), 'status' => 'warning', 'isDismissible' => true, ]; \wp_add_inline_script( 'duplicate_post_edit_script', "duplicatePostNotices.has_original_changed_notice = '" . \wp_json_encode( $notice ) . "';", 'before' ); } } } watchers/bulk-actions-watcher.php 0000755 00000005014 14717635165 0013145 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Watchers; /** * Duplicate Post class to watch for the bulk actions and show notices. */ class Bulk_Actions_Watcher { /** * Initializes the class. */ public function __construct() { $this->register_hooks(); } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { \add_filter( 'removable_query_args', [ $this, 'add_removable_query_args' ] ); \add_action( 'admin_notices', [ $this, 'add_bulk_clone_admin_notice' ] ); \add_action( 'admin_notices', [ $this, 'add_bulk_rewrite_and_republish_admin_notice' ] ); } /** * Adds vars to the removable query args. * * @param array $removable_query_args Array of query args keys. * * @return array The updated array of query args keys. */ public function add_removable_query_args( $removable_query_args ) { if ( \is_array( $removable_query_args ) ) { $removable_query_args[] = 'bulk_cloned'; $removable_query_args[] = 'bulk_rewriting'; } return $removable_query_args; } /** * Shows a notice after the Clone bulk action has succeeded. * * @return void */ public function add_bulk_clone_admin_notice() { if ( ! empty( $_REQUEST['bulk_cloned'] ) ) { $copied_posts = \intval( $_REQUEST['bulk_cloned'] ); \printf( '<div id="message" class="notice notice-success fade"><p>' . \esc_html( /* translators: %s: Number of posts copied. */ \_n( '%s item copied.', '%s items copied.', $copied_posts, 'duplicate-post' ) ) . '</p></div>', \esc_html( $copied_posts ) ); } } /** * Shows a notice after the Rewrite & Republish bulk action has succeeded. * * @return void */ public function add_bulk_rewrite_and_republish_admin_notice() { if ( ! empty( $_REQUEST['bulk_rewriting'] ) ) { $copied_posts = \intval( $_REQUEST['bulk_rewriting'] ); \printf( '<div id="message" class="notice notice-success fade"><p>' . \esc_html( /* translators: %s: Number of posts copied. */ \_n( '%s post duplicated. You can now start rewriting your post in the duplicate of the original post. Once you choose to republish it your changes will be merged back into the original post.', '%s posts duplicated. You can now start rewriting your posts in the duplicates of the original posts. Once you choose to republish them your changes will be merged back into the original post.', $copied_posts, 'duplicate-post' ) ) . '</p></div>', \esc_html( $copied_posts ) ); } } } handlers/check-changes-handler.php 0000755 00000014031 14717635165 0013174 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Handlers; use WP_Post; use Yoast\WP\Duplicate_Post\Permissions_Helper; use Yoast\WP\Duplicate_Post\Utils; /** * Duplicate Post handler class for changes overview. * * Represents the handler for checking the changes between a copy and the original post. * * @since 4.0 */ class Check_Changes_Handler { /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Holds the current post object. * * @var WP_Post */ private $post; /** * Holds the original post object. * * @var WP_Post */ private $original; /** * Initializes the class. * * @param Permissions_Helper $permissions_helper The Permissions Helper object. */ public function __construct( Permissions_Helper $permissions_helper ) { $this->permissions_helper = $permissions_helper; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { \add_action( 'admin_action_duplicate_post_check_changes', [ $this, 'check_changes_action_handler' ] ); } /** * Handles the action for displaying the changes between a copy and the original. * * @return void */ public function check_changes_action_handler() { global $wp_version; if ( ! ( isset( $_GET['post'] ) || isset( $_POST['post'] ) || ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'duplicate_post_check_changes' ) ) ) { \wp_die( \esc_html__( 'No post has been supplied!', 'duplicate-post' ) ); return; } $id = ( isset( $_GET['post'] ) ? \intval( \wp_unslash( $_GET['post'] ) ) : \intval( \wp_unslash( $_POST['post'] ) ) ); \check_admin_referer( 'duplicate_post_check_changes_' . $id ); $this->post = \get_post( $id ); if ( ! $this->post ) { \wp_die( \esc_html( \sprintf( /* translators: %s: post ID. */ \__( 'Changes overview failed, could not find post with ID %s.', 'duplicate-post' ), $id ) ) ); return; } $this->original = Utils::get_original( $this->post ); if ( ! $this->original ) { \wp_die( \esc_html( \__( 'Changes overview failed, could not find original post.', 'duplicate-post' ) ) ); return; } $post_edit_link = \get_edit_post_link( $this->post->ID ); $this->require_wordpress_header(); ?> <div class="wrap"> <h1 class="long-header"> <?php echo \sprintf( /* translators: %s: original item link (to view or edit) or title. */ \esc_html__( 'Compare changes of duplicated post with the original (“%s”)', 'duplicate-post' ), Utils::get_edit_or_view_link( $this->original ) // phpcs:ignore WordPress.Security.EscapeOutput ); ?> </h1> <a href="<?php echo \esc_url( $post_edit_link ); ?>"><?php \esc_html_e( '← Return to editor', 'duplicate-post' ); ?></a> <div class="revisions"> <div class="revisions-control-frame"> <div class="revisions-controls"></div> </div> <div class="revisions-diff-frame"> <div class="revisions-diff"> <div class="diff"> <?php $fields = [ 'post_title' => \__( 'Title', 'duplicate-post' ), 'post_content' => \__( 'Content', 'duplicate-post' ), 'post_excerpt' => \__( 'Excerpt', 'duplicate-post' ), ]; $args = [ 'show_split_view' => true, 'title_left' => \__( 'Removed', 'duplicate-post' ), 'title_right' => \__( 'Added', 'duplicate-post' ), ]; if ( \version_compare( $wp_version, '5.7' ) < 0 ) { unset( $args['title_left'] ); unset( $args['title_right'] ); } $post_array = \get_post( $this->post, \ARRAY_A ); /** This filter is documented in wp-admin/includes/revision.php */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reason: using WP core hook. $fields = \apply_filters( '_wp_post_revision_fields', $fields, $post_array ); foreach ( $fields as $field => $name ) { /** This filter is documented in wp-admin/includes/revision.php */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reason: using WP core hook. $content_from = \apply_filters( "_wp_post_revision_field_{$field}", $this->original->$field, $field, $this->original, 'from' ); /** This filter is documented in wp-admin/includes/revision.php */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reason: using WP core hook. $content_to = \apply_filters( "_wp_post_revision_field_{$field}", $this->post->$field, $field, $this->post, 'to' ); $diff = \wp_text_diff( $content_from, $content_to, $args ); if ( ! $diff && $field === 'post_title' ) { // It's a better user experience to still show the Title, even if it didn't change. $diff = '<table class="diff"><colgroup><col class="content diffsplit left"><col class="content diffsplit middle"><col class="content diffsplit right"></colgroup><tbody><tr>'; $diff .= '<td>' . \esc_html( $this->original->post_title ) . '</td><td></td><td>' . \esc_html( $this->post->post_title ) . '</td>'; $diff .= '</tr></tbody>'; $diff .= '</table>'; } if ( $diff ) { ?> <h3><?php echo \esc_html( $name ); ?></h3> <?php echo $diff; // phpcs:ignore WordPress.Security.EscapeOutput } } ?> </div> </div> </div> </div> </div> <?php $this->require_wordpress_footer(); } /** * Requires the WP admin header. * * @codeCoverageIgnore * * @return void */ public function require_wordpress_header() { global $post; \set_current_screen( 'revision' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- The revision screen expects $post to be set. $post = $this->post; require_once \ABSPATH . 'wp-admin/admin-header.php'; } /** * Requires the WP admin footer. * * @codeCoverageIgnore * * @return void */ public function require_wordpress_footer() { require_once \ABSPATH . 'wp-admin/admin-footer.php'; } } handlers/handler.php 0000755 00000003404 14717635165 0010515 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Handlers; use Yoast\WP\Duplicate_Post\Permissions_Helper; use Yoast\WP\Duplicate_Post\Post_Duplicator; /** * Duplicate Post handler class for duplication actions. * * @since 4.0 */ class Handler { /** * Post_Duplicator object. * * @var Post_Duplicator */ protected $post_duplicator; /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * The bulk actions handler. * * @var Bulk_Handler */ protected $bulk_handler; /** * The link actions handler. * * @var Link_Handler */ protected $link_handler; /** * The save_post action handler. * * @var Save_Post_Handler */ protected $save_post_handler; /** * The link actions handler. * * @var Check_Changes_Handler */ protected $check_handler; /** * Initializes the class. * * @param Post_Duplicator $post_duplicator The Post_Duplicator object. * @param Permissions_Helper $permissions_helper The Permissions Helper object. */ public function __construct( Post_Duplicator $post_duplicator, Permissions_Helper $permissions_helper ) { $this->post_duplicator = $post_duplicator; $this->permissions_helper = $permissions_helper; $this->bulk_handler = new Bulk_Handler( $this->post_duplicator, $this->permissions_helper ); $this->link_handler = new Link_Handler( $this->post_duplicator, $this->permissions_helper ); $this->check_handler = new Check_Changes_Handler( $this->permissions_helper ); $this->save_post_handler = new Save_Post_Handler( $this->permissions_helper ); $this->bulk_handler->register_hooks(); $this->link_handler->register_hooks(); $this->check_handler->register_hooks(); $this->save_post_handler->register_hooks(); } } handlers/save-post-handler.php 0000755 00000002726 14717635165 0012442 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Handlers; use Yoast\WP\Duplicate_Post\Permissions_Helper; /** * Duplicate Post handler class for save_post action. * * @since 4.0 */ class Save_Post_Handler { /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Initializes the class. * * @param Permissions_Helper $permissions_helper The Permissions Helper object. */ public function __construct( Permissions_Helper $permissions_helper ) { $this->permissions_helper = $permissions_helper; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { if ( \intval( \get_option( 'duplicate_post_show_original_meta_box' ) ) === 1 || \intval( \get_option( 'duplicate_post_show_original_column' ) ) === 1 ) { \add_action( 'save_post', [ $this, 'delete_on_save_post' ] ); } } /** * Deletes the custom field with the ID of the original post. * * @param int $post_id The current post ID. * * @return void */ public function delete_on_save_post( $post_id ) { if ( ( \defined( 'DOING_AUTOSAVE' ) && \DOING_AUTOSAVE ) || empty( $_POST['duplicate_post_remove_original'] ) || ! \current_user_can( 'edit_post', $post_id ) ) { return; } $post = \get_post( $post_id ); if ( ! $post ) { return; } if ( ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { \delete_post_meta( $post_id, '_dp_original' ); } } } handlers/bulk-handler.php 0000755 00000010102 14717635165 0011441 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Handlers; use Yoast\WP\Duplicate_Post\Permissions_Helper; use Yoast\WP\Duplicate_Post\Post_Duplicator; use Yoast\WP\Duplicate_Post\Utils; /** * Duplicate Post handler class for duplication bulk actions. * * @since 4.0 */ class Bulk_Handler { /** * Post_Duplicator object. * * @var Post_Duplicator */ protected $post_duplicator; /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Initializes the class. * * @param Post_Duplicator $post_duplicator The Post_Duplicator object. * @param Permissions_Helper $permissions_helper The Permissions Helper object. */ public function __construct( Post_Duplicator $post_duplicator, Permissions_Helper $permissions_helper ) { $this->post_duplicator = $post_duplicator; $this->permissions_helper = $permissions_helper; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { \add_action( 'admin_init', [ $this, 'add_bulk_handlers' ] ); } /** * Hooks the handler for the Rewrite & Republish action for all the selected post types. * * @return void */ public function add_bulk_handlers() { $duplicate_post_types_enabled = $this->permissions_helper->get_enabled_post_types(); foreach ( $duplicate_post_types_enabled as $duplicate_post_type_enabled ) { \add_filter( "handle_bulk_actions-edit-{$duplicate_post_type_enabled}", [ $this, 'bulk_action_handler' ], 10, 3 ); } } /** * Handles the bulk actions. * * @param string $redirect_to The URL to redirect to. * @param string $doaction The action that has been called. * @param array $post_ids The array of marked post IDs. * * @return string The URL to redirect to. */ public function bulk_action_handler( $redirect_to, $doaction, $post_ids ) { $redirect_to = $this->clone_bulk_action_handler( $redirect_to, $doaction, $post_ids ); return $this->rewrite_bulk_action_handler( $redirect_to, $doaction, $post_ids ); } /** * Handles the bulk action for the Rewrite & Republish feature. * * @param string $redirect_to The URL to redirect to. * @param string $doaction The action that has been called. * @param array $post_ids The array of marked post IDs. * * @return string The URL to redirect to. */ public function rewrite_bulk_action_handler( $redirect_to, $doaction, $post_ids ) { if ( $doaction !== 'duplicate_post_bulk_rewrite_republish' ) { return $redirect_to; } $counter = 0; if ( \is_array( $post_ids ) ) { foreach ( $post_ids as $post_id ) { $post = \get_post( $post_id ); if ( ! empty( $post ) && $this->permissions_helper->should_rewrite_and_republish_be_allowed( $post ) ) { $new_post_id = $this->post_duplicator->create_duplicate_for_rewrite_and_republish( $post ); if ( ! \is_wp_error( $new_post_id ) ) { ++$counter; } } } } return \add_query_arg( 'bulk_rewriting', $counter, $redirect_to ); } /** * Handles the bulk action for the Clone feature. * * @param string $redirect_to The URL to redirect to. * @param string $doaction The action that has been called. * @param array $post_ids The array of marked post IDs. * * @return string The URL to redirect to. */ public function clone_bulk_action_handler( $redirect_to, $doaction, $post_ids ) { if ( $doaction !== 'duplicate_post_bulk_clone' ) { return $redirect_to; } $counter = 0; if ( \is_array( $post_ids ) ) { foreach ( $post_ids as $post_id ) { $post = \get_post( $post_id ); if ( ! empty( $post ) && ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { if ( \intval( \get_option( 'duplicate_post_copychildren' ) !== 1 ) || ! \is_post_type_hierarchical( $post->post_type ) || ( \is_post_type_hierarchical( $post->post_type ) && ! Utils::has_ancestors_marked( $post, $post_ids ) ) ) { if ( ! \is_wp_error( \duplicate_post_create_duplicate( $post ) ) ) { ++$counter; } } } } } return \add_query_arg( 'bulk_cloned', $counter, $redirect_to ); } } handlers/link-handler.php 0000755 00000015061 14717635165 0011452 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Handlers; use Yoast\WP\Duplicate_Post\Permissions_Helper; use Yoast\WP\Duplicate_Post\Post_Duplicator; /** * Duplicate Post handler class for duplication actions from links. * * @since 4.0 */ class Link_Handler { /** * Post_Duplicator object. * * @var Post_Duplicator */ protected $post_duplicator; /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Initializes the class. * * @param Post_Duplicator $post_duplicator The Post_Duplicator object. * @param Permissions_Helper $permissions_helper The Permissions Helper object. */ public function __construct( Post_Duplicator $post_duplicator, Permissions_Helper $permissions_helper ) { $this->post_duplicator = $post_duplicator; $this->permissions_helper = $permissions_helper; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { \add_action( 'admin_action_duplicate_post_rewrite', [ $this, 'rewrite_link_action_handler' ] ); \add_action( 'admin_action_duplicate_post_clone', [ $this, 'clone_link_action_handler' ] ); \add_action( 'admin_action_duplicate_post_new_draft', [ $this, 'new_draft_link_action_handler' ] ); } /** * Handles the action for copying a post to a new draft. * * @return void */ public function new_draft_link_action_handler() { if ( ! $this->permissions_helper->is_current_user_allowed_to_copy() ) { \wp_die( \esc_html__( 'Current user is not allowed to copy posts.', 'duplicate-post' ) ); } if ( ! ( isset( $_GET['post'] ) || isset( $_POST['post'] ) || ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'duplicate_post_new_draft' ) ) ) { \wp_die( \esc_html__( 'No post to duplicate has been supplied!', 'duplicate-post' ) ); } $id = ( isset( $_GET['post'] ) ? \intval( \wp_unslash( $_GET['post'] ) ) : \intval( \wp_unslash( $_POST['post'] ) ) ); \check_admin_referer( 'duplicate_post_new_draft_' . $id ); $post = \get_post( $id ); if ( ! $post ) { \wp_die( \esc_html( \__( 'Copy creation failed, could not find original:', 'duplicate-post' ) . ' ' . $id ) ); } if ( $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { \wp_die( \esc_html__( 'You cannot create a copy of a post which is intended for Rewrite & Republish.', 'duplicate-post' ) ); } $new_id = \duplicate_post_create_duplicate( $post, 'draft' ); if ( \is_wp_error( $new_id ) ) { \wp_die( \esc_html__( 'Copy creation failed, could not create a copy.', 'duplicate-post' ) ); } \wp_safe_redirect( \add_query_arg( [ 'cloned' => 1, 'ids' => $post->ID, ], \admin_url( 'post.php?action=edit&post=' . $new_id . ( isset( $_GET['classic-editor'] ) ? '&classic-editor' : '' ) ) ) ); exit(); } /** * Handles the action for copying a post and redirecting to the post list. * * @return void */ public function clone_link_action_handler() { if ( ! $this->permissions_helper->is_current_user_allowed_to_copy() ) { \wp_die( \esc_html__( 'Current user is not allowed to copy posts.', 'duplicate-post' ) ); } if ( ! ( isset( $_GET['post'] ) || isset( $_POST['post'] ) || ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'duplicate_post_clone' ) ) ) { \wp_die( \esc_html__( 'No post to duplicate has been supplied!', 'duplicate-post' ) ); } $id = ( isset( $_GET['post'] ) ? \intval( \wp_unslash( $_GET['post'] ) ) : \intval( \wp_unslash( $_POST['post'] ) ) ); \check_admin_referer( 'duplicate_post_clone_' . $id ); $post = \get_post( $id ); if ( ! $post ) { \wp_die( \esc_html( \__( 'Copy creation failed, could not find original:', 'duplicate-post' ) . ' ' . $id ) ); } if ( $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { \wp_die( \esc_html__( 'You cannot create a copy of a post which is intended for Rewrite & Republish.', 'duplicate-post' ) ); } $new_id = \duplicate_post_create_duplicate( $post ); if ( \is_wp_error( $new_id ) ) { \wp_die( \esc_html__( 'Copy creation failed, could not create a copy.', 'duplicate-post' ) ); } $post_type = $post->post_type; $sendback = \wp_get_referer(); if ( ! $sendback || \strpos( $sendback, 'post.php' ) !== false || \strpos( $sendback, 'post-new.php' ) !== false ) { if ( $post_type === 'attachment' ) { $sendback = \admin_url( 'upload.php' ); } else { $sendback = \admin_url( 'edit.php' ); if ( ! empty( $post_type ) ) { $sendback = \add_query_arg( 'post_type', $post_type, $sendback ); } } } else { $sendback = \remove_query_arg( [ 'trashed', 'untrashed', 'deleted', 'cloned', 'ids' ], $sendback ); } // Redirect to the post list screen. \wp_safe_redirect( \add_query_arg( [ 'cloned' => 1, 'ids' => $post->ID, ], $sendback ) ); exit(); } /** * Handles the action for copying a post for the Rewrite & Republish feature. * * @return void */ public function rewrite_link_action_handler() { if ( ! $this->permissions_helper->is_current_user_allowed_to_copy() ) { \wp_die( \esc_html__( 'Current user is not allowed to copy posts.', 'duplicate-post' ) ); } if ( ! ( isset( $_GET['post'] ) || isset( $_POST['post'] ) || ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'duplicate_post_rewrite' ) ) ) { \wp_die( \esc_html__( 'No post to duplicate has been supplied!', 'duplicate-post' ) ); } $id = ( isset( $_GET['post'] ) ? \intval( \wp_unslash( $_GET['post'] ) ) : \intval( \wp_unslash( $_POST['post'] ) ) ); \check_admin_referer( 'duplicate_post_rewrite_' . $id ); $post = \get_post( $id ); if ( ! $post ) { \wp_die( \esc_html( \__( 'Copy creation failed, could not find original:', 'duplicate-post' ) . ' ' . $id ) ); } if ( ! $this->permissions_helper->should_rewrite_and_republish_be_allowed( $post ) ) { \wp_die( \esc_html__( 'You cannot create a copy for Rewrite & Republish if the original is not published or if it already has a copy.', 'duplicate-post' ) ); } $new_id = $this->post_duplicator->create_duplicate_for_rewrite_and_republish( $post ); if ( \is_wp_error( $new_id ) ) { \wp_die( \esc_html__( 'Copy creation failed, could not create a copy.', 'duplicate-post' ) ); } \wp_safe_redirect( \add_query_arg( [ 'rewriting' => 1, 'ids' => $post->ID, ], \admin_url( 'post.php?action=edit&post=' . $new_id . ( isset( $_GET['classic-editor'] ) ? '&classic-editor' : '' ) ) ) ); exit(); } } permissions-helper.php 0000755 00000017405 14717635165 0011136 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post; use WP_Post; /** * Permissions helper for Duplicate Post. * * @since 4.0 */ class Permissions_Helper { /** * Returns the array of the enabled post types. * * @return array The array of post types. */ public function get_enabled_post_types() { $enabled_post_types = \get_option( 'duplicate_post_types_enabled', [ 'post', 'page' ] ); if ( ! \is_array( $enabled_post_types ) ) { $enabled_post_types = [ $enabled_post_types ]; } if ( Utils::is_plugin_active( 'woocommerce/woocommerce.php' ) ) { $enabled_post_types = \array_diff( $enabled_post_types, [ 'product' ] ); } /** * Filters the list of post types for which the plugin is enabled. * * @param array $enabled_post_types The array of post type names for which the plugin is enabled. * * @return array The filtered array of post types names. */ return \apply_filters( 'duplicate_post_enabled_post_types', $enabled_post_types ); } /** * Determines if post type is enabled to be copied. * * @param string $post_type The post type to check. * * @return bool Whether the post type is enabled to be copied. */ public function is_post_type_enabled( $post_type ) { return \in_array( $post_type, $this->get_enabled_post_types(), true ); } /** * Determines if the current user can copy posts. * * @return bool Whether the current user can copy posts. */ public function is_current_user_allowed_to_copy() { return \current_user_can( 'copy_posts' ); } /** * Determines if the post is a copy intended for Rewrite & Republish. * * @param WP_Post $post The post object. * * @return bool Whether the post is a copy intended for Rewrite & Republish. */ public function is_rewrite_and_republish_copy( WP_Post $post ) { return ( \intval( \get_post_meta( $post->ID, '_dp_is_rewrite_republish_copy', true ) ) === 1 ); } /** * Gets the Rewrite & Republish copy ID for the passed post. * * @param WP_Post $post The post object. * * @return int The Rewrite & Republish copy ID. */ public function get_rewrite_and_republish_copy_id( WP_Post $post ) { return \get_post_meta( $post->ID, '_dp_has_rewrite_republish_copy', true ); } /** * Gets the copy post object for the passed post. * * @param WP_Post $post The post to get the copy for. * * @return WP_Post|null The copy's post object or null if it doesn't exist. */ public function get_rewrite_and_republish_copy( WP_Post $post ) { $copy_id = $this->get_rewrite_and_republish_copy_id( $post ); if ( empty( $copy_id ) ) { return null; } return \get_post( $copy_id ); } /** * Determines if the post has a copy intended for Rewrite & Republish. * * @param WP_Post $post The post object. * * @return bool Whether the post has a copy intended for Rewrite & Republish. */ public function has_rewrite_and_republish_copy( WP_Post $post ) { return ( ! empty( $this->get_rewrite_and_republish_copy_id( $post ) ) ); } /** * Determines if the post has a copy intended for Rewrite & Republish which is scheduled to be published. * * @param WP_Post $post The post object. * * @return bool|WP_Post The scheduled copy if present, false if the post has no scheduled copy. */ public function has_scheduled_rewrite_and_republish_copy( WP_Post $post ) { $copy = $this->get_rewrite_and_republish_copy( $post ); if ( ! empty( $copy ) && $copy->post_status === 'future' ) { return $copy; } return false; } /** * Determines whether the current screen is an edit post screen. * * @return bool Whether or not the current screen is editing an existing post. */ public function is_edit_post_screen() { if ( ! \is_admin() ) { return false; } $current_screen = \get_current_screen(); return $current_screen->base === 'post' && $current_screen->action !== 'add'; } /** * Determines whether the current screen is an new post screen. * * @return bool Whether or not the current screen is editing an new post. */ public function is_new_post_screen() { if ( ! \is_admin() ) { return false; } $current_screen = \get_current_screen(); return $current_screen->base === 'post' && $current_screen->action === 'add'; } /** * Determines if we are currently editing a post with Classic editor. * * @return bool Whether we are currently editing a post with Classic editor. */ public function is_classic_editor() { if ( ! $this->is_edit_post_screen() && ! $this->is_new_post_screen() ) { return false; } $screen = \get_current_screen(); if ( $screen->is_block_editor() ) { return false; } return true; } /** * Determines if the original post has changed since the creation of the copy. * * @param WP_Post $post The post object. * * @return bool Whether the original post has changed since the creation of the copy. */ public function has_original_changed( WP_Post $post ) { if ( ! $this->is_rewrite_and_republish_copy( $post ) ) { return false; } $original = Utils::get_original( $post ); $copy_creation_date_gmt = \get_post_meta( $post->ID, '_dp_creation_date_gmt', true ); if ( $original && $copy_creation_date_gmt ) { if ( \strtotime( $original->post_modified_gmt ) > \strtotime( $copy_creation_date_gmt ) ) { return true; } } return false; } /** * Determines if duplicate links for the post can be displayed. * * @param WP_Post $post The post object. * * @return bool Whether the links can be displayed. */ public function should_links_be_displayed( WP_Post $post ) { /** * Filter allowing displaying duplicate post links for current post. * * @param bool $display_links Whether the duplicate links will be displayed. * @param WP_Post $post The post object. * * @return bool Whether or not to display the duplicate post links. */ $display_links = \apply_filters( 'duplicate_post_show_link', $this->is_current_user_allowed_to_copy() && $this->is_post_type_enabled( $post->post_type ), $post ); return ! $this->is_rewrite_and_republish_copy( $post ) && $display_links; } /** * Determines if the Rewrite & Republish link for the post should be displayed. * * @param WP_Post $post The post object. * * @return bool Whether the links should be displayed. */ public function should_rewrite_and_republish_be_allowed( WP_Post $post ) { return $post->post_status === 'publish' && ! $this->is_rewrite_and_republish_copy( $post ) && ! $this->has_rewrite_and_republish_copy( $post ); } /** * Determines whether the passed post type is public and shows an admin bar. * * @param string $post_type The post_type to copy. * * @return bool Whether or not the post can be copied to a new draft. */ public function post_type_has_admin_bar( $post_type ) { $post_type_object = \get_post_type_object( $post_type ); if ( empty( $post_type_object ) ) { return false; } return $post_type_object->public && $post_type_object->show_in_admin_bar; } /** * Determines whether a Rewrite & Republish copy can be republished. * * @param WP_Post $post The post object. * * @return bool Whether the Rewrite & Republish copy can be republished. */ public function is_copy_allowed_to_be_republished( WP_Post $post ) { return \in_array( $post->post_status, [ 'dp-rewrite-republish', 'private' ], true ); } /** * Determines if the post has a trashed copy intended for Rewrite & Republish. * * @param WP_Post $post The post object. * * @return bool Whether the post has a trashed copy intended for Rewrite & Republish. */ public function has_trashed_rewrite_and_republish_copy( WP_Post $post ) { $copy_id = \get_post_meta( $post->ID, '_dp_has_rewrite_republish_copy', true ); if ( ! $copy_id ) { return false; } $copy = \get_post( $copy_id ); return ( $copy && $copy->post_status === 'trash' ); } } admin/options-inputs.php 0000755 00000003765 14717635165 0011415 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Admin; /** * Class Options_Inputs. */ class Options_Inputs { /** * Creates a basic input based on the passed parameters. * * @param string $type The type of input. * @param string $name The name of the input. * @param string $value The value of the input. * @param string $id The ID of the input. * @param string $attributes The additional attributes to use. Optional. * * @return string The input's HTML output. */ protected function input( $type, $name, $value, $id, $attributes = '' ) { return \sprintf( '<input type="%s" name="%s" id="%s" value="%s" %s />', \esc_attr( $type ), \esc_attr( $name ), \esc_attr( $id ), \esc_attr( $value ), $attributes ); } /** * Creates a checkbox input. * * @param string $name The name of the checkbox. * @param string $value The value of the checkbox. * @param string $id The ID of the checkbox. * @param bool $checked Whether or not the checkbox should be checked. * * @return string The checkbox' HTML output. */ public function checkbox( $name, $value, $id, $checked = false ) { $checked = \checked( $checked, true, false ); return $this->input( 'checkbox', $name, $value, $id, $checked ); } /** * Creates a text field input. * * @param string $name The name of the text field. * @param string $value The value of the text field. * @param string $id The ID of the text field. * * @return string The text field's HTML output. */ public function text( $name, $value, $id ) { return $this->input( 'text', $name, $value, $id ); } /** * Creates a number input. * * @param string $name The name of the number input. * @param string $value The value of the number input. * @param string $id The ID of the number input. * * @return string The number input's HTML output. */ public function number( $name, $value, $id ) { return $this->input( 'number', $name, $value, $id, 'min="0" step="1"' ); } } admin/options-page.php 0000755 00000010540 14717635165 0010774 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Admin; use Yoast\WP\Duplicate_Post\UI\Asset_Manager; use Yoast\WP\Duplicate_Post\Utils; /** * Class Options_Page. */ class Options_Page { /** * The Options instance. * * @var Options */ protected $options; /** * The Options_Form_Generator instance. * * @var Options_Form_Generator */ protected $generator; /** * Holds the asset manager. * * @var Asset_Manager */ protected $asset_manager; /** * Options_Page constructor. * * @param Options $options The Options class instance. * @param Options_Form_Generator $generator The Options_Form_Generator class instance. * @param Asset_Manager $asset_manager The Asset_Manager class instance. */ public function __construct( Options $options, Options_Form_Generator $generator, Asset_Manager $asset_manager ) { $this->options = $options; $this->generator = $generator; $this->asset_manager = $asset_manager; } /** * Registers the necessary hooks. * * @return void */ public function register_hooks() { if ( \is_admin() ) { \add_action( 'admin_menu', [ $this, 'register_menu' ] ); \add_action( 'admin_init', [ $this->options, 'register_settings' ] ); } } /** * Enqueues the assets. * * @return void */ public function enqueue_assets() { $this->asset_manager->enqueue_options_styles(); $this->asset_manager->enqueue_options_script(); } /** * Registers the menu item. * * @return void */ public function register_menu() { $page_hook = \add_options_page( \__( 'Duplicate Post Options', 'duplicate-post' ), \__( 'Duplicate Post', 'duplicate-post' ), 'manage_options', 'duplicatepost', [ $this, 'generate_page' ] ); \add_action( $page_hook, [ $this, 'enqueue_assets' ] ); } /** * Generates the inputs for the specified tab / fieldset. * * @codeCoverageIgnore As this is a simple wrapper for two functions that are already tested elsewhere, we can skip testing. * * @param string $tab The tab to get the configuration for. * @param string $fieldset The fieldset to get the configuration for. Optional. * * @return string The HTML output for the controls present on the tab / fieldset. */ public function generate_tab_inputs( $tab, $fieldset = '' ) { $options = $this->options->get_options_for_tab( $tab, $fieldset ); return $this->generator->generate_options_input( $options ); } /** * Generates an input for a single option. * * @codeCoverageIgnore As this is a simple wrapper for two functions that are already tested elsewhere, we can skip testing. * * @param string $option The option configuration to base the input on. * * @return string The input HTML. */ public function generate_input( $option ) { return $this->generator->generate_options_input( $this->options->get_option( $option ) ); } /** * Registers the proper capabilities. * * @return void */ public function register_capabilities() { if ( ! \current_user_can( 'promote_users' ) || ! $this->settings_updated() ) { return; } $roles = $this->get_duplicate_post_roles(); foreach ( Utils::get_roles() as $name => $display_name ) { $role = \get_role( $name ); if ( ! $role->has_cap( 'copy_posts' ) && \in_array( $name, $roles, true ) ) { /* If the role doesn't have the capability and it was selected, add it. */ $role->add_cap( 'copy_posts' ); } if ( $role->has_cap( 'copy_posts' ) && ! \in_array( $name, $roles, true ) ) { /* If the role has the capability and it wasn't selected, remove it. */ $role->remove_cap( 'copy_posts' ); } } } /** * Generates the options page. * * @codeCoverageIgnore * * @return void */ public function generate_page() { $this->register_capabilities(); require_once \DUPLICATE_POST_PATH . 'src/admin/views/options.php'; } /** * Checks whether settings have been updated. * * @return bool Whether or not the settings have been updated. */ protected function settings_updated() { return isset( $_GET['settings-updated'] ) && $_GET['settings-updated'] === 'true'; } /** * Gets the registered custom roles. * * @return array The roles. Returns an empty array if there are none. */ protected function get_duplicate_post_roles() { $roles = \get_option( 'duplicate_post_roles' ); if ( empty( $roles ) ) { $roles = []; } return $roles; } } admin/views/options.php 0000755 00000021040 14717635165 0011214 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Admin\Views; if ( ! \defined( 'DUPLICATE_POST_CURRENT_VERSION' ) ) { \header( 'Status: 403 Forbidden' ); \header( 'HTTP/1.1 403 Forbidden' ); exit(); } ?> <div class="wrap"> <h1> <?php \esc_html_e( 'Duplicate Post Options', 'duplicate-post' ); ?> </h1> <form id="duplicate_post_settings_form" method="post" action="options.php" style="clear: both"> <?php \settings_fields( 'duplicate_post_group' ); ?> <header role="tablist" aria-label="<?php \esc_attr_e( 'Settings sections', 'duplicate-post' ); ?>" class="nav-tab-wrapper"> <button type="button" role="tab" class="nav-tab nav-tab-active" aria-selected="true" aria-controls="what-tab" id="what"><?php \esc_html_e( 'What to copy', 'duplicate-post' ); ?> </button> <button type="button" role="tab" class="nav-tab" aria-selected="false" aria-controls="who-tab" id="who" tabindex="-1"><?php \esc_html_e( 'Permissions', 'duplicate-post' ); ?> </button> <button type="button" role="tab" class="nav-tab" aria-selected="false" aria-controls="where-tab" id="where" tabindex="-1"><?php \esc_html_e( 'Display', 'duplicate-post' ); ?> </button> </header> <section tabindex="0" role="tabpanel" id="what-tab" aria-labelledby="what"> <h2 class="hide-if-js"><?php \esc_html_e( 'What to copy', 'duplicate-post' ); ?></h2> <table class="form-table" role="presentation"> <tr> <th scope="row"><?php \esc_html_e( 'Post/page elements to copy', 'duplicate-post' ); ?></th> <td> <fieldset> <legend class="screen-reader-text"><?php \esc_html_e( 'Post/page elements to copy', 'duplicate-post' ); ?></legend> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. echo $this->generate_tab_inputs( 'what-to-copy', 'elements-to-copy' ); ?> </fieldset> </td> </tr> <tr> <th scope="row"> <label for="duplicate-post-title-prefix"><?php \esc_html_e( 'Title prefix', 'duplicate-post' ); ?></label> </th> <td> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. echo $this->generate_input( 'duplicate_post_title_prefix' ); ?> </td> </tr> <tr> <th scope="row"> <label for="duplicate-post-title-suffix"><?php \esc_html_e( 'Title suffix', 'duplicate-post' ); ?></label> </th> <td> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. echo $this->generate_input( 'duplicate_post_title_suffix' ); ?> </td> </tr> <tr> <th scope="row"> <label for="duplicate-post-increase-menu-order-by"><?php \esc_html_e( 'Increase menu order by', 'duplicate-post' ); ?></label> </th> <td> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. echo $this->generate_input( 'duplicate_post_increase_menu_order_by' ); ?> </td> </tr> <tr> <th scope="row"> <label for="duplicate-post-blacklist"><?php \esc_html_e( 'Do not copy these fields', 'duplicate-post' ); ?></label> </th> <td id="textfield"> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. echo $this->generate_input( 'duplicate_post_blacklist' ); ?> </td> </tr> <tr> <th scope="row"> <?php \esc_html_e( 'Do not copy these taxonomies', 'duplicate-post' ); ?><br/> </th> <td> <fieldset> <legend class="screen-reader-text"><?php \esc_html_e( 'Do not copy these taxonomies', 'duplicate-post' ); ?></legend> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. echo $this->generate_input( 'duplicate_post_taxonomies_blacklist' ); ?> <button type="button" class="button-link hide-if-no-js toggle-private-taxonomies" aria-expanded="false"> <?php \esc_html_e( 'Show/hide private taxonomies', 'duplicate-post' ); ?> </button> </fieldset> </td> </tr> </table> </section> <section tabindex="0" role="tabpanel" id="who-tab" aria-labelledby="who" hidden="hidden"> <h2 class="hide-if-js"><?php \esc_html_e( 'Permissions', 'duplicate-post' ); ?></h2> <table class="form-table" role="presentation"> <?php if ( \current_user_can( 'promote_users' ) ) { ?> <tr> <th scope="row"><?php \esc_html_e( 'Roles allowed to copy', 'duplicate-post' ); ?></th> <td> <fieldset> <legend class="screen-reader-text"><?php \esc_html_e( 'Roles allowed to copy', 'duplicate-post' ); ?></legend> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. echo $this->generate_input( 'duplicate_post_roles' ); ?> <p> <?php \esc_html_e( 'Warning: users will be able to copy, rewrite and republish all posts, even those of other users.', 'duplicate-post' ); ?> <br/> <?php \esc_html_e( 'Passwords and contents of password-protected posts may become visible to undesired users and visitors.', 'duplicate-post' ); ?> </p> </fieldset> </td> </tr> <?php } ?> <tr> <th scope="row"><?php \esc_html_e( 'Enable for these post types', 'duplicate-post' ); ?> </th> <td> <fieldset> <legend class="screen-reader-text"><?php \esc_html_e( 'Enable for these post types', 'duplicate-post' ); ?></legend> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. echo $this->generate_input( 'duplicate_post_types_enabled' ); ?> <p> <?php \esc_html_e( 'Select the post types you want the plugin to be enabled for.', 'duplicate-post' ); ?> <br/> <?php \esc_html_e( 'Whether the links are displayed for custom post types registered by themes or plugins depends on their use of standard WordPress UI elements.', 'duplicate-post' ); ?> </p> </fieldset> </td> </tr> </table> </section> <section tabindex="0" role="tabpanel" id="where-tab" aria-labelledby="where" hidden="hidden"> <h2 class="hide-if-js"><?php \esc_html_e( 'Display', 'duplicate-post' ); ?></h2> <table class="form-table" role="presentation"> <tr> <th scope="row"><?php \esc_html_e( 'Show these links', 'duplicate-post' ); ?></th> <td> <fieldset> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. echo $this->generate_tab_inputs( 'display', 'show-links' ); ?> </fieldset> </td> </tr> <tr> <th scope="row"><?php \esc_html_e( 'Show links in', 'duplicate-post' ); ?></th> <td> <fieldset> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. echo $this->generate_tab_inputs( 'display', 'show-links-in' ); ?> </fieldset> <p> <?php \esc_html_e( 'Whether the links are displayed for custom post types registered by themes or plugins depends on their use of standard WordPress UI elements.', 'duplicate-post' ); ?> <br/> <?php \printf( /* translators: 1: Code start tag, 2: Code closing tag, 3: Link start tag to the template tag documentation, 4: Link closing tag. */ \esc_html__( 'You can also use the template tag %1$sduplicate_post_clone_post_link( $link, $before, $after, $id )%2$s. %3$sMore info on the template tag%4$s.', 'duplicate-post' ), '<code>', '</code>', '<a href="' . \esc_url( 'https://developer.yoast.com/duplicate-post/functions-template-tags#duplicate_post_clone_post_link' ) . '">', '</a>' ); ?> </p> </td> </tr> <tr> <th scope="row"><?php \esc_html_e( 'Show original item:', 'duplicate-post' ); ?></th> <td> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. echo $this->generate_tab_inputs( 'display', 'show-original' ); ?> </td> </tr> <tr> <th scope="row"><?php \esc_html_e( 'Welcome notice', 'duplicate-post' ); ?></th> <td> <?php // phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly. echo $this->generate_input( 'duplicate_post_show_notice' ); ?> </td> </tr> </table> </section> <p class="submit"> <input type="submit" class="button button-primary" value="<?php \esc_html_e( 'Save changes', 'duplicate-post' ); ?>"/> </p> </form> </div> admin/options.php 0000755 00000024127 14717635165 0010070 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Admin; /** * Options class. * * @since 4.0 */ class Options { /** * Registers the settings. * * @return void */ public function register_settings() { foreach ( \array_keys( $this->get_options() ) as $option ) { \register_setting( 'duplicate_post_group', $option ); } } /** * Gets the options for the specified tab. * * Also allows filtering on a particular fieldset. * * @param string $tab The tab to get the options for. * @param string $fieldset The fieldset to get the options for. Optional. * * @return array The options for the specified tab. */ public function get_options_for_tab( $tab, $fieldset = '' ) { $options = $this->get_options(); $options = \array_filter( $options, static function ( $option ) use ( $tab ) { return \array_key_exists( 'tab', $option ) && $option['tab'] === $tab; } ); if ( empty( $options ) ) { return []; } // If a fieldset is specified, filter out the corresponding options. if ( ! empty( $fieldset ) ) { $options = \array_filter( $options, static function ( $option ) use ( $fieldset ) { return \array_key_exists( 'fieldset', $option ) && $option['fieldset'] === $fieldset; } ); } return $options; } /** * Gets an option from the options array, based on its name. * * @param string $name The name of the option to retrieve. * * @return array The option. Empty array if it does not exist. */ public function get_option( $name ) { $options = $this->get_options(); return \array_key_exists( $name, $options ) ? [ $name => $options[ $name ] ] : []; } /** * Gets the list of registered options. * * @codeCoverageIgnore * * @return array The options. */ public function get_options() { return [ 'duplicate_post_copytitle' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Title', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_copydate' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Date', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_copystatus' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Status', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_copyslug' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Slug', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_copyexcerpt' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Excerpt', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_copycontent' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Content', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_copythumbnail' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Featured Image', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_copytemplate' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Template', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_copyformat' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Post format', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_copyauthor' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Author', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_copypassword' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Password', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_copyattachments' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Attachments', 'duplicate-post' ), 'value' => 1, 'description' => \__( 'you probably want this unchecked, unless you have very special requirements', 'duplicate-post' ), ], 'duplicate_post_copychildren' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Children', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_copycomments' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Comments', 'duplicate-post' ), 'value' => 1, 'description' => \__( 'except pingbacks and trackbacks', 'duplicate-post' ), ], 'duplicate_post_copymenuorder' => [ 'tab' => 'what-to-copy', 'fieldset' => 'elements-to-copy', 'type' => 'checkbox', 'label' => \__( 'Menu order', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_title_prefix' => [ 'tab' => 'what-to-copy', 'type' => 'text', 'label' => \__( 'Title prefix', 'duplicate-post' ), 'value' => \get_option( 'duplicate_post_title_prefix' ), 'description' => [ \__( 'Prefix to be added before the title, e.g. "Copy of" (blank for no prefix)', 'duplicate-post' ) ], ], 'duplicate_post_title_suffix' => [ 'tab' => 'what-to-copy', 'type' => 'text', 'label' => \__( 'Title suffix', 'duplicate-post' ), 'value' => \get_option( 'duplicate_post_title_suffix' ), 'description' => [ \__( 'Suffix to be added after the title, e.g. "(dup)" (blank for no suffix)', 'duplicate-post' ) ], ], 'duplicate_post_increase_menu_order_by' => [ 'tab' => 'what-to-copy', 'type' => 'number', 'label' => \__( 'Increase menu order by', 'duplicate-post' ), 'value' => \get_option( 'duplicate_post_increase_menu_order_by' ), 'description' => [ \__( 'Add this number to the original menu order (blank or zero to retain the value)', 'duplicate-post' ) ], ], 'duplicate_post_blacklist' => [ 'tab' => 'what-to-copy', 'type' => 'text', 'label' => \__( 'Do not copy these fields', 'duplicate-post' ), 'value' => \get_option( 'duplicate_post_blacklist' ), 'description' => [ \__( 'Comma-separated list of meta fields that must not be copied.', 'duplicate-post' ), \__( 'You can use * to match zero or more alphanumeric characters or underscores: e.g. field*', 'duplicate-post' ), ], ], 'duplicate_post_taxonomies_blacklist' => [ 'tab' => 'what-to-copy', 'callback' => 'generate_taxonomy_exclusion_list', ], 'duplicate_post_roles' => [ 'tab' => 'permissions', 'callback' => 'generate_roles_permission_list', ], 'duplicate_post_types_enabled' => [ 'tab' => 'permissions', 'callback' => 'generate_post_types_list', ], 'duplicate_post_show_original_meta_box' => [ 'tab' => 'display', 'fieldset' => 'show-original', 'type' => 'checkbox', 'label' => \__( 'In a metabox in the Edit screen', 'duplicate-post' ), 'value' => 1, 'description' => [ \__( "You'll also be able to delete the reference to the original item with a checkbox", 'duplicate-post' ), ], ], 'duplicate_post_show_original_column' => [ 'tab' => 'display', 'fieldset' => 'show-original', 'type' => 'checkbox', 'label' => \__( 'In a column in the Post list', 'duplicate-post' ), 'value' => 1, 'description' => [ \__( "You'll also be able to delete the reference to the original item with a checkbox in Quick Edit", 'duplicate-post' ), ], ], 'duplicate_post_show_original_in_post_states' => [ 'tab' => 'display', 'fieldset' => 'show-original', 'type' => 'checkbox', 'label' => \__( 'After the title in the Post list', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_show_notice' => [ 'tab' => 'display', 'type' => 'checkbox', 'label' => \__( 'Show welcome notice', 'duplicate-post' ), 'value' => 1, ], 'duplicate_post_show_link' => [ 'tab' => 'display', 'fieldset' => 'show-links', 'sub_options' => [ 'new_draft' => [ 'type' => 'checkbox', 'label' => \__( 'New Draft', 'duplicate-post' ), 'value' => 1, ], 'clone' => [ 'type' => 'checkbox', 'label' => \__( 'Clone', 'duplicate-post' ), 'value' => 1, ], 'rewrite_republish' => [ 'type' => 'checkbox', 'label' => \__( 'Rewrite & Republish', 'duplicate-post' ), 'value' => 1, ], ], ], 'duplicate_post_show_link_in' => [ 'tab' => 'display', 'fieldset' => 'show-links-in', 'sub_options' => [ 'row' => [ 'type' => 'checkbox', 'label' => \__( 'Post list', 'duplicate-post' ), 'value' => 1, ], 'adminbar' => [ 'type' => 'checkbox', 'label' => \__( 'Admin bar', 'duplicate-post' ), 'value' => 1, ], 'submitbox' => [ 'type' => 'checkbox', 'label' => \__( 'Edit screen', 'duplicate-post' ), 'value' => 1, ], 'bulkactions' => [ 'type' => 'checkbox', 'label' => \__( 'Bulk Actions', 'duplicate-post' ), 'value' => 1, ], ], ], ]; } } admin/options-form-generator.php 0000755 00000022652 14717635165 0013016 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post\Admin; use WP_Taxonomy; use Yoast\WP\Duplicate_Post\Utils; /** * Class Options_Form_Generator. */ class Options_Form_Generator { /** * The Options_Inputs instance. * * @var Options_Inputs */ protected $options_inputs; /** * Options_Form_Generator constructor. * * @param Options_Inputs $inputs The Options_Inputs instance. */ public function __construct( Options_Inputs $inputs ) { $this->options_inputs = $inputs; } /** * Generates the HTML output of an input control, based on the passed options. * * @param array $options The options to base the input on. * @param string $parent_option The parent option, used for grouped inputs. Optional. * * @return string The HTML output. */ public function generate_options_input( array $options, $parent_option = '' ) { $output = ''; foreach ( $options as $option => $option_values ) { // Skip empty options. if ( empty( $option_values ) ) { continue; } // Check for support of the current WordPress version. if ( \array_key_exists( 'version', $option_values ) && \version_compare( \get_bloginfo( 'version' ), $option_values['version'] ) < 0 ) { continue; } if ( \array_key_exists( 'sub_options', $option_values ) ) { $output .= $this->generate_options_input( $option_values['sub_options'], $option ); continue; } // If callback, call it. if ( \array_key_exists( 'callback', $option_values ) ) { $output .= $this->{$option_values['callback']}(); continue; } if ( ! \array_key_exists( 'type', $option_values ) ) { continue; } $id = ( \array_key_exists( 'id', $option_values ) ? $option_values['id'] : $this->prepare_input_id( $option ) ); if ( $parent_option !== '' ) { $id = \sprintf( '%s-%s', $this->prepare_input_id( $parent_option ), $id ); $option = \sprintf( '%s[%s]', $parent_option, $option ); } switch ( $option_values['type'] ) { case 'checkbox': $output .= $this->options_inputs->checkbox( $option, $option_values['value'], $id, $this->is_checked( $option, $option_values, $parent_option ) ); $output .= \sprintf( '<label for="%s">%s</label>', $id, \esc_html( $option_values['label'] ) ); break; case 'text': $output .= $this->options_inputs->text( $option, $option_values['value'], $id ); break; case 'number': $output .= $this->options_inputs->number( $option, $option_values['value'], $id ); break; } if ( \array_key_exists( 'description', $option_values ) ) { $output .= ' ' . $this->extract_description( $option_values['description'], $id ); } $output .= '<br />'; } return $output; } /** * Sorts taxonomy objects based on being public, followed by being private * and when the visibility is equal, on the taxonomy public name (case-sensitive). * * @param WP_Taxonomy $taxonomy1 First taxonomy object. * @param WP_Taxonomy $taxonomy2 Second taxonomy object. * * @return int An integer less than, equal to, or greater than zero indicating respectively * the first taxonomy should be sorted before, at the same level or after the second taxonomy. */ public function sort_taxonomy_objects( $taxonomy1, $taxonomy2 ) { if ( $taxonomy1->public === true && $taxonomy2->public === false ) { return -1; } elseif ( $taxonomy1->public === false && $taxonomy2->public === true ) { return 1; } // Same visibility, sort by name. return \strnatcmp( $taxonomy1->labels->name, $taxonomy2->labels->name ); } /** * Extracts and formats the description associated with the input field. * * @param string|array $description The description string. Can be an array of strings. * @param string $id The ID of the input field. * * @return string The description HTML for the input. */ public function extract_description( $description, $id ) { if ( ! \is_array( $description ) ) { return \sprintf( '<span id="%s-description">(%s)</span>', $id, \esc_html( $description ) ); } return \sprintf( '<p id="%s-description">%s</p>', $id, \implode( '<br />', \array_map( '\esc_html', $description ) ) ); } /** * Generates a list of checkboxes for registered taxonomies. * * @return string The generated taxonomies list. */ public function generate_taxonomy_exclusion_list() { $taxonomies = \get_taxonomies( [], 'objects' ); \usort( $taxonomies, [ $this, 'sort_taxonomy_objects' ] ); $taxonomies_blacklist = \get_option( 'duplicate_post_taxonomies_blacklist' ); if ( ! \is_array( $taxonomies_blacklist ) ) { $taxonomies_blacklist = []; } $output = ''; foreach ( $taxonomies as $taxonomy ) { if ( $taxonomy->name === 'post_format' ) { continue; } $is_public = ( $taxonomy->public ) ? 'public' : 'private'; $name = \esc_attr( $taxonomy->name ); $output .= \sprintf( '<div class="taxonomy_%s">', $is_public ); $output .= $this->generate_options_input( [ 'duplicate_post_taxonomies_blacklist[]' => [ 'type' => 'checkbox', 'id' => 'duplicate-post-' . $this->prepare_input_id( $name ), 'value' => $name, 'checked' => \in_array( $taxonomy->name, $taxonomies_blacklist, true ), 'label' => $taxonomy->labels->name . ' [' . $taxonomy->name . ']', ], ] ); $output .= '</div>'; } return $output; } /** * Generates a list of checkboxes for registered roles. * * @return string The generated roles list. */ public function generate_roles_permission_list() { $post_types = \get_post_types( [ 'show_ui' => true ], 'objects' ); $edit_capabilities = [ 'edit_posts' => true ]; foreach ( $post_types as $post_type ) { $edit_capabilities[ $post_type->cap->edit_posts ] = true; } $output = ''; foreach ( Utils::get_roles() as $name => $display_name ) { $role = \get_role( $name ); if ( \count( \array_intersect_key( $role->capabilities, $edit_capabilities ) ) > 0 ) { $output .= $this->generate_options_input( [ 'duplicate_post_roles[]' => [ 'type' => 'checkbox', 'id' => 'duplicate-post-' . $this->prepare_input_id( $name ), 'value' => $name, 'checked' => $role->has_cap( 'copy_posts' ), 'label' => \translate_user_role( $display_name ), ], ] ); } } return $output; } /** * Generates a list of checkboxes for registered post types. * * @return string The generated post types list. */ public function generate_post_types_list() { $post_types = \get_post_types( [ 'show_ui' => true ], 'objects' ); $hidden_post_types = $this->get_hidden_post_types(); $output = ''; foreach ( $post_types as $post_type_object ) { if ( \in_array( $post_type_object->name, $hidden_post_types, true ) ) { continue; } $name = \esc_attr( $post_type_object->name ); $output .= $this->generate_options_input( [ 'duplicate_post_types_enabled[]' => [ 'type' => 'checkbox', 'id' => 'duplicate-post-' . $this->prepare_input_id( $name ), 'value' => $name, 'checked' => $this->is_post_type_enabled( $post_type_object->name ), 'label' => $post_type_object->labels->name, ], ] ); } return $output; } /** * Determines whether the passed option should result in a checked checkbox or not. * * @param string $option The option to search for. * @param array $option_values The option's values. * @param string $parent_option The parent option. Optional. * * @return bool Whether or not the checkbox should be checked. */ public function is_checked( $option, $option_values, $parent_option = '' ) { if ( \array_key_exists( 'checked', $option_values ) ) { return $option_values['checked']; } // Check for serialized options. $saved_option = ! empty( $parent_option ) ? \get_option( $parent_option ) : \get_option( $option ); if ( ! \is_array( $saved_option ) ) { return (int) $saved_option === 1; } // Clean up the sub-option's name. $cleaned_option = \trim( \str_replace( $parent_option, '', $option ), '[]' ); return \array_key_exists( $cleaned_option, $saved_option ) && (int) $saved_option[ $cleaned_option ] === 1; } /** * Prepares the passed ID so it's properly formatted. * * @param string $id The ID to prepare. * * @return string The prepared input ID. */ public function prepare_input_id( $id ) { return \str_replace( '_', '-', $id ); } /** * Checks whether or not a post type is enabled. * * @codeCoverageIgnore As this is a simple wrapper for a function that is also being used elsewhere, we can skip testing for now. * * @param string $post_type The post type. * * @return bool Whether or not the post type is enabled. */ public function is_post_type_enabled( $post_type ) { $duplicate_post_types_enabled = \get_option( 'duplicate_post_types_enabled', [ 'post', 'page' ] ); if ( ! \is_array( $duplicate_post_types_enabled ) ) { $duplicate_post_types_enabled = [ $duplicate_post_types_enabled ]; } return \in_array( $post_type, $duplicate_post_types_enabled, true ); } /** * Generates a list of post types that should be hidden from the options page. * * @return array The array of names of the post types to hide. */ public function get_hidden_post_types() { $hidden_post_types = [ 'attachment', 'wp_block', ]; if ( Utils::is_plugin_active( 'woocommerce/woocommerce.php' ) ) { $hidden_post_types[] = 'product'; } return $hidden_post_types; } } post-republisher.php 0000755 00000031022 14717635165 0010604 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post; use WP_Post; /** * Duplicate Post class to republish a rewritten post. * * @since 4.0 */ class Post_Republisher { /** * Post_Duplicator object. * * @var Post_Duplicator */ protected $post_duplicator; /** * Holds the permissions helper. * * @var Permissions_Helper */ protected $permissions_helper; /** * Initializes the class. * * @param Post_Duplicator $post_duplicator The Post_Duplicator object. * @param Permissions_Helper $permissions_helper The Permissions Helper object. */ public function __construct( Post_Duplicator $post_duplicator, Permissions_Helper $permissions_helper ) { $this->post_duplicator = $post_duplicator; $this->permissions_helper = $permissions_helper; } /** * Adds hooks to integrate with WordPress. * * @return void */ public function register_hooks() { \add_action( 'init', [ $this, 'register_post_statuses' ] ); \add_filter( 'wp_insert_post_data', [ $this, 'change_post_copy_status' ], 1, 2 ); $enabled_post_types = $this->permissions_helper->get_enabled_post_types(); foreach ( $enabled_post_types as $enabled_post_type ) { /** * Called in the REST API when submitting the post copy in the Block Editor. * Runs the republishing of the copy onto the original. */ \add_action( "rest_after_insert_{$enabled_post_type}", [ $this, 'republish_after_rest_api_request' ] ); } /** * Called by `wp_insert_post()` when submitting the post copy, which runs in two cases: * - In the Classic Editor, where there's only one request that updates everything. * - In the Block Editor, only when there are custom meta boxes. */ \add_action( 'wp_insert_post', [ $this, 'republish_after_post_request' ], \PHP_INT_MAX, 2 ); // Clean up after the redirect to the original post. \add_action( 'load-post.php', [ $this, 'clean_up_after_redirect' ] ); // Clean up the original when the copy is manually deleted from the trash. \add_action( 'before_delete_post', [ $this, 'clean_up_when_copy_manually_deleted' ] ); // Ensure scheduled Rewrite and Republish posts are properly handled. \add_action( 'future_to_publish', [ $this, 'republish_scheduled_post' ] ); } /** * Adds custom post statuses. * * These post statuses are meant for internal use. However, we can't use the * `internal` status because the REST API posts controller allows all registered * statuses but the `internal` one. * * @return void */ public function register_post_statuses() { $options = [ 'label' => \__( 'Republish', 'duplicate-post' ), 'exclude_from_search' => false, 'show_in_admin_all_list' => false, 'show_in_admin_status_list' => false, ]; \register_post_status( 'dp-rewrite-republish', $options ); } /** * Changes the post copy status. * * Runs on the `wp_insert_post_data` hook in `wp_insert_post()` when * submitting the post copy. * * @param array $data An array of slashed, sanitized, and processed post data. * @param array $postarr An array of sanitized (and slashed) but otherwise unmodified post data. * * @return array An array of slashed, sanitized, and processed attachment post data. */ public function change_post_copy_status( $data, $postarr ) { if ( ! \array_key_exists( 'ID', $postarr ) || empty( $postarr['ID'] ) ) { return $data; } $post = \get_post( $postarr['ID'] ); if ( ! $post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { return $data; } if ( $data['post_status'] === 'publish' ) { $data['post_status'] = 'dp-rewrite-republish'; } return $data; } /** * Executes the republish request. * * @param WP_Post $post The copy's post object. * * @return void */ public function republish_request( $post ) { if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) || ! $this->permissions_helper->is_copy_allowed_to_be_republished( $post ) ) { return; } $original_post = Utils::get_original( $post->ID ); if ( ! $original_post ) { return; } $this->republish( $post, $original_post ); // Trigger the redirect in the Classic Editor. if ( $this->is_classic_editor_post_request() ) { $this->redirect( $original_post->ID, $post->ID ); } } /** * Republishes the original post with the passed post, when using the Block Editor. * * @param WP_Post $post The copy's post object. * * @return void */ public function republish_after_rest_api_request( $post ) { $this->republish_request( $post ); } /** * Republishes the original post with the passed post, when using the Classic Editor. * * Runs also in the Block Editor to save the custom meta data only when there * are custom meta boxes. * * @param int $post_id The copy's post ID. * @param WP_Post $post The copy's post object. * * @return void */ public function republish_after_post_request( $post_id, $post ) { if ( $this->is_rest_request() ) { return; } $this->republish_request( $post ); } /** * Republishes the scheduled Rewrited and Republish post. * * @param WP_Post $copy The scheduled copy. * * @return void */ public function republish_scheduled_post( $copy ) { if ( ! $copy instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $copy ) ) { return; } $original_post = Utils::get_original( $copy->ID ); // If the original post was permanently deleted, we don't want to republish, so trash instead. if ( ! $original_post ) { $this->delete_copy( $copy->ID, null, false ); return; } \kses_remove_filters(); $this->republish( $copy, $original_post ); \kses_init_filters(); $this->delete_copy( $copy->ID, $original_post->ID ); } /** * Cleans up the copied post and temporary metadata after the user has been redirected. * * @return void */ public function clean_up_after_redirect() { if ( ! empty( $_GET['dprepublished'] ) && ! empty( $_GET['dpcopy'] ) && ! empty( $_GET['post'] ) ) { $copy_id = \intval( \wp_unslash( $_GET['dpcopy'] ) ); $post_id = \intval( \wp_unslash( $_GET['post'] ) ); \check_admin_referer( 'dp-republish', 'dpnonce' ); if ( \intval( \get_post_meta( $copy_id, '_dp_has_been_republished', true ) ) === 1 ) { $this->delete_copy( $copy_id, $post_id ); } else { \wp_die( \esc_html__( 'An error occurred while deleting the Rewrite & Republish copy.', 'duplicate-post' ) ); } } } /** * Checks whether a request is the Classic Editor POST request. * * @return bool Whether the request is the Classic Editor POST request. */ public function is_classic_editor_post_request() { if ( $this->is_rest_request() || \wp_doing_ajax() ) { return false; } return isset( $_GET['meta-box-loader'] ) === false; } /** * Determines whether the current request is a REST request. * * @return bool Whether or not the request is a REST request. */ public function is_rest_request() { return \defined( 'REST_REQUEST' ) && \REST_REQUEST; } /** * Republishes the post by overwriting the original post. * * @param WP_Post $post The Rewrite & Republish copy. * @param WP_Post $original_post The original post. * * @return void */ public function republish( WP_Post $post, WP_Post $original_post ) { // Remove WordPress default filter so a new revision is not created on republish. \remove_action( 'post_updated', 'wp_save_post_revision', 10 ); // Republish taxonomies and meta. $this->republish_post_taxonomies( $post ); $this->republish_post_meta( $post ); // Republish the post. $this->republish_post_elements( $post, $original_post ); // Mark the copy as already published. \update_post_meta( $post->ID, '_dp_has_been_republished', '1' ); // Re-enable the creation of a new revision. \add_action( 'post_updated', 'wp_save_post_revision', 10, 1 ); } /** * Deletes the copy and associated post meta, if applicable. * * @param int $copy_id The copy's ID. * @param int|null $post_id The original post's ID. Optional. * @param bool $permanently_delete Whether to permanently delete the copy. Defaults to true. * * @return void */ public function delete_copy( $copy_id, $post_id = null, $permanently_delete = true ) { /** * Fires before deleting a Rewrite & Republish copy. * * @param int $copy_id The copy's ID. * @param int $post_id The original post's ID.. */ \do_action( 'duplicate_post_after_rewriting', $copy_id, $post_id ); // Delete the copy bypassing the trash so it also deletes the copy post meta. \wp_delete_post( $copy_id, $permanently_delete ); if ( ! \is_null( $post_id ) ) { // Delete the meta that marks the original post has having a copy. \delete_post_meta( $post_id, '_dp_has_rewrite_republish_copy' ); } } /** * Republishes the post elements overwriting the original post. * * @param WP_Post $post The post object. * @param WP_Post $original_post The original post. * * @return void */ protected function republish_post_elements( $post, $original_post ) { // Cast to array and not alter the copy's original object. $post_to_be_rewritten = clone $post; // Prepare post data for republishing. $post_to_be_rewritten->ID = $original_post->ID; $post_to_be_rewritten->post_name = $original_post->post_name; $post_to_be_rewritten->post_status = $this->determine_post_status( $post, $original_post ); /** * Yoast SEO and other plugins prevent from accidentally updating another post's * data (e.g. the Yoast SEO metadata by checking the $_POST data ID with the post object ID. * We need to overwrite the $_POST data ID to allow updating the original post. */ $_POST['ID'] = $original_post->ID; // Republish the original post. $rewritten_post_id = \wp_update_post( $post_to_be_rewritten ); if ( $rewritten_post_id === 0 ) { \wp_die( \esc_html__( 'An error occurred while republishing the post.', 'duplicate-post' ) ); } } /** * Republishes the post taxonomies overwriting the ones of the original post. * * @param WP_Post $post The copy's post object. * * @return void */ protected function republish_post_taxonomies( $post ) { $original_post_id = Utils::get_original_post_id( $post->ID ); $copy_taxonomies_options = [ 'taxonomies_excludelist' => [], 'use_filters' => false, 'copy_format' => true, ]; $this->post_duplicator->copy_post_taxonomies( $original_post_id, $post, $copy_taxonomies_options ); } /** * Republishes the post meta overwriting the ones of the original post. * * @param WP_Post $post The copy's post object. * * @return void */ protected function republish_post_meta( $post ) { $original_post_id = Utils::get_original_post_id( $post->ID ); $copy_meta_options = [ 'meta_excludelist' => Utils::get_default_filtered_meta_names(), 'use_filters' => false, 'copy_thumbnail' => true, 'copy_template' => true, ]; $this->post_duplicator->copy_post_meta_info( $original_post_id, $post, $copy_meta_options ); } /** * Redirects the user to the original post. * * @param int $original_post_id The ID of the original post to redirect to. * @param int $copy_id The ID of the copy post. * * @return void */ protected function redirect( $original_post_id, $copy_id ) { \wp_safe_redirect( \add_query_arg( [ 'dprepublished' => 1, 'dpcopy' => $copy_id, 'dpnonce' => \wp_create_nonce( 'dp-republish' ), ], \admin_url( 'post.php?action=edit&post=' . $original_post_id ) ) ); exit(); } /** * Determines the post status to use when publishing the Rewrite & Republish copy. * * @param WP_Post $post The post object. * @param WP_Post $original_post The original post object. * * @return string The post status to use. */ protected function determine_post_status( $post, $original_post ) { if ( $original_post->post_status === 'trash' ) { return 'trash'; } if ( $post->post_status === 'private' ) { return 'private'; } return 'publish'; } /** * Deletes the original post meta that flags it as having a copy when the copy is manually deleted. * * @param int $post_id Post ID of a post that is going to be deleted. * * @return void */ public function clean_up_when_copy_manually_deleted( $post_id ) { $post = \get_post( $post_id ); if ( ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { return; } $original_post_id = Utils::get_original_post_id( $post_id ); \delete_post_meta( $original_post_id, '_dp_has_rewrite_republish_copy' ); } } duplicate-post.php 0000755 00000003106 14717635165 0010234 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post; use Yoast\WP\Duplicate_Post\Handlers\Handler; use Yoast\WP\Duplicate_Post\UI\User_Interface; use Yoast\WP\Duplicate_Post\Watchers\Watchers; /** * Duplicate Post main class. * * @since 4.0 */ class Duplicate_Post { /** * Permissions_Helper object. * * @var Permissions_Helper */ protected $permissions_helper; /** * User_Interface object. * * @var User_Interface */ protected $user_interface; /** * Post_Duplicator object. * * @var Post_Duplicator */ protected $post_duplicator; /** * Handler object. * * @var Handler */ protected $handler; /** * Post_Republisher object. * * @var Post_Republisher */ protected $post_republisher; /** * Revisions_Migrator object. * * @var Revisions_Migrator */ protected $revisions_migrator; /** * Watchers object. * * @var Watchers */ protected $watchers; /** * Initializes the main class. */ public function __construct() { $this->permissions_helper = new Permissions_Helper(); $this->user_interface = new User_Interface( $this->permissions_helper ); $this->post_duplicator = new Post_Duplicator(); $this->handler = new Handler( $this->post_duplicator, $this->permissions_helper ); $this->post_republisher = new Post_Republisher( $this->post_duplicator, $this->permissions_helper ); $this->revisions_migrator = new Revisions_Migrator(); $this->watchers = new Watchers( $this->permissions_helper ); $this->post_republisher->register_hooks(); $this->revisions_migrator->register_hooks(); } } revisions-migrator.php 0000755 00000003536 14717635165 0011151 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post; /** * Duplicate Post class to migrate revisions from the Rewrite & Republish copy to the original post. * * @since 4.0 */ class Revisions_Migrator { /** * Adds hooks to integrate with the Post Republisher class. * * @return void */ public function register_hooks() { \add_action( 'duplicate_post_after_rewriting', [ $this, 'migrate_revisions' ], 10, 2 ); } /** * Updates the revisions of the Rewrite & Republish copy to make them revisions of the original. * * It mimics the behaviour of wp_save_post_revision() in wp-includes/revision.php * by deleting the revisions (except autosaves) exceeding the maximum allowed number. * * @param int $copy_id The copy's ID. * @param int $original_id The post's ID. * * @return void */ public function migrate_revisions( $copy_id, $original_id ) { $copy = \get_post( $copy_id ); $original_post = \get_post( $original_id ); if ( \is_null( $copy ) || \is_null( $original_post ) || ! \wp_revisions_enabled( $original_post ) ) { return; } $copy_revisions = \wp_get_post_revisions( $copy ); foreach ( $copy_revisions as $revision ) { $revision->post_parent = $original_post->ID; $revision->post_name = "$original_post->ID-revision-v1"; \wp_update_post( $revision ); } $revisions_to_keep = \wp_revisions_to_keep( $original_post ); if ( $revisions_to_keep < 0 ) { return; } $revisions = \wp_get_post_revisions( $original_post, [ 'order' => 'ASC' ] ); $delete = ( \count( $revisions ) - $revisions_to_keep ); if ( $delete < 1 ) { return; } $revisions = \array_slice( $revisions, 0, $delete ); for ( $i = 0; isset( $revisions[ $i ] ); $i++ ) { if ( \strpos( $revisions[ $i ]->post_name, 'autosave' ) !== false ) { continue; } \wp_delete_post_revision( $revisions[ $i ]->ID ); } } } utils.php 0000755 00000014320 14717635165 0006437 0 ustar 00 <?php namespace Yoast\WP\Duplicate_Post; use WP_Post; /** * Utility methods for Duplicate Post. * * @since 4.0 */ class Utils { /** * Flattens a version number for use in a filename. * * @param string $version The original version number. * * @return string The flattened version number. */ public static function flatten_version( $version ) { $parts = \explode( '.', $version ); if ( \count( $parts ) === 2 && \preg_match( '/^\d+$/', $parts[1] ) === 1 ) { $parts[] = '0'; } return \implode( '', $parts ); } /** * Adds slashes only to strings. * * @param mixed $value Value to slash only if string. * * @return string|mixed */ public static function addslashes_to_strings_only( $value ) { return \is_string( $value ) ? \addslashes( $value ) : $value; } /** * Replaces faulty core wp_slash(). * * Until WP 5.5 wp_slash() recursively added slashes not just to strings in array/objects, leading to errors. * * @param mixed $value What to add slashes to. * * @return mixed */ public static function recursively_slash_strings( $value ) { return \map_deep( $value, [ self::class, 'addslashes_to_strings_only' ] ); } /** * Gets the original post. * * @param int|WP_Post|null $post Optional. Post ID or Post object. * @param string $output Optional, default is Object. Either OBJECT, ARRAY_A, or ARRAY_N. * * @return WP_Post|null Post data if successful, null otherwise. */ public static function get_original( $post = null, $output = \OBJECT ) { $post = \get_post( $post ); if ( ! $post ) { return null; } $original_id = self::get_original_post_id( $post->ID ); if ( empty( $original_id ) ) { return null; } return \get_post( $original_id, $output ); } /** * Determines if the post has ancestors marked for copy. * * If we are copying children, and the post has already an ancestor marked for copy, we have to filter it out. * * @param WP_Post $post The post object. * @param array $post_ids The array of marked post IDs. * * @return bool Whether the post has ancestors marked for copy. */ public static function has_ancestors_marked( $post, $post_ids ) { $ancestors_in_array = 0; $parent = \wp_get_post_parent_id( $post->ID ); while ( $parent ) { if ( \in_array( $parent, $post_ids, true ) ) { ++$ancestors_in_array; } $parent = \wp_get_post_parent_id( $parent ); } return ( $ancestors_in_array !== 0 ); } /** * Returns a link to edit, preview or view a post, in accordance to user capabilities. * * @param WP_Post $post Post ID or Post object. * * @return string|null The link to edit, preview or view a post. */ public static function get_edit_or_view_link( $post ) { $post = \get_post( $post ); if ( ! $post ) { return null; } $can_edit_post = \current_user_can( 'edit_post', $post->ID ); $title = \_draft_or_post_title( $post ); $post_type_object = \get_post_type_object( $post->post_type ); if ( $can_edit_post && $post->post_status !== 'trash' ) { return \sprintf( '<a href="%s" aria-label="%s">%s</a>', \esc_url( \get_edit_post_link( $post->ID ) ), /* translators: %s: post title */ \esc_attr( \sprintf( \__( 'Edit “%s”', 'duplicate-post' ), $title ) ), $title ); } elseif ( \is_post_type_viewable( $post_type_object ) ) { if ( \in_array( $post->post_status, [ 'pending', 'draft', 'future' ], true ) ) { if ( $can_edit_post ) { $preview_link = \get_preview_post_link( $post ); return \sprintf( '<a href="%s" rel="bookmark" aria-label="%s">%s</a>', \esc_url( $preview_link ), /* translators: %s: post title */ \esc_attr( \sprintf( \__( 'Preview “%s”', 'duplicate-post' ), $title ) ), $title ); } } elseif ( $post->post_status !== 'trash' ) { return \sprintf( '<a href="%s" rel="bookmark" aria-label="%s">%s</a>', \esc_url( \get_permalink( $post->ID ) ), /* translators: %s: post title */ \esc_attr( \sprintf( \__( 'View “%s”', 'duplicate-post' ), $title ) ), $title ); } } return $title; } /** * Gets the ID of the original post intended to be rewritten with the copy for Rewrite & Republish. * * @param int $post_id The copy post ID. * * @return int The original post id of a copy for Rewrite & Republish. */ public static function get_original_post_id( $post_id ) { return (int) \get_post_meta( $post_id, '_dp_original', true ); } /** * Gets the registered WordPress roles. * * @codeCoverageIgnore As this is a simple wrapper method for a built-in WordPress method, we don't have to test it. * * @return array The roles. */ public static function get_roles() { global $wp_roles; return $wp_roles->get_names(); } /** * Gets the default meta field names to be filtered out. * * @return array The names of the meta fields to filter out by default. */ public static function get_default_filtered_meta_names() { return [ '_edit_lock', '_edit_last', '_dp_original', '_dp_is_rewrite_republish_copy', '_dp_has_rewrite_republish_copy', '_dp_has_been_republished', '_dp_creation_date_gmt', ]; } /** * Gets a Duplicate Post option from the database. * * @param string $option The option to get. * @param string $key The key to retrieve, if the option is an array. * * @return mixed The option. */ public static function get_option( $option, $key = '' ) { $option = \get_option( $option ); if ( ! \is_array( $option ) || empty( $key ) ) { return $option; } if ( ! \array_key_exists( $key, $option ) ) { return ''; } return $option[ $key ]; } /** * Determines if a plugin is active. * * We can't use is_plugin_active because this must work on the frontend too. * * @param string $plugin Path to the plugin file relative to the plugins directory. * * @return bool Whether a plugin is currently active. */ public static function is_plugin_active( $plugin ) { if ( \in_array( $plugin, (array) \get_option( 'active_plugins', [] ), true ) ) { return true; } if ( ! \is_multisite() ) { return false; } $plugins = \get_site_option( 'active_sitewide_plugins' ); return isset( $plugins[ $plugin ] ); } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.3-4ubuntu2.24 | Генерация страницы: 0.02 |
proxy
|
phpinfo
|
Настройка