Completed
Push — update/dialogue-focus-on-conte... ( 9f1745...fa862f )
by
unknown
80:03 queued 71:18
created

class.wpcom-json-api-list-posts-v1-1-endpoint.php (3 issues)

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_List_Posts_v1_1_Endpoint(
4
	array(
5
		'description'      => 'Get a list of matching posts.',
6
		'min_version'      => '1.1',
7
		'max_version'      => '1.1',
8
9
		'group'            => 'posts',
10
		'stat'             => 'posts',
11
12
		'method'           => 'GET',
13
		'path'             => '/sites/%s/posts/',
14
		'path_labels'      => array(
15
			'$site' => '(int|string) Site ID or domain',
16
		),
17
18
		'allow_fallback_to_jetpack_blog_token' => true,
19
20
		'query_parameters' => array(
21
			'number'          => '(int=20) The number of posts to return. Limit: 100.',
22
			'offset'          => '(int=0) 0-indexed offset.',
23
			'page'            => '(int) Return the Nth 1-indexed page of posts. Takes precedence over the <code>offset</code> parameter.',
24
			'page_handle'     => '(string) A page handle, returned from a previous API call as a <code>meta.next_page</code> property. This is the most efficient way to fetch the next page of results.',
25
			'order'           => array(
26
				'DESC' => 'Return posts in descending order. For dates, that means newest to oldest.',
27
				'ASC'  => 'Return posts in ascending order. For dates, that means oldest to newest.',
28
			),
29
			'order_by'        => array(
30
				'date'          => 'Order by the created time of each post.',
31
				'modified'      => 'Order by the modified time of each post.',
32
				'title'         => "Order lexicographically by the posts' titles.",
33
				'comment_count' => 'Order by the number of comments for each post.',
34
				'ID'            => 'Order by post ID.',
35
			),
36
			'after'           => '(ISO 8601 datetime) Return posts dated after the specified datetime.',
37
			'before'          => '(ISO 8601 datetime) Return posts dated before the specified datetime.',
38
			'modified_after'  => '(ISO 8601 datetime) Return posts modified after the specified datetime.',
39
			'modified_before' => '(ISO 8601 datetime) Return posts modified before the specified datetime.',
40
			'tag'             => '(string) Specify the tag name or slug.',
41
			'category'        => '(string) Specify the category name or slug.',
42
			'term'            => '(object:string) Specify comma-separated term slugs to search within, indexed by taxonomy slug.',
43
			'type'            => "(string) Specify the post type. Defaults to 'post', use 'any' to query for both posts and pages. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.",
44
			'parent_id'       => '(int) Returns only posts which are children of the specified post. Applies only to hierarchical post types.',
45
			'exclude'         => '(array:int|int) Excludes the specified post ID(s) from the response',
46
			'exclude_tree'    => '(int) Excludes the specified post and all of its descendants from the response. Applies only to hierarchical post types.',
47
			'status'          => '(string) Comma-separated list of statuses for which to query, including any of: "publish", "private", "draft", "pending", "future", and "trash", or simply "any". Defaults to "publish"',
48
			'sticky'          => array(
49
				'include' => 'Sticky posts are not excluded from the list.',
50
				'exclude' => 'Sticky posts are excluded from the list.',
51
				'require' => 'Only include sticky posts',
52
			),
53
			'author'          => "(int) Author's user ID",
54
			'search'          => '(string) Search query',
55
			'meta_key'        => '(string) Metadata key that the post should contain',
56
			'meta_value'      => '(string) Metadata value that the post should contain. Will only be applied if a `meta_key` is also given',
57
		),
58
59
		'example_request'  => 'https://public-api.wordpress.com/rest/v1.1/sites/en.blog.wordpress.com/posts/?number=2',
60
	)
61
);
62
63
class WPCOM_JSON_API_List_Posts_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint {
64
	public $date_range      = array();
65
	public $modified_range  = array();
66
	public $page_handle     = array();
67
	public $performed_query = null;
68
69
	public $response_format = array(
70
		'found' => '(int) The total number of posts found that match the request (ignoring limits, offsets, and pagination).',
71
		'posts' => '(array:post) An array of post objects.',
72
		'meta'  => '(object) Meta data',
73
	);
74
75
	// /sites/%s/posts/ -> $blog_id
76
	function callback( $path = '', $blog_id = 0 ) {
77
		$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
78
		if ( is_wp_error( $blog_id ) ) {
79
			return $blog_id;
80
		}
81
82
		$args                        = $this->query_args();
83
		$is_eligible_for_page_handle = true;
84
		$site                        = $this->get_platform()->get_site( $blog_id );
85
86
		if ( $args['number'] < 1 ) {
87
			$args['number'] = 20;
88
		} elseif ( 100 < $args['number'] ) {
89
			return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'invalid_number'.

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...
90
		}
91
92 View Code Duplication
		if ( isset( $args['type'] ) &&
93
			   ! in_array( $args['type'], array( 'post', 'revision', 'page', 'any' ) ) &&
94
			   defined( 'IS_WPCOM' ) && IS_WPCOM ) {
95
			$this->load_theme_functions();
96
		}
97
98 View Code Duplication
		if ( isset( $args['type'] ) && ! $site->is_post_type_allowed( $args['type'] ) ) {
99
			return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unknown_post_type'.

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...
100
		}
101
102
		// Normalize post_type
103 View Code Duplication
		if ( isset( $args['type'] ) && 'any' == $args['type'] ) {
104
			if ( version_compare( $this->api->version, '1.1', '<' ) ) {
105
				$args['type'] = array( 'post', 'page' );
106
			} else { // 1.1+
107
				$args['type'] = $site->get_whitelisted_post_types();
108
			}
109
		}
110
111
		// determine statuses
112
		$status = ( ! empty( $args['status'] ) ) ? explode( ',', $args['status'] ) : array( 'publish' );
113 View Code Duplication
		if ( is_user_logged_in() ) {
114
			$statuses_whitelist = array(
115
				'publish',
116
				'pending',
117
				'draft',
118
				'future',
119
				'private',
120
				'trash',
121
				'any',
122
			);
123
			$status             = array_intersect( $status, $statuses_whitelist );
124
		} else {
125
			// logged-out users can see only published posts
126
			$statuses_whitelist = array( 'publish', 'any' );
127
			$status             = array_intersect( $status, $statuses_whitelist );
128
129
			if ( empty( $status ) ) {
130
				// requested only protected statuses? nothing for you here
131
				return array(
132
					'found' => 0,
133
					'posts' => array(),
134
				);
135
			}
136
			// clear it (AKA published only) because "any" includes protected
137
			$status = array();
138
		}
139
140
		// let's be explicit about defaulting to 'post'
141
		$args['type'] = isset( $args['type'] ) ? $args['type'] : 'post';
142
143
		// make sure the user can read or edit the requested post type(s)
144 View Code Duplication
		if ( is_array( $args['type'] ) ) {
145
			$allowed_types = array();
146
			foreach ( $args['type'] as $post_type ) {
147
				if ( $site->current_user_can_access_post_type( $post_type, $args['context'] ) ) {
148
					$allowed_types[] = $post_type;
149
				}
150
			}
151
152
			if ( empty( $allowed_types ) ) {
153
				return array(
154
					'found' => 0,
155
					'posts' => array(),
156
				);
157
			}
158
			$args['type'] = $allowed_types;
159
		} else {
160
			if ( ! $site->current_user_can_access_post_type( $args['type'], $args['context'] ) ) {
161
				return array(
162
					'found' => 0,
163
					'posts' => array(),
164
				);
165
			}
166
		}
167
168
		$query = array(
169
			'posts_per_page' => $args['number'],
170
			'order'          => $args['order'],
171
			'orderby'        => $args['order_by'],
172
			'post_type'      => $args['type'],
173
			'post_status'    => $status,
174
			'post_parent'    => isset( $args['parent_id'] ) ? $args['parent_id'] : null,
175
			'author'         => isset( $args['author'] ) && 0 < $args['author'] ? $args['author'] : null,
176
			's'              => isset( $args['search'] ) && '' !== $args['search'] ? $args['search'] : null,
177
			'fields'         => 'ids',
178
		);
179
180
		if ( ! is_user_logged_in() ) {
181
			$query['has_password'] = false;
182
		}
183
184 View Code Duplication
		if ( isset( $args['meta_key'] ) ) {
185
			$show = false;
186
			if ( WPCOM_JSON_API_Metadata::is_public( $args['meta_key'] ) ) {
187
				$show = true;
188
			}
189
			if ( current_user_can( 'edit_post_meta', $query['post_type'], $args['meta_key'] ) ) {
190
				$show = true;
191
			}
192
193
			if ( is_protected_meta( $args['meta_key'], 'post' ) && ! $show ) {
194
				return new WP_Error( 'invalid_meta_key', 'Invalid meta key', 404 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'invalid_meta_key'.

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...
195
			}
196
197
			$meta = array( 'key' => $args['meta_key'] );
198
			if ( isset( $args['meta_value'] ) ) {
199
				$meta['value'] = $args['meta_value'];
200
			}
201
202
			$query['meta_query'] = array( $meta );
203
		}
204
205 View Code Duplication
		if ( $args['sticky'] === 'include' ) {
206
			$query['ignore_sticky_posts'] = 1;
207
		} elseif ( $args['sticky'] === 'exclude' ) {
208
			$sticky = get_option( 'sticky_posts' );
209
			if ( is_array( $sticky ) ) {
210
				$query['post__not_in'] = $sticky;
211
			}
212
		} elseif ( $args['sticky'] === 'require' ) {
213
			$sticky = get_option( 'sticky_posts' );
214
			if ( is_array( $sticky ) && ! empty( $sticky ) ) {
215
				$query['post__in'] = $sticky;
216
			} else {
217
				// no sticky posts exist
218
				return array(
219
					'found' => 0,
220
					'posts' => array(),
221
				);
222
			}
223
		}
224
225 View Code Duplication
		if ( isset( $args['exclude'] ) ) {
226
			$excluded_ids          = (array) $args['exclude'];
227
			$query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $excluded_ids ) : $excluded_ids;
228
		}
229
230 View Code Duplication
		if ( isset( $args['exclude_tree'] ) && is_post_type_hierarchical( $args['type'] ) ) {
231
			// get_page_children is a misnomer; it supports all hierarchical post types
232
			$page_args        = array(
233
				'child_of'    => $args['exclude_tree'],
234
				'post_type'   => $args['type'],
235
				// since we're looking for things to exclude, be aggressive
236
				'post_status' => 'publish,draft,pending,private,future,trash',
237
			);
238
			$post_descendants = get_pages( $page_args );
239
240
			$exclude_tree = array( $args['exclude_tree'] );
241
			foreach ( $post_descendants as $child ) {
242
				$exclude_tree[] = $child->ID;
243
			}
244
245
			$query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $exclude_tree ) : $exclude_tree;
246
		}
247
248 View Code Duplication
		if ( isset( $args['category'] ) ) {
249
			$category = get_term_by( 'slug', $args['category'], 'category' );
250
			if ( $category === false ) {
251
				$query['category_name'] = $args['category'];
252
			} else {
253
				$query['cat'] = $category->term_id;
254
			}
255
		}
256
257
		if ( isset( $args['tag'] ) ) {
258
			$query['tag'] = $args['tag'];
259
		}
260
261 View Code Duplication
		if ( ! empty( $args['term'] ) ) {
262
			$query['tax_query'] = array();
263
			foreach ( $args['term'] as $taxonomy => $slug ) {
264
				$taxonomy_object = get_taxonomy( $taxonomy );
265
				if ( false === $taxonomy_object || ( ! $taxonomy_object->public &&
266
						! current_user_can( $taxonomy_object->cap->assign_terms ) ) ) {
267
					continue;
268
				}
269
270
				$query['tax_query'][] = array(
271
					'taxonomy' => $taxonomy,
272
					'field'    => 'slug',
273
					'terms'    => explode( ',', $slug ),
274
				);
275
			}
276
		}
277
278 View Code Duplication
		if ( isset( $args['page'] ) ) {
279
			if ( $args['page'] < 1 ) {
280
				$args['page'] = 1;
281
			}
282
283
			$query['paged'] = $args['page'];
284
			if ( $query['paged'] !== 1 ) {
285
				$is_eligible_for_page_handle = false;
286
			}
287
		} else {
288
			if ( $args['offset'] < 0 ) {
289
				$args['offset'] = 0;
290
			}
291
292
			$query['offset'] = $args['offset'];
293
			if ( $query['offset'] !== 0 ) {
294
				$is_eligible_for_page_handle = false;
295
			}
296
		}
297
298
		if ( isset( $args['before_gmt'] ) ) {
299
			$this->date_range['before'] = $args['before_gmt'];
300
		}
301
		if ( isset( $args['after_gmt'] ) ) {
302
			$this->date_range['after'] = $args['after_gmt'];
303
		}
304
305
		if ( isset( $args['modified_before_gmt'] ) ) {
306
			$this->modified_range['before'] = $args['modified_before_gmt'];
307
		}
308
		if ( isset( $args['modified_after_gmt'] ) ) {
309
			$this->modified_range['after'] = $args['modified_after_gmt'];
310
		}
311
312
		if ( $this->date_range ) {
313
			add_filter( 'posts_where', array( $this, 'handle_date_range' ) );
314
		}
315
316
		if ( $this->modified_range ) {
317
			add_filter( 'posts_where', array( $this, 'handle_modified_range' ) );
318
		}
319
320 View Code Duplication
		if ( isset( $args['page_handle'] ) ) {
321
			$page_handle = wp_parse_args( $args['page_handle'] );
322
			if ( isset( $page_handle['value'] ) && isset( $page_handle['id'] ) ) {
323
				// we have a valid looking page handle
324
				$this->page_handle = $page_handle;
325
				add_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) );
326
			}
327
		}
328
329
		/**
330
		 * 'column' necessary for the me/posts endpoint (which extends sites/$site/posts).
331
		 * Would need to be added to the sites/$site/posts definition if we ever want to
332
		 * use it there.
333
		 */
334
		$column_whitelist = array( 'post_modified_gmt' );
335 View Code Duplication
		if ( isset( $args['column'] ) && in_array( $args['column'], $column_whitelist ) ) {
336
			$query['column'] = $args['column'];
337
		}
338
339
		$this->performed_query = $query;
340
		add_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) );
341
342
		$wp_query = new WP_Query( $query );
343
344
		remove_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) );
345
346
		if ( $this->date_range ) {
347
			remove_filter( 'posts_where', array( $this, 'handle_date_range' ) );
348
			$this->date_range = array();
349
		}
350
351
		if ( $this->modified_range ) {
352
			remove_filter( 'posts_where', array( $this, 'handle_modified_range' ) );
353
			$this->modified_range = array();
354
		}
355
356
		if ( $this->page_handle ) {
357
			remove_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) );
358
359
		}
360
361
		$return         = array();
362
		$excluded_count = 0;
363 View Code Duplication
		foreach ( array_keys( $this->response_format ) as $key ) {
364
			switch ( $key ) {
365
				case 'found':
366
					$return[ $key ] = (int) $wp_query->found_posts;
367
					break;
368
				case 'posts':
369
					$posts = array();
370
					foreach ( $wp_query->posts as $post_ID ) {
371
						$the_post = $this->get_post_by( 'ID', $post_ID, $args['context'] );
372
						if ( $the_post && ! is_wp_error( $the_post ) ) {
373
							$posts[] = $the_post;
374
						} else {
375
							$excluded_count++;
376
						}
377
					}
378
379
					if ( $posts ) {
380
						/** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */
381
						do_action( 'wpcom_json_api_objects', 'posts', count( $posts ) );
382
					}
383
384
					$return[ $key ] = $posts;
385
					break;
386
387
				case 'meta':
388
					if ( ! is_array( $args['type'] ) ) {
389
						$return[ $key ] = (object) array(
390
							'links' => (object) array(
391
								'counts' => (string) $this->links->get_site_link( $blog_id, 'post-counts/' . $args['type'] ),
392
							),
393
						);
394
					}
395
396
					if ( $is_eligible_for_page_handle && $return['posts'] ) {
397
						$last_post = end( $return['posts'] );
398
						reset( $return['posts'] );
399
						if ( ( $return['found'] > count( $return['posts'] ) ) && $last_post ) {
400
							if ( ! isset( $return[ $key ] ) ) {
401
								$return[ $key ] = (object) array();
402
							}
403
							if ( isset( $last_post['ID'] ) ) {
404
								$return[ $key ]->next_page = $this->build_page_handle( $last_post, $query );
405
							}
406
						}
407
					}
408
409
					if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
410
						if ( ! isset( $return[ $key ] ) ) {
411
							$return[ $key ] = new stdClass();
412
						}
413
						$return[ $key ]->wpcom = true;
414
					}
415
416
					break;
417
			}
418
		}
419
420
		$return['found'] -= $excluded_count;
421
422
		return $return;
423
	}
424
425 View Code Duplication
	function build_page_handle( $post, $query ) {
426
		$column = $query['orderby'];
427
		if ( ! $column ) {
428
			$column = 'date';
429
		}
430
		return build_query(
431
			array(
432
				'value' => urlencode( $post[ $column ] ),
433
				'id'    => $post['ID'],
434
			)
435
		);
436
	}
437
438 View Code Duplication
	function _build_date_range_query( $column, $range, $where ) {
439
		global $wpdb;
440
441
		switch ( count( $range ) ) {
442
			case 2:
443
				$where .= $wpdb->prepare(
444
					" AND `$wpdb->posts`.$column >= CAST( %s AS DATETIME ) AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ",
445
					$range['after'],
446
					$range['before']
447
				);
448
				break;
449
			case 1:
450
				if ( isset( $range['before'] ) ) {
451
					$where .= $wpdb->prepare(
452
						" AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ",
453
						$range['before']
454
					);
455
				} else {
456
					$where .= $wpdb->prepare(
457
						" AND `$wpdb->posts`.$column > CAST( %s AS DATETIME ) ",
458
						$range['after']
459
					);
460
				}
461
				break;
462
		}
463
464
		return $where;
465
	}
466
467
	function handle_date_range( $where ) {
468
		return $this->_build_date_range_query( 'post_date_gmt', $this->date_range, $where );
469
	}
470
471
	function handle_modified_range( $where ) {
472
		return $this->_build_date_range_query( 'post_modified_gmt', $this->modified_range, $where );
473
	}
474
475 View Code Duplication
	function handle_where_for_page_handle( $where ) {
476
		global $wpdb;
477
478
		$column = $this->performed_query['orderby'];
479
		if ( ! $column ) {
480
			$column = 'date';
481
		}
482
		$order = $this->performed_query['order'];
483
		if ( ! $order ) {
484
			$order = 'DESC';
485
		}
486
487
		if ( ! in_array( $column, array( 'ID', 'title', 'date', 'modified', 'comment_count' ) ) ) {
488
			return $where;
489
		}
490
491
		if ( ! in_array( $order, array( 'DESC', 'ASC' ) ) ) {
492
			return $where;
493
		}
494
495
		$db_column = '';
496
		$db_value  = '';
497
		switch ( $column ) {
498
			case 'ID':
499
				$db_column = 'ID';
500
				$db_value  = '%d';
501
				break;
502
			case 'title':
503
				$db_column = 'post_title';
504
				$db_value  = '%s';
505
				break;
506
			case 'date':
507
				$db_column = 'post_date';
508
				$db_value  = 'CAST( %s as DATETIME )';
509
				break;
510
			case 'modified':
511
				$db_column = 'post_modified';
512
				$db_value  = 'CAST( %s as DATETIME )';
513
				break;
514
			case 'comment_count':
515
				$db_column = 'comment_count';
516
				$db_value  = '%d';
517
				break;
518
		}
519
520
		if ( 'DESC' === $order ) {
521
			$db_order = '<';
522
		} else {
523
			$db_order = '>';
524
		}
525
526
		// Add a clause that limits the results to items beyond the passed item, or equivalent to the passed item
527
		// but with an ID beyond the passed item. When we're ordering by the ID already, we only ask for items
528
		// beyond the passed item.
529
		$where .= $wpdb->prepare( " AND ( ( `$wpdb->posts`.`$db_column` $db_order $db_value ) ", $this->page_handle['value'] );
530
		if ( $db_column !== 'ID' ) {
531
			$where .= $wpdb->prepare( "OR ( `$wpdb->posts`.`$db_column` = $db_value AND `$wpdb->posts`.ID $db_order %d )", $this->page_handle['value'], $this->page_handle['id'] );
532
		}
533
		$where .= ' )';
534
535
		return $where;
536
	}
537
538 View Code Duplication
	function handle_orderby_for_page_handle( $orderby ) {
539
		global $wpdb;
540
		if ( $this->performed_query['orderby'] === 'ID' ) {
541
			// bail if we're already ordering by ID
542
			return $orderby;
543
		}
544
545
		if ( $orderby ) {
546
			$orderby .= ' ,';
547
		}
548
		$order = $this->performed_query['order'];
549
		if ( ! $order ) {
550
			$order = 'DESC';
551
		}
552
		$orderby .= " `$wpdb->posts`.ID $order";
553
		return $orderby;
554
	}
555
}
556