Completed
Branch develop (a2474f)
by
unknown
02:30
created

WP_REST_Posts_Controller::get_collection_params()   C

Complexity

Conditions 11
Paths 96

Size

Total Lines 139
Code Lines 106

Duplication

Lines 10
Ratio 7.19 %

Importance

Changes 0
Metric Value
cc 11
eloc 106
nc 96
nop 0
dl 10
loc 139
rs 5.2653
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
class WP_REST_Posts_Controller extends WP_REST_Controller {
4
5
	protected $post_type;
6
7 View Code Duplication
	public function __construct( $post_type ) {
8
		$this->post_type = $post_type;
9
		$this->namespace = 'wp/v2';
10
		$obj = get_post_type_object( $post_type );
11
		$this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
12
	}
13
14
	/**
15
	 * Register the routes for the objects of the controller.
16
	 */
17 View Code Duplication
	public function register_routes() {
18
19
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
20
			array(
21
				'methods'         => WP_REST_Server::READABLE,
22
				'callback'        => array( $this, 'get_items' ),
23
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
24
				'args'            => $this->get_collection_params(),
25
			),
26
			array(
27
				'methods'         => WP_REST_Server::CREATABLE,
28
				'callback'        => array( $this, 'create_item' ),
29
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
30
				'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
31
			),
32
			'schema' => array( $this, 'get_public_item_schema' ),
33
		) );
34
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
35
			array(
36
				'methods'         => WP_REST_Server::READABLE,
37
				'callback'        => array( $this, 'get_item' ),
38
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
39
				'args'            => array(
40
					'context'          => $this->get_context_param( array( 'default' => 'view' ) ),
41
				),
42
			),
43
			array(
44
				'methods'         => WP_REST_Server::EDITABLE,
45
				'callback'        => array( $this, 'update_item' ),
46
				'permission_callback' => array( $this, 'update_item_permissions_check' ),
47
				'args'            => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
48
			),
49
			array(
50
				'methods'  => WP_REST_Server::DELETABLE,
51
				'callback' => array( $this, 'delete_item' ),
52
				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
53
				'args'     => array(
54
					'force'    => array(
55
						'default'      => false,
56
						'description'  => __( 'Whether to bypass trash and force deletion.' ),
57
					),
58
				),
59
			),
60
			'schema' => array( $this, 'get_public_item_schema' ),
61
		) );
62
	}
63
64
	/**
65
	 * Check if a given request has access to read /posts.
66
	 *
67
	 * @param  WP_REST_Request $request Full details about the request.
68
	 * @return WP_Error|boolean
69
	 */
70 View Code Duplication
	public function get_items_permissions_check( $request ) {
71
72
		$post_type = get_post_type_object( $this->post_type );
73
74
		if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
75
			return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit these posts in this post type' ), array( 'status' => rest_authorization_required_code() ) );
76
		}
77
78
		return true;
79
	}
80
81
	/**
82
	 * Get a collection of posts.
83
	 *
84
	 * @param WP_REST_Request $request Full details about the request.
85
	 * @return WP_Error|WP_REST_Response
86
	 */
87
	public function get_items( $request ) {
88
89
		//Make sure a search string is set in case the orderby is set to 'relevace'
90
		if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) && empty( $request['filter']['s'] ) ) {
91
			return new WP_Error( 'rest_no_search_term_defined', __( 'You need to define a search term to order by relevance.' ), array( 'status' => 400 ) );
92
		}
93
94
		$args                         = array();
95
		$args['author__in']           = $request['author'];
96
		$args['author__not_in']       = $request['author_exclude'];
97
		$args['menu_order']           = $request['menu_order'];
98
		$args['offset']               = $request['offset'];
99
		$args['order']                = $request['order'];
100
		$args['orderby']              = $request['orderby'];
101
		$args['paged']                = $request['page'];
102
		$args['post__in']             = $request['include'];
103
		$args['post__not_in']         = $request['exclude'];
104
		$args['posts_per_page']       = $request['per_page'];
105
		$args['name']                 = $request['slug'];
106
		$args['post_parent__in']      = $request['parent'];
107
		$args['post_parent__not_in']  = $request['parent_exclude'];
108
		$args['post_status']          = $request['status'];
109
		$args['s']                    = $request['search'];
110
111
		$args['date_query'] = array();
112
		// Set before into date query. Date query must be specified as an array of an array.
113
		if ( isset( $request['before'] ) ) {
114
			$args['date_query'][0]['before'] = $request['before'];
115
		}
116
117
		// Set after into date query. Date query must be specified as an array of an array.
118 View Code Duplication
		if ( isset( $request['after'] ) ) {
119
			$args['date_query'][0]['after'] = $request['after'];
120
		}
121
122
		if ( is_array( $request['filter'] ) ) {
123
			$args = array_merge( $args, $request['filter'] );
124
			unset( $args['filter'] );
125
		}
126
127
		if ( isset( $request['sticky'] ) ) {
128
			$sticky_posts = get_option( 'sticky_posts', array() );
129
			if ( $sticky_posts && $request['sticky'] ) {
130
				// As post__in will be used to only get sticky posts,
131
				// we have to support the case where post__in was already
132
				// specified.
133
				$args['post__in'] = $args['post__in'] ? array_intersect( $sticky_posts, $args['post__in'] ) : $sticky_posts;
134
135
				// If we intersected, but there are no post ids in common,
136
				// WP_Query won't return "no posts" for `post__in = array()`
137
				// so we have to fake it a bit.
138
				if ( ! $args['post__in'] ) {
139
					$args['post__in'] = array( -1 );
140
				}
141
			} elseif ( $sticky_posts ) {
142
				// As post___not_in will be used to only get posts that
143
				// are not sticky, we have to support the case where post__not_in
144
				// was already specified.
145
				$args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts );
146
			}
147
		}
148
149
		// Force the post_type argument, since it's not a user input variable.
150
		$args['post_type'] = $this->post_type;
151
152
		/**
153
		 * Filter the query arguments for a request.
154
		 *
155
		 * Enables adding extra arguments or setting defaults for a post
156
		 * collection request.
157
		 *
158
		 * @see https://developer.wordpress.org/reference/classes/wp_user_query/
159
		 *
160
		 * @param array           $args    Key value array of query var to query value.
161
		 * @param WP_REST_Request $request The request used.
162
		 */
163
		$args = apply_filters( "rest_{$this->post_type}_query", $args, $request );
164
		$query_args = $this->prepare_items_query( $args, $request );
165
166
		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
167
		foreach ( $taxonomies as $taxonomy ) {
168
			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
169
170
			if ( ! empty( $request[ $base ] ) ) {
171
				$query_args['tax_query'][] = array(
172
					'taxonomy'         => $taxonomy->name,
173
					'field'            => 'term_id',
174
					'terms'            => $request[ $base ],
175
					'include_children' => false,
176
				);
177
			}
178
		}
179
180
		$posts_query = new WP_Query();
181
		$query_result = $posts_query->query( $query_args );
182
183
		$posts = array();
184 View Code Duplication
		foreach ( $query_result as $post ) {
185
			if ( ! $this->check_read_permission( $post ) ) {
186
				continue;
187
			}
188
189
			$data = $this->prepare_item_for_response( $post, $request );
190
			$posts[] = $this->prepare_response_for_collection( $data );
191
		}
192
193
		$page = (int) $query_args['paged'];
194
		$total_posts = $posts_query->found_posts;
195
196
		if ( $total_posts < 1 ) {
197
			// Out-of-bounds, run the query again without LIMIT for total count
198
			unset( $query_args['paged'] );
199
			$count_query = new WP_Query();
200
			$count_query->query( $query_args );
201
			$total_posts = $count_query->found_posts;
202
		}
203
204
		$max_pages = ceil( $total_posts / (int) $query_args['posts_per_page'] );
205
206
		$response = rest_ensure_response( $posts );
207
		$response->header( 'X-WP-Total', (int) $total_posts );
208
		$response->header( 'X-WP-TotalPages', (int) $max_pages );
209
210
		$request_params = $request->get_query_params();
211
		if ( ! empty( $request_params['filter'] ) ) {
212
			// Normalize the pagination params.
213
			unset( $request_params['filter']['posts_per_page'] );
214
			unset( $request_params['filter']['paged'] );
215
		}
216
		$base = add_query_arg( $request_params, rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
217
218 View Code Duplication
		if ( $page > 1 ) {
219
			$prev_page = $page - 1;
220
			if ( $prev_page > $max_pages ) {
221
				$prev_page = $max_pages;
222
			}
223
			$prev_link = add_query_arg( 'page', $prev_page, $base );
224
			$response->link_header( 'prev', $prev_link );
225
		}
226 View Code Duplication
		if ( $max_pages > $page ) {
227
			$next_page = $page + 1;
228
			$next_link = add_query_arg( 'page', $next_page, $base );
229
			$response->link_header( 'next', $next_link );
230
		}
231
232
		return $response;
233
	}
234
235
	/**
236
	 * Check if a given request has access to read a post.
237
	 *
238
	 * @param  WP_REST_Request $request Full details about the request.
239
	 * @return WP_Error|boolean
240
	 */
241
	public function get_item_permissions_check( $request ) {
242
243
		$post = $this->get_post( (int) $request['id'] );
244
245
		if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
246
			return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit this post' ), array( 'status' => rest_authorization_required_code() ) );
247
		}
248
249
		if ( $post ) {
250
			return $this->check_read_permission( $post );
251
		}
252
253
		return true;
254
	}
255
256
	/**
257
	 * Get a single post.
258
	 *
259
	 * @param WP_REST_Request $request Full details about the request.
260
	 * @return WP_Error|WP_REST_Response
261
	 */
262
	public function get_item( $request ) {
263
		$id = (int) $request['id'];
264
		$post = $this->get_post( $id );
265
266
		if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
267
			return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
268
		}
269
270
		$data = $this->prepare_item_for_response( $post, $request );
271
		$response = rest_ensure_response( $data );
272
273
		if ( is_post_type_viewable( get_post_type_object( $post->post_type ) ) ) {
274
			$response->link_header( 'alternate',  get_permalink( $id ), array( 'type' => 'text/html' ) );
275
		}
276
277
		return $response;
278
	}
279
280
	/**
281
	 * Check if a given request has access to create a post.
282
	 *
283
	 * @param  WP_REST_Request $request Full details about the request.
284
	 * @return WP_Error|boolean
285
	 */
286
	public function create_item_permissions_check( $request ) {
287
288
		$post_type = get_post_type_object( $this->post_type );
289
290
		if ( ! empty( $request['password'] ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
291
			return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to create password protected posts in this post type' ), array( 'status' => rest_authorization_required_code() ) );
292
		}
293
294
		if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
295
			return new WP_Error( 'rest_cannot_edit_others', __( 'You are not allowed to create posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
296
		}
297
298
		if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
299
			return new WP_Error( 'rest_cannot_assign_sticky', __( 'You do not have permission to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
300
		}
301
302
		if ( ! current_user_can( $post_type->cap->create_posts ) ) {
303
			return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to create new posts.' ), array( 'status' => rest_authorization_required_code() ) );
304
		}
305
		return true;
306
	}
307
308
	/**
309
	 * Create a single post.
310
	 *
311
	 * @param WP_REST_Request $request Full details about the request.
312
	 * @return WP_Error|WP_REST_Response
313
	 */
314
	public function create_item( $request ) {
315
		if ( ! empty( $request['id'] ) ) {
316
			return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.' ), array( 'status' => 400 ) );
317
		}
318
319
		$post = $this->prepare_item_for_database( $request );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->prepare_item_for_database($request); of type WP_Error|stdClass adds the type stdClass to the return on line 321 which is incompatible with the return type documented by WP_REST_Posts_Controller::create_item of type WP_Error|WP_REST_Response.
Loading history...
320
		if ( is_wp_error( $post ) ) {
321
			return $post;
322
		}
323
324
		$post->post_type = $this->post_type;
325
		$post_id = wp_insert_post( $post, true );
326
327 View Code Duplication
		if ( is_wp_error( $post_id ) ) {
328
329
			if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) {
330
				$post_id->add_data( array( 'status' => 500 ) );
331
			} else {
332
				$post_id->add_data( array( 'status' => 400 ) );
333
			}
334
			return $post_id;
335
		}
336
		$post->ID = $post_id;
337
338
		$schema = $this->get_item_schema();
339
340 View Code Duplication
		if ( ! empty( $schema['properties']['sticky'] ) ) {
341
			if ( ! empty( $request['sticky'] ) ) {
342
				stick_post( $post_id );
343
			} else {
344
				unstick_post( $post_id );
345
			}
346
		}
347
348 View Code Duplication
		if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
349
			$this->handle_featured_media( $request['featured_media'], $post->ID );
350
		}
351
352 View Code Duplication
		if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
353
			set_post_format( $post, $request['format'] );
354
		}
355
356 View Code Duplication
		if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
357
			$this->handle_template( $request['template'], $post->ID );
358
		}
359
		$terms_update = $this->handle_terms( $post->ID, $request );
360
		if ( is_wp_error( $terms_update ) ) {
361
			return $terms_update;
362
		}
363
364
		$post = $this->get_post( $post_id );
365
		$fields_update = $this->update_additional_fields_for_object( $post, $request );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->update_additional...bject($post, $request); of type boolean|WP_Error adds the type boolean to the return on line 367 which is incompatible with the return type documented by WP_REST_Posts_Controller::create_item of type WP_Error|WP_REST_Response.
Loading history...
Bug introduced by
It seems like $post defined by $this->get_post($post_id) on line 364 can also be of type null; however, WP_REST_Controller::upda...nal_fields_for_object() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
366
		if ( is_wp_error( $fields_update ) ) {
367
			return $fields_update;
368
		}
369
370
		/**
371
		 * Fires after a single post is created or updated via the REST API.
372
		 *
373
		 * @param object          $post      Inserted Post object (not a WP_Post object).
374
		 * @param WP_REST_Request $request   Request object.
375
		 * @param boolean         $creating  True when creating post, false when updating.
376
		 */
377
		do_action( "rest_insert_{$this->post_type}", $post, $request, true );
378
379
		$request->set_param( 'context', 'edit' );
380
		$response = $this->prepare_item_for_response( $post, $request );
0 ignored issues
show
Bug introduced by
It seems like $post defined by $this->get_post($post_id) on line 364 can be null; however, WP_REST_Posts_Controller...are_item_for_response() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
381
		$response = rest_ensure_response( $response );
382
		$response->set_status( 201 );
383
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) );
384
385
		return $response;
386
	}
387
388
	/**
389
	 * Check if a given request has access to update a post.
390
	 *
391
	 * @param  WP_REST_Request $request Full details about the request.
392
	 * @return WP_Error|boolean
393
	 */
394
	public function update_item_permissions_check( $request ) {
395
396
		$post = $this->get_post( $request['id'] );
397
		$post_type = get_post_type_object( $this->post_type );
398
399
		if ( $post && ! $this->check_update_permission( $post ) ) {
400
			return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to update this post.' ), array( 'status' => rest_authorization_required_code() ) );;
401
		}
402
403
		if ( ! empty( $request['password'] ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
404
			return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to create password protected posts in this post type' ), array( 'status' => rest_authorization_required_code() ) );
405
		}
406
407
		if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
408
			return new WP_Error( 'rest_cannot_edit_others', __( 'You are not allowed to update posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
409
		}
410
411
		if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
412
			return new WP_Error( 'rest_cannot_assign_sticky', __( 'You do not have permission to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
413
		}
414
415
		return true;
416
	}
417
418
	/**
419
	 * Update a single post.
420
	 *
421
	 * @param WP_REST_Request $request Full details about the request.
422
	 * @return WP_Error|WP_REST_Response
423
	 */
424
	public function update_item( $request ) {
425
		$id = (int) $request['id'];
426
		$post = $this->get_post( $id );
427
428
		if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
429
			return new WP_Error( 'rest_post_invalid_id', __( 'Post id is invalid.' ), array( 'status' => 404 ) );
430
		}
431
432
		$post = $this->prepare_item_for_database( $request );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->prepare_item_for_database($request); of type WP_Error|stdClass adds the type stdClass to the return on line 434 which is incompatible with the return type documented by WP_REST_Posts_Controller::update_item of type WP_Error|WP_REST_Response.
Loading history...
433
		if ( is_wp_error( $post ) ) {
434
			return $post;
435
		}
436
		// convert the post object to an array, otherwise wp_update_post will expect non-escaped input
437
		$post_id = wp_update_post( (array) $post, true );
438 View Code Duplication
		if ( is_wp_error( $post_id ) ) {
439
			if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) {
440
				$post_id->add_data( array( 'status' => 500 ) );
441
			} else {
442
				$post_id->add_data( array( 'status' => 400 ) );
443
			}
444
			return $post_id;
445
		}
446
447
		$schema = $this->get_item_schema();
448
449 View Code Duplication
		if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
450
			set_post_format( $post, $request['format'] );
451
		}
452
453 View Code Duplication
		if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
454
			$this->handle_featured_media( $request['featured_media'], $post_id );
455
		}
456
457 View Code Duplication
		if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) {
458
			if ( ! empty( $request['sticky'] ) ) {
459
				stick_post( $post_id );
460
			} else {
461
				unstick_post( $post_id );
462
			}
463
		}
464
465 View Code Duplication
		if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
466
			$this->handle_template( $request['template'], $post->ID );
467
		}
468
469
		$terms_update = $this->handle_terms( $post->ID, $request );
470
		if ( is_wp_error( $terms_update ) ) {
471
			return $terms_update;
472
		}
473
474
		$post = $this->get_post( $post_id );
475
		$fields_update = $this->update_additional_fields_for_object( $post, $request );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->update_additional...bject($post, $request); of type boolean|WP_Error adds the type boolean to the return on line 477 which is incompatible with the return type documented by WP_REST_Posts_Controller::update_item of type WP_Error|WP_REST_Response.
Loading history...
Bug introduced by
It seems like $post defined by $this->get_post($post_id) on line 474 can also be of type null; however, WP_REST_Controller::upda...nal_fields_for_object() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
476
		if ( is_wp_error( $fields_update ) ) {
477
			return $fields_update;
478
		}
479
480
		/* This action is documented in lib/endpoints/class-wp-rest-controller.php */
481
		do_action( "rest_insert_{$this->post_type}", $post, $request, false );
482
483
		$request->set_param( 'context', 'edit' );
484
		$response = $this->prepare_item_for_response( $post, $request );
0 ignored issues
show
Bug introduced by
It seems like $post defined by $this->get_post($post_id) on line 474 can be null; however, WP_REST_Posts_Controller...are_item_for_response() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
485
		return rest_ensure_response( $response );
486
	}
487
488
	/**
489
	 * Check if a given request has access to delete a post.
490
	 *
491
	 * @param  WP_REST_Request $request Full details about the request.
492
	 * @return bool|WP_Error
493
	 */
494
	public function delete_item_permissions_check( $request ) {
495
496
		$post = $this->get_post( $request['id'] );
497
498
		if ( $post && ! $this->check_delete_permission( $post ) ) {
499
			return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete posts.' ), array( 'status' => rest_authorization_required_code() ) );
500
		}
501
502
		return true;
503
	}
504
505
	/**
506
	 * Delete a single post.
507
	 *
508
	 * @param WP_REST_Request $request Full details about the request.
509
	 * @return WP_REST_Response|WP_Error
510
	 */
511
	public function delete_item( $request ) {
512
		$id = (int) $request['id'];
513
		$force = (bool) $request['force'];
514
515
		$post = $this->get_post( $id );
516
517
		if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
518
			return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) );
519
		}
520
521
		$supports_trash = ( EMPTY_TRASH_DAYS > 0 );
522
		if ( $post->post_type === 'attachment' ) {
523
			$supports_trash = $supports_trash && MEDIA_TRASH;
524
		}
525
526
		/**
527
		 * Filter whether a post is trashable.
528
		 *
529
		 * Return false to disable trash support for the post.
530
		 *
531
		 * @param boolean $supports_trash Whether the post type support trashing.
532
		 * @param WP_Post $post           The Post object being considered for trashing support.
533
		 */
534
		$supports_trash = apply_filters( "rest_{$this->post_type}_trashable", $supports_trash, $post );
535
536
		if ( ! $this->check_delete_permission( $post ) ) {
537
			return new WP_Error( 'rest_user_cannot_delete_post', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => rest_authorization_required_code() ) );
538
		}
539
540
		$request->set_param( 'context', 'edit' );
541
		$response = $this->prepare_item_for_response( $post, $request );
542
543
		// If we're forcing, then delete permanently.
544
		if ( $force ) {
545
			$result = wp_delete_post( $id, true );
546
		} else {
547
			// If we don't support trashing for this type, error out.
548
			if ( ! $supports_trash ) {
549
				return new WP_Error( 'rest_trash_not_supported', __( 'The post does not support trashing.' ), array( 'status' => 501 ) );
550
			}
551
552
			// Otherwise, only trash if we haven't already.
553 View Code Duplication
			if ( 'trash' === $post->post_status ) {
554
				return new WP_Error( 'rest_already_trashed', __( 'The post has already been deleted.' ), array( 'status' => 410 ) );
555
			}
556
557
			// (Note that internally this falls through to `wp_delete_post` if
558
			// the trash is disabled.)
559
			$result = wp_trash_post( $id );
560
		}
561
562
		if ( ! $result ) {
563
			return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
564
		}
565
566
		/**
567
		 * Fires after a single post is deleted or trashed via the REST API.
568
		 *
569
		 * @param object           $post     The deleted or trashed post.
570
		 * @param WP_REST_Response $response The response data.
571
		 * @param WP_REST_Request  $request  The request sent to the API.
572
		 */
573
		do_action( "rest_delete_{$this->post_type}", $post, $response, $request );
574
575
		return $response;
576
	}
577
578
	/**
579
	 * Determine the allowed query_vars for a get_items() response and
580
	 * prepare for WP_Query.
581
	 *
582
	 * @param array           $prepared_args
583
	 * @param WP_REST_Request $request
584
	 * @return array          $query_args
585
	 */
586
	protected function prepare_items_query( $prepared_args = array(), $request = null ) {
587
588
		$valid_vars = array_flip( $this->get_allowed_query_vars() );
589
		$query_args = array();
590
		foreach ( $valid_vars as $var => $index ) {
591
			if ( isset( $prepared_args[ $var ] ) ) {
592
				/**
593
				 * Filter the query_vars used in `get_items` for the constructed query.
594
				 *
595
				 * The dynamic portion of the hook name, $var, refers to the query_var key.
596
				 *
597
				 * @param mixed $prepared_args[ $var ] The query_var value.
598
				 *
599
				 */
600
				$query_args[ $var ] = apply_filters( "rest_query_var-{$var}", $prepared_args[ $var ] );
601
			}
602
		}
603
604
		if ( 'post' !== $this->post_type || ! isset( $query_args['ignore_sticky_posts'] ) ) {
605
			$query_args['ignore_sticky_posts'] = true;
606
		}
607
608
		if ( 'include' === $query_args['orderby'] ) {
609
			$query_args['orderby'] = 'post__in';
610
		}
611
612
		return $query_args;
613
	}
614
615
	/**
616
	 * Get all the WP Query vars that are allowed for the API request.
617
	 *
618
	 * @return array
619
	 */
620
	protected function get_allowed_query_vars() {
621
		global $wp;
622
623
		/**
624
		 * Filter the publicly allowed query vars.
625
		 *
626
		 * Allows adjusting of the default query vars that are made public.
627
		 *
628
		 * @param array  Array of allowed WP_Query query vars.
629
		 */
630
		$valid_vars = apply_filters( 'query_vars', $wp->public_query_vars );
631
632
		$post_type_obj = get_post_type_object( $this->post_type );
633
		if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
634
			/**
635
			 * Filter the allowed 'private' query vars for authorized users.
636
			 *
637
			 * If the user has the `edit_posts` capability, we also allow use of
638
			 * private query parameters, which are only undesirable on the
639
			 * frontend, but are safe for use in query strings.
640
			 *
641
			 * To disable anyway, use
642
			 * `add_filter( 'rest_private_query_vars', '__return_empty_array' );`
643
			 *
644
			 * @param array $private_query_vars Array of allowed query vars for authorized users.
645
			 * }
646
			 */
647
			$private = apply_filters( 'rest_private_query_vars', $wp->private_query_vars );
648
			$valid_vars = array_merge( $valid_vars, $private );
649
		}
650
		// Define our own in addition to WP's normal vars.
651
		$rest_valid = array(
652
			'author__in',
653
			'author__not_in',
654
			'ignore_sticky_posts',
655
			'menu_order',
656
			'offset',
657
			'post__in',
658
			'post__not_in',
659
			'post_parent',
660
			'post_parent__in',
661
			'post_parent__not_in',
662
			'posts_per_page',
663
			'date_query',
664
		);
665
		$valid_vars = array_merge( $valid_vars, $rest_valid );
666
667
		/**
668
		 * Filter allowed query vars for the REST API.
669
		 *
670
		 * This filter allows you to add or remove query vars from the final allowed
671
		 * list for all requests, including unauthenticated ones. To alter the
672
		 * vars for editors only, {@see rest_private_query_vars}.
673
		 *
674
		 * @param array {
675
		 *    Array of allowed WP_Query query vars.
676
		 *
677
		 *    @param string $allowed_query_var The query var to allow.
678
		 * }
679
		 */
680
		$valid_vars = apply_filters( 'rest_query_vars', $valid_vars );
681
682
		return $valid_vars;
683
	}
684
685
	/**
686
	 * Check the post excerpt and prepare it for single post output.
687
	 *
688
	 * @param string       $excerpt
689
	 * @return string|null $excerpt
690
	 */
691
	protected function prepare_excerpt_response( $excerpt, $post ) {
692
		if ( post_password_required() ) {
693
			return __( 'There is no excerpt because this is a protected post.' );
694
		}
695
696
		/** This filter is documented in wp-includes/post-template.php */
697
		$excerpt = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $excerpt, $post ) );
698
699
		if ( empty( $excerpt ) ) {
700
			return '';
701
		}
702
703
		return $excerpt;
704
	}
705
706
	/**
707
	 * Check the post_date_gmt or modified_gmt and prepare any post or
708
	 * modified date for single post output.
709
	 *
710
	 * @param string       $date_gmt
711
	 * @param string|null  $date
712
	 * @return string|null ISO8601/RFC3339 formatted datetime.
713
	 */
714 View Code Duplication
	protected function prepare_date_response( $date_gmt, $date = null ) {
715
		// Use the date if passed.
716
		if ( isset( $date ) ) {
717
			return mysql_to_rfc3339( $date );
718
		}
719
720
		// Return null if $date_gmt is empty/zeros.
721
		if ( '0000-00-00 00:00:00' === $date_gmt ) {
722
			return null;
723
		}
724
725
		// Return the formatted datetime.
726
		return mysql_to_rfc3339( $date_gmt );
727
	}
728
729
	protected function prepare_password_response( $password ) {
730
		if ( ! empty( $password ) ) {
731
			/**
732
			 * Fake the correct cookie to fool post_password_required().
733
			 * Without this, get_the_content() will give a password form.
734
			 */
735
			require_once ABSPATH . WPINC .'/class-phpass.php';
736
			$hasher = new PasswordHash( 8, true );
737
			$value = $hasher->HashPassword( $password );
738
			$_COOKIE[ 'wp-postpass_' . COOKIEHASH ] = wp_slash( $value );
739
		}
740
741
		return $password;
742
	}
743
744
	/**
745
	 * Prepare a single post for create or update.
746
	 *
747
	 * @param WP_REST_Request $request Request object.
748
	 * @return WP_Error|stdClass $prepared_post Post object.
749
	 */
750
	protected function prepare_item_for_database( $request ) {
751
		$prepared_post = new stdClass;
752
753
		// ID.
754
		if ( isset( $request['id'] ) ) {
755
			$prepared_post->ID = absint( $request['id'] );
756
		}
757
758
		$schema = $this->get_item_schema();
759
760
		// Post title.
761 View Code Duplication
		if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
762
			if ( is_string( $request['title'] ) ) {
763
				$prepared_post->post_title = wp_filter_post_kses( $request['title'] );
764
			} elseif ( ! empty( $request['title']['raw'] ) ) {
765
				$prepared_post->post_title = wp_filter_post_kses( $request['title']['raw'] );
766
			}
767
		}
768
769
		// Post content.
770 View Code Duplication
		if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
771
			if ( is_string( $request['content'] ) ) {
772
				$prepared_post->post_content = wp_filter_post_kses( $request['content'] );
773
			} elseif ( isset( $request['content']['raw'] ) ) {
774
				$prepared_post->post_content = wp_filter_post_kses( $request['content']['raw'] );
775
			}
776
		}
777
778
		// Post excerpt.
779 View Code Duplication
		if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) {
780
			if ( is_string( $request['excerpt'] ) ) {
781
				$prepared_post->post_excerpt = wp_filter_post_kses( $request['excerpt'] );
782
			} elseif ( isset( $request['excerpt']['raw'] ) ) {
783
				$prepared_post->post_excerpt = wp_filter_post_kses( $request['excerpt']['raw'] );
784
			}
785
		}
786
787
		// Post type.
788
		if ( empty( $request['id'] ) ) {
789
			// Creating new post, use default type for the controller.
790
			$prepared_post->post_type = $this->post_type;
791
		} else {
792
			// Updating a post, use previous type.
793
			$prepared_post->post_type = get_post_type( $request['id'] );
794
		}
795
		$post_type = get_post_type_object( $prepared_post->post_type );
796
797
		// Post status.
798
		if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) ) {
799
			$status = $this->handle_status_param( $request['status'], $post_type );
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->handle_status_par...'status'], $post_type); of type WP_Error|string adds the type string to the return on line 801 which is incompatible with the return type documented by WP_REST_Posts_Controller...epare_item_for_database of type WP_Error|stdClass.
Loading history...
800
			if ( is_wp_error( $status ) ) {
801
				return $status;
802
			}
803
804
			$prepared_post->post_status = $status;
805
		}
806
807
		// Post date.
808
		if ( ! empty( $schema['properties']['date'] ) && ! empty( $request['date'] ) ) {
809
			$date_data = rest_get_date_with_gmt( $request['date'] );
810
811
			if ( ! empty( $date_data ) ) {
812
				list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
813
			}
814
		} elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) {
815
			$date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
816
817
			if ( ! empty( $date_data ) ) {
818
				list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
819
			}
820
		}
821
		// Post slug.
822 View Code Duplication
		if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) {
823
			$prepared_post->post_name = $request['slug'];
824
		}
825
826
		// Author
827
		if ( ! empty( $schema['properties']['author'] ) && ! empty( $request['author'] ) ) {
828
			$post_author = (int) $request['author'];
829 View Code Duplication
			if ( get_current_user_id() !== $post_author ) {
830
				$user_obj = get_userdata( $post_author );
831
				if ( ! $user_obj ) {
832
					return new WP_Error( 'rest_invalid_author', __( 'Invalid author id.' ), array( 'status' => 400 ) );
833
				}
834
			}
835
			$prepared_post->post_author = $post_author;
836
		}
837
838
		// Post password.
839
		if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) && '' !== $request['password'] ) {
840
			$prepared_post->post_password = $request['password'];
841
842
			if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
843
				return new WP_Error( 'rest_invalid_field', __( 'A post can not be sticky and have a password.' ), array( 'status' => 400 ) );
844
			}
845
846
			if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) {
847
				return new WP_Error( 'rest_invalid_field', __( 'A sticky post can not be password protected.' ), array( 'status' => 400 ) );
848
			}
849
		}
850
851
		if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
852
			if ( ! empty( $prepared_post->ID ) && post_password_required( $prepared_post->ID ) ) {
853
				return new WP_Error( 'rest_invalid_field', __( 'A password protected post can not be set to sticky.' ), array( 'status' => 400 ) );
854
			}
855
		}
856
857
		// Parent.
858
		if ( ! empty( $schema['properties']['parent'] ) && ! empty( $request['parent'] ) ) {
859
			$parent = $this->get_post( (int) $request['parent'] );
860
			if ( empty( $parent ) ) {
861
				return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post parent id.' ), array( 'status' => 400 ) );
862
			}
863
864
			$prepared_post->post_parent = (int) $parent->ID;
865
		}
866
867
		// Menu order.
868 View Code Duplication
		if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) {
869
			$prepared_post->menu_order = (int) $request['menu_order'];
870
		}
871
872
		// Comment status.
873 View Code Duplication
		if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) {
874
			$prepared_post->comment_status = $request['comment_status'];
875
		}
876
877
		// Ping status.
878 View Code Duplication
		if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) {
879
			$prepared_post->ping_status = $request['ping_status'];
880
		}
881
		/**
882
		 * Filter a post before it is inserted via the REST API.
883
		 *
884
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
885
		 * prepared for insertion.
886
		 *
887
		 * @param stdClass        $prepared_post An object representing a single post prepared
888
		 *                                       for inserting or updating the database.
889
		 * @param WP_REST_Request $request       Request object.
890
		 */
891
		return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
892
893
	}
894
895
	/**
896
	 * Determine validity and normalize provided status param.
897
	 *
898
	 * @param string $post_status
899
	 * @param object $post_type
900
	 * @return WP_Error|string $post_status
901
	 */
902
	protected function handle_status_param( $post_status, $post_type ) {
903
904
		switch ( $post_status ) {
905
			case 'draft':
906
			case 'pending':
907
				break;
908 View Code Duplication
			case 'private':
909
				if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
910
					return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to create private posts in this post type' ), array( 'status' => rest_authorization_required_code() ) );
911
				}
912
				break;
913
			case 'publish':
914 View Code Duplication
			case 'future':
915
				if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
916
					return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to publish posts in this post type' ), array( 'status' => rest_authorization_required_code() ) );
917
				}
918
				break;
919
			default:
920
				if ( ! get_post_status_object( $post_status ) ) {
921
					$post_status = 'draft';
922
				}
923
				break;
924
		}
925
926
		return $post_status;
927
	}
928
929
	/**
930
	 * Determine the featured media based on a request param.
931
	 *
932
	 * @param int $featured_media
933
	 * @param int $post_id
934
	 *
935
	 * @return bool|WP_Error
936
	 */
937
	protected function handle_featured_media( $featured_media, $post_id ) {
938
939
		$featured_media = (int) $featured_media;
940
		if ( $featured_media ) {
941
			$result = set_post_thumbnail( $post_id, $featured_media );
942
			if ( $result ) {
943
				return true;
944
			} else {
945
				return new WP_Error( 'rest_invalid_featured_media', __( 'Invalid featured media id.' ), array( 'status' => 400 ) );
946
			}
947
		} else {
948
			return delete_post_thumbnail( $post_id );
949
		}
950
951
	}
952
953
	/**
954
	 * Set the template for a page.
955
	 *
956
	 * @param string $template
957
	 * @param integer $post_id
958
	 */
959
	public function handle_template( $template, $post_id ) {
960
		if ( in_array( $template, array_keys( wp_get_theme()->get_page_templates( $this->get_post( $post_id ) ) ) ) ) {
961
			update_post_meta( $post_id, '_wp_page_template', $template );
962
		} else {
963
			update_post_meta( $post_id, '_wp_page_template', '' );
964
		}
965
	}
966
967
	/**
968
	 * Update the post's terms from a REST request.
969
	 *
970
	 * @param  int             $post_id The post ID to update the terms form.
971
	 * @param  WP_REST_Request $request The request object with post and terms data.
972
	 * @return null|WP_Error   WP_Error on an error assigning any of ther terms.
973
	 */
974
	protected function handle_terms( $post_id, $request ) {
975
		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
976
		foreach ( $taxonomies as $taxonomy ) {
977
			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
978
979
			if ( ! isset( $request[ $base ] ) ) {
980
				continue;
981
			}
982
			$terms = array_map( 'absint', $request[ $base ] );
983
			$result = wp_set_object_terms( $post_id, $terms, $taxonomy->name );
984
			if ( is_wp_error( $result ) ) {
985
				return $result;
986
			}
987
		}
988
	}
989
990
	/**
991
	 * Check if a given post type should be viewed or managed.
992
	 *
993
	 * @param object|string $post_type
994
	 * @return boolean Is post type allowed?
995
	 */
996
	protected function check_is_post_type_allowed( $post_type ) {
997
		if ( ! is_object( $post_type ) ) {
998
			$post_type = get_post_type_object( $post_type );
999
		}
1000
1001
		if ( ! empty( $post_type ) && ! empty( $post_type->show_in_rest ) ) {
1002
			return true;
1003
		}
1004
1005
		return false;
1006
	}
1007
1008
	/**
1009
	 * Check if we can read a post.
1010
	 *
1011
	 * Correctly handles posts with the inherit status.
1012
	 *
1013
	 * @param object $post Post object.
1014
	 * @return boolean Can we read it?
1015
	 */
1016
	public function check_read_permission( $post ) {
1017
		if ( ! empty( $post->post_password ) && ! $this->check_update_permission( $post ) ) {
1018
			return false;
1019
		}
1020
1021
		$post_type = get_post_type_object( $post->post_type );
1022
		if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
1023
			return false;
1024
		}
1025
1026
		// Can we read the post?
1027
		if ( 'publish' === $post->post_status || current_user_can( $post_type->cap->read_post, $post->ID ) ) {
1028
			return true;
1029
		}
1030
1031
		$post_status_obj = get_post_status_object( $post->post_status );
1032
		if ( $post_status_obj && $post_status_obj->public ) {
1033
			return true;
1034
		}
1035
1036
		// Can we read the parent if we're inheriting?
1037
		if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
1038
			$parent = $this->get_post( $post->post_parent );
1039
			return $this->check_read_permission( $parent );
0 ignored issues
show
Bug introduced by
It seems like $parent defined by $this->get_post($post->post_parent) on line 1038 can be null; however, WP_REST_Posts_Controller::check_read_permission() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1040
		}
1041
1042
		// If we don't have a parent, but the status is set to inherit, assume
1043
		// it's published (as per get_post_status()).
1044
		if ( 'inherit' === $post->post_status ) {
1045
			return true;
1046
		}
1047
1048
		return false;
1049
	}
1050
1051
	/**
1052
	 * Check if we can edit a post.
1053
	 *
1054
	 * @param object $post Post object.
1055
	 * @return boolean Can we edit it?
1056
	 */
1057 View Code Duplication
	protected function check_update_permission( $post ) {
1058
		$post_type = get_post_type_object( $post->post_type );
1059
1060
		if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
1061
			return false;
1062
		}
1063
1064
		return current_user_can( $post_type->cap->edit_post, $post->ID );
1065
	}
1066
1067
	/**
1068
	 * Check if we can create a post.
1069
	 *
1070
	 * @param object $post Post object.
1071
	 * @return boolean Can we create it?.
1072
	 */
1073 View Code Duplication
	protected function check_create_permission( $post ) {
1074
		$post_type = get_post_type_object( $post->post_type );
1075
1076
		if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
1077
			return false;
1078
		}
1079
1080
		return current_user_can( $post_type->cap->create_posts );
1081
	}
1082
1083
	/**
1084
	 * Check if we can delete a post.
1085
	 *
1086
	 * @param object $post Post object.
1087
	 * @return boolean Can we delete it?
1088
	 */
1089 View Code Duplication
	protected function check_delete_permission( $post ) {
1090
		$post_type = get_post_type_object( $post->post_type );
1091
1092
		if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
1093
			return false;
1094
		}
1095
1096
		return current_user_can( $post_type->cap->delete_post, $post->ID );
1097
	}
1098
1099
	/**
1100
	 * Prepare a single post output for response.
1101
	 *
1102
	 * @param WP_Post $post Post object.
1103
	 * @param WP_REST_Request $request Request object.
1104
	 * @return WP_REST_Response $data
1105
	 */
1106
	public function prepare_item_for_response( $post, $request ) {
1107
		$GLOBALS['post'] = $post;
1108
		setup_postdata( $post );
1109
1110
		$schema = $this->get_item_schema();
1111
1112
		// Base fields for every post.
1113
		$data = array();
1114
1115
		if ( ! empty( $schema['properties']['id'] ) ) {
1116
			$data['id'] = $post->ID;
1117
		}
1118
1119 View Code Duplication
		if ( ! empty( $schema['properties']['date'] ) ) {
1120
			$data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
1121
		}
1122
1123 View Code Duplication
		if ( ! empty( $schema['properties']['date_gmt'] ) ) {
1124
			$data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
1125
		}
1126
1127 View Code Duplication
		if ( ! empty( $schema['properties']['guid'] ) ) {
1128
			$data['guid'] = array(
1129
				/** This filter is documented in wp-includes/post-template.php */
1130
				'rendered' => apply_filters( 'get_the_guid', $post->guid ),
1131
				'raw'      => $post->guid,
1132
			);
1133
		}
1134
1135 View Code Duplication
		if ( ! empty( $schema['properties']['modified'] ) ) {
1136
			$data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
1137
		}
1138
1139 View Code Duplication
		if ( ! empty( $schema['properties']['modified_gmt'] ) ) {
1140
			$data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
1141
		}
1142
1143
		if ( ! empty( $schema['properties']['password'] ) ) {
1144
			$data['password'] = $post->post_password;
1145
		}
1146
1147
		if ( ! empty( $schema['properties']['slug'] ) ) {
1148
			$data['slug'] = $post->post_name;
1149
		}
1150
1151
		if ( ! empty( $schema['properties']['status'] ) ) {
1152
			$data['status'] = $post->post_status;
1153
		}
1154
1155
		if ( ! empty( $schema['properties']['type'] ) ) {
1156
			$data['type'] = $post->post_type;
1157
		}
1158
1159
		if ( ! empty( $schema['properties']['link'] ) ) {
1160
			$data['link'] = get_permalink( $post->ID );
1161
		}
1162
1163 View Code Duplication
		if ( ! empty( $schema['properties']['title'] ) ) {
1164
			$data['title'] = array(
1165
				'raw'      => $post->post_title,
1166
				'rendered' => get_the_title( $post->ID ),
1167
			);
1168
		}
1169
1170
		if ( ! empty( $schema['properties']['content'] ) ) {
1171
1172
			if ( ! empty( $post->post_password ) ) {
1173
				$this->prepare_password_response( $post->post_password );
1174
			}
1175
1176
			$data['content'] = array(
1177
				'raw'      => $post->post_content,
1178
				/** This filter is documented in wp-includes/post-template.php */
1179
				'rendered' => apply_filters( 'the_content', $post->post_content ),
1180
			);
1181
1182
			// Don't leave our cookie lying around: https://github.com/WP-API/WP-API/issues/1055.
1183
			if ( ! empty( $post->post_password ) ) {
1184
				$_COOKIE[ 'wp-postpass_' . COOKIEHASH ] = '';
1185
			}
1186
		}
1187
1188 View Code Duplication
		if ( ! empty( $schema['properties']['excerpt'] ) ) {
1189
			$data['excerpt'] = array(
1190
				'raw'      => $post->post_excerpt,
1191
				'rendered' => $this->prepare_excerpt_response( $post->post_excerpt, $post ),
1192
			);
1193
		}
1194
1195
		if ( ! empty( $schema['properties']['author'] ) ) {
1196
			$data['author'] = (int) $post->post_author;
1197
		}
1198
1199
		if ( ! empty( $schema['properties']['featured_media'] ) ) {
1200
			$data['featured_media'] = (int) get_post_thumbnail_id( $post->ID );
1201
		}
1202
1203
		if ( ! empty( $schema['properties']['parent'] ) ) {
1204
			$data['parent'] = (int) $post->post_parent;
1205
		}
1206
1207
		if ( ! empty( $schema['properties']['menu_order'] ) ) {
1208
			$data['menu_order'] = (int) $post->menu_order;
1209
		}
1210
1211
		if ( ! empty( $schema['properties']['comment_status'] ) ) {
1212
			$data['comment_status'] = $post->comment_status;
1213
		}
1214
1215
		if ( ! empty( $schema['properties']['ping_status'] ) ) {
1216
			$data['ping_status'] = $post->ping_status;
1217
		}
1218
1219
		if ( ! empty( $schema['properties']['sticky'] ) ) {
1220
			$data['sticky'] = is_sticky( $post->ID );
1221
		}
1222
1223 View Code Duplication
		if ( ! empty( $schema['properties']['template'] ) ) {
1224
			if ( $template = get_page_template_slug( $post->ID ) ) {
1225
				$data['template'] = $template;
1226
			} else {
1227
				$data['template'] = '';
1228
			}
1229
		}
1230
1231 View Code Duplication
		if ( ! empty( $schema['properties']['format'] ) ) {
1232
			$data['format'] = get_post_format( $post->ID );
1233
			// Fill in blank post format.
1234
			if ( empty( $data['format'] ) ) {
1235
				$data['format'] = 'standard';
1236
			}
1237
		}
1238
1239
		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
1240
		foreach ( $taxonomies as $taxonomy ) {
1241
			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
1242
			if ( ! empty( $schema['properties'][ $base ] ) ) {
1243
				$terms = get_the_terms( $post, $taxonomy->name );
1244
				$data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
1245
			}
1246
		}
1247
1248
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
1249
		$data = $this->add_additional_fields_to_object( $data, $request );
1250
		$data = $this->filter_response_by_context( $data, $context );
1251
1252
		// Wrap the data in a response object.
1253
		$response = rest_ensure_response( $data );
1254
1255
		$response->add_links( $this->prepare_links( $post ) );
1256
1257
		/**
1258
		 * Filter the post data for a response.
1259
		 *
1260
		 * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
1261
		 * prepared for the response.
1262
		 *
1263
		 * @param WP_REST_Response   $response   The response object.
1264
		 * @param WP_Post            $post       Post object.
1265
		 * @param WP_REST_Request    $request    Request object.
1266
		 */
1267
		return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
1268
	}
1269
1270
	/**
1271
	 * Prepare links for the request.
1272
	 *
1273
	 * @param WP_Post $post Post object.
1274
	 * @return array Links for the given post.
1275
	 */
1276
	protected function prepare_links( $post ) {
1277
		$base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
1278
1279
		// Entity meta
1280
		$links = array(
1281
			'self' => array(
1282
				'href'   => rest_url( trailingslashit( $base ) . $post->ID ),
1283
			),
1284
			'collection' => array(
1285
				'href'   => rest_url( $base ),
1286
			),
1287
			'about'      => array(
1288
				'href'   => rest_url( 'wp/v2/types/' . $this->post_type ),
1289
			),
1290
		);
1291
1292 View Code Duplication
		if ( ( in_array( $post->post_type, array( 'post', 'page' ) ) || post_type_supports( $post->post_type, 'author' ) )
1293
			&& ! empty( $post->post_author ) ) {
1294
			$links['author'] = array(
1295
				'href'       => rest_url( 'wp/v2/users/' . $post->post_author ),
1296
				'embeddable' => true,
1297
			);
1298
		};
1299
1300
		if ( in_array( $post->post_type, array( 'post', 'page' ) ) || post_type_supports( $post->post_type, 'comments' ) ) {
1301
			$replies_url = rest_url( 'wp/v2/comments' );
1302
			$replies_url = add_query_arg( 'post', $post->ID, $replies_url );
1303
			$links['replies'] = array(
1304
				'href'         => $replies_url,
1305
				'embeddable'   => true,
1306
			);
1307
		}
1308
1309 View Code Duplication
		if ( in_array( $post->post_type, array( 'post', 'page' ) ) || post_type_supports( $post->post_type, 'revisions' ) ) {
1310
			$links['version-history'] = array(
1311
				'href' => rest_url( trailingslashit( $base ) . $post->ID . '/revisions' ),
1312
			);
1313
		}
1314
		$post_type_obj = get_post_type_object( $post->post_type );
1315
		if ( $post_type_obj->hierarchical && ! empty( $post->post_parent ) ) {
1316
			$links['up'] = array(
1317
				'href'       => rest_url( trailingslashit( $base ) . (int) $post->post_parent ),
1318
				'embeddable' => true,
1319
			);
1320
		}
1321
1322
		// If we have a featured media, add that.
1323
		if ( $featured_media = get_post_thumbnail_id( $post->ID ) ) {
1324
			$image_url = rest_url( 'wp/v2/media/' . $featured_media );
1325
			$links['https://api.w.org/featuredmedia'] = array(
1326
				'href'       => $image_url,
1327
				'embeddable' => true,
1328
			);
1329
		}
1330
		if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ) ) ) {
1331
			$attachments_url = rest_url( 'wp/v2/media' );
1332
			$attachments_url = add_query_arg( 'parent', $post->ID, $attachments_url );
1333
			$links['https://api.w.org/attachment'] = array(
1334
				'href'       => $attachments_url,
1335
			);
1336
		}
1337
1338
		$taxonomies = get_object_taxonomies( $post->post_type );
1339
		if ( ! empty( $taxonomies ) ) {
1340
			$links['https://api.w.org/term'] = array();
1341
1342
			foreach ( $taxonomies as $tax ) {
1343
				$taxonomy_obj = get_taxonomy( $tax );
1344
				// Skip taxonomies that are not public.
1345
				if ( empty( $taxonomy_obj->show_in_rest ) ) {
1346
					continue;
1347
				}
1348
1349
				$tax_base = ! empty( $taxonomy_obj->rest_base ) ? $taxonomy_obj->rest_base : $tax;
1350
				$terms_url = add_query_arg(
1351
					'post',
1352
					$post->ID,
1353
					rest_url( 'wp/v2/' . $tax_base )
1354
				);
1355
1356
				$links['https://api.w.org/term'][] = array(
1357
					'href'       => $terms_url,
1358
					'taxonomy'   => $tax,
1359
					'embeddable' => true,
1360
				);
1361
			}
1362
		}
1363
1364
		return $links;
1365
	}
1366
1367
	/**
1368
	 * Get the Post's schema, conforming to JSON Schema.
1369
	 *
1370
	 * @return array
1371
	 */
1372
	public function get_item_schema() {
1373
1374
		$schema = array(
1375
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
1376
			'title'      => $this->post_type,
1377
			'type'       => 'object',
1378
			/*
1379
			 * Base properties for every Post.
1380
			 */
1381
			'properties' => array(
1382
				'date'            => array(
1383
					'description' => __( "The date the object was published, in the site's timezone." ),
1384
					'type'        => 'string',
1385
					'format'      => 'date-time',
1386
					'context'     => array( 'view', 'edit', 'embed' ),
1387
				),
1388
				'date_gmt'        => array(
1389
					'description' => __( 'The date the object was published, as GMT.' ),
1390
					'type'        => 'string',
1391
					'format'      => 'date-time',
1392
					'context'     => array( 'view', 'edit' ),
1393
				),
1394
				'guid'            => array(
1395
					'description' => __( 'The globally unique identifier for the object.' ),
1396
					'type'        => 'object',
1397
					'context'     => array( 'view', 'edit' ),
1398
					'readonly'    => true,
1399
					'properties'  => array(
1400
						'raw'      => array(
1401
							'description' => __( 'GUID for the object, as it exists in the database.' ),
1402
							'type'        => 'string',
1403
							'context'     => array( 'edit' ),
1404
							'readonly'    => true,
1405
						),
1406
						'rendered' => array(
1407
							'description' => __( 'GUID for the object, transformed for display.' ),
1408
							'type'        => 'string',
1409
							'context'     => array( 'view', 'edit' ),
1410
							'readonly'    => true,
1411
						),
1412
					),
1413
				),
1414
				'id'              => array(
1415
					'description' => __( 'Unique identifier for the object.' ),
1416
					'type'        => 'integer',
1417
					'context'     => array( 'view', 'edit', 'embed' ),
1418
					'readonly'    => true,
1419
				),
1420
				'link'            => array(
1421
					'description' => __( 'URL to the object.' ),
1422
					'type'        => 'string',
1423
					'format'      => 'uri',
1424
					'context'     => array( 'view', 'edit', 'embed' ),
1425
					'readonly'    => true,
1426
				),
1427
				'modified'        => array(
1428
					'description' => __( "The date the object was last modified, in the site's timezone." ),
1429
					'type'        => 'string',
1430
					'format'      => 'date-time',
1431
					'context'     => array( 'view', 'edit' ),
1432
					'readonly'    => true,
1433
				),
1434
				'modified_gmt'    => array(
1435
					'description' => __( 'The date the object was last modified, as GMT.' ),
1436
					'type'        => 'string',
1437
					'format'      => 'date-time',
1438
					'context'     => array( 'view', 'edit' ),
1439
					'readonly'    => true,
1440
				),
1441
				'password'        => array(
1442
					'description' => __( 'A password to protect access to the post.' ),
1443
					'type'        => 'string',
1444
					'context'     => array( 'edit' ),
1445
				),
1446
				'slug'            => array(
1447
					'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
1448
					'type'        => 'string',
1449
					'context'     => array( 'view', 'edit', 'embed' ),
1450
					'arg_options' => array(
1451
						'sanitize_callback' => array( $this, 'sanitize_slug' ),
1452
					),
1453
				),
1454
				'status'          => array(
1455
					'description' => __( 'A named status for the object.' ),
1456
					'type'        => 'string',
1457
					'enum'        => array_keys( get_post_stati( array( 'internal' => false ) ) ),
1458
					'context'     => array( 'edit' ),
1459
				),
1460
				'type'            => array(
1461
					'description' => __( 'Type of Post for the object.' ),
1462
					'type'        => 'string',
1463
					'context'     => array( 'view', 'edit', 'embed' ),
1464
					'readonly'    => true,
1465
				),
1466
			),
1467
		);
1468
1469
		$post_type_obj = get_post_type_object( $this->post_type );
1470 View Code Duplication
		if ( $post_type_obj->hierarchical ) {
1471
			$schema['properties']['parent'] = array(
1472
				'description' => __( 'The id for the parent of the object.' ),
1473
				'type'        => 'integer',
1474
				'context'     => array( 'view', 'edit' ),
1475
			);
1476
		}
1477
1478
		$post_type_attributes = array(
1479
			'title',
1480
			'editor',
1481
			'author',
1482
			'excerpt',
1483
			'thumbnail',
1484
			'comments',
1485
			'revisions',
1486
			'page-attributes',
1487
			'post-formats',
1488
		);
1489
		$fixed_schemas = array(
1490
			'post' => array(
1491
				'title',
1492
				'editor',
1493
				'author',
1494
				'excerpt',
1495
				'thumbnail',
1496
				'comments',
1497
				'revisions',
1498
				'post-formats',
1499
			),
1500
			'page' => array(
1501
				'title',
1502
				'editor',
1503
				'author',
1504
				'excerpt',
1505
				'thumbnail',
1506
				'comments',
1507
				'revisions',
1508
				'page-attributes',
1509
			),
1510
			'attachment' => array(
1511
				'title',
1512
				'author',
1513
				'comments',
1514
				'revisions',
1515
			),
1516
		);
1517
		foreach ( $post_type_attributes as $attribute ) {
1518
			if ( isset( $fixed_schemas[ $this->post_type ] ) && ! in_array( $attribute, $fixed_schemas[ $this->post_type ] ) ) {
1519
				continue;
1520
			} elseif ( ! in_array( $this->post_type, array_keys( $fixed_schemas ) ) && ! post_type_supports( $this->post_type, $attribute ) ) {
1521
				continue;
1522
			}
1523
1524
			switch ( $attribute ) {
1525
1526 View Code Duplication
				case 'title':
1527
					$schema['properties']['title'] = array(
1528
						'description' => __( 'The title for the object.' ),
1529
						'type'        => 'object',
1530
						'context'     => array( 'view', 'edit', 'embed' ),
1531
						'properties'  => array(
1532
							'raw' => array(
1533
								'description' => __( 'Title for the object, as it exists in the database.' ),
1534
								'type'        => 'string',
1535
								'context'     => array( 'edit' ),
1536
							),
1537
							'rendered' => array(
1538
								'description' => __( 'HTML title for the object, transformed for display.' ),
1539
								'type'        => 'string',
1540
								'context'     => array( 'view', 'edit', 'embed' ),
1541
								'readonly'    => true,
1542
							),
1543
						),
1544
					);
1545
					break;
1546
1547
				case 'editor':
1548
					$schema['properties']['content'] = array(
1549
						'description' => __( 'The content for the object.' ),
1550
						'type'        => 'object',
1551
						'context'     => array( 'view', 'edit' ),
1552
						'properties'  => array(
1553
							'raw' => array(
1554
								'description' => __( 'Content for the object, as it exists in the database.' ),
1555
								'type'        => 'string',
1556
								'context'     => array( 'edit' ),
1557
							),
1558
							'rendered' => array(
1559
								'description' => __( 'HTML content for the object, transformed for display.' ),
1560
								'type'        => 'string',
1561
								'context'     => array( 'view', 'edit' ),
1562
								'readonly'    => true,
1563
							),
1564
						),
1565
					);
1566
					break;
1567
1568 View Code Duplication
				case 'author':
1569
					$schema['properties']['author'] = array(
1570
						'description' => __( 'The id for the author of the object.' ),
1571
						'type'        => 'integer',
1572
						'context'     => array( 'view', 'edit', 'embed' ),
1573
					);
1574
					break;
1575
1576 View Code Duplication
				case 'excerpt':
1577
					$schema['properties']['excerpt'] = array(
1578
						'description' => __( 'The excerpt for the object.' ),
1579
						'type'        => 'object',
1580
						'context'     => array( 'view', 'edit', 'embed' ),
1581
						'properties'  => array(
1582
							'raw' => array(
1583
								'description' => __( 'Excerpt for the object, as it exists in the database.' ),
1584
								'type'        => 'string',
1585
								'context'     => array( 'edit' ),
1586
							),
1587
							'rendered' => array(
1588
								'description' => __( 'HTML excerpt for the object, transformed for display.' ),
1589
								'type'        => 'string',
1590
								'context'     => array( 'view', 'edit', 'embed' ),
1591
								'readonly'    => true,
1592
							),
1593
						),
1594
					);
1595
					break;
1596
1597 View Code Duplication
				case 'thumbnail':
1598
					$schema['properties']['featured_media'] = array(
1599
						'description' => __( 'The id of the featured media for the object.' ),
1600
						'type'        => 'integer',
1601
						'context'     => array( 'view', 'edit' ),
1602
					);
1603
					break;
1604
1605
				case 'comments':
1606
					$schema['properties']['comment_status'] = array(
1607
						'description' => __( 'Whether or not comments are open on the object.' ),
1608
						'type'        => 'string',
1609
						'enum'        => array( 'open', 'closed' ),
1610
						'context'     => array( 'view', 'edit' ),
1611
					);
1612
					$schema['properties']['ping_status'] = array(
1613
						'description' => __( 'Whether or not the object can be pinged.' ),
1614
						'type'        => 'string',
1615
						'enum'        => array( 'open', 'closed' ),
1616
						'context'     => array( 'view', 'edit' ),
1617
					);
1618
					break;
1619
1620 View Code Duplication
				case 'page-attributes':
1621
					$schema['properties']['menu_order'] = array(
1622
						'description' => __( 'The order of the object in relation to other object of its type.' ),
1623
						'type'        => 'integer',
1624
						'context'     => array( 'view', 'edit' ),
1625
					);
1626
					break;
1627
1628
				case 'post-formats':
1629
					$schema['properties']['format'] = array(
1630
						'description' => __( 'The format for the object.' ),
1631
						'type'        => 'string',
1632
						'enum'        => array_values( get_post_format_slugs() ),
1633
						'context'     => array( 'view', 'edit' ),
1634
					);
1635
					break;
1636
1637
			}
1638
		}
1639
1640
		if ( 'post' === $this->post_type ) {
1641
			$schema['properties']['sticky'] = array(
1642
				'description' => __( 'Whether or not the object should be treated as sticky.' ),
1643
				'type'        => 'boolean',
1644
				'context'     => array( 'view', 'edit' ),
1645
			);
1646
		}
1647
1648
		if ( 'page' === $this->post_type ) {
1649
			$schema['properties']['template'] = array(
1650
				'description' => __( 'The theme file to use to display the object.' ),
1651
				'type'        => 'string',
1652
				'enum'        => array_keys( wp_get_theme()->get_page_templates() ),
1653
				'context'     => array( 'view', 'edit' ),
1654
			);
1655
		}
1656
1657
		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
1658 View Code Duplication
		foreach ( $taxonomies as $taxonomy ) {
1659
			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
1660
			$schema['properties'][ $base ] = array(
1661
				'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
1662
				'type'        => 'array',
1663
				'context'     => array( 'view', 'edit' ),
1664
			);
1665
		}
1666
1667
		return $this->add_additional_fields_schema( $schema );
1668
	}
1669
1670
	/**
1671
	 * Get the query params for collections of attachments.
1672
	 *
1673
	 * @return array
1674
	 */
1675
	public function get_collection_params() {
1676
		$params = parent::get_collection_params();
1677
1678
		$params['context']['default'] = 'view';
1679
1680
		$params['after'] = array(
1681
			'description'        => __( 'Limit response to resources published after a given ISO8601 compliant date.' ),
1682
			'type'               => 'string',
1683
			'format'             => 'date-time',
1684
			'validate_callback'  => 'rest_validate_request_arg',
1685
		);
1686
		if ( post_type_supports( $this->post_type, 'author' ) ) {
1687
			$params['author'] = array(
1688
				'description'         => __( 'Limit result set to posts assigned to specific authors.' ),
1689
				'type'                => 'array',
1690
				'default'             => array(),
1691
				'sanitize_callback'   => 'wp_parse_id_list',
1692
				'validate_callback'   => 'rest_validate_request_arg',
1693
			);
1694
			$params['author_exclude'] = array(
1695
				'description'         => __( 'Ensure result set excludes posts assigned to specific authors.' ),
1696
				'type'                => 'array',
1697
				'default'             => array(),
1698
				'sanitize_callback'   => 'wp_parse_id_list',
1699
				'validate_callback'   => 'rest_validate_request_arg',
1700
			);
1701
		}
1702
		$params['before'] = array(
1703
			'description'        => __( 'Limit response to resources published before a given ISO8601 compliant date.' ),
1704
			'type'               => 'string',
1705
			'format'             => 'date-time',
1706
			'validate_callback'  => 'rest_validate_request_arg',
1707
		);
1708
		$params['exclude'] = array(
1709
			'description'        => __( 'Ensure result set excludes specific ids.' ),
1710
			'type'               => 'array',
1711
			'default'            => array(),
1712
			'sanitize_callback'  => 'wp_parse_id_list',
1713
		);
1714
		$params['include'] = array(
1715
			'description'        => __( 'Limit result set to specific ids.' ),
1716
			'type'               => 'array',
1717
			'default'            => array(),
1718
			'sanitize_callback'  => 'wp_parse_id_list',
1719
		);
1720
		if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
1721
			$params['menu_order'] = array(
1722
				'description'        => __( 'Limit result set to resources with a specific menu_order value.' ),
1723
				'type'               => 'integer',
1724
				'sanitize_callback'  => 'absint',
1725
				'validate_callback'  => 'rest_validate_request_arg',
1726
			);
1727
		}
1728
		$params['offset'] = array(
1729
			'description'        => __( 'Offset the result set by a specific number of items.' ),
1730
			'type'               => 'integer',
1731
			'sanitize_callback'  => 'absint',
1732
			'validate_callback'  => 'rest_validate_request_arg',
1733
		);
1734
		$params['order'] = array(
1735
			'description'        => __( 'Order sort attribute ascending or descending.' ),
1736
			'type'               => 'string',
1737
			'default'            => 'desc',
1738
			'enum'               => array( 'asc', 'desc' ),
1739
			'validate_callback'  => 'rest_validate_request_arg',
1740
		);
1741
		$params['orderby'] = array(
1742
			'description'        => __( 'Sort collection by object attribute.' ),
1743
			'type'               => 'string',
1744
			'default'            => 'date',
1745
			'enum'               => array(
1746
				'date',
1747
				'relevance',
1748
				'id',
1749
				'include',
1750
				'title',
1751
				'slug',
1752
			),
1753
			'validate_callback'  => 'rest_validate_request_arg',
1754
		);
1755
		if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
1756
			$params['orderby']['enum'][] = 'menu_order';
1757
		}
1758
1759
		$post_type_obj = get_post_type_object( $this->post_type );
1760
		if ( $post_type_obj->hierarchical || 'attachment' === $this->post_type ) {
1761
			$params['parent'] = array(
1762
				'description'       => __( 'Limit result set to those of particular parent ids.' ),
1763
				'type'              => 'array',
1764
				'sanitize_callback' => 'wp_parse_id_list',
1765
				'default'           => array(),
1766
			);
1767
			$params['parent_exclude'] = array(
1768
				'description'       => __( 'Limit result set to all items except those of a particular parent id.' ),
1769
				'type'              => 'array',
1770
				'sanitize_callback' => 'wp_parse_id_list',
1771
				'default'           => array(),
1772
			);
1773
		}
1774
1775
		$params['slug'] = array(
1776
			'description'       => __( 'Limit result set to posts with a specific slug.' ),
1777
			'type'              => 'string',
1778
			'validate_callback' => 'rest_validate_request_arg',
1779
		);
1780
		$params['status'] = array(
1781
			'default'           => 'publish',
1782
			'description'       => __( 'Limit result set to posts assigned a specific status; can be comma-delimited list of status types.' ),
1783
			'enum'              => array_merge( array_keys( get_post_stati() ), array( 'any' ) ),
1784
			'sanitize_callback' => 'sanitize_key',
1785
			'type'              => 'string',
1786
			'validate_callback' => array( $this, 'validate_user_can_query_private_statuses' ),
1787
		);
1788
		$params['filter'] = array(
1789
			'description'       => __( 'Use WP Query arguments to modify the response; private query vars require appropriate authorization.' ),
1790
		);
1791
1792
		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
1793 View Code Duplication
		foreach ( $taxonomies as $taxonomy ) {
1794
			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
1795
1796
			$params[ $base ] = array(
1797
				'description'       => sprintf( __( 'Limit result set to all items that have the specified term assigned in the %s taxonomy.' ), $base ),
1798
				'type'              => 'array',
1799
				'sanitize_callback' => 'wp_parse_id_list',
1800
				'default'           => array(),
1801
			);
1802
		}
1803
1804
		if ( 'post' === $this->post_type ) {
1805
			$params['sticky'] = array(
1806
				'description'       => __( 'Limit result set to items that are sticky.' ),
1807
				'type'              => 'boolean',
1808
				'sanitize_callback' => 'rest_parse_request_arg',
1809
			);
1810
		}
1811
1812
		return $params;
1813
	}
1814
1815
	/**
1816
	 * Validate whether the user can query private statuses
1817
	 *
1818
	 * @param  mixed $value
1819
	 * @param  WP_REST_Request $request
1820
	 * @param  string $parameter
1821
	 * @return WP_Error|boolean
1822
	 */
1823 View Code Duplication
	public function validate_user_can_query_private_statuses( $value, $request, $parameter ) {
1824
		if ( 'publish' === $value ) {
1825
			return true;
1826
		}
1827
		$post_type_obj = get_post_type_object( $this->post_type );
1828
		if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
1829
			return true;
1830
		}
1831
		return new WP_Error( 'rest_forbidden_status', __( 'Status is forbidden' ), array( 'status' => rest_authorization_required_code() ) );
1832
	}
1833
}
1834