/json-endpoints/class.wpcom-json-api-list-posts-v1-1-endpoint.php

  1. <?php 
  2.  
  3. class WPCOM_JSON_API_List_Posts_v1_1_Endpoint extends WPCOM_JSON_API_Post_v1_1_Endpoint { 
  4. public $date_range = array(); 
  5. public $modified_range = array(); 
  6. public $page_handle = array(); 
  7. public $performed_query = null; 
  8.  
  9. public $response_format = array( 
  10. 'found' => '(int) The total number of posts found that match the request (ignoring limits, offsets, and pagination).',  
  11. 'posts' => '(array:post) An array of post objects.',  
  12. 'meta' => '(object) Meta data',  
  13. ); 
  14.  
  15. // /sites/%s/posts/ -> $blog_id 
  16. function callback( $path = '', $blog_id = 0 ) { 
  17. $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) ); 
  18. if ( is_wp_error( $blog_id ) ) { 
  19. return $blog_id; 
  20.  
  21. $args = $this->query_args(); 
  22. $is_eligible_for_page_handle = true; 
  23.  
  24. if ( $args['number'] < 1 ) { 
  25. $args['number'] = 20; 
  26. } elseif ( 100 < $args['number'] ) { 
  27. return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 ); 
  28.  
  29. if ( isset( $args['type'] ) && ! $this->is_post_type_allowed( $args['type'] ) ) { 
  30. return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 ); 
  31.  
  32. // Normalize post_type 
  33. if ( isset( $args['type'] ) && 'any' == $args['type'] ) { 
  34. if ( version_compare( $this->api->version, '1.1', '<' ) ) { 
  35. $args['type'] = array( 'post', 'page' ); 
  36. } else { // 1.1+ 
  37. $args['type'] = $this->_get_whitelisted_post_types(); 
  38.  
  39. // determine statuses 
  40. $status = ( ! empty( $args['status'] ) ) ? explode( ', ', $args['status'] ) : array( 'publish' ); 
  41. if ( is_user_logged_in() ) { 
  42. $statuses_whitelist = array( 
  43. 'publish',  
  44. 'pending',  
  45. 'draft',  
  46. 'future',  
  47. 'private',  
  48. 'trash',  
  49. 'any',  
  50. ); 
  51. $status = array_intersect( $status, $statuses_whitelist ); 
  52. } else { 
  53. // logged-out users can see only published posts 
  54. $statuses_whitelist = array( 'publish', 'any' ); 
  55. $status = array_intersect( $status, $statuses_whitelist ); 
  56.  
  57. if ( empty( $status ) ) { 
  58. // requested only protected statuses? nothing for you here 
  59. return array( 'found' => 0, 'posts' => array() ); 
  60. // clear it (AKA published only) because "any" includes protected 
  61. $status = array(); 
  62.  
  63. if ( isset( $args['type'] ) && 
  64. ! in_array( $args['type'], array( 'post', 'page', 'revision', 'any' ) ) && 
  65. defined( 'IS_WPCOM' ) && IS_WPCOM ) { 
  66. $this->load_theme_functions(); 
  67.  
  68. // let's be explicit about defaulting to 'post' 
  69. $args['type'] = isset( $args['type'] ) ? $args['type'] : 'post'; 
  70.  
  71. // make sure the user can read or edit the requested post type(s) 
  72. if ( is_array( $args['type'] ) ) { 
  73. $allowed_types = array(); 
  74. foreach ( $args['type'] as $post_type ) { 
  75. if ( $this->current_user_can_access_post_type( $post_type, $args['context'] ) ) { 
  76. $allowed_types[] = $post_type; 
  77.  
  78. if ( empty( $allowed_types ) ) { 
  79. return array( 'found' => 0, 'posts' => array() ); 
  80. $args['type'] = $allowed_types; 
  81. else { 
  82. if ( ! $this->current_user_can_access_post_type( $args['type'], $args['context'] ) ) { 
  83. return array( 'found' => 0, 'posts' => array() ); 
  84.  
  85.  
  86. $query = array( 
  87. 'posts_per_page' => $args['number'],  
  88. 'order' => $args['order'],  
  89. 'orderby' => $args['order_by'],  
  90. 'post_type' => $args['type'],  
  91. 'post_status' => $status,  
  92. 'post_parent' => isset( $args['parent_id'] ) ? $args['parent_id'] : null,  
  93. 'author' => isset( $args['author'] ) && 0 < $args['author'] ? $args['author'] : null,  
  94. 's' => isset( $args['search'] ) ? $args['search'] : null,  
  95. 'fields' => 'ids',  
  96. ); 
  97.  
  98. if ( ! is_user_logged_in () ) { 
  99. $query['has_password'] = false; 
  100.  
  101. if ( isset( $args['meta_key'] ) ) { 
  102. $show = false; 
  103. if ( $this->is_metadata_public( $args['meta_key'] ) ) 
  104. $show = true; 
  105. if ( current_user_can( 'edit_post_meta', $query['post_type'], $args['meta_key'] ) ) 
  106. $show = true; 
  107.  
  108. if ( is_protected_meta( $args['meta_key'], 'post' ) && ! $show ) 
  109. return new WP_Error( 'invalid_meta_key', 'Invalid meta key', 404 ); 
  110.  
  111. $meta = array( 'key' => $args['meta_key'] ); 
  112. if ( isset( $args['meta_value'] ) ) 
  113. $meta['value'] = $args['meta_value']; 
  114.  
  115. $query['meta_query'] = array( $meta ); 
  116.  
  117. if ( $args['sticky'] === 'include' ) { 
  118. $query['ignore_sticky_posts'] = 1; 
  119. } else if ( $args['sticky'] === 'exclude' ) { 
  120. $sticky = get_option( 'sticky_posts' ); 
  121. if ( is_array( $sticky ) ) { 
  122. $query['post__not_in'] = $sticky; 
  123. } else if ( $args['sticky'] === 'require' ) { 
  124. $sticky = get_option( 'sticky_posts' ); 
  125. if ( is_array( $sticky ) && ! empty( $sticky ) ) { 
  126. $query['post__in'] = $sticky; 
  127. } else { 
  128. // no sticky posts exist 
  129. return array( 'found' => 0, 'posts' => array() ); 
  130.  
  131. if ( isset( $args['exclude'] ) ) { 
  132. $excluded_ids = (array) $args['exclude']; 
  133. $query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $excluded_ids ) : $excluded_ids; 
  134.  
  135. if ( isset( $args['exclude_tree'] ) && is_post_type_hierarchical( $args['type'] ) ) { 
  136. // get_page_children is a misnomer; it supports all hierarchical post types 
  137. $page_args = array( 
  138. 'child_of' => $args['exclude_tree'],  
  139. 'post_type' => $args['type'],  
  140. // since we're looking for things to exclude, be aggressive 
  141. 'post_status' => 'publish, draft, pending, private, future, trash',  
  142. ); 
  143. $post_descendants = get_pages( $page_args ); 
  144.  
  145. $exclude_tree = array( $args['exclude_tree'] ); 
  146. foreach ( $post_descendants as $child ) { 
  147. $exclude_tree[] = $child->ID; 
  148.  
  149. $query['post__not_in'] = isset( $query['post__not_in'] ) ? array_merge( $query['post__not_in'], $exclude_tree ) : $exclude_tree; 
  150.  
  151. if ( isset( $args['category'] ) ) { 
  152. $category = get_term_by( 'slug', $args['category'], 'category' ); 
  153. if ( $category === false) { 
  154. $query['category_name'] = $args['category']; 
  155. } else { 
  156. $query['cat'] = $category->term_id; 
  157.  
  158. if ( isset( $args['tag'] ) ) { 
  159. $query['tag'] = $args['tag']; 
  160.  
  161. if ( isset( $args['page'] ) ) { 
  162. if ( $args['page'] < 1 ) { 
  163. $args['page'] = 1; 
  164.  
  165. $query['paged'] = $args['page']; 
  166. if ( $query['paged'] !== 1 ) { 
  167. $is_eligible_for_page_handle = false; 
  168. } else { 
  169. if ( $args['offset'] < 0 ) { 
  170. $args['offset'] = 0; 
  171.  
  172. $query['offset'] = $args['offset']; 
  173. if ( $query['offset'] !== 0 ) { 
  174. $is_eligible_for_page_handle = false; 
  175.  
  176. if ( isset( $args['before'] ) ) { 
  177. $this->date_range['before'] = $args['before']; 
  178. if ( isset( $args['after'] ) ) { 
  179. $this->date_range['after'] = $args['after']; 
  180.  
  181. if ( isset( $args['modified_before_gmt'] ) ) { 
  182. $this->modified_range['before'] = $args['modified_before_gmt']; 
  183. if ( isset( $args['modified_after_gmt'] ) ) { 
  184. $this->modified_range['after'] = $args['modified_after_gmt']; 
  185.  
  186. if ( $this->date_range ) { 
  187. add_filter( 'posts_where', array( $this, 'handle_date_range' ) ); 
  188.  
  189. if ( $this->modified_range ) { 
  190. add_filter( 'posts_where', array( $this, 'handle_modified_range' ) ); 
  191.  
  192. if ( isset( $args['page_handle'] ) ) { 
  193. $page_handle = wp_parse_args( $args['page_handle'] ); 
  194. if ( isset( $page_handle['value'] ) && isset( $page_handle['id'] ) ) { 
  195. // we have a valid looking page handle 
  196. $this->page_handle = $page_handle; 
  197. add_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) ); 
  198.  
  199. /** 
  200. * 'column' necessary for the me/posts endpoint (which extends sites/$site/posts). 
  201. * Would need to be added to the sites/$site/posts definition if we ever want to 
  202. * use it there. 
  203. */ 
  204. $column_whitelist = array( 'post_modified_gmt' ); 
  205. if ( isset( $args['column'] ) && in_array( $args['column'], $column_whitelist ) ) { 
  206. $query['column'] = $args['column']; 
  207.  
  208. $this->performed_query = $query; 
  209. add_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) ); 
  210.  
  211. $wp_query = new WP_Query( $query ); 
  212.  
  213. remove_filter( 'posts_orderby', array( $this, 'handle_orderby_for_page_handle' ) ); 
  214.  
  215. if ( $this->date_range ) { 
  216. remove_filter( 'posts_where', array( $this, 'handle_date_range' ) ); 
  217. $this->date_range = array(); 
  218.  
  219. if ( $this->modified_range ) { 
  220. remove_filter( 'posts_where', array( $this, 'handle_modified_range' ) ); 
  221. $this->modified_range = array(); 
  222.  
  223. if ( $this->page_handle ) { 
  224. remove_filter( 'posts_where', array( $this, 'handle_where_for_page_handle' ) ); 
  225.  
  226.  
  227. $return = array(); 
  228. $excluded_count = 0; 
  229. foreach ( array_keys( $this->response_format ) as $key ) { 
  230. switch ( $key ) { 
  231. case 'found' : 
  232. $return[$key] = (int) $wp_query->found_posts; 
  233. break; 
  234. case 'posts' : 
  235. $posts = array(); 
  236. foreach ( $wp_query->posts as $post_ID ) { 
  237. $the_post = $this->get_post_by( 'ID', $post_ID, $args['context'] ); 
  238. if ( $the_post && ! is_wp_error( $the_post ) ) { 
  239. $posts[] = $the_post; 
  240. } else { 
  241. $excluded_count++; 
  242.  
  243. if ( $posts ) { 
  244. /** This action is documented in json-endpoints/class.wpcom-json-api-site-settings-endpoint.php */ 
  245. do_action( 'wpcom_json_api_objects', 'posts', count( $posts ) ); 
  246.  
  247. $return[$key] = $posts; 
  248. break; 
  249.  
  250. case 'meta' : 
  251. if ( ! is_array( $args['type'] ) ) { 
  252. $return[$key] = (object) array( 
  253. 'links' => (object) array( 
  254. 'counts' => (string) $this->get_site_link( $blog_id, 'post-counts/' . $args['type'] ),  
  255. ); 
  256.  
  257. if ( $is_eligible_for_page_handle && $return['posts'] ) { 
  258. $last_post = end( $return['posts'] ); 
  259. reset( $return['posts'] ); 
  260. if ( ( $return['found'] > count( $return['posts'] ) ) && $last_post ) { 
  261. if ( ! isset( $return[$key] ) ) { 
  262. $return[$key] = (object) array(); 
  263. $return[$key]->next_page = $this->build_page_handle( $last_post, $query ); 
  264. break; 
  265.  
  266. $return['found'] -= $excluded_count; 
  267.  
  268. return $return; 
  269.  
  270. function build_page_handle( $post, $query ) { 
  271. $column = $query['orderby']; 
  272. if ( ! $column ) { 
  273. $column = 'date'; 
  274. return build_query( array( 'value' => urlencode($post[$column]), 'id' => $post['ID'] ) ); 
  275.  
  276. function _build_date_range_query( $column, $range, $where ) { 
  277. global $wpdb; 
  278.  
  279. switch ( count( $range ) ) { 
  280. case 2 : 
  281. $where .= $wpdb->prepare( 
  282. " AND `$wpdb->posts`.$column >= CAST( %s AS DATETIME ) AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ",  
  283. $range['after'],  
  284. $range['before'] 
  285. ); 
  286. break; 
  287. case 1 : 
  288. if ( isset( $range['before'] ) ) { 
  289. $where .= $wpdb->prepare( 
  290. " AND `$wpdb->posts`.$column < CAST( %s AS DATETIME ) ",  
  291. $range['before'] 
  292. ); 
  293. } else { 
  294. $where .= $wpdb->prepare( 
  295. " AND `$wpdb->posts`.$column > CAST( %s AS DATETIME ) ",  
  296. $range['after'] 
  297. ); 
  298. break; 
  299.  
  300. return $where; 
  301.  
  302. function handle_date_range( $where ) { 
  303. return $this->_build_date_range_query( 'post_date', $this->date_range, $where ); 
  304.  
  305. function handle_modified_range( $where ) { 
  306. return $this->_build_date_range_query( 'post_modified_gmt', $this->modified_range, $where ); 
  307.  
  308. function handle_where_for_page_handle( $where ) { 
  309. global $wpdb; 
  310.  
  311. $column = $this->performed_query['orderby']; 
  312. if ( ! $column ) { 
  313. $column = 'date'; 
  314. $order = $this->performed_query['order']; 
  315. if ( ! $order ) { 
  316. $order = 'DESC'; 
  317.  
  318. if ( ! in_array( $column, array( 'ID', 'title', 'date', 'modified', 'comment_count' ) ) ) { 
  319. return $where; 
  320.  
  321. if ( ! in_array( $order, array( 'DESC', 'ASC' ) ) ) { 
  322. return $where; 
  323.  
  324. $db_column = ''; 
  325. $db_value = ''; 
  326. switch( $column ) { 
  327. case 'ID': 
  328. $db_column = 'ID'; 
  329. $db_value = '%d'; 
  330. break; 
  331. case 'title': 
  332. $db_column = 'post_title'; 
  333. $db_value = '%s'; 
  334. break; 
  335. case 'date': 
  336. $db_column = 'post_date'; 
  337. $db_value = 'CAST( %s as DATETIME )'; 
  338. break; 
  339. case 'modified': 
  340. $db_column = 'post_modified'; 
  341. $db_value = 'CAST( %s as DATETIME )'; 
  342. break; 
  343. case 'comment_count': 
  344. $db_column = 'comment_count'; 
  345. $db_value = '%d'; 
  346. break; 
  347.  
  348. if ( 'DESC'=== $order ) { 
  349. $db_order = '<'; 
  350. } else { 
  351. $db_order = '>'; 
  352.  
  353. // Add a clause that limits the results to items beyond the passed item, or equivalent to the passed item 
  354. // but with an ID beyond the passed item. When we're ordering by the ID already, we only ask for items 
  355. // beyond the passed item. 
  356. $where .= $wpdb->prepare( " AND ( ( `$wpdb->posts`.`$db_column` $db_order $db_value ) ", $this->page_handle['value'] ); 
  357. if ( $db_column !== 'ID' ) { 
  358. $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'] ); 
  359. $where .= ' )'; 
  360.  
  361. return $where; 
  362.  
  363. function handle_orderby_for_page_handle( $orderby ) { 
  364. global $wpdb; 
  365. if ( $this->performed_query['orderby'] === 'ID' ) { 
  366. // bail if we're already ordering by ID 
  367. return $orderby; 
  368.  
  369. if ( $orderby ) { 
  370. $orderby .= ' , '; 
  371. $order = $this->performed_query['order']; 
  372. if ( ! $order ) { 
  373. $order = 'DESC'; 
  374. $orderby .= " `$wpdb->posts`.ID $order"; 
  375. return $orderby; 
.