PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 14.9
Jetpack – WP Security, Backup, Speed, & Growth v14.9
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 14.9, at modules/copy-post.php

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