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