Completed
Push — fix/nav-unification-menu-open-... ( d629c6...f468b4 )
by
unknown
17:24 queued 09:08
created

class.wpcom-json-api-update-post-endpoint.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
new WPCOM_JSON_API_Update_Post_Endpoint( array(
4
	'description' => 'Create a post.',
5
	'group'       => 'posts',
6
	'stat'        => 'posts:new',
7
	'new_version' => '1.2',
8
	'max_version' => '1',
9
	'method'      => 'POST',
10
	'path'        => '/sites/%s/posts/new',
11
	'path_labels' => array(
12
		'$site' => '(int|string) Site ID or domain',
13
	),
14
15
	'request_format' => array(
16
		// explicitly document all input
17
		'date'      => "(ISO 8601 datetime) The post's creation time.",
18
		'title'     => '(HTML) The post title.',
19
		'content'   => '(HTML) The post content.',
20
		'excerpt'   => '(HTML) An optional post excerpt.',
21
		'slug'      => '(string) The name (slug) for the post, used in URLs.',
22
		'author'    => '(string) The username or ID for the user to assign the post to.',
23
		'publicize' => '(array|bool) True or false if the post be publicized to external services. An array of services if we only want to publicize to a select few. Defaults to true.',
24
		'publicize_message' => '(string) Custom message to be publicized to external services.',
25
		'status'    => array(
26
			'publish' => 'Publish the post.',
27
			'private' => 'Privately publish the post.',
28
			'draft'   => 'Save the post as a draft.',
29
			'pending' => 'Mark the post as pending editorial approval.',
30
			'auto-draft' => 'Save a placeholder for a newly created post, with no content.',
31
		),
32
		'sticky'    => array(
33
			'false'   => 'Post is not marked as sticky.',
34
			'true'    => 'Stick the post to the front page.',
35
		),
36
		'password'  => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.',
37
		'parent'    => "(int) The post ID of the new post's parent.",
38
		'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.",
39
		'categories' => "(array|string) Comma-separated list or array of categories (name or id)",
40
		'tags'       => "(array|string) Comma-separated list or array of tags (name or id)",
41
		'format'     => array_merge( array( 'default' => 'Use default post format' ), get_post_format_strings() ),
42
		'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.",
43
		'media'      => "(media) An array of files to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Multiple media items will be displayed in a gallery. Accepts  jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. Audio and Video may also be available. See <code>allowed_file_types</code> in the options response of the site endpoint. <br /><br /><strong>Example</strong>:<br />" .
44
		 				"<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>",
45
		'media_urls' => "(array) An array of URLs for images to attach to a post. Sideloads the media in for a post.",
46
		'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.",
47
		'comments_open' => "(bool) Should the post be open to comments? Defaults to the blog's preference.",
48
		'pings_open'    => "(bool) Should the post be open to comments? Defaults to the blog's preference.",
49
		'likes_enabled' => "(bool) Should the post be open to likes? Defaults to the blog's preference.",
50
		'sharing_enabled' => "(bool) Should sharing buttons show on this post? Defaults to true.",
51
		'menu_order'    => "(int) (Pages Only) the order pages should appear in. Use 0 to maintain alphabetical order.",
52
	),
53
54
	'example_request'      => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/new/',
55
56
	'example_request_data' => array(
57
		'headers' => array(
58
			'authorization' => 'Bearer YOUR_API_TOKEN'
59
		),
60
61
		'body' => array(
62
			'title'      => 'Hello World',
63
			'content'    => 'Hello. I am a test post. I was created by the API',
64
			'tags'       => 'tests',
65
			'categories' => 'API'
66
		)
67
	)
68
) );
69
70
new WPCOM_JSON_API_Update_Post_Endpoint( array(
71
	'description' => 'Edit a post.',
72
	'group'       => 'posts',
73
	'stat'        => 'posts:1:POST',
74
	'new_version' => '1.2',
75
	'max_version' => '1',
76
	'method'      => 'POST',
77
	'path'        => '/sites/%s/posts/%d',
78
	'path_labels' => array(
79
		'$site'    => '(int|string) Site ID or domain',
80
		'$post_ID' => '(int) The post ID',
81
	),
82
83
	'request_format' => array(
84
		'date'      => "(ISO 8601 datetime) The post's creation time.",
85
		'title'     => '(HTML) The post title.',
86
		'content'   => '(HTML) The post content.',
87
		'excerpt'   => '(HTML) An optional post excerpt.',
88
		'slug'      => '(string) The name (slug) for the post, used in URLs.',
89
		'author'    => '(string) The username or ID for the user to assign the post to.',
90
		'publicize' => '(array|bool) True or false if the post be publicized to external services. An array of services if we only want to publicize to a select few. Defaults to true.',
91
		'publicize_message' => '(string) Custom message to be publicized to external services.',
92
		'status'    => array(
93
			'publish' => 'Publish the post.',
94
			'private' => 'Privately publish the post.',
95
			'draft'   => 'Save the post as a draft.',
96
			'pending' => 'Mark the post as pending editorial approval.',
97
			'trash'   => 'Set the post as trashed.',
98
		),
99
		'sticky'    => array(
100
			'false'   => 'Post is not marked as sticky.',
101
			'true'    => 'Stick the post to the front page.',
102
		),
103
		'password'   => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.',
104
		'parent'     => "(int) The post ID of the new post's parent.",
105
		'categories' => "(array|string) Comma-separated list or array of categories (name or id)",
106
		'tags'       => "(array|string) Comma-separated list or array of tags (name or id)",
107
		'format'     => array_merge( array( 'default' => 'Use default post format' ), get_post_format_strings() ),
108
		'comments_open' => '(bool) Should the post be open to comments?',
109
		'pings_open'    => '(bool) Should the post be open to comments?',
110
		'likes_enabled' => "(bool) Should the post be open to likes?",
111
		'menu_order'    => "(int) (Pages Only) the order pages should appear in. Use 0 to maintain alphabetical order.",
112
		'sharing_enabled' => "(bool) Should sharing buttons show on this post?",
113
		'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.",
114
		'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 />" .
115
		 				"<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>",
116
		'media_urls' => "(array) An array of URLs for images to attach to a post. Sideloads the media in for a post.",
117
		'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.",
118
	),
119
120
	'example_request'      => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/881',
121
122
	'example_request_data' => array(
123
		'headers' => array(
124
			'authorization' => 'Bearer YOUR_API_TOKEN'
125
		),
126
127
		'body' => array(
128
			'title'      => 'Hello World (Again)',
129
			'content'    => 'Hello. I am an edited post. I was edited by the API',
130
			'tags'       => 'tests',
131
			'categories' => 'API'
132
		)
133
	)
134
) );
135
136
new WPCOM_JSON_API_Update_Post_Endpoint( array(
137
	'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.',
138
	'group'       => 'posts',
139
	'stat'        => 'posts:1:delete',
140
	'new_version' => '1.1',
141
	'max_version' => '1',
142
	'method'      => 'POST',
143
	'path'        => '/sites/%s/posts/%d/delete',
144
	'path_labels' => array(
145
		'$site'    => '(int|string) Site ID or domain',
146
		'$post_ID' => '(int) The post ID',
147
	),
148
149
	'example_request'      => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/$post_ID/delete/',
150
151
	'example_request_data' => array(
152
		'headers' => array(
153
			'authorization' => 'Bearer YOUR_API_TOKEN'
154
		)
155
	)
156
) );
157
158
new WPCOM_JSON_API_Update_Post_Endpoint( array(
159
	'description' => 'Restore a post or page from the trash to its previous status.',
160
	'group'       => 'posts',
161
	'stat'        => 'posts:1:restore',
162
163
	'method'      => 'POST',
164
	'new_version' => '1.1',
165
	'max_version' => '1',
166
	'path'        => '/sites/%s/posts/%d/restore',
167
	'path_labels' => array(
168
		'$site'    => '(int|string) Site ID or domain',
169
		'$post_ID' => '(int) The post ID',
170
	),
171
172
	'example_request'      => 'https://public-api.wordpress.com/rest/v1/sites/82974409/posts/$post_ID/restore/',
173
174
	'example_request_data' => array(
175
		'headers' => array(
176
			'authorization' => 'Bearer YOUR_API_TOKEN'
177
		)
178
	)
179
) );
180
181
class WPCOM_JSON_API_Update_Post_Endpoint extends WPCOM_JSON_API_Post_Endpoint {
182
	function __construct( $args ) {
183
		parent::__construct( $args );
184
		if ( $this->api->ends_with( $this->path, '/delete' ) ) {
185
			$this->post_object_format['status']['deleted'] = 'The post has been deleted permanently.';
186
		}
187
	}
188
189
	// /sites/%s/posts/new       -> $blog_id
190
	// /sites/%s/posts/%d        -> $blog_id, $post_id
191
	// /sites/%s/posts/%d/delete -> $blog_id, $post_id
192
	// /sites/%s/posts/%d/restore -> $blog_id, $post_id
193 View Code Duplication
	function callback( $path = '', $blog_id = 0, $post_id = 0 ) {
194
		$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
195
		if ( is_wp_error( $blog_id ) ) {
196
			return $blog_id;
197
		}
198
199
		if ( $this->api->ends_with( $path, '/delete' ) ) {
200
			return $this->delete_post( $path, $blog_id, $post_id );
201
		} elseif ( $this->api->ends_with( $path, '/restore' ) ) {
202
			return $this->restore_post( $path, $blog_id, $post_id );
203
		} else {
204
			return $this->write_post( $path, $blog_id, $post_id );
205
		}
206
	}
207
208
	// /sites/%s/posts/new       -> $blog_id
209
	// /sites/%s/posts/%d        -> $blog_id, $post_id
210
	function write_post( $path, $blog_id, $post_id ) {
211
		$new  = $this->api->ends_with( $path, '/new' );
212
		$args = $this->query_args();
213
214
		// unhook publicize, it's hooked again later -- without this, skipping services is impossible
215
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
216
			remove_action( 'save_post', array( $GLOBALS['publicize_ui']->publicize, 'async_publicize_post' ), 100, 2 );
0 ignored issues
show
The call to remove_action() has too many arguments starting with 2.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
217
			add_action( 'rest_api_inserted_post', array( $GLOBALS['publicize_ui']->publicize, 'async_publicize_post' ) );
218
		}
219
220
		if ( $new ) {
221
			$input = $this->input( true );
222
223 View Code Duplication
			if ( 'revision' === $input['type'] ) {
224
				if ( ! isset( $input['parent'] ) ) {
225
					return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
226
				}
227
				$input['status'] = 'inherit'; // force inherit for revision type
228
				$input['slug'] = $input['parent'] . '-autosave-v1';
229
			}
230
			elseif ( !isset( $input['title'] ) && !isset( $input['content'] ) && !isset( $input['excerpt'] ) ) {
231
				return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
232
			}
233
234
			// default to post
235
			if ( empty( $input['type'] ) )
236
				$input['type'] = 'post';
237
238
			$post_type = get_post_type_object( $input['type'] );
239
240
			if ( ! $this->is_post_type_allowed( $input['type'] ) ) {
241
				return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
242
			}
243
244
			if ( ! empty( $input['author'] ) ) {
245
				$author_id = $this->parse_and_set_author( $input['author'], $input['type'] );
246
				unset( $input['author'] );
247
				if ( is_wp_error( $author_id ) )
248
					return $author_id;
249
			}
250
251 View Code Duplication
			if ( 'publish' === $input['status'] ) {
252
				if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
253
					if ( current_user_can( $post_type->cap->edit_posts ) ) {
254
						$input['status'] = 'pending';
255
					} else {
256
						return new WP_Error( 'unauthorized', 'User cannot publish posts', 403 );
257
					}
258
				}
259
			} else {
260
				if ( !current_user_can( $post_type->cap->edit_posts ) ) {
261
					return new WP_Error( 'unauthorized', 'User cannot edit posts', 403 );
262
				}
263
			}
264
		} else {
265
			$input = $this->input( false );
266
267
			if ( !is_array( $input ) || !$input ) {
268
				return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
269
			}
270
271
			if ( isset( $input['status'] ) && 'trash' === $input['status'] && ! current_user_can( 'delete_post', $post_id ) ) {
272
				return new WP_Error( 'unauthorized', 'User cannot delete post', 403 );
273
			}
274
275
			$post = get_post( $post_id );
276
			$_post_type = ( ! empty( $input['type'] ) ) ? $input['type'] : $post->post_type;
277
			$post_type = get_post_type_object( $_post_type );
278
			if ( !$post || is_wp_error( $post ) ) {
279
				return new WP_Error( 'unknown_post', 'Unknown post', 404 );
280
			}
281
282
			if ( !current_user_can( 'edit_post', $post->ID ) ) {
283
				return new WP_Error( 'unauthorized', 'User cannot edit post', 403 );
284
			}
285
286
			if ( ! empty( $input['author'] ) ) {
287
				$author_id = $this->parse_and_set_author( $input['author'], $_post_type );
288
				unset( $input['author'] );
289
				if ( is_wp_error( $author_id ) )
290
					return $author_id;
291
			}
292
293
			if ( ( isset( $input['status'] ) && 'publish' === $input['status'] ) && 'publish' !== $post->post_status && !current_user_can( 'publish_post', $post->ID ) ) {
294
				$input['status'] = 'pending';
295
			}
296
			$last_status = $post->post_status;
297
			$new_status = isset( $input['status'] ) ? $input['status'] : $last_status;
298
299
			// Make sure that drafts get the current date when transitioning to publish if not supplied in the post.
300
			$date_in_past = ( strtotime($post->post_date_gmt) < time() );
301
			if ( 'publish' === $new_status && 'draft' === $last_status && ! isset( $input['date_gmt'] ) && $date_in_past ) {
302
				$input['date_gmt'] = gmdate( 'Y-m-d H:i:s' );
303
			}
304
305
			// Untrash a post so that the proper hooks get called as well as the comments get untrashed.
306
			if ( 'trash' === $last_status && 'trash' !== $new_status && isset( $post->ID ) ) {
307
				wp_untrash_post( $post->ID );
308
				$untashed_post = get_post( $post->ID );
309
				// Lets make sure that we use the revert the slug.
310 View Code Duplication
				if ( isset( $untashed_post->post_name ) && $untashed_post->post_name . '__trashed' === $input['slug'] ) {
311
					unset( $input['slug'] );
312
				}
313
			}
314
		}
315
316
		if ( function_exists( 'wpcom_switch_to_locale' ) ) {
317
			// fixes calypso-pre-oss #12476: respect blog locale when creating the post slug
318
			wpcom_switch_to_locale( get_blog_lang_code( $blog_id ) );
319
		}
320
321
		// If date was set, $this->input will set date_gmt, date still needs to be adjusted for the blog's offset
322 View Code Duplication
		if ( isset( $input['date_gmt'] ) ) {
323
			$gmt_offset = get_option( 'gmt_offset' );
324
			$time_with_offset = strtotime( $input['date_gmt'] ) + $gmt_offset * HOUR_IN_SECONDS;
325
			$input['date'] = date( 'Y-m-d H:i:s', $time_with_offset );
326
		}
327
328 View Code Duplication
		if ( ! empty( $author_id ) && get_current_user_id() != $author_id ) {
329
			if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) {
330
				return new WP_Error( 'unauthorized', "User is not allowed to publish others' posts.", 403 );
331
			} elseif ( ! user_can( $author_id, $post_type->cap->edit_posts ) ) {
332
				return new WP_Error( 'unauthorized', 'Assigned author cannot publish post.', 403 );
333
			}
334
		}
335
336
		if ( !is_post_type_hierarchical( $post_type->name ) && 'revision' !== $post_type->name ) {
337
			unset( $input['parent'] );
338
		}
339
340
		$tax_input = array();
341
342
		foreach ( array( 'categories' => 'category', 'tags' => 'post_tag' ) as $key => $taxonomy ) {
343
			if ( ! isset( $input[ $key ] ) ) {
344
				continue;
345
			}
346
347
			$tax_input[ $taxonomy ] = array();
348
349
			$is_hierarchical = is_taxonomy_hierarchical( $taxonomy );
350
351
			if ( is_array( $input[$key] ) ) {
352
				$terms = $input[$key];
353
			} else {
354
				$terms = explode( ',', $input[$key] );
355
			}
356
357 View Code Duplication
			foreach ( $terms as $term ) {
358
				/**
359
				 * `curl --data 'category[]=123'` should be interpreted as a category ID,
360
				 * not a category whose name is '123'.
361
				 *
362
				 * Consequence: To add a category/tag whose name is '123', the client must
363
				 * first look up its ID.
364
				 */
365
				$term = (string) $term; // ctype_digit compat
366
				if ( ctype_digit( $term ) ) {
367
					$term = (int) $term;
368
				}
369
370
				$term_info = term_exists( $term, $taxonomy );
371
372
				if ( ! $term_info ) {
373
					// A term ID that doesn't already exist. Ignore it: we don't know what name to give it.
374
					if ( is_int( $term ) ){
375
						continue;
376
					}
377
					// only add a new tag/cat if the user has access to
378
					$tax = get_taxonomy( $taxonomy );
379
380
					// see https://core.trac.wordpress.org/ticket/26409
381
					if ( 'category' === $taxonomy && ! current_user_can( $tax->cap->edit_terms ) ) {
382
						continue;
383
					} else if ( ! current_user_can( $tax->cap->assign_terms ) ) {
384
						continue;
385
					}
386
387
					$term_info = wp_insert_term( $term, $taxonomy );
388
				}
389
390
				if ( ! is_wp_error( $term_info ) ) {
391
					if ( $is_hierarchical ) {
392
						// Categories must be added by ID
393
						$tax_input[$taxonomy][] = (int) $term_info['term_id'];
394
					} else {
395
						// Tags must be added by name
396
						if ( is_int( $term ) ) {
397
							$term = get_term( $term, $taxonomy );
398
							$tax_input[$taxonomy][] = $term->name;
399
						} else {
400
							$tax_input[$taxonomy][] = $term;
401
						}
402
					}
403
				}
404
			}
405
		}
406
407 View Code Duplication
		if ( isset( $input['categories'] ) && empty( $tax_input['category'] ) && 'revision' !== $post_type->name ) {
408
			$tax_input['category'][] = get_option( 'default_category' );
409
		}
410
411
		unset( $input['tags'], $input['categories'] );
412
413
		$insert = array();
414
415 View Code Duplication
		if ( !empty( $input['slug'] ) ) {
416
			$insert['post_name'] = $input['slug'];
417
			unset( $input['slug'] );
418
		}
419
420
		if ( isset( $input['comments_open'] ) ) {
421
			$insert['comment_status'] = ( true === $input['comments_open'] ) ? 'open' : 'closed';
422
		}
423
424
		if ( isset( $input['pings_open'] ) ) {
425
			$insert['ping_status'] = ( true === $input['pings_open'] ) ? 'open' : 'closed';
426
		}
427
428
		unset( $input['comments_open'], $input['pings_open'] );
429
430 View Code Duplication
		if ( isset( $input['menu_order'] ) ) {
431
			$insert['menu_order'] = $input['menu_order'];
432
			unset( $input['menu_order'] );
433
		}
434
435
		$publicize = isset( $input['publicize'] ) ? $input['publicize'] : null;
436
		unset( $input['publicize'] );
437
438
		$publicize_custom_message = isset( $input['publicize_message'] ) ? $input['publicize_message'] : null;
439
		unset( $input['publicize_message'] );
440
441 View Code Duplication
		if ( isset( $input['featured_image'] ) ) {
442
			$featured_image = trim( $input['featured_image'] );
443
			$delete_featured_image = empty( $featured_image );
444
			unset( $input['featured_image'] );
445
		}
446
447
		$metadata = isset( $input['metadata'] ) ? $input['metadata'] : null;
448
		unset( $input['metadata'] );
449
450
		$likes = isset( $input['likes_enabled'] ) ? $input['likes_enabled'] : null;
451
		unset( $input['likes_enabled'] );
452
453
		$sharing = isset( $input['sharing_enabled'] ) ? $input['sharing_enabled'] : null;
454
		unset( $input['sharing_enabled'] );
455
456
		$sticky = isset( $input['sticky'] ) ? $input['sticky'] : null;
457
		unset( $input['sticky'] );
458
459
		foreach ( $input as $key => $value ) {
460
			$insert["post_$key"] = $value;
461
		}
462
463
		if ( ! empty( $author_id ) ) {
464
			$insert['post_author'] = absint( $author_id );
465
		}
466
467
		if ( ! empty( $tax_input ) ) {
468
			$insert['tax_input'] = $tax_input;
469
		}
470
471
		$has_media = isset( $input['media'] ) && $input['media'] ? count( $input['media'] ) : false;
472
		$has_media_by_url = isset( $input['media_urls'] ) && $input['media_urls'] ? count( $input['media_urls'] ) : false;
473
474
		if ( $new ) {
475
476
			if ( isset( $input['content'] ) && ! has_shortcode( $input['content'], 'gallery' ) && ( $has_media || $has_media_by_url ) ) {
477
				switch ( ( $has_media + $has_media_by_url ) ) {
478
				case 0 :
479
					// No images - do nothing.
480
					break;
481
				case 1 :
482
					// 1 image - make it big
483
					$insert['post_content'] = $input['content'] = "[gallery size=full columns=1]\n\n" . $input['content'];
484
					break;
485
				default :
486
					// Several images - 3 column gallery
487
					$insert['post_content'] = $input['content'] = "[gallery]\n\n" . $input['content'];
488
					break;
489
				}
490
			}
491
492
			$post_id = wp_insert_post( add_magic_quotes( $insert ), true );
493
		} else {
494
			$insert['ID'] = $post->ID;
495
496
			// wp_update_post ignores date unless edit_date is set
497
			// See: https://codex.wordpress.org/Function_Reference/wp_update_post#Scheduling_posts
498
			// See: https://core.trac.wordpress.org/browser/tags/3.9.2/src/wp-includes/post.php#L3302
499
			if ( isset( $input['date_gmt'] ) || isset( $input['date'] ) ) {
500
				$insert['edit_date'] = true;
501
			}
502
503
			// this two-step process ensures any changes submitted along with status=trash get saved before trashing
504
			if ( isset( $input['status'] ) && 'trash' === $input['status'] ) {
505
				// if we insert it with status='trash', it will get double-trashed, so insert it as a draft first
506
				unset( $insert['status'] );
507
				$post_id = wp_update_post( (object) $insert );
508
				// now call wp_trash_post so post_meta gets set and any filters get called
509
				wp_trash_post( $post_id );
510
			} else {
511
				$post_id = wp_update_post( (object) $insert );
512
			}
513
514
		}
515
516
		if ( !$post_id || is_wp_error( $post_id ) ) {
517
			return $post_id;
518
		}
519
520
		// make sure this post actually exists and is not an error of some kind (ie, trying to load media in the posts endpoint)
521
		$post_check = $this->get_post_by( 'ID', $post_id, $args['context'] );
522
		if ( is_wp_error( $post_check ) ) {
523
			return $post_check;
524
		}
525
526
		if ( $has_media ) {
527
			$this->api->trap_wp_die( 'upload_error' );
528
			foreach ( $input['media'] as $media_item ) {
529
				$_FILES['.api.media.item.'] = $media_item;
530
				// check for WP_Error if we ever actually need $media_id
531
				$media_id = media_handle_upload( '.api.media.item.', $post_id );
532
			}
533
			$this->api->trap_wp_die( null );
534
535
			unset( $_FILES['.api.media.item.'] );
536
		}
537
538
		if ( $has_media_by_url ) {
539
			foreach ( $input['media_urls'] as $url ) {
540
				$this->handle_media_sideload( $url, $post_id );
541
			}
542
		}
543
544
		// Set like status for the post
545
		/** This filter is documented in modules/likes.php */
546
		$sitewide_likes_enabled = (bool) apply_filters( 'wpl_is_enabled_sitewide', ! get_option( 'disabled_likes' ) );
547 View Code Duplication
		if ( $new ) {
548
			if ( $sitewide_likes_enabled ) {
549
				if ( false === $likes ) {
550
					update_post_meta( $post_id, 'switch_like_status', 0 );
551
				} else {
552
					delete_post_meta( $post_id, 'switch_like_status' );
553
				}
554
			} else {
555
				if ( $likes ) {
556
					update_post_meta( $post_id, 'switch_like_status', 1 );
557
				} else {
558
					delete_post_meta( $post_id, 'switch_like_status' );
559
				}
560
			}
561
		} else {
562
			if ( isset( $likes ) ) {
563
				if ( $sitewide_likes_enabled ) {
564
					if ( false === $likes ) {
565
						update_post_meta( $post_id, 'switch_like_status', 0 );
566
					} else {
567
						delete_post_meta( $post_id, 'switch_like_status' );
568
					}
569
				} else {
570
					if ( true === $likes ) {
571
						update_post_meta( $post_id, 'switch_like_status', 1 );
572
					} else {
573
						delete_post_meta( $post_id, 'switch_like_status' );
574
					}
575
				}
576
			}
577
		}
578
579
		// Set sharing status of the post
580 View Code Duplication
		if ( $new ) {
581
			$sharing_enabled = isset( $sharing ) ? (bool) $sharing : true;
582
			if ( false === $sharing_enabled ) {
583
				update_post_meta( $post_id, 'sharing_disabled', 1 );
584
			}
585
		}
586
		else {
587
			if ( isset( $sharing ) && true === $sharing ) {
588
				delete_post_meta( $post_id, 'sharing_disabled' );
589
			} else if ( isset( $sharing ) && false == $sharing ) {
590
				update_post_meta( $post_id, 'sharing_disabled', 1 );
591
			}
592
		}
593
594
		if ( isset( $sticky ) ) {
595
			if ( true === $sticky ) {
596
				stick_post( $post_id );
597
			} else {
598
				unstick_post( $post_id );
599
			}
600
		}
601
602
		// WPCOM Specific (Jetpack's will get bumped elsewhere
603
		// Tracks how many posts are published and sets meta
604
		// so we can track some other cool stats (like likes & comments on posts published)
605 View Code Duplication
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
606
			if (
607
				( $new && 'publish' == $input['status'] )
608
				|| (
609
					! $new && isset( $last_status )
610
					&& 'publish' != $last_status
611
					&& isset( $new_status )
612
					&& 'publish' == $new_status
613
				)
614
			) {
615
				/** This action is documented in modules/widgets/social-media-icons.php */
616
				do_action( 'jetpack_bump_stats_extras', 'api-insights-posts', $this->api->token_details['client_id'] );
617
				update_post_meta( $post_id, '_rest_api_published', 1 );
618
				update_post_meta( $post_id, '_rest_api_client_id', $this->api->token_details['client_id'] );
619
			}
620
		}
621
622
623
		// We ask the user/dev to pass Publicize services he/she wants activated for the post, but Publicize expects us
624
		// to instead flag the ones we don't want to be skipped. proceed with said logic.
625
		// any posts coming from Path (client ID 25952) should also not publicize
626 View Code Duplication
		if ( $publicize === false || ( isset( $this->api->token_details['client_id'] ) && 25952 == $this->api->token_details['client_id'] ) ) {
627
			// No publicize at all, skip all by ID
628
			foreach ( $GLOBALS['publicize_ui']->publicize->get_services( 'all' ) as $name => $service ) {
629
				delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $name );
630
				$service_connections   = $GLOBALS['publicize_ui']->publicize->get_connections( $name );
631
				if ( ! $service_connections ) {
632
					continue;
633
				}
634
				foreach ( $service_connections as $service_connection ) {
635
					update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id, 1 );
636
				}
637
			}
638
		} else if ( is_array( $publicize ) && ( count ( $publicize ) > 0 ) ) {
639
			foreach ( $GLOBALS['publicize_ui']->publicize->get_services( 'all' ) as $name => $service ) {
640
				/*
641
				 * We support both indexed and associative arrays:
642
				 * * indexed are to pass entire services
643
				 * * associative are to pass specific connections per service
644
				 *
645
				 * We do support mixed arrays: mixed integer and string keys (see 3rd example below).
646
				 *
647
				 * EG: array( 'twitter', 'facebook') will only publicize to those, ignoring the other available services
648
				 * 		Form data: publicize[]=twitter&publicize[]=facebook
649
				 * EG: array( 'twitter' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3', 'facebook' => (int) $pub_conn_id_7 ) will publicize to two Twitter accounts, and one Facebook connection, of potentially many.
650
				 * 		Form data: publicize[twitter]=$pub_conn_id_0,$pub_conn_id_3&publicize[facebook]=$pub_conn_id_7
651
				 * EG: array( 'twitter', 'facebook' => '(int) $pub_conn_id_0, (int) $pub_conn_id_3' ) will publicize to all available Twitter accounts, but only 2 of potentially many Facebook connections
652
				 * 		Form data: publicize[]=twitter&publicize[facebook]=$pub_conn_id_0,$pub_conn_id_3
653
				 */
654
655
				// Delete any stale SKIP value for the service by name. We'll add it back by ID.
656
				delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $name );
657
658
				// Get the user's connections
659
				$service_connections = $GLOBALS['publicize_ui']->publicize->get_connections( $name );
660
661
				// if the user doesn't have any connections for this service, move on
662
				if ( ! $service_connections ) {
663
					continue;
664
				}
665
666
				if ( !in_array( $name, $publicize ) && !array_key_exists( $name, $publicize ) ) {
667
					// Skip the whole service by adding each connection ID
668
					foreach ( $service_connections as $service_connection ) {
669
						update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id, 1 );
670
					}
671
				} else if ( !empty( $publicize[ $name ] ) ) {
672
					// Seems we're being asked to only push to [a] specific connection[s].
673
					// Explode the list on commas, which will also support a single passed ID
674
					$requested_connections = explode( ',', ( preg_replace( '/[\s]*/', '', $publicize[ $name ] ) ) );
675
					// Flag the connections we can't match with the requested list to be skipped.
676
					foreach ( $service_connections as $service_connection ) {
677
						if ( !in_array( $service_connection->meta['connection_data']->id, $requested_connections ) ) {
678
							update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id, 1 );
679
						} else {
680
							delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id );
681
						}
682
					}
683
				} else {
684
					// delete all SKIP values; it's okay to publish to all connected IDs for this service
685
					foreach ( $service_connections as $service_connection ) {
686
						delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $service_connection->unique_id );
687
					}
688
				}
689
			}
690
		}
691
692 View Code Duplication
		if ( ! is_null( $publicize_custom_message ) ) {
693
			if ( empty( $publicize_custom_message ) ) {
694
				delete_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_MESS );
695
			} else {
696
				update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_MESS, trim( $publicize_custom_message ) );
697
			}
698
		}
699
700 View Code Duplication
		if ( ! empty( $insert['post_format'] ) ) {
701
			if ( 'default' !== strtolower( $insert['post_format'] ) ) {
702
				set_post_format( $post_id, $insert['post_format'] );
703
			}
704
			else {
705
				set_post_format( $post_id, get_option( 'default_post_format' ) );
706
			}
707
		}
708
709
		if ( isset( $featured_image  ) ) {
710
			$this->parse_and_set_featured_image( $post_id, $delete_featured_image, $featured_image );
711
		}
712
713 View Code Duplication
		if ( ! empty( $metadata ) ) {
714
			foreach ( (array) $metadata as $meta ) {
715
716
				$meta = (object) $meta;
717
718
				// Custom meta description can only be set on sites that have a business subscription.
719
				if ( Jetpack_SEO_Posts::DESCRIPTION_META_KEY == $meta->key && ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
720
					return new WP_Error( 'unauthorized', __( 'SEO tools are not enabled for this site.', 'jetpack' ), 403 );
721
				}
722
723
				$existing_meta_item = new stdClass;
724
725
				if ( empty( $meta->operation ) )
726
					$meta->operation = 'update';
727
728
				if ( ! empty( $meta->value ) ) {
729
					if ( 'true' == $meta->value )
730
						$meta->value = true;
731
					if ( 'false' == $meta->value )
732
						$meta->value = false;
733
				}
734
735
				if ( ! empty( $meta->id ) ) {
736
					$meta->id = absint( $meta->id );
737
					$existing_meta_item = get_metadata_by_mid( 'post', $meta->id );
738
					if ( $post_id !== (int) $existing_meta_item->post_id ) {
739
						// Only allow updates for metadata on this post
740
						continue;
741
					}
742
				}
743
744
				$unslashed_meta_key = wp_unslash( $meta->key ); // should match what the final key will be
745
				$meta->key = wp_slash( $meta->key );
746
				$unslashed_existing_meta_key = wp_unslash( $existing_meta_item->meta_key );
747
				$existing_meta_item->meta_key = wp_slash( $existing_meta_item->meta_key );
748
749
				// make sure that the meta id passed matches the existing meta key
750
				if ( ! empty( $meta->id ) && ! empty( $meta->key ) ) {
751
					$meta_by_id = get_metadata_by_mid( 'post', $meta->id );
752
					if ( $meta_by_id->meta_key !== $meta->key ) {
753
						continue; // skip this meta
754
					}
755
				}
756
757
				switch ( $meta->operation ) {
758
					case 'delete':
759
760
						if ( ! empty( $meta->id ) && ! empty( $existing_meta_item->meta_key ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_existing_meta_key ) ) {
761
							delete_metadata_by_mid( 'post', $meta->id );
762
						} elseif ( ! empty( $meta->key ) && ! empty( $meta->previous_value ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_meta_key ) ) {
763
							delete_post_meta( $post_id, $meta->key, $meta->previous_value );
764
						} elseif ( ! empty( $meta->key ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_meta_key ) ) {
765
							delete_post_meta( $post_id, $meta->key );
766
						}
767
768
						break;
769
					case 'add':
770
771
						if ( ! empty( $meta->id ) || ! empty( $meta->previous_value ) ) {
772
							break;
773
						} 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 ) ) {
774
							add_post_meta( $post_id, $meta->key, $meta->value );
775
						}
776
777
						break;
778
					case 'update':
779
780
						if ( ! isset( $meta->value ) ) {
781
							break;
782
						} 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 ) ) ) {
783
							update_metadata_by_mid( 'post', $meta->id, $meta->value );
784
						} 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 ) ) ) {
785
							update_post_meta( $post_id, $meta->key,$meta->value, $meta->previous_value );
786
						} elseif ( ! empty( $meta->key ) && ( current_user_can( 'edit_post_meta', $post_id, $unslashed_meta_key ) || WPCOM_JSON_API_Metadata::is_public( $meta->key ) ) ) {
787
							update_post_meta( $post_id, $meta->key, $meta->value );
788
						}
789
790
						break;
791
				}
792
793
			}
794
		}
795
796
		/**
797
		 * Fires when a post is created via the REST API.
798
		 *
799
		 * @module json-api
800
		 *
801
		 * @since 2.3.0
802
		 *
803
		 * @param int $post_id Post ID.
804
		 * @param array $insert Data used to build the post.
805
		 * @param string $new New post URL suffix.
806
		 */
807
		do_action( 'rest_api_inserted_post', $post_id, $insert, $new );
808
809
		$return = $this->get_post_by( 'ID', $post_id, $args['context'] );
810
		if ( !$return || is_wp_error( $return ) ) {
811
			return $return;
812
		}
813
814 View Code Duplication
		if ( isset( $input['type'] ) && 'revision' === $input['type'] ) {
815
			$return['preview_nonce'] = wp_create_nonce( 'post_preview_' . $input['parent'] );
816
		}
817
818
		if ( isset( $sticky ) ) {
819
			// workaround for sticky test occasionally failing, maybe a race condition with stick_post() above
820
			$return['sticky'] = ( true === $sticky );
821
		}
822
823
		/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
824
		do_action( 'wpcom_json_api_objects', 'posts' );
825
826
		return $return;
827
	}
828
829
	// /sites/%s/posts/%d/delete -> $blog_id, $post_id
830 View Code Duplication
	function delete_post( $path, $blog_id, $post_id ) {
831
		$post = get_post( $post_id );
832
		if ( !$post || is_wp_error( $post ) ) {
833
			return new WP_Error( 'unknown_post', 'Unknown post', 404 );
834
		}
835
836
		if ( ! $this->is_post_type_allowed( $post->post_type ) ) {
837
			return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
838
		}
839
840
		if ( !current_user_can( 'delete_post', $post->ID ) ) {
841
			return new WP_Error( 'unauthorized', 'User cannot delete posts', 403 );
842
		}
843
844
		$args  = $this->query_args();
845
		$return = $this->get_post_by( 'ID', $post->ID, $args['context'] );
846
		if ( !$return || is_wp_error( $return ) ) {
847
			return $return;
848
		}
849
850
		/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
851
		do_action( 'wpcom_json_api_objects', 'posts' );
852
853
		// we need to call wp_trash_post so that untrash will work correctly for all post types
854
		if ( 'trash' === $post->post_status )
855
			wp_delete_post( $post->ID );
856
		else
857
			wp_trash_post( $post->ID );
858
859
		$status = get_post_status( $post->ID );
860
		if ( false === $status ) {
861
			$return['status'] = 'deleted';
862
			return $return;
863
		}
864
865
		return $this->get_post_by( 'ID', $post->ID, $args['context'] );
866
	}
867
868
	// /sites/%s/posts/%d/restore -> $blog_id, $post_id
869 View Code Duplication
	function restore_post( $path, $blog_id, $post_id ) {
870
		$args  = $this->query_args();
871
		$post = get_post( $post_id );
872
873
		if ( !$post || is_wp_error( $post ) ) {
874
			return new WP_Error( 'unknown_post', 'Unknown post', 404 );
875
		}
876
877
		if ( !current_user_can( 'delete_post', $post->ID ) ) {
878
			return new WP_Error( 'unauthorized', 'User cannot restore trashed posts', 403 );
879
		}
880
881
		/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
882
		do_action( 'wpcom_json_api_objects', 'posts' );
883
884
		wp_untrash_post( $post->ID );
885
886
		return $this->get_post_by( 'ID', $post->ID, $args['context'] );
887
	}
888
889 View Code Duplication
	private function parse_and_set_featured_image( $post_id, $delete_featured_image, $featured_image ) {
890
		if ( $delete_featured_image ) {
891
			delete_post_thumbnail( $post_id );
892
			return;
893
		}
894
895
		$featured_image = (string) $featured_image;
896
897
		// if we got a post ID, we can just set it as the thumbnail
898
		if ( ctype_digit( $featured_image ) && 'attachment' == get_post_type( $featured_image ) ) {
899
			set_post_thumbnail( $post_id, $featured_image );
900
			return $featured_image;
901
		}
902
903
		$featured_image_id = $this->handle_media_sideload( $featured_image, $post_id, 'image' );
904
905
		if ( empty( $featured_image_id ) || ! is_int( $featured_image_id ) )
906
			return false;
907
908
		set_post_thumbnail( $post_id, $featured_image_id );
909
		return $featured_image_id;
910
	}
911
912 View Code Duplication
	private function parse_and_set_author( $author = null, $post_type = 'post' ) {
913
		if ( empty( $author ) || ! post_type_supports( $post_type, 'author' ) )
914
			return get_current_user_id();
915
916
		$author = (string) $author;
917
		if ( ctype_digit( $author ) ) {
918
			$_user = get_user_by( 'id', $author );
919
			if ( ! $_user || is_wp_error( $_user ) )
920
				return new WP_Error( 'invalid_author', 'Invalid author provided' );
921
922
			return $_user->ID;
923
		}
924
925
		$_user = get_user_by( 'login', $author );
926
		if ( ! $_user || is_wp_error( $_user ) )
927
			return new WP_Error( 'invalid_author', 'Invalid author provided' );
928
929
		return $_user->ID;
930
	}
931
}
932