PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 3.1.5
Jetpack – WP Security, Backup, Speed, & Growth v3.1.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
1,296 lines 39.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 define( 'WPCOM_JSON_API__CURRENT_VERSION', '1' );
4
5 // Endpoint
6 abstract class WPCOM_JSON_API_Endpoint {
7 // The API Object
8 var $api;
9
10 var $pass_wpcom_user_details = false;
11 var $can_use_user_details_instead_of_blog_membership = false;
12
13 // One liner.
14 var $description;
15
16 // Object Grouping For Documentation (Users, Posts, Comments)
17 var $group;
18
19 // Stats extra value to bump
20 var $stat;
21
22 // HTTP Method
23 var $method = 'GET';
24
25 // Minimum version of the api for which to serve this endpoint
26 var $min_version = '0';
27
28 // Maximum version of the api for which to serve this endpoint
29 var $max_version = WPCOM_JSON_API__CURRENT_VERSION;
30
31 // Path at which to serve this endpoint: sprintf() format.
32 var $path = '';
33
34 // Identifiers to fill sprintf() formatted $path
35 var $path_labels = array();
36
37 // Accepted query parameters
38 var $query = array(
39 // Parameter name
40 'context' => array(
41 // Default value => description
42 'display' => 'Formats the output as HTML for display. Shortcodes are parsed, paragraph tags are added, etc..',
43 // Other possible values => description
44 'edit' => 'Formats the output for editing. Shortcodes are left unparsed, significant whitespace is kept, etc..',
45 ),
46 'http_envelope' => array(
47 'false' => '',
48 '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.',
49 ),
50 'pretty' => array(
51 'false' => '',
52 'true' => 'Output pretty JSON',
53 ),
54 'meta' => "(string) Optional. Loads data from the endpoints found in the 'meta' part of the response. Comma separated list. Example: meta=site,likes",
55 'fields' => '(string) Optional. Returns specified fields only. Comma separated list. Example: fields=ID,title',
56 // Parameter name => description (default value is empty)
57 'callback' => '(string) An optional JSONP callback function.',
58 );
59
60 // Response format
61 var $response_format = array();
62
63 // Request format
64 var $request_format = array();
65
66 // Is this endpoint still in testing phase? If so, not available to the public.
67 var $in_testing = false;
68
69 /**
70 * @var string Version of the API
71 */
72 var $version = '';
73
74 /**
75 * @var string Example request to make
76 */
77 var $example_request = '';
78
79 /**
80 * @var string Example request data (for POST methods)
81 */
82 var $example_request_data = '';
83
84 /**
85 * @var string Example response from $example_request
86 */
87 var $example_response = '';
88
89 /**
90 * @var bool Set to true if the endpoint implements its own filtering instead of the standard `fields` query method
91 */
92 var $custom_fields_filtering = false;
93
94 /**
95 * @var bool Set to true if the endpoint accepts all cross origin requests
96 * You probably should not set this flag. If you are thinking of setting it,
97 * then discuss it with someone:
98 * http://operationapi.wordpress.com/2014/06/25/patch-allowing-endpoints-to-do-cross-origin-requests/
99 */
100 var $allow_cross_origin_request = false;
101
102 function __construct( $args ) {
103 $defaults = array(
104 'in_testing' => false,
105 'description' => '',
106 'group' => '',
107 'method' => 'GET',
108 'path' => '/',
109 'min_version' => '0',
110 'max_version' => WPCOM_JSON_API__CURRENT_VERSION,
111 'force' => '',
112 'jp_disabled' => false,
113 'path_labels' => array(),
114 'request_format' => array(),
115 'response_format' => array(),
116 'query_parameters' => array(),
117 'version' => 'v1',
118 'example_request' => '',
119 'example_request_data' => '',
120 'example_response' => '',
121 'required_scope' => '',
122 'pass_wpcom_user_details' => false,
123 'can_use_user_details_instead_of_blog_membership' => false,
124 'custom_fields_filtering' => false,
125 'allow_cross_origin_request' => false,
126 );
127
128 $args = wp_parse_args( $args, $defaults );
129
130 $this->in_testing = $args['in_testing'];
131
132 $this->description = $args['description'];
133 $this->group = $args['group'];
134 $this->stat = $args['stat'];
135 $this->force = $args['force'];
136 $this->jp_disabled = $args['jp_disabled'];
137
138 $this->method = $args['method'];
139 $this->path = $args['path'];
140 $this->path_labels = $args['path_labels'];
141 $this->min_version = $args['min_version'];
142 $this->max_version = $args['max_version'];
143
144 $this->pass_wpcom_user_details = $args['pass_wpcom_user_details'];
145 $this->custom_fields_filtering = (bool) $args['custom_fields_filtering'];
146 $this->can_use_user_details_instead_of_blog_membership = $args['can_use_user_details_instead_of_blog_membership'];
147
148 $this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request'];
149
150 $this->version = $args['version'];
151
152 $this->required_scope = $args['required_scope'];
153
154 if ( $this->request_format ) {
155 $this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) );
156 } else {
157 $this->request_format = $args['request_format'];
158 }
159
160 if ( $this->response_format ) {
161 $this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) );
162 } else {
163 $this->response_format = $args['response_format'];
164 }
165
166 if ( false === $args['query_parameters'] ) {
167 $this->query = array();
168 } elseif ( is_array( $args['query_parameters'] ) ) {
169 $this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) );
170 }
171
172 $this->api = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API
173
174 /** Example Request/Response ******************************************/
175
176 // Examples for endpoint documentation request
177 $this->example_request = $args['example_request'];
178 $this->example_request_data = $args['example_request_data'];
179 $this->example_response = $args['example_response'];
180
181 $this->api->add( $this );
182 }
183
184 // Get all query args. Prefill with defaults
185 function query_args( $return_default_values = true, $cast_and_filter = true ) {
186 $args = array_intersect_key( $this->api->query, $this->query );
187
188 if ( !$cast_and_filter ) {
189 return $args;
190 }
191
192 return $this->cast_and_filter( $args, $this->query, $return_default_values );
193 }
194
195 // Get POST body data
196 function input( $return_default_values = true, $cast_and_filter = true ) {
197 $input = trim( $this->api->post_body );
198 $content_type = $this->api->content_type;
199 if ( $content_type ) {
200 list ( $content_type ) = explode( ';', $content_type );
201 }
202 $content_type = trim( $content_type );
203 switch ( $content_type ) {
204 case 'application/json' :
205 case 'application/x-javascript' :
206 case 'text/javascript' :
207 case 'text/x-javascript' :
208 case 'text/x-json' :
209 case 'text/json' :
210 $return = json_decode( $input, true );
211
212 if ( function_exists( 'json_last_error' ) ) {
213 if ( JSON_ERROR_NONE !== json_last_error() ) {
214 return null;
215 }
216 } else {
217 if ( is_null( $return ) && json_encode( null ) !== $input ) {
218 return null;
219 }
220 }
221
222 break;
223 case 'multipart/form-data' :
224 $return = array_merge( stripslashes_deep( $_POST ), $_FILES );
225 break;
226 case 'application/x-www-form-urlencoded' :
227 //attempt JSON first, since probably a curl command
228 $return = json_decode( $input, true );
229
230 if ( is_null( $return ) ) {
231 wp_parse_str( $input, $return );
232 }
233
234 break;
235 default :
236 wp_parse_str( $input, $return );
237 break;
238 }
239
240 if ( !$cast_and_filter ) {
241 return $return;
242 }
243
244 return $this->cast_and_filter( $return, $this->request_format, $return_default_values );
245 }
246
247 function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) {
248 $return_as_object = false;
249 if ( is_object( $data ) ) {
250 // @todo this should probably be a deep copy if $data can ever have nested objects
251 $data = (array) $data;
252 $return_as_object = true;
253 } elseif ( !is_array( $data ) ) {
254 return $data;
255 }
256
257 $boolean_arg = array( 'false', 'true' );
258 $naeloob_arg = array( 'true', 'false' );
259
260 $return = array();
261
262 foreach ( $documentation as $key => $description ) {
263 if ( is_array( $description ) ) {
264 // String or boolean array keys only
265 $whitelist = array_keys( $description );
266 if ( isset( $data[$key] ) && isset( $description[$data[$key]] ) ) {
267 $return[$key] = (string) $data[$key];
268 } elseif ( $return_default_values ) {
269 $return[$key] = (string) current( $whitelist );
270 } else {
271 continue;
272 }
273
274 // Truthiness
275 if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) {
276 $return[$key] = (bool) WPCOM_JSON_API::is_truthy( $return[$key] );
277 }
278
279 continue;
280 }
281
282 $types = $this->parse_types( $description );
283 $type = array_shift( $types );
284
285 // Explicit default - string and int only for now. Always set these reguardless of $return_default_values
286 if ( isset( $type['default'] ) ) {
287 if ( !isset( $data[$key] ) ) {
288 $data[$key] = $type['default'];
289 }
290 }
291
292 if ( !isset( $data[$key] ) ) {
293 continue;
294 }
295
296 $this->cast_and_filter_item( $return, $type, $key, $data[$key], $types, $for_output );
297 }
298
299 if ( $return_as_object ) {
300 return (object) $return;
301 }
302
303 return $return;
304 }
305
306 /**
307 * Casts $value according to $type.
308 * Handles fallbacks for certain values of $type when $value is not that $type
309 * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way)
310 *
311 * Handles "child types" - array:URL, object:category
312 * array:URL means an array of URLs
313 * object:category means a hash of categories
314 *
315 * Handles object typing - object>post means an object of type post
316 */
317 function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) {
318 if ( is_string( $type ) ) {
319 $type = compact( 'type' );
320 }
321
322 switch ( $type['type'] ) {
323 case 'false' :
324 $return[$key] = false;
325 break;
326 case 'url' :
327 $return[$key] = (string) esc_url_raw( $value );
328 break;
329 case 'string' :
330 // Fallback string -> array
331 if ( is_array( $value ) ) {
332 if ( !empty( $types[0] ) ) {
333 $next_type = array_shift( $types );
334 return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
335 }
336 }
337
338 // Fallback string -> false
339 if ( !is_string( $value ) ) {
340 if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
341 $next_type = array_shift( $types );
342 return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
343 }
344 }
345 $return[$key] = (string) $value;
346 break;
347 case 'html' :
348 $return[$key] = (string) $value;
349 break;
350 case 'safehtml' :
351 $return[$key] = wp_kses( (string) $value, wp_kses_allowed_html() );
352 break;
353 case 'media' :
354 if ( is_array( $value ) ) {
355 if ( isset( $value['name'] ) ) {
356 // It's a $_FILES array
357 // Reformat into array of $_FILES items
358
359 $files = array();
360 foreach ( $value['name'] as $k => $v ) {
361 $files[$k] = array();
362 foreach ( array_keys( $value ) as $file_key ) {
363 $files[$k][$file_key] = $value[$file_key][$k];
364 }
365 }
366
367 foreach ( $files as $k => $file ) {
368 if ( ! isset( $file['tmp_name'] ) || ! is_string( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) {
369 unset( $files[ $k ] );
370 }
371 }
372 if ( $files ) {
373 $return[$key] = $files;
374 }
375 } elseif ( isset( $value['tmp_name'] ) && is_string( $value['tmp_name'] ) && is_uploaded_file( $value['tmp_name'] ) ) {
376 $return[ $key ] = $value;
377 }
378 }
379 break;
380 case 'array' :
381 // Fallback array -> string
382 if ( is_string( $value ) ) {
383 if ( !empty( $types[0] ) ) {
384 $next_type = array_shift( $types );
385 return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
386 }
387 }
388
389 if ( isset( $type['children'] ) ) {
390 $children = array();
391 foreach ( (array) $value as $k => $child ) {
392 $this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
393 }
394 $return[$key] = (array) $children;
395 break;
396 }
397
398 $return[$key] = (array) $value;
399 break;
400 case 'iso 8601 datetime' :
401 case 'datetime' :
402 // (string)s
403 $dates = $this->parse_date( (string) $value );
404 if ( $for_output ) {
405 $return[$key] = $this->format_date( $dates[1], $dates[0] );
406 } else {
407 list( $return[$key], $return["{$key}_gmt"] ) = $dates;
408 }
409 break;
410 case 'float' :
411 $return[$key] = (float) $value;
412 break;
413 case 'int' :
414 case 'integer' :
415 $return[$key] = (int) $value;
416 break;
417 case 'bool' :
418 case 'boolean' :
419 $return[$key] = (bool) WPCOM_JSON_API::is_truthy( $value );
420 break;
421 case 'object' :
422 // Fallback object -> false
423 if ( is_scalar( $value ) || is_null( $value ) ) {
424 if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
425 return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output );
426 }
427 }
428
429 if ( isset( $type['children'] ) ) {
430 $children = array();
431 foreach ( (array) $value as $k => $child ) {
432 $this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
433 }
434 $return[$key] = (object) $children;
435 break;
436 }
437
438 if ( isset( $type['subtype'] ) ) {
439 return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output );
440 }
441
442 $return[$key] = (object) $value;
443 break;
444 case 'post' :
445 $return[$key] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output );
446 break;
447 case 'comment' :
448 $return[$key] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output );
449 break;
450 case 'tag' :
451 case 'category' :
452 $docs = array(
453 'name' => '(string)',
454 'slug' => '(string)',
455 'description' => '(HTML)',
456 'post_count' => '(int)',
457 'meta' => '(object)',
458 );
459 if ( 'category' === $type ) {
460 $docs['parent'] = '(int)';
461 }
462 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
463 break;
464 case 'post_reference' :
465 case 'comment_reference' :
466 $docs = array(
467 'ID' => '(int)',
468 'type' => '(string)',
469 'title' => '(string)',
470 'link' => '(URL)',
471 );
472 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
473 break;
474 case 'geo' :
475 $docs = array(
476 'latitude' => '(float)',
477 'longitude' => '(float)',
478 'address' => '(string)',
479 );
480 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
481 break;
482 case 'author' :
483 $docs = array(
484 'ID' => '(int)',
485 'user_login' => '(string)',
486 'email' => '(string|false)',
487 'name' => '(string)',
488 'URL' => '(URL)',
489 'avatar_URL' => '(URL)',
490 'profile_URL' => '(URL)',
491 );
492 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
493 break;
494 case 'attachment' :
495 $docs = array(
496 'ID' => '(int)',
497 'URL' => '(URL)',
498 'guid' => '(string)',
499 'mime_type' => '(string)',
500 'width' => '(int)',
501 'height' => '(int)',
502 'duration' => '(int)',
503 );
504 $return[$key] = (object) $this->cast_and_filter( $value, apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ), false, $for_output );
505 break;
506 case 'metadata' :
507 $docs = array(
508 'id' => '(int)',
509 'key' => '(string)',
510 'value' => '(string|false|float|int|array|object)',
511 'previous_value' => '(string)',
512 'operation' => '(string)',
513 );
514 $return[$key] = (object) $this->cast_and_filter( $value, apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ), false, $for_output );
515 break;
516 case 'plugin' :
517 $docs = array(
518 'id' => '(string) The plugin\'s ID',
519 'active' => '(boolean) The plugin status.',
520 'update' => '(object) The plugin update info.',
521 'name' => '(string) The name of the plugin.',
522 'plugin_url' => '(url) Link to the plugin\'s web site.',
523 'version' => '(string) The plugin version number.',
524 'description' => '(safehtml) Description of what the plugin does and/or notes from the author',
525 'author' => '(string) The plugin author\'s name',
526 'author_url' => '(url) The plugin author web site address',
527 'network' => '(boolean) Whether the plugin can only be activated network wide.',
528 );
529 $return[$key] = (object) $this->cast_and_filter( $value, apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ), false, $for_output );
530 break;
531
532 default :
533 trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
534 }
535 }
536
537 function parse_types( $text ) {
538 if ( !preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
539 return 'none';
540 }
541
542 $types = explode( '|', strtolower( $matches[1] ) );
543 $return = array();
544 foreach ( $types as $type ) {
545 foreach ( array( ':' => 'children', '>' => 'subtype', '=' => 'default' ) as $operator => $meaning ) {
546 if ( false !== strpos( $type, $operator ) ) {
547 $item = explode( $operator, $type, 2 );
548 $return[] = array( 'type' => $item[0], $meaning => $item[1] );
549 continue 2;
550 }
551 }
552 $return[] = compact( 'type' );
553 }
554
555 return $return;
556 }
557
558 /**
559 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
560 * Echoes HTML.
561 */
562 function document( $show_description = true ) {
563 $original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
564 unset( $GLOBALS['post'] );
565
566 $doc = $this->generate_documentation();
567
568 if ( $show_description ) :
569 ?>
570 <caption>
571 <h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
572 <p><?php echo wp_kses_post( $doc['description'] ); ?></p>
573 </caption>
574
575 <?php endif; ?>
576
577 <section class="resource-url">
578 <h2 id="apidoc-resource-url">Resource URL</h2>
579 <table class="api-doc api-doc-resource-parameters api-doc-resource">
580 <thead>
581 <tr>
582 <th class="api-index-title" scope="column">Type</th>
583 <th class="api-index-title" scope="column">URL and Format</th>
584 </tr>
585 </thead>
586 <tbody>
587 <tr class="api-index-item">
588 <th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></th>
589 <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>
590 </tr>
591 </tbody>
592 </table>
593 </section>
594
595 <?php
596
597 foreach ( array(
598 'path' => 'Method Parameters',
599 'query' => 'Query Parameters',
600 'body' => 'Request Parameters',
601 'response' => 'Response Parameters',
602 ) as $doc_section_key => $label ) :
603 $doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][$doc_section_key];
604 if ( !$doc_section ) {
605 continue;
606 }
607
608 $param_label = strtolower( str_replace( ' ', '-', $label ) );
609 ?>
610
611 <section class="<?php echo $param_label; ?>">
612
613 <h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
614
615 <table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
616
617 <thead>
618 <tr>
619 <th class="api-index-title" scope="column">Parameter</th>
620 <th class="api-index-title" scope="column">Type</th>
621 <th class="api-index-title" scope="column">Description</th>
622 </tr>
623 </thead>
624 <tbody>
625
626 <?php foreach ( $doc_section as $key => $item ) : ?>
627
628 <tr class="api-index-item">
629 <th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
630 <td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
631 <td class="description api-index-item-body"><?php
632
633 $this->generate_doc_description( $item['description'] );
634
635 ?></td>
636 </tr>
637
638 <?php endforeach; ?>
639 </tbody>
640 </table>
641 </section>
642 <?php endforeach; ?>
643
644 <?php
645 // If no example was hardcoded in the doc, try to get some
646 if ( empty( $this->example_response ) ) {
647
648 // Examples for endpoint documentation response
649 $response_key = 'dev_example_response_' . $this->version . '_' . $this->method . '_' . sanitize_key( $this->path );
650 $response_body = wp_cache_get( $response_key );
651
652 // Response doesn't exist, so run the request
653 if ( false === $response_body ) {
654
655 // Only trust GET request
656 if ( 'GET' === $this->method ) {
657 $response = wp_remote_get( $this->example_request );
658 $response_body = wp_remote_retrieve_body( $response );
659
660 // Only cache if there's a result
661 if ( ! is_wp_error( $response ) && strlen( $response_body ) ) {
662 wp_cache_set( $response_key, $response_body );
663 } else {
664 wp_cache_delete( $response_key );
665 }
666 }
667 }
668
669 // Example response was passed into the constructor via params
670 } else {
671 $response_body = $this->example_response;
672 }
673
674 // Wrap the response in a sourcecode shortcode
675 if ( !empty( $response_body ) && !is_wp_error( $response ) ) {
676 $response_body = '[sourcecode language="javascript" wraplines="false" light="true" autolink="false" htmlscript="false"]' . $response_body . '[/sourcecode]';
677 $response_body = apply_filters( 'the_content', $response_body );
678 $this->example_response = $response_body;
679 }
680
681 $curl = 'curl';
682
683 $php_opts = array( 'ignore_errors' => true );
684
685 if ( 'GET' !== $this->method ) {
686 $php_opts['method'] = $this->method;
687 }
688
689 if ( $this->example_request_data ) {
690 if ( isset( $this->example_request_data['headers'] ) && is_array( $this->example_request_data['headers'] ) ) {
691 $php_opts['header'] = array();
692 foreach ( $this->example_request_data['headers'] as $header => $value ) {
693 $curl .= " \\\n -H " . escapeshellarg( "$header: $value" );
694 $php_opts['header'][] = "$header: $value";
695 }
696 }
697
698 if ( isset( $this->example_request_data['body'] ) && is_array( $this->example_request_data['body'] ) ) {
699 $php_opts['content'] = $this->example_request_data['body'];
700 $php_opts['header'][] = 'Content-Type: application/x-www-form-urlencoded';
701 foreach ( $this->example_request_data['body'] as $key => $value ) {
702 $curl .= " \\\n --data-urlencode " . escapeshellarg( "$key=$value" );
703 }
704 }
705 }
706
707 if ( $php_opts ) {
708 $php_opts_exported = var_export( array( 'http' => $php_opts ), true );
709 if ( !empty( $php_opts['content'] ) ) {
710 $content_exported = preg_quote( var_export( $php_opts['content'], true ), '/' );
711 $content_exported = '\\s*' . str_replace( "\n", "\n\\s*", $content_exported ) . '\\s*';
712 $php_opts_exported = preg_replace_callback( "/$content_exported/", array( $this, 'add_http_build_query_to_php_content_example' ), $php_opts_exported );
713 }
714 $php = <<<EOPHP
715 <?php
716
717 \$options = $php_opts_exported;
718
719 \$context = stream_context_create( \$options );
720 \$response = file_get_contents(
721 '$this->example_request',
722 false,
723 \$context
724 );
725 \$response = json_decode( \$response );
726
727 ?>
728 EOPHP;
729 } else {
730 $php = <<<EOPHP
731 <?php
732
733 \$response = file_get_contents( '$this->example_request' );
734 \$response = json_decode( \$response );
735
736 ?>
737 EOPHP;
738 }
739
740 if ( false !== strpos( $curl, "\n" ) ) {
741 $curl .= " \\\n";
742 }
743
744 $curl .= ' ' . escapeshellarg( $this->example_request );
745
746 $curl = '[sourcecode language="bash" wraplines="false" light="true" autolink="false" htmlscript="false"]' . $curl . '[/sourcecode]';
747 $curl = apply_filters( 'the_content', $curl );
748
749 $php = '[sourcecode language="php" wraplines="false" light="true" autolink="false" htmlscript="false"]' . $php . '[/sourcecode]';
750 $php = apply_filters( 'the_content', $php );
751 ?>
752
753 <?php if ( ! empty( $this->example_request ) || ! empty( $this->example_request_data ) || ! empty( $this->example_response ) ) : ?>
754
755 <section class="example-response">
756 <h2 id="apidoc-example">Example</h2>
757
758 <section>
759 <h3>cURL</h3>
760 <?php echo wp_kses_post( $curl ); ?>
761 </section>
762
763 <section>
764 <h3>PHP</h3>
765 <?php echo wp_kses_post( $php ); ?>
766 </section>
767
768 <?php if ( ! empty( $this->example_response ) ) : ?>
769
770 <section>
771 <h3>Response Body</h3>
772 <?php echo $this->example_response; ?>
773 </section>
774
775 <?php endif; ?>
776
777 </section>
778
779 <?php endif; ?>
780
781 <?php
782 if ( 'unset' !== $original_post ) {
783 $GLOBALS['post'] = $original_post;
784 }
785 }
786
787 function add_http_build_query_to_php_content_example( $matches ) {
788 $trimmed_match = ltrim( $matches[0] );
789 $pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
790 $pad = ltrim( $pad, ' ' );
791 $return = ' ' . str_replace( "\n", "\n ", $matches[0] );
792 return " http_build_query({$return}{$pad})";
793 }
794
795 /**
796 * Recursively generates the <dl>'s to document item descriptions.
797 * Echoes HTML.
798 */
799 function generate_doc_description( $item ) {
800 if ( is_array( $item ) ) : ?>
801
802 <dl>
803 <?php foreach ( $item as $description_key => $description_value ) : ?>
804
805 <dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
806 <dd><?php $this->generate_doc_description( $description_value ); ?></dd>
807
808 <?php endforeach; ?>
809
810 </dl>
811
812 <?php
813 else :
814 echo wp_kses_post( $item );
815 endif;
816 }
817
818 /**
819 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
820 * Echoes HTML.
821 */
822 function generate_documentation() {
823 $format = str_replace( '%d', '%s', $this->path );
824 $path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
825 $boolean_arg = array( 'false', 'true' );
826 $naeloob_arg = array( 'true', 'false' );
827
828 $doc = array(
829 'description' => $this->description,
830 'method' => $this->method,
831 'path_format' => $this->path,
832 'path_labeled' => $path_labeled,
833 'group' => $this->group,
834 'request' => array(
835 'path' => array(),
836 'query' => array(),
837 'body' => array(),
838 ),
839 'response' => array(
840 'body' => array(),
841 )
842 );
843
844 foreach ( array( 'path_labels' => 'path', 'query' => 'query', 'request_format' => 'body', 'response_format' => 'body' ) as $_property => $doc_item ) {
845 foreach ( (array) $this->$_property as $key => $description ) {
846 if ( is_array( $description ) ) {
847 $description_keys = array_keys( $description );
848 if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
849 $type = '(bool)';
850 } else {
851 $type = '(string)';
852 }
853
854 if ( 'response_format' !== $_property ) {
855 // hack - don't show "(default)" in response format
856 reset( $description );
857 $description_key = key( $description );
858 $description[$description_key] = "(default) {$description[$description_key]}";
859 }
860 } else {
861 $types = $this->parse_types( $description );
862 $type = array();
863 $default = '';
864
865 if ( 'none' == $types ) {
866 $types = array();
867 $types[]['type'] = 'none';
868 }
869
870 foreach ( $types as $type_array ) {
871 $type[] = $type_array['type'];
872 if ( isset( $type_array['default'] ) ) {
873 $default = $type_array['default'];
874 if ( 'string' === $type_array['type'] ) {
875 $default = "'$default'";
876 }
877 }
878 }
879 $type = '(' . join( '|', $type ) . ')';
880 $noop = ''; // skip an index in list below
881 list( $noop, $description ) = explode( ')', $description, 2 );
882 $description = trim( $description );
883 if ( $default ) {
884 $description .= " Default: $default.";
885 }
886 }
887
888 $item = compact( 'type', 'description' );
889
890 if ( 'response_format' === $_property ) {
891 $doc['response'][$doc_item][$key] = $item;
892 } else {
893 $doc['request'][$doc_item][$key] = $item;
894 }
895 }
896 }
897
898 return $doc;
899 }
900
901 function user_can_view_post( $post_id ) {
902 $post = get_post( $post_id );
903 if ( !$post || is_wp_error( $post ) ) {
904 return false;
905 }
906
907 if ( 'inherit' === $post->post_status ) {
908 $parent_post = get_post( $post->post_parent );
909 $post_status_obj = get_post_status_object( $parent_post->post_status );
910 } else {
911 $post_status_obj = get_post_status_object( $post->post_status );
912 }
913
914 if ( !$post_status_obj->public ) {
915 if ( is_user_logged_in() ) {
916 if ( $post_status_obj->protected ) {
917 if ( !current_user_can( 'edit_post', $post->ID ) ) {
918 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
919 }
920 } elseif ( $post_status_obj->private ) {
921 if ( !current_user_can( 'read_post', $post->ID ) ) {
922 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
923 }
924 } elseif ( 'trash' === $post->post_status ) {
925 if ( !current_user_can( 'edit_post', $post->ID ) ) {
926 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
927 }
928 } else {
929 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
930 }
931 } else {
932 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
933 }
934 }
935
936 if ( -1 == get_option( 'blog_public' ) && !current_user_can( 'read_post', $post->ID ) ) {
937 return new WP_Error( 'unauthorized', 'User cannot view post', array( 'status_code' => 403, 'error' => 'private_blog' ) );
938 }
939
940 if ( strlen( $post->post_password ) && !current_user_can( 'edit_post', $post->ID ) ) {
941 return new WP_Error( 'unauthorized', 'User cannot view password protected post', array( 'status_code' => 403, 'error' => 'password_protected' ) );
942 }
943
944 return true;
945 }
946
947 /**
948 * Returns author object.
949 *
950 * @param $author user ID, user row, WP_User object, comment row, post row
951 * @param $show_email output the author's email address?
952 *
953 * @return (object)
954 */
955 function get_author( $author, $show_email = false ) {
956 if ( isset( $author->comment_author_email ) && !$author->user_id ) {
957 $ID = 0;
958 $login = '';
959 $email = $author->comment_author_email;
960 $name = $author->comment_author;
961 $URL = $author->comment_author_url;
962 $profile_URL = 'http://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
963 $nice = '';
964 $site_id = -1;
965 } else {
966 if ( isset( $author->post_author ) ) {
967 if ( 0 == $author->post_author )
968 return null;
969
970 $author = $author->post_author;
971 } elseif ( isset( $author->user_id ) && $author->user_id ) {
972 $author = $author->user_id;
973 } elseif ( isset( $author->user_email ) ) {
974 $author = $author->ID;
975 }
976
977 $user = get_user_by( 'id', $author );
978 if ( !$user || is_wp_error( $user ) ) {
979 trigger_error( 'Unknown user', E_USER_WARNING );
980 return null;
981 }
982
983 $ID = $user->ID;
984 $email = $user->user_email;
985 $login = $user->user_login;
986 $name = $user->display_name;
987 $URL = $user->user_url;
988 $nice = $user->user_nicename;
989 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
990 $active_blog = get_active_blog_for_user( $ID );
991 $site_id = $active_blog->blog_id;
992 $profile_URL = "http://en.gravatar.com/{$login}";
993 } else {
994 $profile_URL = 'http://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
995 $site_id = -1;
996 }
997 }
998
999 $avatar_URL = $this->api->get_avatar_url( $email );
1000
1001 $email = $show_email ? (string) $email : false;
1002
1003 $author = array(
1004 'ID' => (int) $ID,
1005 'login' => (string) $login,
1006 'email' => $email, // (string|bool)
1007 'name' => (string) $name,
1008 'nice_name' => (string) $nice,
1009 'URL' => (string) esc_url_raw( $URL ),
1010 'avatar_URL' => (string) esc_url_raw( $avatar_URL ),
1011 'profile_URL' => (string) esc_url_raw( $profile_URL ),
1012 );
1013
1014 if ($site_id > -1) {
1015 $author['site_ID'] = (int) $site_id;
1016 }
1017
1018 return (object) $author;
1019 }
1020
1021 function get_media_item( $media_id ) {
1022 $media_item = get_post( $media_id );
1023
1024 if ( !$media_item || is_wp_error( $media_item ) )
1025 return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1026
1027 $response = array(
1028 'id' => strval( $media_item->ID ),
1029 'date' => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1030 'parent' => $media_item->post_parent,
1031 'link' => wp_get_attachment_url( $media_item->ID ),
1032 'title' => $media_item->post_title,
1033 'caption' => $media_item->post_excerpt,
1034 'description' => $media_item->post_content,
1035 'metadata' => wp_get_attachment_metadata( $media_item->ID ),
1036 );
1037
1038 $response['meta'] = (object) array(
1039 'links' => (object) array(
1040 'self' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1041 'help' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1042 'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),
1043 ),
1044 );
1045
1046 return (object) $response;
1047 }
1048
1049 function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1050
1051 $taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1052 /// keep updating this function
1053 if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
1054 return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
1055 }
1056
1057 // Permissions
1058 switch ( $context ) {
1059 case 'edit' :
1060 $tax = get_taxonomy( $taxonomy_type );
1061 if ( !current_user_can( $tax->cap->edit_terms ) )
1062 return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
1063 break;
1064 case 'display' :
1065 if ( -1 == get_option( 'blog_public' ) ) {
1066 return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
1067 }
1068 break;
1069 default :
1070 return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1071 }
1072
1073 $response = array();
1074 $response['ID'] = (int) $taxonomy->term_id;
1075 $response['name'] = (string) $taxonomy->name;
1076 $response['slug'] = (string) $taxonomy_id;
1077 $response['description'] = (string) $taxonomy->description;
1078 $response['post_count'] = (int) $taxonomy->count;
1079
1080 if ( 'category' === $taxonomy_type )
1081 $response['parent'] = (int) $taxonomy->parent;
1082
1083 $response['meta'] = (object) array(
1084 'links' => (object) array(
1085 'self' => (string) $this->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy_id, $taxonomy_type ),
1086 'help' => (string) $this->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy_id, $taxonomy_type, 'help' ),
1087 'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ),
1088 ),
1089 );
1090
1091 return (object) $response;
1092 }
1093
1094 /**
1095 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1096 *
1097 * @param $date_gmt (string) GMT datetime string.
1098 * @param $date (string) Optional. Used to calculate the offset from GMT.
1099 *
1100 * @return string
1101 */
1102 function format_date( $date_gmt, $date = null ) {
1103 $timestamp_gmt = strtotime( "$date_gmt+0000" );
1104 if ( null === $date ) {
1105 $timestamp = $timestamp_gmt;
1106 $hours = $minutes = $west = 0;
1107 } else {
1108 $timestamp = strtotime( "$date+0000" );
1109 $offset = $timestamp - $timestamp_gmt;
1110 $west = $offset < 0;
1111 $offset = abs( $offset );
1112 $hours = (int) floor( $offset / 3600 );
1113 $offset -= $hours * 3600;
1114 $minutes = (int) floor( $offset / 60 );
1115 }
1116
1117 return (string) gmdate( 'Y-m-d\\TH:i:s', $timestamp ) . sprintf( '%s%02d:%02d', $west ? '-' : '+', $hours, $minutes );
1118 }
1119
1120 /**
1121 * Parses a date string and returns the local and GMT representations
1122 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1123 * timezones or offsets. If the parsed datetime was not localized to a
1124 * particular timezone or offset we will assume it was given in GMT
1125 * relative to now and will convert it to local time using either the
1126 * timezone set in the options table for the blog or the GMT offset.
1127 *
1128 * @param datetime string
1129 *
1130 * @return array( $local_time_string, $gmt_time_string )
1131 */
1132 function parse_date( $date_string ) {
1133 $date_string_info = date_parse( $date_string );
1134 if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1135 // Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1136 if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1137 $dt_local = clone $dt_utc = new DateTime( $date_string );
1138 $dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1139 return array(
1140 (string) $dt_local->format( 'Y-m-d H:i:s' ),
1141 (string) $dt_utc->format( 'Y-m-d H:i:s' ),
1142 );
1143 }
1144
1145 // It's parseable but no TZ info so assume UTC
1146 $dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1147 } else {
1148 // Could not parse time, use now in UTC
1149 $dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1150 }
1151
1152 // First try to use timezone as it's daylight savings aware.
1153 $timezone_string = get_option( 'timezone_string' );
1154 if ( $timezone_string ) {
1155 $tz = timezone_open( $timezone_string );
1156 if ( $tz ) {
1157 $dt_local->setTimezone( $tz );
1158 return array(
1159 (string) $dt_local->format( 'Y-m-d H:i:s' ),
1160 (string) $dt_utc->format( 'Y-m-d H:i:s' ),
1161 );
1162 }
1163 }
1164
1165 // Fallback to GMT offset (in hours)
1166 // NOTE: TZ of $dt_local is still UTC, we simply modified the timestamp with an offset.
1167 $gmt_offset_seconds = intval( get_option( 'gmt_offset' ) * 3600 );
1168 $dt_local->modify("+{$gmt_offset_seconds} seconds");
1169 return array(
1170 (string) $dt_local->format( 'Y-m-d H:i:s' ),
1171 (string) $dt_utc->format( 'Y-m-d H:i:s' ),
1172 );
1173 }
1174
1175 function get_link() {
1176 $args = func_get_args();
1177 $format = array_shift( $args );
1178 array_unshift( $args, $this->api->public_api_scheme, WPCOM_JSON_API__BASE );
1179 $path = array_pop( $args );
1180 if ( $path ) {
1181 $path = '/' . ltrim( $path, '/' );
1182 }
1183 $args[] = $path;
1184
1185 // http, WPCOM_JSON_API__BASE, ... , path
1186 // %s , %s , $format, %s
1187 return esc_url_raw( vsprintf( "%s://%s$format%s", $args ) );
1188 }
1189
1190 function get_me_link( $path = '' ) {
1191 return $this->get_link( '/me', $path );
1192 }
1193
1194 function get_taxonomy_link( $blog_id, $taxonomy_id, $taxonomy_type, $path = '' ) {
1195 if ( 'category' === $taxonomy_type )
1196 return $this->get_link( '/sites/%d/categories/slug:%s', $blog_id, $taxonomy_id, $path );
1197 else
1198 return $this->get_link( '/sites/%d/tags/slug:%s', $blog_id, $taxonomy_id, $path );
1199 }
1200
1201 function get_media_link( $blog_id, $media_id, $path = '' ) {
1202 return $this->get_link( '/sites/%d/media/%d', $blog_id, $media_id, $path );
1203 }
1204
1205 function get_site_link( $blog_id, $path = '' ) {
1206 return $this->get_link( '/sites/%d', $blog_id, $path );
1207 }
1208
1209 function get_post_link( $blog_id, $post_id, $path = '' ) {
1210 return $this->get_link( '/sites/%d/posts/%d', $blog_id, $post_id, $path );
1211 }
1212
1213 function get_comment_link( $blog_id, $comment_id, $path = '' ) {
1214 return $this->get_link( '/sites/%d/comments/%d', $blog_id, $comment_id, $path );
1215 }
1216
1217 function is_post_type_allowed( $post_type ) {
1218 // if the post type is empty, that's fine, WordPress will default to post
1219 if ( empty( $post_type ) )
1220 return true;
1221
1222 // allow special 'any' type
1223 if ( 'any' == $post_type )
1224 return true;
1225
1226 // check for allowed types
1227 if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) )
1228 return true;
1229
1230 return false;
1231 }
1232
1233 /**
1234 * Gets the whitelisted post types that JP should allow access to.
1235 *
1236 * @return array Whitelisted post types.
1237 */
1238 protected function _get_whitelisted_post_types() {
1239 $allowed_types = array( 'post', 'page', 'revision' );
1240
1241 $allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1242
1243 return array_unique( $allowed_types );
1244 }
1245
1246 function handle_media_sideload( $url, $parent_post_id = 0 ) {
1247 if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) )
1248 return false;
1249
1250 // if we didn't get a URL, let's bail
1251 $parsed = @parse_url( $url );
1252 if ( empty( $parsed ) )
1253 return false;
1254
1255 $tmp = download_url( $url );
1256 if ( is_wp_error( $tmp ) ) {
1257 return false;
1258 }
1259
1260 if ( ! file_is_displayable_image( $tmp ) ) {
1261 @unlink( $tmp );
1262 return false;
1263 }
1264
1265 // emulate a $_FILES entry
1266 $file_array = array(
1267 'name' => basename( parse_url( $url, PHP_URL_PATH ) ),
1268 'tmp_name' => $tmp,
1269 );
1270
1271 $id = media_handle_sideload( $file_array, $parent_post_id );
1272 @unlink( $tmp );
1273
1274 if ( ! $id || ! is_int( $id ) ) {
1275 return false;
1276 }
1277
1278 return $id;
1279 }
1280
1281 /**
1282 * Return endpoint response
1283 *
1284 * @param ... determined by ->$path
1285 *
1286 * @return
1287 * falsy: HTTP 500, no response body
1288 * WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
1289 * $data: HTTP 200, json_encode( $data ) response body
1290 */
1291 abstract function callback( $path = '' );
1292
1293 }
1294
1295 require_once( JETPACK__PLUGIN_DIR . 'json-endpoints.php' );
1296