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