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

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

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