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