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