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