[ Index ]

PHP Cross Reference of WordPress

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


Generated: Mon May 25 01:00:03 2020 Cross-referenced by PHPXref 0.7.1