Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
28.40% covered (danger)
28.40%
165 / 581
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_JSON_API_Update_Post_Endpoint
40.15% covered (danger)
40.15%
165 / 411
11.11% covered (danger)
11.11%
1 / 9
13436.13
0.00% covered (danger)
0.00%
0 / 1
 __construct
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 callback
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
4.84
 write_post
42.72% covered (danger)
42.72%
132 / 309
0.00% covered (danger)
0.00%
0 / 1
6902.79
 hook_publicize_skip_meta
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
10
 update_publicize_skip_meta
53.85% covered (warning)
53.85%
14 / 26
0.00% covered (danger)
0.00%
0 / 1
41.17
 delete_post
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
90
 restore_post
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 parse_and_set_featured_image
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 parse_and_set_author
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Update post endpoint.
4 *
5 * Endpoints:
6 * Create a post:  /sites/%s/posts/new
7 * Update a post:  /sites/%s/posts/%d
8 * Delete a post:  /sites/%s/posts/%d/delete
9 * Restore a post: /sites/%s/posts/%d/restore
10 */
11
12if ( ! defined( 'ABSPATH' ) ) {
13    exit( 0 );
14}
15
16new WPCOM_JSON_API_Update_Post_Endpoint(
17    array(
18        'description'          => 'Create a post.',
19        'group'                => 'posts',
20        'stat'                 => 'posts:new',
21        'new_version'          => '1.2',
22        'max_version'          => '1',
23        'method'               => 'POST',
24        'path'                 => '/sites/%s/posts/new',
25        'path_labels'          => array(
26            '$site' => '(int|string) Site ID or domain',
27        ),
28
29        'request_format'       => array(
30            // explicitly document all input.
31            'date'              => "(ISO 8601 datetime) The post's creation time.",
32            'title'             => '(HTML) The post title.',
33            'content'           => '(HTML) The post content.',
34            'excerpt'           => '(HTML) An optional post excerpt.',
35            'slug'              => '(string) The name (slug) for the post, used in URLs.',
36            'author'            => '(string) The username or ID for the user to assign the post to.',
37            'publicize'         => '(array|bool) True or false if the post be shared to external services. An array of services if we only want to share to a select few. Defaults to true.',
38            'publicize_message' => '(string) Custom message to be shared to external services.',
39            'status'            => array(
40                'publish'    => 'Publish the post.',
41                'private'    => 'Privately publish the post.',
42                'draft'      => 'Save the post as a draft.',
43                'pending'    => 'Mark the post as pending editorial approval.',
44                'auto-draft' => 'Save a placeholder for a newly created post, with no content.',
45            ),
46            'sticky'            => array(
47                'false' => 'Post is not marked as sticky.',
48                'true'  => 'Stick the post to the front page.',
49            ),
50            'password'          => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.',
51            'parent'            => "(int) The post ID of the new post's parent.",
52            'type'              => "(string) The post type. Defaults to 'post'. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.",
53            'categories'        => '(array|string) Comma-separated list or array of categories (name or id)',
54            'tags'              => '(array|string) Comma-separated list or array of tags (name or id)',
55            'format'            => array_merge( array( 'default' => 'Use default post format' ), get_post_format_strings() ),
56            'featured_image'    => '(string) The post ID of an existing attachment to set as the featured image. Pass an empty string to delete the existing image.',
57            'media'             => '(media) An array of files to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Multiple media items will be displayed in a gallery. Accepts  jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. Audio and Video may also be available. See <code>allowed_file_types</code> in the options response of the site endpoint. <br /><br /><strong>Example</strong>:<br />' .
58                            "<code>curl \<br />--form 'title=Image' \<br />--form 'media[]=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/posts/new'</code>",
59            'media_urls'        => '(array) An array of URLs for images to attach to a post. Sideloads the media in for a post.',
60            'metadata'          => '(array) Array of metadata objects containing the following properties: `key` (metadata key), `id` (meta ID), `previous_value` (if set, the action will only occur for the provided previous value), `value` (the new value to set the meta to), `operation` (the operation to perform: `update` or `add`; defaults to `update`). Unprotected meta keys can be changed by users who can edit the post. Protected meta keys (those starting with an underscore) can be changed only when the user can edit that key, which requires an <code>auth_callback</code> registered with <code>register_meta()</code> or an <code>auth_post_meta_{$meta_key}</code> filter. Keys allowed by the <code>rest_api_allowed_public_metadata</code> filter can also be added or updated.',
61            'comments_open'     => "(bool) Should the post be open to comments? Defaults to the blog's preference.",
62            'pings_open'        => "(bool) Should the post be open to comments? Defaults to the blog's preference.",
63            'likes_enabled'     => "(bool) Should the post be open to likes? Defaults to the blog's preference.",
64            'sharing_enabled'   => '(bool) Should sharing buttons show on this post? Defaults to true.',
65            'menu_order'        => '(int) (Pages Only) the order pages should appear in. Use 0 to maintain alphabetical order.',
66        ),
67
68        'example_request'      => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/new/',
69
70        'example_request_data' => array(
71            'headers' => array(
72                'authorization' => 'Bearer YOUR_API_TOKEN',
73            ),
74
75            'body'    => array(
76                'title'      => 'Hello World',
77                'content'    => 'Hello. I am a test post. I was created by the API',
78                'tags'       => 'tests',
79                'categories' => 'API',
80            ),
81        ),
82    )
83);
84
85new WPCOM_JSON_API_Update_Post_Endpoint(
86    array(
87        'description'          => 'Edit a post.',
88        'group'                => 'posts',
89        'stat'                 => 'posts:1:POST',
90        'new_version'          => '1.2',
91        'max_version'          => '1',
92        'method'               => 'POST',
93        'path'                 => '/sites/%s/posts/%d',
94        'path_labels'          => array(
95            '$site'    => '(int|string) Site ID or domain',
96            '$post_ID' => '(int) The post ID',
97        ),
98
99        'request_format'       => array(
100            'date'              => "(ISO 8601 datetime) The post's creation time.",
101            'title'             => '(HTML) The post title.',
102            'content'           => '(HTML) The post content.',
103            'excerpt'           => '(HTML) An optional post excerpt.',
104            'slug'              => '(string) The name (slug) for the post, used in URLs.',
105            'author'            => '(string) The username or ID for the user to assign the post to.',
106            'publicize'         => '(array|bool) True or false if the post be shared to external services. An array of services if we only want to share to a select few. Defaults to true.',
107            'publicize_message' => '(string) Custom message to be shared to external services.',
108            'status'            => array(
109                'publish' => 'Publish the post.',
110                'private' => 'Privately publish the post.',
111                'draft'   => 'Save the post as a draft.',
112                'pending' => 'Mark the post as pending editorial approval.',
113                'trash'   => 'Set the post as trashed.',
114            ),
115            'sticky'            => array(
116                'false' => 'Post is not marked as sticky.',
117                'true'  => 'Stick the post to the front page.',
118            ),
119            'password'          => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.',
120            'parent'            => "(int) The post ID of the new post's parent.",
121            'categories'        => '(array|string) Comma-separated list or array of categories (name or id)',
122            'tags'              => '(array|string) Comma-separated list or array of tags (name or id)',
123            'format'            => array_merge( array( 'default' => 'Use default post format' ), get_post_format_strings() ),
124            'comments_open'     => '(bool) Should the post be open to comments?',
125            'pings_open'        => '(bool) Should the post be open to comments?',
126            'likes_enabled'     => '(bool) Should the post be open to likes?',
127            'menu_order'        => '(int) (Pages Only) the order pages should appear in. Use 0 to maintain alphabetical order.',
128            'sharing_enabled'   => '(bool) Should sharing buttons show on this post?',
129            'featured_image'    => '(string) The post ID of an existing attachment to set as the featured image. Pass an empty string to delete the existing image.',
130            'media'             => '(media) An array of files to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Multiple media items will be displayed in a gallery. Accepts  jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. Audio and Video may also be available. See <code>allowed_file_types</code> in the options resposne of the site endpoint. <br /><br /><strong>Example</strong>:<br />' .
131                            "<code>curl \<br />--form 'title=Image' \<br />--form 'media[]=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/posts/new'</code>",
132            'media_urls'        => '(array) An array of URLs for images to attach to a post. Sideloads the media in for a post.',
133            'metadata'          => '(array) Array of metadata objects containing the following properties: `key` (metadata key), `id` (meta ID), `previous_value` (if set, the action will only occur for the provided previous value), `value` (the new value to set the meta to), `operation` (the operation to perform: `update` or `add`; defaults to `update`). Users who can edit the post get its unprotected meta keys. Protected meta keys (those starting with an underscore) are returned only when the user can edit that key, which requires an <code>auth_callback</code> registered with <code>register_meta()</code> or an <code>auth_post_meta_{$meta_key}</code> filter. Keys allowed by the <code>rest_api_allowed_public_metadata</code> filter, plus <code>_thumbnail_id</code> and keys starting with <code>geo_</code> or <code>_wpas_</code>, are returned to anyone who can read the post.',
134        ),
135
136        'example_request'      => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/881',
137
138        'example_request_data' => array(
139            'headers' => array(
140                'authorization' => 'Bearer YOUR_API_TOKEN',
141            ),
142
143            'body'    => array(
144                'title'      => 'Hello World (Again)',
145                'content'    => 'Hello. I am an edited post. I was edited by the API',
146                'tags'       => 'tests',
147                'categories' => 'API',
148            ),
149        ),
150    )
151);
152
153new WPCOM_JSON_API_Update_Post_Endpoint(
154    array(
155        'description'          => 'Delete a post. Note: If the trash is enabled, this request will send the post to the trash. A second request will permanently delete the post.',
156        'group'                => 'posts',
157        'stat'                 => 'posts:1:delete',
158        'new_version'          => '1.1',
159        'max_version'          => '1',
160        'method'               => 'POST',
161        'path'                 => '/sites/%s/posts/%d/delete',
162        'path_labels'          => array(
163            '$site'    => '(int|string) Site ID or domain',
164            '$post_ID' => '(int) The post ID',
165        ),
166
167        'example_request'      => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/$post_ID/delete/',
168
169        'example_request_data' => array(
170            'headers' => array(
171                'authorization' => 'Bearer YOUR_API_TOKEN',
172            ),
173        ),
174    )
175);
176
177new WPCOM_JSON_API_Update_Post_Endpoint(
178    array(
179        'description'          => 'Restore a post or page from the trash to its previous status.',
180        'group'                => 'posts',
181        'stat'                 => 'posts:1:restore',
182
183        'method'               => 'POST',
184        'new_version'          => '1.1',
185        'max_version'          => '1',
186        'path'                 => '/sites/%s/posts/%d/restore',
187        'path_labels'          => array(
188            '$site'    => '(int|string) Site ID or domain',
189            '$post_ID' => '(int) The post ID',
190        ),
191
192        'example_request'      => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/$post_ID/restore/',
193
194        'example_request_data' => array(
195            'headers' => array(
196                'authorization' => 'Bearer YOUR_API_TOKEN',
197            ),
198        ),
199    )
200);
201
202/**
203 * Update post endpoint class.
204 *
205 * @phan-constructor-used-for-side-effects
206 */
207class WPCOM_JSON_API_Update_Post_Endpoint extends WPCOM_JSON_API_Post_Endpoint {
208    /**
209     * WPCOM_JSON_API_Update_Post_Endpoint constructor.
210     *
211     * @param array $args Args.
212     */
213    public function __construct( $args ) {
214        parent::__construct( $args );
215        if ( $this->api->ends_with( $this->path, '/delete' ) ) {
216            $this->post_object_format['status']['deleted'] = 'The post has been deleted permanently.';
217        }
218    }
219
220    /**
221     * Update post API callback.
222     *
223     * /sites/%s/posts/new        -> $blog_id
224     * /sites/%s/posts/%d         -> $blog_id, $post_id
225     * /sites/%s/posts/%d/delete  -> $blog_id, $post_id
226     * /sites/%s/posts/%d/restore -> $blog_id, $post_id
227     *
228     * @param string $path API path.
229     * @param int    $blog_id Blog ID.
230     * @param int    $post_id Post ID.
231     *
232     * @return array|bool|WP_Error
233     */
234    public function callback( $path = '', $blog_id = 0, $post_id = 0 ) {
235        $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
236        if ( is_wp_error( $blog_id ) ) {
237            return $blog_id;
238        }
239
240        if ( $this->api->ends_with( $path, '/delete' ) ) {
241            return $this->delete_post( $path, $blog_id, $post_id );
242        } elseif ( $this->api->ends_with( $path, '/restore' ) ) {
243            return $this->restore_post( $path, $blog_id, $post_id );
244        } else {
245            return $this->write_post( $path, $blog_id, $post_id );
246        }
247    }
248
249    /**
250     * Create or update a post.
251     *
252     * /sites/%s/posts/new -> $blog_id
253     * /sites/%s/posts/%d  -> $blog_id, $post_id
254     *
255     * @param string $path API path.
256     * @param int    $blog_id Blog ID.
257     * @param int    $post_id Post ID.
258     */
259    public function write_post( $path, $blog_id, $post_id ) {
260        $delete_featured_image = null;
261        $new                   = $this->api->ends_with( $path, '/new' );
262        $args                  = $this->query_args();
263
264        // unhook publicize, it's hooked again later -- without this, skipping services is impossible.
265        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
266            remove_action( 'save_post', array( $GLOBALS['publicize_ui']->publicize, 'async_publicize_post' ), 100 );
267            add_action( 'rest_api_inserted_post', array( $GLOBALS['publicize_ui']->publicize, 'async_publicize_post' ) );
268        }
269
270        if ( $new ) {
271            $input = $this->input( true );
272
273            // default to post.
274            if ( empty( $input['type'] ) ) {
275                $input['type'] = 'post';
276            }
277
278            if ( 'revision' === $input['type'] ) {
279                if ( ! isset( $input['parent'] ) ) {
280                    return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
281                }
282                $input['status'] = 'inherit'; // force inherit for revision type.
283                $input['slug']   = $input['parent'] . '-autosave-v1';
284            } elseif ( ! isset( $input['title'] ) && ! isset( $input['content'] ) && ! isset( $input['excerpt'] ) ) {
285                return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
286            }
287
288            $post_type = get_post_type_object( $input['type'] );
289
290            if ( ! $this->is_post_type_allowed( $input['type'] ) ) {
291                return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
292            }
293
294            if ( ! empty( $input['author'] ) ) {
295                $author_id = $this->parse_and_set_author( $input['author'], $input['type'] );
296                unset( $input['author'] );
297                if ( is_wp_error( $author_id ) ) {
298                    return $author_id;
299                }
300            }
301
302            if ( 'publish' === $input['status'] ) {
303                if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
304                    if ( current_user_can( $post_type->cap->edit_posts ) ) {
305                        $input['status'] = 'pending';
306                    } else {
307                        return new WP_Error( 'unauthorized', 'User cannot publish posts', 403 );
308                    }
309                }
310            } elseif ( ! current_user_can( $post_type->cap->edit_posts ) ) {
311                return new WP_Error( 'unauthorized', 'User cannot edit posts', 403 );
312            }
313        } else {
314            $input = $this->input( false );
315
316            if ( ! is_array( $input ) || ! $input ) {
317                return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
318            }
319
320            if ( isset( $input['status'] ) && 'trash' === $input['status'] && ! current_user_can( 'delete_post', $post_id ) ) {
321                return new WP_Error( 'unauthorized', 'User cannot delete post', 403 );
322            }
323
324            $post       = get_post( $post_id );
325            $_post_type = ( ! empty( $input['type'] ) ) ? $input['type'] : $post->post_type;
326            $post_type  = get_post_type_object( $_post_type );
327            if ( ! $post || is_wp_error( $post ) ) {
328                return new WP_Error( 'unknown_post', 'Unknown post', 404 );
329            }
330
331            if ( ! current_user_can( 'edit_post', $post->ID ) ) {
332                return new WP_Error( 'unauthorized', 'User cannot edit post', 403 );
333            }
334
335            if ( ! empty( $input['author'] ) ) {
336                $author_id = $this->parse_and_set_author( $input['author'], $_post_type );
337                unset( $input['author'] );
338                if ( is_wp_error( $author_id ) ) {
339                    return $author_id;
340                }
341            }
342
343            if ( ( isset( $input['status'] ) && 'publish' === $input['status'] ) && 'publish' !== $post->post_status && ! current_user_can( 'publish_post', $post->ID ) ) {
344                $input['status'] = 'pending';
345            }
346            $last_status = $post->post_status;
347            $new_status  = $input['status'] ?? $last_status;
348
349            // Make sure that drafts get the current date when transitioning to publish if not supplied in the post.
350            $date_in_past = ( strtotime( $post->post_date_gmt ) < time() );
351            if ( 'publish' === $new_status && 'draft' === $last_status && ! isset( $input['date_gmt'] ) && $date_in_past ) {
352                $input['date_gmt'] = gmdate( 'Y-m-d H:i:s' );
353            }
354
355            // Untrash a post so that the proper hooks get called as well as the comments get untrashed.
356            if ( 'trash' === $last_status && 'trash' !== $new_status && isset( $post->ID ) ) {
357                wp_untrash_post( $post->ID );
358                $untashed_post = get_post( $post->ID );
359                // Lets make sure that we use the revert the slug.
360                if ( isset( $untashed_post->post_name ) && $untashed_post->post_name . '__trashed' === $input['slug'] ) {
361                    unset( $input['slug'] );
362                }
363            }
364        }
365
366        if ( function_exists( 'wpcom_switch_to_locale' ) ) {
367            // fixes calypso-pre-oss #12476: respect blog locale when creating the post slug.
368            wpcom_switch_to_locale( get_blog_lang_code( $blog_id ) );
369        }
370
371        // If date was set, $this->input will set date_gmt, date still needs to be adjusted for the blog's offset.
372        if ( isset( $input['date_gmt'] ) ) {
373            $gmt_offset       = get_option( 'gmt_offset' );
374            $time_with_offset = strtotime( $input['date_gmt'] ) + $gmt_offset * HOUR_IN_SECONDS;
375            $input['date']    = gmdate( 'Y-m-d H:i:s', $time_with_offset );
376        }
377
378        if ( ! empty( $author_id ) && get_current_user_id() !== $author_id ) {
379            if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) {
380                return new WP_Error( 'unauthorized', "User is not allowed to publish others' posts.", 403 );
381            } elseif ( ! user_can( $author_id, $post_type->cap->edit_posts ) ) {
382                return new WP_Error( 'unauthorized', 'Assigned author cannot publish post.', 403 );
383            }
384        }
385
386        if ( ! is_post_type_hierarchical( $post_type->name ) && 'revision' !== $post_type->name ) {
387            unset( $input['parent'] );
388        }
389
390        $tax_input = array();
391
392        foreach ( array(
393            'categories' => 'category',
394            'tags'       => 'post_tag',
395        ) as $key => $taxonomy ) {
396            if ( ! isset( $input[ $key ] ) ) {
397                continue;
398            }
399
400            $tax_input[ $taxonomy ] = array();
401
402            $is_hierarchical = is_taxonomy_hierarchical( $taxonomy );
403
404            if ( is_array( $input[ $key ] ) ) {
405                $terms = $input[ $key ];
406            } else {
407                $terms = explode( ',', $input[ $key ] );
408            }
409
410            foreach ( $terms as $term ) {
411                /**
412                 * `curl --data 'category[]=123'` should be interpreted as a category ID,
413                 * not a category whose name is '123'.
414                 *
415                 * Consequence: To add a category/tag whose name is '123', the client must
416                 * first look up its ID.
417                 */
418                $term = (string) $term; // ctype_digit compat.
419                if ( ctype_digit( $term ) ) {
420                    $term = (int) $term;
421                }
422
423                $term_info = term_exists( $term, $taxonomy );
424
425                if ( ! $term_info ) {
426                    // A term ID that doesn't already exist. Ignore it: we don't know what name to give it.
427                    if ( is_int( $term ) ) {
428                        continue;
429                    }
430                    // only add a new tag/cat if the user has access to.
431                    $tax = get_taxonomy( $taxonomy );
432
433                    // see https://core.trac.wordpress.org/ticket/26409 .
434                    if ( 'category' === $taxonomy && ! current_user_can( $tax->cap->edit_terms ) ) {
435                        continue;
436                    } elseif ( ! current_user_can( $tax->cap->assign_terms ) ) {
437                        continue;
438                    }
439
440                    $term_info = wp_insert_term( $term, $taxonomy );
441                }
442
443                if ( ! is_wp_error( $term_info ) ) {
444                    if ( $is_hierarchical ) {
445                        // Categories must be added by ID.
446                        $tax_input[ $taxonomy ][] = (int) $term_info['term_id'];
447                    } elseif ( is_int( $term ) ) { // Tags must be added by name.
448                        $term                     = get_term( $term, $taxonomy );
449                        $tax_input[ $taxonomy ][] = $term->name;
450                    } else {
451                        $tax_input[ $taxonomy ][] = $term;
452                    }
453                }
454            }
455        }
456
457        if ( isset( $input['categories'] ) && empty( $tax_input['category'] ) && 'revision' !== $post_type->name ) {
458            $tax_input['category'][] = get_option( 'default_category' );
459        }
460
461        unset( $input['tags'], $input['categories'] );
462
463        $insert = array();
464
465        if ( ! empty( $input['slug'] ) ) {
466            $insert['post_name'] = $input['slug'];
467            unset( $input['slug'] );
468        }
469
470        if ( isset( $input['comments_open'] ) ) {
471            $insert['comment_status'] = ( true === $input['comments_open'] ) ? 'open' : 'closed';
472        }
473
474        if ( isset( $input['pings_open'] ) ) {
475            $insert['ping_status'] = ( true === $input['pings_open'] ) ? 'open' : 'closed';
476        }
477
478        unset( $input['comments_open'], $input['pings_open'] );
479
480        if ( isset( $input['menu_order'] ) ) {
481            $insert['menu_order'] = $input['menu_order'];
482            unset( $input['menu_order'] );
483        }
484
485        $publicize = $input['publicize'] ?? null;
486        // The (array|bool) input type casts a JSON boolean to array( true ) or array( false ).
487        if ( array( true ) === $publicize || array( false ) === $publicize ) {
488            $publicize = $publicize[0];
489        }
490        unset( $input['publicize'] );
491
492        $publicize_custom_message = $input['publicize_message'] ?? null;
493        unset( $input['publicize_message'] );
494
495        if ( isset( $input['featured_image'] ) ) {
496            $featured_image        = trim( $input['featured_image'] );
497            $delete_featured_image = empty( $featured_image );
498            unset( $input['featured_image'] );
499        }
500
501        $metadata = $input['metadata'] ?? null;
502        unset( $input['metadata'] );
503
504        $likes = $input['likes_enabled'] ?? null;
505        unset( $input['likes_enabled'] );
506
507        $sharing = $input['sharing_enabled'] ?? null;
508        unset( $input['sharing_enabled'] );
509
510        $sticky = $input['sticky'] ?? null;
511        unset( $input['sticky'] );
512
513        foreach ( $input as $key => $value ) {
514            $insert[ "post_$key" ] = $value;
515        }
516
517        if ( ! empty( $author_id ) ) {
518            $insert['post_author'] = absint( $author_id );
519        }
520
521        if ( ! empty( $tax_input ) ) {
522            $insert['tax_input'] = $tax_input;
523        }
524
525        $has_media        = isset( $input['media'] ) && $input['media'] ? count( $input['media'] ) : false;
526        $has_media_by_url = isset( $input['media_urls'] ) && $input['media_urls'] ? count( $input['media_urls'] ) : false;
527
528        $skip_publicize = false === $publicize;
529
530        $block_publicize_flag = $skip_publicize && ! ( defined( 'IS_WPCOM' ) && IS_WPCOM );
531
532        /**
533         * On Jetpack sites, Sync flags the post for sharing on publish, before the skip meta below exists.
534         * WordPress.com unhooks sharing from save_post above instead.
535         *
536         * @return false
537         */
538        $block_publicize = static function () {
539            return false;
540        };
541        if ( $block_publicize_flag ) {
542            add_filter( 'publicize_should_publicize_published_post', $block_publicize, PHP_INT_MAX );
543        }
544
545        $skip_meta_post_id = 0;
546        $skip_meta_hook    = $this->hook_publicize_skip_meta( $new ? 0 : (int) $post_id, $post_type->name, $publicize, $skip_meta_post_id );
547
548        if ( $new ) {
549
550            if ( isset( $input['content'] ) && ! has_shortcode( $input['content'], 'gallery' ) && ( $has_media || $has_media_by_url ) ) {
551                switch ( ( $has_media + $has_media_by_url ) ) {
552                    case 0:
553                        // No images - do nothing.
554                        break;
555                    case 1:
556                        // 1 image - make it big.
557                        $input['content']       = "[gallery size=full columns=1]\n\n" . $input['content'];
558                        $insert['post_content'] = $input['content'];
559                        break;
560                    default:
561                        // Several images - 3 column gallery.
562                        $input['content']       = "[gallery]\n\n" . $input['content'];
563                        $insert['post_content'] = $input['content'];
564                        break;
565                }
566            }
567
568            $post_id = wp_insert_post( add_magic_quotes( $insert ), true );
569        } else {
570            // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $post is set and validated several blocks earlier if $new (only set once) is falsy.
571            $insert['ID'] = $post->ID;
572
573            // wp_update_post ignores date unless edit_date is set
574            // See: https://codex.wordpress.org/Function_Reference/wp_update_post#Scheduling_posts
575            // See: https://core.trac.wordpress.org/browser/tags/3.9.2/src/wp-includes/post.php#L3302 .
576            if ( isset( $input['date_gmt'] ) || isset( $input['date'] ) ) {
577                $insert['edit_date'] = true;
578            }
579
580            // this two-step process ensures any changes submitted along with status=trash get saved before trashing.
581            if ( isset( $input['status'] ) && 'trash' === $input['status'] ) {
582                // if we insert it with status='trash', it will get double-trashed, so insert it as a draft first.
583                unset( $insert['status'] );
584                $post_id = wp_update_post( (object) $insert );
585                // now call wp_trash_post so post_meta gets set and any filters get called.
586                wp_trash_post( $post_id );
587            } else {
588                $post_id = wp_update_post( (object) $insert );
589            }
590        }
591
592        if ( $block_publicize_flag ) {
593            remove_filter( 'publicize_should_publicize_published_post', $block_publicize, PHP_INT_MAX );
594        }
595
596        if ( $skip_meta_hook ) {
597            remove_action( 'wp_insert_post', $skip_meta_hook, PHP_INT_MAX );
598        }
599
600        if ( ! $post_id || is_wp_error( $post_id ) ) {
601            return $post_id;
602        }
603
604        // make sure this post actually exists and is not an error of some kind (ie, trying to load media in the posts endpoint).
605        $post_check = $this->get_post_by( 'ID', $post_id, $args['context'] );
606        if ( is_wp_error( $post_check ) ) {
607            return $post_check;
608        }
609
610        if ( $has_media ) {
611            $this->api->trap_wp_die( 'upload_error' );
612            foreach ( $input['media'] as $media_item ) {
613                $_FILES['.api.media.item.'] = $media_item;
614                // check for WP_Error if we ever actually need $media_id .
615                $media_id = media_handle_upload( '.api.media.item.', $post_id ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
616            }
617            $this->api->trap_wp_die( null );
618
619            unset( $_FILES['.api.media.item.'] );
620        }
621
622        if ( $has_media_by_url ) {
623            foreach ( $input['media_urls'] as $url ) {
624                $this->handle_media_sideload( $url, $post_id );
625            }
626        }
627
628        // Set like status for the post.
629        /** This filter is documented in modules/likes.php */
630        $sitewide_likes_enabled = (bool) apply_filters( 'wpl_is_enabled_sitewide', ! get_option( 'disabled_likes' ) );
631        if ( $new ) {
632            if ( $sitewide_likes_enabled ) {
633                if ( false === $likes ) {
634                    update_post_meta( $post_id, 'switch_like_status', 0 );
635                } else {
636                    delete_post_meta( $post_id, 'switch_like_status' );
637                }
638            } elseif ( $likes ) {
639                update_post_meta( $post_id, 'switch_like_status', 1 );
640            } else {
641                delete_post_meta( $post_id, 'switch_like_status' );
642            }
643        } elseif ( isset( $likes ) ) {
644            if ( $sitewide_likes_enabled ) {
645                if ( false === $likes ) {
646                    update_post_meta( $post_id, 'switch_like_status', 0 );
647                } else {
648                    delete_post_meta( $post_id, 'switch_like_status' );
649                }
650            } elseif ( true === $likes ) {
651                update_post_meta( $post_id, 'switch_like_status', 1 );
652            } else {
653                delete_post_meta( $post_id, 'switch_like_status' );
654            }
655        }
656
657        // Set sharing status of the post.
658        if ( $new ) {
659            $sharing_enabled = isset( $sharing ) ? (bool) $sharing : true;
660            if ( ! $sharing_enabled ) {
661                update_post_meta( $post_id, 'sharing_disabled', 1 );
662            }
663        } elseif ( isset( $sharing ) && true === $sharing ) {
664            delete_post_meta( $post_id, 'sharing_disabled' );
665        } elseif ( isset( $sharing ) && false == $sharing ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
666            update_post_meta( $post_id, 'sharing_disabled', 1 );
667        }
668
669        if ( isset( $sticky ) ) {
670            if ( true === $sticky ) {
671                stick_post( $post_id );
672            } else {
673                unstick_post( $post_id );
674            }
675        }
676
677        // WPCOM Specific (Jetpack's will get bumped elsewhere
678        // Tracks how many posts are published and sets meta
679        // so we can track some other cool stats (like likes & comments on posts published).
680        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
681            if (
682                ( $new && 'publish' === $input['status'] )
683                || (
684                    ! $new && isset( $last_status )
685                    && 'publish' !== $last_status
686                    && isset( $new_status )
687                    && 'publish' === $new_status
688                )
689            ) {
690                /** This action is documented in modules/widgets/social-media-icons.php */
691                do_action( 'jetpack_bump_stats_extras', 'api-insights-posts', $this->api->token_details['client_id'] );
692                update_post_meta( $post_id, '_rest_api_published', 1 );
693                update_post_meta( $post_id, '_rest_api_client_id', $this->api->token_details['client_id'] );
694            }
695        }
696
697        if ( (int) $post_id !== $skip_meta_post_id ) {
698            $this->update_publicize_skip_meta( $post_id, $publicize );
699        }
700
701        if ( $publicize_custom_message !== null ) {
702            if ( empty( $publicize_custom_message ) ) {
703                delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_MESS );
704            } else {
705                update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_MESS, trim( $publicize_custom_message ) );
706            }
707        }
708
709        if ( ! empty( $insert['post_format'] ) ) {
710            if ( 'default' !== strtolower( $insert['post_format'] ) ) {
711                set_post_format( $post_id, $insert['post_format'] );
712            } else {
713                set_post_format( $post_id, get_option( 'default_post_format' ) );
714            }
715        }
716
717        if ( isset( $featured_image ) ) {
718            $this->parse_and_set_featured_image( $post_id, $delete_featured_image, $featured_image );
719        }
720
721        if ( ! empty( $metadata ) ) {
722            foreach ( (array) $metadata as $meta ) {
723
724                $meta = (object) $meta;
725
726                if (
727                    in_array( $meta->key ?? null, Jetpack_SEO_Posts::POST_META_KEYS_ARRAY, true ) &&
728                    ! Jetpack_SEO_Utils::is_enabled_jetpack_seo()
729                ) {
730                    return new WP_Error( 'unauthorized', __( 'SEO tools are not enabled for this site.', 'jetpack' ), 403 );
731                }
732
733                $existing_meta_item = new stdClass();
734
735                if ( empty( $meta->operation ) ) {
736                    $meta->operation = 'update';
737                }
738
739                if ( ! empty( $meta->value ) ) {
740                    if ( 'true' == $meta->value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
741                        $meta->value = true;
742                    }
743                    if ( 'false' == $meta->value ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
744                        $meta->value = false;
745                    }
746                }
747
748                if ( ! empty( $meta->id ) ) {
749                    $meta->id           = absint( $meta->id );
750                    $existing_meta_item = get_metadata_by_mid( 'post', $meta->id );
751                    if ( $post_id !== (int) $existing_meta_item->post_id ) {
752                        // Only allow updates for metadata on this post.
753                        continue;
754                    }
755                }
756
757                $unslashed_meta_key           = isset( $meta->key ) ? wp_unslash( $meta->key ) : null; // should match what the final key will be.
758                $meta->key                    = isset( $meta->key ) ? wp_slash( $meta->key ) : null;
759                $unslashed_existing_meta_key  = isset( $existing_meta_item->meta_key ) ? wp_unslash( $existing_meta_item->meta_key ) : null;
760                $existing_meta_item->meta_key = isset( $existing_meta_item->meta_key ) ? wp_slash( $existing_meta_item->meta_key ) : null;
761
762                // make sure that the meta id passed matches the existing meta key.
763                if ( ! empty( $meta->id ) && ! empty( $meta->key ) ) {
764                    $meta_by_id = get_metadata_by_mid( 'post', $meta->id );
765                    if ( $meta_by_id->meta_key !== $meta->key ) {
766                        continue; // skip this meta.
767                    }
768                }
769
770                switch ( $meta->operation ) {
771                    case 'delete':
772                        if ( ! empty( $meta->id ) && ! empty( $existing_meta_item->meta_key ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_existing_meta_key ) ) {
773                            delete_metadata_by_mid( 'post', $meta->id );
774                        } elseif ( ! empty( $meta->key ) && ! empty( $meta->previous_value ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_meta_key ) ) {
775                            delete_post_meta( $post_id, $meta->key, $meta->previous_value );
776                        } elseif ( ! empty( $meta->key ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_meta_key ) ) {
777                            delete_post_meta( $post_id, $meta->key );
778                        }
779
780                        break;
781                    case 'add':
782                        if ( ! empty( $meta->id ) || ! empty( $meta->previous_value ) ) {
783                            break;
784                        } elseif ( ! empty( $meta->key ) && ! empty( $meta->value ) && ( current_user_can( 'add_post_meta', $post_id, $unslashed_meta_key ) ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) {
785                            add_post_meta( $post_id, $meta->key, $meta->value );
786                        }
787
788                        break;
789                    case 'update':
790                        if ( ! isset( $meta->value ) ) {
791                            break;
792                        } elseif ( ! empty( $meta->id ) && ! empty( $existing_meta_item->meta_key ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_existing_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) {
793                            update_metadata_by_mid( 'post', $meta->id, $meta->value );
794                        } elseif ( ! empty( $meta->key ) && ! empty( $meta->previous_value ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) {
795                            update_post_meta( $post_id, $meta->key, $meta->value, $meta->previous_value );
796                        } elseif ( ! empty( $meta->key ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) {
797                            update_post_meta( $post_id, $meta->key, $meta->value );
798                        }
799
800                        break;
801                }
802            }
803        }
804
805        /**
806         * Fires when a post is created via the REST API.
807         *
808         * @module json-api
809         *
810         * @since 2.3.0
811         *
812         * @param int $post_id Post ID.
813         * @param array $insert Data used to build the post.
814         * @param string $new New post URL suffix.
815         */
816        do_action( 'rest_api_inserted_post', $post_id, $insert, $new );
817
818        $return = $this->get_post_by( 'ID', $post_id, $args['context'] );
819        if ( ! $return || is_wp_error( $return ) ) {
820            return $return;
821        }
822
823        if ( isset( $input['type'] ) && 'revision' === $input['type'] ) {
824            $return['preview_nonce'] = wp_create_nonce( 'post_preview_' . $input['parent'] );
825        }
826
827        if ( isset( $sticky ) ) {
828            // workaround for sticky test occasionally failing, maybe a race condition with stick_post() above.
829            $return['sticky'] = ( true === $sticky );
830        }
831
832        /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
833        do_action( 'wpcom_json_api_objects', 'posts' );
834
835        return $return;
836    }
837
838    /**
839     * On Jetpack sites, writes the skip meta while the post is saved, before Sync builds the publish event.
840     *
841     * @param int        $post_id         ID of the post being updated, or 0 for a new post.
842     * @param string     $post_type       Post type of the post being saved.
843     * @param array|bool $publicize       The request's `publicize` value.
844     * @param int        $written_post_id Set to the ID of the post the skip meta was written to.
845     * @return Closure|null The hooked callback, or null when nothing is hooked.
846     */
847    protected function hook_publicize_skip_meta( $post_id, $post_type, $publicize, &$written_post_id ) {
848        if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || ( false !== $publicize && ( ! is_array( $publicize ) || array() === $publicize ) ) ) {
849            return null;
850        }
851
852        $callback = function ( $saved_post_id, $saved_post, $update ) use ( $post_id, $post_type, $publicize, &$written_post_id, &$callback ) {
853            // Other posts can be inserted during the save, such as a page's auto-added menu item.
854            $is_saved_post = $post_id ? $post_id === $saved_post_id : ! $update && $post_type === $saved_post->post_type;
855            if ( ! $is_saved_post || wp_is_post_revision( $saved_post ) ) {
856                return;
857            }
858            remove_action( 'wp_insert_post', $callback, PHP_INT_MAX );
859            $written_post_id = $saved_post_id;
860            $this->update_publicize_skip_meta( $saved_post_id, $publicize );
861        };
862        add_action( 'wp_insert_post', $callback, PHP_INT_MAX, 3 );
863
864        return $callback;
865    }
866
867    /**
868     * Marks the connections left out of the request's `publicize` value to be skipped.
869     *
870     * @param int        $post_id   Post ID.
871     * @param array|bool $publicize The request's `publicize` value.
872     */
873    protected function update_publicize_skip_meta( $post_id, $publicize ) {
874        $skip_publicize = false === $publicize;
875
876        // We ask the user/dev to pass Publicize services he/she wants activated for the post, but Publicize expects us
877        // to instead flag the ones we don't want to be skipped. proceed with said logic.
878        if ( $skip_publicize ) {
879            // No publicize at all, skip all by ID.
880            foreach ( $GLOBALS['publicize_ui']->publicize->get_services( 'all' ) as $name => $service ) {
881                delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $name );
882                $service_connections = $GLOBALS['publicize_ui']->publicize->get_connections( $name );
883                if ( ! $service_connections ) {
884                    continue;
885                }
886                foreach ( $service_connections as $service_connection ) {
887                    update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $GLOBALS['publicize_ui']->publicize->get_connection_unique_id( $service_connection ), 1 );
888                }
889            }
890        } elseif ( is_array( $publicize ) && ( $publicize !== array() ) ) {
891            foreach ( $GLOBALS['publicize_ui']->publicize->get_services( 'all' ) as $name => $service ) {
892                /*
893                * We support both indexed and associative arrays:
894                * * indexed are to pass entire services
895                * * associative are to pass specific connections per service
896                *
897                * We do support mixed arrays: mixed integer and string keys (see 3rd example below).
898                *
899                * EG: array( 'linkedin', 'facebook') will only publicize to those, ignoring the other available services
900                *      Form data: publicize[]=linkedin&publicize[]=facebook
901                * EG: array( 'linkedin' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3', 'facebook' => (int) $pub_conn_id_7 ) will publicize to two LinkedIn accounts, and one Facebook connection, of potentially many.
902                *      Form data: publicize[linkedin]=$pub_conn_id_0,$pub_conn_id_3&publicize[facebook]=$pub_conn_id_7
903                * EG: array( 'linkedin', 'facebook' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3' ) will publicize to all available LinkedIn accounts, but only 2 of potentially many Facebook connections
904                *      Form data: publicize[]=linkedin&publicize[facebook]=$pub_conn_id_0,$pub_conn_id_3
905                */
906
907                // Delete any stale SKIP value for the service by name. We'll add it back by ID.
908                delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $name );
909
910                // Get the user's connections.
911                $service_connections = $GLOBALS['publicize_ui']->publicize->get_connections( $name );
912
913                // if the user doesn't have any connections for this service, move on.
914                if ( ! $service_connections ) {
915                    continue;
916                }
917
918                if ( ! in_array( $name, $publicize, true ) && ! array_key_exists( $name, $publicize ) ) {
919                    // Skip the whole service by adding each connection ID.
920                    foreach ( $service_connections as $service_connection ) {
921                        update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $GLOBALS['publicize_ui']->publicize->get_connection_unique_id( $service_connection ), 1 );
922                    }
923                } elseif ( ! empty( $publicize[ $name ] ) ) {
924                    // Seems we're being asked to only push to [a] specific connection[s].
925                    // Explode the list on commas, which will also support a single passed ID.
926                    $requested_connections = explode( ',', ( preg_replace( '/[\s]*/', '', $publicize[ $name ] ) ) );
927                    // Flag the connections we can't match with the requested list to be skipped.
928                    foreach ( $service_connections as $service_connection ) {
929                        if ( ! in_array( (string) $GLOBALS['publicize_ui']->publicize->get_connection_id( $service_connection ), $requested_connections, true ) ) {
930                            update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $GLOBALS['publicize_ui']->publicize->get_connection_unique_id( $service_connection ), 1 );
931                        } else {
932                            delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $GLOBALS['publicize_ui']->publicize->get_connection_unique_id( $service_connection ) );
933                        }
934                    }
935                } else {
936                    // delete all SKIP values; it's okay to publish to all connected IDs for this service.
937                    foreach ( $service_connections as $service_connection ) {
938                        delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $GLOBALS['publicize_ui']->publicize->get_connection_unique_id( $service_connection ) );
939                    }
940                }
941            }
942        }
943    }
944
945    /**
946     * Delete a post.
947     *
948     * /sites/%s/posts/%d/delete -> $blog_id, $post_id
949     *
950     * @param string $path API path.
951     * @param array  $blog_id Blog ID.
952     * @param array  $post_id Post ID.
953     *
954     * @return array|WP_Error
955     */
956    public function delete_post( $path, $blog_id, $post_id ) {
957        $post = get_post( $post_id );
958        if ( ! $post || is_wp_error( $post ) ) {
959            return new WP_Error( 'unknown_post', 'Unknown post', 404 );
960        }
961
962        if ( ! $this->is_post_type_allowed( $post->post_type ) ) {
963            return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
964        }
965
966        if ( ! current_user_can( 'delete_post', $post->ID ) ) {
967            return new WP_Error( 'unauthorized', 'User cannot delete posts', 403 );
968        }
969
970        $args   = $this->query_args();
971        $return = $this->get_post_by( 'ID', $post->ID, $args['context'] );
972        if ( ! $return || is_wp_error( $return ) ) {
973            return $return;
974        }
975
976        /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
977        do_action( 'wpcom_json_api_objects', 'posts' );
978
979        // we need to call wp_trash_post so that untrash will work correctly for all post types.
980        if ( 'trash' === $post->post_status ) {
981            wp_delete_post( $post->ID );
982        } else {
983            wp_trash_post( $post->ID );
984        }
985
986        $status = get_post_status( $post->ID );
987        if ( false === $status ) {
988            $return['status'] = 'deleted';
989            return $return;
990        }
991
992        return $this->get_post_by( 'ID', $post->ID, $args['context'] );
993    }
994
995    /**
996     * Restore a post.
997     *
998     * /sites/%s/posts/%d/restore -> $blog_id, $post_id
999     *
1000     * @param string $path API path.
1001     * @param int    $blog_id Blog ID.
1002     * @param int    $post_id Post ID.
1003     *
1004     * @return array|WP_Error
1005     */
1006    public function restore_post( $path, $blog_id, $post_id ) {
1007        $args = $this->query_args();
1008        $post = get_post( $post_id );
1009
1010        if ( ! $post || is_wp_error( $post ) ) {
1011            return new WP_Error( 'unknown_post', 'Unknown post', 404 );
1012        }
1013
1014        if ( ! current_user_can( 'delete_post', $post->ID ) ) {
1015            return new WP_Error( 'unauthorized', 'User cannot restore trashed posts', 403 );
1016        }
1017
1018        /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
1019        do_action( 'wpcom_json_api_objects', 'posts' );
1020
1021        wp_untrash_post( $post->ID );
1022
1023        return $this->get_post_by( 'ID', $post->ID, $args['context'] );
1024    }
1025
1026    /**
1027     * Set or delete a post's featured image.
1028     *
1029     * @param int  $post_id Post ID.
1030     * @param bool $delete_featured_image Whether to delete the featured image.
1031     * @param int  $featured_image Thumbnail ID to attach.
1032     *
1033     * @return null|int|bool
1034     */
1035    private function parse_and_set_featured_image( $post_id, $delete_featured_image, $featured_image ) {
1036        if ( $delete_featured_image ) {
1037            delete_post_thumbnail( $post_id );
1038            return;
1039        }
1040
1041        $featured_image = (string) $featured_image;
1042
1043        // if we got a post ID, we can just set it as the thumbnail.
1044        if ( ctype_digit( $featured_image ) && 'attachment' === get_post_type( $featured_image ) ) {
1045            set_post_thumbnail( $post_id, $featured_image );
1046            return $featured_image;
1047        }
1048
1049        $featured_image_id = $this->handle_media_sideload( $featured_image, $post_id, 'image' );
1050
1051        if ( empty( $featured_image_id ) || ! is_int( $featured_image_id ) ) {
1052            return false;
1053        }
1054
1055        set_post_thumbnail( $post_id, $featured_image_id );
1056        return $featured_image_id;
1057    }
1058
1059    /**
1060     * Get the Author ID for a post.
1061     *
1062     * @param int|string $author Author ID.
1063     * @param string     $post_type Post type.
1064     *
1065     * @return int|WP_Error
1066     */
1067    private function parse_and_set_author( $author = null, $post_type = 'post' ) {
1068        if ( empty( $author ) || ! post_type_supports( $post_type, 'author' ) ) {
1069            return get_current_user_id();
1070        }
1071
1072        $author = (string) $author;
1073        if ( ctype_digit( $author ) ) {
1074            $_user = get_user_by( 'id', $author );
1075            if ( ! $_user || is_wp_error( $_user ) ) {
1076                return new WP_Error( 'invalid_author', 'Invalid author provided' );
1077            }
1078
1079            return $_user->ID;
1080        }
1081
1082        $_user = get_user_by( 'login', $author );
1083        if ( ! $_user || is_wp_error( $_user ) ) {
1084            return new WP_Error( 'invalid_author', 'Invalid author provided' );
1085        }
1086
1087        return $_user->ID;
1088    }
1089}