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

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

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