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

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