PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 4.2.5
Jetpack – WP Security, Backup, Speed, & Growth v4.2.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,908 lines 60.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 require_once( dirname( __FILE__ ) . '/json-api-config.php' );
4 require_once( dirname( __FILE__ ) . '/sal/class.json-api-links.php' );
5 require_once( dirname( __FILE__ ) . '/sal/class.json-api-metadata.php' );
6 require_once( dirname( __FILE__ ) . '/sal/class.json-api-date.php' );
7
8 // Endpoint
9 abstract class WPCOM_JSON_API_Endpoint {
10 // The API Object
11 public $api;
12
13 // The link-generating utility class
14 public $links;
15
16 public $pass_wpcom_user_details = false;
17
18 // One liner.
19 public $description;
20
21 // Object Grouping For Documentation (Users, Posts, Comments)
22 public $group;
23
24 // Stats extra value to bump
25 public $stat;
26
27 // HTTP Method
28 public $method = 'GET';
29
30 // Minimum version of the api for which to serve this endpoint
31 public $min_version = '0';
32
33 // Maximum version of the api for which to serve this endpoint
34 public $max_version = WPCOM_JSON_API__CURRENT_VERSION;
35
36 // Path at which to serve this endpoint: sprintf() format.
37 public $path = '';
38
39 // Identifiers to fill sprintf() formatted $path
40 public $path_labels = array();
41
42 // Accepted query parameters
43 public $query = array(
44 // Parameter name
45 'context' => array(
46 // Default value => description
47 'display' => 'Formats the output as HTML for display. Shortcodes are parsed, paragraph tags are added, etc..',
48 // Other possible values => description
49 'edit' => 'Formats the output for editing. Shortcodes are left unparsed, significant whitespace is kept, etc..',
50 ),
51 'http_envelope' => array(
52 'false' => '',
53 '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.',
54 ),
55 'pretty' => array(
56 'false' => '',
57 'true' => 'Output pretty JSON',
58 ),
59 'meta' => "(string) Optional. Loads data from the endpoints found in the 'meta' part of the response. Comma-separated list. Example: meta=site,likes",
60 'fields' => '(string) Optional. Returns specified fields only. Comma-separated list. Example: fields=ID,title',
61 // Parameter name => description (default value is empty)
62 'callback' => '(string) An optional JSONP callback function.',
63 );
64
65 // Response format
66 public $response_format = array();
67
68 // Request format
69 public $request_format = array();
70
71 // Is this endpoint still in testing phase? If so, not available to the public.
72 public $in_testing = false;
73
74 // Is this endpoint still allowed if the site in question is flagged?
75 public $allowed_if_flagged = false;
76
77 // Is this endpoint allowed if the site is red flagged?
78 public $allowed_if_red_flagged = false;
79
80 /**
81 * @var string Version of the API
82 */
83 public $version = '';
84
85 /**
86 * @var string Example request to make
87 */
88 public $example_request = '';
89
90 /**
91 * @var string Example request data (for POST methods)
92 */
93 public $example_request_data = '';
94
95 /**
96 * @var string Example response from $example_request
97 */
98 public $example_response = '';
99
100 /**
101 * @var bool Set to true if the endpoint implements its own filtering instead of the standard `fields` query method
102 */
103 public $custom_fields_filtering = false;
104
105 /**
106 * @var bool Set to true if the endpoint accepts all cross origin requests. You probably should not set this flag.
107 */
108 public $allow_cross_origin_request = false;
109
110 /**
111 * @var bool Set to true if the endpoint can recieve unauthorized POST requests.
112 */
113 public $allow_unauthorized_request = false;
114
115 /**
116 * @var bool Set to true if the endpoint should accept site based (not user based) authentication.
117 */
118 public $allow_jetpack_site_auth = false;
119
120 function __construct( $args ) {
121 $defaults = array(
122 'in_testing' => false,
123 'allowed_if_flagged' => false,
124 'allowed_if_red_flagged' => false,
125 'description' => '',
126 'group' => '',
127 'method' => 'GET',
128 'path' => '/',
129 'min_version' => '0',
130 'max_version' => WPCOM_JSON_API__CURRENT_VERSION,
131 'force' => '',
132 'deprecated' => false,
133 'new_version' => WPCOM_JSON_API__CURRENT_VERSION,
134 'jp_disabled' => false,
135 'path_labels' => array(),
136 'request_format' => array(),
137 'response_format' => array(),
138 'query_parameters' => array(),
139 'version' => 'v1',
140 'example_request' => '',
141 'example_request_data' => '',
142 'example_response' => '',
143 'required_scope' => '',
144 'pass_wpcom_user_details' => false,
145 'custom_fields_filtering' => false,
146 'allow_cross_origin_request' => false,
147 'allow_unauthorized_request' => false,
148 'allow_jetpack_site_auth' => false,
149 );
150
151 $args = wp_parse_args( $args, $defaults );
152
153 $this->in_testing = $args['in_testing'];
154
155 $this->allowed_if_flagged = $args['allowed_if_flagged'];
156 $this->allowed_if_red_flagged = $args['allowed_if_red_flagged'];
157
158 $this->description = $args['description'];
159 $this->group = $args['group'];
160 $this->stat = $args['stat'];
161 $this->force = $args['force'];
162 $this->jp_disabled = $args['jp_disabled'];
163
164 $this->method = $args['method'];
165 $this->path = $args['path'];
166 $this->path_labels = $args['path_labels'];
167 $this->min_version = $args['min_version'];
168 $this->max_version = $args['max_version'];
169 $this->deprecated = $args['deprecated'];
170 $this->new_version = $args['new_version'];
171
172 $this->pass_wpcom_user_details = $args['pass_wpcom_user_details'];
173 $this->custom_fields_filtering = (bool) $args['custom_fields_filtering'];
174
175 $this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request'];
176 $this->allow_unauthorized_request = (bool) $args['allow_unauthorized_request'];
177 $this->allow_jetpack_site_auth = (bool) $args['allow_jetpack_site_auth'];
178
179 $this->version = $args['version'];
180
181 $this->required_scope = $args['required_scope'];
182
183 if ( $this->request_format ) {
184 $this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) );
185 } else {
186 $this->request_format = $args['request_format'];
187 }
188
189 if ( $this->response_format ) {
190 $this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) );
191 } else {
192 $this->response_format = $args['response_format'];
193 }
194
195 if ( false === $args['query_parameters'] ) {
196 $this->query = array();
197 } elseif ( is_array( $args['query_parameters'] ) ) {
198 $this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) );
199 }
200
201 $this->api = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API
202 $this->links = WPCOM_JSON_API_Links::getInstance();
203
204 /** Example Request/Response ******************************************/
205
206 // Examples for endpoint documentation request
207 $this->example_request = $args['example_request'];
208 $this->example_request_data = $args['example_request_data'];
209 $this->example_response = $args['example_response'];
210
211 $this->api->add( $this );
212 }
213
214 // Get all query args. Prefill with defaults
215 function query_args( $return_default_values = true, $cast_and_filter = true ) {
216 $args = array_intersect_key( $this->api->query, $this->query );
217
218 if ( !$cast_and_filter ) {
219 return $args;
220 }
221
222 return $this->cast_and_filter( $args, $this->query, $return_default_values );
223 }
224
225 // Get POST body data
226 function input( $return_default_values = true, $cast_and_filter = true ) {
227 $input = trim( $this->api->post_body );
228 $content_type = $this->api->content_type;
229 if ( $content_type ) {
230 list ( $content_type ) = explode( ';', $content_type );
231 }
232 $content_type = trim( $content_type );
233 switch ( $content_type ) {
234 case 'application/json' :
235 case 'application/x-javascript' :
236 case 'text/javascript' :
237 case 'text/x-javascript' :
238 case 'text/x-json' :
239 case 'text/json' :
240 $return = json_decode( $input, true );
241
242 if ( function_exists( 'json_last_error' ) ) {
243 if ( JSON_ERROR_NONE !== json_last_error() ) {
244 return null;
245 }
246 } else {
247 if ( is_null( $return ) && json_encode( null ) !== $input ) {
248 return null;
249 }
250 }
251
252 break;
253 case 'multipart/form-data' :
254 $return = array_merge( stripslashes_deep( $_POST ), $_FILES );
255 break;
256 case 'application/x-www-form-urlencoded' :
257 //attempt JSON first, since probably a curl command
258 $return = json_decode( $input, true );
259
260 if ( is_null( $return ) ) {
261 wp_parse_str( $input, $return );
262 }
263
264 break;
265 default :
266 wp_parse_str( $input, $return );
267 break;
268 }
269
270 if ( !$cast_and_filter ) {
271 return $return;
272 }
273
274 return $this->cast_and_filter( $return, $this->request_format, $return_default_values );
275 }
276
277 function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) {
278 $return_as_object = false;
279 if ( is_object( $data ) ) {
280 // @todo this should probably be a deep copy if $data can ever have nested objects
281 $data = (array) $data;
282 $return_as_object = true;
283 } elseif ( !is_array( $data ) ) {
284 return $data;
285 }
286
287 $boolean_arg = array( 'false', 'true' );
288 $naeloob_arg = array( 'true', 'false' );
289
290 $return = array();
291
292 foreach ( $documentation as $key => $description ) {
293 if ( is_array( $description ) ) {
294 // String or boolean array keys only
295 $whitelist = array_keys( $description );
296
297 if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) {
298 // Truthiness
299 if ( isset( $data[$key] ) ) {
300 $return[$key] = (bool) WPCOM_JSON_API::is_truthy( $data[$key] );
301 } elseif ( $return_default_values ) {
302 $return[$key] = $whitelist === $naeloob_arg; // Default to true for naeloob_arg and false for boolean_arg.
303 }
304 } elseif ( isset( $data[$key] ) && isset( $description[$data[$key]] ) ) {
305 // String Key
306 $return[$key] = (string) $data[$key];
307 } elseif ( $return_default_values ) {
308 // Default value
309 $return[$key] = (string) current( $whitelist );
310 }
311
312 continue;
313 }
314
315 $types = $this->parse_types( $description );
316 $type = array_shift( $types );
317
318 // Explicit default - string and int only for now. Always set these reguardless of $return_default_values
319 if ( isset( $type['default'] ) ) {
320 if ( !isset( $data[$key] ) ) {
321 $data[$key] = $type['default'];
322 }
323 }
324
325 if ( !isset( $data[$key] ) ) {
326 continue;
327 }
328
329 $this->cast_and_filter_item( $return, $type, $key, $data[$key], $types, $for_output );
330 }
331
332 if ( $return_as_object ) {
333 return (object) $return;
334 }
335
336 return $return;
337 }
338
339 /**
340 * Casts $value according to $type.
341 * Handles fallbacks for certain values of $type when $value is not that $type
342 * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way),
343 * and string -> object (one way)
344 *
345 * Handles "child types" - array:URL, object:category
346 * array:URL means an array of URLs
347 * object:category means a hash of categories
348 *
349 * Handles object typing - object>post means an object of type post
350 */
351 function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) {
352 if ( is_string( $type ) ) {
353 $type = compact( 'type' );
354 }
355
356 switch ( $type['type'] ) {
357 case 'false' :
358 $return[$key] = false;
359 break;
360 case 'url' :
361 $return[$key] = (string) esc_url_raw( $value );
362 break;
363 case 'string' :
364 // Fallback string -> array, or for string -> object
365 if ( is_array( $value ) || is_object( $value ) ) {
366 if ( !empty( $types[0] ) ) {
367 $next_type = array_shift( $types );
368 return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
369 }
370 }
371
372 // Fallback string -> false
373 if ( !is_string( $value ) ) {
374 if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
375 $next_type = array_shift( $types );
376 return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
377 }
378 }
379 $return[$key] = (string) $value;
380 break;
381 case 'html' :
382 $return[$key] = (string) $value;
383 break;
384 case 'safehtml' :
385 $return[$key] = wp_kses( (string) $value, wp_kses_allowed_html() );
386 break;
387 case 'media' :
388 if ( is_array( $value ) ) {
389 if ( isset( $value['name'] ) && is_array( $value['name'] ) ) {
390 // It's a $_FILES array
391 // Reformat into array of $_FILES items
392 $files = array();
393
394 foreach ( $value['name'] as $k => $v ) {
395 $files[$k] = array();
396 foreach ( array_keys( $value ) as $file_key ) {
397 $files[$k][$file_key] = $value[$file_key][$k];
398 }
399 }
400
401 foreach ( $files as $k => $file ) {
402 if ( ! isset( $file['tmp_name'] ) || ! is_string( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) {
403 unset( $files[ $k ] );
404 }
405 }
406 if ( $files ) {
407 $return[$key] = $files;
408 }
409 } elseif ( isset( $value['tmp_name'] ) && is_string( $value['tmp_name'] ) && is_uploaded_file( $value['tmp_name'] ) ) {
410 $return[ $key ] = $value;
411 }
412 }
413 break;
414 case 'array' :
415 // Fallback array -> string
416 if ( is_string( $value ) ) {
417 if ( !empty( $types[0] ) ) {
418 $next_type = array_shift( $types );
419 return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
420 }
421 }
422
423 if ( isset( $type['children'] ) ) {
424 $children = array();
425 foreach ( (array) $value as $k => $child ) {
426 $this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
427 }
428 $return[$key] = (array) $children;
429 break;
430 }
431
432 $return[$key] = (array) $value;
433 break;
434 case 'iso 8601 datetime' :
435 case 'datetime' :
436 // (string)s
437 $dates = $this->parse_date( (string) $value );
438 if ( $for_output ) {
439 $return[$key] = $this->format_date( $dates[1], $dates[0] );
440 } else {
441 list( $return[$key], $return["{$key}_gmt"] ) = $dates;
442 }
443 break;
444 case 'float' :
445 $return[$key] = (float) $value;
446 break;
447 case 'int' :
448 case 'integer' :
449 $return[$key] = (int) $value;
450 break;
451 case 'bool' :
452 case 'boolean' :
453 $return[$key] = (bool) WPCOM_JSON_API::is_truthy( $value );
454 break;
455 case 'object' :
456 // Fallback object -> false
457 if ( is_scalar( $value ) || is_null( $value ) ) {
458 if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) {
459 return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output );
460 }
461 }
462
463 if ( isset( $type['children'] ) ) {
464 $children = array();
465 foreach ( (array) $value as $k => $child ) {
466 $this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output );
467 }
468 $return[$key] = (object) $children;
469 break;
470 }
471
472 if ( isset( $type['subtype'] ) ) {
473 return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output );
474 }
475
476 $return[$key] = (object) $value;
477 break;
478 case 'post' :
479 $return[$key] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output );
480 break;
481 case 'comment' :
482 $return[$key] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output );
483 break;
484 case 'tag' :
485 case 'category' :
486 $docs = array(
487 'ID' => '(int)',
488 'name' => '(string)',
489 'slug' => '(string)',
490 'description' => '(HTML)',
491 'post_count' => '(int)',
492 'meta' => '(object)',
493 );
494 if ( 'category' === $type['type'] ) {
495 $docs['parent'] = '(int)';
496 }
497 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
498 break;
499 case 'post_reference' :
500 case 'comment_reference' :
501 $docs = array(
502 'ID' => '(int)',
503 'type' => '(string)',
504 'title' => '(string)',
505 'link' => '(URL)',
506 );
507 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
508 break;
509 case 'geo' :
510 $docs = array(
511 'latitude' => '(float)',
512 'longitude' => '(float)',
513 'address' => '(string)',
514 );
515 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
516 break;
517 case 'author' :
518 $docs = array(
519 'ID' => '(int)',
520 'user_login' => '(string)',
521 'login' => '(string)',
522 'email' => '(string|false)',
523 'name' => '(string)',
524 'first_name' => '(string)',
525 'last_name' => '(string)',
526 'nice_name' => '(string)',
527 'URL' => '(URL)',
528 'avatar_URL' => '(URL)',
529 'profile_URL' => '(URL)',
530 'is_super_admin' => '(bool)',
531 'roles' => '(array:string)'
532 );
533 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
534 break;
535 case 'role' :
536 $docs = array(
537 'name' => '(string)',
538 'display_name' => '(string)',
539 'capabilities' => '(object:boolean)',
540 );
541 $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
542 break;
543 case 'attachment' :
544 $docs = array(
545 'ID' => '(int)',
546 'URL' => '(URL)',
547 'guid' => '(string)',
548 'mime_type' => '(string)',
549 'width' => '(int)',
550 'height' => '(int)',
551 'duration' => '(int)',
552 );
553 $return[$key] = (object) $this->cast_and_filter(
554 $value,
555 /**
556 * Filter the documentation returned for a post attachment.
557 *
558 * @module json-api
559 *
560 * @since 1.9.0
561 *
562 * @param array $docs Array of documentation about a post attachment.
563 */
564 apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
565 false,
566 $for_output
567 );
568 break;
569 case 'metadata' :
570 $docs = array(
571 'id' => '(int)',
572 'key' => '(string)',
573 'value' => '(string|false|float|int|array|object)',
574 'previous_value' => '(string)',
575 'operation' => '(string)',
576 );
577 $return[$key] = (object) $this->cast_and_filter(
578 $value,
579 /** This filter is documented in class.json-api-endpoints.php */
580 apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ),
581 false,
582 $for_output
583 );
584 break;
585 case 'plugin' :
586 $docs = array(
587 'id' => '(safehtml) The plugin\'s ID',
588 'slug' => '(safehtml) The plugin\'s Slug',
589 'active' => '(boolean) The plugin status.',
590 'update' => '(object) The plugin update info.',
591 'name' => '(safehtml) The name of the plugin.',
592 'plugin_url' => '(url) Link to the plugin\'s web site.',
593 'version' => '(safehtml) The plugin version number.',
594 'description' => '(safehtml) Description of what the plugin does and/or notes from the author',
595 'author' => '(safehtml) The plugin author\'s name',
596 'author_url' => '(url) The plugin author web site address',
597 'network' => '(boolean) Whether the plugin can only be activated network wide.',
598 'autoupdate' => '(boolean) Whether the plugin is auto updated',
599 'log' => '(array:safehtml) An array of update log strings.',
600 );
601 $return[$key] = (object) $this->cast_and_filter(
602 $value,
603 /**
604 * Filter the documentation returned for a plugin.
605 *
606 * @module json-api
607 *
608 * @since 3.1.0
609 *
610 * @param array $docs Array of documentation about a plugin.
611 */
612 apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
613 false,
614 $for_output
615 );
616 break;
617 case 'jetpackmodule' :
618 $docs = array(
619 'id' => '(string) The module\'s ID',
620 'active' => '(boolean) The module\'s status.',
621 'name' => '(string) The module\'s name.',
622 'description' => '(safehtml) The module\'s description.',
623 'sort' => '(int) The module\'s display order.',
624 'introduced' => '(string) The Jetpack version when the module was introduced.',
625 'changed' => '(string) The Jetpack version when the module was changed.',
626 'free' => '(boolean) The module\'s Free or Paid status.',
627 'module_tags' => '(array) The module\'s tags.'
628 );
629 $return[$key] = (object) $this->cast_and_filter(
630 $value,
631 /** This filter is documented in class.json-api-endpoints.php */
632 apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
633 false,
634 $for_output
635 );
636 break;
637 case 'sharing_button' :
638 $docs = array(
639 'ID' => '(string)',
640 'name' => '(string)',
641 'URL' => '(string)',
642 'icon' => '(string)',
643 'enabled' => '(bool)',
644 'visibility' => '(string)',
645 );
646 $return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
647 break;
648 case 'sharing_button_service':
649 $docs = array(
650 'ID' => '(string) The service identifier',
651 'name' => '(string) The service name',
652 'class_name' => '(string) Class name for custom style sharing button elements',
653 'genericon' => '(string) The Genericon unicode character for the custom style sharing button icon',
654 'preview_smart' => '(string) An HTML snippet of a rendered sharing button smart preview',
655 'preview_smart_js' => '(string) An HTML snippet of the page-wide initialization scripts used for rendering the sharing button smart preview'
656 );
657 $return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
658 break;
659 case 'taxonomy':
660 $docs = array(
661 'name' => '(string) The taxonomy slug',
662 'label' => '(string) The taxonomy human-readable name',
663 'labels' => '(object) Mapping of labels for the taxonomy',
664 'description' => '(string) The taxonomy description',
665 'hierarchical' => '(bool) Whether the taxonomy is hierarchical',
666 'public' => '(bool) Whether the taxonomy is public',
667 'capabilities' => '(object) Mapping of current user capabilities for the taxonomy',
668 );
669 $return[$key] = (array) $this->cast_and_filter( $value, $docs, false, $for_output );
670 break;
671
672 default :
673 $method_name = $type['type'] . '_docs';
674 if ( method_exists( WPCOM_JSON_API_Jetpack_Overrides, $method_name ) ) {
675 $docs = WPCOM_JSON_API_Jetpack_Overrides::$method_name();
676 }
677
678 if ( ! empty( $docs ) ) {
679 $return[$key] = (object) $this->cast_and_filter(
680 $value,
681 /** This filter is documented in class.json-api-endpoints.php */
682 apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ),
683 false,
684 $for_output
685 );
686 } else {
687 trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING );
688 }
689 }
690 }
691
692 function parse_types( $text ) {
693 if ( !preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) {
694 return 'none';
695 }
696
697 $types = explode( '|', strtolower( $matches[1] ) );
698 $return = array();
699 foreach ( $types as $type ) {
700 foreach ( array( ':' => 'children', '>' => 'subtype', '=' => 'default' ) as $operator => $meaning ) {
701 if ( false !== strpos( $type, $operator ) ) {
702 $item = explode( $operator, $type, 2 );
703 $return[] = array( 'type' => $item[0], $meaning => $item[1] );
704 continue 2;
705 }
706 }
707 $return[] = compact( 'type' );
708 }
709
710 return $return;
711 }
712
713 /**
714 * Checks if the endpoint is publicly displayable
715 */
716 function is_publicly_documentable() {
717 return '__do_not_document' !== $this->group && true !== $this->in_testing;
718 }
719
720 /**
721 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
722 * Echoes HTML.
723 */
724 function document( $show_description = true ) {
725 global $wpdb;
726 $original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
727 unset( $GLOBALS['post'] );
728
729 $doc = $this->generate_documentation();
730
731 if ( $show_description ) :
732 ?>
733 <caption>
734 <h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1>
735 <p><?php echo wp_kses_post( $doc['description'] ); ?></p>
736 </caption>
737
738 <?php endif; ?>
739
740 <?php if ( true === $this->deprecated ) { ?>
741 <p><strong>This endpoint is deprecated in favor of version <?php echo floatval( $this->new_version ); ?></strong></p>
742 <?php } ?>
743
744 <section class="resource-info">
745 <h2 id="apidoc-resource-info">Resource Information</h2>
746
747 <table class="api-doc api-doc-resource-parameters api-doc-resource">
748
749 <thead>
750 <tr>
751 <th class="api-index-title" scope="column">&nbsp;</th>
752 <th class="api-index-title" scope="column">&nbsp;</th>
753 </tr>
754 </thead>
755 <tbody>
756
757 <tr class="api-index-item">
758 <th scope="row" class="parameter api-index-item-title">Method</th>
759 <td class="type api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></td>
760 </tr>
761
762 <tr class="api-index-item">
763 <th scope="row" class="parameter api-index-item-title">URL</th>
764 <?php
765 $version = WPCOM_JSON_API__CURRENT_VERSION;
766 if ( !empty( $this->max_version ) ) {
767 $version = $this->max_version;
768 }
769 ?>
770 <td class="type api-index-item-title">https://public-api.wordpress.com/rest/v<?php echo floatval( $version ); ?><?php echo wp_kses_post( $doc['path_labeled'] ); ?></td>
771 </tr>
772
773 <tr class="api-index-item">
774 <th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
775 <?php
776 $requires_auth = $wpdb->get_row( $wpdb->prepare( "SELECT requires_authentication FROM rest_api_documentation WHERE `version` = %s AND `path` = %s AND `method` = %s LIMIT 1", $version, untrailingslashit( $doc['path_labeled'] ), $doc['method'] ) );
777 ?>
778 <td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
779 </tr>
780
781 </tbody>
782 </table>
783
784 </section>
785
786 <?php
787
788 foreach ( array(
789 'path' => 'Method Parameters',
790 'query' => 'Query Parameters',
791 'body' => 'Request Parameters',
792 'response' => 'Response Parameters',
793 ) as $doc_section_key => $label ) :
794 $doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][$doc_section_key];
795 if ( !$doc_section ) {
796 continue;
797 }
798
799 $param_label = strtolower( str_replace( ' ', '-', $label ) );
800 ?>
801
802 <section class="<?php echo $param_label; ?>">
803
804 <h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2>
805
806 <table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>">
807
808 <thead>
809 <tr>
810 <th class="api-index-title" scope="column">Parameter</th>
811 <th class="api-index-title" scope="column">Type</th>
812 <th class="api-index-title" scope="column">Description</th>
813 </tr>
814 </thead>
815 <tbody>
816
817 <?php foreach ( $doc_section as $key => $item ) : ?>
818
819 <tr class="api-index-item">
820 <th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th>
821 <td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td>
822 <td class="description api-index-item-body"><?php
823
824 $this->generate_doc_description( $item['description'] );
825
826 ?></td>
827 </tr>
828
829 <?php endforeach; ?>
830 </tbody>
831 </table>
832 </section>
833 <?php endforeach; ?>
834
835 <?php
836 if ( 'unset' !== $original_post ) {
837 $GLOBALS['post'] = $original_post;
838 }
839 }
840
841 function add_http_build_query_to_php_content_example( $matches ) {
842 $trimmed_match = ltrim( $matches[0] );
843 $pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) );
844 $pad = ltrim( $pad, ' ' );
845 $return = ' ' . str_replace( "\n", "\n ", $matches[0] );
846 return " http_build_query({$return}{$pad})";
847 }
848
849 /**
850 * Recursively generates the <dl>'s to document item descriptions.
851 * Echoes HTML.
852 */
853 function generate_doc_description( $item ) {
854 if ( is_array( $item ) ) : ?>
855
856 <dl>
857 <?php foreach ( $item as $description_key => $description_value ) : ?>
858
859 <dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt>
860 <dd><?php $this->generate_doc_description( $description_value ); ?></dd>
861
862 <?php endforeach; ?>
863
864 </dl>
865
866 <?php
867 else :
868 echo wp_kses_post( $item );
869 endif;
870 }
871
872 /**
873 * Auto generates documentation based on description, method, path, path_labels, and query parameters.
874 * Echoes HTML.
875 */
876 function generate_documentation() {
877 $format = str_replace( '%d', '%s', $this->path );
878 $path_labeled = $format;
879 if ( ! empty( $this->path_labels ) ) {
880 $path_labeled = vsprintf( $format, array_keys( $this->path_labels ) );
881 }
882 $boolean_arg = array( 'false', 'true' );
883 $naeloob_arg = array( 'true', 'false' );
884
885 $doc = array(
886 'description' => $this->description,
887 'method' => $this->method,
888 'path_format' => $this->path,
889 'path_labeled' => $path_labeled,
890 'group' => $this->group,
891 'request' => array(
892 'path' => array(),
893 'query' => array(),
894 'body' => array(),
895 ),
896 'response' => array(
897 'body' => array(),
898 )
899 );
900
901 foreach ( array( 'path_labels' => 'path', 'query' => 'query', 'request_format' => 'body', 'response_format' => 'body' ) as $_property => $doc_item ) {
902 foreach ( (array) $this->$_property as $key => $description ) {
903 if ( is_array( $description ) ) {
904 $description_keys = array_keys( $description );
905 if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) {
906 $type = '(bool)';
907 } else {
908 $type = '(string)';
909 }
910
911 if ( 'response_format' !== $_property ) {
912 // hack - don't show "(default)" in response format
913 reset( $description );
914 $description_key = key( $description );
915 $description[$description_key] = "(default) {$description[$description_key]}";
916 }
917 } else {
918 $types = $this->parse_types( $description );
919 $type = array();
920 $default = '';
921
922 if ( 'none' == $types ) {
923 $types = array();
924 $types[]['type'] = 'none';
925 }
926
927 foreach ( $types as $type_array ) {
928 $type[] = $type_array['type'];
929 if ( isset( $type_array['default'] ) ) {
930 $default = $type_array['default'];
931 if ( 'string' === $type_array['type'] ) {
932 $default = "'$default'";
933 }
934 }
935 }
936 $type = '(' . join( '|', $type ) . ')';
937 $noop = ''; // skip an index in list below
938 list( $noop, $description ) = explode( ')', $description, 2 );
939 $description = trim( $description );
940 if ( $default ) {
941 $description .= " Default: $default.";
942 }
943 }
944
945 $item = compact( 'type', 'description' );
946
947 if ( 'response_format' === $_property ) {
948 $doc['response'][$doc_item][$key] = $item;
949 } else {
950 $doc['request'][$doc_item][$key] = $item;
951 }
952 }
953 }
954
955 return $doc;
956 }
957
958 function user_can_view_post( $post_id ) {
959 $post = get_post( $post_id );
960 if ( !$post || is_wp_error( $post ) ) {
961 return false;
962 }
963
964 if ( 'inherit' === $post->post_status ) {
965 $parent_post = get_post( $post->post_parent );
966 $post_status_obj = get_post_status_object( $parent_post->post_status );
967 } else {
968 $post_status_obj = get_post_status_object( $post->post_status );
969 }
970
971 if ( !$post_status_obj->public ) {
972 if ( is_user_logged_in() ) {
973 if ( $post_status_obj->protected ) {
974 if ( !current_user_can( 'edit_post', $post->ID ) ) {
975 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
976 }
977 } elseif ( $post_status_obj->private ) {
978 if ( !current_user_can( 'read_post', $post->ID ) ) {
979 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
980 }
981 } elseif ( 'trash' === $post->post_status ) {
982 if ( !current_user_can( 'edit_post', $post->ID ) ) {
983 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
984 }
985 } elseif ( 'auto-draft' === $post->post_status ) {
986 //allow auto-drafts
987 } else {
988 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
989 }
990 } else {
991 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
992 }
993 }
994
995 if (
996 -1 == get_option( 'blog_public' ) &&
997 /**
998 * Filter access to a specific post.
999 *
1000 * @module json-api
1001 *
1002 * @since 3.4.0
1003 *
1004 * @param bool current_user_can( 'read_post', $post->ID ) Can the current user access the post.
1005 * @param WP_Post $post Post data.
1006 */
1007 ! apply_filters(
1008 'wpcom_json_api_user_can_view_post',
1009 current_user_can( 'read_post', $post->ID ),
1010 $post
1011 )
1012 ) {
1013 return new WP_Error( 'unauthorized', 'User cannot view post', array( 'status_code' => 403, 'error' => 'private_blog' ) );
1014 }
1015
1016 if ( strlen( $post->post_password ) && !current_user_can( 'edit_post', $post->ID ) ) {
1017 return new WP_Error( 'unauthorized', 'User cannot view password protected post', array( 'status_code' => 403, 'error' => 'password_protected' ) );
1018 }
1019
1020 return true;
1021 }
1022
1023 /**
1024 * Returns author object.
1025 *
1026 * @param $author user ID, user row, WP_User object, comment row, post row
1027 * @param $show_email output the author's email address?
1028 *
1029 * @return (object)
1030 */
1031 function get_author( $author, $show_email = false ) {
1032 if ( isset( $author->comment_author_email ) && !$author->user_id ) {
1033 $ID = 0;
1034 $login = '';
1035 $email = $author->comment_author_email;
1036 $name = $author->comment_author;
1037 $first_name = '';
1038 $last_name = '';
1039 $URL = $author->comment_author_url;
1040 $avatar_URL = $this->api->get_avatar_url( $author );
1041 $profile_URL = 'http://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1042 $nice = '';
1043 $site_id = -1;
1044
1045 // Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1046 // "&" is the only email/URL character altered by wp_kses()
1047 foreach ( array( 'email', 'URL' ) as $field ) {
1048 $$field = str_replace( '&amp;', '&', $$field );
1049 }
1050 } else {
1051 if ( isset( $author->user_id ) && $author->user_id ) {
1052 $author = $author->user_id;
1053 } elseif ( isset( $author->user_email ) ) {
1054 $author = $author->ID;
1055 } elseif ( isset( $author->post_author ) ) {
1056 // then $author is a Post Object.
1057 if ( 0 == $author->post_author )
1058 return null;
1059 /**
1060 * Filter whether the current site is a Jetpack site.
1061 *
1062 * @module json-api
1063 *
1064 * @since 3.3.0
1065 *
1066 * @param bool false Is the current site a Jetpack site. Default to false.
1067 * @param int get_current_blog_id() Blog ID.
1068 */
1069 $is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1070 $post_id = $author->ID;
1071 if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1072 $ID = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1073 $email = get_post_meta( $post_id, '_jetpack_author_email', true );
1074 $login = '';
1075 $name = get_post_meta( $post_id, '_jetpack_author', true );
1076 $first_name = '';
1077 $last_name = '';
1078 $URL = '';
1079 $nice = '';
1080 } else {
1081 $author = $author->post_author;
1082 }
1083 }
1084
1085 if ( ! isset( $ID ) ) {
1086 $user = get_user_by( 'id', $author );
1087 if ( ! $user || is_wp_error( $user ) ) {
1088 trigger_error( 'Unknown user', E_USER_WARNING );
1089
1090 return null;
1091 }
1092 $ID = $user->ID;
1093 $email = $user->user_email;
1094 $login = $user->user_login;
1095 $name = $user->display_name;
1096 $first_name = $user->first_name;
1097 $last_name = $user->last_name;
1098 $URL = $user->user_url;
1099 $nice = $user->user_nicename;
1100 }
1101 if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) {
1102 $active_blog = get_active_blog_for_user( $ID );
1103 $site_id = $active_blog->blog_id;
1104 $profile_URL = "http://en.gravatar.com/{$login}";
1105 } else {
1106 $profile_URL = 'http://en.gravatar.com/' . md5( strtolower( trim( $email ) ) );
1107 $site_id = -1;
1108 }
1109
1110 $avatar_URL = $this->api->get_avatar_url( $email );
1111 }
1112
1113 $email = $show_email ? (string) $email : false;
1114
1115 $author = array(
1116 'ID' => (int) $ID,
1117 'login' => (string) $login,
1118 'email' => $email, // (string|bool)
1119 'name' => (string) $name,
1120 'first_name' => (string) $first_name,
1121 'last_name' => (string) $last_name,
1122 'nice_name' => (string) $nice,
1123 'URL' => (string) esc_url_raw( $URL ),
1124 'avatar_URL' => (string) esc_url_raw( $avatar_URL ),
1125 'profile_URL' => (string) esc_url_raw( $profile_URL ),
1126 );
1127
1128 if ($site_id > -1) {
1129 $author['site_ID'] = (int) $site_id;
1130 }
1131
1132 return (object) $author;
1133 }
1134
1135 function get_media_item( $media_id ) {
1136 $media_item = get_post( $media_id );
1137
1138 if ( !$media_item || is_wp_error( $media_item ) )
1139 return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1140
1141 $response = array(
1142 'id' => strval( $media_item->ID ),
1143 'date' => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1144 'parent' => $media_item->post_parent,
1145 'link' => wp_get_attachment_url( $media_item->ID ),
1146 'title' => $media_item->post_title,
1147 'caption' => $media_item->post_excerpt,
1148 'description' => $media_item->post_content,
1149 'metadata' => wp_get_attachment_metadata( $media_item->ID ),
1150 );
1151
1152 if ( defined( 'IS_WPCOM' ) && IS_WPCOM && is_array( $response['metadata'] ) && ! empty( $response['metadata']['file'] ) ) {
1153 remove_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10 );
1154 $response['metadata']['file'] = _wp_relative_upload_path( $response['metadata']['file'] );
1155 add_filter( '_wp_relative_upload_path', 'wpcom_wp_relative_upload_path', 10, 2 );
1156 }
1157
1158 $response['meta'] = (object) array(
1159 'links' => (object) array(
1160 'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1161 'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1162 'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1163 ),
1164 );
1165
1166 return (object) $response;
1167 }
1168
1169 function get_media_item_v1_1( $media_id ) {
1170 $media_item = get_post( $media_id );
1171
1172 if ( ! $media_item || is_wp_error( $media_item ) )
1173 return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1174
1175 $file = basename( wp_get_attachment_url( $media_item->ID ) );
1176 $file_info = pathinfo( $file );
1177 $ext = $file_info['extension'];
1178
1179 $response = array(
1180 'ID' => $media_item->ID,
1181 'URL' => wp_get_attachment_url( $media_item->ID ),
1182 'guid' => $media_item->guid,
1183 'date' => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1184 'post_ID' => $media_item->post_parent,
1185 'author_ID' => (int) $media_item->post_author,
1186 'file' => $file,
1187 'mime_type' => $media_item->post_mime_type,
1188 'extension' => $ext,
1189 'title' => $media_item->post_title,
1190 'caption' => $media_item->post_excerpt,
1191 'description' => $media_item->post_content,
1192 'alt' => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1193 'thumbnails' => array()
1194 );
1195
1196 if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1197 $metadata = wp_get_attachment_metadata( $media_item->ID );
1198 if ( isset( $metadata['height'], $metadata['width'] ) ) {
1199 $response['height'] = $metadata['height'];
1200 $response['width'] = $metadata['width'];
1201 }
1202
1203 if ( isset( $metadata['sizes'] ) ) {
1204 /**
1205 * Filter the thumbnail sizes available for each attachment ID.
1206 *
1207 * @module json-api
1208 *
1209 * @since 3.9.0
1210 *
1211 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1212 * @param string $media_id Attachment ID.
1213 */
1214 $sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_id );
1215 if ( is_array( $sizes ) ) {
1216 foreach ( $sizes as $size => $size_details ) {
1217 $response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1218 }
1219 }
1220 }
1221
1222 if ( isset( $metadata['image_meta'] ) ) {
1223 $response['exif'] = $metadata['image_meta'];
1224 }
1225 }
1226
1227 if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1228 $metadata = wp_get_attachment_metadata( $media_item->ID );
1229 $response['length'] = $metadata['length'];
1230 $response['exif'] = $metadata;
1231 }
1232
1233 if ( in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) ) ) {
1234 $metadata = wp_get_attachment_metadata( $media_item->ID );
1235 if ( isset( $metadata['height'], $metadata['width'] ) ) {
1236 $response['height'] = $metadata['height'];
1237 $response['width'] = $metadata['width'];
1238 }
1239
1240 if ( isset( $metadata['length'] ) ) {
1241 $response['length'] = $metadata['length'];
1242 }
1243
1244 // add VideoPress info
1245 if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1246 $info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_id );
1247
1248 // Thumbnails
1249 if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1250 $response['thumbnails'] = array( 'fmt_hd' => '', 'fmt_dvd' => '', 'fmt_std' => '' );
1251 foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1252 if ( video_format_done( $info, $size ) ) {
1253 $response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1254 } else {
1255 unset( $response['thumbnails'][ $size ] );
1256 }
1257 }
1258 }
1259
1260 $response['videopress_guid'] = $info->guid;
1261 $response['videopress_processing_done'] = true;
1262 if ( '0000-00-00 00:00:00' == $info->finish_date_gmt ) {
1263 $response['videopress_processing_done'] = false;
1264 }
1265 }
1266 }
1267
1268 $response['thumbnails'] = (object) $response['thumbnails'];
1269
1270 $response['meta'] = (object) array(
1271 'links' => (object) array(
1272 'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id ),
1273 'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ),
1274 'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1275 ),
1276 );
1277
1278 // add VideoPress link to the meta
1279 if ( in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) ) ) {
1280 if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1281 $response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1282 }
1283 }
1284
1285 if ( $media_item->post_parent > 0 ) {
1286 $response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1287 }
1288
1289 return (object) $response;
1290 }
1291
1292 function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1293
1294 $taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1295 /// keep updating this function
1296 if ( !$taxonomy || is_wp_error( $taxonomy ) ) {
1297 return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
1298 }
1299
1300 return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1301 }
1302
1303 function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1304 // Permissions
1305 switch ( $context ) {
1306 case 'edit' :
1307 $tax = get_taxonomy( $taxonomy_type );
1308 if ( !current_user_can( $tax->cap->edit_terms ) )
1309 return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
1310 break;
1311 case 'display' :
1312 if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1313 return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
1314 }
1315 break;
1316 default :
1317 return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1318 }
1319
1320 $response = array();
1321 $response['ID'] = (int) $taxonomy->term_id;
1322 $response['name'] = (string) $taxonomy->name;
1323 $response['slug'] = (string) $taxonomy->slug;
1324 $response['description'] = (string) $taxonomy->description;
1325 $response['post_count'] = (int) $taxonomy->count;
1326
1327 if ( is_taxonomy_hierarchical( $taxonomy_type ) ) {
1328 $response['parent'] = (int) $taxonomy->parent;
1329 }
1330
1331 $response['meta'] = (object) array(
1332 'links' => (object) array(
1333 'self' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1334 'help' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1335 'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1336 ),
1337 );
1338
1339 return (object) $response;
1340 }
1341
1342 /**
1343 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1344 *
1345 * @param $date_gmt (string) GMT datetime string.
1346 * @param $date (string) Optional. Used to calculate the offset from GMT.
1347 *
1348 * @return string
1349 */
1350 function format_date( $date_gmt, $date = null ) {
1351 return WPCOM_JSON_API_Date::format_date( $date_gmt, $date );
1352 }
1353
1354 /**
1355 * Parses a date string and returns the local and GMT representations
1356 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1357 * timezones or offsets. If the parsed datetime was not localized to a
1358 * particular timezone or offset we will assume it was given in GMT
1359 * relative to now and will convert it to local time using either the
1360 * timezone set in the options table for the blog or the GMT offset.
1361 *
1362 * @param datetime string
1363 *
1364 * @return array( $local_time_string, $gmt_time_string )
1365 */
1366 function parse_date( $date_string ) {
1367 $date_string_info = date_parse( $date_string );
1368 if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1369 // Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1370 if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1371 $dt_local = clone $dt_utc = new DateTime( $date_string );
1372 $dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1373 return array(
1374 (string) $dt_local->format( 'Y-m-d H:i:s' ),
1375 (string) $dt_utc->format( 'Y-m-d H:i:s' ),
1376 );
1377 }
1378
1379 // It's parseable but no TZ info so assume UTC
1380 $dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1381 } else {
1382 // Could not parse time, use now in UTC
1383 $dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1384 }
1385
1386 // First try to use timezone as it's daylight savings aware.
1387 $timezone_string = get_option( 'timezone_string' );
1388 if ( $timezone_string ) {
1389 $tz = timezone_open( $timezone_string );
1390 if ( $tz ) {
1391 $dt_local->setTimezone( $tz );
1392 return array(
1393 (string) $dt_local->format( 'Y-m-d H:i:s' ),
1394 (string) $dt_utc->format( 'Y-m-d H:i:s' ),
1395 );
1396 }
1397 }
1398
1399 // Fallback to GMT offset (in hours)
1400 // NOTE: TZ of $dt_local is still UTC, we simply modified the timestamp with an offset.
1401 $gmt_offset_seconds = intval( get_option( 'gmt_offset' ) * 3600 );
1402 $dt_local->modify("+{$gmt_offset_seconds} seconds");
1403 return array(
1404 (string) $dt_local->format( 'Y-m-d H:i:s' ),
1405 (string) $dt_utc->format( 'Y-m-d H:i:s' ),
1406 );
1407 }
1408
1409 // Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1410 function load_theme_functions() {
1411 // bail if we've done this already (can happen when calling /batch endpoint)
1412 if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) )
1413 return;
1414
1415 // VIP context loading is handled elsewhere, so bail to prevent
1416 // duplicate loading. See `switch_to_blog_and_validate_user()`
1417 if ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ) {
1418 return;
1419 }
1420
1421 define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1422
1423 // the theme info we care about is found either within functions.php or one of the jetpack files.
1424 $function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1425
1426 $copy_dirs = array( get_template_directory() );
1427
1428 // Is this a child theme? Load the child theme's functions file.
1429 if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1430 foreach ( $function_files as $function_file ) {
1431 if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1432 require_once( get_stylesheet_directory() . $function_file );
1433 }
1434 }
1435 $copy_dirs[] = get_stylesheet_directory();
1436 }
1437
1438 foreach ( $function_files as $function_file ) {
1439 if ( file_exists( get_template_directory() . $function_file ) ) {
1440 require_once( get_template_directory() . $function_file );
1441 }
1442 }
1443
1444 // add inc/wpcom.php and/or includes/wpcom.php
1445 wpcom_load_theme_compat_file();
1446
1447 // since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1448 $this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1449
1450 /**
1451 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1452 *
1453 * The REST API does not load the theme when processing requests.
1454 * To enable theme-based functionality, the API will load the '/functions.php',
1455 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1456 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1457 *
1458 * @module json-api
1459 *
1460 * @since 3.2.0
1461 */
1462 do_action( 'restapi_theme_after_setup_theme' );
1463 $this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1464
1465 /**
1466 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1467 *
1468 * The REST API does not load the theme when processing requests.
1469 * To enable theme-based functionality, the API will load the '/functions.php',
1470 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1471 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1472 *
1473 * @module json-api
1474 *
1475 * @since 3.2.0
1476 */
1477 do_action( 'restapi_theme_init' );
1478 }
1479
1480 function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1481 global $wp_filter;
1482 foreach ( $wp_filter as $hook => $actions ) {
1483 if ( $from_hook <> $hook )
1484 continue;
1485 foreach ( (array) $actions as $priority => $callbacks ) {
1486 foreach( $callbacks as $callback_key => $callback_data ) {
1487 $callback = $callback_data['function'];
1488 $reflection = $this->get_reflection( $callback ); // use reflection api to determine filename where function is defined
1489 if ( false !== $reflection ) {
1490 $file_name = $reflection->getFileName();
1491 foreach( $base_paths as $base_path ) {
1492 if ( 0 === strpos( $file_name, $base_path ) ) { // only copy hooks with functions which are part of the specified files
1493 $wp_filter[ $to_hook ][ $priority ][ 'cph' . $callback_key ] = $callback_data;
1494 }
1495 }
1496 }
1497 }
1498 }
1499 }
1500 }
1501
1502 function get_reflection( $callback ) {
1503 if ( is_array( $callback ) ) {
1504 list( $class, $method ) = $callback;
1505 return new ReflectionMethod( $class, $method );
1506 }
1507
1508 if ( is_string( $callback ) && strpos( $callback, "::" ) !== false ) {
1509 list( $class, $method ) = explode( "::", $callback );
1510 return new ReflectionMethod( $class, $method );
1511 }
1512
1513 if ( version_compare( PHP_VERSION, "5.3.0", ">=" ) && method_exists( $callback, "__invoke" ) ) {
1514 return new ReflectionMethod( $callback, "__invoke" );
1515 }
1516
1517 if ( is_string( $callback ) && strpos( $callback, "::" ) == false && function_exists( $callback ) ) {
1518 return new ReflectionFunction( $callback );
1519 }
1520
1521 return false;
1522 }
1523
1524 /**
1525 * Check whether a user can view or edit a post type
1526 * @param string $post_type post type to check
1527 * @param string $context 'display' or 'edit'
1528 * @return bool
1529 */
1530 function current_user_can_access_post_type( $post_type, $context='display' ) {
1531 $post_type_object = get_post_type_object( $post_type );
1532 if ( ! $post_type_object ) {
1533 return false;
1534 }
1535
1536 switch( $context ) {
1537 case 'edit':
1538 return current_user_can( $post_type_object->cap->edit_posts );
1539 case 'display':
1540 return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1541 default:
1542 return false;
1543 }
1544 }
1545
1546 function is_post_type_allowed( $post_type ) {
1547 // if the post type is empty, that's fine, WordPress will default to post
1548 if ( empty( $post_type ) )
1549 return true;
1550
1551 // allow special 'any' type
1552 if ( 'any' == $post_type )
1553 return true;
1554
1555 // check for allowed types
1556 if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) )
1557 return true;
1558
1559 return false;
1560 }
1561
1562 /**
1563 * Gets the whitelisted post types that JP should allow access to.
1564 *
1565 * @return array Whitelisted post types.
1566 */
1567 protected function _get_whitelisted_post_types() {
1568 $allowed_types = array( 'post', 'page', 'revision' );
1569
1570 /**
1571 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1572 *
1573 * @module json-api
1574 *
1575 * @since 2.2.3
1576 *
1577 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1578 */
1579 $allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1580
1581 return array_unique( $allowed_types );
1582 }
1583
1584 function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1585
1586 add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1587
1588 $media_ids = $errors = array();
1589 $user_can_upload_files = current_user_can( 'upload_files' );
1590 $media_attrs = array_values( $media_attrs ); // reset the keys
1591 $i = 0;
1592
1593 if ( ! empty( $media_files ) ) {
1594 $this->api->trap_wp_die( 'upload_error' );
1595 foreach ( $media_files as $media_item ) {
1596 $_FILES['.api.media.item.'] = $media_item;
1597 if ( ! $user_can_upload_files ) {
1598 $media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1599 } else {
1600 if ( $force_parent_id ) {
1601 $parent_id = absint( $force_parent_id );
1602 } elseif ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1603 $parent_id = absint( $media_attrs[$i]['parent_id'] );
1604 } else {
1605 $parent_id = 0;
1606 }
1607 $media_id = media_handle_upload( '.api.media.item.', $parent_id );
1608 }
1609 if ( is_wp_error( $media_id ) ) {
1610 $errors[$i]['file'] = $media_item['name'];
1611 $errors[$i]['error'] = $media_id->get_error_code();
1612 $errors[$i]['message'] = $media_id->get_error_message();
1613 } else {
1614 $media_ids[$i] = $media_id;
1615 }
1616
1617 $i++;
1618 }
1619 $this->api->trap_wp_die( null );
1620 unset( $_FILES['.api.media.item.'] );
1621 }
1622
1623 if ( ! empty( $media_urls ) ) {
1624 foreach ( $media_urls as $url ) {
1625 if ( ! $user_can_upload_files ) {
1626 $media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1627 } else {
1628 if ( $force_parent_id ) {
1629 $parent_id = absint( $force_parent_id );
1630 } else if ( ! empty( $media_attrs[$i] ) && ! empty( $media_attrs[$i]['parent_id'] ) ) {
1631 $parent_id = absint( $media_attrs[$i]['parent_id'] );
1632 } else {
1633 $parent_id = 0;
1634 }
1635 $media_id = $this->handle_media_sideload( $url, $parent_id );
1636 }
1637 if ( is_wp_error( $media_id ) ) {
1638 $errors[$i] = array(
1639 'file' => $url,
1640 'error' => $media_id->get_error_code(),
1641 'message' => $media_id->get_error_message(),
1642 );
1643 } elseif ( ! empty( $media_id ) ) {
1644 $media_ids[$i] = $media_id;
1645 }
1646
1647 $i++;
1648 }
1649 }
1650
1651 if ( ! empty( $media_attrs ) ) {
1652 foreach ( $media_ids as $index => $media_id ) {
1653 if ( empty( $media_attrs[$index] ) )
1654 continue;
1655
1656 $attrs = $media_attrs[$index];
1657 $insert = array();
1658
1659 // Attributes: Title, Caption, Description
1660
1661 if ( isset( $attrs['title'] ) ) {
1662 $insert['post_title'] = $attrs['title'];
1663 }
1664
1665 if ( isset( $attrs['caption'] ) ) {
1666 $insert['post_excerpt'] = $attrs['caption'];
1667 }
1668
1669 if ( isset( $attrs['description'] ) ) {
1670 $insert['post_content'] = $attrs['description'];
1671 }
1672
1673 if ( ! empty( $insert ) ) {
1674 $insert['ID'] = $media_id;
1675 wp_update_post( (object) $insert );
1676 }
1677
1678 // Attributes: Alt
1679
1680 if ( isset( $attrs['alt'] ) ) {
1681 $alt = wp_strip_all_tags( $attrs['alt'], true );
1682 update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
1683 }
1684
1685 // Attributes: Artist, Album
1686
1687 $id3_meta = array();
1688
1689 foreach ( array( 'artist', 'album' ) as $key ) {
1690 if ( isset( $attrs[ $key ] ) ) {
1691 $id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
1692 }
1693 }
1694
1695 if ( ! empty( $id3_meta ) ) {
1696 // Before updating metadata, ensure that the item is audio
1697 $item = $this->get_media_item_v1_1( $media_id );
1698 if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
1699 wp_update_attachment_metadata( $media_id, $id3_meta );
1700 }
1701 }
1702 }
1703 }
1704
1705 return array( 'media_ids' => $media_ids, 'errors' => $errors );
1706
1707 }
1708
1709 function handle_media_sideload( $url, $parent_post_id = 0, $type = 'any' ) {
1710 if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) )
1711 return false;
1712
1713 // if we didn't get a URL, let's bail
1714 $parsed = @parse_url( $url );
1715 if ( empty( $parsed ) )
1716 return false;
1717
1718 $tmp = download_url( $url );
1719 if ( is_wp_error( $tmp ) ) {
1720 return $tmp;
1721 }
1722
1723 // First check to see if we get a mime-type match by file, otherwise, check to
1724 // see if WordPress supports this file as an image. If neither, then it is not supported.
1725 if ( ! $this->is_file_supported_for_sideloading( $tmp ) && 'image' === $type && ! file_is_displayable_image( $tmp ) ) {
1726 @unlink( $tmp );
1727 return false;
1728 }
1729
1730 // emulate a $_FILES entry
1731 $file_array = array(
1732 'name' => basename( parse_url( $url, PHP_URL_PATH ) ),
1733 'tmp_name' => $tmp,
1734 );
1735
1736 $id = media_handle_sideload( $file_array, $parent_post_id );
1737 @unlink( $tmp );
1738
1739 if ( is_wp_error( $id ) ) {
1740 return $id;
1741 }
1742
1743 if ( ! $id || ! is_int( $id ) ) {
1744 return false;
1745 }
1746
1747 return $id;
1748 }
1749
1750 /**
1751 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
1752 *
1753 * @param string $file Path to file to get its mime type.
1754 *
1755 * @return bool
1756 */
1757 protected function is_file_supported_for_sideloading( $file ) {
1758 if ( class_exists( 'finfo' ) ) { // php 5.3+
1759 $finfo = new finfo( FILEINFO_MIME );
1760 $mime = explode( '; ', $finfo->file( $file ) );
1761 $type = $mime[0];
1762
1763 } elseif ( function_exists( 'mime_content_type' ) ) { // PHP 5.2
1764 $type = mime_content_type( $file );
1765
1766 } else {
1767 return false;
1768 }
1769
1770 /**
1771 * Filter the list of supported mime types for media sideloading.
1772 *
1773 * @since 4.0.0
1774 *
1775 * @module json-api
1776 *
1777 * @param array $supported_mime_types Array of the supported mime types for media sideloading.
1778 */
1779 $supported_mime_types = apply_filters( 'jetpack_supported_media_sideload_types', array(
1780 'image/png',
1781 'image/jpeg',
1782 'image/gif',
1783 'image/bmp',
1784 'video/quicktime',
1785 'video/mp4',
1786 'video/mpeg',
1787 'video/ogg',
1788 'video/3gpp',
1789 'video/3gpp2',
1790 'video/h261',
1791 'video/h262',
1792 'video/h264',
1793 'video/x-msvideo',
1794 'video/x-ms-wmv',
1795 'video/x-ms-asf',
1796 ) );
1797
1798 // If the type returned was not an array as expected, then we know we don't have a match.
1799 if ( ! is_array( $supported_mime_types ) ) {
1800 return false;
1801 }
1802
1803 return in_array( $type, $supported_mime_types );
1804 }
1805
1806 function allow_video_uploads( $mimes ) {
1807 // if we are on Jetpack, bail - Videos are already allowed
1808 if ( ! defined( 'IS_WPCOM' ) || !IS_WPCOM ) {
1809 return $mimes;
1810 }
1811
1812 // extra check that this filter is only ever applied during REST API requests
1813 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
1814 return $mimes;
1815 }
1816
1817 // bail early if they already have the upgrade..
1818 if ( get_option( 'video_upgrade' ) == '1' ) {
1819 return $mimes;
1820 }
1821
1822 // lets whitelist to only specific clients right now
1823 $clients_allowed_video_uploads = array();
1824 /**
1825 * Filter the list of whitelisted video clients.
1826 *
1827 * @module json-api
1828 *
1829 * @since 3.2.0
1830 *
1831 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
1832 */
1833 $clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
1834 if ( !in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
1835 return $mimes;
1836 }
1837
1838 $mime_list = wp_get_mime_types();
1839
1840 $video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
1841 /**
1842 * Filter the video filetypes allowed on the site.
1843 *
1844 * @module json-api
1845 *
1846 * @since 3.2.0
1847 *
1848 * @param array $video_exts Array of video filetypes allowed on the site.
1849 */
1850 $video_exts = apply_filters( 'video_upload_filetypes', $video_exts );
1851 $video_mimes = array();
1852
1853 if ( !empty( $video_exts ) ) {
1854 foreach ( $video_exts as $ext ) {
1855 foreach ( $mime_list as $ext_pattern => $mime ) {
1856 if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false )
1857 $video_mimes[$ext_pattern] = $mime;
1858 }
1859 }
1860
1861 $mimes = array_merge( $mimes, $video_mimes );
1862 }
1863
1864 return $mimes;
1865 }
1866
1867 function is_current_site_multi_user() {
1868 $users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
1869 if ( false === $users ) {
1870 $user_query = new WP_User_Query( array(
1871 'blog_id' => get_current_blog_id(),
1872 'fields' => 'ID',
1873 ) );
1874 $users = (int) $user_query->get_total();
1875 wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
1876 }
1877 return $users > 1;
1878 }
1879
1880 function allows_cross_origin_requests() {
1881 return 'GET' == $this->method || $this->allow_cross_origin_request;
1882 }
1883
1884 function allows_unauthorized_requests( $origin, $complete_access_origins ) {
1885 return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
1886 }
1887
1888 function get_platform() {
1889 return wpcom_get_sal_platform( $this->api->token_details );
1890 }
1891
1892 /**
1893 * Return endpoint response
1894 *
1895 * @param ... determined by ->$path
1896 *
1897 * @return
1898 * falsy: HTTP 500, no response body
1899 * WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
1900 * $data: HTTP 200, json_encode( $data ) response body
1901 */
1902 abstract function callback( $path = '' );
1903
1904
1905 }
1906
1907 require_once( dirname( __FILE__ ) . '/json-endpoints.php' );
1908