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

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