PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.3-a.1 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 All 503 releases
← All changes | modules/copy-post.php +98 -8 12.7.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
@@ -12,12 +12,20 @@
12 12 *
13 13 * @package automattic/jetpack
14 14 */
15 15
16 +use Automattic\Jetpack\Assets\Logo;
17 +
18 +if ( ! defined( 'ABSPATH' ) ) {
19 + exit( 0 );
20 +}
21 +
16 22 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
17 23
18 24 /**
19 25 * Copy Post class.
26 + *
27 + * @phan-constructor-used-for-side-effects
20 28 */
21 29 class Jetpack_Copy_Post {
22 30 /**
23 31 * Jetpack_Copy_Post_By_Param constructor.
@@ -26,9 +34,10 @@
26 34 *
27 35 * @return void
28 36 */
29 37 public function __construct() {
30 - 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.
39 + add_action( 'admin_head', array( $this, 'print_inline_styles' ) );
31 40 add_filter( 'post_row_actions', array( $this, 'add_row_action' ), 10, 2 );
32 41 add_filter( 'page_row_actions', array( $this, 'add_row_action' ), 10, 2 );
33 42 return;
34 43 }
@@ -39,8 +48,28 @@
39 48 }
40 49 }
41 50
42 51 /**
52 + * Echos inline styles for the Jetpack logo.
53 + *
54 + * @return void
55 + */
56 + public function print_inline_styles() {
57 + echo '
58 + <style id="jetpack-copy-post-styles">
59 + #jetpack-logo__icon {
60 + height: 14px;
61 + width: 14px;
62 + vertical-align: text-bottom;
63 + }
64 + #jetpack-logo__icon path {
65 + fill: inherit;
66 + }
67 + </style>
68 + ';
69 + }
70 +
71 + /**
43 72 * Update the new (target) post data with the source post data.
44 73 *
45 74 * @param int $target_post_id Target post ID.
46 75 * @param WP_Post $post Target post object (not used).
@@ -125,8 +154,11 @@
125 154 'post_password' => $source_post->post_password,
126 155 'tags_input' => $source_post->tags_input,
127 156 );
128 157
158 + // Copy footnotes with regenerated IDs.
159 + $data = $this->copy_footnotes( $data, $source_post, $target_post_id );
160 +
129 161 /**
130 162 * Fires just before the target post is updated with its new data.
131 163 * Allows for final data adjustments before updating the target post.
132 164 *
@@ -138,9 +170,9 @@
138 170 * @param WP_Post $source_post Post object being copied.
139 171 * @param int $target_post_id Target post ID.
140 172 */
141 173 $data = apply_filters( 'jetpack_copy_post_data', $data, $source_post, $target_post_id );
142 - return wp_update_post( $data );
174 + return wp_update_post( wp_slash( $data ) );
143 175 }
144 176
145 177 /**
146 178 * Update terms for post types.
@@ -230,8 +262,63 @@
230 262 );
231 263 }
232 264
233 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 + /**
234 321 * Update the target post's title.
235 322 *
236 323 * @param string $post_title Post title determined by `get_default_post_to_edit()`.
237 324 * @param WP_Post $post Post object of newly-inserted post.
@@ -307,9 +394,9 @@
307 394 ! $this->validate_post_type( $post ) ) {
308 395 return $actions;
309 396 }
310 397
311 - $edit_url = add_query_arg(
398 + $edit_url = add_query_arg(
312 399 array(
313 400 'post_type' => $post->post_type,
314 401 'jetpack-copy' => $post->ID,
315 402 ),
@@ -314,14 +401,17 @@
314 401 'jetpack-copy' => $post->ID,
315 402 ),
316 403 admin_url( 'post-new.php' )
317 404 );
318 - $edit_action = array(
405 +
406 + $jetpack_logo = new Logo();
407 + $edit_action = array(
319 408 'jetpack-copy' => sprintf(
320 - '<a href="%s" aria-label="%s">%s</a>',
409 + '<a href="%1$s" aria-label="%2$s">%3$s %4$s</a>',
321 410 esc_url( $edit_url ),
322 - esc_attr__( 'Copy this post.', 'jetpack' ),
323 - esc_html__( 'Copy', 'jetpack' )
411 + esc_attr__( 'Duplicate this post with Jetpack.', 'jetpack' ),
412 + esc_html__( 'Duplicate', 'jetpack' ),
413 + $jetpack_logo->get_jp_emblem()
324 414 ),
325 415 );
326 416
327 417 // Insert the Copy action before the Trash action.