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

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

2,176 lines 70.1 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 // File operations are handled differently on WordPress.com.
1348 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
1349 $attachment_metadata = wp_get_attachment_metadata( $media_item->ID );
1350 $filesize = ! empty( $attachment_metadata['filesize'] )
1351 ? $attachment_metadata['filesize']
1352 : 0;
1353 } else {
1354 $filesize = filesize( $attachment_file );
1355 }
1356
1357 $response = array(
1358 'ID' => $media_item->ID,
1359 'URL' => wp_get_attachment_url( $media_item->ID ),
1360 'guid' => $media_item->guid,
1361 'date' => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1362 'post_ID' => $media_item->post_parent,
1363 'author_ID' => (int) $media_item->post_author,
1364 'file' => $file,
1365 'mime_type' => $media_item->post_mime_type,
1366 'extension' => $ext,
1367 'title' => $media_item->post_title,
1368 'caption' => $media_item->post_excerpt,
1369 'description' => $media_item->post_content,
1370 'alt' => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1371 'icon' => wp_mime_type_icon( $media_item->ID ),
1372 'size' => size_format( (int) $filesize, 2 ),
1373 'thumbnails' => array(),
1374 );
1375
1376 if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif' ) ) ) {
1377 $metadata = wp_get_attachment_metadata( $media_item->ID );
1378 if ( isset( $metadata['height'], $metadata['width'] ) ) {
1379 $response['height'] = $metadata['height'];
1380 $response['width'] = $metadata['width'];
1381 }
1382
1383 if ( isset( $metadata['sizes'] ) ) {
1384 /**
1385 * Filter the thumbnail sizes available for each attachment ID.
1386 *
1387 * @module json-api
1388 *
1389 * @since 3.9.0
1390 *
1391 * @param array $metadata['sizes'] Array of thumbnail sizes available for a given attachment ID.
1392 * @param string $media_id Attachment ID.
1393 */
1394 $sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_item->ID );
1395 if ( is_array( $sizes ) ) {
1396 foreach ( $sizes as $size => $size_details ) {
1397 $response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1398 }
1399 /**
1400 * Filter the thumbnail URLs for attachment files.
1401 *
1402 * @module json-api
1403 *
1404 * @since 7.1.0
1405 *
1406 * @param array $metadata['sizes'] Array with thumbnail sizes as keys and URLs as values.
1407 */
1408 $response['thumbnails'] = apply_filters( 'rest_api_thumbnail_size_urls', $response['thumbnails'] );
1409 }
1410 }
1411
1412 if ( isset( $metadata['image_meta'] ) ) {
1413 $response['exif'] = $metadata['image_meta'];
1414 }
1415 }
1416
1417 if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ) ) ) {
1418 $metadata = wp_get_attachment_metadata( $media_item->ID );
1419 $response['length'] = $metadata['length'];
1420 $response['exif'] = $metadata;
1421 }
1422
1423 $is_video = false;
1424
1425 if (
1426 in_array( $ext, array( 'ogv', 'mp4', 'mov', 'wmv', 'avi', 'mpg', '3gp', '3g2', 'm4v' ) )
1427 ||
1428 $response['mime_type'] === 'video/videopress'
1429 ) {
1430 $is_video = true;
1431 }
1432
1433 if ( $is_video ) {
1434 $metadata = wp_get_attachment_metadata( $media_item->ID );
1435
1436 if ( isset( $metadata['height'], $metadata['width'] ) ) {
1437 $response['height'] = $metadata['height'];
1438 $response['width'] = $metadata['width'];
1439 }
1440
1441 if ( isset( $metadata['length'] ) ) {
1442 $response['length'] = $metadata['length'];
1443 }
1444
1445 // add VideoPress info
1446 if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1447 $info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_item->ID );
1448
1449 // If we failed to get VideoPress info, but it exists in the meta data (for some reason)
1450 // then let's use that.
1451 if ( false === $info && isset( $metadata['videopress'] ) ) {
1452 $info = (object) $metadata['videopress'];
1453 }
1454
1455 // Thumbnails
1456 if ( function_exists( 'video_format_done' ) && function_exists( 'video_image_url_by_guid' ) ) {
1457 $response['thumbnails'] = array(
1458 'fmt_hd' => '',
1459 'fmt_dvd' => '',
1460 'fmt_std' => '',
1461 );
1462 foreach ( $response['thumbnails'] as $size => $thumbnail_url ) {
1463 if ( video_format_done( $info, $size ) ) {
1464 $response['thumbnails'][ $size ] = video_image_url_by_guid( $info->guid, $size );
1465 } else {
1466 unset( $response['thumbnails'][ $size ] );
1467 }
1468 }
1469 }
1470
1471 // If we didn't get VideoPress information (for some reason) then let's
1472 // not try and include it in the response.
1473 if ( isset( $info->guid ) ) {
1474 $response['videopress_guid'] = $info->guid;
1475 $response['videopress_processing_done'] = true;
1476 if ( '0000-00-00 00:00:00' === $info->finish_date_gmt ) {
1477 $response['videopress_processing_done'] = false;
1478 }
1479 }
1480 }
1481 }
1482
1483 $response['thumbnails'] = (object) $response['thumbnails'];
1484
1485 $response['meta'] = (object) array(
1486 'links' => (object) array(
1487 'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID ),
1488 'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID, 'help' ),
1489 'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1490 ),
1491 );
1492
1493 // add VideoPress link to the meta
1494 if ( isset( $response['videopress_guid'] ) ) {
1495 if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1496 $response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1497 }
1498 }
1499
1500 if ( $media_item->post_parent > 0 ) {
1501 $response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1502 }
1503
1504 return (object) $response;
1505 }
1506
1507 function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) {
1508
1509 $taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type );
1510 // keep updating this function
1511 if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1512 return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 );
1513 }
1514
1515 return $this->format_taxonomy( $taxonomy, $taxonomy_type, $context );
1516 }
1517
1518 function format_taxonomy( $taxonomy, $taxonomy_type, $context ) {
1519 // Permissions
1520 switch ( $context ) {
1521 case 'edit':
1522 $tax = get_taxonomy( $taxonomy_type );
1523 if ( ! current_user_can( $tax->cap->edit_terms ) ) {
1524 return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 );
1525 }
1526 break;
1527 case 'display':
1528 if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
1529 return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 );
1530 }
1531 break;
1532 default:
1533 return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
1534 }
1535
1536 $response = array();
1537 $response['ID'] = (int) $taxonomy->term_id;
1538 $response['name'] = (string) $taxonomy->name;
1539 $response['slug'] = (string) $taxonomy->slug;
1540 $response['description'] = (string) $taxonomy->description;
1541 $response['post_count'] = (int) $taxonomy->count;
1542 $response['feed_url'] = get_term_feed_link( $taxonomy->term_id, $taxonomy_type );
1543
1544 if ( is_taxonomy_hierarchical( $taxonomy_type ) ) {
1545 $response['parent'] = (int) $taxonomy->parent;
1546 }
1547
1548 $response['meta'] = (object) array(
1549 'links' => (object) array(
1550 'self' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type ),
1551 'help' => (string) $this->links->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy->slug, $taxonomy_type, 'help' ),
1552 'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1553 ),
1554 );
1555
1556 return (object) $response;
1557 }
1558
1559 /**
1560 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
1561 *
1562 * @param $date_gmt (string) GMT datetime string.
1563 * @param $date (string) Optional. Used to calculate the offset from GMT.
1564 *
1565 * @return string
1566 */
1567 function format_date( $date_gmt, $date = null ) {
1568 return WPCOM_JSON_API_Date::format_date( $date_gmt, $date );
1569 }
1570
1571 /**
1572 * Parses a date string and returns the local and GMT representations
1573 * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without
1574 * timezones or offsets. If the parsed datetime was not localized to a
1575 * particular timezone or offset we will assume it was given in GMT
1576 * relative to now and will convert it to local time using either the
1577 * timezone set in the options table for the blog or the GMT offset.
1578 *
1579 * @param datetime string $date_string Date to parse.
1580 *
1581 * @return array( $local_time_string, $gmt_time_string )
1582 */
1583 public function parse_date( $date_string ) {
1584 $date_string_info = date_parse( $date_string );
1585 if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1586 // Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1587 if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1588 $dt_utc = new DateTime( $date_string );
1589 $dt_local = clone $dt_utc;
1590 $dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1591 return array(
1592 (string) $dt_local->format( 'Y-m-d H:i:s' ),
1593 (string) $dt_utc->format( 'Y-m-d H:i:s' ),
1594 );
1595 }
1596
1597 // It's parseable but no TZ info so assume UTC.
1598 $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) );
1599 $dt_local = clone $dt_utc;
1600 } else {
1601 // Could not parse time, use now in UTC.
1602 $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) );
1603 $dt_local = clone $dt_utc;
1604 }
1605
1606 $dt_local->setTimezone( wp_timezone() );
1607
1608 return array(
1609 (string) $dt_local->format( 'Y-m-d H:i:s' ),
1610 (string) $dt_utc->format( 'Y-m-d H:i:s' ),
1611 );
1612 }
1613
1614 // Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1615 function load_theme_functions() {
1616 if ( false === defined( 'STYLESHEETPATH' ) ) {
1617 wp_templating_constants();
1618 }
1619
1620 // bail if we've done this already (can happen when calling /batch endpoint)
1621 if ( defined( 'REST_API_THEME_FUNCTIONS_LOADED' ) ) {
1622 return;
1623 }
1624
1625 // VIP context loading is handled elsewhere, so bail to prevent
1626 // duplicate loading. See `switch_to_blog_and_validate_user()`
1627 if ( defined( 'WPCOM_IS_VIP_ENV' ) && WPCOM_IS_VIP_ENV ) {
1628 return;
1629 }
1630
1631 define( 'REST_API_THEME_FUNCTIONS_LOADED', true );
1632
1633 // the theme info we care about is found either within functions.php or one of the jetpack files.
1634 $function_files = array( '/functions.php', '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php' );
1635
1636 $copy_dirs = array( get_template_directory() );
1637
1638 // Is this a child theme? Load the child theme's functions file.
1639 if ( get_stylesheet_directory() !== get_template_directory() && wpcom_is_child_theme() ) {
1640 foreach ( $function_files as $function_file ) {
1641 if ( file_exists( get_stylesheet_directory() . $function_file ) ) {
1642 require_once get_stylesheet_directory() . $function_file;
1643 }
1644 }
1645 $copy_dirs[] = get_stylesheet_directory();
1646 }
1647
1648 foreach ( $function_files as $function_file ) {
1649 if ( file_exists( get_template_directory() . $function_file ) ) {
1650 require_once get_template_directory() . $function_file;
1651 }
1652 }
1653
1654 // add inc/wpcom.php and/or includes/wpcom.php
1655 wpcom_load_theme_compat_file();
1656
1657 // Enable including additional directories or files in actions to be copied
1658 $copy_dirs = apply_filters( 'restapi_theme_action_copy_dirs', $copy_dirs );
1659
1660 // since the stuff we care about (CPTS, post formats, are usually on setup or init hooks, we want to load those)
1661 $this->copy_hooks( 'after_setup_theme', 'restapi_theme_after_setup_theme', $copy_dirs );
1662
1663 /**
1664 * Fires functions hooked onto `after_setup_theme` by the theme for the purpose of the REST API.
1665 *
1666 * The REST API does not load the theme when processing requests.
1667 * To enable theme-based functionality, the API will load the '/functions.php',
1668 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1669 * of the theme (parent and child) and copy functions hooked onto 'after_setup_theme' within those files.
1670 *
1671 * @module json-api
1672 *
1673 * @since 3.2.0
1674 */
1675 do_action( 'restapi_theme_after_setup_theme' );
1676 $this->copy_hooks( 'init', 'restapi_theme_init', $copy_dirs );
1677
1678 /**
1679 * Fires functions hooked onto `init` by the theme for the purpose of the REST API.
1680 *
1681 * The REST API does not load the theme when processing requests.
1682 * To enable theme-based functionality, the API will load the '/functions.php',
1683 * '/inc/jetpack.compat.php', '/inc/jetpack.php', '/includes/jetpack.compat.php files
1684 * of the theme (parent and child) and copy functions hooked onto 'init' within those files.
1685 *
1686 * @module json-api
1687 *
1688 * @since 3.2.0
1689 */
1690 do_action( 'restapi_theme_init' );
1691 }
1692
1693 function copy_hooks( $from_hook, $to_hook, $base_paths ) {
1694 global $wp_filter;
1695 foreach ( $wp_filter as $hook => $actions ) {
1696
1697 if ( $from_hook != $hook ) {
1698 continue;
1699 }
1700 if ( ! has_action( $hook ) ) {
1701 continue;
1702 }
1703
1704 foreach ( $actions as $priority => $callbacks ) {
1705 foreach ( $callbacks as $callback_key => $callback_data ) {
1706 $callback = $callback_data['function'];
1707
1708 // use reflection api to determine filename where function is defined
1709 $reflection = $this->get_reflection( $callback );
1710
1711 if ( false !== $reflection ) {
1712 $file_name = $reflection->getFileName();
1713 foreach ( $base_paths as $base_path ) {
1714
1715 // only copy hooks with functions which are part of the specified files
1716 if ( 0 === strpos( $file_name, $base_path ) ) {
1717 add_action(
1718 $to_hook,
1719 $callback_data['function'],
1720 $priority,
1721 $callback_data['accepted_args']
1722 );
1723 }
1724 }
1725 }
1726 }
1727 }
1728 }
1729 }
1730
1731 function get_reflection( $callback ) {
1732 if ( is_array( $callback ) ) {
1733 list( $class, $method ) = $callback;
1734 return new ReflectionMethod( $class, $method );
1735 }
1736
1737 if ( is_string( $callback ) && strpos( $callback, '::' ) !== false ) {
1738 list( $class, $method ) = explode( '::', $callback );
1739 return new ReflectionMethod( $class, $method );
1740 }
1741
1742 if ( method_exists( $callback, "__invoke" ) ) {
1743 return new ReflectionMethod( $callback, "__invoke" );
1744 }
1745
1746 if ( is_string( $callback ) && strpos( $callback, '::' ) == false && function_exists( $callback ) ) {
1747 return new ReflectionFunction( $callback );
1748 }
1749
1750 return false;
1751 }
1752
1753 /**
1754 * Check whether a user can view or edit a post type
1755 *
1756 * @param string $post_type post type to check
1757 * @param string $context 'display' or 'edit'
1758 * @return bool
1759 */
1760 function current_user_can_access_post_type( $post_type, $context = 'display' ) {
1761 $post_type_object = get_post_type_object( $post_type );
1762 if ( ! $post_type_object ) {
1763 return false;
1764 }
1765
1766 switch ( $context ) {
1767 case 'edit':
1768 return current_user_can( $post_type_object->cap->edit_posts );
1769 case 'display':
1770 return $post_type_object->public || current_user_can( $post_type_object->cap->read_private_posts );
1771 default:
1772 return false;
1773 }
1774 }
1775
1776 function is_post_type_allowed( $post_type ) {
1777 // if the post type is empty, that's fine, WordPress will default to post
1778 if ( empty( $post_type ) ) {
1779 return true;
1780 }
1781
1782 // allow special 'any' type
1783 if ( 'any' == $post_type ) {
1784 return true;
1785 }
1786
1787 // check for allowed types
1788 if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) {
1789 return true;
1790 }
1791
1792 if ( $post_type_object = get_post_type_object( $post_type ) ) {
1793 if ( ! empty( $post_type_object->show_in_rest ) ) {
1794 return $post_type_object->show_in_rest;
1795 }
1796 if ( ! empty( $post_type_object->publicly_queryable ) ) {
1797 return $post_type_object->publicly_queryable;
1798 }
1799 }
1800
1801 return ! empty( $post_type_object->public );
1802 }
1803
1804 /**
1805 * Gets the whitelisted post types that JP should allow access to.
1806 *
1807 * @return array Whitelisted post types.
1808 */
1809 protected function _get_whitelisted_post_types() {
1810 $allowed_types = array( 'post', 'page', 'revision' );
1811
1812 /**
1813 * Filter the post types Jetpack has access to, and can synchronize with WordPress.com.
1814 *
1815 * @module json-api
1816 *
1817 * @since 2.2.3
1818 *
1819 * @param array $allowed_types Array of whitelisted post types. Default to `array( 'post', 'page', 'revision' )`.
1820 */
1821 $allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types );
1822
1823 return array_unique( $allowed_types );
1824 }
1825
1826 function handle_media_creation_v1_1( $media_files, $media_urls, $media_attrs = array(), $force_parent_id = false ) {
1827
1828 add_filter( 'upload_mimes', array( $this, 'allow_video_uploads' ) );
1829
1830 $media_ids = $errors = array();
1831 $user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token();
1832 $media_attrs = array_values( $media_attrs ); // reset the keys
1833 $i = 0;
1834
1835 if ( ! empty( $media_files ) ) {
1836 $this->api->trap_wp_die( 'upload_error' );
1837 foreach ( $media_files as $media_item ) {
1838 $_FILES['.api.media.item.'] = $media_item;
1839 if ( ! $user_can_upload_files ) {
1840 $media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1841 } else {
1842 if ( $force_parent_id ) {
1843 $parent_id = absint( $force_parent_id );
1844 } elseif ( ! empty( $media_attrs[ $i ] ) && ! empty( $media_attrs[ $i ]['parent_id'] ) ) {
1845 $parent_id = absint( $media_attrs[ $i ]['parent_id'] );
1846 } else {
1847 $parent_id = 0;
1848 }
1849 $media_id = media_handle_upload( '.api.media.item.', $parent_id );
1850 }
1851 if ( is_wp_error( $media_id ) ) {
1852 $errors[ $i ]['file'] = $media_item['name'];
1853 $errors[ $i ]['error'] = $media_id->get_error_code();
1854 $errors[ $i ]['message'] = $media_id->get_error_message();
1855 } else {
1856 $media_ids[ $i ] = $media_id;
1857 }
1858
1859 $i++;
1860 }
1861 $this->api->trap_wp_die( null );
1862 unset( $_FILES['.api.media.item.'] );
1863 }
1864
1865 if ( ! empty( $media_urls ) ) {
1866 foreach ( $media_urls as $url ) {
1867 if ( ! $user_can_upload_files ) {
1868 $media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
1869 } else {
1870 if ( $force_parent_id ) {
1871 $parent_id = absint( $force_parent_id );
1872 } elseif ( ! empty( $media_attrs[ $i ] ) && ! empty( $media_attrs[ $i ]['parent_id'] ) ) {
1873 $parent_id = absint( $media_attrs[ $i ]['parent_id'] );
1874 } else {
1875 $parent_id = 0;
1876 }
1877 $media_id = $this->handle_media_sideload( $url, $parent_id );
1878 }
1879 if ( is_wp_error( $media_id ) ) {
1880 $errors[ $i ] = array(
1881 'file' => $url,
1882 'error' => $media_id->get_error_code(),
1883 'message' => $media_id->get_error_message(),
1884 );
1885 } elseif ( ! empty( $media_id ) ) {
1886 $media_ids[ $i ] = $media_id;
1887 }
1888
1889 $i++;
1890 }
1891 }
1892
1893 if ( ! empty( $media_attrs ) ) {
1894 foreach ( $media_ids as $index => $media_id ) {
1895 if ( empty( $media_attrs[ $index ] ) ) {
1896 continue;
1897 }
1898
1899 $attrs = $media_attrs[ $index ];
1900 $insert = array();
1901
1902 // Attributes: Title, Caption, Description
1903
1904 if ( isset( $attrs['title'] ) ) {
1905 $insert['post_title'] = $attrs['title'];
1906 }
1907
1908 if ( isset( $attrs['caption'] ) ) {
1909 $insert['post_excerpt'] = $attrs['caption'];
1910 }
1911
1912 if ( isset( $attrs['description'] ) ) {
1913 $insert['post_content'] = $attrs['description'];
1914 }
1915
1916 if ( ! empty( $insert ) ) {
1917 $insert['ID'] = $media_id;
1918 wp_update_post( (object) $insert );
1919 }
1920
1921 // Attributes: Alt
1922
1923 if ( isset( $attrs['alt'] ) ) {
1924 $alt = wp_strip_all_tags( $attrs['alt'], true );
1925 update_post_meta( $media_id, '_wp_attachment_image_alt', $alt );
1926 }
1927
1928 // Attributes: Artist, Album
1929
1930 $id3_meta = array();
1931
1932 foreach ( array( 'artist', 'album' ) as $key ) {
1933 if ( isset( $attrs[ $key ] ) ) {
1934 $id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
1935 }
1936 }
1937
1938 if ( ! empty( $id3_meta ) ) {
1939 // Before updating metadata, ensure that the item is audio
1940 $item = $this->get_media_item_v1_1( $media_id );
1941 if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
1942 wp_update_attachment_metadata( $media_id, $id3_meta );
1943 }
1944 }
1945 }
1946 }
1947
1948 return array(
1949 'media_ids' => $media_ids,
1950 'errors' => $errors,
1951 );
1952
1953 }
1954
1955 function handle_media_sideload( $url, $parent_post_id = 0, $type = 'any' ) {
1956 if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) ) {
1957 return false;
1958 }
1959
1960 // if we didn't get a URL, let's bail
1961 $parsed = wp_parse_url( $url );
1962 if ( empty( $parsed ) ) {
1963 return false;
1964 }
1965
1966 $tmp = download_url( $url );
1967 if ( is_wp_error( $tmp ) ) {
1968 return $tmp;
1969 }
1970
1971 // First check to see if we get a mime-type match by file, otherwise, check to
1972 // see if WordPress supports this file as an image. If neither, then it is not supported.
1973 if ( ! $this->is_file_supported_for_sideloading( $tmp ) || 'image' === $type && ! file_is_displayable_image( $tmp ) ) {
1974 @unlink( $tmp );
1975 return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
1976 }
1977
1978 // emulate a $_FILES entry
1979 $file_array = array(
1980 'name' => basename( wp_parse_url( $url, PHP_URL_PATH ) ),
1981 'tmp_name' => $tmp,
1982 );
1983
1984 $id = media_handle_sideload( $file_array, $parent_post_id );
1985 if ( file_exists( $tmp ) ) {
1986 @unlink( $tmp );
1987 }
1988
1989 if ( is_wp_error( $id ) ) {
1990 return $id;
1991 }
1992
1993 if ( ! $id || ! is_int( $id ) ) {
1994 return false;
1995 }
1996
1997 return $id;
1998 }
1999
2000 /**
2001 * Checks that the mime type of the specified file is among those in a filterable list of mime types.
2002 *
2003 * @param string $file Path to file to get its mime type.
2004 *
2005 * @return bool
2006 */
2007 protected function is_file_supported_for_sideloading( $file ) {
2008 return jetpack_is_file_supported_for_sideloading( $file );
2009 }
2010
2011 function allow_video_uploads( $mimes ) {
2012 // if we are on Jetpack, bail - Videos are already allowed
2013 if ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) {
2014 return $mimes;
2015 }
2016
2017 // extra check that this filter is only ever applied during REST API requests
2018 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
2019 return $mimes;
2020 }
2021
2022 // bail early if they already have the upgrade..
2023 if ( get_option( 'video_upgrade' ) == '1' ) {
2024 return $mimes;
2025 }
2026
2027 // lets whitelist to only specific clients right now
2028 $clients_allowed_video_uploads = array();
2029 /**
2030 * Filter the list of whitelisted video clients.
2031 *
2032 * @module json-api
2033 *
2034 * @since 3.2.0
2035 *
2036 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
2037 */
2038 $clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
2039 if ( ! in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) {
2040 return $mimes;
2041 }
2042
2043 $mime_list = wp_get_mime_types();
2044
2045 $video_exts = explode( ' ', get_site_option( 'video_upload_filetypes', false, false ) );
2046 /**
2047 * Filter the video filetypes allowed on the site.
2048 *
2049 * @module json-api
2050 *
2051 * @since 3.2.0
2052 *
2053 * @param array $video_exts Array of video filetypes allowed on the site.
2054 */
2055 $video_exts = apply_filters( 'video_upload_filetypes', $video_exts );
2056 $video_mimes = array();
2057
2058 if ( ! empty( $video_exts ) ) {
2059 foreach ( $video_exts as $ext ) {
2060 foreach ( $mime_list as $ext_pattern => $mime ) {
2061 if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false ) {
2062 $video_mimes[ $ext_pattern ] = $mime;
2063 }
2064 }
2065 }
2066
2067 $mimes = array_merge( $mimes, $video_mimes );
2068 }
2069
2070 return $mimes;
2071 }
2072
2073 function is_current_site_multi_user() {
2074 $users = wp_cache_get( 'site_user_count', 'WPCOM_JSON_API_Endpoint' );
2075 if ( false === $users ) {
2076 $user_query = new WP_User_Query(
2077 array(
2078 'blog_id' => get_current_blog_id(),
2079 'fields' => 'ID',
2080 )
2081 );
2082 $users = (int) $user_query->get_total();
2083 wp_cache_set( 'site_user_count', $users, 'WPCOM_JSON_API_Endpoint', DAY_IN_SECONDS );
2084 }
2085 return $users > 1;
2086 }
2087
2088 function allows_cross_origin_requests() {
2089 return 'GET' == $this->method || $this->allow_cross_origin_request;
2090 }
2091
2092 function allows_unauthorized_requests( $origin, $complete_access_origins ) {
2093 return 'GET' == $this->method || ( $this->allow_unauthorized_request && in_array( $origin, $complete_access_origins ) );
2094 }
2095
2096 /**
2097 * Whether this endpoint accepts site based authentication for the current request.
2098 *
2099 * @since 9.1.0
2100 *
2101 * @return bool true, if Jetpack blog token is used and `allow_jetpack_site_auth` is true,
2102 * false otherwise.
2103 */
2104 public function accepts_site_based_authentication() {
2105 return $this->allow_jetpack_site_auth &&
2106 $this->api->is_jetpack_authorized_for_site();
2107 }
2108
2109 function get_platform() {
2110 return wpcom_get_sal_platform( $this->api->token_details );
2111 }
2112
2113 /**
2114 * Allows the endpoint to perform logic to allow it to decide whether-or-not it should force a
2115 * response from the WPCOM API, or potentially go to the Jetpack blog.
2116 *
2117 * Override this method if you want to do something different.
2118 *
2119 * @param int $blog_id
2120 * @return bool
2121 */
2122 function force_wpcom_request( $blog_id ) {
2123 return false;
2124 }
2125
2126 /**
2127 * Get an array of all valid AMP origins for a blog's siteurl.
2128 *
2129 * @param string $siteurl Origin url of the API request.
2130 * @return array
2131 */
2132 public function get_amp_cache_origins( $siteurl ) {
2133 $host = parse_url( $siteurl, PHP_URL_HOST );
2134
2135 /*
2136 * From AMP docs:
2137 * "When possible, the Google AMP Cache will create a subdomain for each AMP document's domain by first converting it
2138 * from IDN (punycode) to UTF-8. The caches replaces every - (dash) with -- (2 dashes) and replace every . (dot) with
2139 * - (dash). For example, pub.com will map to pub-com.cdn.ampproject.org."
2140 */
2141 if ( function_exists( 'idn_to_utf8' ) ) {
2142 // The third parameter is set explicitly to prevent issues with newer PHP versions compiled with an old ICU version.
2143 // phpcs:ignore PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003Deprecated, PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003DeprecatedRemoved
2144 $host = idn_to_utf8( $host, IDNA_DEFAULT, defined( 'INTL_IDNA_VARIANT_UTS46' ) ? INTL_IDNA_VARIANT_UTS46 : INTL_IDNA_VARIANT_2003 );
2145 }
2146 $subdomain = str_replace( array( '-', '.' ), array( '--', '-' ), $host );
2147 return array(
2148 $siteurl,
2149 // Google AMP Cache (legacy).
2150 'https://cdn.ampproject.org',
2151 // Google AMP Cache subdomain.
2152 sprintf( 'https://%s.cdn.ampproject.org', $subdomain ),
2153 // Cloudflare AMP Cache.
2154 sprintf( 'https://%s.amp.cloudflare.com', $subdomain ),
2155 // Bing AMP Cache.
2156 sprintf( 'https://%s.bing-amp.com', $subdomain ),
2157 );
2158 }
2159
2160 /**
2161 * Return endpoint response
2162 *
2163 * @param string $path ... determined by ->$path.
2164 *
2165 * @return array|WP_Error
2166 * falsy: HTTP 500, no response body
2167 * WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body
2168 * $data: HTTP 200, json_encode( $data ) response body
2169 */
2170 abstract public function callback( $path = '' );
2171
2172
2173 }
2174
2175 require_once dirname( __FILE__ ) . '/json-endpoints.php';
2176