[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

title

Body

[close]

/wp-includes/rest-api/ -> class-wp-rest-server.php (source)

   1  <?php
   2  /**
   3   * REST API: WP_REST_Server class
   4   *
   5   * @package WordPress
   6   * @subpackage REST_API
   7   * @since 4.4.0
   8   */
   9  
  10  /**
  11   * Core class used to implement the WordPress REST API server.
  12   *
  13   * @since 4.4.0
  14   */
  15  class WP_REST_Server {
  16  
  17      /**
  18       * Alias for GET transport method.
  19       *
  20       * @since 4.4.0
  21       * @var string
  22       */
  23      const READABLE = 'GET';
  24  
  25      /**
  26       * Alias for POST transport method.
  27       *
  28       * @since 4.4.0
  29       * @var string
  30       */
  31      const CREATABLE = 'POST';
  32  
  33      /**
  34       * Alias for POST, PUT, PATCH transport methods together.
  35       *
  36       * @since 4.4.0
  37       * @var string
  38       */
  39      const EDITABLE = 'POST, PUT, PATCH';
  40  
  41      /**
  42       * Alias for DELETE transport method.
  43       *
  44       * @since 4.4.0
  45       * @var string
  46       */
  47      const DELETABLE = 'DELETE';
  48  
  49      /**
  50       * Alias for GET, POST, PUT, PATCH & DELETE transport methods together.
  51       *
  52       * @since 4.4.0
  53       * @var string
  54       */
  55      const ALLMETHODS = 'GET, POST, PUT, PATCH, DELETE';
  56  
  57      /**
  58       * Namespaces registered to the server.
  59       *
  60       * @since 4.4.0
  61       * @var array
  62       */
  63      protected $namespaces = array();
  64  
  65      /**
  66       * Endpoints registered to the server.
  67       *
  68       * @since 4.4.0
  69       * @var array
  70       */
  71      protected $endpoints = array();
  72  
  73      /**
  74       * Options defined for the routes.
  75       *
  76       * @since 4.4.0
  77       * @var array
  78       */
  79      protected $route_options = array();
  80  
  81      /**
  82       * Instantiates the REST server.
  83       *
  84       * @since 4.4.0
  85       */
  86  	public function __construct() {
  87          $this->endpoints = array(
  88              // Meta endpoints.
  89              '/' => array(
  90                  'callback' => array( $this, 'get_index' ),
  91                  'methods'  => 'GET',
  92                  'args'     => array(
  93                      'context' => array(
  94                          'default' => 'view',
  95                      ),
  96                  ),
  97              ),
  98          );
  99      }
 100  
 101  
 102      /**
 103       * Checks the authentication headers if supplied.
 104       *
 105       * @since 4.4.0
 106       *
 107       * @return WP_Error|null WP_Error indicates unsuccessful login, null indicates successful
 108       *                       or no authentication provided
 109       */
 110  	public function check_authentication() {
 111          /**
 112           * Filters REST authentication errors.
 113           *
 114           * This is used to pass a WP_Error from an authentication method back to
 115           * the API.
 116           *
 117           * Authentication methods should check first if they're being used, as
 118           * multiple authentication methods can be enabled on a site (cookies,
 119           * HTTP basic auth, OAuth). If the authentication method hooked in is
 120           * not actually being attempted, null should be returned to indicate
 121           * another authentication method should check instead. Similarly,
 122           * callbacks should ensure the value is `null` before checking for
 123           * errors.
 124           *
 125           * A WP_Error instance can be returned if an error occurs, and this should
 126           * match the format used by API methods internally (that is, the `status`
 127           * data should be used). A callback can return `true` to indicate that
 128           * the authentication method was used, and it succeeded.
 129           *
 130           * @since 4.4.0
 131           *
 132           * @param WP_Error|null|true $errors WP_Error if authentication error, null if authentication
 133           *                                   method wasn't used, true if authentication succeeded.
 134           */
 135          return apply_filters( 'rest_authentication_errors', null );
 136      }
 137  
 138      /**
 139       * Converts an error to a response object.
 140       *
 141       * This iterates over all error codes and messages to change it into a flat
 142       * array. This enables simpler client behaviour, as it is represented as a
 143       * list in JSON rather than an object/map.
 144       *
 145       * @since 4.4.0
 146       *
 147       * @param WP_Error $error WP_Error instance.
 148       * @return WP_REST_Response List of associative arrays with code and message keys.
 149       */
 150  	protected function error_to_response( $error ) {
 151          $error_data = $error->get_error_data();
 152  
 153          if ( is_array( $error_data ) && isset( $error_data['status'] ) ) {
 154              $status = $error_data['status'];
 155          } else {
 156              $status = 500;
 157          }
 158  
 159          $errors = array();
 160  
 161          foreach ( (array) $error->errors as $code => $messages ) {
 162              foreach ( (array) $messages as $message ) {
 163                  $errors[] = array(
 164                      'code'    => $code,
 165                      'message' => $message,
 166                      'data'    => $error->get_error_data( $code ),
 167                  );
 168              }
 169          }
 170  
 171          $data = $errors[0];
 172          if ( count( $errors ) > 1 ) {
 173              // Remove the primary error.
 174              array_shift( $errors );
 175              $data['additional_errors'] = $errors;
 176          }
 177  
 178          $response = new WP_REST_Response( $data, $status );
 179  
 180          return $response;
 181      }
 182  
 183      /**
 184       * Retrieves an appropriate error representation in JSON.
 185       *
 186       * Note: This should only be used in WP_REST_Server::serve_request(), as it
 187       * cannot handle WP_Error internally. All callbacks and other internal methods
 188       * should instead return a WP_Error with the data set to an array that includes
 189       * a 'status' key, with the value being the HTTP status to send.
 190       *
 191       * @since 4.4.0
 192       *
 193       * @param string $code    WP_Error-style code.
 194       * @param string $message Human-readable message.
 195       * @param int    $status  Optional. HTTP status code to send. Default null.
 196       * @return string JSON representation of the error
 197       */
 198  	protected function json_error( $code, $message, $status = null ) {
 199          if ( $status ) {
 200              $this->set_status( $status );
 201          }
 202  
 203          $error = compact( 'code', 'message' );
 204  
 205          return wp_json_encode( $error );
 206      }
 207  
 208      /**
 209       * Handles serving an API request.
 210       *
 211       * Matches the current server URI to a route and runs the first matching
 212       * callback then outputs a JSON representation of the returned value.
 213       *
 214       * @since 4.4.0
 215       *
 216       * @see WP_REST_Server::dispatch()
 217       *
 218       * @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
 219       *                     Default null.
 220       * @return false|null Null if not served and a HEAD request, false otherwise.
 221       */
 222  	public function serve_request( $path = null ) {
 223          $content_type = isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json';
 224          $this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
 225          $this->send_header( 'X-Robots-Tag', 'noindex' );
 226  
 227          $api_root = get_rest_url();
 228          if ( ! empty( $api_root ) ) {
 229              $this->send_header( 'Link', '<' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"' );
 230          }
 231  
 232          /*
 233           * Mitigate possible JSONP Flash attacks.
 234           *
 235           * https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
 236           */
 237          $this->send_header( 'X-Content-Type-Options', 'nosniff' );
 238          $this->send_header( 'Access-Control-Expose-Headers', 'X-WP-Total, X-WP-TotalPages' );
 239          $this->send_header( 'Access-Control-Allow-Headers', 'Authorization, Content-Type' );
 240  
 241          /**
 242           * Send nocache headers on authenticated requests.
 243           *
 244           * @since 4.4.0
 245           *
 246           * @param bool $rest_send_nocache_headers Whether to send no-cache headers.
 247           */
 248          $send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );
 249          if ( $send_no_cache_headers ) {
 250              foreach ( wp_get_nocache_headers() as $header => $header_value ) {
 251                  if ( empty( $header_value ) ) {
 252                      $this->remove_header( $header );
 253                  } else {
 254                      $this->send_header( $header, $header_value );
 255                  }
 256              }
 257          }
 258  
 259          /**
 260           * Filters whether the REST API is enabled.
 261           *
 262           * @since 4.4.0
 263           * @deprecated 4.7.0 Use the {@see 'rest_authentication_errors'} filter to
 264           *                   restrict access to the API.
 265           *
 266           * @param bool $rest_enabled Whether the REST API is enabled. Default true.
 267           */
 268          apply_filters_deprecated(
 269              'rest_enabled',
 270              array( true ),
 271              '4.7.0',
 272              'rest_authentication_errors',
 273              __( 'The REST API can no longer be completely disabled, the rest_authentication_errors filter can be used to restrict access to the API, instead.' )
 274          );
 275  
 276          /**
 277           * Filters whether jsonp is enabled.
 278           *
 279           * @since 4.4.0
 280           *
 281           * @param bool $jsonp_enabled Whether jsonp is enabled. Default true.
 282           */
 283          $jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
 284  
 285          $jsonp_callback = null;
 286  
 287          if ( isset( $_GET['_jsonp'] ) ) {
 288              if ( ! $jsonp_enabled ) {
 289                  echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
 290                  return false;
 291              }
 292  
 293              $jsonp_callback = $_GET['_jsonp'];
 294              if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
 295                  echo $this->json_error( 'rest_callback_invalid', __( 'Invalid JSONP callback function.' ), 400 );
 296                  return false;
 297              }
 298          }
 299  
 300          if ( empty( $path ) ) {
 301              if ( isset( $_SERVER['PATH_INFO'] ) ) {
 302                  $path = $_SERVER['PATH_INFO'];
 303              } else {
 304                  $path = '/';
 305              }
 306          }
 307  
 308          $request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );
 309  
 310          $request->set_query_params( wp_unslash( $_GET ) );
 311          $request->set_body_params( wp_unslash( $_POST ) );
 312          $request->set_file_params( $_FILES );
 313          $request->set_headers( $this->get_headers( wp_unslash( $_SERVER ) ) );
 314          $request->set_body( self::get_raw_data() );
 315  
 316          /*
 317           * HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
 318           * $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
 319           * header.
 320           */
 321          if ( isset( $_GET['_method'] ) ) {
 322              $request->set_method( $_GET['_method'] );
 323          } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
 324              $request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
 325          }
 326  
 327          $result = $this->check_authentication();
 328  
 329          if ( ! is_wp_error( $result ) ) {
 330              $result = $this->dispatch( $request );
 331          }
 332  
 333          // Normalize to either WP_Error or WP_REST_Response...
 334          $result = rest_ensure_response( $result );
 335  
 336          // ...then convert WP_Error across.
 337          if ( is_wp_error( $result ) ) {
 338              $result = $this->error_to_response( $result );
 339          }
 340  
 341          /**
 342           * Filters the API response.
 343           *
 344           * Allows modification of the response before returning.
 345           *
 346           * @since 4.4.0
 347           * @since 4.5.0 Applied to embedded responses.
 348           *
 349           * @param WP_HTTP_Response $result  Result to send to the client. Usually a WP_REST_Response.
 350           * @param WP_REST_Server   $this    Server instance.
 351           * @param WP_REST_Request  $request Request used to generate the response.
 352           */
 353          $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );
 354  
 355          // Wrap the response in an envelope if asked for.
 356          if ( isset( $_GET['_envelope'] ) ) {
 357              $result = $this->envelope_response( $result, isset( $_GET['_embed'] ) );
 358          }
 359  
 360          // Send extra data from response objects.
 361          $headers = $result->get_headers();
 362          $this->send_headers( $headers );
 363  
 364          $code = $result->get_status();
 365          $this->set_status( $code );
 366  
 367          /**
 368           * Filters whether the request has already been served.
 369           *
 370           * Allow sending the request manually - by returning true, the API result
 371           * will not be sent to the client.
 372           *
 373           * @since 4.4.0
 374           *
 375           * @param bool             $served  Whether the request has already been served.
 376           *                                           Default false.
 377           * @param WP_HTTP_Response $result  Result to send to the client. Usually a WP_REST_Response.
 378           * @param WP_REST_Request  $request Request used to generate the response.
 379           * @param WP_REST_Server   $this    Server instance.
 380           */
 381          $served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );
 382  
 383          if ( ! $served ) {
 384              if ( 'HEAD' === $request->get_method() ) {
 385                  return null;
 386              }
 387  
 388              // Embed links inside the request.
 389              $result = $this->response_to_data( $result, isset( $_GET['_embed'] ) );
 390  
 391              /**
 392               * Filters the API response.
 393               *
 394               * Allows modification of the response data after inserting
 395               * embedded data (if any) and before echoing the response data.
 396               *
 397               * @since 4.8.1
 398               *
 399               * @param array            $result  Response data to send to the client.
 400               * @param WP_REST_Server   $this    Server instance.
 401               * @param WP_REST_Request  $request Request used to generate the response.
 402               */
 403              $result = apply_filters( 'rest_pre_echo_response', $result, $this, $request );
 404  
 405              // The 204 response shouldn't have a body.
 406              if ( 204 === $code || null === $result ) {
 407                  return null;
 408              }
 409  
 410              $result = wp_json_encode( $result );
 411  
 412              $json_error_message = $this->get_json_last_error();
 413              if ( $json_error_message ) {
 414                  $json_error_obj = new WP_Error( 'rest_encode_error', $json_error_message, array( 'status' => 500 ) );
 415                  $result         = $this->error_to_response( $json_error_obj );
 416                  $result         = wp_json_encode( $result->data[0] );
 417              }
 418  
 419              if ( $jsonp_callback ) {
 420                  // Prepend '/**/' to mitigate possible JSONP Flash attacks.
 421                  // https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
 422                  echo '/**/' . $jsonp_callback . '(' . $result . ')';
 423              } else {
 424                  echo $result;
 425              }
 426          }
 427          return null;
 428      }
 429  
 430      /**
 431       * Converts a response to data to send.
 432       *
 433       * @since 4.4.0
 434       *
 435       * @param WP_REST_Response $response Response object.
 436       * @param bool             $embed    Whether links should be embedded.
 437       * @return array {
 438       *     Data with sub-requests embedded.
 439       *
 440       *     @type array [$_links]    Links.
 441       *     @type array [$_embedded] Embeddeds.
 442       * }
 443       */
 444  	public function response_to_data( $response, $embed ) {
 445          $data  = $response->get_data();
 446          $links = self::get_compact_response_links( $response );
 447  
 448          if ( ! empty( $links ) ) {
 449              // Convert links to part of the data.
 450              $data['_links'] = $links;
 451          }
 452          if ( $embed ) {
 453              // Determine if this is a numeric array.
 454              if ( wp_is_numeric_array( $data ) ) {
 455                  $data = array_map( array( $this, 'embed_links' ), $data );
 456              } else {
 457                  $data = $this->embed_links( $data );
 458              }
 459          }
 460  
 461          return $data;
 462      }
 463  
 464      /**
 465       * Retrieves links from a response.
 466       *
 467       * Extracts the links from a response into a structured hash, suitable for
 468       * direct output.
 469       *
 470       * @since 4.4.0
 471       *
 472       * @param WP_REST_Response $response Response to extract links from.
 473       * @return array Map of link relation to list of link hashes.
 474       */
 475  	public static function get_response_links( $response ) {
 476          $links = $response->get_links();
 477          if ( empty( $links ) ) {
 478              return array();
 479          }
 480  
 481          // Convert links to part of the data.
 482          $data = array();
 483          foreach ( $links as $rel => $items ) {
 484              $data[ $rel ] = array();
 485  
 486              foreach ( $items as $item ) {
 487                  $attributes         = $item['attributes'];
 488                  $attributes['href'] = $item['href'];
 489                  $data[ $rel ][]     = $attributes;
 490              }
 491          }
 492  
 493          return $data;
 494      }
 495  
 496      /**
 497       * Retrieves the CURIEs (compact URIs) used for relations.
 498       *
 499       * Extracts the links from a response into a structured hash, suitable for
 500       * direct output.
 501       *
 502       * @since 4.5.0
 503       *
 504       * @param WP_REST_Response $response Response to extract links from.
 505       * @return array Map of link relation to list of link hashes.
 506       */
 507  	public static function get_compact_response_links( $response ) {
 508          $links = self::get_response_links( $response );
 509  
 510          if ( empty( $links ) ) {
 511              return array();
 512          }
 513  
 514          $curies      = $response->get_curies();
 515          $used_curies = array();
 516  
 517          foreach ( $links as $rel => $items ) {
 518  
 519              // Convert $rel URIs to their compact versions if they exist.
 520              foreach ( $curies as $curie ) {
 521                  $href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) );
 522                  if ( strpos( $rel, $href_prefix ) !== 0 ) {
 523                      continue;
 524                  }
 525  
 526                  // Relation now changes from '$uri' to '$curie:$relation'.
 527                  $rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) );
 528                  preg_match( '!' . $rel_regex . '!', $rel, $matches );
 529                  if ( $matches ) {
 530                      $new_rel                       = $curie['name'] . ':' . $matches[1];
 531                      $used_curies[ $curie['name'] ] = $curie;
 532                      $links[ $new_rel ]             = $items;
 533                      unset( $links[ $rel ] );
 534                      break;
 535                  }
 536              }
 537          }
 538  
 539          // Push the curies onto the start of the links array.
 540          if ( $used_curies ) {
 541              $links['curies'] = array_values( $used_curies );
 542          }
 543  
 544          return $links;
 545      }
 546  
 547      /**
 548       * Embeds the links from the data into the request.
 549       *
 550       * @since 4.4.0
 551       *
 552       * @param array $data Data from the request.
 553       * @return array {
 554       *     Data with sub-requests embedded.
 555       *
 556       *     @type array [$_links]    Links.
 557       *     @type array [$_embedded] Embeddeds.
 558       * }
 559       */
 560  	protected function embed_links( $data ) {
 561          if ( empty( $data['_links'] ) ) {
 562              return $data;
 563          }
 564  
 565          $embedded = array();
 566  
 567          foreach ( $data['_links'] as $rel => $links ) {
 568              $embeds = array();
 569  
 570              foreach ( $links as $item ) {
 571                  // Determine if the link is embeddable.
 572                  if ( empty( $item['embeddable'] ) ) {
 573                      // Ensure we keep the same order.
 574                      $embeds[] = array();
 575                      continue;
 576                  }
 577  
 578                  // Run through our internal routing and serve.
 579                  $request = WP_REST_Request::from_url( $item['href'] );
 580                  if ( ! $request ) {
 581                      $embeds[] = array();
 582                      continue;
 583                  }
 584  
 585                  // Embedded resources get passed context=embed.
 586                  if ( empty( $request['context'] ) ) {
 587                      $request['context'] = 'embed';
 588                  }
 589  
 590                  $response = $this->dispatch( $request );
 591  
 592                  /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
 593                  $response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request );
 594  
 595                  $embeds[] = $this->response_to_data( $response, false );
 596              }
 597  
 598              // Determine if any real links were found.
 599              $has_links = count( array_filter( $embeds ) );
 600  
 601              if ( $has_links ) {
 602                  $embedded[ $rel ] = $embeds;
 603              }
 604          }
 605  
 606          if ( ! empty( $embedded ) ) {
 607              $data['_embedded'] = $embedded;
 608          }
 609  
 610          return $data;
 611      }
 612  
 613      /**
 614       * Wraps the response in an envelope.
 615       *
 616       * The enveloping technique is used to work around browser/client
 617       * compatibility issues. Essentially, it converts the full HTTP response to
 618       * data instead.
 619       *
 620       * @since 4.4.0
 621       *
 622       * @param WP_REST_Response $response Response object.
 623       * @param bool             $embed    Whether links should be embedded.
 624       * @return WP_REST_Response New response with wrapped data
 625       */
 626  	public function envelope_response( $response, $embed ) {
 627          $envelope = array(
 628              'body'    => $this->response_to_data( $response, $embed ),
 629              'status'  => $response->get_status(),
 630              'headers' => $response->get_headers(),
 631          );
 632  
 633          /**
 634           * Filters the enveloped form of a response.
 635           *
 636           * @since 4.4.0
 637           *
 638           * @param array            $envelope Envelope data.
 639           * @param WP_REST_Response $response Original response data.
 640           */
 641          $envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
 642  
 643          // Ensure it's still a response and return.
 644          return rest_ensure_response( $envelope );
 645      }
 646  
 647      /**
 648       * Registers a route to the server.
 649       *
 650       * @since 4.4.0
 651       *
 652       * @param string $namespace  Namespace.
 653       * @param string $route      The REST route.
 654       * @param array  $route_args Route arguments.
 655       * @param bool   $override   Optional. Whether the route should be overridden if it already exists.
 656       *                           Default false.
 657       */
 658  	public function register_route( $namespace, $route, $route_args, $override = false ) {
 659          if ( ! isset( $this->namespaces[ $namespace ] ) ) {
 660              $this->namespaces[ $namespace ] = array();
 661  
 662              $this->register_route(
 663                  $namespace,
 664                  '/' . $namespace,
 665                  array(
 666                      array(
 667                          'methods'  => self::READABLE,
 668                          'callback' => array( $this, 'get_namespace_index' ),
 669                          'args'     => array(
 670                              'namespace' => array(
 671                                  'default' => $namespace,
 672                              ),
 673                              'context'   => array(
 674                                  'default' => 'view',
 675                              ),
 676                          ),
 677                      ),
 678                  )
 679              );
 680          }
 681  
 682          // Associative to avoid double-registration.
 683          $this->namespaces[ $namespace ][ $route ] = true;
 684          $route_args['namespace']                  = $namespace;
 685  
 686          if ( $override || empty( $this->endpoints[ $route ] ) ) {
 687              $this->endpoints[ $route ] = $route_args;
 688          } else {
 689              $this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
 690          }
 691      }
 692  
 693      /**
 694       * Retrieves the route map.
 695       *
 696       * The route map is an associative array with path regexes as the keys. The
 697       * value is an indexed array with the callback function/method as the first
 698       * item, and a bitmask of HTTP methods as the second item (see the class
 699       * constants).
 700       *
 701       * Each route can be mapped to more than one callback by using an array of
 702       * the indexed arrays. This allows mapping e.g. GET requests to one callback
 703       * and POST requests to another.
 704       *
 705       * Note that the path regexes (array keys) must have @ escaped, as this is
 706       * used as the delimiter with preg_match()
 707       *
 708       * @since 4.4.0
 709       *
 710       * @return array `'/path/regex' => array( $callback, $bitmask )` or
 711       *               `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
 712       */
 713  	public function get_routes() {
 714  
 715          /**
 716           * Filters the array of available endpoints.
 717           *
 718           * @since 4.4.0
 719           *
 720           * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
 721           *                         to an array of callbacks for the endpoint. These take the format
 722           *                         `'/path/regex' => array( $callback, $bitmask )` or
 723           *                         `'/path/regex' => array( array( $callback, $bitmask ).
 724           */
 725          $endpoints = apply_filters( 'rest_endpoints', $this->endpoints );
 726  
 727          // Normalise the endpoints.
 728          $defaults = array(
 729              'methods'       => '',
 730              'accept_json'   => false,
 731              'accept_raw'    => false,
 732              'show_in_index' => true,
 733              'args'          => array(),
 734          );
 735  
 736          foreach ( $endpoints as $route => &$handlers ) {
 737  
 738              if ( isset( $handlers['callback'] ) ) {
 739                  // Single endpoint, add one deeper.
 740                  $handlers = array( $handlers );
 741              }
 742  
 743              if ( ! isset( $this->route_options[ $route ] ) ) {
 744                  $this->route_options[ $route ] = array();
 745              }
 746  
 747              foreach ( $handlers as $key => &$handler ) {
 748  
 749                  if ( ! is_numeric( $key ) ) {
 750                      // Route option, move it to the options.
 751                      $this->route_options[ $route ][ $key ] = $handler;
 752                      unset( $handlers[ $key ] );
 753                      continue;
 754                  }
 755  
 756                  $handler = wp_parse_args( $handler, $defaults );
 757  
 758                  // Allow comma-separated HTTP methods.
 759                  if ( is_string( $handler['methods'] ) ) {
 760                      $methods = explode( ',', $handler['methods'] );
 761                  } elseif ( is_array( $handler['methods'] ) ) {
 762                      $methods = $handler['methods'];
 763                  } else {
 764                      $methods = array();
 765                  }
 766  
 767                  $handler['methods'] = array();
 768  
 769                  foreach ( $methods as $method ) {
 770                      $method                        = strtoupper( trim( $method ) );
 771                      $handler['methods'][ $method ] = true;
 772                  }
 773              }
 774          }
 775  
 776          return $endpoints;
 777      }
 778  
 779      /**
 780       * Retrieves namespaces registered on the server.
 781       *
 782       * @since 4.4.0
 783       *
 784       * @return string[] List of registered namespaces.
 785       */
 786  	public function get_namespaces() {
 787          return array_keys( $this->namespaces );
 788      }
 789  
 790      /**
 791       * Retrieves specified options for a route.
 792       *
 793       * @since 4.4.0
 794       *
 795       * @param string $route Route pattern to fetch options for.
 796       * @return array|null Data as an associative array if found, or null if not found.
 797       */
 798  	public function get_route_options( $route ) {
 799          if ( ! isset( $this->route_options[ $route ] ) ) {
 800              return null;
 801          }
 802  
 803          return $this->route_options[ $route ];
 804      }
 805  
 806      /**
 807       * Matches the request to a callback and call it.
 808       *
 809       * @since 4.4.0
 810       *
 811       * @param WP_REST_Request $request Request to attempt dispatching.
 812       * @return WP_REST_Response Response returned by the callback.
 813       */
 814  	public function dispatch( $request ) {
 815          /**
 816           * Filters the pre-calculated result of a REST dispatch request.
 817           *
 818           * Allow hijacking the request before dispatching by returning a non-empty. The returned value
 819           * will be used to serve the request instead.
 820           *
 821           * @since 4.4.0
 822           *
 823           * @param mixed           $result  Response to replace the requested version with. Can be anything
 824           *                                 a normal endpoint can return, or null to not hijack the request.
 825           * @param WP_REST_Server  $this    Server instance.
 826           * @param WP_REST_Request $request Request used to generate the response.
 827           */
 828          $result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
 829  
 830          if ( ! empty( $result ) ) {
 831              return $result;
 832          }
 833  
 834          $method = $request->get_method();
 835          $path   = $request->get_route();
 836  
 837          foreach ( $this->get_routes() as $route => $handlers ) {
 838              $match = preg_match( '@^' . $route . '$@i', $path, $matches );
 839  
 840              if ( ! $match ) {
 841                  continue;
 842              }
 843  
 844              $args = array();
 845              foreach ( $matches as $param => $value ) {
 846                  if ( ! is_int( $param ) ) {
 847                      $args[ $param ] = $value;
 848                  }
 849              }
 850  
 851              foreach ( $handlers as $handler ) {
 852                  $callback = $handler['callback'];
 853                  $response = null;
 854  
 855                  // Fallback to GET method if no HEAD method is registered.
 856                  $checked_method = $method;
 857                  if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) {
 858                      $checked_method = 'GET';
 859                  }
 860                  if ( empty( $handler['methods'][ $checked_method ] ) ) {
 861                      continue;
 862                  }
 863  
 864                  if ( ! is_callable( $callback ) ) {
 865                      $response = new WP_Error( 'rest_invalid_handler', __( 'The handler for the route is invalid' ), array( 'status' => 500 ) );
 866                  }
 867  
 868                  if ( ! is_wp_error( $response ) ) {
 869                      // Remove the redundant preg_match argument.
 870                      unset( $args[0] );
 871  
 872                      $request->set_url_params( $args );
 873                      $request->set_attributes( $handler );
 874  
 875                      $defaults = array();
 876  
 877                      foreach ( $handler['args'] as $arg => $options ) {
 878                          if ( isset( $options['default'] ) ) {
 879                              $defaults[ $arg ] = $options['default'];
 880                          }
 881                      }
 882  
 883                      $request->set_default_params( $defaults );
 884  
 885                      $check_required = $request->has_valid_params();
 886                      if ( is_wp_error( $check_required ) ) {
 887                          $response = $check_required;
 888                      } else {
 889                          $check_sanitized = $request->sanitize_params();
 890                          if ( is_wp_error( $check_sanitized ) ) {
 891                              $response = $check_sanitized;
 892                          }
 893                      }
 894                  }
 895  
 896                  /**
 897                   * Filters the response before executing any REST API callbacks.
 898                   *
 899                   * Allows plugins to perform additional validation after a
 900                   * request is initialized and matched to a registered route,
 901                   * but before it is executed.
 902                   *
 903                   * Note that this filter will not be called for requests that
 904                   * fail to authenticate or match to a registered route.
 905                   *
 906                   * @since 4.7.0
 907                   *
 908                   * @param WP_HTTP_Response|WP_Error $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
 909                   * @param array                     $handler  Route handler used for the request.
 910                   * @param WP_REST_Request           $request  Request used to generate the response.
 911                   */
 912                  $response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
 913  
 914                  if ( ! is_wp_error( $response ) ) {
 915                      // Check permission specified on the route.
 916                      if ( ! empty( $handler['permission_callback'] ) ) {
 917                          $permission = call_user_func( $handler['permission_callback'], $request );
 918  
 919                          if ( is_wp_error( $permission ) ) {
 920                              $response = $permission;
 921                          } elseif ( false === $permission || null === $permission ) {
 922                              $response = new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to do that.' ), array( 'status' => rest_authorization_required_code() ) );
 923                          }
 924                      }
 925                  }
 926  
 927                  if ( ! is_wp_error( $response ) ) {
 928                      /**
 929                       * Filters the REST dispatch request result.
 930                       *
 931                       * Allow plugins to override dispatching the request.
 932                       *
 933                       * @since 4.4.0
 934                       * @since 4.5.0 Added `$route` and `$handler` parameters.
 935                       *
 936                       * @param mixed           $dispatch_result Dispatch result, will be used if not empty.
 937                       * @param WP_REST_Request $request         Request used to generate the response.
 938                       * @param string          $route           Route matched for the request.
 939                       * @param array           $handler         Route handler used for the request.
 940                       */
 941                      $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
 942  
 943                      // Allow plugins to halt the request via this filter.
 944                      if ( null !== $dispatch_result ) {
 945                          $response = $dispatch_result;
 946                      } else {
 947                          $response = call_user_func( $callback, $request );
 948                      }
 949                  }
 950  
 951                  /**
 952                   * Filters the response immediately after executing any REST API
 953                   * callbacks.
 954                   *
 955                   * Allows plugins to perform any needed cleanup, for example,
 956                   * to undo changes made during the {@see 'rest_request_before_callbacks'}
 957                   * filter.
 958                   *
 959                   * Note that this filter will not be called for requests that
 960                   * fail to authenticate or match to a registered route.
 961                   *
 962                   * Note that an endpoint's `permission_callback` can still be
 963                   * called after this filter - see `rest_send_allow_header()`.
 964                   *
 965                   * @since 4.7.0
 966                   *
 967                   * @param WP_HTTP_Response|WP_Error $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
 968                   * @param array                     $handler  Route handler used for the request.
 969                   * @param WP_REST_Request           $request  Request used to generate the response.
 970                   */
 971                  $response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );
 972  
 973                  if ( is_wp_error( $response ) ) {
 974                      $response = $this->error_to_response( $response );
 975                  } else {
 976                      $response = rest_ensure_response( $response );
 977                  }
 978  
 979                  $response->set_matched_route( $route );
 980                  $response->set_matched_handler( $handler );
 981  
 982                  return $response;
 983              }
 984          }
 985  
 986          return $this->error_to_response( new WP_Error( 'rest_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) ) );
 987      }
 988  
 989      /**
 990       * Returns if an error occurred during most recent JSON encode/decode.
 991       *
 992       * Strings to be translated will be in format like
 993       * "Encoding error: Maximum stack depth exceeded".
 994       *
 995       * @since 4.4.0
 996       *
 997       * @return bool|string Boolean false or string error message.
 998       */
 999  	protected function get_json_last_error() {
1000          $last_error_code = json_last_error();
1001  
1002          if ( JSON_ERROR_NONE === $last_error_code || empty( $last_error_code ) ) {
1003              return false;
1004          }
1005  
1006          return json_last_error_msg();
1007      }
1008  
1009      /**
1010       * Retrieves the site index.
1011       *
1012       * This endpoint describes the capabilities of the site.
1013       *
1014       * @since 4.4.0
1015       *
1016       * @param array $request {
1017       *     Request.
1018       *
1019       *     @type string $context Context.
1020       * }
1021       * @return WP_REST_Response The API root index data.
1022       */
1023  	public function get_index( $request ) {
1024          // General site data.
1025          $available = array(
1026              'name'            => get_option( 'blogname' ),
1027              'description'     => get_option( 'blogdescription' ),
1028              'url'             => get_option( 'siteurl' ),
1029              'home'            => home_url(),
1030              'gmt_offset'      => get_option( 'gmt_offset' ),
1031              'timezone_string' => get_option( 'timezone_string' ),
1032              'namespaces'      => array_keys( $this->namespaces ),
1033              'authentication'  => array(),
1034              'routes'          => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
1035          );
1036  
1037          $response = new WP_REST_Response( $available );
1038  
1039          $response->add_link( 'help', 'http://v2.wp-api.org/' );
1040  
1041          /**
1042           * Filters the API root index data.
1043           *
1044           * This contains the data describing the API. This includes information
1045           * about supported authentication schemes, supported namespaces, routes
1046           * available on the API, and a small amount of data about the site.
1047           *
1048           * @since 4.4.0
1049           *
1050           * @param WP_REST_Response $response Response data.
1051           */
1052          return apply_filters( 'rest_index', $response );
1053      }
1054  
1055      /**
1056       * Retrieves the index for a namespace.
1057       *
1058       * @since 4.4.0
1059       *
1060       * @param WP_REST_Request $request REST request instance.
1061       * @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
1062       *                                   WP_Error if the namespace isn't set.
1063       */
1064  	public function get_namespace_index( $request ) {
1065          $namespace = $request['namespace'];
1066  
1067          if ( ! isset( $this->namespaces[ $namespace ] ) ) {
1068              return new WP_Error( 'rest_invalid_namespace', __( 'The specified namespace could not be found.' ), array( 'status' => 404 ) );
1069          }
1070  
1071          $routes    = $this->namespaces[ $namespace ];
1072          $endpoints = array_intersect_key( $this->get_routes(), $routes );
1073  
1074          $data     = array(
1075              'namespace' => $namespace,
1076              'routes'    => $this->get_data_for_routes( $endpoints, $request['context'] ),
1077          );
1078          $response = rest_ensure_response( $data );
1079  
1080          // Link to the root index.
1081          $response->add_link( 'up', rest_url( '/' ) );
1082  
1083          /**
1084           * Filters the namespace index data.
1085           *
1086           * This typically is just the route data for the namespace, but you can
1087           * add any data you'd like here.
1088           *
1089           * @since 4.4.0
1090           *
1091           * @param WP_REST_Response $response Response data.
1092           * @param WP_REST_Request  $request  Request data. The namespace is passed as the 'namespace' parameter.
1093           */
1094          return apply_filters( 'rest_namespace_index', $response, $request );
1095      }
1096  
1097      /**
1098       * Retrieves the publicly-visible data for routes.
1099       *
1100       * @since 4.4.0
1101       *
1102       * @param array  $routes  Routes to get data for.
1103       * @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
1104       * @return array[] Route data to expose in indexes, keyed by route.
1105       */
1106  	public function get_data_for_routes( $routes, $context = 'view' ) {
1107          $available = array();
1108  
1109          // Find the available routes.
1110          foreach ( $routes as $route => $callbacks ) {
1111              $data = $this->get_data_for_route( $route, $callbacks, $context );
1112              if ( empty( $data ) ) {
1113                  continue;
1114              }
1115  
1116              /**
1117               * Filters the REST endpoint data.
1118               *
1119               * @since 4.4.0
1120               *
1121               * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
1122               */
1123              $available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
1124          }
1125  
1126          /**
1127           * Filters the publicly-visible data for routes.
1128           *
1129           * This data is exposed on indexes and can be used by clients or
1130           * developers to investigate the site and find out how to use it. It
1131           * acts as a form of self-documentation.
1132           *
1133           * @since 4.4.0
1134           *
1135           * @param array[] $available Route data to expose in indexes, keyed by route.
1136           * @param array   $routes    Internal route data as an associative array.
1137           */
1138          return apply_filters( 'rest_route_data', $available, $routes );
1139      }
1140  
1141      /**
1142       * Retrieves publicly-visible data for the route.
1143       *
1144       * @since 4.4.0
1145       *
1146       * @param string $route     Route to get data for.
1147       * @param array  $callbacks Callbacks to convert to data.
1148       * @param string $context   Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
1149       * @return array|null Data for the route, or null if no publicly-visible data.
1150       */
1151  	public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
1152          $data = array(
1153              'namespace' => '',
1154              'methods'   => array(),
1155              'endpoints' => array(),
1156          );
1157  
1158          if ( isset( $this->route_options[ $route ] ) ) {
1159              $options = $this->route_options[ $route ];
1160  
1161              if ( isset( $options['namespace'] ) ) {
1162                  $data['namespace'] = $options['namespace'];
1163              }
1164  
1165              if ( isset( $options['schema'] ) && 'help' === $context ) {
1166                  $data['schema'] = call_user_func( $options['schema'] );
1167              }
1168          }
1169  
1170          $route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
1171  
1172          foreach ( $callbacks as $callback ) {
1173              // Skip to the next route if any callback is hidden.
1174              if ( empty( $callback['show_in_index'] ) ) {
1175                  continue;
1176              }
1177  
1178              $data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
1179              $endpoint_data   = array(
1180                  'methods' => array_keys( $callback['methods'] ),
1181              );
1182  
1183              if ( isset( $callback['args'] ) ) {
1184                  $endpoint_data['args'] = array();
1185                  foreach ( $callback['args'] as $key => $opts ) {
1186                      $arg_data = array(
1187                          'required' => ! empty( $opts['required'] ),
1188                      );
1189                      if ( isset( $opts['default'] ) ) {
1190                          $arg_data['default'] = $opts['default'];
1191                      }
1192                      if ( isset( $opts['enum'] ) ) {
1193                          $arg_data['enum'] = $opts['enum'];
1194                      }
1195                      if ( isset( $opts['description'] ) ) {
1196                          $arg_data['description'] = $opts['description'];
1197                      }
1198                      if ( isset( $opts['type'] ) ) {
1199                          $arg_data['type'] = $opts['type'];
1200                      }
1201                      if ( isset( $opts['items'] ) ) {
1202                          $arg_data['items'] = $opts['items'];
1203                      }
1204                      $endpoint_data['args'][ $key ] = $arg_data;
1205                  }
1206              }
1207  
1208              $data['endpoints'][] = $endpoint_data;
1209  
1210              // For non-variable routes, generate links.
1211              if ( strpos( $route, '{' ) === false ) {
1212                  $data['_links'] = array(
1213                      'self' => rest_url( $route ),
1214                  );
1215              }
1216          }
1217  
1218          if ( empty( $data['methods'] ) ) {
1219              // No methods supported, hide the route.
1220              return null;
1221          }
1222  
1223          return $data;
1224      }
1225  
1226      /**
1227       * Sends an HTTP status code.
1228       *
1229       * @since 4.4.0
1230       *
1231       * @param int $code HTTP status.
1232       */
1233  	protected function set_status( $code ) {
1234          status_header( $code );
1235      }
1236  
1237      /**
1238       * Sends an HTTP header.
1239       *
1240       * @since 4.4.0
1241       *
1242       * @param string $key Header key.
1243       * @param string $value Header value.
1244       */
1245  	public function send_header( $key, $value ) {
1246          /*
1247           * Sanitize as per RFC2616 (Section 4.2):
1248           *
1249           * Any LWS that occurs between field-content MAY be replaced with a
1250           * single SP before interpreting the field value or forwarding the
1251           * message downstream.
1252           */
1253          $value = preg_replace( '/\s+/', ' ', $value );
1254          header( sprintf( '%s: %s', $key, $value ) );
1255      }
1256  
1257      /**
1258       * Sends multiple HTTP headers.
1259       *
1260       * @since 4.4.0
1261       *
1262       * @param array $headers Map of header name to header value.
1263       */
1264  	public function send_headers( $headers ) {
1265          foreach ( $headers as $key => $value ) {
1266              $this->send_header( $key, $value );
1267          }
1268      }
1269  
1270      /**
1271       * Removes an HTTP header from the current response.
1272       *
1273       * @since 4.8.0
1274       *
1275       * @param string $key Header key.
1276       */
1277  	public function remove_header( $key ) {
1278          header_remove( $key );
1279      }
1280  
1281      /**
1282       * Retrieves the raw request entity (body).
1283       *
1284       * @since 4.4.0
1285       *
1286       * @global string $HTTP_RAW_POST_DATA Raw post data.
1287       *
1288       * @return string Raw request data.
1289       */
1290  	public static function get_raw_data() {
1291          global $HTTP_RAW_POST_DATA;
1292  
1293          /*
1294           * A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
1295           * but we can do it ourself.
1296           */
1297          if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
1298              $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
1299          }
1300  
1301          return $HTTP_RAW_POST_DATA;
1302      }
1303  
1304      /**
1305       * Extracts headers from a PHP-style $_SERVER array.
1306       *
1307       * @since 4.4.0
1308       *
1309       * @param array $server Associative array similar to `$_SERVER`.
1310       * @return array Headers extracted from the input.
1311       */
1312  	public function get_headers( $server ) {
1313          $headers = array();
1314  
1315          // CONTENT_* headers are not prefixed with HTTP_.
1316          $additional = array(
1317              'CONTENT_LENGTH' => true,
1318              'CONTENT_MD5'    => true,
1319              'CONTENT_TYPE'   => true,
1320          );
1321  
1322          foreach ( $server as $key => $value ) {
1323              if ( strpos( $key, 'HTTP_' ) === 0 ) {
1324                  $headers[ substr( $key, 5 ) ] = $value;
1325              } elseif ( isset( $additional[ $key ] ) ) {
1326                  $headers[ $key ] = $value;
1327              }
1328          }
1329  
1330          return $headers;
1331      }
1332  }


Generated: Sat Nov 23 20:47:33 2019 Cross-referenced by PHPXref 0.7