| @@ -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 | |