├── .nvmrc ├── admin-functions.php ├── common-functions.php ├── compat ├── jetpack-functions.php └── wpml-functions.php ├── composer.json ├── css ├── duplicate-post-options.css └── duplicate-post.css ├── duplicate-post.php ├── duplicate_post_yoast_icon-125x125.png ├── gpl-2.0.txt ├── js └── src │ ├── duplicate-post-edit-script.js │ ├── duplicate-post-elementor.js │ ├── duplicate-post-functions.js │ ├── duplicate-post-options.js │ ├── duplicate-post-quick-edit-script.js │ ├── duplicate-post-strings.js │ └── helpers │ └── safe-create-interpolate-element.js ├── options.php ├── readme.txt ├── src ├── admin │ ├── options-form-generator.php │ ├── options-inputs.php │ ├── options-page.php │ ├── options.php │ └── views │ │ └── options.php ├── duplicate-post.php ├── handlers │ ├── bulk-handler.php │ ├── check-changes-handler.php │ ├── handler.php │ ├── link-handler.php │ └── save-post-handler.php ├── permissions-helper.php ├── post-duplicator.php ├── post-republisher.php ├── revisions-migrator.php ├── ui │ ├── admin-bar.php │ ├── asset-manager.php │ ├── block-editor.php │ ├── bulk-actions.php │ ├── classic-editor.php │ ├── column.php │ ├── link-builder.php │ ├── metabox.php │ ├── newsletter.php │ ├── post-states.php │ ├── row-actions.php │ └── user-interface.php ├── utils.php └── watchers │ ├── bulk-actions-watcher.php │ ├── copied-post-watcher.php │ ├── link-actions-watcher.php │ ├── original-post-watcher.php │ ├── republished-post-watcher.php │ └── watchers.php └── yarn.lock /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /common-functions.php: -------------------------------------------------------------------------------- 1 | should_links_be_displayed( $post ) ) { 48 | return ''; 49 | } 50 | 51 | if ( $draft ) { 52 | return $link_builder->build_new_draft_link( $post, $context ); 53 | } 54 | else { 55 | return $link_builder->build_clone_link( $post, $context ); 56 | } 57 | } 58 | 59 | /** 60 | * Displays duplicate post link for post. 61 | * 62 | * @param string|null $link Optional. Anchor text. 63 | * @param string $before Optional. Display before edit link. 64 | * @param string $after Optional. Display after edit link. 65 | * @param int $id Optional. Post ID. 66 | * 67 | * @return void 68 | */ 69 | function duplicate_post_clone_post_link( $link = null, $before = '', $after = '', $id = 0 ) { 70 | $post = get_post( $id ); 71 | if ( ! $post ) { 72 | return; 73 | } 74 | 75 | $url = duplicate_post_get_clone_post_link( $post->ID ); 76 | if ( ! $url ) { 77 | return; 78 | } 79 | 80 | if ( $link === null ) { 81 | $link = __( 'Copy to a new draft', 'duplicate-post' ); 82 | } 83 | 84 | $link = '' . esc_html( $link ) . ''; 85 | 86 | /** 87 | * Filter on the clone link HTML. 88 | * 89 | * @param string $link The full HTML tag of the link. 90 | * @param int $ID The ID of the post. 91 | * 92 | * @return string 93 | */ 94 | echo $before . apply_filters( 'duplicate_post_clone_post_link', $link, $post->ID ) . $after; // phpcs:ignore WordPress.Security.EscapeOutput 95 | } 96 | 97 | /** 98 | * Gets the original post. 99 | * 100 | * @param int|null $post Optional. Post ID or Post object. 101 | * @param string $output Optional, default is Object. Either OBJECT, ARRAY_A, or ARRAY_N. 102 | * @return mixed Post data. 103 | */ 104 | function duplicate_post_get_original( $post = null, $output = OBJECT ) { 105 | return Utils::get_original( $post, $output ); 106 | } 107 | -------------------------------------------------------------------------------- /compat/jetpack-functions.php: -------------------------------------------------------------------------------- 1 | unload_markdown_for_posts(); 49 | } 50 | 51 | /** 52 | * Enaable Markdown. 53 | * 54 | * To be called after copy. 55 | * 56 | * @return void 57 | */ 58 | function duplicate_post_jetpack_enable_markdown() { 59 | WPCom_Markdown::get_instance()->load_markdown_for_posts(); 60 | } 61 | -------------------------------------------------------------------------------- /compat/wpml-functions.php: -------------------------------------------------------------------------------- 1 | get_current_language(); 51 | $trid = $sitepress->get_element_trid( $post->ID ); 52 | if ( ! empty( $trid ) ) { 53 | $translations = $sitepress->get_element_translations( $trid ); 54 | $new_trid = $sitepress->get_element_trid( $post_id ); 55 | foreach ( $translations as $code => $details ) { 56 | if ( $code !== $current_language ) { 57 | if ( $details->element_id ) { 58 | $translation = get_post( $details->element_id ); 59 | if ( ! $translation ) { 60 | continue; 61 | } 62 | $new_post_id = duplicate_post_create_duplicate( $translation, $status ); 63 | if ( ! is_wp_error( $new_post_id ) ) { 64 | $sitepress->set_element_language_details( 65 | $new_post_id, 66 | 'post_' . $translation->post_type, 67 | $new_trid, 68 | $code, 69 | $current_language 70 | ); 71 | } 72 | } 73 | } 74 | } 75 | 76 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: see above. 77 | $duplicated_posts[ $post->ID ] = $post_id; 78 | } 79 | } 80 | 81 | /** 82 | * Duplicate string packages. 83 | * 84 | * @global array() $duplicated_posts Array of duplicated posts. 85 | * 86 | * @return void 87 | */ 88 | function duplicate_wpml_string_packages() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: renaming the function would be a BC-break. 89 | global $duplicated_posts; 90 | 91 | foreach ( $duplicated_posts as $original_post_id => $duplicate_post_id ) { 92 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. 93 | $original_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $original_post_id ); 94 | 95 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. 96 | $new_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $duplicate_post_id ); 97 | 98 | if ( is_array( $original_string_packages ) ) { 99 | foreach ( $original_string_packages as $original_string_package ) { 100 | $translated_original_strings = $original_string_package->get_translated_strings( [] ); 101 | 102 | foreach ( $new_string_packages as $new_string_package ) { 103 | $cache = new WPML_WP_Cache( 'WPML_Package' ); 104 | $cache->flush_group_cache(); 105 | $new_strings = $new_string_package->get_package_strings(); 106 | foreach ( $new_strings as $new_string ) { 107 | 108 | if ( isset( $translated_original_strings[ $new_string->name ] ) ) { 109 | foreach ( $translated_original_strings[ $new_string->name ] as $language => $translated_string ) { 110 | 111 | do_action( 112 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. 113 | 'wpml_add_string_translation', 114 | $new_string->id, 115 | $language, 116 | $translated_string['value'], 117 | $translated_string['status'] 118 | ); 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yoast/duplicate-post", 3 | "description": "The go-to tool for cloning posts and pages, including the powerful Rewrite & Republish feature.", 4 | "license": "GPL-2.0-or-later", 5 | "type": "wordpress-plugin", 6 | "keywords": [ 7 | "wordpress", 8 | "post", 9 | "copy", 10 | "clone" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Enrico Battocchi & Team Yoast", 15 | "email": "support@yoast.com", 16 | "homepage": "https://yoast.com" 17 | } 18 | ], 19 | "homepage": "https://wordpress.org/plugins/duplicate-post/", 20 | "support": { 21 | "issues": "https://github.com/Yoast/duplicate-post/issues", 22 | "forum": "https://wordpress.org/support/plugin/duplicate-post", 23 | "source": "https://github.com/Yoast/duplicate-post", 24 | "security": "https://yoast.com/security-program/" 25 | }, 26 | "require": { 27 | "php": "^7.4 || ^8.0", 28 | "composer/installers": "^1.12.0 || ^2.0" 29 | }, 30 | "require-dev": { 31 | "roave/security-advisories": "dev-master", 32 | "yoast/wp-test-utils": "^1.2.0", 33 | "yoast/yoastcs": "^3.2.0" 34 | }, 35 | "autoload": { 36 | "classmap": [ 37 | "src/" 38 | ] 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Yoast\\WP\\Duplicate_Post\\Tests\\": "tests/" 43 | }, 44 | "classmap": [ 45 | "config/" 46 | ] 47 | }, 48 | "config": { 49 | "allow-plugins": { 50 | "composer/installers": true, 51 | "dealerdirect/phpcodesniffer-composer-installer": true 52 | }, 53 | "classmap-authoritative": true, 54 | "lock": false 55 | }, 56 | "scripts": { 57 | "lint": [ 58 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --show-deprecated --exclude vendor --exclude node_modules --exclude .git" 59 | ], 60 | "cs": [ 61 | "Yoast\\WP\\Duplicate_Post\\Config\\Composer\\Actions::check_coding_standards" 62 | ], 63 | "check-cs-thresholds": [ 64 | "@putenv YOASTCS_THRESHOLD_ERRORS=65", 65 | "@putenv YOASTCS_THRESHOLD_WARNINGS=0", 66 | "Yoast\\WP\\Duplicate_Post\\Config\\Composer\\Actions::check_cs_thresholds" 67 | ], 68 | "check-cs": [ 69 | "@check-cs-warnings -n" 70 | ], 71 | "check-cs-warnings": [ 72 | "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs" 73 | ], 74 | "check-staged-cs": [ 75 | "@check-cs-warnings --filter=GitStaged" 76 | ], 77 | "check-branch-cs": [ 78 | "Yoast\\WP\\Duplicate_Post\\Config\\Composer\\Actions::check_branch_cs" 79 | ], 80 | "fix-cs": [ 81 | "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf" 82 | ], 83 | "test": [ 84 | "@php ./vendor/phpunit/phpunit/phpunit --no-coverage" 85 | ], 86 | "coverage": [ 87 | "@php ./vendor/phpunit/phpunit/phpunit" 88 | ], 89 | "test-wp": [ 90 | "@php ./vendor/phpunit/phpunit/phpunit -c phpunit-wp.xml.dist --no-coverage" 91 | ], 92 | "coverage-wp": [ 93 | "@php ./vendor/phpunit/phpunit/phpunit -c phpunit-wp.xml.dist" 94 | ], 95 | "integration-test": [ 96 | "@test-wp" 97 | ], 98 | "integration-coverage": [ 99 | "@coverage-wp" 100 | ] 101 | }, 102 | "scripts-descriptions": { 103 | "lint": "Check the PHP files for parse errors.", 104 | "cs": "See a menu with the code style checking script options.", 105 | "check-cs-thresholds": "Check the PHP files for code style violations and best practices and verify the number of issues does not exceed predefined thresholds.", 106 | "check-cs": "Check the PHP files for code style violations and best practices, ignoring warnings.", 107 | "check-cs-warnings": "Check the PHP files for code style violations and best practices, including warnings.", 108 | "check-staged-cs": "Check the staged PHP files for code style violations and best practices.", 109 | "check-branch-cs": "Check the PHP files changed in the current branch for code style violations and best practices.", 110 | "fix-cs": "Auto-fix code style violations in the PHP files.", 111 | "test": "Run the unit tests without code coverage.", 112 | "coverage": "Run the unit tests with code coverage.", 113 | "test-wp": "Run the WP unit tests without code coverage.", 114 | "coverage-wp": "Run the WP unit tests with code coverage.", 115 | "integration-test": "Deprecated. Alias for the \"test-wp\" script.", 116 | "integration-coverage": "Deprecated. Alias for the \"coverage-wp\" script." 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /css/duplicate-post-options.css: -------------------------------------------------------------------------------- 1 | #duplicate_post_settings_form header.nav-tab-wrapper { 2 | margin: 22px 0 0 0; 3 | } 4 | 5 | #duplicate_post_settings_form header .nav-tab{ 6 | cursor: pointer; 7 | } 8 | 9 | #duplicate_post_settings_form header .nav-tab:focus { 10 | color: #555; 11 | box-shadow: none; 12 | outline: 1px dotted #000000; 13 | } 14 | 15 | .no-js #duplicate_post_settings_form header.nav-tab-wrapper { 16 | display: none; 17 | } 18 | 19 | .no-js #duplicate_post_settings_form section { 20 | border-top: 1px dashed #aaa; 21 | margin-top: 22px; 22 | padding-top: 22px; 23 | } 24 | 25 | .no-js #duplicate_post_settings_form section:first-of-type { 26 | margin: 0; 27 | padding: 0; 28 | border: 0; 29 | } 30 | 31 | .no-js #duplicate_post_settings_form section[hidden] { 32 | display: block; 33 | } 34 | 35 | #duplicate_post_settings_form label.taxonomy_private { 36 | font-style: italic; 37 | } 38 | 39 | #duplicate_post_settings_form .toggle-private-taxonomies.button-link { 40 | font-size: small; 41 | margin-top: 1em; 42 | } 43 | -------------------------------------------------------------------------------- /css/duplicate-post.css: -------------------------------------------------------------------------------- 1 | #wp-admin-bar-root-default>#wp-admin-bar-duplicate-post>.ab-item .ab-icon::before, 2 | #wp-admin-bar-root-default>#wp-admin-bar-new-draft>.ab-item .ab-icon::before, 3 | #wp-admin-bar-root-default>#wp-admin-bar-rewrite-republish>.ab-item .ab-icon::before{ 4 | content: 5 | url("data:image/svg+xml;utf8,"); 6 | top: 2px; 7 | } 8 | 9 | #wp-admin-bar-root-default>#wp-admin-bar-duplicate-post:hover>.ab-item .ab-icon::before, 10 | #wp-admin-bar-root-default>#wp-admin-bar-new-draft:hover>.ab-item .ab-icon::before, 11 | #wp-admin-bar-root-default>#wp-admin-bar-rewrite-republish:hover>.ab-item .ab-icon::before, 12 | #wp-admin-bar-root-default>#wp-admin-bar-duplicate-post:focus>.ab-item .ab-icon::before, 13 | #wp-admin-bar-root-default>#wp-admin-bar-new-draft:focus>.ab-item .ab-icon::before, 14 | #wp-admin-bar-root-default>#wp-admin-bar-rewrite-republish:focus>.ab-item .ab-icon::before{ 15 | content: 16 | url("data:image/svg+xml;utf8,"); 17 | } 18 | 19 | /* Copy links in the classic editor. */ 20 | #duplicate-action { 21 | margin-bottom: 12px; 22 | } 23 | 24 | #rewrite-republish-action { 25 | margin-bottom: -2px; 26 | } 27 | 28 | #rewrite-republish-action + #delete-action { 29 | margin-top: 8px; 30 | } 31 | 32 | /* Copy links in the block editor. */ 33 | .components-button.dp-editor-post-copy-to-draft, 34 | .components-button.dp-editor-post-rewrite-republish { 35 | margin-left: -6px; 36 | text-decoration: underline; 37 | } 38 | 39 | #check-changes-action { 40 | padding: 6px 10px 8px; 41 | } 42 | 43 | @media screen and (max-width: 782px){ 44 | #wp-admin-bar-root-default>#wp-admin-bar-duplicate-post, 45 | #wp-admin-bar-root-default>#wp-admin-bar-new-draft, 46 | #wp-admin-bar-root-default>#wp-admin-bar-rewrite-republish { 47 | display: block; 48 | position: static; 49 | } 50 | #wp-admin-bar-root-default>#wp-admin-bar-duplicate-post>.ab-item, 51 | #wp-admin-bar-root-default>#wp-admin-bar-new-draft>.ab-item, 52 | #wp-admin-bar-root-default>#wp-admin-bar-rewrite-republish>.ab-item { 53 | text-indent: 100%; 54 | white-space: nowrap; 55 | overflow: hidden; 56 | width: 52px; 57 | padding: 0; 58 | color: #999; 59 | position: static; 60 | } 61 | #wp-admin-bar-root-default>#wp-admin-bar-duplicate-post>.ab-item .ab-icon::before, 62 | #wp-admin-bar-root-default>#wp-admin-bar-new-draft>.ab-item .ab-icon::before, 63 | #wp-admin-bar-root-default>#wp-admin-bar-rewrite-republish>.ab-item .ab-icon::before { 64 | display: block; 65 | text-indent: 0; 66 | font: 400 32px/1 dashicons; 67 | speak: none; 68 | top: 0px; 69 | width: 52px; 70 | text-align: center; 71 | -webkit-font-smoothing: antialiased; 72 | -moz-osx-font-smoothing: grayscale; 73 | } 74 | #rewrite-republish-action + #delete-action { 75 | margin-top: 0; 76 | } 77 | } 78 | 79 | fieldset#duplicate_post_quick_edit_fieldset{ 80 | clear: both; 81 | } 82 | 83 | fieldset#duplicate_post_quick_edit_fieldset label{ 84 | display: inline; 85 | margin: 0; 86 | vertical-align: unset; 87 | } 88 | 89 | fieldset#duplicate_post_quick_edit_fieldset a{ 90 | text-decoration: underline; 91 | } 92 | -------------------------------------------------------------------------------- /duplicate-post.php: -------------------------------------------------------------------------------- 1 | sprintf( 92 | '%3$s', 93 | menu_page_url( 'duplicatepost', false ), 94 | /* translators: Hidden accessibility text. */ 95 | 'aria-label="' . __( 'Settings for Duplicate Post', 'duplicate-post' ) . '"', 96 | esc_html__( 'Settings', 'duplicate-post' ) 97 | ), 98 | ]; 99 | 100 | $actions = ( $settings_action + $actions ); 101 | return $actions; 102 | } 103 | 104 | require_once DUPLICATE_POST_PATH . 'common-functions.php'; 105 | 106 | if ( is_admin() ) { 107 | include_once DUPLICATE_POST_PATH . 'admin-functions.php'; 108 | } 109 | -------------------------------------------------------------------------------- /duplicate_post_yoast_icon-125x125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yoast/duplicate-post/6231de5544a901a5bba32974579c5a6eb1e46bfa/duplicate_post_yoast_icon-125x125.png -------------------------------------------------------------------------------- /js/src/duplicate-post-edit-script.js: -------------------------------------------------------------------------------- 1 | /* global duplicatePost, duplicatePostNotices */ 2 | 3 | import { registerPlugin } from "@wordpress/plugins"; 4 | import { PluginPostStatusInfo } from "@wordpress/edit-post"; 5 | import { Fragment } from "@wordpress/element"; 6 | import { Button } from '@wordpress/components'; 7 | import { __ } from "@wordpress/i18n"; 8 | import { select, subscribe, dispatch } from "@wordpress/data"; 9 | import { redirectOnSaveCompletion } from "./duplicate-post-functions"; 10 | 11 | 12 | class DuplicatePost { 13 | constructor() { 14 | this.renderNotices(); 15 | this.removeSlugSidebarPanel(); 16 | } 17 | 18 | /** 19 | * Handles the redirect from the copy to the original. 20 | * 21 | * @returns {void} 22 | */ 23 | handleRedirect() { 24 | if ( ! parseInt( duplicatePost.rewriting, 10 ) ) { 25 | return; 26 | } 27 | 28 | let wasSavingPost = false; 29 | let wasSavingMetaboxes = false; 30 | let wasAutoSavingPost = false; 31 | 32 | /** 33 | * Determines when the redirect needs to happen. 34 | * 35 | * @returns {void} 36 | */ 37 | subscribe( () => { 38 | if ( ! this.isSafeRedirectURL( duplicatePost.originalEditURL ) || ! this.isCopyAllowedToBeRepublished() ) { 39 | return; 40 | } 41 | 42 | const completed = redirectOnSaveCompletion( duplicatePost.originalEditURL, { wasSavingPost, wasSavingMetaboxes, wasAutoSavingPost } ); 43 | 44 | wasSavingPost = completed.isSavingPost; 45 | wasSavingMetaboxes = completed.isSavingMetaBoxes; 46 | wasAutoSavingPost = completed.isAutosavingPost; 47 | } ); 48 | } 49 | 50 | /** 51 | * Checks whether the URL for the redirect from the copy to the original matches the expected format. 52 | * 53 | * Allows only URLs with a http(s) protocol, a pathname matching the admin 54 | * post.php page and a parameter string with the expected parameters. 55 | * 56 | * @returns {bool} Whether the redirect URL matches the expected format. 57 | */ 58 | isSafeRedirectURL( url ) { 59 | const parser = document.createElement( 'a' ); 60 | parser.href = url; 61 | 62 | if ( 63 | /^https?:$/.test( parser.protocol ) && 64 | /\/wp-admin\/post\.php$/.test( parser.pathname ) && 65 | /\?action=edit&post=[0-9]+&dprepublished=1&dpcopy=[0-9]+&dpnonce=[a-z0-9]+/i.test( parser.search ) 66 | ) { 67 | return true; 68 | } 69 | 70 | return false; 71 | } 72 | 73 | /** 74 | * Determines whether a Rewrite & Republish copy can be republished. 75 | * 76 | * @return bool Whether the Rewrite & Republish copy can be republished. 77 | */ 78 | isCopyAllowedToBeRepublished() { 79 | const currentPostStatus = select( 'core/editor' ).getCurrentPostAttribute( 'status' ); 80 | 81 | if ( currentPostStatus === 'dp-rewrite-republish' || currentPostStatus === 'private' ) { 82 | return true; 83 | } 84 | 85 | return false; 86 | } 87 | 88 | /** 89 | * Renders the notices in the block editor. 90 | * 91 | * @returns {void} 92 | */ 93 | renderNotices() { 94 | if ( ! duplicatePostNotices || ! ( duplicatePostNotices instanceof Object ) ) { 95 | return; 96 | } 97 | 98 | for ( const [ key, notice ] of Object.entries( duplicatePostNotices ) ) { 99 | if ( notice.status && notice.text ) { 100 | dispatch( 'core/notices' ).createNotice( 101 | notice.status, 102 | notice.text, 103 | { 104 | isDismissible: notice.isDismissible || true, 105 | } 106 | ); 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * Removes the slug panel from the block editor sidebar when the post is a Rewrite & Republish copy. 113 | * 114 | * @returns {void} 115 | */ 116 | removeSlugSidebarPanel() { 117 | if ( parseInt( duplicatePost.rewriting, 10 ) ) { 118 | dispatch( 'core/edit-post' ).removeEditorPanel( 'post-link' ); 119 | } 120 | } 121 | 122 | /** 123 | * Renders the links in the PluginPostStatusInfo component. 124 | * 125 | * @returns {JSX.Element} The rendered links. 126 | */ 127 | render() { 128 | // Don't try to render anything if there is no store. 129 | if ( ! select( 'core/editor' ) || ! ( wp.editPost && wp.editPost.PluginPostStatusInfo ) ) { 130 | return null; 131 | } 132 | 133 | const currentPostStatus = select( 'core/editor' ).getEditedPostAttribute( 'status' ); 134 | 135 | return ( 136 | ( duplicatePost.showLinksIn.submitbox === '1' ) && 137 | 138 | { ( duplicatePost.newDraftLink !== '' && duplicatePost.showLinks.new_draft === '1' ) && 139 | 140 | 147 | 148 | } 149 | { ( currentPostStatus === 'publish' && duplicatePost.rewriteAndRepublishLink !== '' && duplicatePost.showLinks.rewrite_republish === '1' ) && 150 | 151 | 158 | 159 | } 160 | 161 | ); 162 | } 163 | } 164 | 165 | const instance = new DuplicatePost(); 166 | instance.handleRedirect(); 167 | 168 | registerPlugin( 'duplicate-post', { 169 | render: instance.render 170 | } ); 171 | -------------------------------------------------------------------------------- /js/src/duplicate-post-elementor.js: -------------------------------------------------------------------------------- 1 | /* global $e, duplicatePost, elementor */ 2 | 3 | /** 4 | * Hooks into Elementor on initialization. 5 | * 6 | * @returns {void} 7 | */ 8 | function duplicatePostOnElementorInitialize() { 9 | 10 | /** 11 | * Class that defines the redirect action to execute after republishing. 12 | */ 13 | class RedirectAfterRepublish extends $e.modules.hookUI.After { 14 | 15 | /** 16 | * Gets the command to run on. 17 | * 18 | * @returns {string} The command. 19 | */ 20 | getCommand() { 21 | return "document/save/save"; 22 | } 23 | 24 | /** 25 | * Gets the conditions on which to run on. 26 | * 27 | * @param {Object} args The arguments to use. 28 | * 29 | * @returns {boolean} Whether the post status is that of a published post. 30 | */ 31 | getConditions( args ) { 32 | const { status } = args; 33 | 34 | return status === "publish"; 35 | } 36 | 37 | /** 38 | * Gets the ID of the action. 39 | * 40 | * @returns {string} The ID. 41 | */ 42 | getId() { 43 | return "redirect-after-republish"; 44 | } 45 | 46 | /** 47 | * Applies the redirect if the condtions are met. 48 | * 49 | * @param {Object} args The arguments to use. 50 | * 51 | * @returns {void} 52 | */ 53 | apply( args ) { 54 | if ( args.status === "publish" && duplicatePost.originalEditURL && duplicatePost.rewriting === "1" ) { 55 | window.location.assign( duplicatePost.originalEditURL ); 56 | } 57 | } 58 | } 59 | 60 | $e.hooks.registerUIAfter( new RedirectAfterRepublish() ); 61 | } 62 | 63 | /** 64 | * Removes the Save as Template option for Rewrite and Republish copy. 65 | * 66 | * @returns {void} 67 | */ 68 | function duplicatePostRemoveSaveTemplate() { 69 | if ( duplicatePost.rewriting === "0" ) { 70 | return; 71 | } 72 | 73 | elementor 74 | .getPanelView() 75 | .footer 76 | .currentView 77 | .removeSubMenuItem( "saver-options", { 78 | name: "save-template", 79 | } ); 80 | } 81 | 82 | // Wait on `window.elementor`. 83 | jQuery( window ).on( "elementor:init", () => { 84 | // Wait on Elementor app to have started. 85 | window.elementor.on( "panel:init", () => { 86 | duplicatePostOnElementorInitialize(); 87 | duplicatePostRemoveSaveTemplate(); 88 | } ); 89 | } ); 90 | -------------------------------------------------------------------------------- /js/src/duplicate-post-functions.js: -------------------------------------------------------------------------------- 1 | import { dispatch, select } from "@wordpress/data"; 2 | 3 | /** 4 | * This redirects without showing the warning that occurs due to a Gutenberg bug. 5 | * 6 | * Edits made to the post on the PHP side are not correctly recognized and thus the warning for unsaved changes is shown. 7 | * By updating the post status ourselves on the JS side as well we avoid this. 8 | * 9 | * @param {string} url The url to redirect to. 10 | * 11 | * @returns {void} 12 | */ 13 | const redirectWithoutWarning = ( url ) => { 14 | const currentPostStatus = select( 'core/editor' ).getCurrentPostAttribute( 'status' ); 15 | const editedPostStatus = select( 'core/editor' ).getEditedPostAttribute( 'status' ); 16 | 17 | if ( currentPostStatus === 'dp-rewrite-republish' && editedPostStatus === 'publish' ) { 18 | dispatch( 'core/editor' ).editPost( { status: currentPostStatus } ); 19 | } 20 | 21 | window.location.assign( url ); 22 | } 23 | 24 | /** 25 | * Redirects to url when saving in the block editor has completed. 26 | * 27 | * @param {string} url The url to redirect to. 28 | * @param {Object} editorState The current editor state regarding saving the post, metaboxes and autosaving. 29 | * 30 | * @returns {Object} The updated editor state. 31 | */ 32 | export const redirectOnSaveCompletion = ( url, editorState ) => { 33 | const isSavingPost = select( 'core/editor' ).isSavingPost(); 34 | const isAutosavingPost = select( 'core/editor' ).isAutosavingPost(); 35 | const hasActiveMetaBoxes = select( 'core/edit-post' ).hasMetaBoxes(); 36 | const isSavingMetaBoxes = select( 'core/edit-post' ).isSavingMetaBoxes(); 37 | 38 | // When there are custom meta boxes, redirect after they're saved. 39 | if ( hasActiveMetaBoxes && ! isSavingMetaBoxes && editorState.wasSavingMetaboxes ) { 40 | redirectWithoutWarning( url ); 41 | } 42 | 43 | // When there are no custom meta boxes, redirect after the post is saved. 44 | if ( ! hasActiveMetaBoxes && ! isSavingPost && editorState.wasSavingPost && ! editorState.wasAutoSavingPost ) { 45 | redirectWithoutWarning( url ); 46 | } 47 | 48 | return { isSavingPost, isSavingMetaBoxes, isAutosavingPost }; 49 | }; 50 | -------------------------------------------------------------------------------- /js/src/duplicate-post-options.js: -------------------------------------------------------------------------------- 1 | let tablist; 2 | let tabs; 3 | let panels; 4 | 5 | // For easy reference 6 | const keys = { 7 | end: 35, 8 | home: 36, 9 | left: 37, 10 | up: 38, 11 | right: 39, 12 | down: 40, 13 | delete: 46 14 | }; 15 | 16 | // Add or substract depending on key pressed 17 | const direction = { 18 | 37: - 1, 19 | 38: - 1, 20 | 39: 1, 21 | 40: 1 22 | }; 23 | 24 | 25 | function generateArrays() { 26 | tabs = document.querySelectorAll( "#duplicate_post_settings_form [role=\"tab\"]" ); 27 | panels = document.querySelectorAll( "#duplicate_post_settings_form [role=\"tabpanel\"]" ); 28 | } 29 | 30 | function addListeners( index ) { 31 | tabs[index].addEventListener( "click", function ( event ) { 32 | const tab = event.target; 33 | activateTab( tab, false ); 34 | } ); 35 | tabs[index].addEventListener( "keydown", function ( event ) { 36 | const key = event.keyCode; 37 | 38 | switch ( key ) { 39 | case keys.end: 40 | event.preventDefault(); 41 | // Activate last tab 42 | activateTab( tabs[tabs.length - 1] ); 43 | break; 44 | case keys.home: 45 | event.preventDefault(); 46 | // Activate first tab 47 | activateTab( tabs[0] ); 48 | break; 49 | default: 50 | break; 51 | } 52 | } ); 53 | tabs[index].addEventListener( "keyup", function ( event ) { 54 | const key = event.keyCode; 55 | 56 | switch ( key ) { 57 | case keys.left: 58 | case keys.right: 59 | switchTabOnArrowPress( event ); 60 | break; 61 | default: 62 | break; 63 | } 64 | } ); 65 | 66 | // Build an array with all tabs (', 39 | 'duplicate-post' ), 40 | { 41 | button: ', 54 | 'duplicate-post' ), 55 | { 56 | button: 34 | 43 | 52 | 53 | 54 |
59 |

60 | 61 | 62 | 63 | 72 | 73 | 74 | 77 | 83 | 84 | 85 | 88 | 94 | 95 | 96 | 99 | 106 | 107 | 108 | 111 | 117 | 118 | 119 | 122 | 135 | 136 | 137 |
138 | 185 | 250 |

251 | 252 |

253 | 254 | 255 | -------------------------------------------------------------------------------- /src/duplicate-post.php: -------------------------------------------------------------------------------- 1 | permissions_helper = new Permissions_Helper(); 70 | $this->user_interface = new User_Interface( $this->permissions_helper ); 71 | $this->post_duplicator = new Post_Duplicator(); 72 | $this->handler = new Handler( $this->post_duplicator, $this->permissions_helper ); 73 | $this->post_republisher = new Post_Republisher( $this->post_duplicator, $this->permissions_helper ); 74 | $this->revisions_migrator = new Revisions_Migrator(); 75 | $this->watchers = new Watchers( $this->permissions_helper ); 76 | 77 | $this->post_republisher->register_hooks(); 78 | $this->revisions_migrator->register_hooks(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/handlers/bulk-handler.php: -------------------------------------------------------------------------------- 1 | post_duplicator = $post_duplicator; 38 | $this->permissions_helper = $permissions_helper; 39 | } 40 | 41 | /** 42 | * Adds hooks to integrate with WordPress. 43 | * 44 | * @return void 45 | */ 46 | public function register_hooks() { 47 | \add_action( 'admin_init', [ $this, 'add_bulk_handlers' ] ); 48 | } 49 | 50 | /** 51 | * Hooks the handler for the Rewrite & Republish action for all the selected post types. 52 | * 53 | * @return void 54 | */ 55 | public function add_bulk_handlers() { 56 | $duplicate_post_types_enabled = $this->permissions_helper->get_enabled_post_types(); 57 | 58 | foreach ( $duplicate_post_types_enabled as $duplicate_post_type_enabled ) { 59 | \add_filter( "handle_bulk_actions-edit-{$duplicate_post_type_enabled}", [ $this, 'bulk_action_handler' ], 10, 3 ); 60 | } 61 | } 62 | 63 | /** 64 | * Handles the bulk actions. 65 | * 66 | * @param string $redirect_to The URL to redirect to. 67 | * @param string $doaction The action that has been called. 68 | * @param array $post_ids The array of marked post IDs. 69 | * 70 | * @return string The URL to redirect to. 71 | */ 72 | public function bulk_action_handler( $redirect_to, $doaction, $post_ids ) { 73 | $redirect_to = $this->clone_bulk_action_handler( $redirect_to, $doaction, $post_ids ); 74 | return $this->rewrite_bulk_action_handler( $redirect_to, $doaction, $post_ids ); 75 | } 76 | 77 | /** 78 | * Handles the bulk action for the Rewrite & Republish feature. 79 | * 80 | * @param string $redirect_to The URL to redirect to. 81 | * @param string $doaction The action that has been called. 82 | * @param array $post_ids The array of marked post IDs. 83 | * 84 | * @return string The URL to redirect to. 85 | */ 86 | public function rewrite_bulk_action_handler( $redirect_to, $doaction, $post_ids ) { 87 | if ( $doaction !== 'duplicate_post_bulk_rewrite_republish' ) { 88 | return $redirect_to; 89 | } 90 | 91 | $counter = 0; 92 | if ( \is_array( $post_ids ) ) { 93 | foreach ( $post_ids as $post_id ) { 94 | $post = \get_post( $post_id ); 95 | if ( ! empty( $post ) && $this->permissions_helper->should_rewrite_and_republish_be_allowed( $post ) ) { 96 | $new_post_id = $this->post_duplicator->create_duplicate_for_rewrite_and_republish( $post ); 97 | if ( ! \is_wp_error( $new_post_id ) ) { 98 | ++$counter; 99 | } 100 | } 101 | } 102 | } 103 | return \add_query_arg( 'bulk_rewriting', $counter, $redirect_to ); 104 | } 105 | 106 | /** 107 | * Handles the bulk action for the Clone feature. 108 | * 109 | * @param string $redirect_to The URL to redirect to. 110 | * @param string $doaction The action that has been called. 111 | * @param array $post_ids The array of marked post IDs. 112 | * 113 | * @return string The URL to redirect to. 114 | */ 115 | public function clone_bulk_action_handler( $redirect_to, $doaction, $post_ids ) { 116 | if ( $doaction !== 'duplicate_post_bulk_clone' ) { 117 | return $redirect_to; 118 | } 119 | 120 | $counter = 0; 121 | if ( \is_array( $post_ids ) ) { 122 | foreach ( $post_ids as $post_id ) { 123 | $post = \get_post( $post_id ); 124 | if ( ! empty( $post ) && ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 125 | if ( \intval( \get_option( 'duplicate_post_copychildren' ) !== 1 ) 126 | || ! \is_post_type_hierarchical( $post->post_type ) 127 | || ( \is_post_type_hierarchical( $post->post_type ) && ! Utils::has_ancestors_marked( $post, $post_ids ) ) 128 | ) { 129 | if ( ! \is_wp_error( \duplicate_post_create_duplicate( $post ) ) ) { 130 | ++$counter; 131 | } 132 | } 133 | } 134 | } 135 | } 136 | return \add_query_arg( 'bulk_cloned', $counter, $redirect_to ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/handlers/check-changes-handler.php: -------------------------------------------------------------------------------- 1 | permissions_helper = $permissions_helper; 46 | } 47 | 48 | /** 49 | * Adds hooks to integrate with WordPress. 50 | * 51 | * @return void 52 | */ 53 | public function register_hooks() { 54 | \add_action( 'admin_action_duplicate_post_check_changes', [ $this, 'check_changes_action_handler' ] ); 55 | } 56 | 57 | /** 58 | * Handles the action for displaying the changes between a copy and the original. 59 | * 60 | * @return void 61 | */ 62 | public function check_changes_action_handler() { 63 | global $wp_version; 64 | 65 | if ( ! ( isset( $_GET['post'] ) || isset( $_POST['post'] ) 66 | || ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'duplicate_post_check_changes' ) ) ) { 67 | \wp_die( 68 | \esc_html__( 'No post has been supplied!', 'duplicate-post' ) 69 | ); 70 | return; 71 | } 72 | 73 | $id = ( isset( $_GET['post'] ) ? \intval( \wp_unslash( $_GET['post'] ) ) : \intval( \wp_unslash( $_POST['post'] ) ) ); 74 | 75 | \check_admin_referer( 'duplicate_post_check_changes_' . $id ); 76 | 77 | $this->post = \get_post( $id ); 78 | 79 | if ( ! $this->post ) { 80 | \wp_die( 81 | \esc_html( 82 | \sprintf( 83 | /* translators: %s: post ID. */ 84 | \__( 'Changes overview failed, could not find post with ID %s.', 'duplicate-post' ), 85 | $id 86 | ) 87 | ) 88 | ); 89 | return; 90 | } 91 | 92 | $this->original = Utils::get_original( $this->post ); 93 | 94 | if ( ! $this->original ) { 95 | \wp_die( 96 | \esc_html( 97 | \__( 'Changes overview failed, could not find original post.', 'duplicate-post' ) 98 | ) 99 | ); 100 | return; 101 | } 102 | $post_edit_link = \get_edit_post_link( $this->post->ID ); 103 | 104 | $this->require_wordpress_header(); 105 | ?> 106 |
107 |

108 | original ) // phpcs:ignore WordPress.Security.EscapeOutput 113 | ); 114 | ?> 115 |

116 | 117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | \__( 'Title', 'duplicate-post' ), 127 | 'post_content' => \__( 'Content', 'duplicate-post' ), 128 | 'post_excerpt' => \__( 'Excerpt', 'duplicate-post' ), 129 | ]; 130 | 131 | $args = [ 132 | 'show_split_view' => true, 133 | 'title_left' => \__( 'Removed', 'duplicate-post' ), 134 | 'title_right' => \__( 'Added', 'duplicate-post' ), 135 | ]; 136 | 137 | if ( \version_compare( $wp_version, '5.7' ) < 0 ) { 138 | unset( $args['title_left'] ); 139 | unset( $args['title_right'] ); 140 | } 141 | 142 | $post_array = \get_post( $this->post, \ARRAY_A ); 143 | 144 | /** This filter is documented in wp-admin/includes/revision.php */ 145 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reason: using WP core hook. 146 | $fields = \apply_filters( '_wp_post_revision_fields', $fields, $post_array ); 147 | 148 | foreach ( $fields as $field => $name ) { 149 | /** This filter is documented in wp-admin/includes/revision.php */ 150 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reason: using WP core hook. 151 | $content_from = \apply_filters( "_wp_post_revision_field_{$field}", $this->original->$field, $field, $this->original, 'from' ); 152 | 153 | /** This filter is documented in wp-admin/includes/revision.php */ 154 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reason: using WP core hook. 155 | $content_to = \apply_filters( "_wp_post_revision_field_{$field}", $this->post->$field, $field, $this->post, 'to' ); 156 | 157 | $diff = \wp_text_diff( $content_from, $content_to, $args ); 158 | 159 | if ( ! $diff && $field === 'post_title' ) { 160 | // It's a better user experience to still show the Title, even if it didn't change. 161 | $diff = ''; 162 | $diff .= ''; 163 | $diff .= ''; 164 | $diff .= '
' . \esc_html( $this->original->post_title ) . '' . \esc_html( $this->post->post_title ) . '
'; 165 | } 166 | 167 | if ( $diff ) { 168 | ?> 169 |

170 | 175 | 176 |
177 |
178 |
179 |
180 |
181 | require_wordpress_footer(); 183 | } 184 | 185 | /** 186 | * Requires the WP admin header. 187 | * 188 | * @codeCoverageIgnore 189 | * 190 | * @return void 191 | */ 192 | public function require_wordpress_header() { 193 | global $post; 194 | \set_current_screen( 'revision' ); 195 | // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- The revision screen expects $post to be set. 196 | $post = $this->post; 197 | require_once \ABSPATH . 'wp-admin/admin-header.php'; 198 | } 199 | 200 | /** 201 | * Requires the WP admin footer. 202 | * 203 | * @codeCoverageIgnore 204 | * 205 | * @return void 206 | */ 207 | public function require_wordpress_footer() { 208 | require_once \ABSPATH . 'wp-admin/admin-footer.php'; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/handlers/handler.php: -------------------------------------------------------------------------------- 1 | post_duplicator = $post_duplicator; 65 | $this->permissions_helper = $permissions_helper; 66 | 67 | $this->bulk_handler = new Bulk_Handler( $this->post_duplicator, $this->permissions_helper ); 68 | $this->link_handler = new Link_Handler( $this->post_duplicator, $this->permissions_helper ); 69 | $this->check_handler = new Check_Changes_Handler( $this->permissions_helper ); 70 | $this->save_post_handler = new Save_Post_Handler( $this->permissions_helper ); 71 | 72 | $this->bulk_handler->register_hooks(); 73 | $this->link_handler->register_hooks(); 74 | $this->check_handler->register_hooks(); 75 | $this->save_post_handler->register_hooks(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/handlers/link-handler.php: -------------------------------------------------------------------------------- 1 | post_duplicator = $post_duplicator; 37 | $this->permissions_helper = $permissions_helper; 38 | } 39 | 40 | /** 41 | * Adds hooks to integrate with WordPress. 42 | * 43 | * @return void 44 | */ 45 | public function register_hooks() { 46 | \add_action( 'admin_action_duplicate_post_rewrite', [ $this, 'rewrite_link_action_handler' ] ); 47 | \add_action( 'admin_action_duplicate_post_clone', [ $this, 'clone_link_action_handler' ] ); 48 | \add_action( 'admin_action_duplicate_post_new_draft', [ $this, 'new_draft_link_action_handler' ] ); 49 | } 50 | 51 | /** 52 | * Handles the action for copying a post to a new draft. 53 | * 54 | * @return void 55 | */ 56 | public function new_draft_link_action_handler() { 57 | if ( ! $this->permissions_helper->is_current_user_allowed_to_copy() ) { 58 | \wp_die( \esc_html__( 'Current user is not allowed to copy posts.', 'duplicate-post' ) ); 59 | } 60 | 61 | if ( ! ( isset( $_GET['post'] ) || isset( $_POST['post'] ) 62 | || ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'duplicate_post_new_draft' ) ) ) { 63 | \wp_die( \esc_html__( 'No post to duplicate has been supplied!', 'duplicate-post' ) ); 64 | } 65 | 66 | $id = ( isset( $_GET['post'] ) ? \intval( \wp_unslash( $_GET['post'] ) ) : \intval( \wp_unslash( $_POST['post'] ) ) ); 67 | 68 | \check_admin_referer( 'duplicate_post_new_draft_' . $id ); 69 | 70 | $post = \get_post( $id ); 71 | 72 | if ( ! $post ) { 73 | \wp_die( 74 | \esc_html( 75 | \__( 'Copy creation failed, could not find original:', 'duplicate-post' ) . ' ' 76 | . $id 77 | ) 78 | ); 79 | } 80 | 81 | if ( $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 82 | \wp_die( 83 | \esc_html__( 'You cannot create a copy of a post which is intended for Rewrite & Republish.', 'duplicate-post' ) 84 | ); 85 | } 86 | 87 | $new_id = \duplicate_post_create_duplicate( $post, 'draft' ); 88 | 89 | if ( \is_wp_error( $new_id ) ) { 90 | \wp_die( 91 | \esc_html__( 'Copy creation failed, could not create a copy.', 'duplicate-post' ) 92 | ); 93 | } 94 | 95 | \wp_safe_redirect( 96 | \add_query_arg( 97 | [ 98 | 'cloned' => 1, 99 | 'ids' => $post->ID, 100 | ], 101 | \admin_url( 'post.php?action=edit&post=' . $new_id . ( isset( $_GET['classic-editor'] ) ? '&classic-editor' : '' ) ) 102 | ) 103 | ); 104 | exit(); 105 | } 106 | 107 | /** 108 | * Handles the action for copying a post and redirecting to the post list. 109 | * 110 | * @return void 111 | */ 112 | public function clone_link_action_handler() { 113 | if ( ! $this->permissions_helper->is_current_user_allowed_to_copy() ) { 114 | \wp_die( \esc_html__( 'Current user is not allowed to copy posts.', 'duplicate-post' ) ); 115 | } 116 | 117 | if ( ! ( isset( $_GET['post'] ) || isset( $_POST['post'] ) 118 | || ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'duplicate_post_clone' ) ) ) { 119 | \wp_die( \esc_html__( 'No post to duplicate has been supplied!', 'duplicate-post' ) ); 120 | } 121 | 122 | $id = ( isset( $_GET['post'] ) ? \intval( \wp_unslash( $_GET['post'] ) ) : \intval( \wp_unslash( $_POST['post'] ) ) ); 123 | 124 | \check_admin_referer( 'duplicate_post_clone_' . $id ); 125 | 126 | $post = \get_post( $id ); 127 | 128 | if ( ! $post ) { 129 | \wp_die( 130 | \esc_html( 131 | \__( 'Copy creation failed, could not find original:', 'duplicate-post' ) . ' ' 132 | . $id 133 | ) 134 | ); 135 | } 136 | 137 | if ( $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 138 | \wp_die( 139 | \esc_html__( 'You cannot create a copy of a post which is intended for Rewrite & Republish.', 'duplicate-post' ) 140 | ); 141 | } 142 | 143 | $new_id = \duplicate_post_create_duplicate( $post ); 144 | 145 | if ( \is_wp_error( $new_id ) ) { 146 | \wp_die( 147 | \esc_html__( 'Copy creation failed, could not create a copy.', 'duplicate-post' ) 148 | ); 149 | } 150 | 151 | $post_type = $post->post_type; 152 | $sendback = \wp_get_referer(); 153 | if ( ! $sendback || \strpos( $sendback, 'post.php' ) !== false || \strpos( $sendback, 'post-new.php' ) !== false ) { 154 | if ( $post_type === 'attachment' ) { 155 | $sendback = \admin_url( 'upload.php' ); 156 | } 157 | else { 158 | $sendback = \admin_url( 'edit.php' ); 159 | if ( ! empty( $post_type ) ) { 160 | $sendback = \add_query_arg( 'post_type', $post_type, $sendback ); 161 | } 162 | } 163 | } 164 | else { 165 | $sendback = \remove_query_arg( [ 'trashed', 'untrashed', 'deleted', 'cloned', 'ids' ], $sendback ); 166 | } 167 | 168 | // Redirect to the post list screen. 169 | \wp_safe_redirect( 170 | \add_query_arg( 171 | [ 172 | 'cloned' => 1, 173 | 'ids' => $post->ID, 174 | ], 175 | $sendback 176 | ) 177 | ); 178 | exit(); 179 | } 180 | 181 | /** 182 | * Handles the action for copying a post for the Rewrite & Republish feature. 183 | * 184 | * @return void 185 | */ 186 | public function rewrite_link_action_handler() { 187 | if ( ! $this->permissions_helper->is_current_user_allowed_to_copy() ) { 188 | \wp_die( \esc_html__( 'Current user is not allowed to copy posts.', 'duplicate-post' ) ); 189 | } 190 | 191 | if ( ! ( isset( $_GET['post'] ) || isset( $_POST['post'] ) 192 | || ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'duplicate_post_rewrite' ) ) ) { 193 | \wp_die( \esc_html__( 'No post to duplicate has been supplied!', 'duplicate-post' ) ); 194 | } 195 | 196 | $id = ( isset( $_GET['post'] ) ? \intval( \wp_unslash( $_GET['post'] ) ) : \intval( \wp_unslash( $_POST['post'] ) ) ); 197 | 198 | \check_admin_referer( 'duplicate_post_rewrite_' . $id ); 199 | 200 | $post = \get_post( $id ); 201 | 202 | if ( ! $post ) { 203 | \wp_die( 204 | \esc_html( 205 | \__( 'Copy creation failed, could not find original:', 'duplicate-post' ) . ' ' 206 | . $id 207 | ) 208 | ); 209 | } 210 | 211 | if ( ! $this->permissions_helper->should_rewrite_and_republish_be_allowed( $post ) ) { 212 | \wp_die( 213 | \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' ) 214 | ); 215 | } 216 | 217 | $new_id = $this->post_duplicator->create_duplicate_for_rewrite_and_republish( $post ); 218 | 219 | if ( \is_wp_error( $new_id ) ) { 220 | \wp_die( 221 | \esc_html__( 'Copy creation failed, could not create a copy.', 'duplicate-post' ) 222 | ); 223 | } 224 | 225 | \wp_safe_redirect( 226 | \add_query_arg( 227 | [ 228 | 'rewriting' => 1, 229 | 'ids' => $post->ID, 230 | ], 231 | \admin_url( 'post.php?action=edit&post=' . $new_id . ( isset( $_GET['classic-editor'] ) ? '&classic-editor' : '' ) ) 232 | ) 233 | ); 234 | exit(); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/handlers/save-post-handler.php: -------------------------------------------------------------------------------- 1 | permissions_helper = $permissions_helper; 28 | } 29 | 30 | /** 31 | * Adds hooks to integrate with WordPress. 32 | * 33 | * @return void 34 | */ 35 | public function register_hooks() { 36 | if ( \intval( \get_option( 'duplicate_post_show_original_meta_box' ) ) === 1 37 | || \intval( \get_option( 'duplicate_post_show_original_column' ) ) === 1 ) { 38 | \add_action( 'save_post', [ $this, 'delete_on_save_post' ] ); 39 | } 40 | } 41 | 42 | /** 43 | * Deletes the custom field with the ID of the original post. 44 | * 45 | * @param int $post_id The current post ID. 46 | * 47 | * @return void 48 | */ 49 | public function delete_on_save_post( $post_id ) { 50 | if ( ( \defined( 'DOING_AUTOSAVE' ) && \DOING_AUTOSAVE ) 51 | || empty( $_POST['duplicate_post_remove_original'] ) 52 | || ! \current_user_can( 'edit_post', $post_id ) ) { 53 | return; 54 | } 55 | 56 | $post = \get_post( $post_id ); 57 | if ( ! $post ) { 58 | return; 59 | } 60 | if ( ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 61 | \delete_post_meta( $post_id, '_dp_original' ); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/permissions-helper.php: -------------------------------------------------------------------------------- 1 | get_enabled_post_types(), true ); 48 | } 49 | 50 | /** 51 | * Determines if the current user can copy posts. 52 | * 53 | * @return bool Whether the current user can copy posts. 54 | */ 55 | public function is_current_user_allowed_to_copy() { 56 | return \current_user_can( 'copy_posts' ); 57 | } 58 | 59 | /** 60 | * Determines if the post is a copy intended for Rewrite & Republish. 61 | * 62 | * @param WP_Post $post The post object. 63 | * 64 | * @return bool Whether the post is a copy intended for Rewrite & Republish. 65 | */ 66 | public function is_rewrite_and_republish_copy( WP_Post $post ) { 67 | return ( \intval( \get_post_meta( $post->ID, '_dp_is_rewrite_republish_copy', true ) ) === 1 ); 68 | } 69 | 70 | /** 71 | * Gets the Rewrite & Republish copy ID for the passed post. 72 | * 73 | * @param WP_Post $post The post object. 74 | * 75 | * @return int The Rewrite & Republish copy ID. 76 | */ 77 | public function get_rewrite_and_republish_copy_id( WP_Post $post ) { 78 | return \get_post_meta( $post->ID, '_dp_has_rewrite_republish_copy', true ); 79 | } 80 | 81 | /** 82 | * Gets the copy post object for the passed post. 83 | * 84 | * @param WP_Post $post The post to get the copy for. 85 | * 86 | * @return WP_Post|null The copy's post object or null if it doesn't exist. 87 | */ 88 | public function get_rewrite_and_republish_copy( WP_Post $post ) { 89 | $copy_id = $this->get_rewrite_and_republish_copy_id( $post ); 90 | 91 | if ( empty( $copy_id ) ) { 92 | return null; 93 | } 94 | 95 | return \get_post( $copy_id ); 96 | } 97 | 98 | /** 99 | * Determines if the post has a copy intended for Rewrite & Republish. 100 | * 101 | * @param WP_Post $post The post object. 102 | * 103 | * @return bool Whether the post has a copy intended for Rewrite & Republish. 104 | */ 105 | public function has_rewrite_and_republish_copy( WP_Post $post ) { 106 | return ( ! empty( $this->get_rewrite_and_republish_copy_id( $post ) ) ); 107 | } 108 | 109 | /** 110 | * Determines if the post has a copy intended for Rewrite & Republish which is scheduled to be published. 111 | * 112 | * @param WP_Post $post The post object. 113 | * 114 | * @return bool|WP_Post The scheduled copy if present, false if the post has no scheduled copy. 115 | */ 116 | public function has_scheduled_rewrite_and_republish_copy( WP_Post $post ) { 117 | $copy = $this->get_rewrite_and_republish_copy( $post ); 118 | 119 | if ( ! empty( $copy ) && $copy->post_status === 'future' ) { 120 | return $copy; 121 | } 122 | 123 | return false; 124 | } 125 | 126 | /** 127 | * Determines whether the current screen is an edit post screen. 128 | * 129 | * @return bool Whether or not the current screen is editing an existing post. 130 | */ 131 | public function is_edit_post_screen() { 132 | if ( ! \is_admin() ) { 133 | return false; 134 | } 135 | 136 | $current_screen = \get_current_screen(); 137 | 138 | return $current_screen->base === 'post' && $current_screen->action !== 'add'; 139 | } 140 | 141 | /** 142 | * Determines whether the current screen is an new post screen. 143 | * 144 | * @return bool Whether or not the current screen is editing an new post. 145 | */ 146 | public function is_new_post_screen() { 147 | if ( ! \is_admin() ) { 148 | return false; 149 | } 150 | 151 | $current_screen = \get_current_screen(); 152 | 153 | return $current_screen->base === 'post' && $current_screen->action === 'add'; 154 | } 155 | 156 | /** 157 | * Determines if we are currently editing a post with Classic editor. 158 | * 159 | * @return bool Whether we are currently editing a post with Classic editor. 160 | */ 161 | public function is_classic_editor() { 162 | if ( ! $this->is_edit_post_screen() && ! $this->is_new_post_screen() ) { 163 | return false; 164 | } 165 | 166 | $screen = \get_current_screen(); 167 | if ( $screen->is_block_editor() ) { 168 | return false; 169 | } 170 | 171 | return true; 172 | } 173 | 174 | /** 175 | * Determines if the original post has changed since the creation of the copy. 176 | * 177 | * @param WP_Post $post The post object. 178 | * 179 | * @return bool Whether the original post has changed since the creation of the copy. 180 | */ 181 | public function has_original_changed( WP_Post $post ) { 182 | if ( ! $this->is_rewrite_and_republish_copy( $post ) ) { 183 | return false; 184 | } 185 | 186 | $original = Utils::get_original( $post ); 187 | $copy_creation_date_gmt = \get_post_meta( $post->ID, '_dp_creation_date_gmt', true ); 188 | 189 | if ( $original && $copy_creation_date_gmt ) { 190 | if ( \strtotime( $original->post_modified_gmt ) > \strtotime( $copy_creation_date_gmt ) ) { 191 | return true; 192 | } 193 | } 194 | 195 | return false; 196 | } 197 | 198 | /** 199 | * Determines if duplicate links for the post can be displayed. 200 | * 201 | * @param WP_Post $post The post object. 202 | * 203 | * @return bool Whether the links can be displayed. 204 | */ 205 | public function should_links_be_displayed( WP_Post $post ) { 206 | /** 207 | * Filter allowing displaying duplicate post links for current post. 208 | * 209 | * @param bool $display_links Whether the duplicate links will be displayed. 210 | * @param WP_Post $post The post object. 211 | * 212 | * @return bool Whether or not to display the duplicate post links. 213 | */ 214 | $display_links = \apply_filters( 'duplicate_post_show_link', $this->is_current_user_allowed_to_copy() && $this->is_post_type_enabled( $post->post_type ), $post ); 215 | 216 | return ! $this->is_rewrite_and_republish_copy( $post ) && $display_links; 217 | } 218 | 219 | /** 220 | * Determines if the Rewrite & Republish link for the post should be displayed. 221 | * 222 | * @param WP_Post $post The post object. 223 | * 224 | * @return bool Whether the links should be displayed. 225 | */ 226 | public function should_rewrite_and_republish_be_allowed( WP_Post $post ) { 227 | return $post->post_status === 'publish' 228 | && ! $this->is_rewrite_and_republish_copy( $post ) 229 | && ! $this->has_rewrite_and_republish_copy( $post ); 230 | } 231 | 232 | /** 233 | * Determines whether the passed post type is public and shows an admin bar. 234 | * 235 | * @param string $post_type The post_type to copy. 236 | * 237 | * @return bool Whether or not the post can be copied to a new draft. 238 | */ 239 | public function post_type_has_admin_bar( $post_type ) { 240 | $post_type_object = \get_post_type_object( $post_type ); 241 | 242 | if ( empty( $post_type_object ) ) { 243 | return false; 244 | } 245 | 246 | return $post_type_object->public && $post_type_object->show_in_admin_bar; 247 | } 248 | 249 | /** 250 | * Determines whether a Rewrite & Republish copy can be republished. 251 | * 252 | * @param WP_Post $post The post object. 253 | * 254 | * @return bool Whether the Rewrite & Republish copy can be republished. 255 | */ 256 | public function is_copy_allowed_to_be_republished( WP_Post $post ) { 257 | return \in_array( $post->post_status, [ 'dp-rewrite-republish', 'private' ], true ); 258 | } 259 | 260 | /** 261 | * Determines if the post has a trashed copy intended for Rewrite & Republish. 262 | * 263 | * @param WP_Post $post The post object. 264 | * 265 | * @return bool Whether the post has a trashed copy intended for Rewrite & Republish. 266 | */ 267 | public function has_trashed_rewrite_and_republish_copy( WP_Post $post ) { 268 | $copy_id = \get_post_meta( $post->ID, '_dp_has_rewrite_republish_copy', true ); 269 | 270 | if ( ! $copy_id ) { 271 | return false; 272 | } 273 | 274 | $copy = \get_post( $copy_id ); 275 | 276 | return ( $copy && $copy->post_status === 'trash' ); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/revisions-migrator.php: -------------------------------------------------------------------------------- 1 | post_parent = $original_post->ID; 43 | $revision->post_name = "$original_post->ID-revision-v1"; 44 | \wp_update_post( $revision ); 45 | } 46 | 47 | $revisions_to_keep = \wp_revisions_to_keep( $original_post ); 48 | if ( $revisions_to_keep < 0 ) { 49 | return; 50 | } 51 | 52 | $revisions = \wp_get_post_revisions( $original_post, [ 'order' => 'ASC' ] ); 53 | $delete = ( \count( $revisions ) - $revisions_to_keep ); 54 | if ( $delete < 1 ) { 55 | return; 56 | } 57 | 58 | $revisions = \array_slice( $revisions, 0, $delete ); 59 | 60 | for ( $i = 0; isset( $revisions[ $i ] ); $i++ ) { 61 | if ( \strpos( $revisions[ $i ]->post_name, 'autosave' ) !== false ) { 62 | continue; 63 | } 64 | \wp_delete_post_revision( $revisions[ $i ]->ID ); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ui/admin-bar.php: -------------------------------------------------------------------------------- 1 | link_builder = $link_builder; 44 | $this->permissions_helper = $permissions_helper; 45 | $this->asset_manager = $asset_manager; 46 | } 47 | 48 | /** 49 | * Adds hooks to integrate with WordPress. 50 | * 51 | * @return void 52 | */ 53 | public function register_hooks() { 54 | if ( \intval( Utils::get_option( 'duplicate_post_show_link_in', 'adminbar' ) ) === 1 ) { 55 | \add_action( 'wp_before_admin_bar_render', [ $this, 'admin_bar_render' ] ); 56 | \add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_styles' ] ); 57 | \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_styles' ] ); 58 | } 59 | } 60 | 61 | /** 62 | * Shows Rewrite & Republish link in the Toolbar. 63 | * 64 | * @global \WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance. 65 | * 66 | * @return void 67 | */ 68 | public function admin_bar_render() { 69 | global $wp_admin_bar; 70 | 71 | if ( ! \is_admin_bar_showing() ) { 72 | return; 73 | } 74 | 75 | $post = $this->get_current_post(); 76 | 77 | if ( ! $post ) { 78 | return; 79 | } 80 | 81 | $show_new_draft = ( \intval( Utils::get_option( 'duplicate_post_show_link', 'new_draft' ) ) === 1 ); 82 | $show_rewrite_and_republish = ( \intval( Utils::get_option( 'duplicate_post_show_link', 'rewrite_republish' ) ) === 1 ) 83 | && $this->permissions_helper->should_rewrite_and_republish_be_allowed( $post ); 84 | 85 | if ( $show_new_draft && $show_rewrite_and_republish ) { 86 | $wp_admin_bar->add_menu( 87 | [ 88 | 'id' => 'duplicate-post', 89 | 'title' => '' . \__( 'Duplicate Post', 'duplicate-post' ) . '', 90 | 'href' => $this->link_builder->build_new_draft_link( $post ), 91 | ] 92 | ); 93 | $wp_admin_bar->add_menu( 94 | [ 95 | 'id' => 'new-draft', 96 | 'parent' => 'duplicate-post', 97 | 'title' => \__( 'Copy to a new draft', 'duplicate-post' ), 98 | 'href' => $this->link_builder->build_new_draft_link( $post ), 99 | ] 100 | ); 101 | $wp_admin_bar->add_menu( 102 | [ 103 | 'id' => 'rewrite-republish', 104 | 'parent' => 'duplicate-post', 105 | 'title' => \__( 'Rewrite & Republish', 'duplicate-post' ), 106 | 'href' => $this->link_builder->build_rewrite_and_republish_link( $post ), 107 | ] 108 | ); 109 | } 110 | else { 111 | if ( $show_new_draft ) { 112 | $wp_admin_bar->add_menu( 113 | [ 114 | 'id' => 'new-draft', 115 | 'title' => '' . \__( 'Copy to a new draft', 'duplicate-post' ) . '', 116 | 'href' => $this->link_builder->build_new_draft_link( $post ), 117 | ] 118 | ); 119 | } 120 | 121 | if ( $show_rewrite_and_republish ) { 122 | $wp_admin_bar->add_menu( 123 | [ 124 | 'id' => 'rewrite-republish', 125 | 'title' => '' . \__( 'Rewrite & Republish', 'duplicate-post' ) . '', 126 | 'href' => $this->link_builder->build_rewrite_and_republish_link( $post ), 127 | ] 128 | ); 129 | } 130 | } 131 | } 132 | 133 | /** 134 | * Links stylesheet for Toolbar link. 135 | * 136 | * @global \WP_Query $wp_the_query. 137 | * 138 | * @return void 139 | */ 140 | public function enqueue_styles() { 141 | if ( ! \is_admin_bar_showing() ) { 142 | return; 143 | } 144 | 145 | $post = $this->get_current_post(); 146 | 147 | if ( ! $post ) { 148 | return; 149 | } 150 | 151 | $this->asset_manager->enqueue_styles(); 152 | } 153 | 154 | /** 155 | * Returns the current post object (both if it's displayed or being edited). 156 | * 157 | * @global \WP_Query $wp_the_query 158 | * 159 | * @return false|WP_Post The Post object, false if we are not on a post. 160 | */ 161 | public function get_current_post() { 162 | global $wp_the_query; 163 | 164 | if ( \is_admin() ) { 165 | $post = \get_post(); 166 | } 167 | else { 168 | $post = $wp_the_query->get_queried_object(); 169 | } 170 | 171 | if ( empty( $post ) || ! $post instanceof WP_Post ) { 172 | return false; 173 | } 174 | 175 | if ( 176 | ( ! $this->permissions_helper->is_edit_post_screen() && ! \is_singular( $post->post_type ) ) 177 | || ! $this->permissions_helper->post_type_has_admin_bar( $post->post_type ) 178 | ) { 179 | return false; 180 | } 181 | 182 | if ( ! $this->permissions_helper->should_links_be_displayed( $post ) ) { 183 | return false; 184 | } 185 | 186 | return $post; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/ui/asset-manager.php: -------------------------------------------------------------------------------- 1 | link_builder = $link_builder; 44 | $this->permissions_helper = $permissions_helper; 45 | $this->asset_manager = $asset_manager; 46 | } 47 | 48 | /** 49 | * Adds hooks to integrate with WordPress. 50 | * 51 | * @return void 52 | */ 53 | public function register_hooks() { 54 | \add_action( 'elementor/editor/after_enqueue_styles', [ $this, 'hide_elementor_post_status' ] ); 55 | \add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue_elementor_script' ], 9 ); 56 | \add_action( 'admin_enqueue_scripts', [ $this, 'should_previously_used_keyword_assessment_run' ], 9 ); 57 | \add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_block_editor_scripts' ] ); 58 | \add_filter( 'wpseo_link_suggestions_indexables', [ $this, 'remove_original_from_wpseo_link_suggestions' ], 10, 3 ); 59 | } 60 | 61 | /** 62 | * Enqueues the necessary Elementor script for the current post. 63 | * 64 | * @return void 65 | */ 66 | public function enqueue_elementor_script() { 67 | $post = \get_post(); 68 | 69 | if ( ! $post instanceof WP_Post ) { 70 | return; 71 | } 72 | 73 | $edit_js_object = $this->generate_js_object( $post ); 74 | $this->asset_manager->enqueue_elementor_script( $edit_js_object ); 75 | } 76 | 77 | /** 78 | * Hides the post status control if we're working on a Rewrite and Republish post. 79 | * 80 | * @return void 81 | */ 82 | public function hide_elementor_post_status() { 83 | $post = \get_post(); 84 | 85 | if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 86 | return; 87 | } 88 | \wp_add_inline_style( 89 | 'elementor-editor', 90 | '.elementor-control-post_status { display: none !important; }' 91 | ); 92 | } 93 | 94 | /** 95 | * Disables the Yoast SEO PreviouslyUsedKeyword assessment for Rewrite & Republish original and duplicate posts. 96 | * 97 | * @return void 98 | */ 99 | public function should_previously_used_keyword_assessment_run() { 100 | if ( $this->permissions_helper->is_edit_post_screen() || $this->permissions_helper->is_new_post_screen() ) { 101 | 102 | $post = \get_post(); 103 | 104 | if ( 105 | $post instanceof WP_Post 106 | && ( 107 | $this->permissions_helper->is_rewrite_and_republish_copy( $post ) 108 | || $this->permissions_helper->has_rewrite_and_republish_copy( $post ) 109 | ) 110 | ) { 111 | \add_filter( 'wpseo_previously_used_keyword_active', '__return_false' ); 112 | } 113 | } 114 | } 115 | 116 | /** 117 | * Enqueues the necessary JavaScript code for the block editor. 118 | * 119 | * @return void 120 | */ 121 | public function enqueue_block_editor_scripts() { 122 | if ( ! $this->permissions_helper->is_edit_post_screen() && ! $this->permissions_helper->is_new_post_screen() ) { 123 | return; 124 | } 125 | 126 | $post = \get_post(); 127 | 128 | if ( ! $post instanceof WP_Post ) { 129 | return; 130 | } 131 | 132 | $edit_js_object = $this->generate_js_object( $post ); 133 | $this->asset_manager->enqueue_edit_script( $edit_js_object ); 134 | 135 | if ( $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 136 | $string_js_object = [ 137 | 'checkLink' => $this->get_check_permalink(), 138 | ]; 139 | $this->asset_manager->enqueue_strings_script( $string_js_object ); 140 | } 141 | } 142 | 143 | /** 144 | * Generates a New Draft permalink for the current post. 145 | * 146 | * @return string The permalink. Returns empty if the post can't be copied. 147 | */ 148 | public function get_new_draft_permalink() { 149 | $post = \get_post(); 150 | 151 | if ( ! $post instanceof WP_Post || ! $this->permissions_helper->should_links_be_displayed( $post ) ) { 152 | return ''; 153 | } 154 | 155 | return $this->link_builder->build_new_draft_link( $post ); 156 | } 157 | 158 | /** 159 | * Generates a Rewrite & Republish permalink for the current post. 160 | * 161 | * @return string The permalink. Returns empty if the post cannot be copied for Rewrite & Republish. 162 | */ 163 | public function get_rewrite_republish_permalink() { 164 | $post = \get_post(); 165 | 166 | if ( 167 | ! $post instanceof WP_Post 168 | || $this->permissions_helper->is_rewrite_and_republish_copy( $post ) 169 | || $this->permissions_helper->has_rewrite_and_republish_copy( $post ) 170 | || ! $this->permissions_helper->should_links_be_displayed( $post ) 171 | ) { 172 | return ''; 173 | } 174 | 175 | return $this->link_builder->build_rewrite_and_republish_link( $post ); 176 | } 177 | 178 | /** 179 | * Generates a Check Changes permalink for the current post, if it's intended for Rewrite & Republish. 180 | * 181 | * @return string The permalink. Returns empty if the post does not exist or it's not a Rewrite & Republish copy. 182 | */ 183 | public function get_check_permalink() { 184 | $post = \get_post(); 185 | 186 | if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 187 | return ''; 188 | } 189 | 190 | return $this->link_builder->build_check_link( $post ); 191 | } 192 | 193 | /** 194 | * Generates a URL to the original post edit screen. 195 | * 196 | * @return string The URL. Empty if the copy post doesn't have an original. 197 | */ 198 | public function get_original_post_edit_url() { 199 | $post = \get_post(); 200 | 201 | if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 202 | return ''; 203 | } 204 | 205 | $original_post_id = Utils::get_original_post_id( $post->ID ); 206 | 207 | if ( ! $original_post_id ) { 208 | return ''; 209 | } 210 | 211 | return \add_query_arg( 212 | [ 213 | 'dprepublished' => 1, 214 | 'dpcopy' => $post->ID, 215 | 'dpnonce' => \wp_create_nonce( 'dp-republish' ), 216 | ], 217 | \admin_url( 'post.php?action=edit&post=' . $original_post_id ) 218 | ); 219 | } 220 | 221 | /** 222 | * Generates an array of data to be passed as a localization object to JavaScript. 223 | * 224 | * @param WP_Post $post The current post object. 225 | * 226 | * @return array The data to pass to JavaScript. 227 | */ 228 | protected function generate_js_object( WP_Post $post ) { 229 | $is_rewrite_and_republish_copy = $this->permissions_helper->is_rewrite_and_republish_copy( $post ); 230 | 231 | return [ 232 | 'newDraftLink' => $this->get_new_draft_permalink(), 233 | 'rewriteAndRepublishLink' => $this->get_rewrite_republish_permalink(), 234 | 'showLinks' => Utils::get_option( 'duplicate_post_show_link' ), 235 | 'showLinksIn' => Utils::get_option( 'duplicate_post_show_link_in' ), 236 | 'rewriting' => ( $is_rewrite_and_republish_copy ) ? 1 : 0, 237 | 'originalEditURL' => $this->get_original_post_edit_url(), 238 | ]; 239 | } 240 | 241 | /** 242 | * Filters the Yoast SEO Premium link suggestions. 243 | * 244 | * Removes the original post from the Yoast SEO Premium link suggestions 245 | * displayed on the Rewrite & Republish copy. 246 | * 247 | * @param array $suggestions An array of suggestion indexables that can be filtered. 248 | * @param int $object_id The object id for the current indexable. 249 | * @param string $object_type The object type for the current indexable. 250 | * 251 | * @return array The filtered array of suggestion indexables. 252 | */ 253 | public function remove_original_from_wpseo_link_suggestions( $suggestions, $object_id, $object_type ) { 254 | if ( $object_type !== 'post' ) { 255 | return $suggestions; 256 | } 257 | 258 | // WordPress get_post already checks if the passed ID is valid and returns null if it's not. 259 | $post = \get_post( $object_id ); 260 | 261 | if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 262 | return $suggestions; 263 | } 264 | 265 | $original_post_id = Utils::get_original_post_id( $post->ID ); 266 | 267 | return \array_filter( 268 | $suggestions, 269 | static function ( $suggestion ) use ( $original_post_id ) { 270 | return $suggestion->object_id !== $original_post_id; 271 | } 272 | ); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/ui/bulk-actions.php: -------------------------------------------------------------------------------- 1 | permissions_helper = $permissions_helper; 27 | } 28 | 29 | /** 30 | * Adds hooks to integrate with WordPress. 31 | * 32 | * @return void 33 | */ 34 | public function register_hooks() { 35 | if ( \intval( Utils::get_option( 'duplicate_post_show_link_in', 'bulkactions' ) ) === 0 ) { 36 | return; 37 | } 38 | 39 | \add_action( 'admin_init', [ $this, 'add_bulk_filters' ] ); 40 | } 41 | 42 | /** 43 | * Hooks the function to add the Rewrite & Republish option in the bulk actions for the selected post types. 44 | * 45 | * @return void 46 | */ 47 | public function add_bulk_filters() { 48 | if ( ! $this->permissions_helper->is_current_user_allowed_to_copy() ) { 49 | return; 50 | } 51 | 52 | $duplicate_post_types_enabled = $this->permissions_helper->get_enabled_post_types(); 53 | foreach ( $duplicate_post_types_enabled as $duplicate_post_type_enabled ) { 54 | \add_filter( "bulk_actions-edit-{$duplicate_post_type_enabled}", [ $this, 'register_bulk_action' ] ); 55 | } 56 | } 57 | 58 | /** 59 | * Adds 'Rewrite & Republish' to the bulk action dropdown. 60 | * 61 | * @param array $bulk_actions The bulk actions array. 62 | * 63 | * @return array The bulk actions array. 64 | */ 65 | public function register_bulk_action( $bulk_actions ) { 66 | $is_draft_or_trash = isset( $_REQUEST['post_status'] ) && \in_array( $_REQUEST['post_status'], [ 'draft', 'trash' ], true ); 67 | 68 | if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'clone' ) ) === 1 ) { 69 | $bulk_actions['duplicate_post_bulk_clone'] = \esc_html__( 'Clone', 'duplicate-post' ); 70 | } 71 | 72 | if ( ! $is_draft_or_trash 73 | && \intval( Utils::get_option( 'duplicate_post_show_link', 'rewrite_republish' ) ) === 1 ) { 74 | $bulk_actions['duplicate_post_bulk_rewrite_republish'] = \esc_html__( 'Rewrite & Republish', 'duplicate-post' ); 75 | } 76 | 77 | return $bulk_actions; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ui/classic-editor.php: -------------------------------------------------------------------------------- 1 | link_builder = $link_builder; 44 | $this->permissions_helper = $permissions_helper; 45 | $this->asset_manager = $asset_manager; 46 | } 47 | 48 | /** 49 | * Adds hooks to integrate with WordPress. 50 | * 51 | * @return void 52 | */ 53 | public function register_hooks() { 54 | \add_action( 'post_submitbox_misc_actions', [ $this, 'add_check_changes_link' ], 90 ); 55 | 56 | if ( \intval( Utils::get_option( 'duplicate_post_show_link_in', 'submitbox' ) ) === 1 ) { 57 | if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'new_draft' ) ) === 1 ) { 58 | \add_action( 'post_submitbox_start', [ $this, 'add_new_draft_post_button' ] ); 59 | } 60 | 61 | if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'rewrite_republish' ) ) === 1 ) { 62 | \add_action( 'post_submitbox_start', [ $this, 'add_rewrite_and_republish_post_button' ] ); 63 | } 64 | } 65 | 66 | \add_action( 'load-post.php', [ $this, 'hook_translations' ] ); 67 | \add_filter( 'post_updated_messages', [ $this, 'change_scheduled_notice_classic_editor' ] ); 68 | 69 | \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_classic_editor_scripts' ] ); 70 | if ( \intval( Utils::get_option( 'duplicate_post_show_link_in', 'submitbox' ) ) === 1 ) { 71 | if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'new_draft' ) ) === 1 72 | || \intval( Utils::get_option( 'duplicate_post_show_link', 'rewrite_republish' ) ) === 1 ) { 73 | \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_classic_editor_styles' ] ); 74 | } 75 | } 76 | 77 | // Remove slug editing from Classic Editor. 78 | \add_action( 'add_meta_boxes', [ $this, 'remove_slug_meta_box' ], 10, 2 ); 79 | \add_filter( 'get_sample_permalink_html', [ $this, 'remove_sample_permalink_slug_editor' ], 10, 5 ); 80 | } 81 | 82 | /** 83 | * Hooks the functions to change the translations. 84 | * 85 | * @return void 86 | */ 87 | public function hook_translations() { 88 | \add_filter( 'gettext', [ $this, 'change_republish_strings_classic_editor' ], 10, 3 ); 89 | \add_filter( 'gettext_with_context', [ $this, 'change_schedule_strings_classic_editor' ], 10, 4 ); 90 | } 91 | 92 | /** 93 | * Enqueues the necessary JavaScript code for the Classic editor. 94 | * 95 | * @return void 96 | */ 97 | public function enqueue_classic_editor_scripts() { 98 | if ( $this->permissions_helper->is_classic_editor() && isset( $_GET['post'] ) ) { 99 | $id = \intval( \wp_unslash( $_GET['post'] ) ); 100 | $post = \get_post( $id ); 101 | 102 | if ( ! \is_null( $post ) && $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 103 | $this->asset_manager->enqueue_strings_script(); 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * Enqueues the necessary styles for the Classic editor. 110 | * 111 | * @return void 112 | */ 113 | public function enqueue_classic_editor_styles() { 114 | if ( $this->permissions_helper->is_classic_editor() 115 | && isset( $_GET['post'] ) ) { 116 | $id = \intval( \wp_unslash( $_GET['post'] ) ); 117 | $post = \get_post( $id ); 118 | 119 | if ( ! \is_null( $post ) && $this->permissions_helper->should_links_be_displayed( $post ) ) { 120 | $this->asset_manager->enqueue_styles(); 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Adds a button in the post/page edit screen to create a clone 127 | * 128 | * @param WP_Post|null $post The post object that's being edited. 129 | * 130 | * @return void 131 | */ 132 | public function add_new_draft_post_button( $post = null ) { 133 | if ( \is_null( $post ) ) { 134 | if ( isset( $_GET['post'] ) ) { 135 | $id = \intval( \wp_unslash( $_GET['post'] ) ); 136 | $post = \get_post( $id ); 137 | } 138 | } 139 | 140 | if ( $post instanceof WP_Post && $this->permissions_helper->should_links_be_displayed( $post ) ) { 141 | ?> 142 |
143 | 145 | 146 |
147 | permissions_helper->should_rewrite_and_republish_be_allowed( $post ) 169 | && $this->permissions_helper->should_links_be_displayed( $post ) 170 | ) { 171 | ?> 172 |
173 | 174 | 175 |
176 | permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 196 | ?> 197 |
198 | 199 |

200 | link_builder->build_check_link( $post ) ); ?>> 201 | 202 | 203 |
204 | should_change_rewrite_republish_copy( \get_post() ) ) { 224 | return \__( 'Republish', 'duplicate-post' ); 225 | } 226 | elseif ( $text === 'Publish on: %s' 227 | && $this->should_change_rewrite_republish_copy( \get_post() ) ) { 228 | /* translators: %s: Date on which the post is to be republished. */ 229 | return \__( 'Republish on: %s', 'duplicate-post' ); 230 | } 231 | 232 | return $translation; 233 | } 234 | 235 | /** 236 | * Changes the 'Schedule' copy in the submitbox to 'Schedule republish' if a post is intended for republishing. 237 | * 238 | * @param string $translation The translated text. 239 | * @param string $text The text to translate. 240 | * @param string $context The translation context. 241 | * @param string $domain The translation domain. 242 | * 243 | * @return string The to-be-used copy of the text. 244 | */ 245 | public function change_schedule_strings_classic_editor( $translation, $text, $context, $domain ) { 246 | if ( $domain !== 'default' || $context !== 'post action/button label' ) { 247 | return $translation; 248 | } 249 | 250 | if ( $text === 'Schedule' 251 | && $this->should_change_rewrite_republish_copy( \get_post() ) ) { 252 | return \__( 'Schedule republish', 'duplicate-post' ); 253 | } 254 | 255 | return $translation; 256 | } 257 | 258 | /** 259 | * Changes the post-scheduled notice when a post or page intended for republishing is scheduled. 260 | * 261 | * @param array[] $messages Post updated messaged. 262 | * 263 | * @return array[] The to-be-used messages. 264 | */ 265 | public function change_scheduled_notice_classic_editor( $messages ) { 266 | $post = \get_post(); 267 | if ( ! $this->should_change_rewrite_republish_copy( $post ) ) { 268 | return $messages; 269 | } 270 | 271 | $permalink = \get_permalink( $post->ID ); 272 | $scheduled_date = \get_the_time( \get_option( 'date_format' ), $post ); 273 | $scheduled_time = \get_the_time( \get_option( 'time_format' ), $post ); 274 | 275 | if ( $post->post_type === 'post' ) { 276 | $messages['post'][9] = \sprintf( 277 | /* translators: 1: The post title with a link to the frontend page, 2: The scheduled date and time. */ 278 | \esc_html__( 279 | 'This rewritten post %1$s is now scheduled to replace the original post. It will be published on %2$s.', 280 | 'duplicate-post' 281 | ), 282 | '' . $post->post_title . '', 283 | '' . $scheduled_date . ' ' . $scheduled_time . '' 284 | ); 285 | return $messages; 286 | } 287 | 288 | if ( $post->post_type === 'page' ) { 289 | $messages['page'][9] = \sprintf( 290 | /* translators: 1: The page title with a link to the frontend page, 2: The scheduled date and time. */ 291 | \esc_html__( 292 | 'This rewritten page %1$s is now scheduled to replace the original page. It will be published on %2$s.', 293 | 'duplicate-post' 294 | ), 295 | '' . $post->post_title . '', 296 | '' . $scheduled_date . ' ' . $scheduled_time . '' 297 | ); 298 | } 299 | 300 | return $messages; 301 | } 302 | 303 | /** 304 | * Determines if the Rewrite & Republish copies for the post should be used. 305 | * 306 | * @param WP_Post $post The current post object. 307 | * 308 | * @return bool True if the Rewrite & Republish copies should be used. 309 | */ 310 | public function should_change_rewrite_republish_copy( $post ) { 311 | global $pagenow; 312 | if ( ! \in_array( $pagenow, [ 'post.php', 'post-new.php' ], true ) ) { 313 | return false; 314 | } 315 | 316 | if ( ! $post instanceof WP_Post ) { 317 | return false; 318 | } 319 | 320 | return $this->permissions_helper->is_rewrite_and_republish_copy( $post ); 321 | } 322 | 323 | /** 324 | * Removes the slug meta box in the Classic Editor when the post is a Rewrite & Republish copy. 325 | * 326 | * @param string $post_type Post type. 327 | * @param WP_Post $post Post object. 328 | * 329 | * @return void 330 | */ 331 | public function remove_slug_meta_box( $post_type, $post ) { 332 | if ( $post instanceof WP_Post && $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 333 | \remove_meta_box( 'slugdiv', $post_type, 'normal' ); 334 | } 335 | } 336 | 337 | /** 338 | * Removes the sample permalink slug editor in the Classic Editor when the post is a Rewrite & Republish copy. 339 | * 340 | * @param string $html Sample permalink HTML markup. 341 | * @param int $post_id Post ID. 342 | * @param string $new_title New sample permalink title. 343 | * @param string $new_slug New sample permalink slug. 344 | * @param WP_Post $post Post object. 345 | * 346 | * @return string The filtered HTML of the sample permalink slug editor. 347 | */ 348 | public function remove_sample_permalink_slug_editor( $html, $post_id, $new_title, $new_slug, $post ) { 349 | if ( ! $post instanceof WP_Post ) { 350 | return $html; 351 | } 352 | 353 | if ( $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 354 | return ''; 355 | } 356 | 357 | return $html; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/ui/column.php: -------------------------------------------------------------------------------- 1 | permissions_helper = $permissions_helper; 36 | $this->asset_manager = $asset_manager; 37 | } 38 | 39 | /** 40 | * Adds hooks to integrate with WordPress. 41 | * 42 | * @return void 43 | */ 44 | public function register_hooks() { 45 | if ( \intval( \get_option( 'duplicate_post_show_original_column' ) ) === 1 ) { 46 | $enabled_post_types = $this->permissions_helper->get_enabled_post_types(); 47 | if ( \count( $enabled_post_types ) ) { 48 | foreach ( $enabled_post_types as $enabled_post_type ) { 49 | \add_filter( "manage_{$enabled_post_type}_posts_columns", [ $this, 'add_original_column' ] ); 50 | \add_action( "manage_{$enabled_post_type}_posts_custom_column", [ $this, 'show_original_item' ], 10, 2 ); 51 | } 52 | \add_action( 'quick_edit_custom_box', [ $this, 'quick_edit_remove_original' ] ); 53 | \add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] ); 54 | \add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_styles' ] ); 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Adds Original item column to the post list. 61 | * 62 | * @param array $post_columns The post columns array. 63 | * 64 | * @return array The updated array. 65 | */ 66 | public function add_original_column( $post_columns ) { 67 | if ( \is_array( $post_columns ) ) { 68 | $post_columns['duplicate_post_original_item'] = \__( 'Original item', 'duplicate-post' ); 69 | } 70 | return $post_columns; 71 | } 72 | 73 | /** 74 | * Sets the text to be displayed in the Original item column for the current post. 75 | * 76 | * @param string $column_name The name for the current column. 77 | * @param int $post_id The ID for the current post. 78 | * 79 | * @return void 80 | */ 81 | public function show_original_item( $column_name, $post_id ) { 82 | if ( $column_name === 'duplicate_post_original_item' ) { 83 | $column_content = '-'; 84 | $data_attr = ' data-no-original="1"'; 85 | $original_item = Utils::get_original( $post_id ); 86 | if ( $original_item ) { 87 | $post = \get_post( $post_id ); 88 | $data_attr = ''; 89 | 90 | if ( $post instanceof WP_Post 91 | && $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 92 | $data_attr = ' data-copy-is-for-rewrite-and-republish="1"'; 93 | } 94 | 95 | $column_content = Utils::get_edit_or_view_link( $original_item ); 96 | } 97 | \printf( 98 | '%s', 99 | $data_attr, // phpcs:ignore WordPress.Security.EscapeOutput 100 | $column_content // phpcs:ignore WordPress.Security.EscapeOutput 101 | ); 102 | } 103 | } 104 | 105 | /** 106 | * Adds original item checkbox + edit link in the Quick Edit. 107 | * 108 | * @param string $column_name The name for the current column. 109 | * 110 | * @return void 111 | */ 112 | public function quick_edit_remove_original( $column_name ) { 113 | if ( $column_name !== 'duplicate_post_original_item' ) { 114 | return; 115 | } 116 | \printf( 117 | '
118 |
119 | 124 | 127 | %s 128 |
129 |
', 130 | \esc_html__( 131 | 'Delete reference to original item.', 132 | 'duplicate-post' 133 | ), 134 | \wp_kses( 135 | \__( 136 | 'The original item this was copied from is: ', 137 | 'duplicate-post' 138 | ), 139 | [ 140 | 'span' => [ 141 | 'class' => [], 142 | ], 143 | ] 144 | ) 145 | ); 146 | } 147 | 148 | /** 149 | * Enqueues the Javascript file to inject column data into the Quick Edit. 150 | * 151 | * @param string $hook The current admin page. 152 | * 153 | * @return void 154 | */ 155 | public function admin_enqueue_scripts( $hook ) { 156 | if ( $hook === 'edit.php' ) { 157 | $this->asset_manager->enqueue_quick_edit_script(); 158 | } 159 | } 160 | 161 | /** 162 | * Enqueues the CSS file to for the Quick edit 163 | * 164 | * @param string $hook The current admin page. 165 | * 166 | * @return void 167 | */ 168 | public function admin_enqueue_styles( $hook ) { 169 | if ( $hook === 'edit.php' ) { 170 | $this->asset_manager->enqueue_styles(); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/ui/link-builder.php: -------------------------------------------------------------------------------- 1 | build_link( $post, $context, 'duplicate_post_rewrite' ); 22 | } 23 | 24 | /** 25 | * Builds URL for the "Clone" action. 26 | * 27 | * @param int|WP_Post $post The post object or ID. 28 | * @param string $context The context in which the URL will be used. 29 | * 30 | * @return string The URL for the link. 31 | */ 32 | public function build_clone_link( $post, $context = 'display' ) { 33 | return $this->build_link( $post, $context, 'duplicate_post_clone' ); 34 | } 35 | 36 | /** 37 | * Builds URL for the "Copy to a new draft" action. 38 | * 39 | * @param int|WP_Post $post The post object or ID. 40 | * @param string $context The context in which the URL will be used. 41 | * 42 | * @return string The URL for the link. 43 | */ 44 | public function build_new_draft_link( $post, $context = 'display' ) { 45 | return $this->build_link( $post, $context, 'duplicate_post_new_draft' ); 46 | } 47 | 48 | /** 49 | * Builds URL for the "Check Changes" action. 50 | * 51 | * @param int|WP_Post $post The post object or ID. 52 | * @param string $context The context in which the URL will be used. 53 | * 54 | * @return string The URL for the link. 55 | */ 56 | public function build_check_link( $post, $context = 'display' ) { 57 | return $this->build_link( $post, $context, 'duplicate_post_check_changes' ); 58 | } 59 | 60 | /** 61 | * Builds URL for duplication action. 62 | * 63 | * @param int|WP_Post $post The post object or ID. 64 | * @param string $context The context in which the URL will be used. 65 | * @param string $action_name The action for the URL. 66 | * 67 | * @return string The URL for the link. 68 | */ 69 | public function build_link( $post, $context, $action_name ) { 70 | $post = \get_post( $post ); 71 | if ( ! $post instanceof WP_Post ) { 72 | return ''; 73 | } 74 | 75 | if ( $context === 'display' ) { 76 | $action = '?action=' . $action_name . '&post=' . $post->ID; 77 | } 78 | else { 79 | $action = '?action=' . $action_name . '&post=' . $post->ID; 80 | } 81 | 82 | return \wp_nonce_url( 83 | /** 84 | * Filter on the URL of the clone link 85 | * 86 | * @param string $url The URL of the clone link. 87 | * @param int $ID The ID of the post 88 | * @param string $context The context in which the URL is used. 89 | * @param string $action_name The action name. 90 | * 91 | * @return string 92 | */ 93 | \apply_filters( 'duplicate_post_get_clone_post_link', \admin_url( 'admin.php' . $action ), $post->ID, $context, $action_name ), 94 | $action_name . '_' . $post->ID 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/ui/metabox.php: -------------------------------------------------------------------------------- 1 | permissions_helper = $permissions_helper; 28 | } 29 | 30 | /** 31 | * Adds hooks to integrate with WordPress. 32 | * 33 | * @return void 34 | */ 35 | public function register_hooks() { 36 | if ( \intval( \get_option( 'duplicate_post_show_original_meta_box' ) ) === 1 ) { 37 | \add_action( 'add_meta_boxes', [ $this, 'add_custom_metabox' ], 10, 2 ); 38 | } 39 | } 40 | 41 | /** 42 | * Adds a metabox to Edit screen. 43 | * 44 | * @param string $post_type The post type. 45 | * @param WP_Post $post The current post object. 46 | * 47 | * @return void 48 | */ 49 | public function add_custom_metabox( $post_type, $post ) { 50 | $enabled_post_types = $this->permissions_helper->get_enabled_post_types(); 51 | 52 | if ( \in_array( $post_type, $enabled_post_types, true ) 53 | && $post instanceof WP_Post ) { 54 | $original_item = Utils::get_original( $post ); 55 | 56 | if ( $original_item instanceof WP_Post ) { 57 | \add_meta_box( 58 | 'duplicate_post_show_original', 59 | \__( 'Duplicate Post', 'duplicate-post' ), 60 | [ $this, 'custom_metabox_html' ], 61 | $post_type, 62 | 'side', 63 | 'default', 64 | [ 'original' => $original_item ] 65 | ); 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * Outputs the HTML for the metabox. 72 | * 73 | * @param WP_Post $post The current post. 74 | * @param array $metabox The array containing the metabox data. 75 | * 76 | * @return void 77 | */ 78 | public function custom_metabox_html( $post, $metabox ) { 79 | $original_item = $metabox['args']['original']; 80 | if ( ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 81 | ?> 82 |

83 | 88 | 91 |

92 | 95 |

96 | %s', 102 | 'duplicate-post' 103 | ), 104 | [ 105 | 'span' => [ 106 | 'class' => [], 107 | ], 108 | ] 109 | ), 110 | Utils::get_edit_or_view_link( $original_item ) // phpcs:ignore WordPress.Security.EscapeOutput 111 | ); 112 | ?> 113 |

114 | ', 37 | '' 38 | ); 39 | 40 | $response_html = ''; 41 | if ( \is_array( $newsletter_form_response ) ) { 42 | $response_status = $newsletter_form_response['status']; 43 | $response_message = $newsletter_form_response['message']; 44 | 45 | $response_html = '
' . $response_message . '
'; 46 | } 47 | 48 | $html = ' 49 | 50 |
51 | ' . \wp_nonce_field( 'newsletter', 'newsletter_nonce', true, false ) . ' 52 |

' . $copy . '

53 | 61 | ' . $response_html . ' 62 |
63 | 64 | '; 65 | 66 | return $html; 67 | } 68 | 69 | /** 70 | * Handles and validates Newsletter form. 71 | * 72 | * @return array|null 73 | */ 74 | private static function newsletter_handle_form() { 75 | 76 | //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Already sanitized. 77 | if ( isset( $_POST['newsletter_nonce'] ) && ! \wp_verify_nonce( \wp_unslash( $_POST['newsletter_nonce'] ), 'newsletter' ) ) { 78 | return [ 79 | 'status' => 'error', 80 | 'message' => \esc_html__( 'Something went wrong. Please try again later.', 'duplicate-post' ), 81 | ]; 82 | } 83 | 84 | $email = null; 85 | if ( isset( $_POST['EMAIL'] ) ) { 86 | $email = \sanitize_email( \wp_unslash( $_POST['EMAIL'] ) ); 87 | } 88 | 89 | if ( $email === null ) { 90 | return null; 91 | } 92 | 93 | if ( ! \is_email( $email ) ) { 94 | return [ 95 | 'status' => 'error', 96 | 'message' => \esc_html__( 'Please enter valid e-mail address.', 'duplicate-post' ), 97 | ]; 98 | } 99 | 100 | return self::newsletter_subscribe_to_mailblue( $email ); 101 | } 102 | 103 | /** 104 | * Handles subscription request and provides feedback response. 105 | * 106 | * @param string $email Subscriber email. 107 | * 108 | * @return array Feedback response. 109 | */ 110 | private static function newsletter_subscribe_to_mailblue( $email ) { 111 | $response = \wp_remote_post( 112 | 'https://my.yoast.com/api/Mailing-list/subscribe', 113 | [ 114 | 'method' => 'POST', 115 | 'body' => [ 116 | 'customerDetails' => [ 117 | 'email' => $email, 118 | 'firstName' => '', 119 | ], 120 | 'list' => 'Yoast newsletter', 121 | ], 122 | ] 123 | ); 124 | 125 | $wp_remote_retrieve_response_code = \wp_remote_retrieve_response_code( $response ); 126 | 127 | if ( $wp_remote_retrieve_response_code <= 200 || $wp_remote_retrieve_response_code >= 300 ) { 128 | return [ 129 | 'status' => 'error', 130 | 'message' => \esc_html__( 'Something went wrong. Please try again later.', 'duplicate-post' ), 131 | ]; 132 | } 133 | 134 | return [ 135 | 'status' => 'success', 136 | 'message' => \esc_html__( 'You have successfully subscribed to the newsletter. Please check your inbox.', 'duplicate-post' ), 137 | ]; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/ui/post-states.php: -------------------------------------------------------------------------------- 1 | permissions_helper = $permissions_helper; 28 | } 29 | 30 | /** 31 | * Adds hooks to integrate with WordPress. 32 | * 33 | * @return void 34 | */ 35 | public function register_hooks() { 36 | \add_filter( 'display_post_states', [ $this, 'show_original_in_post_states' ], 10, 2 ); 37 | } 38 | 39 | /** 40 | * Shows link to original post in the post states. 41 | * 42 | * @param array $post_states The array of post states. 43 | * @param WP_Post $post The current post. 44 | * 45 | * @return array The updated post states array. 46 | */ 47 | public function show_original_in_post_states( $post_states, $post ) { 48 | if ( ! $post instanceof WP_Post 49 | || ! \is_array( $post_states ) ) { 50 | return $post_states; 51 | } 52 | 53 | $original_item = Utils::get_original( $post ); 54 | 55 | if ( ! $original_item ) { 56 | return $post_states; 57 | } 58 | 59 | if ( $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { 60 | /* translators: %s: Original item link (to view or edit) or title. */ 61 | $post_states['duplicate_post_original_item'] = \sprintf( \esc_html__( 'Rewrite & Republish of %s', 'duplicate-post' ), Utils::get_edit_or_view_link( $original_item ) ); 62 | return $post_states; 63 | } 64 | 65 | if ( \intval( \get_option( 'duplicate_post_show_original_in_post_states' ) ) === 1 ) { 66 | /* translators: %s: Original item link (to view or edit) or title. */ 67 | $post_states['duplicate_post_original_item'] = \sprintf( \__( 'Original: %s', 'duplicate-post' ), Utils::get_edit_or_view_link( $original_item ) ); 68 | } 69 | 70 | return $post_states; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/ui/row-actions.php: -------------------------------------------------------------------------------- 1 | link_builder = $link_builder; 36 | $this->permissions_helper = $permissions_helper; 37 | } 38 | 39 | /** 40 | * Adds hooks to integrate with WordPress. 41 | * 42 | * @return void 43 | */ 44 | public function register_hooks() { 45 | if ( \intval( Utils::get_option( 'duplicate_post_show_link_in', 'row' ) ) === 0 ) { 46 | return; 47 | } 48 | 49 | if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'clone' ) ) === 1 ) { 50 | \add_filter( 'post_row_actions', [ $this, 'add_clone_action_link' ], 10, 2 ); 51 | \add_filter( 'page_row_actions', [ $this, 'add_clone_action_link' ], 10, 2 ); 52 | } 53 | 54 | if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'new_draft' ) ) === 1 ) { 55 | \add_filter( 'post_row_actions', [ $this, 'add_new_draft_action_link' ], 10, 2 ); 56 | \add_filter( 'page_row_actions', [ $this, 'add_new_draft_action_link' ], 10, 2 ); 57 | } 58 | 59 | if ( \intval( Utils::get_option( 'duplicate_post_show_link', 'rewrite_republish' ) ) === 1 ) { 60 | \add_filter( 'post_row_actions', [ $this, 'add_rewrite_and_republish_action_link' ], 10, 2 ); 61 | \add_filter( 'page_row_actions', [ $this, 'add_rewrite_and_republish_action_link' ], 10, 2 ); 62 | } 63 | } 64 | 65 | /** 66 | * Hooks in the `post_row_actions` and `page_row_actions` filters to add a 'Clone' link. 67 | * 68 | * @param array $actions The array of actions from the filter. 69 | * @param WP_Post $post The post object. 70 | * 71 | * @return array The updated array of actions. 72 | */ 73 | public function add_clone_action_link( $actions, $post ) { 74 | if ( ! $post instanceof WP_Post 75 | || ! $this->permissions_helper->should_links_be_displayed( $post ) 76 | || ! \is_array( $actions ) ) { 77 | return $actions; 78 | } 79 | 80 | $title = \_draft_or_post_title( $post ); 81 | 82 | $actions['clone'] = '' 87 | . \esc_html_x( 'Clone', 'verb', 'duplicate-post' ) . ''; 88 | 89 | return $actions; 90 | } 91 | 92 | /** 93 | * Hooks in the `post_row_actions` and `page_row_actions` filters to add a 'New Draft' link. 94 | * 95 | * @param array $actions The array of actions from the filter. 96 | * @param WP_Post $post The post object. 97 | * 98 | * @return array The updated array of actions. 99 | */ 100 | public function add_new_draft_action_link( $actions, $post ) { 101 | if ( ! $post instanceof WP_Post 102 | || ! $this->permissions_helper->should_links_be_displayed( $post ) 103 | || ! \is_array( $actions ) ) { 104 | return $actions; 105 | } 106 | 107 | $title = \_draft_or_post_title( $post ); 108 | 109 | $actions['edit_as_new_draft'] = '' 114 | . \esc_html__( 'New Draft', 'duplicate-post' ) 115 | . ''; 116 | 117 | return $actions; 118 | } 119 | 120 | /** 121 | * Hooks in the `post_row_actions` and `page_row_actions` filters to add a 'Rewrite & Republish' link. 122 | * 123 | * @param array $actions The array of actions from the filter. 124 | * @param WP_Post $post The post object. 125 | * 126 | * @return array The updated array of actions. 127 | */ 128 | public function add_rewrite_and_republish_action_link( $actions, $post ) { 129 | if ( 130 | ! $post instanceof WP_Post 131 | || ! $this->permissions_helper->should_rewrite_and_republish_be_allowed( $post ) 132 | || ! $this->permissions_helper->should_links_be_displayed( $post ) 133 | || ! \is_array( $actions ) 134 | ) { 135 | return $actions; 136 | } 137 | 138 | $title = \_draft_or_post_title( $post ); 139 | 140 | $actions['rewrite'] = '' 145 | . \esc_html_x( 'Rewrite & Republish', 'verb', 'duplicate-post' ) . ''; 146 | 147 | return $actions; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/ui/user-interface.php: -------------------------------------------------------------------------------- 1 | permissions_helper = $permissions_helper; 103 | $this->link_builder = new Link_Builder(); 104 | $this->asset_manager = new Asset_Manager(); 105 | $this->asset_manager->register_hooks(); 106 | 107 | $this->admin_bar = new Admin_Bar( $this->link_builder, $this->permissions_helper, $this->asset_manager ); 108 | $this->block_editor = new Block_Editor( $this->link_builder, $this->permissions_helper, $this->asset_manager ); 109 | $this->bulk_actions = new Bulk_Actions( $this->permissions_helper ); 110 | $this->column = new Column( $this->permissions_helper, $this->asset_manager ); 111 | $this->metabox = new Metabox( $this->permissions_helper ); 112 | $this->newsletter = new Newsletter(); 113 | $this->post_states = new Post_States( $this->permissions_helper ); 114 | $this->classic_editor = new Classic_Editor( $this->link_builder, $this->permissions_helper, $this->asset_manager ); 115 | $this->row_actions = new Row_Actions( $this->link_builder, $this->permissions_helper ); 116 | 117 | $this->admin_bar->register_hooks(); 118 | $this->block_editor->register_hooks(); 119 | $this->bulk_actions->register_hooks(); 120 | $this->column->register_hooks(); 121 | $this->metabox->register_hooks(); 122 | $this->post_states->register_hooks(); 123 | $this->classic_editor->register_hooks(); 124 | $this->row_actions->register_hooks(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/utils.php: -------------------------------------------------------------------------------- 1 | ID ); 70 | 71 | if ( empty( $original_id ) ) { 72 | return null; 73 | } 74 | 75 | return \get_post( $original_id, $output ); 76 | } 77 | 78 | /** 79 | * Determines if the post has ancestors marked for copy. 80 | * 81 | * If we are copying children, and the post has already an ancestor marked for copy, we have to filter it out. 82 | * 83 | * @param WP_Post $post The post object. 84 | * @param array $post_ids The array of marked post IDs. 85 | * 86 | * @return bool Whether the post has ancestors marked for copy. 87 | */ 88 | public static function has_ancestors_marked( $post, $post_ids ) { 89 | $ancestors_in_array = 0; 90 | $parent = \wp_get_post_parent_id( $post->ID ); 91 | while ( $parent ) { 92 | if ( \in_array( $parent, $post_ids, true ) ) { 93 | ++$ancestors_in_array; 94 | } 95 | $parent = \wp_get_post_parent_id( $parent ); 96 | } 97 | return ( $ancestors_in_array !== 0 ); 98 | } 99 | 100 | /** 101 | * Returns a link to edit, preview or view a post, in accordance to user capabilities. 102 | * 103 | * @param WP_Post $post Post ID or Post object. 104 | * 105 | * @return string|null The link to edit, preview or view a post. 106 | */ 107 | public static function get_edit_or_view_link( $post ) { 108 | $post = \get_post( $post ); 109 | if ( ! $post ) { 110 | return null; 111 | } 112 | 113 | $can_edit_post = \current_user_can( 'edit_post', $post->ID ); 114 | $title = \_draft_or_post_title( $post ); 115 | $post_type_object = \get_post_type_object( $post->post_type ); 116 | 117 | if ( $can_edit_post && $post->post_status !== 'trash' ) { 118 | return \sprintf( 119 | '%s', 120 | \esc_url( \get_edit_post_link( $post->ID ) ), 121 | /* translators: Hidden accessibility text; %s: post title */ 122 | \esc_attr( \sprintf( \__( 'Edit “%s”', 'duplicate-post' ), $title ) ), 123 | $title 124 | ); 125 | } 126 | elseif ( \is_post_type_viewable( $post_type_object ) ) { 127 | if ( \in_array( $post->post_status, [ 'pending', 'draft', 'future' ], true ) ) { 128 | if ( $can_edit_post ) { 129 | $preview_link = \get_preview_post_link( $post ); 130 | return \sprintf( 131 | '%s', 132 | \esc_url( $preview_link ), 133 | /* translators: Hidden accessibility text; %s: post title */ 134 | \esc_attr( \sprintf( \__( 'Preview “%s”', 'duplicate-post' ), $title ) ), 135 | $title 136 | ); 137 | } 138 | } 139 | elseif ( $post->post_status !== 'trash' ) { 140 | return \sprintf( 141 | '%s', 142 | \esc_url( \get_permalink( $post->ID ) ), 143 | /* translators: Hidden accessibility text; %s: post title */ 144 | \esc_attr( \sprintf( \__( 'View “%s”', 'duplicate-post' ), $title ) ), 145 | $title 146 | ); 147 | } 148 | } 149 | 150 | return $title; 151 | } 152 | 153 | /** 154 | * Gets the ID of the original post intended to be rewritten with the copy for Rewrite & Republish. 155 | * 156 | * @param int $post_id The copy post ID. 157 | * 158 | * @return int The original post id of a copy for Rewrite & Republish. 159 | */ 160 | public static function get_original_post_id( $post_id ) { 161 | return (int) \get_post_meta( $post_id, '_dp_original', true ); 162 | } 163 | 164 | /** 165 | * Gets the registered WordPress roles. 166 | * 167 | * @codeCoverageIgnore As this is a simple wrapper method for a built-in WordPress method, we don't have to test it. 168 | * 169 | * @return array The roles. 170 | */ 171 | public static function get_roles() { 172 | global $wp_roles; 173 | 174 | return $wp_roles->get_names(); 175 | } 176 | 177 | /** 178 | * Gets the default meta field names to be filtered out. 179 | * 180 | * @return array The names of the meta fields to filter out by default. 181 | */ 182 | public static function get_default_filtered_meta_names() { 183 | return [ 184 | '_edit_lock', 185 | '_edit_last', 186 | '_dp_original', 187 | '_dp_is_rewrite_republish_copy', 188 | '_dp_has_rewrite_republish_copy', 189 | '_dp_has_been_republished', 190 | '_dp_creation_date_gmt', 191 | ]; 192 | } 193 | 194 | /** 195 | * Gets a Duplicate Post option from the database. 196 | * 197 | * @param string $option The option to get. 198 | * @param string $key The key to retrieve, if the option is an array. 199 | * 200 | * @return mixed The option. 201 | */ 202 | public static function get_option( $option, $key = '' ) { 203 | $option = \get_option( $option ); 204 | 205 | if ( ! \is_array( $option ) || empty( $key ) ) { 206 | return $option; 207 | } 208 | 209 | if ( ! \array_key_exists( $key, $option ) ) { 210 | return ''; 211 | } 212 | 213 | return $option[ $key ]; 214 | } 215 | 216 | /** 217 | * Determines if a plugin is active. 218 | * 219 | * We can't use is_plugin_active because this must work on the frontend too. 220 | * 221 | * @param string $plugin Path to the plugin file relative to the plugins directory. 222 | * 223 | * @return bool Whether a plugin is currently active. 224 | */ 225 | public static function is_plugin_active( $plugin ) { 226 | if ( \in_array( $plugin, (array) \get_option( 'active_plugins', [] ), true ) ) { 227 | return true; 228 | } 229 | 230 | if ( ! \is_multisite() ) { 231 | return false; 232 | } 233 | 234 | $plugins = \get_site_option( 'active_sitewide_plugins' ); 235 | return isset( $plugins[ $plugin ] ); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/watchers/bulk-actions-watcher.php: -------------------------------------------------------------------------------- 1 | register_hooks(); 15 | } 16 | 17 | /** 18 | * Adds hooks to integrate with WordPress. 19 | * 20 | * @return void 21 | */ 22 | public function register_hooks() { 23 | \add_filter( 'removable_query_args', [ $this, 'add_removable_query_args' ] ); 24 | \add_action( 'admin_notices', [ $this, 'add_bulk_clone_admin_notice' ] ); 25 | \add_action( 'admin_notices', [ $this, 'add_bulk_rewrite_and_republish_admin_notice' ] ); 26 | } 27 | 28 | /** 29 | * Adds vars to the removable query args. 30 | * 31 | * @param array $removable_query_args Array of query args keys. 32 | * 33 | * @return array The updated array of query args keys. 34 | */ 35 | public function add_removable_query_args( $removable_query_args ) { 36 | if ( \is_array( $removable_query_args ) ) { 37 | $removable_query_args[] = 'bulk_cloned'; 38 | $removable_query_args[] = 'bulk_rewriting'; 39 | } 40 | return $removable_query_args; 41 | } 42 | 43 | /** 44 | * Shows a notice after the Clone bulk action has succeeded. 45 | * 46 | * @return void 47 | */ 48 | public function add_bulk_clone_admin_notice() { 49 | if ( ! empty( $_REQUEST['bulk_cloned'] ) ) { 50 | $copied_posts = \intval( $_REQUEST['bulk_cloned'] ); 51 | \printf( 52 | '

' 53 | . \esc_html( 54 | /* translators: %s: Number of posts copied. */ 55 | \_n( 56 | '%s item copied.', 57 | '%s items copied.', 58 | $copied_posts, 59 | 'duplicate-post' 60 | ) 61 | ) . '

', 62 | \esc_html( $copied_posts ) 63 | ); 64 | } 65 | } 66 | 67 | /** 68 | * Shows a notice after the Rewrite & Republish bulk action has succeeded. 69 | * 70 | * @return void 71 | */ 72 | public function add_bulk_rewrite_and_republish_admin_notice() { 73 | if ( ! empty( $_REQUEST['bulk_rewriting'] ) ) { 74 | $copied_posts = \intval( $_REQUEST['bulk_rewriting'] ); 75 | \printf( 76 | '

' 77 | . \esc_html( 78 | /* translators: %s: Number of posts copied. */ 79 | \_n( 80 | '%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.', 81 | '%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.', 82 | $copied_posts, 83 | 'duplicate-post' 84 | ) 85 | ) . '

', 86 | \esc_html( $copied_posts ) 87 | ); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/watchers/copied-post-watcher.php: -------------------------------------------------------------------------------- 1 | permissions_helper = $permissions_helper; 27 | 28 | $this->register_hooks(); 29 | } 30 | 31 | /** 32 | * Adds hooks to integrate with WordPress. 33 | * 34 | * @return void 35 | */ 36 | public function register_hooks() { 37 | \add_action( 'admin_notices', [ $this, 'add_admin_notice' ] ); 38 | \add_action( 'enqueue_block_editor_assets', [ $this, 'add_block_editor_notice' ], 11 ); 39 | } 40 | 41 | /** 42 | * Generates the translated text for the notice. 43 | * 44 | * @param WP_Post $post The current post object. 45 | * 46 | * @return string The translated text for the notice. 47 | */ 48 | public function get_notice_text( $post ) { 49 | if ( $this->permissions_helper->has_trashed_rewrite_and_republish_copy( $post ) ) { 50 | return \__( 51 | '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.', 52 | 'duplicate-post' 53 | ); 54 | } 55 | 56 | $scheduled_copy = $this->permissions_helper->has_scheduled_rewrite_and_republish_copy( $post ); 57 | if ( ! $scheduled_copy ) { 58 | return \__( 59 | '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.', 60 | 'duplicate-post' 61 | ); 62 | } 63 | 64 | return \sprintf( 65 | /* translators: %1$s: scheduled date of the copy, %2$s: scheduled time of the copy. */ 66 | \__( 67 | 'A duplicate of this post was made, which is scheduled to replace this post on %1$s at %2$s.', 68 | 'duplicate-post' 69 | ), 70 | \get_the_time( \get_option( 'date_format' ), $scheduled_copy ), 71 | \get_the_time( \get_option( 'time_format' ), $scheduled_copy ) 72 | ); 73 | } 74 | 75 | /** 76 | * Shows a notice on the Classic editor. 77 | * 78 | * @return void 79 | */ 80 | public function add_admin_notice() { 81 | if ( ! $this->permissions_helper->is_classic_editor() ) { 82 | return; 83 | } 84 | 85 | $post = \get_post(); 86 | 87 | if ( ! $post instanceof WP_Post ) { 88 | return; 89 | } 90 | 91 | if ( $this->permissions_helper->has_rewrite_and_republish_copy( $post ) ) { 92 | print '

' 93 | . \esc_html( $this->get_notice_text( $post ) ) 94 | . '

'; 95 | } 96 | } 97 | 98 | /** 99 | * Shows a notice on the Block editor. 100 | * 101 | * @return void 102 | */ 103 | public function add_block_editor_notice() { 104 | $post = \get_post(); 105 | 106 | if ( ! $post instanceof WP_Post ) { 107 | return; 108 | } 109 | 110 | if ( $this->permissions_helper->has_rewrite_and_republish_copy( $post ) ) { 111 | 112 | $notice = [ 113 | 'text' => $this->get_notice_text( $post ), 114 | 'status' => 'warning', 115 | 'isDismissible' => true, 116 | ]; 117 | 118 | \wp_add_inline_script( 119 | 'duplicate_post_edit_script', 120 | 'duplicatePostNotices.has_rewrite_and_republish_notice = ' . \wp_json_encode( $notice ) . ';', 121 | 'before' 122 | ); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/watchers/link-actions-watcher.php: -------------------------------------------------------------------------------- 1 | permissions_helper = $permissions_helper; 26 | 27 | $this->register_hooks(); 28 | } 29 | 30 | /** 31 | * Adds hooks to integrate with WordPress. 32 | * 33 | * @return void 34 | */ 35 | public function register_hooks() { 36 | \add_filter( 'removable_query_args', [ $this, 'add_removable_query_args' ], 10, 1 ); 37 | \add_action( 'admin_notices', [ $this, 'add_clone_admin_notice' ] ); 38 | \add_action( 'admin_notices', [ $this, 'add_rewrite_and_republish_admin_notice' ] ); 39 | \add_action( 'enqueue_block_editor_assets', [ $this, 'add_rewrite_and_republish_block_editor_notice' ] ); 40 | } 41 | 42 | /** 43 | * Adds vars to the removable query args. 44 | * 45 | * @param array $removable_query_args Array of query args keys. 46 | * 47 | * @return array The updated array of query args keys. 48 | */ 49 | public function add_removable_query_args( $removable_query_args ) { 50 | if ( \is_array( $removable_query_args ) ) { 51 | $removable_query_args[] = 'cloned'; 52 | $removable_query_args[] = 'rewriting'; 53 | } 54 | return $removable_query_args; 55 | } 56 | 57 | /** 58 | * Shows a notice after the Clone link action has succeeded. 59 | * 60 | * @return void 61 | */ 62 | public function add_clone_admin_notice() { 63 | if ( ! empty( $_REQUEST['cloned'] ) ) { 64 | if ( ! $this->permissions_helper->is_classic_editor() ) { 65 | return; 66 | } 67 | 68 | $copied_posts = \intval( $_REQUEST['cloned'] ); 69 | \printf( 70 | '

' 71 | . \esc_html( 72 | /* translators: %s: Number of posts copied. */ 73 | \_n( 74 | '%s item copied.', 75 | '%s items copied.', 76 | $copied_posts, 77 | 'duplicate-post' 78 | ) 79 | ) . '

', 80 | \esc_html( $copied_posts ) 81 | ); 82 | } 83 | } 84 | 85 | /** 86 | * Shows a notice in Classic editor after the Rewrite & Republish action via link has succeeded. 87 | * 88 | * @return void 89 | */ 90 | public function add_rewrite_and_republish_admin_notice() { 91 | if ( ! empty( $_REQUEST['rewriting'] ) ) { 92 | if ( ! $this->permissions_helper->is_classic_editor() ) { 93 | return; 94 | } 95 | 96 | print '

' 97 | . \esc_html__( 98 | '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.', 99 | 'duplicate-post' 100 | ) . '

'; 101 | } 102 | } 103 | 104 | /** 105 | * Shows a notice on the Block editor after the Rewrite & Republish action via link has succeeded. 106 | * 107 | * @return void 108 | */ 109 | public function add_rewrite_and_republish_block_editor_notice() { 110 | if ( ! empty( $_REQUEST['rewriting'] ) ) { 111 | $notice = [ 112 | 'text' => \__( 113 | '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.', 114 | 'duplicate-post' 115 | ), 116 | 'status' => 'warning', 117 | 'isDismissible' => true, 118 | ]; 119 | 120 | \wp_add_inline_script( 121 | 'duplicate_post_edit_script', 122 | 'duplicatePostNotices.rewriting_notice = ' . \wp_json_encode( $notice ) . ';', 123 | 'before' 124 | ); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/watchers/original-post-watcher.php: -------------------------------------------------------------------------------- 1 | permissions_helper = $permissions_helper; 31 | 32 | $this->register_hooks(); 33 | } 34 | 35 | /** 36 | * Registers the hooks. 37 | * 38 | * @return void 39 | */ 40 | public function register_hooks() { 41 | \add_action( 'admin_notices', [ $this, 'add_admin_notice' ] ); 42 | \add_action( 'enqueue_block_editor_assets', [ $this, 'add_block_editor_notice' ], 11 ); 43 | } 44 | 45 | /** 46 | * Generates the translated text for the notice. 47 | * 48 | * @return string The translated text for the notice. 49 | */ 50 | public function get_notice_text() { 51 | return \__( 52 | 'The original post has been edited in the meantime. If you click "Republish", this rewritten post will replace the original post.', 53 | 'duplicate-post' 54 | ); 55 | } 56 | 57 | /** 58 | * Shows a notice on the Classic editor. 59 | * 60 | * @return void 61 | */ 62 | public function add_admin_notice() { 63 | if ( ! $this->permissions_helper->is_classic_editor() ) { 64 | return; 65 | } 66 | 67 | $post = \get_post(); 68 | 69 | if ( ! $post instanceof WP_Post ) { 70 | return; 71 | } 72 | 73 | if ( $this->permissions_helper->has_original_changed( $post ) ) { 74 | print '

' 75 | . \esc_html( $this->get_notice_text() ) 76 | . '

'; 77 | } 78 | } 79 | 80 | /** 81 | * Shows a notice on the Block editor. 82 | * 83 | * @return void 84 | */ 85 | public function add_block_editor_notice() { 86 | $post = \get_post(); 87 | 88 | if ( ! $post instanceof WP_Post ) { 89 | return; 90 | } 91 | 92 | if ( $this->permissions_helper->has_original_changed( $post ) ) { 93 | 94 | $notice = [ 95 | 'text' => $this->get_notice_text(), 96 | 'status' => 'warning', 97 | 'isDismissible' => true, 98 | ]; 99 | 100 | \wp_add_inline_script( 101 | 'duplicate_post_edit_script', 102 | 'duplicatePostNotices.has_original_changed_notice = ' . \wp_json_encode( $notice ) . ';', 103 | 'before' 104 | ); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/watchers/republished-post-watcher.php: -------------------------------------------------------------------------------- 1 | permissions_helper = $permissions_helper; 28 | 29 | $this->register_hooks(); 30 | } 31 | 32 | /** 33 | * Adds hooks to integrate with WordPress. 34 | * 35 | * @return void 36 | */ 37 | public function register_hooks() { 38 | \add_filter( 'removable_query_args', [ $this, 'add_removable_query_args' ] ); 39 | \add_action( 'admin_notices', [ $this, 'add_admin_notice' ] ); 40 | \add_action( 'enqueue_block_editor_assets', [ $this, 'add_block_editor_notice' ], 11 ); 41 | } 42 | 43 | /** 44 | * Adds vars to the removable query args. 45 | * 46 | * @param array $removable_query_args Array of query args keys. 47 | * 48 | * @return array The updated array of query args keys. 49 | */ 50 | public function add_removable_query_args( $removable_query_args ) { 51 | if ( \is_array( $removable_query_args ) ) { 52 | $removable_query_args[] = 'dprepublished'; 53 | $removable_query_args[] = 'dpcopy'; 54 | $removable_query_args[] = 'dpnonce'; 55 | } 56 | return $removable_query_args; 57 | } 58 | 59 | /** 60 | * Generates the translated text for the republished notice. 61 | * 62 | * @return string The translated text for the republished notice. 63 | */ 64 | public function get_notice_text() { 65 | return \__( 66 | 'Your original post has been replaced with the rewritten post. You are now viewing the (rewritten) original post.', 67 | 'duplicate-post' 68 | ); 69 | } 70 | 71 | /** 72 | * Shows a notice on the Classic editor. 73 | * 74 | * @return void 75 | */ 76 | public function add_admin_notice() { 77 | if ( ! $this->permissions_helper->is_classic_editor() ) { 78 | return; 79 | } 80 | 81 | if ( ! empty( $_REQUEST['dprepublished'] ) ) { 82 | echo '

' 83 | . \esc_html( $this->get_notice_text() ) 84 | . '

'; 85 | } 86 | } 87 | 88 | /** 89 | * Shows a notice on the Block editor. 90 | * 91 | * @return void 92 | */ 93 | public function add_block_editor_notice() { 94 | if ( ! empty( $_REQUEST['dprepublished'] ) ) { 95 | $notice = [ 96 | 'text' => $this->get_notice_text(), 97 | 'status' => 'success', 98 | 'isDismissible' => true, 99 | ]; 100 | 101 | \wp_add_inline_script( 102 | 'duplicate_post_edit_script', 103 | 'duplicatePostNotices.republished_notice = ' . \wp_json_encode( $notice ) . ';', 104 | 'before' 105 | ); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/watchers/watchers.php: -------------------------------------------------------------------------------- 1 | permissions_helper = $permissions_helper; 61 | $this->copied_post_watcher = new Copied_Post_Watcher( $this->permissions_helper ); 62 | $this->original_post_watcher = new Original_Post_Watcher( $this->permissions_helper ); 63 | $this->bulk_actions_watcher = new Bulk_Actions_Watcher(); 64 | $this->link_actions_watcher = new Link_Actions_Watcher( $this->permissions_helper ); 65 | $this->republished_post_watcher = new Republished_Post_Watcher( $this->permissions_helper ); 66 | } 67 | } 68 | --------------------------------------------------------------------------------