PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 10.2.3
Jetpack – WP Security, Backup, Speed, & Growth v10.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 in Jetpack – WP Security, Backup, Speed, & Growth 10.2.3, at class.json-api-endpoints.php

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