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