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

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

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