PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 All 502 releases
← All changes | modules/copy-post.php +69 -5 13.3.316.2 View file →
@@ -1,8 +1,8 @@
1 1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 2 /**
3 3 * Module Name: Copy Post
4 - * Module Description: Enable the option to copy entire posts and pages, including tags and settings
4 + * Module Description: Duplicate any post or page in one click to speed up content creation.
5 5 * Sort Order: 15
6 6 * First Introduced: 7.0
7 7 * Requires Connection: No
8 8 * Auto Activate: No
@@ -14,12 +14,18 @@
14 14 */
15 15
16 16 use Automattic\Jetpack\Assets\Logo;
17 17
18 +if ( ! defined( 'ABSPATH' ) ) {
19 + exit( 0 );
20 +}
21 +
18 22 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
19 23
20 24 /**
21 25 * Copy Post class.
26 + *
27 + * @phan-constructor-used-for-side-effects
22 28 */
23 29 class Jetpack_Copy_Post {
24 30 /**
25 31 * Jetpack_Copy_Post_By_Param constructor.
@@ -28,9 +34,9 @@
28 34 *
29 35 * @return void
30 36 */
31 37 public function __construct() {
32 - if ( 'edit.php' === $GLOBALS['pagenow'] ) {
38 + if ( 'edit.php' === $GLOBALS['pagenow'] || ( 'admin-ajax.php' === $GLOBALS['pagenow'] && ! empty( $_POST['post_view'] ) && 'list' === $_POST['post_view'] && ! empty( $_POST['action'] ) && 'inline-save' === $_POST['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- update_post_data() handles access check.
33 39 add_action( 'admin_head', array( $this, 'print_inline_styles' ) );
34 40 add_filter( 'post_row_actions', array( $this, 'add_row_action' ), 10, 2 );
35 41 add_filter( 'page_row_actions', array( $this, 'add_row_action' ), 10, 2 );
36 42 return;
@@ -148,8 +154,11 @@
148 154 'post_password' => $source_post->post_password,
149 155 'tags_input' => $source_post->tags_input,
150 156 );
151 157
158 + // Copy footnotes with regenerated IDs.
159 + $data = $this->copy_footnotes( $data, $source_post, $target_post_id );
160 +
152 161 /**
153 162 * Fires just before the target post is updated with its new data.
154 163 * Allows for final data adjustments before updating the target post.
155 164 *
@@ -161,9 +170,9 @@
161 170 * @param WP_Post $source_post Post object being copied.
162 171 * @param int $target_post_id Target post ID.
163 172 */
164 173 $data = apply_filters( 'jetpack_copy_post_data', $data, $source_post, $target_post_id );
165 - return wp_update_post( $data );
174 + return wp_update_post( wp_slash( $data ) );
166 175 }
167 176
168 177 /**
169 178 * Update terms for post types.
@@ -253,8 +262,63 @@
253 262 );
254 263 }
255 264
256 265 /**
266 + * Copy footnotes from source post, regenerating IDs for uniqueness.
267 + *
268 + * Gutenberg's Footnotes block stores content in post meta rather than
269 + * in the block markup itself. The IDs must be regenerated to ensure uniqueness,
270 + * otherwise multiple posts on the same page (e.g., archive views) would
271 + * have conflicting footnote anchor links.
272 + *
273 + * @param array $data Post data with which to update the target (new) post.
274 + * @param WP_Post $source_post Post object being copied.
275 + * @param int $target_post_id Target post ID.
276 + * @return array Modified post data.
277 + */
278 + public function copy_footnotes( $data, $source_post, $target_post_id ) {
279 + $footnotes_json = get_post_meta( $source_post->ID, 'footnotes', true );
280 +
281 + if ( '' === $footnotes_json ) {
282 + return $data;
283 + }
284 +
285 + $footnotes = json_decode( $footnotes_json, true );
286 +
287 + if ( ! is_array( $footnotes ) || empty( $footnotes ) ) {
288 + return $data;
289 + }
290 +
291 + // Build a mapping of old IDs to new IDs and update the footnotes array.
292 + $id_mapping = array();
293 + foreach ( $footnotes as &$footnote ) {
294 + if ( isset( $footnote['id'] ) ) {
295 + $old_id = $footnote['id'];
296 + $new_id = wp_generate_uuid4();
297 + $id_mapping[ $old_id ] = $new_id;
298 + $footnote['id'] = $new_id;
299 + }
300 + }
301 + unset( $footnote );
302 +
303 + // Update the post content to use the new footnote IDs.
304 + foreach ( $id_mapping as $old_id => $new_id ) {
305 + // Replace data-fn attribute values.
306 + $data['post_content'] = str_replace( 'data-fn="' . $old_id . '"', 'data-fn="' . $new_id . '"', $data['post_content'] );
307 + // Replace href anchor links.
308 + $data['post_content'] = str_replace( 'href="#' . $old_id . '"', 'href="#' . $new_id . '"', $data['post_content'] );
309 + // Replace id attributes (e.g., id="uuid-link").
310 + $data['post_content'] = str_replace( 'id="' . $old_id . '-link"', 'id="' . $new_id . '-link"', $data['post_content'] );
311 + $data['post_content'] = str_replace( 'id="' . $old_id . '"', 'id="' . $new_id . '"', $data['post_content'] );
312 + }
313 +
314 + // Save the footnotes meta with new IDs.
315 + update_post_meta( $target_post_id, 'footnotes', wp_json_encode( $footnotes, JSON_UNESCAPED_SLASHES ) );
316 +
317 + return $data;
318 + }
319 +
320 + /**
257 321 * Update the target post's title.
258 322 *
259 323 * @param string $post_title Post title determined by `get_default_post_to_edit()`.
260 324 * @param WP_Post $post Post object of newly-inserted post.
@@ -343,10 +407,10 @@
343 407 $edit_action = array(
344 408 'jetpack-copy' => sprintf(
345 409 '<a href="%1$s" aria-label="%2$s">%3$s %4$s</a>',
346 410 esc_url( $edit_url ),
347 - esc_attr__( 'Copy this post with Jetpack', 'jetpack' ),
348 - esc_html__( 'Copy', 'jetpack' ),
411 + esc_attr__( 'Duplicate this post with Jetpack.', 'jetpack' ),
412 + esc_html__( 'Duplicate', 'jetpack' ),
349 413 $jetpack_logo->get_jp_emblem()
350 414 ),
351 415 );
352 416