PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.0-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.0-beta
16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / json-endpoints / class.wpcom-json-api-update-post-v1-1-endpoint.php
jetpack / json-endpoints Last commit date
jetpack 1 week ago class.wpcom-json-api-add-widget-endpoint.php 7 months ago class.wpcom-json-api-autosave-post-v1-1-endpoint.php 7 months ago class.wpcom-json-api-bulk-delete-post-endpoint.php 7 months ago class.wpcom-json-api-bulk-restore-post-endpoint.php 7 months ago class.wpcom-json-api-bulk-update-comments-endpoint.php 1 month ago class.wpcom-json-api-comment-endpoint.php 7 months ago class.wpcom-json-api-delete-media-endpoint.php 7 months ago class.wpcom-json-api-delete-media-v1-1-endpoint.php 7 months ago class.wpcom-json-api-edit-media-v1-2-endpoint.php 1 month ago class.wpcom-json-api-get-autosave-v1-1-endpoint.php 7 months ago class.wpcom-json-api-get-comment-counts-endpoint.php 7 months ago class.wpcom-json-api-get-comment-endpoint.php 7 months ago class.wpcom-json-api-get-comment-history-endpoint.php 7 months ago class.wpcom-json-api-get-comments-tree-endpoint.php 7 months ago class.wpcom-json-api-get-comments-tree-v1-1-endpoint.php 7 months ago class.wpcom-json-api-get-comments-tree-v1-2-endpoint.php 7 months ago class.wpcom-json-api-get-customcss.php 7 months ago class.wpcom-json-api-get-media-endpoint.php 7 months ago class.wpcom-json-api-get-media-v1-1-endpoint.php 7 months ago class.wpcom-json-api-get-media-v1-2-endpoint.php 7 months ago class.wpcom-json-api-get-post-counts-v1-1-endpoint.php 1 week ago class.wpcom-json-api-get-post-endpoint.php 7 months ago class.wpcom-json-api-get-post-v1-1-endpoint.php 7 months ago class.wpcom-json-api-get-site-endpoint.php 1 week ago class.wpcom-json-api-get-site-v1-2-endpoint.php 1 week ago class.wpcom-json-api-get-taxonomies-endpoint.php 2 months ago class.wpcom-json-api-get-taxonomy-endpoint.php 7 months ago class.wpcom-json-api-get-term-endpoint.php 7 months ago class.wpcom-json-api-list-comments-endpoint.php 1 week ago class.wpcom-json-api-list-dropdown-pages-endpoint.php 7 months ago class.wpcom-json-api-list-embeds-endpoint.php 7 months ago class.wpcom-json-api-list-media-endpoint.php 7 months ago class.wpcom-json-api-list-media-v1-1-endpoint.php 1 month ago class.wpcom-json-api-list-media-v1-2-endpoint.php 7 months ago class.wpcom-json-api-list-post-type-taxonomies-endpoint.php 7 months ago class.wpcom-json-api-list-post-types-endpoint.php 7 months ago class.wpcom-json-api-list-posts-endpoint.php 1 week ago class.wpcom-json-api-list-posts-v1-1-endpoint.php 1 week ago class.wpcom-json-api-list-posts-v1-2-endpoint.php 1 week ago class.wpcom-json-api-list-roles-endpoint.php 7 months ago class.wpcom-json-api-list-shortcodes-endpoint.php 7 months ago class.wpcom-json-api-list-terms-endpoint.php 7 months ago class.wpcom-json-api-list-users-endpoint.php 1 week ago class.wpcom-json-api-menus-v1-1-endpoint.php 1 month ago class.wpcom-json-api-post-endpoint.php 7 months ago class.wpcom-json-api-post-v1-1-endpoint.php 2 months ago class.wpcom-json-api-render-embed-endpoint.php 7 months ago class.wpcom-json-api-render-embed-reversal-endpoint.php 7 months ago class.wpcom-json-api-render-endpoint.php 1 month ago class.wpcom-json-api-render-shortcode-endpoint.php 7 months ago class.wpcom-json-api-sharing-buttons-endpoint.php 1 month ago class.wpcom-json-api-site-settings-endpoint.php 1 week ago class.wpcom-json-api-site-settings-v1-2-endpoint.php 7 months ago class.wpcom-json-api-site-settings-v1-3-endpoint.php 7 months ago class.wpcom-json-api-site-settings-v1-4-endpoint.php 1 week ago class.wpcom-json-api-site-user-endpoint.php 7 months ago class.wpcom-json-api-taxonomy-endpoint.php 7 months ago class.wpcom-json-api-update-comment-endpoint.php 5 months ago class.wpcom-json-api-update-customcss.php 7 months ago class.wpcom-json-api-update-media-endpoint.php 7 months ago class.wpcom-json-api-update-media-v1-1-endpoint.php 1 month ago class.wpcom-json-api-update-post-endpoint.php 1 month ago class.wpcom-json-api-update-post-v1-1-endpoint.php 1 month ago class.wpcom-json-api-update-post-v1-2-endpoint.php 1 month ago class.wpcom-json-api-update-site-homepage-endpoint.php 7 months ago class.wpcom-json-api-update-site-logo-endpoint.php 7 months ago class.wpcom-json-api-update-taxonomy-endpoint.php 6 months ago class.wpcom-json-api-update-term-endpoint.php 7 months ago class.wpcom-json-api-update-user-endpoint.php 7 months ago class.wpcom-json-api-upload-media-endpoint.php 7 months ago class.wpcom-json-api-upload-media-v1-1-endpoint.php 7 months ago
class.wpcom-json-api-update-post-v1-1-endpoint.php
1144 lines
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
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit( 0 );
14 }
15
16 new 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
90 new 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
161 new 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
185 new 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 */
215 class 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 = $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 = $input['publicize'] ?? null;
546 unset( $input['publicize'] );
547
548 $publicize_custom_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 = $input['metadata'] ?? null;
558 unset( $input['metadata'] );
559
560 $likes = $input['likes_enabled'] ?? null;
561 unset( $input['likes_enabled'] );
562
563 $sharing = $input['sharing_enabled'] ?? null;
564 unset( $input['sharing_enabled'] );
565
566 $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 ( ! $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( $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 = $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 }
1144