PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 15.1
Jetpack – WP Security, Backup, Speed, & Growth v15.1
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 14.3.1 All 501 releases
jetpack / modules / copy-post.php

copy-post.php in Jetpack – WP Security, Backup, Speed, & Growth 15.1, at modules/copy-post.php

390 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * Module Name: Copy Post
4 * Module Description: Duplicate any post or page in one click to speed up content creation.
5 * Sort Order: 15
6 * First Introduced: 7.0
7 * Requires Connection: No
8 * Auto Activate: No
9 * Module Tags: Writing
10 * Feature: Writing
11 * Additional Search Queries: copy, duplicate
12 *
13 * @package automattic/jetpack
14 */
15
16 use Automattic\Jetpack\Assets\Logo;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit( 0 );
20 }
21
22 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
23
24 /**
25 * Copy Post class.
26 *
27 * @phan-constructor-used-for-side-effects
28 */
29 class Jetpack_Copy_Post {
30 /**
31 * Jetpack_Copy_Post_By_Param constructor.
32 * Add row actions to post/page/CPT listing screens.
33 * Process any `?copy` param if on a create new post/page/CPT screen.
34 *
35 * @return void
36 */
37 public function __construct() {
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' ) );
40 add_filter( 'post_row_actions', array( $this, 'add_row_action' ), 10, 2 );
41 add_filter( 'page_row_actions', array( $this, 'add_row_action' ), 10, 2 );
42 return;
43 }
44
45 if ( ! empty( $_GET['jetpack-copy'] ) && 'post-new.php' === $GLOBALS['pagenow'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- update_post_data() handles access check.
46 add_action( 'wp_insert_post', array( $this, 'update_post_data' ), 10, 3 );
47 add_filter( 'pre_option_default_post_format', '__return_empty_string' );
48 }
49 }
50
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 /**
72 * Update the new (target) post data with the source post data.
73 *
74 * @param int $target_post_id Target post ID.
75 * @param WP_Post $post Target post object (not used).
76 * @param bool $update Whether this is an existing post being updated or not.
77 * @return void
78 */
79 public function update_post_data( $target_post_id, $post, $update ) {
80 // This `$update` check avoids infinite loops of trying to update our updated post.
81 if ( $update ) {
82 return;
83 }
84
85 // Shouldn't happen, since this filter is only added when the value isn't empty, but check anyway.
86 if ( empty( $_GET['jetpack-copy'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
87 return;
88 }
89
90 $source_post = get_post( intval( $_GET['jetpack-copy'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
91 if ( ! $source_post instanceof WP_Post ||
92 ! $this->user_can_access_post( $source_post->ID ) ||
93 ! $this->validate_post_type( $source_post ) ) {
94 return;
95 }
96
97 $update_results = array(
98 'update_content' => $this->update_content( $source_post, $target_post_id ),
99 'update_featured_image' => $this->update_featured_image( $source_post, $target_post_id ),
100 'update_post_format' => $this->update_post_format( $source_post, $target_post_id ),
101 'update_likes_sharing' => $this->update_likes_sharing( $source_post, $target_post_id ),
102 'update_post_type_terms' => $this->update_post_type_terms( $source_post, $target_post_id ),
103 );
104
105 // Required to satisfy get_default_post_to_edit(), which has these filters after post creation.
106 add_filter( 'default_title', array( $this, 'filter_title' ), 10, 2 );
107 add_filter( 'default_content', array( $this, 'filter_content' ), 10, 2 );
108 add_filter( 'default_excerpt', array( $this, 'filter_excerpt' ), 10, 2 );
109
110 // Required to avoid the block editor from adding default blocks according to post format.
111 add_filter( 'block_editor_settings_all', array( $this, 'remove_post_format_template' ) );
112
113 /**
114 * Fires after all updates have been performed, and default content filters have been added.
115 * Allows for any cleanup or post operations, and default content filters can be removed or modified.
116 *
117 * @module copy-post
118 *
119 * @since 7.0.0
120 *
121 * @param WP_Post $source_post Post object that was copied.
122 * @param int $target_post_id Target post ID.
123 * @param array $update_results Results of all update operations, allowing action to be taken.
124 */
125 do_action( 'jetpack_copy_post', $source_post, $target_post_id, $update_results );
126 }
127
128 /**
129 * Determine if the current user has edit access to the source post.
130 *
131 * @param int $post_id Source post ID (the post being copied).
132 * @return bool True if user has the meta cap of `edit_post` for the given post ID, false otherwise.
133 */
134 protected function user_can_access_post( $post_id ) {
135 return current_user_can( 'edit_post', $post_id );
136 }
137
138 /**
139 * Update the target post's title, content, excerpt, categories, and tags.
140 *
141 * @param WP_Post $source_post Post object to be copied.
142 * @param int $target_post_id Target post ID.
143 * @return int 0 on failure, or the updated post ID on success.
144 */
145 protected function update_content( $source_post, $target_post_id ) {
146 $data = array(
147 'ID' => $target_post_id,
148 'post_title' => $source_post->post_title,
149 'post_content' => $source_post->post_content,
150 'post_excerpt' => $source_post->post_excerpt,
151 'comment_status' => $source_post->comment_status,
152 'ping_status' => $source_post->ping_status,
153 'post_category' => wp_get_post_categories( $source_post->ID ),
154 'post_password' => $source_post->post_password,
155 'tags_input' => $source_post->tags_input,
156 );
157
158 /**
159 * Fires just before the target post is updated with its new data.
160 * Allows for final data adjustments before updating the target post.
161 *
162 * @module copy-post
163 *
164 * @since 7.0.0
165 *
166 * @param array $data Post data with which to update the target (new) post.
167 * @param WP_Post $source_post Post object being copied.
168 * @param int $target_post_id Target post ID.
169 */
170 $data = apply_filters( 'jetpack_copy_post_data', $data, $source_post, $target_post_id );
171 return wp_update_post( $data );
172 }
173
174 /**
175 * Update terms for post types.
176 *
177 * @param WP_Post $source_post Post object to be copied.
178 * @param int $target_post_id Target post ID.
179 * @return array Results of attempts to set each term to the target (new) post.
180 */
181 protected function update_post_type_terms( $source_post, $target_post_id ) {
182 $results = array();
183
184 $bypassed_post_types = apply_filters( 'jetpack_copy_post_bypassed_post_types', array( 'post', 'page' ), $source_post, $target_post_id );
185 if ( in_array( $source_post->post_type, $bypassed_post_types, true ) ) {
186 return $results;
187 }
188
189 $taxonomies = get_object_taxonomies( $source_post, 'objects' );
190 foreach ( $taxonomies as $taxonomy ) {
191 $terms = wp_get_post_terms( $source_post->ID, $taxonomy->name, array( 'fields' => 'ids' ) );
192 $results[] = wp_set_post_terms( $target_post_id, $terms, $taxonomy->name );
193 }
194
195 return $results;
196 }
197
198 /**
199 * Update the target post's featured image.
200 *
201 * @param WP_Post $source_post Post object to be copied.
202 * @param int $target_post_id Target post ID.
203 * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure.
204 */
205 protected function update_featured_image( $source_post, $target_post_id ) {
206 $featured_image_id = get_post_thumbnail_id( $source_post );
207 return update_post_meta( $target_post_id, '_thumbnail_id', $featured_image_id );
208 }
209
210 /**
211 * Update the target post's post format.
212 *
213 * @param WP_Post $source_post Post object to be copied.
214 * @param int $target_post_id Target post ID.
215 * @return array|WP_Error|false WP_Error on error, array of affected term IDs on success.
216 */
217 protected function update_post_format( $source_post, $target_post_id ) {
218 $post_format = get_post_format( $source_post );
219 return set_post_format( $target_post_id, $post_format );
220 }
221
222 /**
223 * Ensure the block editor doesn't modify the source post content for non-standard post formats.
224 *
225 * @param array $settings Settings to be passed into the block editor.
226 * @return array Settings with any `template` key removed.
227 */
228 public function remove_post_format_template( $settings ) {
229 unset( $settings['template'] );
230 return $settings;
231 }
232
233 /**
234 * Update the target post's Likes and Sharing statuses.
235 *
236 * @param WP_Post $source_post Post object to be copied.
237 * @param int $target_post_id Target post ID.
238 * @return array Array with the results of each update action.
239 */
240 protected function update_likes_sharing( $source_post, $target_post_id ) {
241 $likes = get_post_meta( $source_post->ID, 'switch_like_status', true );
242 $sharing = get_post_meta( $source_post->ID, 'sharing_disabled', true );
243
244 if ( '' !== $likes ) {
245 $likes_result = update_post_meta( $target_post_id, 'switch_like_status', $likes );
246 } else {
247 $likes_result = null;
248 }
249
250 if ( '' !== $sharing ) {
251 $sharing_result = update_post_meta( $target_post_id, 'sharing_disabled', $sharing );
252 } else {
253 $sharing_result = null;
254 }
255
256 return array(
257 'likes' => $likes_result,
258 'sharing' => $sharing_result,
259 );
260 }
261
262 /**
263 * Update the target post's title.
264 *
265 * @param string $post_title Post title determined by `get_default_post_to_edit()`.
266 * @param WP_Post $post Post object of newly-inserted post.
267 * @return string Updated post title from source post.
268 */
269 public function filter_title( $post_title, $post ) {
270 return $post->post_title;
271 }
272
273 /**
274 * Update the target post's content (`post_content`).
275 *
276 * @param string $post_content Post content determined by `get_default_post_to_edit()`.
277 * @param WP_Post $post Post object of newly-inserted post.
278 * @return string Updated post content from source post.
279 */
280 public function filter_content( $post_content, $post ) {
281 return $post->post_content;
282 }
283
284 /**
285 * Update the target post's excerpt.
286 *
287 * @param string $post_excerpt Post excerpt determined by `get_default_post_to_edit()`.
288 * @param WP_Post $post Post object of newly-inserted post.
289 * @return string Updated post excerpt from source post.
290 */
291 public function filter_excerpt( $post_excerpt, $post ) {
292 return $post->post_excerpt;
293 }
294
295 /**
296 * Validate the post type to be used for the target post.
297 *
298 * @param WP_Post $post Post object of current post in listing.
299 * @return bool True if the post type is in a list of supported psot types; false otherwise.
300 */
301 protected function validate_post_type( $post ) {
302 /**
303 * Fires when determining if the "Copy" row action should be made available.
304 * Allows overriding supported post types.
305 *
306 * @module copy-post
307 *
308 * @since 7.0.0
309 *
310 * @param array Post types supported by default.
311 * @param WP_Post $post Post object of current post in listing.
312 */
313 $valid_post_types = apply_filters(
314 'jetpack_copy_post_post_types',
315 array(
316 'post',
317 'page',
318 'jetpack-testimonial',
319 'jetpack-portfolio',
320 ),
321 $post
322 );
323 return in_array( $post->post_type, $valid_post_types, true );
324 }
325
326 /**
327 * Add a "Copy" row action to supported posts/pages/CPTs on list views.
328 *
329 * @param array $actions Existing actions.
330 * @param WP_Post $post Post object of current post in list.
331 * @return array Array of updated row actions.
332 */
333 public function add_row_action( $actions, $post ) {
334 if ( ! $this->user_can_access_post( $post->ID ) ||
335 ! $post instanceof WP_Post ||
336 ! $this->validate_post_type( $post ) ) {
337 return $actions;
338 }
339
340 $edit_url = add_query_arg(
341 array(
342 'post_type' => $post->post_type,
343 'jetpack-copy' => $post->ID,
344 ),
345 admin_url( 'post-new.php' )
346 );
347
348 $jetpack_logo = new Logo();
349 $edit_action = array(
350 'jetpack-copy' => sprintf(
351 '<a href="%1$s" aria-label="%2$s">%3$s %4$s</a>',
352 esc_url( $edit_url ),
353 esc_attr__( 'Duplicate this post with Jetpack.', 'jetpack' ),
354 esc_html__( 'Duplicate', 'jetpack' ),
355 $jetpack_logo->get_jp_emblem()
356 ),
357 );
358
359 // Insert the Copy action before the Trash action.
360 $edit_offset = array_search( 'trash', array_keys( $actions ), true );
361 $updated_actions = array_merge(
362 array_slice( $actions, 0, $edit_offset ),
363 $edit_action,
364 array_slice( $actions, $edit_offset )
365 );
366
367 /**
368 * Fires after the new Copy action has been added to the row actions.
369 * Allows changes to the action presentation, or other final checks.
370 *
371 * @module copy-post
372 *
373 * @since 7.0.0
374 *
375 * @param array $updated_actions Updated row actions with the Copy Post action.
376 * @param array $actions Original row actions passed to this filter.
377 * @param WP_Post $post Post object of current post in listing.
378 */
379 return apply_filters( 'jetpack_copy_post_row_actions', $updated_actions, $actions, $post );
380 }
381 }
382
383 /**
384 * Instantiate an instance of Jetpack_Copy_Post on the `admin_init` hook.
385 */
386 function jetpack_copy_post_init() {
387 new Jetpack_Copy_Post();
388 }
389 add_action( 'admin_init', 'jetpack_copy_post_init' );
390