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

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

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