• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / duplicate-post / 23666236007

27 Mar 2026 08:31PM UTC coverage: 59.598% (-0.3%) from 59.881%
23666236007

Pull #485

github

web-flow
Merge bfb362225 into 1eddfe027
Pull Request #485: WP 7.0: Fix Rewrite & Republish race conditions with Real-Time Collaboration

27 of 54 new or added lines in 3 files covered. (50.0%)

8 existing lines in 1 file now uncovered.

1630 of 2735 relevant lines covered (59.6%)

7.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.67
/src/ui/block-editor.php
1
<?php
2

3
namespace Yoast\WP\Duplicate_Post\UI;
4

5
use WP_Post;
6
use Yoast\WP\Duplicate_Post\Permissions_Helper;
7
use Yoast\WP\Duplicate_Post\Utils;
8

9
/**
10
 * Duplicate Post class to manage the block editor UI.
11
 */
12
class Block_Editor {
13

14
        /**
15
         * Holds the object to create the action link to duplicate.
16
         *
17
         * @var Link_Builder
18
         */
19
        protected $link_builder;
20

21
        /**
22
         * Holds the permissions helper.
23
         *
24
         * @var Permissions_Helper
25
         */
26
        protected $permissions_helper;
27

28
        /**
29
         * Holds the asset manager.
30
         *
31
         * @var Asset_Manager
32
         */
33
        protected $asset_manager;
34

35
        /**
36
         * Initializes the class.
37
         *
38
         * @param Link_Builder       $link_builder       The link builder.
39
         * @param Permissions_Helper $permissions_helper The permissions helper.
40
         * @param Asset_Manager      $asset_manager      The asset manager.
41
         */
42
        public function __construct( Link_Builder $link_builder, Permissions_Helper $permissions_helper, Asset_Manager $asset_manager ) {
2✔
43
                $this->link_builder       = $link_builder;
2✔
44
                $this->permissions_helper = $permissions_helper;
2✔
45
                $this->asset_manager      = $asset_manager;
2✔
46
        }
47

48
        /**
49
         * Adds hooks to integrate with WordPress.
50
         *
51
         * @return void
52
         */
53
        public function register_hooks() {
2✔
54
                \add_action( 'init', [ $this, 'register_post_meta' ] );
2✔
55
                \add_action( 'elementor/editor/after_enqueue_styles', [ $this, 'hide_elementor_post_status' ] );
2✔
56
                \add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue_elementor_script' ], 9 );
2✔
57
                \add_action( 'admin_enqueue_scripts', [ $this, 'should_previously_used_keyword_assessment_run' ], 9 );
2✔
58
                \add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_block_editor_scripts' ] );
2✔
59
                \add_filter( 'wpseo_link_suggestions_indexables', [ $this, 'remove_original_from_wpseo_link_suggestions' ], 10, 3 );
2✔
60
        }
61

62
        /**
63
         * Registers the _dp_has_rewrite_republish_copy post meta for the REST API.
64
         *
65
         * This allows the block editor to reactively check whether a post already
66
         * has an R&R copy, enabling dynamic button state updates when collaborators
67
         * create a copy via Real-Time Collaboration.
68
         *
69
         * @return void
70
         */
71
        public function register_post_meta() {
2✔
72
                $enabled_post_types = $this->permissions_helper->get_enabled_post_types();
2✔
73

74
                foreach ( $enabled_post_types as $post_type ) {
2✔
75
                        \register_post_meta(
2✔
76
                                $post_type,
2✔
77
                                '_dp_has_rewrite_republish_copy',
2✔
78
                                [
2✔
79
                                        'show_in_rest'      => true,
2✔
80
                                        'single'            => true,
2✔
81
                                        'type'              => 'string',
2✔
82
                                        'sanitize_callback' => 'sanitize_text_field',
2✔
83
                                        'auth_callback'     => static function () {
2✔
NEW
84
                                                return \current_user_can( 'edit_posts' );
×
85
                                        },
2✔
86
                                ],
2✔
87
                        );
2✔
88
                }
89
        }
90

91
        /**
92
         * Enqueues the necessary Elementor script for the current post.
93
         *
94
         * @return void
95
         */
96
        public function enqueue_elementor_script() {
×
97
                $post = \get_post();
×
98

99
                if ( ! $post instanceof WP_Post ) {
×
100
                        return;
×
101
                }
102

103
                $edit_js_object = $this->generate_js_object( $post );
×
104
                $this->asset_manager->enqueue_elementor_script( $edit_js_object );
×
105
        }
106

107
        /**
108
         * Hides the post status control if we're working on a Rewrite and Republish post.
109
         *
110
         * @return void
111
         */
112
        public function hide_elementor_post_status() {
4✔
113
                $post = \get_post();
4✔
114

115
                if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) {
4✔
116
                        return;
2✔
117
                }
118
                \wp_add_inline_style(
2✔
119
                        'elementor-editor',
2✔
120
                        '.elementor-control-post_status { display: none !important; }',
2✔
121
                );
2✔
122
        }
123

124
        /**
125
         * Disables the Yoast SEO PreviouslyUsedKeyword assessment for Rewrite & Republish original and duplicate posts.
126
         *
127
         * @return void
128
         */
129
        public function should_previously_used_keyword_assessment_run() {
12✔
130
                if ( $this->permissions_helper->is_edit_post_screen() || $this->permissions_helper->is_new_post_screen() ) {
12✔
131

132
                        $post = \get_post();
10✔
133

134
                        if (
135
                                $post instanceof WP_Post
10✔
136
                                && (
137
                                        $this->permissions_helper->is_rewrite_and_republish_copy( $post )
10✔
138
                                        || $this->permissions_helper->has_rewrite_and_republish_copy( $post )
10✔
139
                                )
140
                        ) {
141
                                \add_filter( 'wpseo_previously_used_keyword_active', '__return_false' );
6✔
142
                        }
143
                }
144
        }
145

146
        /**
147
         * Enqueues the necessary JavaScript code for the block editor.
148
         *
149
         * @return void
150
         */
151
        public function enqueue_block_editor_scripts() {
8✔
152
                if ( ! $this->permissions_helper->is_edit_post_screen() && ! $this->permissions_helper->is_new_post_screen() ) {
8✔
153
                        return;
2✔
154
                }
155

156
                $post = \get_post();
6✔
157

158
                if ( ! $post instanceof WP_Post ) {
6✔
159
                        return;
2✔
160
                }
161

162
                $this->asset_manager->enqueue_styles();
4✔
163

164
                $edit_js_object = $this->generate_js_object( $post );
4✔
165
                $this->asset_manager->enqueue_edit_script( $edit_js_object );
4✔
166

167
                if ( $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) {
4✔
168
                        $string_js_object = [
2✔
169
                                'checkLink' => $this->get_check_permalink(),
2✔
170
                        ];
2✔
171
                        $this->asset_manager->enqueue_strings_script( $string_js_object );
2✔
172
                }
173
        }
174

175
        /**
176
         * Generates a New Draft permalink for the current post.
177
         *
178
         * @return string The permalink. Returns empty if the post can't be copied.
179
         */
180
        public function get_new_draft_permalink() {
4✔
181
                $post = \get_post();
4✔
182

183
                if ( ! $post instanceof WP_Post || ! $this->permissions_helper->should_links_be_displayed( $post ) ) {
4✔
184
                        return '';
2✔
185
                }
186

187
                return $this->link_builder->build_new_draft_link( $post );
2✔
188
        }
189

190
        /**
191
         * Generates a Rewrite & Republish permalink for the current post.
192
         *
193
         * @return string The permalink. Returns empty if the post cannot be copied for Rewrite & Republish.
194
         */
195
        public function get_rewrite_republish_permalink() {
8✔
196
                $post = \get_post();
8✔
197

198
                if (
199
                        ! $post instanceof WP_Post
8✔
200
                        || $this->permissions_helper->is_rewrite_and_republish_copy( $post )
8✔
201
                        || $this->permissions_helper->has_rewrite_and_republish_copy( $post )
6✔
202
                        || ! $this->permissions_helper->should_links_be_displayed( $post )
8✔
203
                ) {
204
                        return '';
6✔
205
                }
206

207
                return $this->link_builder->build_rewrite_and_republish_link( $post );
2✔
208
        }
209

210
        /**
211
         * Generates a Check Changes permalink for the current post, if it's intended for Rewrite & Republish.
212
         *
213
         * @return string The permalink. Returns empty if the post does not exist or it's not a Rewrite & Republish copy.
214
         */
215
        public function get_check_permalink() {
4✔
216
                $post = \get_post();
4✔
217

218
                if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) {
4✔
219
                        return '';
2✔
220
                }
221

222
                return $this->link_builder->build_check_link( $post );
2✔
223
        }
224

225
        /**
226
         * Generates a URL to the original post edit screen.
227
         *
228
         * @return string The URL. Empty if the copy post doesn't have an original.
229
         */
230
        public function get_original_post_edit_url() {
8✔
231
                $post = \get_post();
8✔
232

233
                if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) {
8✔
234
                        return '';
4✔
235
                }
236

237
                $original_post_id = Utils::get_original_post_id( $post->ID );
4✔
238

239
                if ( ! $original_post_id ) {
4✔
240
                        return '';
2✔
241
                }
242

243
                return \add_query_arg(
2✔
244
                        [
2✔
245
                                'dprepublished' => 1,
2✔
246
                                'dpcopy'        => $post->ID,
2✔
247
                                'dpnonce'       => \wp_create_nonce( 'dp-republish' ),
2✔
248
                        ],
2✔
249
                        \admin_url( 'post.php?action=edit&post=' . $original_post_id ),
2✔
250
                );
2✔
251
        }
252

253
        /**
254
         * Generates an array of data to be passed as a localization object to JavaScript.
255
         *
256
         * @param WP_Post $post The current post object.
257
         *
258
         * @return array<string, mixed> The data to pass to JavaScript.
259
         */
260
        protected function generate_js_object( WP_Post $post ) {
×
261
                $is_rewrite_and_republish_copy = $this->permissions_helper->is_rewrite_and_republish_copy( $post );
×
262
                $original_item                 = Utils::get_original( $post );
×
263
                $original_data                 = null;
×
264

265
                if ( $original_item instanceof WP_Post ) {
×
266
                        $original_data = [
×
267
                                'editUrl'  => \esc_url_raw( \get_edit_post_link( $original_item->ID, 'raw' ) ),
×
268
                                'viewUrl'  => \esc_url_raw( \get_permalink( $original_item->ID ) ),
×
269
                                'title'    => \html_entity_decode( \_draft_or_post_title( $original_item ), \ENT_QUOTES, 'UTF-8' ),
×
270
                                'canEdit'  => \current_user_can( 'edit_post', $original_item->ID ),
×
271
                        ];
×
272
                }
273

NEW
274
                $post_type_object = \get_post_type_object( $post->post_type );
×
275

276
                return [
×
277
                        'postId'                  => $post->ID,
×
NEW
278
                        'restBase'                => $post_type_object ? $post_type_object->rest_base : '',
×
279
                        'newDraftLink'            => $this->get_new_draft_permalink(),
×
280
                        'rewriteAndRepublishLink' => $this->get_rewrite_republish_permalink(),
×
281
                        'showLinks'               => Utils::get_option( 'duplicate_post_show_link' ),
×
282
                        'showLinksIn'             => Utils::get_option( 'duplicate_post_show_link_in' ),
×
283
                        'rewriting'               => ( $is_rewrite_and_republish_copy ) ? 1 : 0,
×
284
                        'originalEditURL'         => $this->get_original_post_edit_url(),
×
285
                        'showOriginalMetaBox'     => (int) \get_option( 'duplicate_post_show_original_meta_box' ) === 1,
×
286
                        'originalItem'            => $original_data,
×
287
                ];
×
288
        }
289

290
        /**
291
         * Filters the Yoast SEO Premium link suggestions.
292
         *
293
         * Removes the original post from the Yoast SEO Premium link suggestions
294
         * displayed on the Rewrite & Republish copy.
295
         *
296
         * @param array  $suggestions An array of suggestion indexables that can be filtered.
297
         * @param int    $object_id   The object id for the current indexable.
298
         * @param string $object_type The object type for the current indexable.
299
         *
300
         * @return array The filtered array of suggestion indexables.
301
         */
UNCOV
302
        public function remove_original_from_wpseo_link_suggestions( $suggestions, $object_id, $object_type ) {
×
UNCOV
303
                if ( $object_type !== 'post' ) {
×
UNCOV
304
                        return $suggestions;
×
305
                }
306

307
                // WordPress get_post already checks if the passed ID is valid and returns null if it's not.
308
                $post = \get_post( $object_id );
×
309

UNCOV
310
                if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) {
×
UNCOV
311
                        return $suggestions;
×
312
                }
313

314
                $original_post_id = Utils::get_original_post_id( $post->ID );
×
315

UNCOV
316
                return \array_filter(
×
UNCOV
317
                        $suggestions,
×
318
                        static function ( $suggestion ) use ( $original_post_id ) {
×
UNCOV
319
                                return $suggestion->object_id !== $original_post_id;
×
320
                        },
×
321
                );
×
322
        }
323
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc