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

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