PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 3.6.4
Jetpack – WP Security, Backup, Speed, & Growth v3.6.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 in Jetpack – WP Security, Backup, Speed, & Growth 3.6.4, at class.json-api-endpoints.php

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