PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 2.7.5
Jetpack – WP Security, Backup, Speed, & Growth v2.7.5
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / class.json-api-endpoints.php

class.json-api-endpoints.php in Jetpack – WP Security, Backup, Speed, & Growth 2.7.5, at class.json-api-endpoints.php

4,198 lines 134.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // Endpoint
4 abstract class WPCOM_JSON_API_Endpoint {
5 // The API Object
6 var $api;
7
8 var $pass_wpcom_user_details = false;
9 var $can_use_user_details_instead_of_blog_membership = false;
10
11 // One liner.
12 var $description;
13
14 // Object Grouping For Documentation (Users, Posts, Comments)
15 var $group;
16
17 // Stats extra value to bump
18 var $stat;
19
20 // HTTP Method
21 var $method = 'GET';
22
23 // Path at which to serve this endpoint: sprintf() format.
24 var $path = '';
25
26 // Identifiers to fill sprintf() formatted $path
27 var $path_labels = array();
28
29 // Accepted query parameters
30 var $query = array(
31 // Parameter name
32 'context' => array(
33 // Default value => description
34 'display' => 'Formats the output as HTML for display. Shortcodes are parsed, paragraph tags are added, etc..',
35 // Other possible values => description
36 'edit' => 'Formats the output for editing. Shortcodes are left unparsed, significant whitespace is kept, etc..',
37 ),
38 'http_envelope' => array(
39 'false' => '',
40 'true' => 'Some environments (like in-browser Javascript or Flash) block or divert responses with a non-200 HTTP status code. Setting this parameter will force the HTTP status code to always be 200. The JSON response is wrapped in an "envelope" containing the "real" HTTP status code and headers.',
41 ),
42 'pretty' => array(
43 'false' => '',
44 'true' => 'Output pretty JSON',
45 ),
46 'meta' => "(string) Optional. Loads data from the endpoints found in the 'meta' part of the response. Comma separated list. Example: meta=site,likes",
47 // Parameter name => description (default value is empty)
48 'callback' => '(string) An optional JSONP callback function.',
49 );
50
51 // Response format
52 var $response_format = array();
53
54 // Request format
55 var $request_format = array();
56
57 // Is this endpoint still in testing phase? If so, not available to the public.
58 var $in_testing = false;
59
60 /**
61 * @var string Version of the API
62 */
63 var $version = '';
64
65 /**
66 * @var string Example request to make
67 */
68 var $example_request = '';
69
70 /**
71 * @var string Example request data (for POST methods)
72 */
73 var $example_request_data = '';
74
75 /**
76 * @var string Example response from $example_request
77 */
78 var $example_response = '';
79
80 function __construct( $args ) {
81 $defaults = array(
82 'in_testing' => false,
83 'description' => '',
84 'group' => '',
85 'method' => 'GET',
86 'path' => '/',
87 'force' => '',
88 'jp_disabled' => false,
89 'path_labels' => array(),
90 'request_format' => array(),
91 'response_format' => array(),
92 'query_parameters' => array(),
93 'version' => 'v1',
94 'example_request' => '',
95 'example_request_data' => '',
96 'example_response' => '',
97
98 'pass_wpcom_user_details' => false,
99 'can_use_user_details_instead_of_blog_membership' => false,
100 );
101
102 $args = wp_parse_args( $args, $defaults );
103
104 $this->in_testing = $args['in_testing'];
105
106 $this->description = $args['description'];
107 $this->group = $args['group'];
108 $this->stat = $args['stat'];
109 $this->force = $args['force'];
110 $this->jp_disabled = $args['jp_disabled'];
111
112 $this->method = $args['method'];
113 $this->path = $args['path'];
114 $this->path_labels = $args['path_labels'];
115
116 $this->pass_wpcom_user_details = $args['pass_wpcom_user_details'];
117 $this->can_use_user_details_instead_of_blog_membership = $args['can_use_user_details_instead_of_blog_membership'];
118
119 $this->version = $args['version'];
120
121 if ( $this->request_format ) {
122 $this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) );
123 } else {
124 $this->request_format = $args['request_format'];
125 }
126
127 if ( $this->response_format ) {
128 $this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) );
129 } else {
130 $this->response_format = $args['response_format'];
131 }
132
133 if ( false === $args['query_parameters'] ) {
134 $this->query = array();
135 } elseif ( is_array( $args['query_parameters'] ) ) {
136 $this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) );
137 }
138
139 $this->api = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API
140
141 /** Example Request/Response ******************************************/
142
143 // Examples for endpoint documentation request
144 $this->example_request = $args['example_request'];
145 $this->example_request_data = $args['example_request_data'];
146 $this->example_response = $args['example_response'];
147
148 $this->api->add( $this );
149 }
150
151 // Get all query args. Prefill with defaults
152 function query_args( $return_default_values = true, $cast_and_filter = true ) {
153 $args = array_intersect_key( $this->api->query, $this->query );
154
155 if ( !$cast_and_filter ) {
156 return $args;
157 }
158
159 return $this->cast_and_filter( $args, $this->query, $return_default_values );
160 }
161
162 // Get POST body data
163 function input( $return_default_values = true, $cast_and_filter = true ) {
164 $input = trim( $this->api->post_body );
165 switch ( $this->api->content_type ) {
166 case 'application/json; charset=utf-8' :
167 case 'application/json' :
168 case 'application/x-javascript' :
169 case 'text/javascript' :
170 case 'text/x-javascript' :
171 case 'text/x-json' :
172 case 'text/json' :
173 $return = json_decode( $input, true );
174
175 if ( function_exists( 'json_last_error' ) ) {
176 if ( JSON_ERROR_NONE !== json_last_error() ) {
177 return null;
178 }
179 } else {
180 if ( is_null( $return ) && json_encode( null ) !== $input ) {
181 return null;
182 }
183 }
184
185 break;
186 case 'multipart/form-data' :
187 $return = array_merge( stripslashes_deep( $_POST ), $_FILES );
188 break;
189 case 'application/x-www-form-urlencoded' :
190 case 'application/x-www-form-urlencoded; charset=UTF-8' :
191 //attempt JSON first, since probably a curl command
192 $return = json_decode( $input, true );
193
194 if ( is_null( $return ) ) {
195 wp_parse_str( $input, $return );
196 }
197
198 break;
199 default :
200 wp_parse_str( $input, $return );
201 break;
202 }
203
204 if ( !$cast_and_filter ) {
205 return $return;
206 }
207
208 return $this->cast_and_filter( $return, $this->request_format, $return_default_values );
209 }
210
211 function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) {
212 $return_as_object = false;
213 if ( is_object( $data ) ) {
214 // @todo this should probably be a deep copy if $data can ever have nested objects
215 $data = (array) $data;
216 $return_as_object = true;
217 } elseif ( !is_array( $data ) ) {
218 return $data;
219 }
220
221 $boolean_arg = array( 'false', 'true' );
222 $naeloob_arg = array( 'true', 'false' );
223
224 $return = array();
225
226 foreach ( $documentation as $key => $description ) {
227 if ( is_array( $description ) ) {
228 // String or boolean array keys only
229 $whitelist = array_keys( $description );
230 if ( isset( $data[$key] ) && isset( $description[$data[$key]] ) ) {
231 $return[$key] = (string) $data[$key];
232 } elseif ( $return_default_values ) {
233 $return[$key] = (string) current( $whitelist );
234 } else {
235 continue;
236 }
237
238 // Truthiness
239 if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) {
240 $return[$key] = (bool) WPCOM_JSON_API::is_truthy( $return[$key] );
241 }
242
243 continue;
244 }
245
246 $types = $this->parse_types( $description );
247 $type = array_shift( $types );
248
249 // Explicit default - string and int only for now. Always set these reguardless of $return_default_values
250 if ( isset( $type['default'] ) ) {
251 if ( !isset( $data[$key] ) ) {
252 $data[$key] = $type['default'];
253 }
254 }
255
256 if ( !isset( $data[$key] ) ) {
257 continue;
258 }
259
260 $this->cast_and_filter_item( $return, $type, $key, $data[$key], $types, $for_output );
261 }
262
263 if ( $return_as_object ) {
264 return (object) $return;
265 }
266
267 return $return;
268 }
269
270 /**
271 * Casts $value according to $type.
272 * Handles fallbacks for certain values of $type when $value is not that $type
273 * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way)
274 *
275 * Handles "child types" - array:URL, object:category
276 * array:URL means an array of URLs
277 * object:category means a hash of categories
278 *
279 * Handles object typing - object>post means an object of type post
280 */
281 function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) {
282 if ( is_string( $type ) ) {
283 $type = compact( 'type' );
284 }
285
286 switch ( $type['type'] ) {
287 case 'false' :
288 $return[$key] = false;
289 break;
290 case 'url' :
291 $return[$key] = (string) esc_url_raw( $value );
292 break;
293 case 'string' :
294 // Fallback string -> array
295 if ( is_array( $value ) ) {
296 if ( !empty( $types[0] ) ) {
297 $next_type = array_shift( $types );
298 return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
299 }
300 }
301
302 // Fallback string -> false
303 if ( !is_string( $value ) ) {
304 if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
305 $next_type = array_shift( $types );
306 return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
307 }
308 }
309 $return[$key] = (string) $value;
310 break;
311 case 'html' :
312 $return[$key] = (string) $value;
313 break;
314 case 'media' :
315 if ( is_array( $value ) ) {
316 if ( isset( $value['name'] ) ) {
317 // It's a $_FILES array
318 // Reformat into array of $_FILES items
319
320 $files = array();
321 foreach ( $value['name'] as $k => $v ) {
322 $files[$k] = array();
323 foreach ( array_keys( $value ) as $file_key ) {
324 $files[$k][$file_key] = $value[$file_key][$k];
325 }
326 }
327
328 foreach ( $files as $k => $file ) {
329 if ( ! isset( $file['tmp_name'] ) || ! is_string( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) {
330 unset( $files[$k] );
331 }
332 }
333 if ( $files ) {
334 $return[$key] = $files;
335 }
336 } elseif ( isset( $value['tmp_name'] ) && is_string( $value['tmp_name'] ) && is_uploaded_file( $value['tmp_name'] ) ) {
337 $return[ $key ] = $value;
338 }
339 }
340 break;
341 case 'array' :
342 // Fallback array -> string
343 if ( is_string( $value ) ) {
344 if ( !empty( $types[0] ) ) {
345 $next_type = array_shift( $types );
346 return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
347 }
348 }
349
350 if ( isset( $type['children'] ) ) {
351 $children = array();
352 foreach ( (array) $value as $k => $child ) {
353 $this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
354 }
355 $return[$key] = (array) $children;
356 break;
357 }
358
359 $return[$key] = (array) $value;
360 break;
361 case 'iso 8601 datetime' :
362 case 'datetime' :
363 // (string)s
364 $dates = $this->parse_date( (string) $value );
365 if ( $for_output ) {
366 $return[$key] = $this->format_date( $dates[1], $dates[0] );
367 } else {
368 list( $return[$key], $return["{$key}_gmt"] ) = $dates;
369 }
370 break;
371 case 'float' :
372 $return[$key] = (float) $value;
373 break;
374 case 'int' :
375 case 'integer' :
376 $return[$key] = (int) $value;
377 break;
378 case 'bool' :
379 case 'boolean' :
380 $return[$key] = (bool) WPCOM_JSON_API::is_truthy( $value );
381 break;
382 case 'object' :
383 // Fallback object -> false
384 if ( is_scalar( $value ) || is_null( $value ) ) {
385 if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
386 return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output );
387 }
388 }
389
390 if ( isset( $type['children'] ) ) {
391 $children = array();
392 foreach ( (array) $value as $k => $child ) {
393 $this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
394 }
395 $return[$key] = (object) $children;
396 break;
397 }
398
399 if ( isset( $type['subtype'] ) ) {
400 return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output );
401 }
402
403 $return[$key] = (object) $value;
404 break;
405 case 'post' :
406 $return[$key] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output );
407 break;
408 case 'comment' :
409 $return[$key] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output );
410 break;
411 case 'tag' :
412 case 'category' :
413 $docs = array(
414 'name' => '(string)',
415 'slug' => '(string)',
416 'description' => '(HTML)',
417 'post_count' => '(int)',
418 'meta' => '(object)',
419 );
420 if ( 'category' === $type ) {
421 $docs['parent'] = '(int)';
422 }
423 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
424 break;
425 case 'post_reference' :
426 case 'comment_reference' :
427 $docs = array(
428 'ID' => '(int)',
429 'type' => '(string)',
430 'link' => '(URL)',
431 );
432 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
433 break;
434 case 'geo' :
435 $docs = array(
436 'latitude' => '(float)',
437 'longitude' => '(float)',
438 'address' => '(string)',
439 );
440 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
441 break;
442 case 'author' :
443 $docs = array(
444 'ID' => '(int)',
445 'email' => '(string|false)',
446 'name' => '(string)',
447 'URL' => '(URL)',
448 'avatar_URL' => '(URL)',
449 'profile_URL' => '(URL)',
450 );
451 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
452 break;
453 case 'attachment' :
454 $docs = array(
455 'ID' => '(int)',
456 'URL' => '(URL)',
457 'guid' => '(string)',
458 'mime_type' => '(string)',
459 'width' => '(int)',
460 'height' => '(int)',
461 'duration' => '(int)',
462 );
463 $return[$key] = (object) $this->cast_and_filter( $value, apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ), false, $for_output );
464 break;
465 case 'metadata' :
466 $docs = array(
467 'id' => '(int)',
468 'key' => '(string)',
469 'value' => '(string|false|float|int|array|object)',
470 'previous_value' => '(string)',
471 'operation' => '(string)',
472 );
473 $return[$key] = (object) $this->cast_and_filter( $value, apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ), false, $for_output );
474 break;
475 default :
476 trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
477 }
478 }
479
480 function parse_types( $text ) {
481 if ( !preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
482 return 'none';
483 }
484
485 $types = explode( '|', strtolower( $matches[1] ) );
486 $return = array();
487 foreach ( $types as $type ) {
488 foreach ( array( ':' => 'children', '>' => 'subtype', '=' => 'default' ) as $operator => $meaning ) {
489 if ( false !== strpos( $type, $operator ) ) {
490 $item = explode( $operator, $type, 2 );
491 $return[] = array( 'type' => $item[0], $meaning => $item[1] );
492 continue 2;
493 }
494 }
495 $return[] = compact( 'type' );
496 }
497
498 return $return;
499 }
500
501 /**
502 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
503 * Echoes HTML.
504 */
505 function document( $show_description = true ) {
506 $original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
507 unset( $GLOBALS['post'] );
508
509 $doc = $this->generate_documentation();
510
511 if ( $show_description ) :
512 ?>
513 <caption>
514 <h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
515 <p><?php echo wp_kses_post( $doc['description'] ); ?></p>
516 </caption>
517
518 <?php endif; ?>
519
520 <section class="resource-url">
521 <h2 id="apidoc-resource-url">Resource URL</h2>
522 <table class="api-doc api-doc-resource-parameters api-doc-resource">
523 <thead>
524 <tr>
525 <th class="api-index-title" scope="column">Type</th>
526 <th class="api-index-title" scope="column">URL and Format</th>
527 </tr>
528 </thead>
529 <tbody>
530 <tr class="api-index-item">
531 <th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></th>
532 <td class="type api-index-item-title" style="white-space: nowrap;">https://public-api.wordpress.com/rest/v1<?php echo wp_kses_post( $doc['path_labeled'] ); ?></td>
533 </tr>
534 </tbody>
535 </table>
536 </section>
537
538 <?php
539
540 foreach ( array(
541 'path' => 'Method Parameters',
542 'query' => 'Query Parameters',
543 'body' => 'Request Parameters',
544 'response' => 'Response Parameters',
545 ) as $doc_section_key => $label ) :
546 $doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][$doc_section_key];
547 if ( !$doc_section ) {
548 continue;
549 }
550
551 $param_label = strtolower( str_replace( ' ', '-', $label ) );
552 ?>
553
554 <section class="<?php echo $param_label; ?>">
555
556 <h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
557
558 <table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
559
560 <thead>
561 <tr>
562 <th class="api-index-title" scope="column">Parameter</th>
563 <th class="api-index-title" scope="column">Type</th>
564 <th class="api-index-title" scope="column">Description</th>
565 </tr>
566 </thead>
567 <tbody>
568
569 <?php foreach ( $doc_section as $key => $item ) : ?>
570
571 <tr class="api-index-item">
572 <th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
573 <td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
574 <td class="description api-index-item-body"><?php
575
576 $this->generate_doc_description( $item['description'] );
577
578 ?></td>
579 </tr>
580
581 <?php endforeach; ?>
582 </tbody>
583 </table>
584 </section>
585 <?php endforeach; ?>
586
587 <?php
588 // If no example was hardcoded in the doc, try to get some
589 if ( empty( $this->example_response ) ) {
590
591 // Examples for endpoint documentation response
592 $response_key = 'dev_response_' . $this->version . '_' . $this->method . '_' . sanitize_title( $this->path );
593 $response = wp_cache_get( $response_key );
594
595 // Response doesn't exist, so run the request
596 if ( false === $response ) {
597
598 // Only trust GET request
599 if ( 'GET' === $this->method ) {
600 $response = wp_remote_get( $this->example_request );
601 $response_body = wp_remote_retrieve_body( $response );
602
603 // Only cache if there's a result
604 if ( strlen( $response_body ) ) {
605 wp_cache_set( $response_key, $response );
606 } else {
607 wp_cache_delete( $response_key );
608 }
609 }
610 }
611
612 // Example response was passed into the constructor via params
613 } else {
614 $response = $this->example_response;
615 }
616
617 // Wrap the response in a sourcecode shortcode
618 if ( !empty( $response ) ) {
619 $response = '[sourcecode language="php" wraplines="false" light="true" autolink="false" htmlscript="false"]' . $response . '[/sourcecode]';
620 $response = apply_filters( 'the_content', $response );
621 $this->example_response = $response;
622 }
623
624 $curl = 'curl';
625
626 $php_opts = array( 'ignore_errors' => true );
627
628 if ( 'GET' !== $this->method ) {
629 $php_opts['method'] = $this->method;
630 }
631
632 if ( $this->example_request_data ) {
633 if ( isset( $this->example_request_data['headers'] ) && is_array( $this->example_request_data['headers'] ) ) {
634 $php_opts['header'] = array();
635 foreach ( $this->example_request_data['headers'] as $header => $value ) {
636 $curl .= " \\\n -H " . escapeshellarg( "$header: $value" );
637 $php_opts['header'][] = "$header: $value";
638 }
639 }
640
641 if ( isset( $this->example_request_data['body'] ) && is_array( $this->example_request_data['body'] ) ) {
642 $php_opts['content'] = $this->example_request_data['body'];
643 $php_opts['header'][] = 'Content-Type: application/x-www-form-urlencoded';
644 foreach ( $this->example_request_data['body'] as $key => $value ) {
645 $curl .= " \\\n --data-urlencode " . escapeshellarg( "$key=$value" );
646 }
647 }
648 }
649
650 if ( $php_opts ) {
651 $php_opts_exported = var_export( array( 'http' => $php_opts ), true );
652 if ( !empty( $php_opts['content'] ) ) {
653 $content_exported = preg_quote( var_export( $php_opts['content'], true ), '/' );
654 $content_exported = '\\s*' . str_replace( "\n", "\n\\s*", $content_exported ) . '\\s*';
655 $php_opts_exported = preg_replace_callback( "/$content_exported/", array( $this, 'add_http_build_query_to_php_content_example' ), $php_opts_exported );
656 }
657 $php = <<<EOPHP
658 <?php
659
660 \$options = $php_opts_exported;
661
662 \$context = stream_context_create( \$options );
663 \$response = file_get_contents(
664 '$this->example_request',
665 false,
666 \$context
667 );
668 \$response = json_decode( \$response );
669
670 ?>
671 EOPHP;
672 } else {
673 $php = <<<EOPHP
674 <?php
675
676 \$response = file_get_contents( '$this->example_request' );
677 \$response = json_decode( \$response );
678
679 ?>
680 EOPHP;
681 }
682
683 if ( false !== strpos( $curl, "\n" ) ) {
684 $curl .= " \\\n";
685 }
686
687 $curl .= ' ' . escapeshellarg( $this->example_request );
688
689 $curl = '[sourcecode language="bash" wraplines="false" light="true" autolink="false" htmlscript="false"]' . $curl . '[/sourcecode]';
690 $curl = apply_filters( 'the_content', $curl );
691
692 $php = '[sourcecode language="php" wraplines="false" light="true" autolink="false" htmlscript="false"]' . $php . '[/sourcecode]';
693 $php = apply_filters( 'the_content', $php );
694 ?>
695
696 <?php if ( ! empty( $this->example_request ) || ! empty( $this->example_request_data ) || ! empty( $this->example_response ) ) : ?>
697
698 <section class="example-response">
699 <h2 id="apidoc-example">Example</h2>
700
701 <section>
702 <h3>cURL</h3>
703 <?php echo wp_kses_post( $curl ); ?>
704 </section>
705
706 <section>
707 <h3>PHP</h3>
708 <?php echo wp_kses_post( $php ); ?>
709 </section>
710
711 <?php if ( ! empty( $this->example_response ) ) : ?>
712
713 <section>
714 <h3>Response Body</h3>
715 <?php echo $this->example_response; ?>
716 </section>
717
718 <?php endif; ?>
719
720 </section>
721
722 <?php endif; ?>
723
724 <?php
725 if ( 'unset' !== $original_post ) {
726 $GLOBALS['post'] = $original_post;
727 }
728 }
729
730 function add_http_build_query_to_php_content_example( $matches ) {
731 $trimmed_match = ltrim( $matches[0] );
732 $pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
733 $pad = ltrim( $pad, ' ' );
734 $return = ' ' . str_replace( "\n", "\n ", $matches[0] );
735 return " http_build_query({$return}{$pad})";
736 }
737
738 /**
739 * Recursively generates the <dl>'s to document item descriptions.
740 * Echoes HTML.
741 */
742 function generate_doc_description( $item ) {
743 if ( is_array( $item ) ) : ?>
744
745 <dl>
746 <?php foreach ( $item as $description_key => $description_value ) : ?>
747
748 <dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
749 <dd><?php $this->generate_doc_description( $description_value ); ?></dd>
750
751 <?php endforeach; ?>
752
753 </dl>
754
755 <?php
756 else :
757 echo wp_kses_post( $item );
758 endif;
759 }
760
761 /**
762 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
763 * Echoes HTML.
764 */
765 function generate_documentation() {
766 $format = str_replace( '%d', '%s', $this->path );
767 $path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
768 $boolean_arg = array( 'false', 'true' );
769 $naeloob_arg = array( 'true', 'false' );
770
771 $doc = array(
772 'description' => $this->description,
773 'method' => $this->method,
774 'path_format' => $this->path,
775 'path_labeled' => $path_labeled,
776 'group' => $this->group,
777 'request' => array(
778 'path' => array(),
779 'query' => array(),
780 'body' => array(),
781 ),
782 'response' => array(
783 'body' => array(),
784 )
785 );
786
787 foreach ( array( 'path_labels' => 'path', 'query' => 'query', 'request_format' => 'body', 'response_format' => 'body' ) as $_property => $doc_item ) {
788 foreach ( $this->$_property as $key => $description ) {
789 if ( is_array( $description ) ) {
790 $description_keys = array_keys( $description );
791 if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
792 $type = '(bool)';
793 } else {
794 $type = '(string)';
795 }
796
797 if ( 'response_format' !== $_property ) {
798 // hack - don't show "(default)" in response format
799 reset( $description );
800 $description_key = key( $description );
801 $description[$description_key] = "(default) {$description[$description_key]}";
802 }
803 } else {
804 $types = $this->parse_types( $description );
805 $type = array();
806 $default = '';
807
808 foreach ( $types as $type_array ) {
809 $type[] = $type_array['type'];
810 if ( isset( $type_array['default'] ) ) {
811 $default = $type_array['default'];
812 if ( 'string' === $type_array['type'] ) {
813 $default = "'$default'";
814 }
815 }
816 }
817 $type = '(' . join( '|', $type ) . ')';
818 $noop = ''; // skip an index in list below
819 list( $noop, $description ) = explode( ')', $description, 2 );
820 $description = trim( $description );
821 if ( $default ) {
822 $description .= " Default: $default.";
823 }
824 }
825
826 $item = compact( 'type', 'description' );
827
828 if ( 'response_format' === $_property ) {
829 $doc['response'][$doc_item][$key] = $item;
830 } else {
831 $doc['request'][$doc_item][$key] = $item;
832 }
833 }
834 }
835
836 return $doc;
837 }
838
839 function user_can_view_post( $post_id ) {
840 $post = get_post( $post_id );
841 if ( !$post || is_wp_error( $post ) ) {
842 return false;
843 }
844
845 if ( 'inherit' === $post->post_status ) {
846 $parent_post = get_post( $post->post_parent );
847 $post_status_obj = get_post_status_object( $parent_post->post_status );
848 } else {
849 $post_status_obj = get_post_status_object( $post->post_status );
850 }
851
852 if ( !$post_status_obj->public ) {
853 if ( is_user_logged_in() ) {
854 if ( $post_status_obj->protected ) {
855 if ( !current_user_can( 'edit_post', $post->ID ) ) {
856 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
857 }
858 } elseif ( $post_status_obj->private ) {
859 if ( !current_user_can( 'read_post', $post->ID ) ) {
860 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
861 }
862 } elseif ( 'trash' === $post->post_status ) {
863 if ( !current_user_can( 'edit_post', $post->ID ) ) {
864 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
865 }
866 } else {
867 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
868 }
869 } else {
870 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
871 }
872 }
873
874 if ( -1 == get_option( 'blog_public' ) && !current_user_can( 'read_post', $post->ID ) ) {
875 return new WP_Error( 'unauthorized', 'User cannot view post', array( 'status_code' => 403, 'error' => 'private_blog' ) );
876 }
877
878 if ( strlen( $post->post_password ) && !current_user_can( 'edit_post', $post->ID ) ) {
879 return new WP_Error( 'unauthorized', 'User cannot view password protected post', array( 'status_code' => 403, 'error' => 'password_protected' ) );
880 }
881
882 return true;
883 }
884
885 /**
886 * Returns author object.
887 *
888 * @param $author user ID, user row, WP_User object, comment row, post row
889 * @param $show_email output the author's email address?
890 *
891 * @return (object)
892 */
893 function get_author( $author, $show_email = false ) {
894 if ( isset( $author->comment_author_email ) && !$author->user_id ) {
895 $ID = 0;
896 $email = $author->comment_author_email;
897 $name = $author->comment_author;
898 $URL = $author->comment_author_url;
899 $profile_URL = 'http://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
900 $nice = '';
901 } else {
902 if ( isset( $author->post_author ) ) {
903 if ( 0 == $author->post_author )
904 return null;
905
906 $author = $author->post_author;
907 } elseif ( isset( $author->user_id ) && $author->user_id ) {
908 $author = $author->user_id;
909 } elseif ( isset( $author->user_email ) ) {
910 $author = $author->ID;
911 }
912
913 $user = get_user_by( 'id', $author );
914 if ( !$user || is_wp_error( $user ) ) {
915 trigger_error( 'Unknown user', E_USER_WARNING );
916 return null;
917 }
918
919 $ID = $user->ID;
920 $email = $user->user_email;
921 $name = $user->display_name;
922 $URL = $user->user_url;
923 $nice = $user->user_nicename;
924 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
925 $profile_URL = "http://en.gravatar.com/{$user->user_login}";
926 } else {
927 $profile_URL = 'http://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
928 }
929 }
930
931 $avatar_URL = $this->api->get_avatar_url( $email );
932
933 $email = $show_email ? (string) $email : false;
934
935 return (object) array(
936 'ID' => (int) $ID,
937 'email' => $email, // (string|bool)
938 'name' => (string) $name,
939 'nice_name' => (string) $nice,
940 'URL' => (string) esc_url_raw( $URL ),
941 'avatar_URL' => (string) esc_url_raw( $avatar_URL ),
942 'profile_URL' => (string) esc_url_raw( $profile_URL ),
943 );
944 }
945
946 function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
947
948 $taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
949 /// keep updating this function
950 if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
951 return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
952 }
953
954 // Permissions
955 switch ( $context ) {
956 case 'edit' :
957 $tax = get_taxonomy( $taxonomy_type );
958 if ( !current_user_can( $tax->cap->edit_terms ) )
959 return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
960 break;
961 case 'display' :
962 if ( -1 == get_option( 'blog_public' ) ) {
963 return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
964 }
965 break;
966 default :
967 return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
968 }
969
970 $response = array();
971 $response['name'] = (string) $taxonomy->name;
972 $response['slug'] = (string) $taxonomy_id;
973 $response['description'] = (string) $taxonomy->description;
974 $response['post_count'] = (int) $taxonomy->count;
975
976 if ( 'category' === $taxonomy_type )
977 $response['parent'] = (int) $taxonomy->parent;
978
979 $response['meta'] = (object) array(
980 'links' => (object) array(
981 'self' => (string) $this->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy_id, $taxonomy_type ),
982 'help' => (string) $this->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy_id, $taxonomy_type, 'help' ),
983 'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),
984 ),
985 );
986
987 return (object) $response;
988 }
989
990 /**
991 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
992 *
993 * @param $date_gmt (string) GMT datetime string.
994 * @param $date (string) Optional. Used to calculate the offset from GMT.
995 *
996 * @return string
997 */
998 function format_date( $date_gmt, $date = null ) {
999 $timestamp_gmt = strtotime( "$date_gmt+0000" );
1000 if ( null === $date ) {
1001 $timestamp = $timestamp_gmt;
1002 $hours = $minutes = $west = 0;
1003 } else {
1004 $timestamp = strtotime( "$date+0000" );
1005 $offset = $timestamp - $timestamp_gmt;
1006 $west = $offset < 0;
1007 $offset = abs( $offset );
1008 $hours = (int) floor( $offset / 3600 );
1009 $offset -= $hours * 3600;
1010 $minutes = (int) floor( $offset / 60 );
1011 }
1012
1013 return (string) gmdate( 'Y-m-d\\TH:i:s', $timestamp ) . sprintf( '%s%02d:%02d', $west ? '-' : '+', $hours, $minutes );
1014 }
1015
1016 /**
1017 * @param datetime string
1018 *
1019 * @return array( $local_time_string, $gmt_time_string )
1020 */
1021 function parse_date( $date_string ) {
1022 $time = strtotime( $date_string );
1023 if ( !$time ) {
1024 $time = time();
1025 }
1026
1027 $datetime = new DateTime( "@$time" );
1028 $gmt = $datetime->format( 'Y-m-d H:i:s' );
1029 $timezone_string = get_option( 'timezone_string' );
1030 if ( $timezone_string ) {
1031 $tz = timezone_open( $timezone_string );
1032 if ( $tz ) {
1033 $datetime->setTimezone( $tz );
1034 $local = $datetime->format( 'Y-m-d H:i:s' );
1035 return array( (string) $local, (string) $gmt );
1036 }
1037 }
1038
1039 $gmt_offset = get_option( 'gmt_offset' );
1040 $local_time = $time + $gmt_offset * 3600;
1041
1042 $date = getdate( ( int ) $local_time );
1043 $datetime->setDate( $date['year'], $date['mon'], $date['mday'] );
1044 $datetime->setTime( $date['hours'], $date['minutes'], $date['seconds'] );
1045
1046 $local = $datetime->format( 'Y-m-d H:i:s' );
1047 return array( (string) $local, (string) $gmt );
1048 }
1049
1050 function get_link() {
1051 $args = func_get_args();
1052 $format = array_shift( $args );
1053 array_unshift( $args, $this->api->public_api_scheme, WPCOM_JSON_API__BASE );
1054 $path = array_pop( $args );
1055 if ( $path ) {
1056 $path = '/' . ltrim( $path, '/' );
1057 }
1058 $args[] = $path;
1059
1060 // http, WPCOM_JSON_API__BASE, ... , path
1061 // %s , %s , $format, %s
1062 return esc_url_raw( vsprintf( "%s://%s$format%s", $args ) );
1063 }
1064
1065 function get_me_link( $path = '' ) {
1066 return $this->get_link( '/me', $path );
1067 }
1068
1069 function get_taxonomy_link( $blog_id, $taxonomy_id, $taxonomy_type, $path = '' ) {
1070 if ( 'category' === $taxonomy_type )
1071 return $this->get_link( '/sites/%d/categories/slug:%s', $blog_id, $taxonomy_id, $path );
1072 else
1073 return $this->get_link( '/sites/%d/tags/slug:%s', $blog_id, $taxonomy_id, $path );
1074 }
1075
1076 function get_site_link( $blog_id, $path = '' ) {
1077 return $this->get_link( '/sites/%d', $blog_id, $path );
1078 }
1079
1080 function get_post_link( $blog_id, $post_id, $path = '' ) {
1081 return $this->get_link( '/sites/%d/posts/%d', $blog_id, $post_id, $path );
1082 }
1083
1084 function get_comment_link( $blog_id, $comment_id, $path = '' ) {
1085 return $this->get_link( '/sites/%d/comments/%d', $blog_id, $comment_id, $path );
1086 }
1087
1088 /**
1089 * Return endpoint response
1090 *
1091 * @param ... determined by ->$path
1092 *
1093 * @return
1094 * falsy: HTTP 500, no response body
1095 * WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
1096 * $data: HTTP 200, json_encode( $data ) response body
1097 */
1098 abstract function callback( $path = '' );
1099 }
1100
1101 abstract class WPCOM_JSON_API_Post_Endpoint extends WPCOM_JSON_API_Endpoint {
1102 var $post_object_format = array(
1103 // explicitly document and cast all output
1104 'ID' => '(int) The post ID.',
1105 'author' => '(object>author) The author of the post.',
1106 'date' => "(ISO 8601 datetime) The post's creation time.",
1107 'modified' => "(ISO 8601 datetime) The post's creation time.",
1108 'title' => '(HTML) <code>context</code> dependent.',
1109 'URL' => '(URL) The full permalink URL to the post.',
1110 'short_URL' => '(URL) The wp.me short URL.',
1111 'content' => '(HTML) <code>context</code> dependent.',
1112 'excerpt' => '(HTML) <code>context</code> dependent.',
1113 'slug' => '(string) The name (slug) for your post, used in URLs.',
1114 'status' => array(
1115 'publish' => 'The post is published.',
1116 'draft' => 'The post is saved as a draft.',
1117 'pending' => 'The post is pending editorial approval.',
1118 'future' => 'The post is scheduled for future publishing.',
1119 'trash' => 'The post is in the trash.',
1120 ),
1121 'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.',
1122 'parent' => "(object>post_reference|false) A reference to the post's parent, if it has one.",
1123 'type' => "(string) The post's post_type. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.",
1124 'comments_open' => '(bool) Is the post open for comments?',
1125 'pings_open' => '(bool) Is the post open for pingbacks, trackbacks?',
1126 'comment_count' => '(int) The number of comments for this post.',
1127 'like_count' => '(int) The number of likes for this post.',
1128 'i_like' => '(bool) Does the current user like this post?',
1129 'is_reblogged' => '(bool) Did the current user reblog this post?',
1130 'is_following' => '(bool) Is the current user following this blog?',
1131 'global_ID' => '(string) A unique WordPress.com-wide representation of a post.',
1132 'featured_image' => '(URL) The URL to the featured image for this post if it has one.',
1133 'format' => array(), // see constructor
1134 'geo' => '(object>geo|false)',
1135 'publicize_URLs' => '(array:URL) Array of Twitter and Facebook URLs published by this post.',
1136 'tags' => '(object:tag) Hash of tags (keyed by tag name) applied to the post.',
1137 'categories' => '(object:category) Hash of categories (keyed by category name) applied to the post.',
1138 'attachments' => '(object:attachment) Hash of post attachments (keyed by attachment ID).',
1139 'metadata' => '(array) Array of post metadata keys and values. All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are avaiable for authenticated requests with access. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.',
1140 'meta' => '(object) API result meta data',
1141 );
1142
1143 // var $response_format =& $this->post_object_format;
1144
1145 function __construct( $args ) {
1146 if ( is_array( $this->post_object_format ) && isset( $this->post_object_format['format'] ) ) {
1147 $this->post_object_format['format'] = get_post_format_strings();
1148 }
1149 if ( !$this->response_format ) {
1150 $this->response_format =& $this->post_object_format;
1151 }
1152 parent::__construct( $args );
1153 }
1154
1155 function is_post_type_allowed( $post_type ) {
1156
1157 // if the post type is empty, that's fine, WordPress will default to post
1158 if ( empty( $post_type ) )
1159 return true;
1160
1161 // whitelist of post types that can be accessed
1162 if ( in_array( $post_type, apply_filters( 'rest_api_allowed_post_types', array( 'post', 'page', 'any' ) ) ) )
1163 return true;
1164
1165 return false;
1166 }
1167
1168 function is_metadata_public( $key ) {
1169 if ( empty( $key ) )
1170 return false;
1171
1172 // whitelist of metadata that can be accessed
1173 if ( in_array( $key, apply_filters( 'rest_api_allowed_public_metadata', array() ) ) )
1174 return true;
1175
1176 return false;
1177 }
1178
1179 function the_password_form() {
1180 return __( 'This post is password protected.', 'jetpack' );
1181 }
1182
1183 function get_post_by( $field, $post_id, $context = 'display' ) {
1184 global $blog_id;
1185
1186 if ( defined( 'GEO_LOCATION__CLASS' ) && class_exists( GEO_LOCATION__CLASS ) ) {
1187 $geo = call_user_func( array( GEO_LOCATION__CLASS, 'init' ) );
1188 } else {
1189 $geo = false;
1190 }
1191
1192 if ( 'display' === $context ) {
1193 $args = $this->query_args();
1194 if ( isset( $args['content_width'] ) && $args['content_width'] ) {
1195 $GLOBALS['content_width'] = (int) $args['content_width'];
1196 }
1197 }
1198
1199 if ( strpos( $_SERVER['HTTP_USER_AGENT'], 'wp-windows8' ) ) {
1200 remove_shortcode( 'gallery', 'gallery_shortcode' );
1201 add_shortcode( 'gallery', array( &$this, 'win8_gallery_shortcode' ) );
1202 }
1203
1204 switch ( $field ) {
1205 case 'name' :
1206 $post_id = sanitize_title( $post_id );
1207 if ( !$post_id ) {
1208 return new WP_Error( 'invalid_post', 'Invalid post', 400 );
1209 }
1210
1211 $posts = get_posts( array( 'name' => $post_id ) );
1212 if ( !$posts || !isset( $posts[0]->ID ) || !$posts[0]->ID ) {
1213 $page = get_page_by_path( $post_id );
1214 if ( !$page )
1215 return new WP_Error( 'unknown_post', 'Unknown post', 404 );
1216 $post_id = $page->ID;
1217 } else {
1218 $post_id = (int) $posts[0]->ID;
1219 }
1220 break;
1221 default :
1222 $post_id = (int) $post_id;
1223 break;
1224 }
1225
1226 $post = get_post( $post_id );
1227 if ( !$post || is_wp_error( $post ) ) {
1228 return new WP_Error( 'unknown_post', 'Unknown post', 404 );
1229 }
1230
1231 if ( ! $this->is_post_type_allowed( $post->post_type ) ) {
1232 return new WP_Error( 'unknown_post', 'Unknown post', 404 );
1233 }
1234
1235 // Permissions
1236 switch ( $context ) {
1237 case 'edit' :
1238 if ( !current_user_can( 'edit_post', $post->ID ) ) {
1239 return new WP_Error( 'unauthorized', 'User cannot edit post', 403 );
1240 }
1241 break;
1242 case 'display' :
1243 break;
1244 default :
1245 return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1246 }
1247
1248 $can_view = $this->user_can_view_post( $post->ID );
1249 if ( !$can_view || is_wp_error( $can_view ) ) {
1250 return $can_view;
1251 }
1252
1253 // Re-get post according to the correct $context
1254 $post = get_post( $post->ID, OBJECT, $context );
1255 $GLOBALS['post'] = $post;
1256
1257 if ( 'display' === $context ) {
1258 setup_postdata( $post );
1259 }
1260
1261 $response = array();
1262 foreach ( array_keys( $this->post_object_format ) as $key ) {
1263 switch ( $key ) {
1264 case 'ID' :
1265 // explicitly cast all output
1266 $response[$key] = (int) $post->ID;
1267 break;
1268 case 'author' :
1269 $response[$key] = (object) $this->get_author( $post, 'edit' === $context && current_user_can( 'edit_post', $post->ID ) );
1270 break;
1271 case 'date' :
1272 $response[$key] = (string) $this->format_date( $post->post_date_gmt, $post->post_date );
1273 break;
1274 case 'modified' :
1275 $response[$key] = (string) $this->format_date( $post->post_modified_gmt, $post->post_modified );
1276 break;
1277 case 'title' :
1278 if ( 'display' === $context ) {
1279 $response[$key] = (string) get_the_title( $post->ID );
1280 } else {
1281 $response[$key] = (string) $post->post_title;
1282 }
1283 break;
1284 case 'URL' :
1285 $response[$key] = (string) esc_url_raw( get_permalink( $post->ID ) );
1286 break;
1287 case 'short_URL' :
1288 $response[$key] = (string) esc_url_raw( wp_get_shortlink( $post->ID ) );
1289 break;
1290 case 'content' :
1291 if ( 'display' === $context ) {
1292 add_filter( 'the_password_form', array( $this, 'the_password_form' ) );
1293 $response[$key] = (string) $this->get_the_post_content_for_display();
1294 remove_filter( 'the_password_form', array( $this, 'the_password_form' ) );
1295 } else {
1296 $response[$key] = (string) $post->post_content;
1297 }
1298 break;
1299 case 'excerpt' :
1300 if ( 'display' === $context ) {
1301 add_filter( 'the_password_form', array( $this, 'the_password_form' ) );
1302 ob_start();
1303 the_excerpt();
1304 $response[$key] = (string) ob_get_clean();
1305 remove_filter( 'the_password_form', array( $this, 'the_password_form' ) );
1306 } else {
1307 $response[$key] = (string) $post->post_excerpt;
1308 }
1309 break;
1310 case 'status' :
1311 $response[$key] = (string) get_post_status( $post->ID );
1312 break;
1313 case 'slug' :
1314 $response[$key] = (string) $post->post_name;
1315 break;
1316 case 'password' :
1317 $response[$key] = (string) $post->post_password;
1318 break;
1319 case 'parent' : // (object|false)
1320 if ( $post->post_parent ) {
1321 $parent = get_post( $post->post_parent );
1322 $response[$key] = (object) array(
1323 'ID' => (int) $parent->ID,
1324 'type' => (string) $parent->post_type,
1325 'link' => (string) $this->get_post_link( $this->api->get_blog_id_for_output(), $parent->ID ),
1326 );
1327 } else {
1328 $response[$key] = false;
1329 }
1330 break;
1331 case 'type' :
1332 $response[$key] = (string) $post->post_type;
1333 break;
1334 case 'comments_open' :
1335 $response[$key] = (bool) comments_open( $post->ID );
1336 break;
1337 case 'pings_open' :
1338 $response[$key] = (bool) pings_open( $post->ID );
1339 break;
1340 case 'comment_count' :
1341 $response[$key] = (int) $post->comment_count;
1342 break;
1343 case 'like_count' :
1344 $response[$key] = (int) $this->api->post_like_count( $blog_id, $post->ID );
1345 break;
1346 case 'i_like' :
1347 $response[$key] = (int) $this->api->is_liked( $blog_id, $post->ID );
1348 break;
1349 case 'is_reblogged':
1350 $response[$key] = (int) $this->api->is_reblogged( $blog_id, $post->ID );
1351 break;
1352 case 'is_following':
1353 $response[$key] = (int) $this->api->is_following( $blog_id );
1354 break;
1355 case 'global_ID':
1356 $response[$key] = (string) $this->api->add_global_ID( $blog_id, $post->ID );
1357 break;
1358 case 'featured_image' :
1359 $image_attributes = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'full' );
1360 if ( is_array( $image_attributes ) && isset( $image_attributes[0] ) )
1361 $response[$key] = (string) $image_attributes[0];
1362 else
1363 $response[$key] = '';
1364 break;
1365 case 'format' :
1366 $response[$key] = (string) get_post_format( $post->ID );
1367 if ( !$response[$key] ) {
1368 $response[$key] = 'standard';
1369 }
1370 break;
1371 case 'geo' : // (object|false)
1372 if ( !$geo ) {
1373 $response[$key] = false;
1374 } else {
1375 $geo_data = $geo->get_geo( 'post', $post->ID );
1376 $response[$key] = false;
1377 if ( $geo_data ) {
1378 $geo_data = array_intersect_key( $geo_data, array( 'latitude' => true, 'longitude' => true, 'address' => true, 'public' => true ) );
1379 if ( $geo_data ) {
1380 $response[$key] = (object) array(
1381 'latitude' => isset( $geo_data['latitude'] ) ? (float) $geo_data['latitude'] : 0,
1382 'longitude' => isset( $geo_data['longitude'] ) ? (float) $geo_data['longitude'] : 0,
1383 'address' => isset( $geo_data['address'] ) ? (string) $geo_data['address'] : '',
1384 );
1385 } else {
1386 $response[$key] = false;
1387 }
1388 // Private
1389 if ( !isset( $geo_data['public'] ) || !$geo_data['public'] ) {
1390 if ( 'edit' !== $context || !current_user_can( 'edit_post', $post->ID ) ) {
1391 // user can't access
1392 $response[$key] = false;
1393 }
1394 }
1395 }
1396 }
1397 break;
1398 case 'publicize_URLs' :
1399 $publicize_URLs = array();
1400 $publicize = get_post_meta( $post->ID, 'publicize_results', true );
1401 if ( $publicize ) {
1402 foreach ( $publicize as $service => $data ) {
1403 switch ( $service ) {
1404 case 'twitter' :
1405 foreach ( $data as $datum ) {
1406 $publicize_URLs[] = esc_url_raw( "https://twitter.com/{$datum['user_id']}/status/{$datum['post_id']}" );
1407 }
1408 break;
1409 case 'fb' :
1410 foreach ( $data as $datum ) {
1411 $publicize_URLs[] = esc_url_raw( "https://www.facebook.com/permalink.php?story_fbid={$datum['post_id']}&id={$datum['user_id']}" );
1412 }
1413 break;
1414 }
1415 }
1416 }
1417 $response[$key] = (array) $publicize_URLs;
1418 break;
1419 case 'tags' :
1420 $response[$key] = array();
1421 $terms = wp_get_post_tags( $post->ID );
1422 foreach ( $terms as $term ) {
1423 if ( !empty( $term->name ) ) {
1424 $response[$key][$term->name] = $this->get_taxonomy( $term->slug, 'post_tag', $context );
1425 }
1426 }
1427 $response[$key] = (object) $response[$key];
1428 break;
1429 case 'categories':
1430 $response[$key] = array();
1431 $terms = wp_get_post_categories( $post->ID );
1432 foreach ( $terms as $term ) {
1433 $category = $taxonomy = get_term_by( 'id', $term, 'category' );
1434 if ( !empty( $category->name ) ) {
1435 $response[$key][$category->name] = $this->get_taxonomy( $category->slug, 'category', $context );
1436 }
1437 }
1438 $response[$key] = (object) $response[$key];
1439 break;
1440 case 'attachments':
1441 $response[$key] = array();
1442 $_attachments = get_posts( array( 'post_parent' => $post->ID, 'post_status' => 'inherit', 'post_type' => 'attachment' ) );
1443 foreach ( $_attachments as $attachment ) {
1444 $response[$key][$attachment->ID] = $this->get_attachment( $attachment );
1445 }
1446 $response[$key] = (object) $response[$key];
1447 break;
1448 case 'metadata' : // (array|false)
1449 $metadata = array();
1450 foreach ( (array) has_meta( $post_id ) as $meta ) {
1451 // Don't expose protected fields.
1452 $show = false;
1453 if ( $this->is_metadata_public( $meta['meta_key'] ) )
1454 $show = true;
1455 if ( current_user_can( 'edit_post_meta', $post_id , $meta['meta_key'] ) )
1456 $show = true;
1457
1458 if ( !$show )
1459 continue;
1460
1461 $metadata[] = array(
1462 'id' => $meta['meta_id'],
1463 'key' => $meta['meta_key'],
1464 'value' => maybe_unserialize( $meta['meta_value'] ),
1465 );
1466 }
1467
1468 if ( ! empty( $metadata ) ) {
1469 $response[$key] = $metadata;
1470 } else {
1471 $response[$key] = false;
1472 }
1473 break;
1474 case 'meta' :
1475 $response[$key] = (object) array(
1476 'links' => (object) array(
1477 'self' => (string) $this->get_post_link( $this->api->get_blog_id_for_output(), $post->ID ),
1478 'help' => (string) $this->get_post_link( $this->api->get_blog_id_for_output(), $post->ID, 'help' ),
1479 'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),
1480 // 'author' => (string) $this->get_user_link( $post->post_author ),
1481 // 'via' => (string) $this->get_post_link( $reblog_origin_blog_id, $reblog_origin_post_id ),
1482 'replies' => (string) $this->get_post_link( $this->api->get_blog_id_for_output(), $post->ID, 'replies/' ),
1483 'likes' => (string) $this->get_post_link( $this->api->get_blog_id_for_output(), $post->ID, 'likes/' ),
1484 ),
1485 );
1486 break;
1487 }
1488 }
1489
1490 unset( $GLOBALS['post'] );
1491 return $response;
1492 }
1493
1494 // No Blog ID parameter. No Post ID parameter. Depends on globals.
1495 // Expects setup_postdata() to already have been run
1496 function get_the_post_content_for_display() {
1497 global $pages, $page;
1498
1499 $old_pages = $pages;
1500 $old_page = $page;
1501
1502 $content = join( "\n\n", $pages );
1503 $content = preg_replace( '/<!--more(.*?)?-->/', '', $content );
1504 $pages = array( $content );
1505 $page = 1;
1506
1507 ob_start();
1508 the_content();
1509 $return = ob_get_clean();
1510
1511 $pages = $old_pages;
1512 $page = $old_page;
1513
1514 return $return;
1515 }
1516
1517 function get_blog_post( $blog_id, $post_id, $context = 'display' ) {
1518 $blog_id = $this->api->get_blog_id( $blog_id );
1519 if ( !$blog_id || is_wp_error( $blog_id ) ) {
1520 return $blog_id;
1521 }
1522 switch_to_blog( $blog_id );
1523 $post = $this->get_post_by( 'ID', $post_id, $context );
1524 restore_current_blog();
1525 return $post;
1526 }
1527
1528 function win8_gallery_shortcode( $attr ) {
1529 global $post;
1530
1531 static $instance = 0;
1532 $instance++;
1533
1534 $output = '';
1535
1536 // We're trusting author input, so let's at least make sure it looks like a valid orderby statement
1537 if ( isset( $attr['orderby'] ) ) {
1538 $attr['orderby'] = sanitize_sql_orderby( $attr['orderby'] );
1539 if ( !$attr['orderby'] )
1540 unset( $attr['orderby'] );
1541 }
1542
1543 extract( shortcode_atts( array(
1544 'order' => 'ASC',
1545 'orderby' => 'menu_order ID',
1546 'id' => $post->ID,
1547 'include' => '',
1548 'exclude' => '',
1549 'slideshow' => false
1550 ), $attr ) );
1551
1552 // Custom image size and always use it
1553 add_image_size( 'win8app-column', 480 );
1554 $size = 'win8app-column';
1555
1556 $id = intval( $id );
1557 if ( 'RAND' === $order )
1558 $orderby = 'none';
1559
1560 if ( !empty( $include ) ) {
1561 $include = preg_replace( '/[^0-9,]+/', '', $include );
1562 $_attachments = get_posts( array( 'include' => $include, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby ) );
1563 $attachments = array();
1564 foreach ( $_attachments as $key => $val ) {
1565 $attachments[$val->ID] = $_attachments[$key];
1566 }
1567 } elseif ( !empty( $exclude ) ) {
1568 $exclude = preg_replace( '/[^0-9,]+/', '', $exclude );
1569 $attachments = get_children( array( 'post_parent' => $id, 'exclude' => $exclude, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby ) );
1570 } else {
1571 $attachments = get_children( array( 'post_parent' => $id, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby ) );
1572 }
1573
1574 if ( ! empty( $attachments ) ) {
1575 foreach ( $attachments as $id => $attachment ) {
1576 $link = isset( $attr['link'] ) && 'file' === $attr['link'] ? wp_get_attachment_link( $id, $size, false, false ) : wp_get_attachment_link( $id, $size, true, false );
1577
1578 if ( $captiontag && trim($attachment->post_excerpt) ) {
1579 $output .= "<div class='wp-caption aligncenter'>$link
1580 <p class='wp-caption-text'>" . wptexturize($attachment->post_excerpt) . "</p>
1581 </div>";
1582 } else {
1583 $output .= $link . ' ';
1584 }
1585 }
1586 }
1587 }
1588
1589 /**
1590 * Returns attachment object.
1591 *
1592 * @param $attachment attachment row
1593 *
1594 * @return (object)
1595 */
1596 function get_attachment( $attachment ) {
1597 $metadata = wp_get_attachment_metadata( $attachment->ID );
1598
1599 $result = array(
1600 'ID' => (int) $attachment->ID,
1601 'URL' => (string) wp_get_attachment_url( $attachment->ID ),
1602 'guid' => (string) $attachment->guid,
1603 'mime_type' => (string) $attachment->post_mime_type,
1604 'width' => (int) isset( $metadata['width'] ) ? $metadata['width'] : 0,
1605 'height' => (int) isset( $metadata['height'] ) ? $metadata['height'] : 0,
1606 );
1607
1608 if ( isset( $metadata['duration'] ) ) {
1609 $result['duration'] = (int) $metadata['duration'];
1610 }
1611
1612 return (object) apply_filters( 'get_attachment', $result );
1613 }
1614 }
1615
1616 class WPCOM_JSON_API_Get_Post_Endpoint extends WPCOM_JSON_API_Post_Endpoint {
1617 // /sites/%s/posts/%d -> $blog_id, $post_id
1618 // /sites/%s/posts/name:%s -> $blog_id, $post_id // not documented
1619 // /sites/%s/posts/slug:%s -> $blog_id, $post_id
1620 function callback( $path = '', $blog_id = 0, $post_id = 0 ) {
1621 $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
1622 if ( is_wp_error( $blog_id ) ) {
1623 return $blog_id;
1624 }
1625
1626 $args = $this->query_args();
1627
1628 if ( false === strpos( $path, '/posts/slug:' ) && false === strpos( $path, '/posts/name:' ) ) {
1629 $get_by = 'ID';
1630 } else {
1631 $get_by = 'name';
1632 }
1633
1634 $return = $this->get_post_by( $get_by, $post_id, $args['context'] );
1635 if ( !$return || is_wp_error( $return ) ) {
1636 return $return;
1637 }
1638
1639 do_action( 'wpcom_json_api_objects', 'posts' );
1640
1641 return $return;
1642 }
1643 }
1644
1645 class WPCOM_JSON_API_List_Posts_Endpoint extends WPCOM_JSON_API_Post_Endpoint {
1646 var $date_range = array();
1647
1648 var $response_format = array(
1649 'found' => '(int) The total number of posts found that match the request (ignoring limits, offsets, and pagination).',
1650 'posts' => '(array:post) An array of post objects.',
1651 );
1652
1653 // /sites/%s/posts/ -> $blog_id
1654 function callback( $path = '', $blog_id = 0 ) {
1655 $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
1656 if ( is_wp_error( $blog_id ) ) {
1657 return $blog_id;
1658 }
1659
1660 $args = $this->query_args();
1661
1662 if ( $args['number'] < 1 ) {
1663 $args['number'] = 20;
1664 } elseif ( 100 < $args['number'] ) {
1665 return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 );
1666 }
1667
1668 if ( ! $this->is_post_type_allowed( $args['type'] ) ) {
1669 return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
1670 }
1671
1672 $query = array(
1673 'posts_per_page' => $args['number'],
1674 'order' => $args['order'],
1675 'orderby' => $args['order_by'],
1676 'post_type' => ( 'any' == $args['type'] ) ? array( 'post', 'page' ) : $args['type'],
1677 'post_status' => $args['status'],
1678 'author' => isset( $args['author'] ) && 0 < $args['author'] ? $args['author'] : null,
1679 's' => isset( $args['search'] ) ? $args['search'] : null,
1680 );
1681
1682 if ( isset( $args['meta_key'] ) ) {
1683 $show = false;
1684 if ( $this->is_metadata_public( $args['meta_key'] ) )
1685 $show = true;
1686 if ( current_user_can( 'edit_post_meta', $query['post_type'], $args['meta_key'] ) )
1687 $show = true;
1688
1689 if ( is_protected_meta( $args['meta_key'], 'post' ) && ! $show )
1690 return new WP_Error( 'invalid_meta_key', 'Invalid meta key', 404 );
1691
1692 $meta = array( 'key' => $args['meta_key'] );
1693 if ( isset( $args['meta_value'] ) )
1694 $meta['value'] = $args['meta_value'];
1695
1696 $query['meta_query'] = array( $meta );
1697 }
1698
1699 if (
1700 isset( $args['sticky'] )
1701 &&
1702 ( $sticky = get_option( 'sticky_posts' ) )
1703 &&
1704 is_array( $sticky )
1705 ) {
1706 if ( $args['sticky'] ) {
1707 $query['post__in'] = $sticky;
1708 } else {
1709 $query['post__not_in'] = $sticky;
1710 $query['ignore_sticky_posts'] = 1;
1711 }
1712 }
1713
1714 if ( isset( $args['category'] ) ) {
1715 $category = get_term_by( 'slug', $args['category'], 'category' );
1716 if ( $category === false) {
1717 $query['category_name'] = $args['category'];
1718 } else {
1719 $query['cat'] = $category->term_id;
1720 }
1721 }
1722
1723 if ( isset( $args['tag'] ) ) {
1724 $query['tag'] = $args['tag'];
1725 }
1726
1727 if ( isset( $args['page'] ) ) {
1728 if ( $args['page'] < 1 ) {
1729 $args['page'] = 1;
1730 }
1731
1732 $query['paged'] = $args['page'];
1733 } else {
1734 if ( $args['offset'] < 0 ) {
1735 $args['offset'] = 0;
1736 }
1737
1738 $query['offset'] = $args['offset'];
1739 }
1740
1741 if ( isset( $args['before'] ) ) {
1742 $this->date_range['before'] = $args['before'];
1743 }
1744 if ( isset( $args['after'] ) ) {
1745 $this->date_range['after'] = $args['after'];
1746 }
1747
1748 if ( $this->date_range ) {
1749 add_filter( 'posts_where', array( $this, 'handle_date_range' ) );
1750 }
1751 $wp_query = new WP_Query( $query );
1752 if ( $this->date_range ) {
1753 remove_filter( 'posts_where', array( $this, 'handle_date_range' ) );
1754 $this->date_range = array();
1755 }
1756
1757 $return = array();
1758 foreach ( array_keys( $this->response_format ) as $key ) {
1759 switch ( $key ) {
1760 case 'found' :
1761 $return[$key] = (int) $wp_query->found_posts;
1762 break;
1763 case 'posts' :
1764 $posts = array();
1765 foreach ( $wp_query->posts as $post ) {
1766 $the_post = $this->get_post_by( 'ID', $post->ID, $args['context'] );
1767 if ( $the_post && !is_wp_error( $the_post ) ) {
1768 $posts[] = $the_post;
1769 }
1770 }
1771
1772 if ( $posts ) {
1773 do_action( 'wpcom_json_api_objects', 'posts', count( $posts ) );
1774 }
1775
1776 $return[$key] = $posts;
1777 break;
1778 }
1779 }
1780
1781 return $return;
1782 }
1783
1784 function handle_date_range( $where ) {
1785 global $wpdb;
1786
1787 switch ( count( $this->date_range ) ) {
1788 case 2 :
1789 $where .= $wpdb->prepare(
1790 " AND `$wpdb->posts`.post_date BETWEEN CAST( %s AS DATETIME ) AND CAST( %s AS DATETIME ) ",
1791 $this->date_range['after'],
1792 $this->date_range['before']
1793 );
1794 break;
1795 case 1 :
1796 if ( isset( $this->date_range['before'] ) ) {
1797 $where .= $wpdb->prepare(
1798 " AND `$wpdb->posts`.post_date <= CAST( %s AS DATETIME ) ",
1799 $this->date_range['before']
1800 );
1801 } else {
1802 $where .= $wpdb->prepare(
1803 " AND `$wpdb->posts`.post_date >= CAST( %s AS DATETIME ) ",
1804 $this->date_range['after']
1805 );
1806 }
1807 break;
1808 }
1809
1810 return $where;
1811 }
1812 }
1813
1814 class WPCOM_JSON_API_Update_Post_Endpoint extends WPCOM_JSON_API_Post_Endpoint {
1815 function __construct( $args ) {
1816 parent::__construct( $args );
1817 if ( $this->api->ends_with( $this->path, '/delete' ) ) {
1818 $this->post_object_format['status']['deleted'] = 'The post has been deleted permanently.';
1819 }
1820 }
1821
1822 // /sites/%s/posts/new -> $blog_id
1823 // /sites/%s/posts/%d -> $blog_id, $post_id
1824 // /sites/%s/posts/%d/delete -> $blog_id, $post_id
1825 function callback( $path = '', $blog_id = 0, $post_id = 0 ) {
1826 $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
1827 if ( is_wp_error( $blog_id ) ) {
1828 return $blog_id;
1829 }
1830
1831 if ( $this->api->ends_with( $path, '/delete' ) ) {
1832 return $this->delete_post( $path, $blog_id, $post_id );
1833 } else {
1834 return $this->write_post( $path, $blog_id, $post_id );
1835 }
1836 }
1837
1838 // /sites/%s/posts/new -> $blog_id
1839 // /sites/%s/posts/%d -> $blog_id, $post_id
1840 function write_post( $path, $blog_id, $post_id ) {
1841 $new = $this->api->ends_with( $path, '/new' );
1842 $args = $this->query_args();
1843
1844 if ( $new ) {
1845 $input = $this->input( true );
1846
1847 if ( !isset( $input['title'] ) && !isset( $input['content'] ) && !isset( $input['excerpt'] ) ) {
1848 return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
1849 }
1850
1851 // default to post
1852 if ( empty( $input['type'] ) )
1853 $input['type'] = 'post';
1854
1855 $post_type = get_post_type_object( $input['type'] );
1856
1857 if ( ! $this->is_post_type_allowed( $input['type'] ) ) {
1858 return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
1859 }
1860
1861 if ( 'publish' === $input['status'] ) {
1862 if ( !current_user_can( $post_type->cap->publish_posts ) ) {
1863 if ( current_user_can( $post_type->cap->edit_posts ) ) {
1864 $input['status'] = 'pending';
1865 } else {
1866 return new WP_Error( 'unauthorized', 'User cannot publish posts', 403 );
1867 }
1868 }
1869 } else {
1870 if ( !current_user_can( $post_type->cap->edit_posts ) ) {
1871 return new WP_Error( 'unauthorized', 'User cannot edit posts', 403 );
1872 }
1873 }
1874 } else {
1875 $input = $this->input( false );
1876
1877 if ( !is_array( $input ) || !$input ) {
1878 return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
1879 }
1880
1881 $post = get_post( $post_id );
1882 if ( !$post || is_wp_error( $post ) ) {
1883 return new WP_Error( 'unknown_post', 'Unknown post', 404 );
1884 }
1885
1886 if ( !current_user_can( 'edit_post', $post->ID ) ) {
1887 return new WP_Error( 'unauthorized', 'User cannot edit post', 403 );
1888 }
1889
1890 if ( 'publish' === $input['status'] && 'publish' !== $post->post_status && !current_user_can( 'publish_post', $post->ID ) ) {
1891 $input['status'] = 'pending';
1892 }
1893
1894 $post_type = get_post_type_object( $post->post_type );
1895 }
1896
1897 if ( !is_post_type_hierarchical( $post_type->name ) ) {
1898 unset( $input['parent'] );
1899 }
1900
1901 $categories = null;
1902 $tags = null;
1903
1904 if ( !empty( $input['categories'] )) {
1905 if ( is_array( $input['categories'] ) ) {
1906 $_categories = $input['categories'];
1907 } else {
1908 foreach ( explode( ',', $input['categories'] ) as $category ) {
1909 $_categories[] = $category;
1910 }
1911 }
1912 foreach ( $_categories as $category ) {
1913 if ( !$category_info = term_exists( $category, 'category' ) ) {
1914 if ( is_int( $category ) )
1915 continue;
1916 $category_info = wp_insert_term( $category, 'category' );
1917 }
1918 if ( !is_wp_error( $category_info ) )
1919 $categories[] = (int) $category_info['term_id'];
1920 }
1921 }
1922
1923 if ( !empty( $input['tags'] ) ) {
1924 if ( is_array( $input['tags'] ) ) {
1925 $tags = $input['tags'];
1926 } else {
1927 foreach ( explode( ',', $input['tags'] ) as $tag ) {
1928 $tags[] = $tag;
1929 }
1930 }
1931 $tags_string = implode( ',', $tags );
1932 }
1933
1934 unset( $input['tags'], $input['categories'] );
1935
1936 $insert = array();
1937
1938 if ( !empty( $input['slug'] ) ) {
1939 $insert['post_name'] = $input['slug'];
1940 unset( $input['slug'] );
1941 }
1942
1943 if ( true === $input['comments_open'] )
1944 $insert['comment_status'] = 'open';
1945 else if ( false === $input['comments_open'] )
1946 $insert['comment_status'] = 'closed';
1947
1948 if ( true === $input['pings_open'] )
1949 $insert['ping_status'] = 'open';
1950 else if ( false === $input['pings_open'] )
1951 $insert['ping_status'] = 'closed';
1952
1953 unset( $input['comments_open'], $input['pings_open'] );
1954
1955 $publicize = $input['publicize'];
1956 $publicize_custom_message = $input['publicize_message'];
1957 unset( $input['publicize'], $input['publicize_message'] );
1958
1959 $metadata = $input['metadata'];
1960 unset( $input['metadata'] );
1961
1962 foreach ( $input as $key => $value ) {
1963 $insert["post_$key"] = $value;
1964 }
1965
1966 if ( !empty( $tags ) )
1967 $insert["tax_input"]["post_tag"] = $tags;
1968 if ( !empty( $categories ) )
1969 $insert["tax_input"]["category"] = $categories;
1970
1971 $has_media = isset( $input['media'] ) && $input['media'] ? count( $input['media'] ) : false;
1972
1973 if ( $new ) {
1974 if ( false === strpos( $input['content'], '[gallery' ) && $has_media ) {
1975 switch ( $has_media ) {
1976 case 0 :
1977 // No images - do nothing.
1978 break;
1979 case 1 :
1980 // 1 image - make it big
1981 $insert['post_content'] = $input['content'] = "[gallery size=full columns=1]\n\n" . $input['content'];
1982 break;
1983 default :
1984 // Several images - 3 column gallery
1985 $insert['post_content'] = $input['content'] = "[gallery]\n\n" . $input['content'];
1986 break;
1987 }
1988 }
1989
1990 $post_id = wp_insert_post( add_magic_quotes( $insert ), true );
1991
1992 if ( $has_media ) {
1993 $this->api->trap_wp_die( 'upload_error' );
1994 foreach ( $input['media'] as $media_item ) {
1995 $_FILES['.api.media.item.'] = $media_item;
1996 // check for WP_Error if we ever actually need $media_id
1997 $media_id = media_handle_upload( '.api.media.item.', $post_id );
1998 }
1999 $this->api->trap_wp_die( null );
2000
2001 unset( $_FILES['.api.media.item.'] );
2002 }
2003 } else {
2004 $insert['ID'] = $post->ID;
2005 $post_id = wp_update_post( (object) $insert );
2006 }
2007
2008 if ( !$post_id || is_wp_error( $post_id ) ) {
2009 return $post_id;
2010 }
2011
2012 if ( $publicize === false ) {
2013 foreach ( $GLOBALS['publicize_ui']->publicize->get_services( 'all' ) as $name => $service ) {
2014 update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $name, 1 );
2015 }
2016 } else if ( is_array( $publicize ) && ( count ( $publicize ) > 0 ) ) {
2017 foreach ( $GLOBALS['publicize_ui']->publicize->get_services( 'all' ) as $name => $service ) {
2018 if ( !in_array( $name, $publicize ) ) {
2019 update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_SKIP . $name, 1 );
2020 }
2021 }
2022 }
2023
2024 if ( !empty( $publicize_custom_message ) )
2025 update_post_meta( $post_id, $GLOBALS['publicize_ui']->publicize->POST_MESS, trim( $publicize_custom_message ) );
2026
2027 set_post_format( $post_id, $insert['post_format'] );
2028
2029 if ( ! empty( $metadata ) ) {
2030 foreach ( (array) $metadata as $meta ) {
2031
2032 $meta = (object) $meta;
2033
2034 $existing_meta_item = new stdClass;
2035
2036 if ( empty( $meta->operation ) )
2037 $meta->operation = 'update';
2038
2039 if ( ! empty( $meta->value ) ) {
2040 if ( 'true' == $meta->value )
2041 $meta->value = true;
2042 if ( 'false' == $meta->value )
2043 $meta->value = false;
2044 }
2045
2046 if ( ! empty( $meta->id ) ) {
2047 $meta->id = absint( $meta->id );
2048 $existing_meta_item = get_metadata_by_mid( 'post', $meta->id );
2049 }
2050
2051 $unslashed_meta_key = wp_unslash( $meta->key ); // should match what the final key will be
2052 $meta->key = wp_slash( $meta->key );
2053 $unslashed_existing_meta_key = wp_unslash( $existing_meta_item->meta_key );
2054 $existing_meta_item->meta_key = wp_slash( $existing_meta_item->meta_key );
2055
2056 switch ( $meta->operation ) {
2057 case 'delete':
2058
2059 if ( ! empty( $meta->id ) && ! empty( $existing_meta_item->meta_key ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_existing_meta_key ) ) {
2060 delete_metadata_by_mid( 'post', $meta->id );
2061 } elseif ( ! empty( $meta->key ) && ! empty( $meta->previous_value ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_meta_key ) ) {
2062 delete_post_meta( $post_id, $meta->key, $meta->previous_value );
2063 } elseif ( ! empty( $meta->key ) && current_user_can( 'delete_post_meta', $post_id, $unslashed_meta_key ) ) {
2064 delete_post_meta( $post_id, $meta->key );
2065 }
2066
2067 break;
2068 case 'add':
2069
2070 if ( ! empty( $meta->id ) || ! empty( $meta->previous_value ) ) {
2071 continue;
2072 } elseif ( ! empty( $meta->key ) && ! empty( $meta->value ) && current_user_can( 'add_post_meta', $post_id, $unslashed_meta_key ) ) {
2073 add_post_meta( $post_id, $meta->key, $meta->value );
2074 }
2075
2076 break;
2077 case 'update':
2078
2079 if ( ! isset( $meta->value ) ) {
2080 continue;
2081 } elseif ( ! empty( $meta->id ) && ! empty( $existing_meta_item->meta_key ) && current_user_can( 'edit_post_meta', $post_id, $unslashed_existing_meta_key ) ) {
2082 update_metadata_by_mid( 'post', $meta->id, $meta->value );
2083 } elseif ( ! empty( $meta->key ) && ! empty( $meta->previous_value ) && current_user_can( 'edit_post_meta', $post_id, $unslashed_meta_key ) ) {
2084 update_post_meta( $post_id, $meta->key,$meta->value, $meta->previous_value );
2085 } elseif ( ! empty( $meta->key ) && current_user_can( 'edit_post_meta', $post_id, $unslashed_meta_key ) ) {
2086 update_post_meta( $post_id, $meta->key, $meta->value );
2087 }
2088
2089 break;
2090 }
2091
2092 }
2093 }
2094
2095 do_action( 'rest_api_inserted_post', $post_id, $insert, $new );
2096
2097 $return = $this->get_post_by( 'ID', $post_id, $args['context'] );
2098 if ( !$return || is_wp_error( $return ) ) {
2099 return $return;
2100 }
2101
2102 do_action( 'wpcom_json_api_objects', 'posts' );
2103
2104 return $return;
2105 }
2106
2107 // /sites/%s/posts/%d/delete -> $blog_id, $post_id
2108 function delete_post( $path, $blog_id, $post_id ) {
2109 $post = get_post( $post_id );
2110 if ( !$post || is_wp_error( $post ) ) {
2111 return new WP_Error( 'unknown_post', 'Unknown post', 404 );
2112 }
2113
2114 if ( ! $this->is_post_type_allowed( $post->post_type ) ) {
2115 return new WP_Error( 'unknown_post_type', 'Unknown post type', 404 );
2116 }
2117
2118 if ( !current_user_can( 'delete_post', $post->ID ) ) {
2119 return new WP_Error( 'unauthorized', 'User cannot delete posts', 403 );
2120 }
2121
2122 $args = $this->query_args();
2123 $return = $this->get_post_by( 'ID', $post->ID, $args['context'] );
2124 if ( !$return || is_wp_error( $return ) ) {
2125 return $return;
2126 }
2127
2128 do_action( 'wpcom_json_api_objects', 'posts' );
2129
2130 wp_delete_post( $post->ID );
2131
2132 $status = get_post_status( $post->ID );
2133 if ( false === $status ) {
2134 $return['status'] = 'deleted';
2135 return $return;
2136 }
2137
2138 return $this->get_post_by( 'ID', $post->ID, $args['context'] );
2139 }
2140 }
2141
2142 abstract class WPCOM_JSON_API_Taxonomy_Endpoint extends WPCOM_JSON_API_Endpoint {
2143 var $category_object_format = array(
2144 'ID' => '(int) The category ID.',
2145 'name' => "(string) The name of the category.",
2146 'slug' => "(string) The slug of the category.",
2147 'description' => '(string) The description of the category.',
2148 'post_count' => "(int) The number of posts using this category.",
2149 'parent' => "(int) The parent ID for the category.",
2150 'meta' => '(object) Meta data',
2151 );
2152
2153 var $tag_object_format = array(
2154 'ID' => '(int) The tag ID.',
2155 'name' => "(string) The name of the tag.",
2156 'slug' => "(string) The slug of the tag.",
2157 'description' => '(string) The description of the tag.',
2158 'post_count' => "(int) The number of posts using this t.",
2159 'meta' => '(object) Meta data',
2160 );
2161
2162 function __construct( $args ) {
2163 parent::__construct( $args );
2164 if ( preg_match( '#/tags/#i', $this->path ) )
2165 $this->response_format =& $this->tag_object_format;
2166 else
2167 $this->response_format =& $this->category_object_format;
2168 }
2169 }
2170
2171
2172 class WPCOM_JSON_API_Get_Taxonomy_Endpoint extends WPCOM_JSON_API_Taxonomy_Endpoint {
2173 // /sites/%s/tags/slug:%s -> $blog_id, $tag_id
2174 // /sites/%s/categories/slug:%s -> $blog_id, $tag_id
2175 function callback( $path = '', $blog_id = 0, $taxonomy_id = 0 ) {
2176 $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
2177 if ( is_wp_error( $blog_id ) ) {
2178 return $blog_id;
2179 }
2180
2181 $args = $this->query_args();
2182 if ( preg_match( '#/tags/#i', $path ) ) {
2183 $taxonomy_type = "post_tag";
2184 } else {
2185 $taxonomy_type = "category";
2186 }
2187
2188 $return = $this->get_taxonomy( $taxonomy_id, $taxonomy_type, $args['context'] );
2189 if ( !$return || is_wp_error( $return ) ) {
2190 return $return;
2191 }
2192
2193 do_action( 'wpcom_json_api_objects', 'taxonomies' );
2194
2195 return $return;
2196 }
2197 }
2198
2199
2200 class WPCOM_JSON_API_Update_Taxonomy_Endpoint extends WPCOM_JSON_API_Taxonomy_Endpoint {
2201 // /sites/%s/tags|categories/new -> $blog_id
2202 // /sites/%s/tags|categories/slug:%s -> $blog_id, $taxonomy_id
2203 // /sites/%s/tags|categories/slug:%s/delete -> $blog_id, $taxonomy_id
2204 function callback( $path = '', $blog_id = 0, $object_id = 0 ) {
2205 $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
2206 if ( is_wp_error( $blog_id ) ) {
2207 return $blog_id;
2208 }
2209
2210 if ( preg_match( '#/tags/#i', $path ) ) {
2211 $taxonomy_type = "post_tag";
2212 } else {
2213 $taxonomy_type = "category";
2214 }
2215
2216 if ( $this->api->ends_with( $path, '/delete' ) ) {
2217 return $this->delete_taxonomy( $path, $blog_id, $object_id, $taxonomy_type );
2218 } elseif ( $this->api->ends_with( $path, '/new' ) ) {
2219 return $this->new_taxonomy( $path, $blog_id, $taxonomy_type );
2220 }
2221
2222 return $this->update_taxonomy( $path, $blog_id, $object_id, $taxonomy_type );
2223 }
2224
2225 // /sites/%s/tags|categories/new -> $blog_id
2226 function new_taxonomy( $path, $blog_id, $taxonomy_type ) {
2227 $args = $this->query_args();
2228 $input = $this->input();
2229 if ( !is_array( $input ) || !$input || !strlen( $input['name'] ) ) {
2230 return new WP_Error( 'unknown_taxonomy', 'Unknown data passed', 404 );
2231 }
2232
2233 $user = wp_get_current_user();
2234 if ( !$user || is_wp_error( $user ) || !$user->ID ) {
2235 return new WP_Error( 'authorization_required', 'An active access token must be used to manage taxonomies.', 403 );
2236 }
2237
2238 $tax = get_taxonomy( $taxonomy_type );
2239 if ( !current_user_can( $tax->cap->edit_terms ) ) {
2240 return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
2241 }
2242
2243 if ( term_exists( $input['name'], $taxonomy_type ) ) {
2244 return new WP_Error( 'unknown_taxonomy', 'A taxonomy with that name already exists', 404 );
2245 }
2246
2247 if ( 'category' !== $taxonomy_type )
2248 $input['parent'] = 0;
2249
2250 $data = wp_insert_term( addslashes( $input['name'] ), $taxonomy_type,
2251 array(
2252 'description' => addslashes( $input['description'] ),
2253 'parent' => $input['parent']
2254 )
2255 );
2256
2257 if ( is_wp_error( $data ) )
2258 return $data;
2259
2260 $taxonomy = get_term_by( 'id', $data['term_id'], $taxonomy_type );
2261
2262 $return = $this->get_taxonomy( $taxonomy->slug, $taxonomy_type, $args['context'] );
2263 if ( !$return || is_wp_error( $return ) ) {
2264 return $return;
2265 }
2266
2267 do_action( 'wpcom_json_api_objects', 'taxonomies' );
2268 return $return;
2269 }
2270
2271 // /sites/%s/tags|categories/slug:%s -> $blog_id, $taxonomy_id
2272 function update_taxonomy( $path, $blog_id, $object_id, $taxonomy_type ) {
2273 $taxonomy = get_term_by( 'slug', $object_id, $taxonomy_type );
2274 $tax = get_taxonomy( $taxonomy_type );
2275 if ( !current_user_can( $tax->cap->edit_terms ) )
2276 return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
2277
2278 if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
2279 return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
2280 }
2281
2282 if ( false === term_exists( $object_id, $taxonomy_type ) ) {
2283 return new WP_Error( 'unknown_taxonomy', 'That taxonomy does not exist', 404 );
2284 }
2285
2286 $args = $this->query_args();
2287 $input = $this->input( false );
2288 if ( !is_array( $input ) || !$input ) {
2289 return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
2290 }
2291
2292 $update = array();
2293 if ( 'category' === $taxonomy_type && !empty( $input['parent'] ) )
2294 $update['parent'] = $input['parent'];
2295
2296 if ( !empty( $input['description'] ) )
2297 $update['description'] = addslashes( $input['description'] );
2298
2299 if ( !empty( $input['name'] ) )
2300 $update['name'] = addslashes( $input['name'] );
2301
2302
2303 $data = wp_update_term( $taxonomy->term_id, $taxonomy_type, $update );
2304 $taxonomy = get_term_by( 'id', $data['term_id'], $taxonomy_type );
2305
2306 $return = $this->get_taxonomy( $taxonomy->slug, $taxonomy_type, $args['context'] );
2307 if ( !$return || is_wp_error( $return ) ) {
2308 return $return;
2309 }
2310
2311 do_action( 'wpcom_json_api_objects', 'taxonomies' );
2312 return $return;
2313 }
2314
2315 // /sites/%s/tags|categories/%s/delete -> $blog_id, $taxonomy_id
2316 function delete_taxonomy( $path, $blog_id, $object_id, $taxonomy_type ) {
2317 $taxonomy = get_term_by( 'slug', $object_id, $taxonomy_type );
2318 $tax = get_taxonomy( $taxonomy_type );
2319 if ( !current_user_can( $tax->cap->delete_terms ) )
2320 return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
2321
2322 if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
2323 return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
2324 }
2325
2326 if ( false === term_exists( $object_id, $taxonomy_type ) ) {
2327 return new WP_Error( 'unknown_taxonomy', 'That taxonomy does not exist', 404 );
2328 }
2329
2330 $args = $this->query_args();
2331 $return = $this->get_taxonomy( $taxonomy->slug, $taxonomy_type, $args['context'] );
2332 if ( !$return || is_wp_error( $return ) ) {
2333 return $return;
2334 }
2335
2336 do_action( 'wpcom_json_api_objects', 'taxonomies' );
2337
2338 wp_delete_term( $taxonomy->term_id, $taxonomy_type );
2339
2340 return array(
2341 'slug' => (string) $taxonomy->slug,
2342 'success' => 'true',
2343 );
2344 }
2345 }
2346
2347 abstract class WPCOM_JSON_API_Comment_Endpoint extends WPCOM_JSON_API_Endpoint {
2348 var $comment_object_format = array(
2349 // explicitly document and cast all output
2350 'ID' => '(int) The comment ID.',
2351 'post' => "(object>post_reference) A reference to the comment's post.",
2352 'author' => '(object>author) The author of the comment.',
2353 'date' => "(ISO 8601 datetime) The comment's creation time.",
2354 'URL' => '(URL) The full permalink URL to the comment.',
2355 'short_URL' => '(URL) The wp.me short URL.',
2356 'content' => '(HTML) <code>context</code> dependent.',
2357 'status' => array(
2358 'approved' => 'The comment has been approved.',
2359 'unapproved' => 'The comment has been held for review in the moderation queue.',
2360 'spam' => 'The comment has been marked as spam.',
2361 'trash' => 'The comment is in the trash.',
2362 ),
2363 'parent' => "(object>comment_reference|false) A reference to the comment's parent, if it has one.",
2364 'type' => array(
2365 'comment' => 'The comment is a regular comment.',
2366 'trackback' => 'The comment is a trackback.',
2367 'pingback' => 'The comment is a pingback.',
2368 ),
2369 'meta' => '(object) Meta data',
2370 );
2371
2372 // var $response_format =& $this->comment_object_format;
2373
2374 function __construct( $args ) {
2375 if ( !$this->response_format ) {
2376 $this->response_format =& $this->comment_object_format;
2377 }
2378 parent::__construct( $args );
2379 }
2380
2381 function get_comment( $comment_id, $context ) {
2382 global $blog_id;
2383
2384 $comment = get_comment( $comment_id );
2385 if ( !$comment || is_wp_error( $comment ) ) {
2386 return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
2387 }
2388
2389 $types = array( '', 'comment', 'pingback', 'trackback' );
2390 if ( !in_array( $comment->comment_type, $types ) ) {
2391 return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
2392 }
2393
2394 $post = get_post( $comment->comment_post_ID );
2395 if ( !$post || is_wp_error( $post ) ) {
2396 return new WP_Error( 'unknown_post', 'Unknown post', 404 );
2397 }
2398
2399 $status = wp_get_comment_status( $comment->comment_ID );
2400
2401 // Permissions
2402 switch ( $context ) {
2403 case 'edit' :
2404 if ( !current_user_can( 'edit_comment', $comment->comment_ID ) ) {
2405 return new WP_Error( 'unauthorized', 'User cannot edit comment', 403 );
2406 }
2407
2408 $GLOBALS['post'] = $post;
2409 $comment = get_comment_to_edit( $comment->comment_ID );
2410 break;
2411 case 'display' :
2412 if ( 'approved' !== $status ) {
2413 $current_user_id = get_current_user_id();
2414 $user_can_read_coment = false;
2415 if ( $current_user_id && $comment->user_id && $current_user_id == $comment->user_id ) {
2416 $user_can_read_coment = true;
2417 } elseif (
2418 $comment->comment_author_email && $comment->comment_author
2419 &&
2420 isset( $this->api->token_details['user'] )
2421 &&
2422 $this->api->token_details['user']['user_email'] === $comment->comment_author_email
2423 &&
2424 $this->api->token_details['user']['display_name'] === $comment->comment_author
2425 ) {
2426 $user_can_read_coment = true;
2427 } else {
2428 $user_can_read_coment = current_user_can( 'edit_comment', $comment->comment_ID );
2429 }
2430
2431 if ( !$user_can_read_coment ) {
2432 return new WP_Error( 'unauthorized', 'User cannot read unapproved comment', 403 );
2433 }
2434 }
2435
2436 $GLOBALS['post'] = $post;
2437 setup_postdata( $post );
2438 break;
2439 default :
2440 return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
2441 }
2442
2443 $can_view = $this->user_can_view_post( $post->ID );
2444 if ( !$can_view || is_wp_error( $can_view ) ) {
2445 return $can_view;
2446 }
2447
2448 $GLOBALS['comment'] = $comment;
2449 $response = array();
2450
2451 foreach ( array_keys( $this->comment_object_format ) as $key ) {
2452 switch ( $key ) {
2453 case 'ID' :
2454 // explicitly cast all output
2455 $response[$key] = (int) $comment->comment_ID;
2456 break;
2457 case 'post' :
2458 $response[$key] = (object) array(
2459 'ID' => (int) $post->ID,
2460 'type' => (string) $post->post_type,
2461 'link' => (string) $this->get_post_link( $this->api->get_blog_id_for_output(), $post->ID ),
2462 );
2463 break;
2464 case 'author' :
2465 $response[$key] = (object) $this->get_author( $comment, 'edit' === $context && current_user_can( 'edit_comment', $comment->comment_ID ) );
2466 break;
2467 case 'date' :
2468 $response[$key] = (string) $this->format_date( $comment->comment_date_gmt, $comment->comment_date );
2469 break;
2470 case 'URL' :
2471 $response[$key] = (string) esc_url_raw( get_comment_link( $comment->comment_ID ) );
2472 break;
2473 case 'short_URL' :
2474 // @todo - pagination
2475 $response[$key] = (string) esc_url_raw( wp_get_shortlink( $post->ID ) . "%23comment-{$comment->comment_ID}" );
2476 break;
2477 case 'content' :
2478 if ( 'display' === $context ) {
2479 ob_start();
2480 comment_text();
2481 $response[$key] = (string) ob_get_clean();
2482 } else {
2483 $response[$key] = (string) $comment->comment_content;
2484 }
2485 break;
2486 case 'status' :
2487 $response[$key] = (string) $status;
2488 break;
2489 case 'parent' : // (object|false)
2490 if ( $comment->comment_parent ) {
2491 $parent = get_comment( $comment->comment_parent );
2492 $response[$key] = (object) array(
2493 'ID' => (int) $parent->comment_ID,
2494 'type' => (string) ( $parent->comment_type ? $parent->comment_type : 'comment' ),
2495 'link' => (string) $this->get_comment_link( $blog_id, $parent->comment_ID ),
2496 );
2497 } else {
2498 $response[$key] = false;
2499 }
2500 break;
2501 case 'type' :
2502 $response[$key] = (string) ( $comment->comment_type ? $comment->comment_type : 'comment' );
2503 break;
2504 case 'meta' :
2505 $response[$key] = (object) array(
2506 'links' => (object) array(
2507 'self' => (string) $this->get_comment_link( $this->api->get_blog_id_for_output(), $comment->comment_ID ),
2508 'help' => (string) $this->get_comment_link( $this->api->get_blog_id_for_output(), $comment->comment_ID, 'help' ),
2509 'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),
2510 'post' => (string) $this->get_post_link( $this->api->get_blog_id_for_output(), $comment->comment_post_ID ),
2511 'replies' => (string) $this->get_comment_link( $this->api->get_blog_id_for_output(), $comment->comment_ID, 'replies/' ),
2512 // 'author' => (string) $this->get_user_link( $comment->user_id ),
2513 // 'via' => (string) $this->get_post_link( $ping_origin_blog_id, $ping_origin_post_id ), // Ping/trackbacks
2514 ),
2515 );
2516 break;
2517 }
2518 }
2519
2520 unset( $GLOBALS['comment'], $GLOBALS['post'] );
2521 return $response;
2522 }
2523 }
2524
2525 class WPCOM_JSON_API_Get_Comment_Endpoint extends WPCOM_JSON_API_Comment_Endpoint {
2526 // /sites/%s/comments/%d -> $blog_id, $comment_id
2527 function callback( $path = '', $blog_id = 0, $comment_id = 0 ) {
2528 $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
2529 if ( is_wp_error( $blog_id ) ) {
2530 return $blog_id;
2531 }
2532
2533 $args = $this->query_args();
2534
2535 $return = $this->get_comment( $comment_id, $args['context'] );
2536 if ( !$return || is_wp_error( $return ) ) {
2537 return $return;
2538 }
2539
2540 do_action( 'wpcom_json_api_objects', 'comments' );
2541
2542 return $return;
2543 }
2544 }
2545
2546 // @todo permissions
2547 class WPCOM_JSON_API_List_Comments_Endpoint extends WPCOM_JSON_API_Comment_Endpoint {
2548 var $date_range = array();
2549
2550 var $response_format = array(
2551 'found' => '(int) The total number of comments found that match the request (ignoring limits, offsets, and pagination).',
2552 'comments' => '(array:comment) An array of comment objects.',
2553 );
2554
2555 function __construct( $args ) {
2556 parent::__construct( $args );
2557 $this->query = array_merge( $this->query, array(
2558 'number' => '(int=20) The number of comments to return. Limit: 100.',
2559 'offset' => '(int=0) 0-indexed offset.',
2560 'page' => '(int) Return the Nth 1-indexed page of comments. Takes precedence over the <code>offset</code> parameter.',
2561 'order' => array(
2562 'DESC' => 'Return comments in descending order from newest to oldest.',
2563 'ASC' => 'Return comments in ascending order from oldest to newest.',
2564 ),
2565 'after' => '(ISO 8601 datetime) Return comments dated on or after the specified datetime.',
2566 'before' => '(ISO 8601 datetime) Return comments dated on or before the specified datetime.',
2567 'type' => array(
2568 'any' => 'Return all comments regardless of type.',
2569 'comment' => 'Return only regular comments.',
2570 'trackback' => 'Return only trackbacks.',
2571 'pingback' => 'Return only pingbacks.',
2572 'pings' => 'Return both trackbacks and pingbacks.',
2573 ),
2574 'status' => array(
2575 'approved' => 'Return only approved comments.',
2576 'unapproved' => 'Return only comments in the moderation queue.',
2577 'spam' => 'Return only comments marked as spam.',
2578 'trash' => 'Return only comments in the trash.',
2579 ),
2580 ) );
2581 }
2582
2583 // /sites/%s/comments/ -> $blog_id
2584 // /sites/%s/posts/%d/replies/ -> $blog_id, $post_id
2585 // /sites/%s/comments/%d/replies/ -> $blog_id, $comment_id
2586 function callback( $path = '', $blog_id = 0, $object_id = 0 ) {
2587 $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
2588 if ( is_wp_error( $blog_id ) ) {
2589 return $blog_id;
2590 }
2591
2592 $args = $this->query_args();
2593
2594 if ( $args['number'] < 1 ) {
2595 $args['number'] = 20;
2596 } elseif ( 100 < $args['number'] ) {
2597 return new WP_Error( 'invalid_number', 'The NUMBER parameter must be less than or equal to 100.', 400 );
2598 }
2599
2600 if ( false !== strpos( $path, '/posts/' ) ) {
2601 // We're looking for comments of a particular post
2602 $post_id = $object_id;
2603 $comment_id = 0;
2604 } else {
2605 // We're looking for comments for the whole blog, or replies to a single comment
2606 $comment_id = $object_id;
2607 $post_id = 0;
2608 }
2609
2610 // We can't efficiently get the number of replies to a single comment
2611 $count = false;
2612 $found = -1;
2613
2614 if ( !$comment_id ) {
2615 // We can get comment counts for the whole site or for a single post, but only for certain queries
2616 if ( 'any' === $args['type'] && !isset( $args['after'] ) && !isset( $args['before'] ) ) {
2617 $count = wp_count_comments( $post_id );
2618 }
2619 }
2620
2621 switch ( $args['status'] ) {
2622 case 'approved' :
2623 $status = 'approve';
2624 if ( $count ) {
2625 $found = $count->approved;
2626 }
2627 break;
2628 default :
2629 if ( !current_user_can( 'moderate_comments' ) ) {
2630 return new WP_Error( 'unauthorized', 'User cannot read non-approved comments', 403 );
2631 }
2632 if ( 'unapproved' === $args['status'] ) {
2633 $status = 'hold';
2634 $count_status = 'moderated';
2635 } else {
2636 $status = $count_status = $args['status'];
2637 }
2638 if ( $count ) {
2639 $found = $count->$count_status;
2640 }
2641 }
2642
2643 $query = array(
2644 'number' => $args['number'],
2645 'order' => $args['order'],
2646 'type' => 'any' === $args['type'] ? false : $args['type'],
2647 'status' => $status,
2648 );
2649
2650 if ( $post_id ) {
2651 $post = get_post( $post_id );
2652 if ( !$post || is_wp_error( $post ) ) {
2653 return new WP_Error( 'unknown_post', 'Unknown post', 404 );
2654 }
2655 $query['post_id'] = $post->ID;
2656 if ( $this->api->ends_with( $this->path, '/replies' ) ) {
2657 $query['parent'] = 0;
2658 }
2659 } elseif ( $comment_id ) {
2660 $comment = get_comment( $comment_id );
2661 if ( !$comment || is_wp_error( $comment ) ) {
2662 return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
2663 }
2664 $query['parent'] = $comment_id;
2665 }
2666
2667 if ( isset( $args['page'] ) ) {
2668 if ( $args['page'] < 1 ) {
2669 $args['page'] = 1;
2670 }
2671
2672 $query['offset'] = ( $args['page'] - 1 ) * $args['number'];
2673 } else {
2674 if ( $args['offset'] < 0 ) {
2675 $args['offset'] = 0;
2676 }
2677
2678 $query['offset'] = $args['offset'];
2679 }
2680
2681 if ( isset( $args['before_gmt'] ) ) {
2682 $this->date_range['before_gmt'] = $args['before_gmt'];
2683 }
2684 if ( isset( $args['after_gmt'] ) ) {
2685 $this->date_range['after_gmt'] = $args['after_gmt'];
2686 }
2687
2688 if ( $this->date_range ) {
2689 add_filter( 'comments_clauses', array( $this, 'handle_date_range' ) );
2690 }
2691 $comments = get_comments( $query );
2692 if ( $this->date_range ) {
2693 remove_filter( 'comments_clauses', array( $this, 'handle_date_range' ) );
2694 $this->date_range = array();
2695 }
2696
2697 $return = array();
2698
2699 foreach ( array_keys( $this->response_format ) as $key ) {
2700 switch ( $key ) {
2701 case 'found' :
2702 $return[$key] = (int) $found;
2703 break;
2704 case 'comments' :
2705 $return_comments = array();
2706 foreach ( $comments as $comment ) {
2707 $the_comment = $this->get_comment( $comment->comment_ID, $args['context'] );
2708 if ( $the_comment && !is_wp_error( $the_comment ) ) {
2709 $return_comments[] = $the_comment;
2710 }
2711 }
2712
2713 if ( $return_comments ) {
2714 do_action( 'wpcom_json_api_objects', 'comments', count( $return_comments ) );
2715 }
2716
2717 $return[$key] = $return_comments;
2718 break;
2719 }
2720 }
2721
2722 return $return;
2723 }
2724
2725 function handle_date_range( $clauses ) {
2726 global $wpdb;
2727
2728 switch ( count( $this->date_range ) ) {
2729 case 2 :
2730 $clauses['where'] .= $wpdb->prepare(
2731 " AND `$wpdb->comments`.comment_date_gmt BETWEEN CAST( %s AS DATETIME ) AND CAST( %s AS DATETIME ) ",
2732 $this->date_range['after_gmt'],
2733 $this->date_range['before_gmt']
2734 );
2735 break;
2736 case 1 :
2737 if ( isset( $this->date_range['before_gmt'] ) ) {
2738 $clauses['where'] .= $wpdb->prepare(
2739 " AND `$wpdb->comments`.comment_date_gmt <= CAST( %s AS DATETIME ) ",
2740 $this->date_range['before_gmt']
2741 );
2742 } else {
2743 $clauses['where'] .= $wpdb->prepare(
2744 " AND `$wpdb->comments`.comment_date_gmt >= CAST( %s AS DATETIME ) ",
2745 $this->date_range['after_gmt']
2746 );
2747 }
2748 break;
2749 }
2750
2751 return $clauses;
2752 }
2753 }
2754
2755 class WPCOM_JSON_API_Update_Comment_Endpoint extends WPCOM_JSON_API_Comment_Endpoint {
2756 function __construct( $args ) {
2757 parent::__construct( $args );
2758 if ( $this->api->ends_with( $this->path, '/delete' ) ) {
2759 $this->comment_object_format['status']['deleted'] = 'The comment has been deleted permanently.';
2760 }
2761 }
2762
2763 // /sites/%s/posts/%d/replies/new -> $blog_id, $post_id
2764 // /sites/%s/comments/%d/replies/new -> $blog_id, $comment_id
2765 // /sites/%s/comments/%d -> $blog_id, $comment_id
2766 // /sites/%s/comments/%d/delete -> $blog_id, $comment_id
2767 function callback( $path = '', $blog_id = 0, $object_id = 0 ) {
2768 if ( $this->api->ends_with( $path, '/new' ) )
2769 $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ), false );
2770 else
2771 $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
2772 if ( is_wp_error( $blog_id ) ) {
2773 return $blog_id;
2774 }
2775
2776 if ( $this->api->ends_with( $path, '/delete' ) ) {
2777 return $this->delete_comment( $path, $blog_id, $object_id );
2778 } elseif ( $this->api->ends_with( $path, '/new' ) ) {
2779 if ( false !== strpos( $path, '/posts/' ) ) {
2780 return $this->new_comment( $path, $blog_id, $object_id, 0 );
2781 } else {
2782 return $this->new_comment( $path, $blog_id, 0, $object_id );
2783 }
2784 }
2785
2786 return $this->update_comment( $path, $blog_id, $object_id );
2787 }
2788
2789 // /sites/%s/posts/%d/replies/new -> $blog_id, $post_id
2790 // /sites/%s/comments/%d/replies/new -> $blog_id, $comment_id
2791 function new_comment( $path, $blog_id, $post_id, $comment_parent_id ) {
2792 if ( !$post_id ) {
2793 $comment_parent = get_comment( $comment_parent_id );
2794 if ( !$comment_parent_id || !$comment_parent || is_wp_error( $comment_parent ) ) {
2795 return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
2796 }
2797
2798 $post_id = $comment_parent->comment_post_ID;
2799 }
2800
2801 $post = get_post( $post_id );
2802 if ( !$post || is_wp_error( $post ) ) {
2803 return new WP_Error( 'unknown_post', 'Unknown post', 404 );
2804 }
2805
2806 if ( -1 == get_option( 'blog_public' ) && ! is_user_member_of_blog() && ! is_super_admin() ) {
2807 return new WP_Error( 'unauthorized', 'User cannot create comments', 403 );
2808 }
2809
2810 if ( !comments_open( $post->ID ) ) {
2811 return new WP_Error( 'unauthorized', 'Comments on this post are closed', 403 );
2812 }
2813
2814 $can_view = $this->user_can_view_post( $post->ID );
2815 if ( !$can_view || is_wp_error( $can_view ) ) {
2816 return $can_view;
2817 }
2818
2819 $post_status = get_post_status_object( $post->post_status );
2820 if ( !$post_status->public && !$post_status->private ) {
2821 return new WP_Error( 'unauthorized', 'Comments on drafts are not allowed', 403 );
2822 }
2823
2824 $args = $this->query_args();
2825 $input = $this->input();
2826 if ( !is_array( $input ) || !$input || !strlen( $input['content'] ) ) {
2827 return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
2828 }
2829
2830 $user = wp_get_current_user();
2831 if ( !$user || is_wp_error( $user ) || !$user->ID ) {
2832 $auth_required = false;
2833 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
2834 $auth_required = true;
2835 } elseif ( isset( $this->api->token_details['user'] ) ) {
2836 $user = (object) $this->api->token_details['user'];
2837 foreach ( array( 'display_name', 'user_email', 'user_url' ) as $user_datum ) {
2838 if ( !isset( $user->$user_datum ) ) {
2839 $auth_required = true;
2840 }
2841 }
2842 if ( !isset( $user->ID ) ) {
2843 $user->ID = 0;
2844 }
2845 } else {
2846 $auth_required = true;
2847 }
2848
2849 if ( $auth_required ) {
2850 return new WP_Error( 'authorization_required', 'An active access token must be used to comment.', 403 );
2851 }
2852 }
2853
2854 $insert = array(
2855 'comment_post_ID' => $post->ID,
2856 'user_ID' => $user->ID,
2857 'comment_author' => $user->display_name,
2858 'comment_author_email' => $user->user_email,
2859 'comment_author_url' => $user->user_url,
2860 'comment_content' => $input['content'],
2861 'comment_parent' => $comment_parent_id,
2862 'comment_type' => '',
2863 );
2864
2865 $this->api->trap_wp_die( 'comment_failure' );
2866 $comment_id = wp_new_comment( add_magic_quotes( $insert ) );
2867 $this->api->trap_wp_die( null );
2868
2869 $return = $this->get_comment( $comment_id, $args['context'] );
2870 if ( !$return ) {
2871 return new WP_Error( 400, __( 'Comment cache problem?', 'jetpack' ) );
2872 }
2873 if ( is_wp_error( $return ) ) {
2874 return $return;
2875 }
2876
2877 do_action( 'wpcom_json_api_objects', 'comments' );
2878 return $return;
2879 }
2880
2881 // /sites/%s/comments/%d -> $blog_id, $comment_id
2882 function update_comment( $path, $blog_id, $comment_id ) {
2883 $comment = get_comment( $comment_id );
2884 if ( !$comment || is_wp_error( $comment ) ) {
2885 return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
2886 }
2887
2888 if ( !current_user_can( 'edit_comment', $comment->comment_ID ) ) {
2889 return new WP_Error( 'unauthorized', 'User cannot edit comment', 403 );
2890 }
2891
2892 $args = $this->query_args();
2893 $input = $this->input( false );
2894 if ( !is_array( $input ) || !$input ) {
2895 return new WP_Error( 'invalid_input', 'Invalid request input', 400 );
2896 }
2897
2898 $update = array();
2899 foreach ( $input as $key => $value ) {
2900 $update["comment_$key"] = $value;
2901 }
2902
2903 $comment_status = wp_get_comment_status( $comment->comment_ID );
2904 if ( $comment_status !== $update['status'] && !current_user_can( 'moderate_comments' ) ) {
2905 return new WP_Error( 'unauthorized', 'User cannot moderate comments', 403 );
2906 }
2907
2908 if ( isset( $update['comment_status'] ) ) {
2909 if ( count( $update ) === 1 ) {
2910 // We are only here to update the comment status so let's respond ASAP
2911 add_action( 'wp_set_comment_status', array( $this, 'output_comment' ), 0, 1 );
2912 }
2913 switch ( $update['comment_status'] ) {
2914 case 'approved' :
2915 if ( 'approve' !== $comment_status ) {
2916 wp_set_comment_status( $comment->comment_ID, 'approve' );
2917 }
2918 break;
2919 case 'unapproved' :
2920 if ( 'hold' !== $comment_status ) {
2921 wp_set_comment_status( $comment->comment_ID, 'hold' );
2922 }
2923 break;
2924 case 'spam' :
2925 if ( 'spam' !== $comment_status ) {
2926 wp_spam_comment( $comment->comment_ID );
2927 }
2928 break;
2929 case 'unspam' :
2930 if ( 'spam' === $comment_status ) {
2931 wp_unspam_comment( $comment->comment_ID );
2932 }
2933 break;
2934 case 'trash' :
2935 if ( ! EMPTY_TRASH_DAYS ) {
2936 return new WP_Error( 'trash_disabled', 'Cannot trash comment', 403 );
2937 }
2938
2939 if ( 'trash' !== $comment_status ) {
2940 wp_trash_comment( $comment_id );
2941 }
2942 break;
2943 case 'untrash' :
2944 if ( 'trash' === $comment_status ) {
2945 wp_untrash_comment( $comment->comment_ID );
2946 }
2947 break;
2948 default:
2949 $update['comment_approved'] = 1;
2950 break;
2951 }
2952 unset( $update['comment_status'] );
2953 }
2954
2955 if ( ! empty( $update ) ) {
2956 $update['comment_ID'] = $comment->comment_ID;
2957 wp_update_comment( add_magic_quotes( $update ) );
2958 }
2959
2960 $return = $this->get_comment( $comment->comment_ID, $args['context'] );
2961 if ( !$return || is_wp_error( $return ) ) {
2962 return $return;
2963 }
2964
2965 do_action( 'wpcom_json_api_objects', 'comments' );
2966 return $return;
2967 }
2968
2969 // /sites/%s/comments/%d/delete -> $blog_id, $comment_id
2970 function delete_comment( $path, $blog_id, $comment_id ) {
2971 $comment = get_comment( $comment_id );
2972 if ( !$comment || is_wp_error( $comment ) ) {
2973 return new WP_Error( 'unknown_comment', 'Unknown comment', 404 );
2974 }
2975
2976 if ( !current_user_can( 'edit_comment', $comment->comment_ID ) ) { // [sic] There is no delete_comment cap
2977 return new WP_Error( 'unauthorized', 'User cannot delete comment', 403 );
2978 }
2979
2980 $args = $this->query_args();
2981 $return = $this->get_comment( $comment->comment_ID, $args['context'] );
2982 if ( !$return || is_wp_error( $return ) ) {
2983 return $return;
2984 }
2985
2986 do_action( 'wpcom_json_api_objects', 'comments' );
2987
2988 wp_delete_comment( $comment->comment_ID );
2989 $status = wp_get_comment_status( $comment->comment_ID );
2990 if ( false === $status ) {
2991 $return['status'] = 'deleted';
2992 return $return;
2993 }
2994
2995 return $this->get_comment( $comment->comment_ID, $args['context'] );
2996 }
2997
2998 function output_comment( $comment_id ) {
2999 $args = $this->query_args();
3000 $output = $this->get_comment( $comment_id, $args['context'] );
3001 $this->api->output_early( 200, $output );
3002 }
3003 }
3004
3005 class WPCOM_JSON_API_GET_Site_Endpoint extends WPCOM_JSON_API_Endpoint {
3006 // /sites/mine
3007 // /sites/%s -> $blog_id
3008 function callback( $path = '', $blog_id = 0 ) {
3009 global $wpdb;
3010 if ( 'mine' === $blog_id ) {
3011 $api = WPCOM_JSON_API::init();
3012 if ( !$api->token_details || empty( $api->token_details['blog_id'] ) ) {
3013 return new WP_Error( 'authorization_required', 'An active access token must be used to query information about the current blog.', 403 );
3014 }
3015 $blog_id = $api->token_details['blog_id'];
3016 }
3017
3018 $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
3019 if ( is_wp_error( $blog_id ) ) {
3020 return $blog_id;
3021 }
3022
3023 $is_user_logged_in = is_user_logged_in();
3024
3025 $response = array();
3026 foreach ( array_keys( $this->response_format ) as $key ) {
3027 switch ( $key ) {
3028 case 'ID' :
3029 $response[$key] = (int) $this->api->get_blog_id_for_output();
3030 break;
3031 case 'name' :
3032 $response[$key] = (string) get_bloginfo( 'name' );
3033 break;
3034 case 'description' :
3035 $response[$key] = (string) get_bloginfo( 'description' );
3036 break;
3037 case 'URL' :
3038 $response[$key] = (string) home_url();
3039 break;
3040 case 'jetpack' :
3041 $response[$key] = false; // magic
3042 break;
3043 case 'is_private' :
3044 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
3045 $public_setting = get_option( 'blog_public' );
3046 if ( -1 == $public_setting )
3047 $response[$key] = true;
3048 else
3049 $response[$key] = false;
3050 } else {
3051 $response[$key] = false; // magic
3052 }
3053 break;
3054 case 'post_count' :
3055 if ( $is_user_logged_in )
3056 $response[$key] = (int) $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->posts WHERE post_status = 'publish'");
3057 break;
3058 case 'lang' :
3059 if ( $is_user_logged_in )
3060 $response[$key] = (string) get_bloginfo( 'language' );
3061 break;
3062 case 'subscribers_count' :
3063 if ( function_exists( 'wpcom_subs_total_wpcom_subscribers' ) ) {
3064 $total_wpcom_subs = wpcom_subs_total_wpcom_subscribers(
3065 array(
3066 'blog_id' => $blog_id,
3067 )
3068 );
3069 $response[$key] = $total_wpcom_subs;
3070 } else {
3071 $response[$key] = 0; // magic
3072 }
3073 break;
3074 case 'meta' :
3075 $response[$key] = (object) array(
3076 'links' => (object) array(
3077 'self' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),
3078 'help' => (string) $this->get_site_link( $this->api->get_blog_id_for_output(), 'help' ),
3079 'posts' => (string) $this->get_site_link( $this->api->get_blog_id_for_output(), 'posts/' ),
3080 'comments' => (string) $this->get_site_link( $this->api->get_blog_id_for_output(), 'comments/' ),
3081 ),
3082 );
3083 break;
3084 }
3085 }
3086
3087 do_action( 'wpcom_json_api_objects', 'sites' );
3088
3089 return $response;
3090 }
3091 }
3092
3093
3094
3095 /*
3096 * Set up endpoints
3097 */
3098
3099
3100
3101 /*
3102 * Site endpoints
3103 */
3104 new WPCOM_JSON_API_GET_Site_Endpoint( array(
3105 'description' => 'Information about a site ID/domain',
3106 'group' => 'sites',
3107 'stat' => 'sites:X',
3108
3109 'method' => 'GET',
3110 'path' => '/sites/%s',
3111 'path_labels' => array(
3112 '$site' => '(int|string) The site ID, The site domain',
3113 ),
3114
3115 'query_parameters' => array(
3116 'context' => false,
3117 ),
3118
3119 'response_format' => array(
3120 'ID' => '(int) Blog ID',
3121 'name' => '(string) Title of blog',
3122 'description' => '(string) Tagline or description of blog',
3123 'URL' => '(string) Full URL to the blog',
3124 'jetpack' => '(bool) Whether the blog is a Jetpack blog or not',
3125 'post_count' => '(int) The number of posts the blog has',
3126 'subscribers_count' => '(int) The number of subscribers the blog has',
3127 'lang' => '(string) Primary language code of the blog',
3128 'meta' => '(object) Meta data',
3129 'is_private' => '(bool) If the blog is a private blog or not',
3130 ),
3131
3132 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/?pretty=1',
3133 ) );
3134
3135
3136 /*
3137 * Post endpoints
3138 */
3139 new WPCOM_JSON_API_List_Posts_Endpoint( array(
3140 'description' => 'Return matching Posts',
3141 'group' => 'posts',
3142 'stat' => 'posts',
3143
3144 'method' => 'GET',
3145 'path' => '/sites/%s/posts/',
3146 'path_labels' => array(
3147 '$site' => '(int|string) The site ID, The site domain',
3148 ),
3149
3150 'query_parameters' => array(
3151 'number' => '(int=20) The number of posts to return. Limit: 100.',
3152 'offset' => '(int=0) 0-indexed offset.',
3153 'page' => '(int) Return the Nth 1-indexed page of posts. Takes precedence over the <code>offset</code> parameter.',
3154 'order' => array(
3155 'DESC' => 'Return posts in descending order. For dates, that means newest to oldest.',
3156 'ASC' => 'Return posts in ascending order. For dates, that means oldest to newest.',
3157 ),
3158 'order_by' => array(
3159 'date' => 'Order by the created time of each post.',
3160 'modified' => 'Order by the modified time of each post.',
3161 'title' => "Order lexicographically by the posts' titles.",
3162 'comment_count' => 'Order by the number of comments for each post.',
3163 ),
3164 'after' => '(ISO 8601 datetime) Return posts dated on or after the specified datetime.',
3165 'before' => '(ISO 8601 datetime) Return posts dated on or before the specified datetime.',
3166 'tag' => '(string) Specify the tag name or slug.',
3167 'category' => '(string) Specify the category name or slug.',
3168 '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.",
3169 'status' => array(
3170 'publish' => 'Return only published posts.',
3171 'private' => 'Return only private posts.',
3172 'draft' => 'Return only draft posts.',
3173 'pending' => 'Return only posts pending editorial approval.',
3174 'future' => 'Return only posts scheduled for future publishing.',
3175 'trash' => 'Return only posts in the trash.',
3176 'any' => 'Return all posts regardless of status.',
3177 ),
3178 'sticky' => '(bool) Specify the stickiness.',
3179 'author' => "(int) Author's user ID",
3180 'search' => '(string) Search query',
3181 'meta_key' => '(string) Metadata key that the post should contain',
3182 'meta_value' => '(string) Metadata value that the post should contain. Will only be applied if a `meta_key` is also given',
3183 ),
3184
3185 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/?number=5&pretty=1'
3186 ) );
3187
3188 new WPCOM_JSON_API_Get_Post_Endpoint( array(
3189 'description' => 'Return a single Post (by ID)',
3190 'group' => 'posts',
3191 'stat' => 'posts:1',
3192
3193 'method' => 'GET',
3194 'path' => '/sites/%s/posts/%d',
3195 'path_labels' => array(
3196 '$site' => '(int|string) The site ID, The site domain',
3197 '$post_ID' => '(int) The post ID',
3198 ),
3199
3200 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/7/?pretty=1'
3201 ) );
3202
3203 new WPCOM_JSON_API_Get_Post_Endpoint( array(
3204 'description' => 'Return a single Post (by name)',
3205 'group' => '__do_not_document',
3206 'stat' => 'posts:name',
3207
3208 'method' => 'GET',
3209 'path' => '/sites/%s/posts/name:%s',
3210 'path_labels' => array(
3211 '$site' => '(int|string) The site ID, The site domain',
3212 '$post_name' => '(string) The post name (a.k.a. slug)',
3213 ),
3214
3215 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/name:blogging-and-stuff?pretty=1',
3216 ) );
3217
3218 new WPCOM_JSON_API_Get_Post_Endpoint( array(
3219 'description' => 'Return a single Post (by slug)',
3220 'group' => 'posts',
3221 'stat' => 'posts:slug',
3222
3223 'method' => 'GET',
3224 'path' => '/sites/%s/posts/slug:%s',
3225 'path_labels' => array(
3226 '$site' => '(int|string) The site ID, The site domain',
3227 '$post_slug' => '(string) The post slug (a.k.a. sanitized name)',
3228 ),
3229
3230 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/slug:blogging-and-stuff?pretty=1',
3231 ) );
3232
3233 new WPCOM_JSON_API_Update_Post_Endpoint( array(
3234 'description' => 'Create a Post',
3235 'group' => 'posts',
3236 'stat' => 'posts:new',
3237
3238 'method' => 'POST',
3239 'path' => '/sites/%s/posts/new',
3240 'path_labels' => array(
3241 '$site' => '(int|string) The site ID, The site domain',
3242 ),
3243
3244 'request_format' => array(
3245 // explicitly document all input
3246 'date' => "(ISO 8601 datetime) The post's creation time.",
3247 'title' => '(HTML) The post title.',
3248 'content' => '(HTML) The post content.',
3249 'excerpt' => '(HTML) An optional post excerpt.',
3250 'slug' => '(string) The name (slug) for your post, used in URLs.',
3251 'publicize' => '(array|bool) True or false if the post be publicized to external services. An array of services if we only want to publicize to a select few. Defaults to true.',
3252 'publicize_message' => '(string) Custom message to be publicized to external services.',
3253 'status' => array(
3254 'publish' => 'Publish the post.',
3255 'private' => 'Privately publish the post.',
3256 'draft' => 'Save the post as a draft.',
3257 'pending' => 'Mark the post as pending editorial approval.',
3258 ),
3259 'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.',
3260 'parent' => "(int) The post ID of the new post's parent.",
3261 'type' => "(string) The post type. Defaults to 'post'. Post types besides post and page need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.",
3262 'categories' => "(array|string) Comma separated list or array of categories (name or id)",
3263 'tags' => "(array|string) Comma separated list or array of tags (name or id)",
3264 'format' => get_post_format_strings(),
3265 'media' => "(media) An array of images to attach to the post. To upload media, the entire request should be multipart/form-data encoded. Multiple media items will be displayed in a gallery. Accepts images (image/gif, image/jpeg, image/png) only.<br /><br /><strong>Example</strong>:<br />" .
3266 "<code>curl \<br />--form 'title=Image' \<br />--form 'media[]=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/posts/new'</code>",
3267 'metadata' => "(array) Array of metadata objects containing the following properties: `key` (metadata key), `id` (meta ID), `previous_value` (if set, the action will only occur for the provided previous value), `value` (the new value to set the meta to), `operation` (the operation to perform: `update` or `add`; defaults to `update`). All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are avaiable for authenticated requests with access. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.",
3268 'comments_open' => "(bool) Should the post be open to comments? Defaults to the blog's preference.",
3269 'pings_open' => "(bool) Should the post be open to comments? Defaults to the blog's preference.",
3270 ),
3271
3272 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/posts/new/',
3273
3274 'example_request_data' => array(
3275 'headers' => array(
3276 'authorization' => 'Bearer YOUR_API_TOKEN'
3277 ),
3278
3279 'body' => array(
3280 'title' => 'Hello World',
3281 'content' => 'Hello. I am a test post. I was created by the API',
3282 'tags' => 'tests',
3283 'categories' => 'API'
3284 )
3285 ),
3286
3287 'example_response' => '
3288 {
3289 "ID": 1270,
3290 "author": {
3291 "ID": 18342963,
3292 "email": false,
3293 "name": "binarysmash",
3294 "URL": "http:\/\/binarysmash.wordpress.com",
3295 "avatar_URL": "http:\/\/0.gravatar.com\/avatar\/a178ebb1731d432338e6bb0158720fcc?s=96&d=identicon&r=G",
3296 "profile_URL": "http:\/\/en.gravatar.com\/binarysmash"
3297 },
3298 "date": "2012-04-11T19:42:44+00:00",
3299 "modified": "2012-04-11T19:42:44+00:00",
3300 "title": "Hello World",
3301 "URL": "http:\/\/opossumapi.wordpress.com\/2012\/04\/11\/hello-world-3\/",
3302 "short_URL": "http:\/\/wp.me\/p23HjV-ku",
3303 "content": "<p>Hello. I am a test post. I was created by the API<\/p>\n",
3304 "excerpt": "<p>Hello. I am a test post. I was created by the API<\/p>\n",
3305 "status": "publish",
3306 "password": "",
3307 "parent": false,
3308 "type": "post",
3309 "comments_open": true,
3310 "pings_open": true,
3311 "comment_count": 0,
3312 "like_count": 0,
3313 "i_like": false,
3314 "is_reblogged": false,
3315 "is_following": false,
3316 "featured_image": "",
3317 "format": "standard",
3318 "geo": false,
3319 "publicize_URLs": [
3320
3321 ],
3322 "tags": {
3323 "tests": {
3324 "name": "tests",
3325 "slug": "tests",
3326 "description": "",
3327 "post_count": 1,
3328 "meta": {
3329 "links": {
3330 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/tags\/tests",
3331 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/tags\/tests\/help",
3332 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183"
3333 }
3334 }
3335 }
3336 },
3337 "categories": {
3338 "API": {
3339 "name": "API",
3340 "slug": "api",
3341 "description": "",
3342 "post_count": 1,
3343 "parent": 0,
3344 "meta": {
3345 "links": {
3346 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/categories\/api",
3347 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/categories\/api\/help",
3348 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183"
3349 }
3350 }
3351 }
3352 },
3353 "metadata {
3354 {
3355 "id" : 123,
3356 "key" : "test_meta_key",
3357 "value" : "test_value",
3358 }
3359 },
3360 "meta": {
3361 "links": {
3362 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1270",
3363 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1270\/help",
3364 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183",
3365 "replies": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1270\/replies\/",
3366 "likes": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1270\/likes\/"
3367 }
3368 }
3369 }'
3370 ) );
3371
3372 new WPCOM_JSON_API_Update_Post_Endpoint( array(
3373 'description' => 'Edit a Post',
3374 'group' => 'posts',
3375 'stat' => 'posts:1:POST',
3376
3377 'method' => 'POST',
3378 'path' => '/sites/%s/posts/%d',
3379 'path_labels' => array(
3380 '$site' => '(int|string) The site ID, The site domain',
3381 '$post_ID' => '(int) The post ID',
3382 ),
3383
3384 'request_format' => array(
3385 'date' => "(ISO 8601 datetime) The post's creation time.",
3386 'title' => '(HTML) The post title.',
3387 'content' => '(HTML) The post content.',
3388 'excerpt' => '(HTML) An optional post excerpt.',
3389 'slug' => '(string) The name (slug) for your post, used in URLs.',
3390 'publicize' => '(array|bool) True or false if the post be publicized to external services. An array of services if we only want to publicize to a select few. Defaults to true.',
3391 'publicize_message' => '(string) Custom message to be publicized to external services.',
3392 'status' => array(
3393 'publish' => 'Publish the post.',
3394 'private' => 'Privately publish the post.',
3395 'draft' => 'Save the post as a draft.',
3396 'pending' => 'Mark the post as pending editorial approval.',
3397 ),
3398 'password' => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.',
3399 'parent' => "(int) The post ID of the new post's parent.",
3400 'categories' => "(string) Comma separated list of categories (name or id)",
3401 'tags' => "(string) Comma separated list of tags (name or id)",
3402 'format' => get_post_format_strings(),
3403 'comments_open' => '(bool) Should the post be open to comments?',
3404 'pings_open' => '(bool) Should the post be open to comments?',
3405 'metadata' => "(array) Array of metadata objects containing the following properties: `key` (metadata key), `id` (meta ID), `previous_value` (if set, the action will only occur for the provided previous value), `value` (the new value to set the meta to), `operation` (the operation to perform: `update` or `add`; defaults to `update`). All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are avaiable for authenticated requests with access. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.",
3406 ),
3407
3408 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/posts/1222/',
3409
3410 'example_request_data' => array(
3411 'headers' => array(
3412 'authorization' => 'Bearer YOUR_API_TOKEN'
3413 ),
3414
3415 'body' => array(
3416 'title' => 'Hello World (Again)',
3417 'content' => 'Hello. I am an edited post. I was edited by the API',
3418 'tags' => 'tests',
3419 'categories' => 'API'
3420 )
3421 ),
3422
3423 'example_response' => '
3424 {
3425 "ID": 1222,
3426 "author": {
3427 "ID": 422,
3428 "email": false,
3429 "name": "Justin Shreve",
3430 "URL": "http:\/\/justin.wordpress.com",
3431 "avatar_URL": "http:\/\/1.gravatar.com\/avatar\/9ea5b460afb2859968095ad3afe4804b?s=96&d=identicon&r=G",
3432 "profile_URL": "http:\/\/en.gravatar.com\/justin"
3433 },
3434 "date": "2012-04-11T15:53:52+00:00",
3435 "modified": "2012-04-11T19:44:35+00:00",
3436 "title": "Hello World (Again)",
3437 "URL": "http:\/\/opossumapi.wordpress.com\/2012\/04\/11\/hello-world-2\/",
3438 "short_URL": "http:\/\/wp.me\/p23HjV-jI",
3439 "content": "<p>Hello. I am an edited post. I was edited by the API<\/p>\n",
3440 "excerpt": "<p>Hello. I am an edited post. I was edited by the API<\/p>\n",
3441 "status": "publish",
3442 "password": "",
3443 "parent": false,
3444 "type": "post",
3445 "comments_open": true,
3446 "pings_open": true,
3447 "comment_count": 5,
3448 "like_count": 0,
3449 "i_like": false,
3450 "is_reblogged": false,
3451 "is_following": false,
3452 "featured_image": "",
3453 "format": "standard",
3454 "geo": false,
3455 "publicize_URLs": [
3456
3457 ],
3458 "tags": {
3459 "tests": {
3460 "name": "tests",
3461 "slug": "tests",
3462 "description": "",
3463 "post_count": 2,
3464 "meta": {
3465 "links": {
3466 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/tags\/tests",
3467 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/tags\/tests\/help",
3468 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183"
3469 }
3470 }
3471 }
3472 },
3473 "categories": {
3474 "API": {
3475 "name": "API",
3476 "slug": "api",
3477 "description": "",
3478 "post_count": 2,
3479 "parent": 0,
3480 "meta": {
3481 "links": {
3482 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/categories\/api",
3483 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/categories\/api\/help",
3484 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183"
3485 }
3486 }
3487 }
3488 },
3489 "metadata {
3490 {
3491 "id" : 123,
3492 "key" : "test_meta_key",
3493 "value" : "test_value",
3494 }
3495 },
3496 "meta": {
3497 "links": {
3498 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1222",
3499 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1222\/help",
3500 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183",
3501 "replies": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1222\/replies\/",
3502 "likes": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1222\/likes\/"
3503 }
3504 }
3505 }'
3506
3507 ) );
3508
3509 new WPCOM_JSON_API_Update_Post_Endpoint( array(
3510 'description' => 'Delete a Post. Note: If the post object is of type post or page and the trash is enabled, this request will send the post to the trash. A second request will permanently delete the post.',
3511 'group' => 'posts',
3512 'stat' => 'posts:1:delete',
3513
3514 'method' => 'POST',
3515 'path' => '/sites/%s/posts/%d/delete',
3516 'path_labels' => array(
3517 '$site' => '(int|string) The site ID, The site domain',
3518 '$post_ID' => '(int) The post ID',
3519 ),
3520
3521 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/posts/1222/delete/',
3522
3523 'example_request_data' => array(
3524 'headers' => array(
3525 'authorization' => 'Bearer YOUR_API_TOKEN'
3526 )
3527 ),
3528
3529 'example_response' => '
3530 {
3531 "ID": 1222,
3532 "author": {
3533 "ID": 422,
3534 "email": false,
3535 "name": "Justin Shreve",
3536 "URL": "http:\/\/justin.wordpress.com",
3537 "avatar_URL": "http:\/\/1.gravatar.com\/avatar\/9ea5b460afb2859968095ad3afe4804b?s=96&d=identicon&r=G",
3538 "profile_URL": "http:\/\/en.gravatar.com\/justin"
3539 },
3540 "date": "2012-04-11T15:53:52+00:00",
3541 "modified": "2012-04-11T19:49:42+00:00",
3542 "title": "Hello World (Again)",
3543 "URL": "http:\/\/opossumapi.wordpress.com\/2012\/04\/11\/hello-world-2\/",
3544 "short_URL": "http:\/\/wp.me\/p23HjV-jI",
3545 "content": "<p>Hello. I am an edited post. I was edited by the API<\/p>\n",
3546 "excerpt": "<p>Hello. I am an edited post. I was edited by the API<\/p>\n",
3547 "status": "trash",
3548 "password": "",
3549 "parent": false,
3550 "type": "post",
3551 "comments_open": true,
3552 "pings_open": true,
3553 "comment_count": 5,
3554 "like_count": 0,
3555 "i_like": false,
3556 "is_reblogged": false,
3557 "is_following": false,
3558 "featured_image": "",
3559 "format": "standard",
3560 "geo": false,
3561 "publicize_URLs": [
3562
3563 ],
3564 "tags": {
3565 "tests": {
3566 "name": "tests",
3567 "slug": "tests",
3568 "description": "",
3569 "post_count": 1,
3570 "meta": {
3571 "links": {
3572 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/tags\/tests",
3573 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/tags\/tests\/help",
3574 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183"
3575 }
3576 }
3577 }
3578 },
3579 "metadata {
3580 {
3581 "id" : 123,
3582 "key" : "test_meta_key",
3583 "value" : "test_value",
3584 }
3585 },
3586 "categories": {
3587 "API": {
3588 "name": "API",
3589 "slug": "api",
3590 "description": "",
3591 "post_count": 1,
3592 "parent": 0,
3593 "meta": {
3594 "links": {
3595 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/categories\/api",
3596 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/categories\/api\/help",
3597 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183"
3598 }
3599 }
3600 }
3601 },
3602 "meta": {
3603 "links": {
3604 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1222",
3605 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1222\/help",
3606 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183",
3607 "replies": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1222\/replies\/",
3608 "likes": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1222\/likes\/"
3609 }
3610 }
3611 }'
3612
3613 ) );
3614
3615 /*
3616 * Comment endpoints
3617 */
3618 new WPCOM_JSON_API_List_Comments_Endpoint( array(
3619 'description' => 'Return recent Comments',
3620 'group' => 'comments',
3621 'stat' => 'comments',
3622
3623 'method' => 'GET',
3624 'path' => '/sites/%s/comments/',
3625 'path_labels' => array(
3626 '$site' => '(int|string) The site ID, The site domain',
3627 ),
3628
3629 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comments/?number=5&pretty=1'
3630 ) );
3631
3632 new WPCOM_JSON_API_List_Comments_Endpoint( array(
3633 'description' => 'Return recent Comments for a Post',
3634 'group' => 'comments',
3635 'stat' => 'posts:1:replies',
3636
3637 'method' => 'GET',
3638 'path' => '/sites/%s/posts/%d/replies/',
3639 'path_labels' => array(
3640 '$site' => '(int|string) The site ID, The site domain',
3641 '$post_ID' => '(int) The post ID',
3642 ),
3643
3644 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/7/replies/?number=5&pretty=1'
3645 ) );
3646
3647 new WPCOM_JSON_API_Get_Comment_Endpoint( array(
3648 'description' => 'Return a single Comment',
3649 'group' => 'comments',
3650 'stat' => 'comments:1',
3651
3652 'method' => 'GET',
3653 'path' => '/sites/%s/comments/%d',
3654 'path_labels' => array(
3655 '$site' => '(int|string) The site ID, The site domain',
3656 '$comment_ID' => '(int) The comment ID'
3657 ),
3658
3659 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/comments/11/?pretty=1'
3660 ) );
3661
3662 new WPCOM_JSON_API_Update_Comment_Endpoint( array(
3663 'description' => 'Create a Comment on a Post',
3664 'group' => 'comments',
3665 'stat' => 'posts:1:replies:new',
3666
3667 'method' => 'POST',
3668 'path' => '/sites/%s/posts/%d/replies/new',
3669 'path_labels' => array(
3670 '$site' => '(int|string) The site ID, The site domain',
3671 '$post_ID' => '(int) The post ID'
3672 ),
3673
3674 'request_format' => array(
3675 // explicitly document all input
3676 'content' => '(HTML) The comment text.',
3677 // @todo Should we open this up to unauthenticated requests too?
3678 // 'author' => '(author object) The author of the comment.',
3679 ),
3680
3681 'pass_wpcom_user_details' => true,
3682 'can_use_user_details_instead_of_blog_membership' => true,
3683
3684 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/posts/1222/replies/new/',
3685 'example_request_data' => array(
3686 'headers' => array(
3687 'authorization' => 'Bearer YOUR_API_TOKEN'
3688 ),
3689 'body' => array(
3690 'content' => 'Your reply is very interesting. This is a reply.'
3691 )
3692 ),
3693
3694 'example_response' => '
3695 {
3696 "ID": 9,
3697 "post": {
3698 "ID": 1222,
3699 "type": "post",
3700 "link": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1222"
3701 },
3702 "author": {
3703 "ID": 18342963,
3704 "email": false,
3705 "name": "binarysmash",
3706 "URL": "http:\/\/binarysmash.wordpress.com",
3707 "avatar_URL": "http:\/\/0.gravatar.com\/avatar\/a178ebb1731d432338e6bb0158720fcc?s=96&d=identicon&r=G",
3708 "profile_URL": "http:\/\/en.gravatar.com\/binarysmash"
3709 },
3710 "date": "2012-04-11T18:09:41+00:00",
3711 "URL": "http:\/\/opossumapi.wordpress.com\/2012\/04\/11\/hello-world-2\/#comment-9",
3712 "short_URL": "http:\/\/wp.me\/p23HjV-jI%23comment-9",
3713 "content": "<p>Your reply is very interesting. This is a reply.<\/p>\n",
3714 "status": "approved",
3715 "parent": {
3716 "ID":8,
3717 "type": "comment",
3718 "link": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/8"
3719 },
3720 "type": "comment",
3721 "meta": {
3722 "links": {
3723 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/9",
3724 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/9\/help",
3725 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183",
3726 "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1222",
3727 "replies": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/9\/replies\/"
3728 }
3729 }
3730 }',
3731 ) );
3732
3733 new WPCOM_JSON_API_Update_Comment_Endpoint( array(
3734 'description' => 'Create a Comment as a reply to another Comment',
3735 'group' => 'comments',
3736 'stat' => 'comments:1:replies:new',
3737
3738 'method' => 'POST',
3739 'path' => '/sites/%s/comments/%d/replies/new',
3740 'path_labels' => array(
3741 '$site' => '(int|string) The site ID, The site domain',
3742 '$comment_ID' => '(int) The comment ID'
3743 ),
3744
3745 'request_format' => array(
3746 'content' => '(HTML) The comment text.',
3747 // @todo Should we open this up to unauthenticated requests too?
3748 // 'author' => '(author object) The author of the comment.',
3749 ),
3750
3751 'pass_wpcom_user_details' => true,
3752 'can_use_user_details_instead_of_blog_membership' => true,
3753
3754 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/comments/8/replies/new/',
3755 'example_request_data' => array(
3756 'headers' => array(
3757 'authorization' => 'Bearer YOUR_API_TOKEN'
3758 ),
3759 'body' => array(
3760 'content' => 'This reply is very interesting. This is editing a comment reply via the API.',
3761 )
3762 ),
3763 'example_response' => '
3764 {
3765 "ID": 13,
3766 "post": {
3767 "ID": 1,
3768 "type": "post",
3769 "link": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1"
3770 },
3771 "author": {
3772 "ID": 18342963,
3773 "email": false,
3774 "name": "binarysmash",
3775 "URL": "http:\/\/binarysmash.wordpress.com",
3776 "avatar_URL": "http:\/\/0.gravatar.com\/avatar\/a178ebb1731d432338e6bb0158720fcc?s=96&d=identicon&r=G",
3777 "profile_URL": "http:\/\/en.gravatar.com\/binarysmash"
3778 },
3779 "date": "2012-04-11T20:16:28+00:00",
3780 "URL": "http:\/\/opossumapi.wordpress.com\/2011\/12\/13\/hello-world\/#comment-13",
3781 "short_URL": "http:\/\/wp.me\/p23HjV-1%23comment-13",
3782 "content": "<p>This reply is very interesting. This is editing a comment reply via the API.<\/p>\n",
3783 "status": "approved",
3784 "parent": {
3785 "ID": 1,
3786 "type": "comment",
3787 "link": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/1"
3788 },
3789 "type": "comment",
3790 "meta": {
3791 "links": {
3792 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/13",
3793 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/13\/help",
3794 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183",
3795 "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1",
3796 "replies": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/13\/replies\/"
3797 }
3798 }
3799 }'
3800
3801 ) );
3802
3803 new WPCOM_JSON_API_Update_Comment_Endpoint( array(
3804 'description' => 'Edit a Comment',
3805 'group' => 'comments',
3806 'stat' => 'comments:1:POST',
3807
3808 'method' => 'POST',
3809 'path' => '/sites/%s/comments/%d',
3810 'path_labels' => array(
3811 '$site' => '(int|string) The site ID, The site domain',
3812 '$comment_ID' => '(int) The comment ID'
3813 ),
3814
3815 'request_format' => array(
3816 'date' => "(ISO 8601 datetime) The comment's creation time.",
3817 'content' => '(HTML) The comment text.',
3818 'status' => array(
3819 'approved' => 'Approve the comment.',
3820 'unapproved' => 'Remove the comment from public view and send it to the moderation queue.',
3821 'spam' => 'Mark the comment as spam.',
3822 'unspam' => 'Unmark the comment as spam. Will attempt to set it to the previous status.',
3823 'trash' => 'Send a comment to the trash if trashing is enabled (see constant: EMPTY_TRASH_DAYS).',
3824 'untrash' => 'Untrash a comment. Only works when the comment is in the trash.',
3825 ),
3826 ),
3827
3828 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/comments/8/',
3829 'example_request_data' => array(
3830 'headers' => array(
3831 'authorization' => 'Bearer YOUR_API_TOKEN'
3832 ),
3833 'body' => array(
3834 'content' => 'This reply is now edited via the API.',
3835 'status' => 'approved',
3836 )
3837 ),
3838 'example_response' => '
3839 {
3840 "ID": 13,
3841 "post": {
3842 "ID": 1,
3843 "type": "post",
3844 "link": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1"
3845 },
3846 "author": {
3847 "ID": 18342963,
3848 "email": false,
3849 "name": "binarysmash",
3850 "URL": "http:\/\/binarysmash.wordpress.com",
3851 "avatar_URL": "http:\/\/0.gravatar.com\/avatar\/a178ebb1731d432338e6bb0158720fcc?s=96&d=identicon&r=G",
3852 "profile_URL": "http:\/\/en.gravatar.com\/binarysmash"
3853 },
3854 "date": "2012-04-11T20:16:28+00:00",
3855 "URL": "http:\/\/opossumapi.wordpress.com\/2011\/12\/13\/hello-world\/#comment-13",
3856 "short_URL": "http:\/\/wp.me\/p23HjV-1%23comment-13",
3857 "content": "<p>This reply is very interesting. This is editing a comment reply via the API.<\/p>\n",
3858 "status": "approved",
3859 "parent": {
3860 "ID": 1,
3861 "type": "comment",
3862 "link": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/1"
3863 },
3864 "type": "comment",
3865 "meta": {
3866 "links": {
3867 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/13",
3868 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/13\/help",
3869 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183",
3870 "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1",
3871 "replies": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/13\/replies\/"
3872 }
3873 }
3874 }'
3875
3876 ) );
3877
3878 new WPCOM_JSON_API_Update_Comment_Endpoint( array(
3879 'description' => 'Delete a Comment',
3880 'group' => 'comments',
3881 'stat' => 'comments:1:delete',
3882
3883 'method' => 'POST',
3884 'path' => '/sites/%s/comments/%d/delete',
3885 'path_labels' => array(
3886 '$site' => '(int|string) The site ID, The site domain',
3887 '$comment_ID' => '(int) The comment ID'
3888 ),
3889
3890 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/comments/8/delete/',
3891 'example_request_data' => array(
3892 'headers' => array(
3893 'authorization' => 'Bearer YOUR_API_TOKEN'
3894 )
3895 ),
3896
3897 'example_response' => '
3898 {
3899 "ID": 13,
3900 "post": {
3901 "ID": 1,
3902 "type": "post",
3903 "link": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1"
3904 },
3905 "author": {
3906 "ID": 18342963,
3907 "email": false,
3908 "name": "binarysmash",
3909 "URL": "http:\/\/binarysmash.wordpress.com",
3910 "avatar_URL": "http:\/\/0.gravatar.com\/avatar\/a178ebb1731d432338e6bb0158720fcc?s=96&d=identicon&r=G",
3911 "profile_URL": "http:\/\/en.gravatar.com\/binarysmash"
3912 },
3913 "date": "2012-04-11T20:16:28+00:00",
3914 "URL": "http:\/\/opossumapi.wordpress.com\/2011\/12\/13\/hello-world\/#comment-13",
3915 "short_URL": "http:\/\/wp.me\/p23HjV-1%23comment-13",
3916 "content": "<p>This reply is very interesting. This is editing a comment reply via the API.<\/p>\n",
3917 "status": "deleted",
3918 "parent": {
3919 "ID": 1,
3920 "type": "comment",
3921 "link": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/1"
3922 },
3923 "type": "comment",
3924 "meta": {
3925 "links": {
3926 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/13",
3927 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/13\/help",
3928 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183",
3929 "post": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/posts\/1",
3930 "replies": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/comments\/13\/replies\/"
3931 }
3932 }
3933 }'
3934
3935 ) );
3936
3937 /**
3938 * Taxonomy Management Endpoints
3939 */
3940 new WPCOM_JSON_API_Get_Taxonomy_Endpoint( array(
3941 'description' => 'Returns information on a single Category',
3942 'group' => 'taxonomy',
3943 'stat' => 'categories:1',
3944
3945 'method' => 'GET',
3946 'path' => '/sites/%s/categories/slug:%s',
3947 'path_labels' => array(
3948 '$site' => '(int|string) The site ID, The site domain',
3949 '$category' => '(string) The category slug'
3950 ),
3951
3952 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/categories/slug:community?pretty=1'
3953 ) );
3954
3955 new WPCOM_JSON_API_Get_Taxonomy_Endpoint( array(
3956 'description' => 'Returns information on a single Tag',
3957 'group' => 'taxonomy',
3958 'stat' => 'tags:1',
3959
3960 'method' => 'GET',
3961 'path' => '/sites/%s/tags/slug:%s',
3962 'path_labels' => array(
3963 '$site' => '(int|string) The site ID, The site domain',
3964 '$tag' => '(string) The tag slug'
3965 ),
3966
3967 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/tags/slug:wordpresscom?pretty=1'
3968 ) );
3969
3970 new WPCOM_JSON_API_Update_Taxonomy_Endpoint( array(
3971 'description' => 'Create a new Category',
3972 'group' => 'taxonomy',
3973 'stat' => 'categories:new',
3974
3975 'method' => 'POST',
3976 'path' => '/sites/%s/categories/new',
3977 'path_labels' => array(
3978 '$site' => '(int|string) The site ID, The site domain',
3979 ),
3980
3981 'request_format' => array(
3982 'name' => '(string) Name of the category',
3983 'description' => '(string) A description of the category',
3984 'parent' => '(id) ID of the parent category',
3985 ),
3986
3987 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/categories/new/',
3988 'example_request_data' => array(
3989 'headers' => array(
3990 'authorization' => 'Bearer YOUR_API_TOKEN'
3991 ),
3992 'body' => array(
3993 'name' => 'Puppies',
3994 )
3995 ),
3996 'example_response' => '
3997 {
3998 "name": "Puppies",
3999 "slug": "puppies",
4000 "description": "",
4001 "post_count": 0,
4002 "meta": {
4003 "links": {
4004 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/tags\/puppies",
4005 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/tags\/puppies\/help",
4006 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183"
4007 }
4008 }
4009 }'
4010
4011 ) );
4012
4013 new WPCOM_JSON_API_Update_Taxonomy_Endpoint( array(
4014 'description' => 'Create a new Tag',
4015 'group' => 'taxonomy',
4016 'stat' => 'tags:new',
4017
4018 'method' => 'POST',
4019 'path' => '/sites/%s/tags/new',
4020 'path_labels' => array(
4021 '$site' => '(int|string) The site ID, The site domain',
4022 ),
4023
4024 'request_format' => array(
4025 'name' => '(string) Name of the tag',
4026 'description' => '(string) A description of the tag',
4027 ),
4028
4029 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/tags/new/',
4030 'example_request_data' => array(
4031 'headers' => array(
4032 'authorization' => 'Bearer YOUR_API_TOKEN'
4033 ),
4034 'body' => array(
4035 'name' => 'Kitties'
4036 )
4037 ),
4038 'example_response' => '
4039 {
4040 "name": "Kitties",
4041 "slug": "kitties",
4042 "description": "",
4043 "post_count": 0,
4044 "meta": {
4045 "links": {
4046 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/tags\/kitties",
4047 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/tags\/kitties\/help",
4048 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183"
4049 }
4050 }
4051 }'
4052
4053 ) );
4054
4055 new WPCOM_JSON_API_Update_Taxonomy_Endpoint( array(
4056 'description' => 'Edit a Tag',
4057 'group' => 'taxonomy',
4058 'stat' => 'tags:1:POST',
4059
4060 'method' => 'POST',
4061 'path' => '/sites/%s/tags/slug:%s',
4062 'path_labels' => array(
4063 '$site' => '(int|string) The site ID, The site domain',
4064 '$tag' => '(string) The tag slug',
4065 ),
4066
4067 'request_format' => array(
4068 'name' => '(string) Name of the tag',
4069 'description' => '(string) A description of the tag',
4070 ),
4071
4072 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/tags/slug:testing-tag',
4073 'example_request_data' => array(
4074 'headers' => array(
4075 'authorization' => 'Bearer YOUR_API_TOKEN'
4076 ),
4077 'body' => array(
4078 'description' => 'Kitties are awesome!'
4079 )
4080 ),
4081 'example_response' => '
4082 {
4083 "name": "testing tag",
4084 "slug": "testing-tag",
4085 "description": "Kitties are awesome!",
4086 "post_count": 0,
4087 "meta": {
4088 "links": {
4089 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/tags\/testing-tag",
4090 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/tags\/testing-tag\/help",
4091 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183"
4092 }
4093 }
4094 }'
4095
4096 ) );
4097
4098 new WPCOM_JSON_API_Update_Taxonomy_Endpoint( array(
4099 'description' => 'Edit a Category',
4100 'group' => 'taxonomy',
4101 'stat' => 'categories:1:POST',
4102
4103 'method' => 'POST',
4104 'path' => '/sites/%s/categories/slug:%s',
4105 'path_labels' => array(
4106 '$site' => '(int|string) The site ID, The site domain',
4107 '$category' => '(string) The category slug',
4108 ),
4109
4110 'request_format' => array(
4111 'name' => '(string) Name of the category',
4112 'description' => '(string) A description of the category',
4113 'parent' => '(id) ID of the parent category',
4114 ),
4115
4116 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/categories/slug:testing-category',
4117 'example_request_data' => array(
4118 'headers' => array(
4119 'authorization' => 'Bearer YOUR_API_TOKEN'
4120 ),
4121 'body' => array(
4122 'description' => 'Puppies are great!'
4123 )
4124 ),
4125 'example_response' => '
4126 {
4127 "name": "testing category",
4128 "slug": "testing-category",
4129 "description": "Puppies are great!",
4130 "post_count": 0,
4131 "parent": 0,
4132 "meta": {
4133 "links": {
4134 "self": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/categories\/testing-category",
4135 "help": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183\/categories\/testing-category\/help",
4136 "site": "https:\/\/public-api.wordpress.com\/rest\/v1\/sites\/30434183"
4137 }
4138 }
4139 }'
4140
4141 ) );
4142
4143 new WPCOM_JSON_API_Update_Taxonomy_Endpoint( array(
4144 'description' => 'Delete a Category',
4145 'group' => 'taxonomy',
4146 'stat' => 'categories:1:delete',
4147
4148 'method' => 'POST',
4149 'path' => '/sites/%s/categories/slug:%s/delete',
4150 'path_labels' => array(
4151 '$site' => '(int|string) The site ID, The site domain',
4152 '$category' => '(string) The category slug',
4153 ),
4154 'response_format' => array(
4155 'slug' => '(string) The slug of the deleted category',
4156 'success' => '(bool) Was the operation successful?',
4157 ),
4158
4159 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/categories/slug:some-category-name/delete',
4160 'example_request_data' => array(
4161 'headers' => array(
4162 'authorization' => 'Bearer YOUR_API_TOKEN'
4163 ),
4164 ),
4165 'example_response' => '{
4166 "slug": "some-category-name",
4167 "success": "true"
4168 }'
4169 ) );
4170
4171 new WPCOM_JSON_API_Update_Taxonomy_Endpoint( array(
4172 'description' => 'Delete a Tag',
4173 'group' => 'taxonomy',
4174 'stat' => 'tags:1:delete',
4175
4176 'method' => 'POST',
4177 'path' => '/sites/%s/tags/slug:%s/delete',
4178 'path_labels' => array(
4179 '$site' => '(int|string) The site ID, The site domain',
4180 '$tag' => '(string) The tag slug',
4181 ),
4182 'response_format' => array(
4183 'slug' => '(string) The slug of the deleted tag',
4184 'success' => '(bool) Was the operation successful?',
4185 ),
4186
4187 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/30434183/tags/slug:some-tag-name/delete',
4188 'example_request_data' => array(
4189 'headers' => array(
4190 'authorization' => 'Bearer YOUR_API_TOKEN'
4191 ),
4192 ),
4193 'example_response' => '{
4194 "slug": "some-tag-name",
4195 "success": "true"
4196 }'
4197 ) );
4198