class.json-api-endpoints.php
| 1 | <?php |
| 2 | |
| 3 | define( 'WPCOM_JSON_API__CURRENT_VERSION', '1' ); |
| 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 | /** |
| 70 | * @var string Version of the API |
| 71 | */ |
| 72 | var $version = ''; |
| 73 | |
| 74 | /** |
| 75 | * @var string Example request to make |
| 76 | */ |
| 77 | var $example_request = ''; |
| 78 | |
| 79 | /** |
| 80 | * @var string Example request data (for POST methods) |
| 81 | */ |
| 82 | var $example_request_data = ''; |
| 83 | |
| 84 | /** |
| 85 | * @var string Example response from $example_request |
| 86 | */ |
| 87 | var $example_response = ''; |
| 88 | |
| 89 | /** |
| 90 | * @var bool Set to true if the endpoint implements its own filtering instead of the standard `fields` query method |
| 91 | */ |
| 92 | var $custom_fields_filtering = false; |
| 93 | |
| 94 | /** |
| 95 | * @var bool Set to true if the endpoint accepts all cross origin requests |
| 96 | * You probably should not set this flag. If you are thinking of setting it, |
| 97 | * then discuss it with someone: |
| 98 | * http://operationapi.wordpress.com/2014/06/25/patch-allowing-endpoints-to-do-cross-origin-requests/ |
| 99 | */ |
| 100 | var $allow_cross_origin_request = false; |
| 101 | |
| 102 | function __construct( $args ) { |
| 103 | $defaults = array( |
| 104 | 'in_testing' => false, |
| 105 | 'description' => '', |
| 106 | 'group' => '', |
| 107 | 'method' => 'GET', |
| 108 | 'path' => '/', |
| 109 | 'min_version' => '0', |
| 110 | 'max_version' => WPCOM_JSON_API__CURRENT_VERSION, |
| 111 | 'force' => '', |
| 112 | 'jp_disabled' => false, |
| 113 | 'path_labels' => array(), |
| 114 | 'request_format' => array(), |
| 115 | 'response_format' => array(), |
| 116 | 'query_parameters' => array(), |
| 117 | 'version' => 'v1', |
| 118 | 'example_request' => '', |
| 119 | 'example_request_data' => '', |
| 120 | 'example_response' => '', |
| 121 | 'required_scope' => '', |
| 122 | 'pass_wpcom_user_details' => false, |
| 123 | 'can_use_user_details_instead_of_blog_membership' => false, |
| 124 | 'custom_fields_filtering' => false, |
| 125 | 'allow_cross_origin_request' => false, |
| 126 | ); |
| 127 | |
| 128 | $args = wp_parse_args( $args, $defaults ); |
| 129 | |
| 130 | $this->in_testing = $args['in_testing']; |
| 131 | |
| 132 | $this->description = $args['description']; |
| 133 | $this->group = $args['group']; |
| 134 | $this->stat = $args['stat']; |
| 135 | $this->force = $args['force']; |
| 136 | $this->jp_disabled = $args['jp_disabled']; |
| 137 | |
| 138 | $this->method = $args['method']; |
| 139 | $this->path = $args['path']; |
| 140 | $this->path_labels = $args['path_labels']; |
| 141 | $this->min_version = $args['min_version']; |
| 142 | $this->max_version = $args['max_version']; |
| 143 | |
| 144 | $this->pass_wpcom_user_details = $args['pass_wpcom_user_details']; |
| 145 | $this->custom_fields_filtering = (bool) $args['custom_fields_filtering']; |
| 146 | $this->can_use_user_details_instead_of_blog_membership = $args['can_use_user_details_instead_of_blog_membership']; |
| 147 | |
| 148 | $this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request']; |
| 149 | |
| 150 | $this->version = $args['version']; |
| 151 | |
| 152 | $this->required_scope = $args['required_scope']; |
| 153 | |
| 154 | if ( $this->request_format ) { |
| 155 | $this->request_format = array_filter( array_merge( $this->request_format, $args['request_format'] ) ); |
| 156 | } else { |
| 157 | $this->request_format = $args['request_format']; |
| 158 | } |
| 159 | |
| 160 | if ( $this->response_format ) { |
| 161 | $this->response_format = array_filter( array_merge( $this->response_format, $args['response_format'] ) ); |
| 162 | } else { |
| 163 | $this->response_format = $args['response_format']; |
| 164 | } |
| 165 | |
| 166 | if ( false === $args['query_parameters'] ) { |
| 167 | $this->query = array(); |
| 168 | } elseif ( is_array( $args['query_parameters'] ) ) { |
| 169 | $this->query = array_filter( array_merge( $this->query, $args['query_parameters'] ) ); |
| 170 | } |
| 171 | |
| 172 | $this->api = WPCOM_JSON_API::init(); // Auto-add to WPCOM_JSON_API |
| 173 | |
| 174 | /** Example Request/Response ******************************************/ |
| 175 | |
| 176 | // Examples for endpoint documentation request |
| 177 | $this->example_request = $args['example_request']; |
| 178 | $this->example_request_data = $args['example_request_data']; |
| 179 | $this->example_response = $args['example_response']; |
| 180 | |
| 181 | $this->api->add( $this ); |
| 182 | } |
| 183 | |
| 184 | // Get all query args. Prefill with defaults |
| 185 | function query_args( $return_default_values = true, $cast_and_filter = true ) { |
| 186 | $args = array_intersect_key( $this->api->query, $this->query ); |
| 187 | |
| 188 | if ( !$cast_and_filter ) { |
| 189 | return $args; |
| 190 | } |
| 191 | |
| 192 | return $this->cast_and_filter( $args, $this->query, $return_default_values ); |
| 193 | } |
| 194 | |
| 195 | // Get POST body data |
| 196 | function input( $return_default_values = true, $cast_and_filter = true ) { |
| 197 | $input = trim( $this->api->post_body ); |
| 198 | $content_type = $this->api->content_type; |
| 199 | if ( $content_type ) { |
| 200 | list ( $content_type ) = explode( ';', $content_type ); |
| 201 | } |
| 202 | $content_type = trim( $content_type ); |
| 203 | switch ( $content_type ) { |
| 204 | case 'application/json' : |
| 205 | case 'application/x-javascript' : |
| 206 | case 'text/javascript' : |
| 207 | case 'text/x-javascript' : |
| 208 | case 'text/x-json' : |
| 209 | case 'text/json' : |
| 210 | $return = json_decode( $input, true ); |
| 211 | |
| 212 | if ( function_exists( 'json_last_error' ) ) { |
| 213 | if ( JSON_ERROR_NONE !== json_last_error() ) { |
| 214 | return null; |
| 215 | } |
| 216 | } else { |
| 217 | if ( is_null( $return ) && json_encode( null ) !== $input ) { |
| 218 | return null; |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | break; |
| 223 | case 'multipart/form-data' : |
| 224 | $return = array_merge( stripslashes_deep( $_POST ), $_FILES ); |
| 225 | break; |
| 226 | case 'application/x-www-form-urlencoded' : |
| 227 | //attempt JSON first, since probably a curl command |
| 228 | $return = json_decode( $input, true ); |
| 229 | |
| 230 | if ( is_null( $return ) ) { |
| 231 | wp_parse_str( $input, $return ); |
| 232 | } |
| 233 | |
| 234 | break; |
| 235 | default : |
| 236 | wp_parse_str( $input, $return ); |
| 237 | break; |
| 238 | } |
| 239 | |
| 240 | if ( !$cast_and_filter ) { |
| 241 | return $return; |
| 242 | } |
| 243 | |
| 244 | return $this->cast_and_filter( $return, $this->request_format, $return_default_values ); |
| 245 | } |
| 246 | |
| 247 | function cast_and_filter( $data, $documentation, $return_default_values = false, $for_output = false ) { |
| 248 | $return_as_object = false; |
| 249 | if ( is_object( $data ) ) { |
| 250 | // @todo this should probably be a deep copy if $data can ever have nested objects |
| 251 | $data = (array) $data; |
| 252 | $return_as_object = true; |
| 253 | } elseif ( !is_array( $data ) ) { |
| 254 | return $data; |
| 255 | } |
| 256 | |
| 257 | $boolean_arg = array( 'false', 'true' ); |
| 258 | $naeloob_arg = array( 'true', 'false' ); |
| 259 | |
| 260 | $return = array(); |
| 261 | |
| 262 | foreach ( $documentation as $key => $description ) { |
| 263 | if ( is_array( $description ) ) { |
| 264 | // String or boolean array keys only |
| 265 | $whitelist = array_keys( $description ); |
| 266 | if ( isset( $data[$key] ) && isset( $description[$data[$key]] ) ) { |
| 267 | $return[$key] = (string) $data[$key]; |
| 268 | } elseif ( $return_default_values ) { |
| 269 | $return[$key] = (string) current( $whitelist ); |
| 270 | } else { |
| 271 | continue; |
| 272 | } |
| 273 | |
| 274 | // Truthiness |
| 275 | if ( $whitelist === $boolean_arg || $whitelist === $naeloob_arg ) { |
| 276 | $return[$key] = (bool) WPCOM_JSON_API::is_truthy( $return[$key] ); |
| 277 | } |
| 278 | |
| 279 | continue; |
| 280 | } |
| 281 | |
| 282 | $types = $this->parse_types( $description ); |
| 283 | $type = array_shift( $types ); |
| 284 | |
| 285 | // Explicit default - string and int only for now. Always set these reguardless of $return_default_values |
| 286 | if ( isset( $type['default'] ) ) { |
| 287 | if ( !isset( $data[$key] ) ) { |
| 288 | $data[$key] = $type['default']; |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | if ( !isset( $data[$key] ) ) { |
| 293 | continue; |
| 294 | } |
| 295 | |
| 296 | $this->cast_and_filter_item( $return, $type, $key, $data[$key], $types, $for_output ); |
| 297 | } |
| 298 | |
| 299 | if ( $return_as_object ) { |
| 300 | return (object) $return; |
| 301 | } |
| 302 | |
| 303 | return $return; |
| 304 | } |
| 305 | |
| 306 | /** |
| 307 | * Casts $value according to $type. |
| 308 | * Handles fallbacks for certain values of $type when $value is not that $type |
| 309 | * Currently, only handles fallback between string <-> array (two way), from string -> false (one way), and from object -> false (one way) |
| 310 | * |
| 311 | * Handles "child types" - array:URL, object:category |
| 312 | * array:URL means an array of URLs |
| 313 | * object:category means a hash of categories |
| 314 | * |
| 315 | * Handles object typing - object>post means an object of type post |
| 316 | */ |
| 317 | function cast_and_filter_item( &$return, $type, $key, $value, $types = array(), $for_output = false ) { |
| 318 | if ( is_string( $type ) ) { |
| 319 | $type = compact( 'type' ); |
| 320 | } |
| 321 | |
| 322 | switch ( $type['type'] ) { |
| 323 | case 'false' : |
| 324 | $return[$key] = false; |
| 325 | break; |
| 326 | case 'url' : |
| 327 | $return[$key] = (string) esc_url_raw( $value ); |
| 328 | break; |
| 329 | case 'string' : |
| 330 | // Fallback string -> array |
| 331 | if ( is_array( $value ) ) { |
| 332 | if ( !empty( $types[0] ) ) { |
| 333 | $next_type = array_shift( $types ); |
| 334 | return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output ); |
| 335 | } |
| 336 | } |
| 337 | |
| 338 | // Fallback string -> false |
| 339 | if ( !is_string( $value ) ) { |
| 340 | if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) { |
| 341 | $next_type = array_shift( $types ); |
| 342 | return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output ); |
| 343 | } |
| 344 | } |
| 345 | $return[$key] = (string) $value; |
| 346 | break; |
| 347 | case 'html' : |
| 348 | $return[$key] = (string) $value; |
| 349 | break; |
| 350 | case 'safehtml' : |
| 351 | $return[$key] = wp_kses( (string) $value, wp_kses_allowed_html() ); |
| 352 | break; |
| 353 | case 'media' : |
| 354 | if ( is_array( $value ) ) { |
| 355 | if ( isset( $value['name'] ) ) { |
| 356 | // It's a $_FILES array |
| 357 | // Reformat into array of $_FILES items |
| 358 | |
| 359 | $files = array(); |
| 360 | foreach ( $value['name'] as $k => $v ) { |
| 361 | $files[$k] = array(); |
| 362 | foreach ( array_keys( $value ) as $file_key ) { |
| 363 | $files[$k][$file_key] = $value[$file_key][$k]; |
| 364 | } |
| 365 | } |
| 366 | |
| 367 | foreach ( $files as $k => $file ) { |
| 368 | if ( ! isset( $file['tmp_name'] ) || ! is_string( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) { |
| 369 | unset( $files[ $k ] ); |
| 370 | } |
| 371 | } |
| 372 | if ( $files ) { |
| 373 | $return[$key] = $files; |
| 374 | } |
| 375 | } elseif ( isset( $value['tmp_name'] ) && is_string( $value['tmp_name'] ) && is_uploaded_file( $value['tmp_name'] ) ) { |
| 376 | $return[ $key ] = $value; |
| 377 | } |
| 378 | } |
| 379 | break; |
| 380 | case 'array' : |
| 381 | // Fallback array -> string |
| 382 | if ( is_string( $value ) ) { |
| 383 | if ( !empty( $types[0] ) ) { |
| 384 | $next_type = array_shift( $types ); |
| 385 | return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output ); |
| 386 | } |
| 387 | } |
| 388 | |
| 389 | if ( isset( $type['children'] ) ) { |
| 390 | $children = array(); |
| 391 | foreach ( (array) $value as $k => $child ) { |
| 392 | $this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output ); |
| 393 | } |
| 394 | $return[$key] = (array) $children; |
| 395 | break; |
| 396 | } |
| 397 | |
| 398 | $return[$key] = (array) $value; |
| 399 | break; |
| 400 | case 'iso 8601 datetime' : |
| 401 | case 'datetime' : |
| 402 | // (string)s |
| 403 | $dates = $this->parse_date( (string) $value ); |
| 404 | if ( $for_output ) { |
| 405 | $return[$key] = $this->format_date( $dates[1], $dates[0] ); |
| 406 | } else { |
| 407 | list( $return[$key], $return["{$key}_gmt"] ) = $dates; |
| 408 | } |
| 409 | break; |
| 410 | case 'float' : |
| 411 | $return[$key] = (float) $value; |
| 412 | break; |
| 413 | case 'int' : |
| 414 | case 'integer' : |
| 415 | $return[$key] = (int) $value; |
| 416 | break; |
| 417 | case 'bool' : |
| 418 | case 'boolean' : |
| 419 | $return[$key] = (bool) WPCOM_JSON_API::is_truthy( $value ); |
| 420 | break; |
| 421 | case 'object' : |
| 422 | // Fallback object -> false |
| 423 | if ( is_scalar( $value ) || is_null( $value ) ) { |
| 424 | if ( !empty( $types[0] ) && 'false' === $types[0]['type'] ) { |
| 425 | return $this->cast_and_filter_item( $return, 'false', $key, $value, $types, $for_output ); |
| 426 | } |
| 427 | } |
| 428 | |
| 429 | if ( isset( $type['children'] ) ) { |
| 430 | $children = array(); |
| 431 | foreach ( (array) $value as $k => $child ) { |
| 432 | $this->cast_and_filter_item( $children, $type['children'], $k, $child, array(), $for_output ); |
| 433 | } |
| 434 | $return[$key] = (object) $children; |
| 435 | break; |
| 436 | } |
| 437 | |
| 438 | if ( isset( $type['subtype'] ) ) { |
| 439 | return $this->cast_and_filter_item( $return, $type['subtype'], $key, $value, $types, $for_output ); |
| 440 | } |
| 441 | |
| 442 | $return[$key] = (object) $value; |
| 443 | break; |
| 444 | case 'post' : |
| 445 | $return[$key] = (object) $this->cast_and_filter( $value, $this->post_object_format, false, $for_output ); |
| 446 | break; |
| 447 | case 'comment' : |
| 448 | $return[$key] = (object) $this->cast_and_filter( $value, $this->comment_object_format, false, $for_output ); |
| 449 | break; |
| 450 | case 'tag' : |
| 451 | case 'category' : |
| 452 | $docs = array( |
| 453 | 'name' => '(string)', |
| 454 | 'slug' => '(string)', |
| 455 | 'description' => '(HTML)', |
| 456 | 'post_count' => '(int)', |
| 457 | 'meta' => '(object)', |
| 458 | ); |
| 459 | if ( 'category' === $type ) { |
| 460 | $docs['parent'] = '(int)'; |
| 461 | } |
| 462 | $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output ); |
| 463 | break; |
| 464 | case 'post_reference' : |
| 465 | case 'comment_reference' : |
| 466 | $docs = array( |
| 467 | 'ID' => '(int)', |
| 468 | 'type' => '(string)', |
| 469 | 'title' => '(string)', |
| 470 | 'link' => '(URL)', |
| 471 | ); |
| 472 | $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output ); |
| 473 | break; |
| 474 | case 'geo' : |
| 475 | $docs = array( |
| 476 | 'latitude' => '(float)', |
| 477 | 'longitude' => '(float)', |
| 478 | 'address' => '(string)', |
| 479 | ); |
| 480 | $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output ); |
| 481 | break; |
| 482 | case 'author' : |
| 483 | $docs = array( |
| 484 | 'ID' => '(int)', |
| 485 | 'user_login' => '(string)', |
| 486 | 'email' => '(string|false)', |
| 487 | 'name' => '(string)', |
| 488 | 'URL' => '(URL)', |
| 489 | 'avatar_URL' => '(URL)', |
| 490 | 'profile_URL' => '(URL)', |
| 491 | ); |
| 492 | $return[$key] = (object) $this->cast_and_filter( $value, $docs, false, $for_output ); |
| 493 | break; |
| 494 | case 'attachment' : |
| 495 | $docs = array( |
| 496 | 'ID' => '(int)', |
| 497 | 'URL' => '(URL)', |
| 498 | 'guid' => '(string)', |
| 499 | 'mime_type' => '(string)', |
| 500 | 'width' => '(int)', |
| 501 | 'height' => '(int)', |
| 502 | 'duration' => '(int)', |
| 503 | ); |
| 504 | $return[$key] = (object) $this->cast_and_filter( $value, apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ), false, $for_output ); |
| 505 | break; |
| 506 | case 'metadata' : |
| 507 | $docs = array( |
| 508 | 'id' => '(int)', |
| 509 | 'key' => '(string)', |
| 510 | 'value' => '(string|false|float|int|array|object)', |
| 511 | 'previous_value' => '(string)', |
| 512 | 'operation' => '(string)', |
| 513 | ); |
| 514 | $return[$key] = (object) $this->cast_and_filter( $value, apply_filters( 'wpcom_json_api_attachment_cast_and_filter', $docs ), false, $for_output ); |
| 515 | break; |
| 516 | case 'plugin' : |
| 517 | $docs = array( |
| 518 | 'id' => '(string) The plugin\'s ID', |
| 519 | 'active' => '(boolean) The plugin status.', |
| 520 | 'update' => '(object) The plugin update info.', |
| 521 | 'name' => '(string) The name of the plugin.', |
| 522 | 'plugin_url' => '(url) Link to the plugin\'s web site.', |
| 523 | 'version' => '(string) The plugin version number.', |
| 524 | 'description' => '(safehtml) Description of what the plugin does and/or notes from the author', |
| 525 | 'author' => '(string) The plugin author\'s name', |
| 526 | 'author_url' => '(url) The plugin author web site address', |
| 527 | 'network' => '(boolean) Whether the plugin can only be activated network wide.', |
| 528 | ); |
| 529 | $return[$key] = (object) $this->cast_and_filter( $value, apply_filters( 'wpcom_json_api_plugin_cast_and_filter', $docs ), false, $for_output ); |
| 530 | break; |
| 531 | |
| 532 | default : |
| 533 | trigger_error( "Unknown API casting type {$type['type']}", E_USER_WARNING ); |
| 534 | } |
| 535 | } |
| 536 | |
| 537 | function parse_types( $text ) { |
| 538 | if ( !preg_match( '#^\(([^)]+)\)#', ltrim( $text ), $matches ) ) { |
| 539 | return 'none'; |
| 540 | } |
| 541 | |
| 542 | $types = explode( '|', strtolower( $matches[1] ) ); |
| 543 | $return = array(); |
| 544 | foreach ( $types as $type ) { |
| 545 | foreach ( array( ':' => 'children', '>' => 'subtype', '=' => 'default' ) as $operator => $meaning ) { |
| 546 | if ( false !== strpos( $type, $operator ) ) { |
| 547 | $item = explode( $operator, $type, 2 ); |
| 548 | $return[] = array( 'type' => $item[0], $meaning => $item[1] ); |
| 549 | continue 2; |
| 550 | } |
| 551 | } |
| 552 | $return[] = compact( 'type' ); |
| 553 | } |
| 554 | |
| 555 | return $return; |
| 556 | } |
| 557 | |
| 558 | /** |
| 559 | * Auto generates documentation based on description, method, path, path_labels, and query parameters. |
| 560 | * Echoes HTML. |
| 561 | */ |
| 562 | function document( $show_description = true ) { |
| 563 | $original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset'; |
| 564 | unset( $GLOBALS['post'] ); |
| 565 | |
| 566 | $doc = $this->generate_documentation(); |
| 567 | |
| 568 | if ( $show_description ) : |
| 569 | ?> |
| 570 | <caption> |
| 571 | <h1><?php echo wp_kses_post( $doc['method'] ); ?> <?php echo wp_kses_post( $doc['path_labeled'] ); ?></h1> |
| 572 | <p><?php echo wp_kses_post( $doc['description'] ); ?></p> |
| 573 | </caption> |
| 574 | |
| 575 | <?php endif; ?> |
| 576 | |
| 577 | <section class="resource-url"> |
| 578 | <h2 id="apidoc-resource-url">Resource URL</h2> |
| 579 | <table class="api-doc api-doc-resource-parameters api-doc-resource"> |
| 580 | <thead> |
| 581 | <tr> |
| 582 | <th class="api-index-title" scope="column">Type</th> |
| 583 | <th class="api-index-title" scope="column">URL and Format</th> |
| 584 | </tr> |
| 585 | </thead> |
| 586 | <tbody> |
| 587 | <tr class="api-index-item"> |
| 588 | <th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $doc['method'] ); ?></th> |
| 589 | <td class="type api-index-item-title" style="white-space: nowrap;">https://public-api.wordpress.com/rest/v1<?php echo wp_kses_post( $doc['path_labeled'] ); ?></td> |
| 590 | </tr> |
| 591 | </tbody> |
| 592 | </table> |
| 593 | </section> |
| 594 | |
| 595 | <?php |
| 596 | |
| 597 | foreach ( array( |
| 598 | 'path' => 'Method Parameters', |
| 599 | 'query' => 'Query Parameters', |
| 600 | 'body' => 'Request Parameters', |
| 601 | 'response' => 'Response Parameters', |
| 602 | ) as $doc_section_key => $label ) : |
| 603 | $doc_section = 'response' === $doc_section_key ? $doc['response']['body'] : $doc['request'][$doc_section_key]; |
| 604 | if ( !$doc_section ) { |
| 605 | continue; |
| 606 | } |
| 607 | |
| 608 | $param_label = strtolower( str_replace( ' ', '-', $label ) ); |
| 609 | ?> |
| 610 | |
| 611 | <section class="<?php echo $param_label; ?>"> |
| 612 | |
| 613 | <h2 id="apidoc-<?php echo esc_attr( $doc_section_key ); ?>"><?php echo wp_kses_post( $label ); ?></h2> |
| 614 | |
| 615 | <table class="api-doc api-doc-<?php echo $param_label; ?>-parameters api-doc-<?php echo strtolower( str_replace( ' ', '-', $doc['group'] ) ); ?>"> |
| 616 | |
| 617 | <thead> |
| 618 | <tr> |
| 619 | <th class="api-index-title" scope="column">Parameter</th> |
| 620 | <th class="api-index-title" scope="column">Type</th> |
| 621 | <th class="api-index-title" scope="column">Description</th> |
| 622 | </tr> |
| 623 | </thead> |
| 624 | <tbody> |
| 625 | |
| 626 | <?php foreach ( $doc_section as $key => $item ) : ?> |
| 627 | |
| 628 | <tr class="api-index-item"> |
| 629 | <th scope="row" class="parameter api-index-item-title"><?php echo wp_kses_post( $key ); ?></th> |
| 630 | <td class="type api-index-item-title"><?php echo wp_kses_post( $item['type'] ); // @todo auto-link? ?></td> |
| 631 | <td class="description api-index-item-body"><?php |
| 632 | |
| 633 | $this->generate_doc_description( $item['description'] ); |
| 634 | |
| 635 | ?></td> |
| 636 | </tr> |
| 637 | |
| 638 | <?php endforeach; ?> |
| 639 | </tbody> |
| 640 | </table> |
| 641 | </section> |
| 642 | <?php endforeach; ?> |
| 643 | |
| 644 | <?php |
| 645 | // If no example was hardcoded in the doc, try to get some |
| 646 | if ( empty( $this->example_response ) ) { |
| 647 | |
| 648 | // Examples for endpoint documentation response |
| 649 | $response_key = 'dev_example_response_' . $this->version . '_' . $this->method . '_' . sanitize_key( $this->path ); |
| 650 | $response_body = wp_cache_get( $response_key ); |
| 651 | |
| 652 | // Response doesn't exist, so run the request |
| 653 | if ( false === $response_body ) { |
| 654 | |
| 655 | // Only trust GET request |
| 656 | if ( 'GET' === $this->method ) { |
| 657 | $response = wp_remote_get( $this->example_request ); |
| 658 | $response_body = wp_remote_retrieve_body( $response ); |
| 659 | |
| 660 | // Only cache if there's a result |
| 661 | if ( ! is_wp_error( $response ) && strlen( $response_body ) ) { |
| 662 | wp_cache_set( $response_key, $response_body ); |
| 663 | } else { |
| 664 | wp_cache_delete( $response_key ); |
| 665 | } |
| 666 | } |
| 667 | } |
| 668 | |
| 669 | // Example response was passed into the constructor via params |
| 670 | } else { |
| 671 | $response_body = $this->example_response; |
| 672 | } |
| 673 | |
| 674 | // Wrap the response in a sourcecode shortcode |
| 675 | if ( !empty( $response_body ) && !is_wp_error( $response ) ) { |
| 676 | $response_body = '[sourcecode language="javascript" wraplines="false" light="true" autolink="false" htmlscript="false"]' . $response_body . '[/sourcecode]'; |
| 677 | $response_body = apply_filters( 'the_content', $response_body ); |
| 678 | $this->example_response = $response_body; |
| 679 | } |
| 680 | |
| 681 | $curl = 'curl'; |
| 682 | |
| 683 | $php_opts = array( 'ignore_errors' => true ); |
| 684 | |
| 685 | if ( 'GET' !== $this->method ) { |
| 686 | $php_opts['method'] = $this->method; |
| 687 | } |
| 688 | |
| 689 | if ( $this->example_request_data ) { |
| 690 | if ( isset( $this->example_request_data['headers'] ) && is_array( $this->example_request_data['headers'] ) ) { |
| 691 | $php_opts['header'] = array(); |
| 692 | foreach ( $this->example_request_data['headers'] as $header => $value ) { |
| 693 | $curl .= " \\\n -H " . escapeshellarg( "$header: $value" ); |
| 694 | $php_opts['header'][] = "$header: $value"; |
| 695 | } |
| 696 | } |
| 697 | |
| 698 | if ( isset( $this->example_request_data['body'] ) && is_array( $this->example_request_data['body'] ) ) { |
| 699 | $php_opts['content'] = $this->example_request_data['body']; |
| 700 | $php_opts['header'][] = 'Content-Type: application/x-www-form-urlencoded'; |
| 701 | foreach ( $this->example_request_data['body'] as $key => $value ) { |
| 702 | $curl .= " \\\n --data-urlencode " . escapeshellarg( "$key=$value" ); |
| 703 | } |
| 704 | } |
| 705 | } |
| 706 | |
| 707 | if ( $php_opts ) { |
| 708 | $php_opts_exported = var_export( array( 'http' => $php_opts ), true ); |
| 709 | if ( !empty( $php_opts['content'] ) ) { |
| 710 | $content_exported = preg_quote( var_export( $php_opts['content'], true ), '/' ); |
| 711 | $content_exported = '\\s*' . str_replace( "\n", "\n\\s*", $content_exported ) . '\\s*'; |
| 712 | $php_opts_exported = preg_replace_callback( "/$content_exported/", array( $this, 'add_http_build_query_to_php_content_example' ), $php_opts_exported ); |
| 713 | } |
| 714 | $php = <<<EOPHP |
| 715 | <?php |
| 716 | |
| 717 | \$options = $php_opts_exported; |
| 718 | |
| 719 | \$context = stream_context_create( \$options ); |
| 720 | \$response = file_get_contents( |
| 721 | '$this->example_request', |
| 722 | false, |
| 723 | \$context |
| 724 | ); |
| 725 | \$response = json_decode( \$response ); |
| 726 | |
| 727 | ?> |
| 728 | EOPHP; |
| 729 | } else { |
| 730 | $php = <<<EOPHP |
| 731 | <?php |
| 732 | |
| 733 | \$response = file_get_contents( '$this->example_request' ); |
| 734 | \$response = json_decode( \$response ); |
| 735 | |
| 736 | ?> |
| 737 | EOPHP; |
| 738 | } |
| 739 | |
| 740 | if ( false !== strpos( $curl, "\n" ) ) { |
| 741 | $curl .= " \\\n"; |
| 742 | } |
| 743 | |
| 744 | $curl .= ' ' . escapeshellarg( $this->example_request ); |
| 745 | |
| 746 | $curl = '[sourcecode language="bash" wraplines="false" light="true" autolink="false" htmlscript="false"]' . $curl . '[/sourcecode]'; |
| 747 | $curl = apply_filters( 'the_content', $curl ); |
| 748 | |
| 749 | $php = '[sourcecode language="php" wraplines="false" light="true" autolink="false" htmlscript="false"]' . $php . '[/sourcecode]'; |
| 750 | $php = apply_filters( 'the_content', $php ); |
| 751 | ?> |
| 752 | |
| 753 | <?php if ( ! empty( $this->example_request ) || ! empty( $this->example_request_data ) || ! empty( $this->example_response ) ) : ?> |
| 754 | |
| 755 | <section class="example-response"> |
| 756 | <h2 id="apidoc-example">Example</h2> |
| 757 | |
| 758 | <section> |
| 759 | <h3>cURL</h3> |
| 760 | <?php echo wp_kses_post( $curl ); ?> |
| 761 | </section> |
| 762 | |
| 763 | <section> |
| 764 | <h3>PHP</h3> |
| 765 | <?php echo wp_kses_post( $php ); ?> |
| 766 | </section> |
| 767 | |
| 768 | <?php if ( ! empty( $this->example_response ) ) : ?> |
| 769 | |
| 770 | <section> |
| 771 | <h3>Response Body</h3> |
| 772 | <?php echo $this->example_response; ?> |
| 773 | </section> |
| 774 | |
| 775 | <?php endif; ?> |
| 776 | |
| 777 | </section> |
| 778 | |
| 779 | <?php endif; ?> |
| 780 | |
| 781 | <?php |
| 782 | if ( 'unset' !== $original_post ) { |
| 783 | $GLOBALS['post'] = $original_post; |
| 784 | } |
| 785 | } |
| 786 | |
| 787 | function add_http_build_query_to_php_content_example( $matches ) { |
| 788 | $trimmed_match = ltrim( $matches[0] ); |
| 789 | $pad = substr( $matches[0], 0, -1 * strlen( $trimmed_match ) ); |
| 790 | $pad = ltrim( $pad, ' ' ); |
| 791 | $return = ' ' . str_replace( "\n", "\n ", $matches[0] ); |
| 792 | return " http_build_query({$return}{$pad})"; |
| 793 | } |
| 794 | |
| 795 | /** |
| 796 | * Recursively generates the <dl>'s to document item descriptions. |
| 797 | * Echoes HTML. |
| 798 | */ |
| 799 | function generate_doc_description( $item ) { |
| 800 | if ( is_array( $item ) ) : ?> |
| 801 | |
| 802 | <dl> |
| 803 | <?php foreach ( $item as $description_key => $description_value ) : ?> |
| 804 | |
| 805 | <dt><?php echo wp_kses_post( $description_key . ':' ); ?></dt> |
| 806 | <dd><?php $this->generate_doc_description( $description_value ); ?></dd> |
| 807 | |
| 808 | <?php endforeach; ?> |
| 809 | |
| 810 | </dl> |
| 811 | |
| 812 | <?php |
| 813 | else : |
| 814 | echo wp_kses_post( $item ); |
| 815 | endif; |
| 816 | } |
| 817 | |
| 818 | /** |
| 819 | * Auto generates documentation based on description, method, path, path_labels, and query parameters. |
| 820 | * Echoes HTML. |
| 821 | */ |
| 822 | function generate_documentation() { |
| 823 | $format = str_replace( '%d', '%s', $this->path ); |
| 824 | $path_labeled = vsprintf( $format, array_keys( $this->path_labels ) ); |
| 825 | $boolean_arg = array( 'false', 'true' ); |
| 826 | $naeloob_arg = array( 'true', 'false' ); |
| 827 | |
| 828 | $doc = array( |
| 829 | 'description' => $this->description, |
| 830 | 'method' => $this->method, |
| 831 | 'path_format' => $this->path, |
| 832 | 'path_labeled' => $path_labeled, |
| 833 | 'group' => $this->group, |
| 834 | 'request' => array( |
| 835 | 'path' => array(), |
| 836 | 'query' => array(), |
| 837 | 'body' => array(), |
| 838 | ), |
| 839 | 'response' => array( |
| 840 | 'body' => array(), |
| 841 | ) |
| 842 | ); |
| 843 | |
| 844 | foreach ( array( 'path_labels' => 'path', 'query' => 'query', 'request_format' => 'body', 'response_format' => 'body' ) as $_property => $doc_item ) { |
| 845 | foreach ( (array) $this->$_property as $key => $description ) { |
| 846 | if ( is_array( $description ) ) { |
| 847 | $description_keys = array_keys( $description ); |
| 848 | if ( $boolean_arg === $description_keys || $naeloob_arg === $description_keys ) { |
| 849 | $type = '(bool)'; |
| 850 | } else { |
| 851 | $type = '(string)'; |
| 852 | } |
| 853 | |
| 854 | if ( 'response_format' !== $_property ) { |
| 855 | // hack - don't show "(default)" in response format |
| 856 | reset( $description ); |
| 857 | $description_key = key( $description ); |
| 858 | $description[$description_key] = "(default) {$description[$description_key]}"; |
| 859 | } |
| 860 | } else { |
| 861 | $types = $this->parse_types( $description ); |
| 862 | $type = array(); |
| 863 | $default = ''; |
| 864 | |
| 865 | if ( 'none' == $types ) { |
| 866 | $types = array(); |
| 867 | $types[]['type'] = 'none'; |
| 868 | } |
| 869 | |
| 870 | foreach ( $types as $type_array ) { |
| 871 | $type[] = $type_array['type']; |
| 872 | if ( isset( $type_array['default'] ) ) { |
| 873 | $default = $type_array['default']; |
| 874 | if ( 'string' === $type_array['type'] ) { |
| 875 | $default = "'$default'"; |
| 876 | } |
| 877 | } |
| 878 | } |
| 879 | $type = '(' . join( '|', $type ) . ')'; |
| 880 | $noop = ''; // skip an index in list below |
| 881 | list( $noop, $description ) = explode( ')', $description, 2 ); |
| 882 | $description = trim( $description ); |
| 883 | if ( $default ) { |
| 884 | $description .= " Default: $default."; |
| 885 | } |
| 886 | } |
| 887 | |
| 888 | $item = compact( 'type', 'description' ); |
| 889 | |
| 890 | if ( 'response_format' === $_property ) { |
| 891 | $doc['response'][$doc_item][$key] = $item; |
| 892 | } else { |
| 893 | $doc['request'][$doc_item][$key] = $item; |
| 894 | } |
| 895 | } |
| 896 | } |
| 897 | |
| 898 | return $doc; |
| 899 | } |
| 900 | |
| 901 | function user_can_view_post( $post_id ) { |
| 902 | $post = get_post( $post_id ); |
| 903 | if ( !$post || is_wp_error( $post ) ) { |
| 904 | return false; |
| 905 | } |
| 906 | |
| 907 | if ( 'inherit' === $post->post_status ) { |
| 908 | $parent_post = get_post( $post->post_parent ); |
| 909 | $post_status_obj = get_post_status_object( $parent_post->post_status ); |
| 910 | } else { |
| 911 | $post_status_obj = get_post_status_object( $post->post_status ); |
| 912 | } |
| 913 | |
| 914 | if ( !$post_status_obj->public ) { |
| 915 | if ( is_user_logged_in() ) { |
| 916 | if ( $post_status_obj->protected ) { |
| 917 | if ( !current_user_can( 'edit_post', $post->ID ) ) { |
| 918 | return new WP_Error( 'unauthorized', 'User cannot view post', 403 ); |
| 919 | } |
| 920 | } elseif ( $post_status_obj->private ) { |
| 921 | if ( !current_user_can( 'read_post', $post->ID ) ) { |
| 922 | return new WP_Error( 'unauthorized', 'User cannot view post', 403 ); |
| 923 | } |
| 924 | } elseif ( 'trash' === $post->post_status ) { |
| 925 | if ( !current_user_can( 'edit_post', $post->ID ) ) { |
| 926 | return new WP_Error( 'unauthorized', 'User cannot view post', 403 ); |
| 927 | } |
| 928 | } else { |
| 929 | return new WP_Error( 'unauthorized', 'User cannot view post', 403 ); |
| 930 | } |
| 931 | } else { |
| 932 | return new WP_Error( 'unauthorized', 'User cannot view post', 403 ); |
| 933 | } |
| 934 | } |
| 935 | |
| 936 | if ( -1 == get_option( 'blog_public' ) && !current_user_can( 'read_post', $post->ID ) ) { |
| 937 | return new WP_Error( 'unauthorized', 'User cannot view post', array( 'status_code' => 403, 'error' => 'private_blog' ) ); |
| 938 | } |
| 939 | |
| 940 | if ( strlen( $post->post_password ) && !current_user_can( 'edit_post', $post->ID ) ) { |
| 941 | return new WP_Error( 'unauthorized', 'User cannot view password protected post', array( 'status_code' => 403, 'error' => 'password_protected' ) ); |
| 942 | } |
| 943 | |
| 944 | return true; |
| 945 | } |
| 946 | |
| 947 | /** |
| 948 | * Returns author object. |
| 949 | * |
| 950 | * @param $author user ID, user row, WP_User object, comment row, post row |
| 951 | * @param $show_email output the author's email address? |
| 952 | * |
| 953 | * @return (object) |
| 954 | */ |
| 955 | function get_author( $author, $show_email = false ) { |
| 956 | if ( isset( $author->comment_author_email ) && !$author->user_id ) { |
| 957 | $ID = 0; |
| 958 | $login = ''; |
| 959 | $email = $author->comment_author_email; |
| 960 | $name = $author->comment_author; |
| 961 | $URL = $author->comment_author_url; |
| 962 | $profile_URL = 'http://en.gravatar.com/' . md5( strtolower( trim( $email ) ) ); |
| 963 | $nice = ''; |
| 964 | $site_id = -1; |
| 965 | } else { |
| 966 | if ( isset( $author->post_author ) ) { |
| 967 | if ( 0 == $author->post_author ) |
| 968 | return null; |
| 969 | |
| 970 | $author = $author->post_author; |
| 971 | } elseif ( isset( $author->user_id ) && $author->user_id ) { |
| 972 | $author = $author->user_id; |
| 973 | } elseif ( isset( $author->user_email ) ) { |
| 974 | $author = $author->ID; |
| 975 | } |
| 976 | |
| 977 | $user = get_user_by( 'id', $author ); |
| 978 | if ( !$user || is_wp_error( $user ) ) { |
| 979 | trigger_error( 'Unknown user', E_USER_WARNING ); |
| 980 | return null; |
| 981 | } |
| 982 | |
| 983 | $ID = $user->ID; |
| 984 | $email = $user->user_email; |
| 985 | $login = $user->user_login; |
| 986 | $name = $user->display_name; |
| 987 | $URL = $user->user_url; |
| 988 | $nice = $user->user_nicename; |
| 989 | if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { |
| 990 | $active_blog = get_active_blog_for_user( $ID ); |
| 991 | $site_id = $active_blog->blog_id; |
| 992 | $profile_URL = "http://en.gravatar.com/{$login}"; |
| 993 | } else { |
| 994 | $profile_URL = 'http://en.gravatar.com/' . md5( strtolower( trim( $email ) ) ); |
| 995 | $site_id = -1; |
| 996 | } |
| 997 | } |
| 998 | |
| 999 | $avatar_URL = $this->api->get_avatar_url( $email ); |
| 1000 | |
| 1001 | $email = $show_email ? (string) $email : false; |
| 1002 | |
| 1003 | $author = array( |
| 1004 | 'ID' => (int) $ID, |
| 1005 | 'login' => (string) $login, |
| 1006 | 'email' => $email, // (string|bool) |
| 1007 | 'name' => (string) $name, |
| 1008 | 'nice_name' => (string) $nice, |
| 1009 | 'URL' => (string) esc_url_raw( $URL ), |
| 1010 | 'avatar_URL' => (string) esc_url_raw( $avatar_URL ), |
| 1011 | 'profile_URL' => (string) esc_url_raw( $profile_URL ), |
| 1012 | ); |
| 1013 | |
| 1014 | if ($site_id > -1) { |
| 1015 | $author['site_ID'] = (int) $site_id; |
| 1016 | } |
| 1017 | |
| 1018 | return (object) $author; |
| 1019 | } |
| 1020 | |
| 1021 | function get_media_item( $media_id ) { |
| 1022 | $media_item = get_post( $media_id ); |
| 1023 | |
| 1024 | if ( !$media_item || is_wp_error( $media_item ) ) |
| 1025 | return new WP_Error( 'unknown_media', 'Unknown Media', 404 ); |
| 1026 | |
| 1027 | $response = array( |
| 1028 | 'id' => strval( $media_item->ID ), |
| 1029 | 'date' => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ), |
| 1030 | 'parent' => $media_item->post_parent, |
| 1031 | 'link' => wp_get_attachment_url( $media_item->ID ), |
| 1032 | 'title' => $media_item->post_title, |
| 1033 | 'caption' => $media_item->post_excerpt, |
| 1034 | 'description' => $media_item->post_content, |
| 1035 | 'metadata' => wp_get_attachment_metadata( $media_item->ID ), |
| 1036 | ); |
| 1037 | |
| 1038 | $response['meta'] = (object) array( |
| 1039 | 'links' => (object) array( |
| 1040 | 'self' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id ), |
| 1041 | 'help' => (string) $this->get_media_link( $this->api->get_blog_id_for_output(), $media_id, 'help' ), |
| 1042 | 'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ), |
| 1043 | ), |
| 1044 | ); |
| 1045 | |
| 1046 | return (object) $response; |
| 1047 | } |
| 1048 | |
| 1049 | function get_taxonomy( $taxonomy_id, $taxonomy_type, $context ) { |
| 1050 | |
| 1051 | $taxonomy = get_term_by( 'slug', $taxonomy_id, $taxonomy_type ); |
| 1052 | /// keep updating this function |
| 1053 | if ( !$taxonomy || is_wp_error( $taxonomy ) ) { |
| 1054 | return new WP_Error( 'unknown_taxonomy', 'Unknown taxonomy', 404 ); |
| 1055 | } |
| 1056 | |
| 1057 | // Permissions |
| 1058 | switch ( $context ) { |
| 1059 | case 'edit' : |
| 1060 | $tax = get_taxonomy( $taxonomy_type ); |
| 1061 | if ( !current_user_can( $tax->cap->edit_terms ) ) |
| 1062 | return new WP_Error( 'unauthorized', 'User cannot edit taxonomy', 403 ); |
| 1063 | break; |
| 1064 | case 'display' : |
| 1065 | if ( -1 == get_option( 'blog_public' ) ) { |
| 1066 | return new WP_Error( 'unauthorized', 'User cannot view taxonomy', 403 ); |
| 1067 | } |
| 1068 | break; |
| 1069 | default : |
| 1070 | return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 ); |
| 1071 | } |
| 1072 | |
| 1073 | $response = array(); |
| 1074 | $response['ID'] = (int) $taxonomy->term_id; |
| 1075 | $response['name'] = (string) $taxonomy->name; |
| 1076 | $response['slug'] = (string) $taxonomy_id; |
| 1077 | $response['description'] = (string) $taxonomy->description; |
| 1078 | $response['post_count'] = (int) $taxonomy->count; |
| 1079 | |
| 1080 | if ( 'category' === $taxonomy_type ) |
| 1081 | $response['parent'] = (int) $taxonomy->parent; |
| 1082 | |
| 1083 | $response['meta'] = (object) array( |
| 1084 | 'links' => (object) array( |
| 1085 | 'self' => (string) $this->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy_id, $taxonomy_type ), |
| 1086 | 'help' => (string) $this->get_taxonomy_link( $this->api->get_blog_id_for_output(), $taxonomy_id, $taxonomy_type, 'help' ), |
| 1087 | 'site' => (string) $this->get_site_link( $this->api->get_blog_id_for_output() ), |
| 1088 | ), |
| 1089 | ); |
| 1090 | |
| 1091 | return (object) $response; |
| 1092 | } |
| 1093 | |
| 1094 | /** |
| 1095 | * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00 |
| 1096 | * |
| 1097 | * @param $date_gmt (string) GMT datetime string. |
| 1098 | * @param $date (string) Optional. Used to calculate the offset from GMT. |
| 1099 | * |
| 1100 | * @return string |
| 1101 | */ |
| 1102 | function format_date( $date_gmt, $date = null ) { |
| 1103 | $timestamp_gmt = strtotime( "$date_gmt+0000" ); |
| 1104 | if ( null === $date ) { |
| 1105 | $timestamp = $timestamp_gmt; |
| 1106 | $hours = $minutes = $west = 0; |
| 1107 | } else { |
| 1108 | $timestamp = strtotime( "$date+0000" ); |
| 1109 | $offset = $timestamp - $timestamp_gmt; |
| 1110 | $west = $offset < 0; |
| 1111 | $offset = abs( $offset ); |
| 1112 | $hours = (int) floor( $offset / 3600 ); |
| 1113 | $offset -= $hours * 3600; |
| 1114 | $minutes = (int) floor( $offset / 60 ); |
| 1115 | } |
| 1116 | |
| 1117 | return (string) gmdate( 'Y-m-d\\TH:i:s', $timestamp ) . sprintf( '%s%02d:%02d', $west ? '-' : '+', $hours, $minutes ); |
| 1118 | } |
| 1119 | |
| 1120 | /** |
| 1121 | * Parses a date string and returns the local and GMT representations |
| 1122 | * of that date & time in 'YYYY-MM-DD HH:MM:SS' format without |
| 1123 | * timezones or offsets. If the parsed datetime was not localized to a |
| 1124 | * particular timezone or offset we will assume it was given in GMT |
| 1125 | * relative to now and will convert it to local time using either the |
| 1126 | * timezone set in the options table for the blog or the GMT offset. |
| 1127 | * |
| 1128 | * @param datetime string |
| 1129 | * |
| 1130 | * @return array( $local_time_string, $gmt_time_string ) |
| 1131 | */ |
| 1132 | function parse_date( $date_string ) { |
| 1133 | $date_string_info = date_parse( $date_string ); |
| 1134 | if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) { |
| 1135 | // Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP. |
| 1136 | if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) { |
| 1137 | $dt_local = clone $dt_utc = new DateTime( $date_string ); |
| 1138 | $dt_utc->setTimezone( new DateTimeZone( 'UTC' ) ); |
| 1139 | return array( |
| 1140 | (string) $dt_local->format( 'Y-m-d H:i:s' ), |
| 1141 | (string) $dt_utc->format( 'Y-m-d H:i:s' ), |
| 1142 | ); |
| 1143 | } |
| 1144 | |
| 1145 | // It's parseable but no TZ info so assume UTC |
| 1146 | $dt_local = clone $dt_utc = new DateTime( $date_string, new DateTimeZone( 'UTC' ) ); |
| 1147 | } else { |
| 1148 | // Could not parse time, use now in UTC |
| 1149 | $dt_local = clone $dt_utc = new DateTime( 'now', new DateTimeZone( 'UTC' ) ); |
| 1150 | } |
| 1151 | |
| 1152 | // First try to use timezone as it's daylight savings aware. |
| 1153 | $timezone_string = get_option( 'timezone_string' ); |
| 1154 | if ( $timezone_string ) { |
| 1155 | $tz = timezone_open( $timezone_string ); |
| 1156 | if ( $tz ) { |
| 1157 | $dt_local->setTimezone( $tz ); |
| 1158 | return array( |
| 1159 | (string) $dt_local->format( 'Y-m-d H:i:s' ), |
| 1160 | (string) $dt_utc->format( 'Y-m-d H:i:s' ), |
| 1161 | ); |
| 1162 | } |
| 1163 | } |
| 1164 | |
| 1165 | // Fallback to GMT offset (in hours) |
| 1166 | // NOTE: TZ of $dt_local is still UTC, we simply modified the timestamp with an offset. |
| 1167 | $gmt_offset_seconds = intval( get_option( 'gmt_offset' ) * 3600 ); |
| 1168 | $dt_local->modify("+{$gmt_offset_seconds} seconds"); |
| 1169 | return array( |
| 1170 | (string) $dt_local->format( 'Y-m-d H:i:s' ), |
| 1171 | (string) $dt_utc->format( 'Y-m-d H:i:s' ), |
| 1172 | ); |
| 1173 | } |
| 1174 | |
| 1175 | function get_link() { |
| 1176 | $args = func_get_args(); |
| 1177 | $format = array_shift( $args ); |
| 1178 | array_unshift( $args, $this->api->public_api_scheme, WPCOM_JSON_API__BASE ); |
| 1179 | $path = array_pop( $args ); |
| 1180 | if ( $path ) { |
| 1181 | $path = '/' . ltrim( $path, '/' ); |
| 1182 | } |
| 1183 | $args[] = $path; |
| 1184 | |
| 1185 | // http, WPCOM_JSON_API__BASE, ... , path |
| 1186 | // %s , %s , $format, %s |
| 1187 | return esc_url_raw( vsprintf( "%s://%s$format%s", $args ) ); |
| 1188 | } |
| 1189 | |
| 1190 | function get_me_link( $path = '' ) { |
| 1191 | return $this->get_link( '/me', $path ); |
| 1192 | } |
| 1193 | |
| 1194 | function get_taxonomy_link( $blog_id, $taxonomy_id, $taxonomy_type, $path = '' ) { |
| 1195 | if ( 'category' === $taxonomy_type ) |
| 1196 | return $this->get_link( '/sites/%d/categories/slug:%s', $blog_id, $taxonomy_id, $path ); |
| 1197 | else |
| 1198 | return $this->get_link( '/sites/%d/tags/slug:%s', $blog_id, $taxonomy_id, $path ); |
| 1199 | } |
| 1200 | |
| 1201 | function get_media_link( $blog_id, $media_id, $path = '' ) { |
| 1202 | return $this->get_link( '/sites/%d/media/%d', $blog_id, $media_id, $path ); |
| 1203 | } |
| 1204 | |
| 1205 | function get_site_link( $blog_id, $path = '' ) { |
| 1206 | return $this->get_link( '/sites/%d', $blog_id, $path ); |
| 1207 | } |
| 1208 | |
| 1209 | function get_post_link( $blog_id, $post_id, $path = '' ) { |
| 1210 | return $this->get_link( '/sites/%d/posts/%d', $blog_id, $post_id, $path ); |
| 1211 | } |
| 1212 | |
| 1213 | function get_comment_link( $blog_id, $comment_id, $path = '' ) { |
| 1214 | return $this->get_link( '/sites/%d/comments/%d', $blog_id, $comment_id, $path ); |
| 1215 | } |
| 1216 | |
| 1217 | function is_post_type_allowed( $post_type ) { |
| 1218 | // if the post type is empty, that's fine, WordPress will default to post |
| 1219 | if ( empty( $post_type ) ) |
| 1220 | return true; |
| 1221 | |
| 1222 | // allow special 'any' type |
| 1223 | if ( 'any' == $post_type ) |
| 1224 | return true; |
| 1225 | |
| 1226 | // check for allowed types |
| 1227 | if ( in_array( $post_type, $this->_get_whitelisted_post_types() ) ) |
| 1228 | return true; |
| 1229 | |
| 1230 | return false; |
| 1231 | } |
| 1232 | |
| 1233 | /** |
| 1234 | * Gets the whitelisted post types that JP should allow access to. |
| 1235 | * |
| 1236 | * @return array Whitelisted post types. |
| 1237 | */ |
| 1238 | protected function _get_whitelisted_post_types() { |
| 1239 | $allowed_types = array( 'post', 'page', 'revision' ); |
| 1240 | |
| 1241 | $allowed_types = apply_filters( 'rest_api_allowed_post_types', $allowed_types ); |
| 1242 | |
| 1243 | return array_unique( $allowed_types ); |
| 1244 | } |
| 1245 | |
| 1246 | function handle_media_sideload( $url, $parent_post_id = 0 ) { |
| 1247 | if ( ! function_exists( 'download_url' ) || ! function_exists( 'media_handle_sideload' ) ) |
| 1248 | return false; |
| 1249 | |
| 1250 | // if we didn't get a URL, let's bail |
| 1251 | $parsed = @parse_url( $url ); |
| 1252 | if ( empty( $parsed ) ) |
| 1253 | return false; |
| 1254 | |
| 1255 | $tmp = download_url( $url ); |
| 1256 | if ( is_wp_error( $tmp ) ) { |
| 1257 | return false; |
| 1258 | } |
| 1259 | |
| 1260 | if ( ! file_is_displayable_image( $tmp ) ) { |
| 1261 | @unlink( $tmp ); |
| 1262 | return false; |
| 1263 | } |
| 1264 | |
| 1265 | // emulate a $_FILES entry |
| 1266 | $file_array = array( |
| 1267 | 'name' => basename( parse_url( $url, PHP_URL_PATH ) ), |
| 1268 | 'tmp_name' => $tmp, |
| 1269 | ); |
| 1270 | |
| 1271 | $id = media_handle_sideload( $file_array, $parent_post_id ); |
| 1272 | @unlink( $tmp ); |
| 1273 | |
| 1274 | if ( ! $id || ! is_int( $id ) ) { |
| 1275 | return false; |
| 1276 | } |
| 1277 | |
| 1278 | return $id; |
| 1279 | } |
| 1280 | |
| 1281 | /** |
| 1282 | * Return endpoint response |
| 1283 | * |
| 1284 | * @param ... determined by ->$path |
| 1285 | * |
| 1286 | * @return |
| 1287 | * falsy: HTTP 500, no response body |
| 1288 | * WP_Error( $error_code, $error_message, $http_status_code ): HTTP $status_code, json_encode( array( 'error' => $error_code, 'message' => $error_message ) ) response body |
| 1289 | * $data: HTTP 200, json_encode( $data ) response body |
| 1290 | */ |
| 1291 | abstract function callback( $path = '' ); |
| 1292 | |
| 1293 | } |
| 1294 | |
| 1295 | require_once( JETPACK__PLUGIN_DIR . 'json-endpoints.php' ); |
| 1296 |