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