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

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

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