[ 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              '/batch/v1' => array(
 107                  'callback' => array( $this, 'serve_batch_request_v1' ),
 108                  'methods'  => 'POST',
 109                  'args'     => array(
 110                      'validation' => array(
 111                          'type'    => 'string',
 112                          'enum'    => array( 'require-all-validate', 'normal' ),
 113                          'default' => 'normal',
 114                      ),
 115                      'requests'   => array(
 116                          'required' => true,
 117                          'type'     => 'array',
 118                          'maxItems' => $this->get_max_batch_size(),
 119                          'items'    => array(
 120                              'type'       => 'object',
 121                              'properties' => array(
 122                                  'method'  => array(
 123                                      'type'    => 'string',
 124                                      'enum'    => array( 'POST', 'PUT', 'PATCH', 'DELETE' ),
 125                                      'default' => 'POST',
 126                                  ),
 127                                  'path'    => array(
 128                                      'type'     => 'string',
 129                                      'required' => true,
 130                                  ),
 131                                  'body'    => array(
 132                                      'type'                 => 'object',
 133                                      'properties'           => array(),
 134                                      'additionalProperties' => true,
 135                                  ),
 136                                  'headers' => array(
 137                                      'type'                 => 'object',
 138                                      'properties'           => array(),
 139                                      'additionalProperties' => array(
 140                                          'type'  => array( 'string', 'array' ),
 141                                          'items' => array(
 142                                              'type' => 'string',
 143                                          ),
 144                                      ),
 145                                  ),
 146                              ),
 147                          ),
 148                      ),
 149                  ),
 150              ),
 151          );
 152      }
 153  
 154  
 155      /**
 156       * Checks the authentication headers if supplied.
 157       *
 158       * @since 4.4.0
 159       *
 160       * @return WP_Error|null WP_Error indicates unsuccessful login, null indicates successful
 161       *                       or no authentication provided
 162       */
 163  	public function check_authentication() {
 164          /**
 165           * Filters REST API authentication errors.
 166           *
 167           * This is used to pass a WP_Error from an authentication method back to
 168           * the API.
 169           *
 170           * Authentication methods should check first if they're being used, as
 171           * multiple authentication methods can be enabled on a site (cookies,
 172           * HTTP basic auth, OAuth). If the authentication method hooked in is
 173           * not actually being attempted, null should be returned to indicate
 174           * another authentication method should check instead. Similarly,
 175           * callbacks should ensure the value is `null` before checking for
 176           * errors.
 177           *
 178           * A WP_Error instance can be returned if an error occurs, and this should
 179           * match the format used by API methods internally (that is, the `status`
 180           * data should be used). A callback can return `true` to indicate that
 181           * the authentication method was used, and it succeeded.
 182           *
 183           * @since 4.4.0
 184           *
 185           * @param WP_Error|null|true $errors WP_Error if authentication error, null if authentication
 186           *                                   method wasn't used, true if authentication succeeded.
 187           */
 188          return apply_filters( 'rest_authentication_errors', null );
 189      }
 190  
 191      /**
 192       * Converts an error to a response object.
 193       *
 194       * This iterates over all error codes and messages to change it into a flat
 195       * array. This enables simpler client behaviour, as it is represented as a
 196       * list in JSON rather than an object/map.
 197       *
 198       * @since 4.4.0
 199       * @since 5.7.0 Converted to a wrapper of {@see rest_convert_error_to_response()}.
 200       *
 201       * @param WP_Error $error WP_Error instance.
 202       * @return WP_REST_Response List of associative arrays with code and message keys.
 203       */
 204  	protected function error_to_response( $error ) {
 205          return rest_convert_error_to_response( $error );
 206      }
 207  
 208      /**
 209       * Retrieves an appropriate error representation in JSON.
 210       *
 211       * Note: This should only be used in WP_REST_Server::serve_request(), as it
 212       * cannot handle WP_Error internally. All callbacks and other internal methods
 213       * should instead return a WP_Error with the data set to an array that includes
 214       * a 'status' key, with the value being the HTTP status to send.
 215       *
 216       * @since 4.4.0
 217       *
 218       * @param string $code    WP_Error-style code.
 219       * @param string $message Human-readable message.
 220       * @param int    $status  Optional. HTTP status code to send. Default null.
 221       * @return string JSON representation of the error
 222       */
 223  	protected function json_error( $code, $message, $status = null ) {
 224          if ( $status ) {
 225              $this->set_status( $status );
 226          }
 227  
 228          $error = compact( 'code', 'message' );
 229  
 230          return wp_json_encode( $error );
 231      }
 232  
 233      /**
 234       * Handles serving a REST API request.
 235       *
 236       * Matches the current server URI to a route and runs the first matching
 237       * callback then outputs a JSON representation of the returned value.
 238       *
 239       * @since 4.4.0
 240       *
 241       * @see WP_REST_Server::dispatch()
 242       *
 243       * @global WP_User $current_user The currently authenticated user.
 244       *
 245       * @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
 246       *                     Default null.
 247       * @return null|false Null if not served and a HEAD request, false otherwise.
 248       */
 249  	public function serve_request( $path = null ) {
 250          /* @var WP_User|null $current_user */
 251          global $current_user;
 252  
 253          if ( $current_user instanceof WP_User && ! $current_user->exists() ) {
 254              /*
 255               * If there is no current user authenticated via other means, clear
 256               * the cached lack of user, so that an authenticate check can set it
 257               * properly.
 258               *
 259               * This is done because for authentications such as Application
 260               * Passwords, we don't want it to be accepted unless the current HTTP
 261               * request is a REST API request, which can't always be identified early
 262               * enough in evaluation.
 263               */
 264              $current_user = null;
 265          }
 266  
 267          /**
 268           * Filters whether JSONP is enabled for the REST API.
 269           *
 270           * @since 4.4.0
 271           *
 272           * @param bool $jsonp_enabled Whether JSONP is enabled. Default true.
 273           */
 274          $jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
 275  
 276          $jsonp_callback = false;
 277          if ( isset( $_GET['_jsonp'] ) ) {
 278              $jsonp_callback = $_GET['_jsonp'];
 279          }
 280  
 281          $content_type = ( $jsonp_callback && $jsonp_enabled ) ? 'application/javascript' : 'application/json';
 282          $this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
 283          $this->send_header( 'X-Robots-Tag', 'noindex' );
 284  
 285          $api_root = get_rest_url();
 286          if ( ! empty( $api_root ) ) {
 287              $this->send_header( 'Link', '<' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"' );
 288          }
 289  
 290          /*
 291           * Mitigate possible JSONP Flash attacks.
 292           *
 293           * https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
 294           */
 295          $this->send_header( 'X-Content-Type-Options', 'nosniff' );
 296          $expose_headers = array( 'X-WP-Total', 'X-WP-TotalPages', 'Link' );
 297  
 298          /**
 299           * Filters the list of response headers that are exposed to REST API CORS requests.
 300           *
 301           * @since 5.5.0
 302           *
 303           * @param string[] $expose_headers The list of response headers to expose.
 304           */
 305          $expose_headers = apply_filters( 'rest_exposed_cors_headers', $expose_headers );
 306  
 307          $this->send_header( 'Access-Control-Expose-Headers', implode( ', ', $expose_headers ) );
 308  
 309          $allow_headers = array(
 310              'Authorization',
 311              'X-WP-Nonce',
 312              'Content-Disposition',
 313              'Content-MD5',
 314              'Content-Type',
 315          );
 316  
 317          /**
 318           * Filters the list of request headers that are allowed for REST API CORS requests.
 319           *
 320           * The allowed headers are passed to the browser to specify which
 321           * headers can be passed to the REST API. By default, we allow the
 322           * Content-* headers needed to upload files to the media endpoints.
 323           * As well as the Authorization and Nonce headers for allowing authentication.
 324           *
 325           * @since 5.5.0
 326           *
 327           * @param string[] $allow_headers The list of request headers to allow.
 328           */
 329          $allow_headers = apply_filters( 'rest_allowed_cors_headers', $allow_headers );
 330  
 331          $this->send_header( 'Access-Control-Allow-Headers', implode( ', ', $allow_headers ) );
 332  
 333          /**
 334           * Filters whether to send nocache headers on a REST API request.
 335           *
 336           * @since 4.4.0
 337           *
 338           * @param bool $rest_send_nocache_headers Whether to send no-cache headers.
 339           */
 340          $send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );
 341          if ( $send_no_cache_headers ) {
 342              foreach ( wp_get_nocache_headers() as $header => $header_value ) {
 343                  if ( empty( $header_value ) ) {
 344                      $this->remove_header( $header );
 345                  } else {
 346                      $this->send_header( $header, $header_value );
 347                  }
 348              }
 349          }
 350  
 351          /**
 352           * Filters whether the REST API is enabled.
 353           *
 354           * @since 4.4.0
 355           * @deprecated 4.7.0 Use the {@see 'rest_authentication_errors'} filter to
 356           *                   restrict access to the REST API.
 357           *
 358           * @param bool $rest_enabled Whether the REST API is enabled. Default true.
 359           */
 360          apply_filters_deprecated(
 361              'rest_enabled',
 362              array( true ),
 363              '4.7.0',
 364              'rest_authentication_errors',
 365              sprintf(
 366                  /* translators: %s: rest_authentication_errors */
 367                  __( 'The REST API can no longer be completely disabled, the %s filter can be used to restrict access to the API, instead.' ),
 368                  'rest_authentication_errors'
 369              )
 370          );
 371  
 372          if ( $jsonp_callback ) {
 373              if ( ! $jsonp_enabled ) {
 374                  echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
 375                  return false;
 376              }
 377  
 378              if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
 379                  echo $this->json_error( 'rest_callback_invalid', __( 'Invalid JSONP callback function.' ), 400 );
 380                  return false;
 381              }
 382          }
 383  
 384          if ( empty( $path ) ) {
 385              if ( isset( $_SERVER['PATH_INFO'] ) ) {
 386                  $path = $_SERVER['PATH_INFO'];
 387              } else {
 388                  $path = '/';
 389              }
 390          }
 391  
 392          $request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );
 393  
 394          $request->set_query_params( wp_unslash( $_GET ) );
 395          $request->set_body_params( wp_unslash( $_POST ) );
 396          $request->set_file_params( $_FILES );
 397          $request->set_headers( $this->get_headers( wp_unslash( $_SERVER ) ) );
 398          $request->set_body( self::get_raw_data() );
 399  
 400          /*
 401           * HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
 402           * $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
 403           * header.
 404           */
 405          if ( isset( $_GET['_method'] ) ) {
 406              $request->set_method( $_GET['_method'] );
 407          } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
 408              $request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
 409          }
 410  
 411          $result = $this->check_authentication();
 412  
 413          if ( ! is_wp_error( $result ) ) {
 414              $result = $this->dispatch( $request );
 415          }
 416  
 417          // Normalize to either WP_Error or WP_REST_Response...
 418          $result = rest_ensure_response( $result );
 419  
 420          // ...then convert WP_Error across.
 421          if ( is_wp_error( $result ) ) {
 422              $result = $this->error_to_response( $result );
 423          }
 424  
 425          /**
 426           * Filters the REST API response.
 427           *
 428           * Allows modification of the response before returning.
 429           *
 430           * @since 4.4.0
 431           * @since 4.5.0 Applied to embedded responses.
 432           *
 433           * @param WP_HTTP_Response $result  Result to send to the client. Usually a `WP_REST_Response`.
 434           * @param WP_REST_Server   $server  Server instance.
 435           * @param WP_REST_Request  $request Request used to generate the response.
 436           */
 437          $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );
 438  
 439          // Wrap the response in an envelope if asked for.
 440          if ( isset( $_GET['_envelope'] ) ) {
 441              $embed  = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false;
 442              $result = $this->envelope_response( $result, $embed );
 443          }
 444  
 445          // Send extra data from response objects.
 446          $headers = $result->get_headers();
 447          $this->send_headers( $headers );
 448  
 449          $code = $result->get_status();
 450          $this->set_status( $code );
 451  
 452          /**
 453           * Filters whether the REST API request has already been served.
 454           *
 455           * Allow sending the request manually - by returning true, the API result
 456           * will not be sent to the client.
 457           *
 458           * @since 4.4.0
 459           *
 460           * @param bool             $served  Whether the request has already been served.
 461           *                                           Default false.
 462           * @param WP_HTTP_Response $result  Result to send to the client. Usually a `WP_REST_Response`.
 463           * @param WP_REST_Request  $request Request used to generate the response.
 464           * @param WP_REST_Server   $server  Server instance.
 465           */
 466          $served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );
 467  
 468          if ( ! $served ) {
 469              if ( 'HEAD' === $request->get_method() ) {
 470                  return null;
 471              }
 472  
 473              // Embed links inside the request.
 474              $embed  = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false;
 475              $result = $this->response_to_data( $result, $embed );
 476  
 477              /**
 478               * Filters the REST API response.
 479               *
 480               * Allows modification of the response data after inserting
 481               * embedded data (if any) and before echoing the response data.
 482               *
 483               * @since 4.8.1
 484               *
 485               * @param array            $result  Response data to send to the client.
 486               * @param WP_REST_Server   $server  Server instance.
 487               * @param WP_REST_Request  $request Request used to generate the response.
 488               */
 489              $result = apply_filters( 'rest_pre_echo_response', $result, $this, $request );
 490  
 491              // The 204 response shouldn't have a body.
 492              if ( 204 === $code || null === $result ) {
 493                  return null;
 494              }
 495  
 496              $result = wp_json_encode( $result );
 497  
 498              $json_error_message = $this->get_json_last_error();
 499  
 500              if ( $json_error_message ) {
 501                  $this->set_status( 500 );
 502                  $json_error_obj = new WP_Error(
 503                      'rest_encode_error',
 504                      $json_error_message,
 505                      array( 'status' => 500 )
 506                  );
 507  
 508                  $result = $this->error_to_response( $json_error_obj );
 509                  $result = wp_json_encode( $result->data );
 510              }
 511  
 512              if ( $jsonp_callback ) {
 513                  // Prepend '/**/' to mitigate possible JSONP Flash attacks.
 514                  // https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
 515                  echo '/**/' . $jsonp_callback . '(' . $result . ')';
 516              } else {
 517                  echo $result;
 518              }
 519          }
 520  
 521          return null;
 522      }
 523  
 524      /**
 525       * Converts a response to data to send.
 526       *
 527       * @since 4.4.0
 528       * @since 5.4.0 The $embed parameter can now contain a list of link relations to include.
 529       *
 530       * @param WP_REST_Response $response Response object.
 531       * @param bool|string[]    $embed    Whether to embed all links, a filtered list of link relations, or no links.
 532       * @return array {
 533       *     Data with sub-requests embedded.
 534       *
 535       *     @type array $_links    Links.
 536       *     @type array $_embedded Embedded objects.
 537       * }
 538       */
 539  	public function response_to_data( $response, $embed ) {
 540          $data  = $response->get_data();
 541          $links = self::get_compact_response_links( $response );
 542  
 543          if ( ! empty( $links ) ) {
 544              // Convert links to part of the data.
 545              $data['_links'] = $links;
 546          }
 547  
 548          if ( $embed ) {
 549              $this->embed_cache = array();
 550              // Determine if this is a numeric array.
 551              if ( wp_is_numeric_array( $data ) ) {
 552                  foreach ( $data as $key => $item ) {
 553                      $data[ $key ] = $this->embed_links( $item, $embed );
 554                  }
 555              } else {
 556                  $data = $this->embed_links( $data, $embed );
 557              }
 558              $this->embed_cache = array();
 559          }
 560  
 561          return $data;
 562      }
 563  
 564      /**
 565       * Retrieves links from a response.
 566       *
 567       * Extracts the links from a response into a structured hash, suitable for
 568       * direct output.
 569       *
 570       * @since 4.4.0
 571       *
 572       * @param WP_REST_Response $response Response to extract links from.
 573       * @return array Map of link relation to list of link hashes.
 574       */
 575  	public static function get_response_links( $response ) {
 576          $links = $response->get_links();
 577  
 578          if ( empty( $links ) ) {
 579              return array();
 580          }
 581  
 582          // Convert links to part of the data.
 583          $data = array();
 584          foreach ( $links as $rel => $items ) {
 585              $data[ $rel ] = array();
 586  
 587              foreach ( $items as $item ) {
 588                  $attributes         = $item['attributes'];
 589                  $attributes['href'] = $item['href'];
 590                  $data[ $rel ][]     = $attributes;
 591              }
 592          }
 593  
 594          return $data;
 595      }
 596  
 597      /**
 598       * Retrieves the CURIEs (compact URIs) used for relations.
 599       *
 600       * Extracts the links from a response into a structured hash, suitable for
 601       * direct output.
 602       *
 603       * @since 4.5.0
 604       *
 605       * @param WP_REST_Response $response Response to extract links from.
 606       * @return array Map of link relation to list of link hashes.
 607       */
 608  	public static function get_compact_response_links( $response ) {
 609          $links = self::get_response_links( $response );
 610  
 611          if ( empty( $links ) ) {
 612              return array();
 613          }
 614  
 615          $curies      = $response->get_curies();
 616          $used_curies = array();
 617  
 618          foreach ( $links as $rel => $items ) {
 619  
 620              // Convert $rel URIs to their compact versions if they exist.
 621              foreach ( $curies as $curie ) {
 622                  $href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) );
 623                  if ( strpos( $rel, $href_prefix ) !== 0 ) {
 624                      continue;
 625                  }
 626  
 627                  // Relation now changes from '$uri' to '$curie:$relation'.
 628                  $rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) );
 629                  preg_match( '!' . $rel_regex . '!', $rel, $matches );
 630                  if ( $matches ) {
 631                      $new_rel                       = $curie['name'] . ':' . $matches[1];
 632                      $used_curies[ $curie['name'] ] = $curie;
 633                      $links[ $new_rel ]             = $items;
 634                      unset( $links[ $rel ] );
 635                      break;
 636                  }
 637              }
 638          }
 639  
 640          // Push the curies onto the start of the links array.
 641          if ( $used_curies ) {
 642              $links['curies'] = array_values( $used_curies );
 643          }
 644  
 645          return $links;
 646      }
 647  
 648      /**
 649       * Embeds the links from the data into the request.
 650       *
 651       * @since 4.4.0
 652       * @since 5.4.0 The $embed parameter can now contain a list of link relations to include.
 653       *
 654       * @param array         $data  Data from the request.
 655       * @param bool|string[] $embed Whether to embed all links or a filtered list of link relations.
 656       * @return array {
 657       *     Data with sub-requests embedded.
 658       *
 659       *     @type array $_links    Links.
 660       *     @type array $_embedded Embedded objects.
 661       * }
 662       */
 663  	protected function embed_links( $data, $embed = true ) {
 664          if ( empty( $data['_links'] ) ) {
 665              return $data;
 666          }
 667  
 668          $embedded = array();
 669  
 670          foreach ( $data['_links'] as $rel => $links ) {
 671              // If a list of relations was specified, and the link relation
 672              // is not in the list of allowed relations, don't process the link.
 673              if ( is_array( $embed ) && ! in_array( $rel, $embed, true ) ) {
 674                  continue;
 675              }
 676  
 677              $embeds = array();
 678  
 679              foreach ( $links as $item ) {
 680                  // Determine if the link is embeddable.
 681                  if ( empty( $item['embeddable'] ) ) {
 682                      // Ensure we keep the same order.
 683                      $embeds[] = array();
 684                      continue;
 685                  }
 686  
 687                  if ( ! array_key_exists( $item['href'], $this->embed_cache ) ) {
 688                      // Run through our internal routing and serve.
 689                      $request = WP_REST_Request::from_url( $item['href'] );
 690                      if ( ! $request ) {
 691                          $embeds[] = array();
 692                          continue;
 693                      }
 694  
 695                      // Embedded resources get passed context=embed.
 696                      if ( empty( $request['context'] ) ) {
 697                          $request['context'] = 'embed';
 698                      }
 699  
 700                      $response = $this->dispatch( $request );
 701  
 702                      /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
 703                      $response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request );
 704  
 705                      $this->embed_cache[ $item['href'] ] = $this->response_to_data( $response, false );
 706                  }
 707  
 708                  $embeds[] = $this->embed_cache[ $item['href'] ];
 709              }
 710  
 711              // Determine if any real links were found.
 712              $has_links = count( array_filter( $embeds ) );
 713  
 714              if ( $has_links ) {
 715                  $embedded[ $rel ] = $embeds;
 716              }
 717          }
 718  
 719          if ( ! empty( $embedded ) ) {
 720              $data['_embedded'] = $embedded;
 721          }
 722  
 723          return $data;
 724      }
 725  
 726      /**
 727       * Wraps the response in an envelope.
 728       *
 729       * The enveloping technique is used to work around browser/client
 730       * compatibility issues. Essentially, it converts the full HTTP response to
 731       * data instead.
 732       *
 733       * @since 4.4.0
 734       * @since 6.0.0 The $embed parameter can now contain a list of link relations to include
 735       *
 736       * @param WP_REST_Response $response Response object.
 737       * @param bool|string[]    $embed    Whether to embed all links, a filtered list of link relations, or no links.
 738       * @return WP_REST_Response New response with wrapped data
 739       */
 740  	public function envelope_response( $response, $embed ) {
 741          $envelope = array(
 742              'body'    => $this->response_to_data( $response, $embed ),
 743              'status'  => $response->get_status(),
 744              'headers' => $response->get_headers(),
 745          );
 746  
 747          /**
 748           * Filters the enveloped form of a REST API response.
 749           *
 750           * @since 4.4.0
 751           *
 752           * @param array            $envelope {
 753           *     Envelope data.
 754           *
 755           *     @type array $body    Response data.
 756           *     @type int   $status  The 3-digit HTTP status code.
 757           *     @type array $headers Map of header name to header value.
 758           * }
 759           * @param WP_REST_Response $response Original response data.
 760           */
 761          $envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
 762  
 763          // Ensure it's still a response and return.
 764          return rest_ensure_response( $envelope );
 765      }
 766  
 767      /**
 768       * Registers a route to the server.
 769       *
 770       * @since 4.4.0
 771       *
 772       * @param string $namespace  Namespace.
 773       * @param string $route      The REST route.
 774       * @param array  $route_args Route arguments.
 775       * @param bool   $override   Optional. Whether the route should be overridden if it already exists.
 776       *                           Default false.
 777       */
 778  	public function register_route( $namespace, $route, $route_args, $override = false ) {
 779          if ( ! isset( $this->namespaces[ $namespace ] ) ) {
 780              $this->namespaces[ $namespace ] = array();
 781  
 782              $this->register_route(
 783                  $namespace,
 784                  '/' . $namespace,
 785                  array(
 786                      array(
 787                          'methods'  => self::READABLE,
 788                          'callback' => array( $this, 'get_namespace_index' ),
 789                          'args'     => array(
 790                              'namespace' => array(
 791                                  'default' => $namespace,
 792                              ),
 793                              'context'   => array(
 794                                  'default' => 'view',
 795                              ),
 796                          ),
 797                      ),
 798                  )
 799              );
 800          }
 801  
 802          // Associative to avoid double-registration.
 803          $this->namespaces[ $namespace ][ $route ] = true;
 804          $route_args['namespace']                  = $namespace;
 805  
 806          if ( $override || empty( $this->endpoints[ $route ] ) ) {
 807              $this->endpoints[ $route ] = $route_args;
 808          } else {
 809              $this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
 810          }
 811      }
 812  
 813      /**
 814       * Retrieves the route map.
 815       *
 816       * The route map is an associative array with path regexes as the keys. The
 817       * value is an indexed array with the callback function/method as the first
 818       * item, and a bitmask of HTTP methods as the second item (see the class
 819       * constants).
 820       *
 821       * Each route can be mapped to more than one callback by using an array of
 822       * the indexed arrays. This allows mapping e.g. GET requests to one callback
 823       * and POST requests to another.
 824       *
 825       * Note that the path regexes (array keys) must have @ escaped, as this is
 826       * used as the delimiter with preg_match()
 827       *
 828       * @since 4.4.0
 829       * @since 5.4.0 Add $namespace parameter.
 830       *
 831       * @param string $namespace Optionally, only return routes in the given namespace.
 832       * @return array `'/path/regex' => array( $callback, $bitmask )` or
 833       *               `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
 834       */
 835  	public function get_routes( $namespace = '' ) {
 836          $endpoints = $this->endpoints;
 837  
 838          if ( $namespace ) {
 839              $endpoints = wp_list_filter( $endpoints, array( 'namespace' => $namespace ) );
 840          }
 841  
 842          /**
 843           * Filters the array of available REST API endpoints.
 844           *
 845           * @since 4.4.0
 846           *
 847           * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
 848           *                         to an array of callbacks for the endpoint. These take the format
 849           *                         `'/path/regex' => array( $callback, $bitmask )` or
 850           *                         `'/path/regex' => array( array( $callback, $bitmask ).
 851           */
 852          $endpoints = apply_filters( 'rest_endpoints', $endpoints );
 853  
 854          // Normalize the endpoints.
 855          $defaults = array(
 856              'methods'       => '',
 857              'accept_json'   => false,
 858              'accept_raw'    => false,
 859              'show_in_index' => true,
 860              'args'          => array(),
 861          );
 862  
 863          foreach ( $endpoints as $route => &$handlers ) {
 864  
 865              if ( isset( $handlers['callback'] ) ) {
 866                  // Single endpoint, add one deeper.
 867                  $handlers = array( $handlers );
 868              }
 869  
 870              if ( ! isset( $this->route_options[ $route ] ) ) {
 871                  $this->route_options[ $route ] = array();
 872              }
 873  
 874              foreach ( $handlers as $key => &$handler ) {
 875  
 876                  if ( ! is_numeric( $key ) ) {
 877                      // Route option, move it to the options.
 878                      $this->route_options[ $route ][ $key ] = $handler;
 879                      unset( $handlers[ $key ] );
 880                      continue;
 881                  }
 882  
 883                  $handler = wp_parse_args( $handler, $defaults );
 884  
 885                  // Allow comma-separated HTTP methods.
 886                  if ( is_string( $handler['methods'] ) ) {
 887                      $methods = explode( ',', $handler['methods'] );
 888                  } elseif ( is_array( $handler['methods'] ) ) {
 889                      $methods = $handler['methods'];
 890                  } else {
 891                      $methods = array();
 892                  }
 893  
 894                  $handler['methods'] = array();
 895  
 896                  foreach ( $methods as $method ) {
 897                      $method                        = strtoupper( trim( $method ) );
 898                      $handler['methods'][ $method ] = true;
 899                  }
 900              }
 901          }
 902  
 903          return $endpoints;
 904      }
 905  
 906      /**
 907       * Retrieves namespaces registered on the server.
 908       *
 909       * @since 4.4.0
 910       *
 911       * @return string[] List of registered namespaces.
 912       */
 913  	public function get_namespaces() {
 914          return array_keys( $this->namespaces );
 915      }
 916  
 917      /**
 918       * Retrieves specified options for a route.
 919       *
 920       * @since 4.4.0
 921       *
 922       * @param string $route Route pattern to fetch options for.
 923       * @return array|null Data as an associative array if found, or null if not found.
 924       */
 925  	public function get_route_options( $route ) {
 926          if ( ! isset( $this->route_options[ $route ] ) ) {
 927              return null;
 928          }
 929  
 930          return $this->route_options[ $route ];
 931      }
 932  
 933      /**
 934       * Matches the request to a callback and call it.
 935       *
 936       * @since 4.4.0
 937       *
 938       * @param WP_REST_Request $request Request to attempt dispatching.
 939       * @return WP_REST_Response Response returned by the callback.
 940       */
 941  	public function dispatch( $request ) {
 942          /**
 943           * Filters the pre-calculated result of a REST API dispatch request.
 944           *
 945           * Allow hijacking the request before dispatching by returning a non-empty. The returned value
 946           * will be used to serve the request instead.
 947           *
 948           * @since 4.4.0
 949           *
 950           * @param mixed           $result  Response to replace the requested version with. Can be anything
 951           *                                 a normal endpoint can return, or null to not hijack the request.
 952           * @param WP_REST_Server  $server  Server instance.
 953           * @param WP_REST_Request $request Request used to generate the response.
 954           */
 955          $result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
 956  
 957          if ( ! empty( $result ) ) {
 958              return $result;
 959          }
 960  
 961          $error   = null;
 962          $matched = $this->match_request_to_handler( $request );
 963  
 964          if ( is_wp_error( $matched ) ) {
 965              return $this->error_to_response( $matched );
 966          }
 967  
 968          list( $route, $handler ) = $matched;
 969  
 970          if ( ! is_callable( $handler['callback'] ) ) {
 971              $error = new WP_Error(
 972                  'rest_invalid_handler',
 973                  __( 'The handler for the route is invalid.' ),
 974                  array( 'status' => 500 )
 975              );
 976          }
 977  
 978          if ( ! is_wp_error( $error ) ) {
 979              $check_required = $request->has_valid_params();
 980              if ( is_wp_error( $check_required ) ) {
 981                  $error = $check_required;
 982              } else {
 983                  $check_sanitized = $request->sanitize_params();
 984                  if ( is_wp_error( $check_sanitized ) ) {
 985                      $error = $check_sanitized;
 986                  }
 987              }
 988          }
 989  
 990          return $this->respond_to_request( $request, $route, $handler, $error );
 991      }
 992  
 993      /**
 994       * Matches a request object to its handler.
 995       *
 996       * @access private
 997       * @since 5.6.0
 998       *
 999       * @param WP_REST_Request $request The request object.
1000       * @return array|WP_Error The route and request handler on success or a WP_Error instance if no handler was found.
1001       */
1002  	protected function match_request_to_handler( $request ) {
1003          $method = $request->get_method();
1004          $path   = $request->get_route();
1005  
1006          $with_namespace = array();
1007  
1008          foreach ( $this->get_namespaces() as $namespace ) {
1009              if ( 0 === strpos( trailingslashit( ltrim( $path, '/' ) ), $namespace ) ) {
1010                  $with_namespace[] = $this->get_routes( $namespace );
1011              }
1012          }
1013  
1014          if ( $with_namespace ) {
1015              $routes = array_merge( ...$with_namespace );
1016          } else {
1017              $routes = $this->get_routes();
1018          }
1019  
1020          foreach ( $routes as $route => $handlers ) {
1021              $match = preg_match( '@^' . $route . '$@i', $path, $matches );
1022  
1023              if ( ! $match ) {
1024                  continue;
1025              }
1026  
1027              $args = array();
1028  
1029              foreach ( $matches as $param => $value ) {
1030                  if ( ! is_int( $param ) ) {
1031                      $args[ $param ] = $value;
1032                  }
1033              }
1034  
1035              foreach ( $handlers as $handler ) {
1036                  $callback = $handler['callback'];
1037                  $response = null;
1038  
1039                  // Fallback to GET method if no HEAD method is registered.
1040                  $checked_method = $method;
1041                  if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) {
1042                      $checked_method = 'GET';
1043                  }
1044                  if ( empty( $handler['methods'][ $checked_method ] ) ) {
1045                      continue;
1046                  }
1047  
1048                  if ( ! is_callable( $callback ) ) {
1049                      return array( $route, $handler );
1050                  }
1051  
1052                  $request->set_url_params( $args );
1053                  $request->set_attributes( $handler );
1054  
1055                  $defaults = array();
1056  
1057                  foreach ( $handler['args'] as $arg => $options ) {
1058                      if ( isset( $options['default'] ) ) {
1059                          $defaults[ $arg ] = $options['default'];
1060                      }
1061                  }
1062  
1063                  $request->set_default_params( $defaults );
1064  
1065                  return array( $route, $handler );
1066              }
1067          }
1068  
1069          return new WP_Error(
1070              'rest_no_route',
1071              __( 'No route was found matching the URL and request method.' ),
1072              array( 'status' => 404 )
1073          );
1074      }
1075  
1076      /**
1077       * Dispatches the request to the callback handler.
1078       *
1079       * @access private
1080       * @since 5.6.0
1081       *
1082       * @param WP_REST_Request $request  The request object.
1083       * @param string          $route    The matched route regex.
1084       * @param array           $handler  The matched route handler.
1085       * @param WP_Error|null   $response The current error object if any.
1086       * @return WP_REST_Response
1087       */
1088  	protected function respond_to_request( $request, $route, $handler, $response ) {
1089          /**
1090           * Filters the response before executing any REST API callbacks.
1091           *
1092           * Allows plugins to perform additional validation after a
1093           * request is initialized and matched to a registered route,
1094           * but before it is executed.
1095           *
1096           * Note that this filter will not be called for requests that
1097           * fail to authenticate or match to a registered route.
1098           *
1099           * @since 4.7.0
1100           *
1101           * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
1102           *                                                                   Usually a WP_REST_Response or WP_Error.
1103           * @param array                                            $handler  Route handler used for the request.
1104           * @param WP_REST_Request                                  $request  Request used to generate the response.
1105           */
1106          $response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
1107  
1108          // Check permission specified on the route.
1109          if ( ! is_wp_error( $response ) && ! empty( $handler['permission_callback'] ) ) {
1110              $permission = call_user_func( $handler['permission_callback'], $request );
1111  
1112              if ( is_wp_error( $permission ) ) {
1113                  $response = $permission;
1114              } elseif ( false === $permission || null === $permission ) {
1115                  $response = new WP_Error(
1116                      'rest_forbidden',
1117                      __( 'Sorry, you are not allowed to do that.' ),
1118                      array( 'status' => rest_authorization_required_code() )
1119                  );
1120              }
1121          }
1122  
1123          if ( ! is_wp_error( $response ) ) {
1124              /**
1125               * Filters the REST API dispatch request result.
1126               *
1127               * Allow plugins to override dispatching the request.
1128               *
1129               * @since 4.4.0
1130               * @since 4.5.0 Added `$route` and `$handler` parameters.
1131               *
1132               * @param mixed           $dispatch_result Dispatch result, will be used if not empty.
1133               * @param WP_REST_Request $request         Request used to generate the response.
1134               * @param string          $route           Route matched for the request.
1135               * @param array           $handler         Route handler used for the request.
1136               */
1137              $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
1138  
1139              // Allow plugins to halt the request via this filter.
1140              if ( null !== $dispatch_result ) {
1141                  $response = $dispatch_result;
1142              } else {
1143                  $response = call_user_func( $handler['callback'], $request );
1144              }
1145          }
1146  
1147          /**
1148           * Filters the response immediately after executing any REST API
1149           * callbacks.
1150           *
1151           * Allows plugins to perform any needed cleanup, for example,
1152           * to undo changes made during the {@see 'rest_request_before_callbacks'}
1153           * filter.
1154           *
1155           * Note that this filter will not be called for requests that
1156           * fail to authenticate or match to a registered route.
1157           *
1158           * Note that an endpoint's `permission_callback` can still be
1159           * called after this filter - see `rest_send_allow_header()`.
1160           *
1161           * @since 4.7.0
1162           *
1163           * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
1164           *                                                                   Usually a WP_REST_Response or WP_Error.
1165           * @param array                                            $handler  Route handler used for the request.
1166           * @param WP_REST_Request                                  $request  Request used to generate the response.
1167           */
1168          $response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );
1169  
1170          if ( is_wp_error( $response ) ) {
1171              $response = $this->error_to_response( $response );
1172          } else {
1173              $response = rest_ensure_response( $response );
1174          }
1175  
1176          $response->set_matched_route( $route );
1177          $response->set_matched_handler( $handler );
1178  
1179          return $response;
1180      }
1181  
1182      /**
1183       * Returns if an error occurred during most recent JSON encode/decode.
1184       *
1185       * Strings to be translated will be in format like
1186       * "Encoding error: Maximum stack depth exceeded".
1187       *
1188       * @since 4.4.0
1189       *
1190       * @return false|string Boolean false or string error message.
1191       */
1192  	protected function get_json_last_error() {
1193          $last_error_code = json_last_error();
1194  
1195          if ( JSON_ERROR_NONE === $last_error_code || empty( $last_error_code ) ) {
1196              return false;
1197          }
1198  
1199          return json_last_error_msg();
1200      }
1201  
1202      /**
1203       * Retrieves the site index.
1204       *
1205       * This endpoint describes the capabilities of the site.
1206       *
1207       * @since 4.4.0
1208       *
1209       * @param array $request {
1210       *     Request.
1211       *
1212       *     @type string $context Context.
1213       * }
1214       * @return WP_REST_Response The API root index data.
1215       */
1216  	public function get_index( $request ) {
1217          // General site data.
1218          $available = array(
1219              'name'            => get_option( 'blogname' ),
1220              'description'     => get_option( 'blogdescription' ),
1221              'url'             => get_option( 'siteurl' ),
1222              'home'            => home_url(),
1223              'gmt_offset'      => get_option( 'gmt_offset' ),
1224              'timezone_string' => get_option( 'timezone_string' ),
1225              'namespaces'      => array_keys( $this->namespaces ),
1226              'authentication'  => array(),
1227              'routes'          => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
1228          );
1229  
1230          $response = new WP_REST_Response( $available );
1231          $response->add_link( 'help', 'https://developer.wordpress.org/rest-api/' );
1232          $this->add_active_theme_link_to_index( $response );
1233          $this->add_site_logo_to_index( $response );
1234          $this->add_site_icon_to_index( $response );
1235  
1236          /**
1237           * Filters the REST API root index data.
1238           *
1239           * This contains the data describing the API. This includes information
1240           * about supported authentication schemes, supported namespaces, routes
1241           * available on the API, and a small amount of data about the site.
1242           *
1243           * @since 4.4.0
1244           * @since 6.0.0 Added `$request` parameter.
1245           *
1246           * @param WP_REST_Response $response Response data.
1247           * @param WP_REST_Request  $request  Request data.
1248           */
1249          return apply_filters( 'rest_index', $response, $request );
1250      }
1251  
1252      /**
1253       * Adds a link to the active theme for users who have proper permissions.
1254       *
1255       * @since 5.7.0
1256       *
1257       * @param WP_REST_Response $response REST API response.
1258       */
1259  	protected function add_active_theme_link_to_index( WP_REST_Response $response ) {
1260          $should_add = current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' );
1261  
1262          if ( ! $should_add && current_user_can( 'edit_posts' ) ) {
1263              $should_add = true;
1264          }
1265  
1266          if ( ! $should_add ) {
1267              foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
1268                  if ( current_user_can( $post_type->cap->edit_posts ) ) {
1269                      $should_add = true;
1270                      break;
1271                  }
1272              }
1273          }
1274  
1275          if ( $should_add ) {
1276              $theme = wp_get_theme();
1277              $response->add_link( 'https://api.w.org/active-theme', rest_url( 'wp/v2/themes/' . $theme->get_stylesheet() ) );
1278          }
1279      }
1280  
1281      /**
1282       * Exposes the site logo through the WordPress REST API.
1283       *
1284       * This is used for fetching this information when user has no rights
1285       * to update settings.
1286       *
1287       * @since 5.8.0
1288       *
1289       * @param WP_REST_Response $response REST API response.
1290       */
1291  	protected function add_site_logo_to_index( WP_REST_Response $response ) {
1292          $site_logo_id = get_theme_mod( 'custom_logo', 0 );
1293  
1294          $this->add_image_to_index( $response, $site_logo_id, 'site_logo' );
1295      }
1296  
1297      /**
1298       * Exposes the site icon through the WordPress REST API.
1299       *
1300       * This is used for fetching this information when user has no rights
1301       * to update settings.
1302       *
1303       * @since 5.9.0
1304       *
1305       * @param WP_REST_Response $response REST API response.
1306       */
1307  	protected function add_site_icon_to_index( WP_REST_Response $response ) {
1308          $site_icon_id = get_option( 'site_icon', 0 );
1309  
1310          $this->add_image_to_index( $response, $site_icon_id, 'site_icon' );
1311      }
1312  
1313      /**
1314       * Exposes an image through the WordPress REST API.
1315       * This is used for fetching this information when user has no rights
1316       * to update settings.
1317       *
1318       * @since 5.9.0
1319       *
1320       * @param WP_REST_Response $response REST API response.
1321       * @param int              $image_id Image attachment ID.
1322       * @param string           $type     Type of Image.
1323       */
1324  	protected function add_image_to_index( WP_REST_Response $response, $image_id, $type ) {
1325          $response->data[ $type ] = (int) $image_id;
1326          if ( $image_id ) {
1327              $response->add_link(
1328                  'https://api.w.org/featuredmedia',
1329                  rest_url( rest_get_route_for_post( $image_id ) ),
1330                  array(
1331                      'embeddable' => true,
1332                      'type'       => $type,
1333                  )
1334              );
1335          }
1336      }
1337  
1338      /**
1339       * Retrieves the index for a namespace.
1340       *
1341       * @since 4.4.0
1342       *
1343       * @param WP_REST_Request $request REST request instance.
1344       * @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
1345       *                                   WP_Error if the namespace isn't set.
1346       */
1347  	public function get_namespace_index( $request ) {
1348          $namespace = $request['namespace'];
1349  
1350          if ( ! isset( $this->namespaces[ $namespace ] ) ) {
1351              return new WP_Error(
1352                  'rest_invalid_namespace',
1353                  __( 'The specified namespace could not be found.' ),
1354                  array( 'status' => 404 )
1355              );
1356          }
1357  
1358          $routes    = $this->namespaces[ $namespace ];
1359          $endpoints = array_intersect_key( $this->get_routes(), $routes );
1360  
1361          $data     = array(
1362              'namespace' => $namespace,
1363              'routes'    => $this->get_data_for_routes( $endpoints, $request['context'] ),
1364          );
1365          $response = rest_ensure_response( $data );
1366  
1367          // Link to the root index.
1368          $response->add_link( 'up', rest_url( '/' ) );
1369  
1370          /**
1371           * Filters the REST API namespace index data.
1372           *
1373           * This typically is just the route data for the namespace, but you can
1374           * add any data you'd like here.
1375           *
1376           * @since 4.4.0
1377           *
1378           * @param WP_REST_Response $response Response data.
1379           * @param WP_REST_Request  $request  Request data. The namespace is passed as the 'namespace' parameter.
1380           */
1381          return apply_filters( 'rest_namespace_index', $response, $request );
1382      }
1383  
1384      /**
1385       * Retrieves the publicly-visible data for routes.
1386       *
1387       * @since 4.4.0
1388       *
1389       * @param array  $routes  Routes to get data for.
1390       * @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
1391       * @return array[] Route data to expose in indexes, keyed by route.
1392       */
1393  	public function get_data_for_routes( $routes, $context = 'view' ) {
1394          $available = array();
1395  
1396          // Find the available routes.
1397          foreach ( $routes as $route => $callbacks ) {
1398              $data = $this->get_data_for_route( $route, $callbacks, $context );
1399              if ( empty( $data ) ) {
1400                  continue;
1401              }
1402  
1403              /**
1404               * Filters the publicly-visible data for a single REST API route.
1405               *
1406               * @since 4.4.0
1407               *
1408               * @param array $data Publicly-visible data for the route.
1409               */
1410              $available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
1411          }
1412  
1413          /**
1414           * Filters the publicly-visible data for REST API routes.
1415           *
1416           * This data is exposed on indexes and can be used by clients or
1417           * developers to investigate the site and find out how to use it. It
1418           * acts as a form of self-documentation.
1419           *
1420           * @since 4.4.0
1421           *
1422           * @param array[] $available Route data to expose in indexes, keyed by route.
1423           * @param array   $routes    Internal route data as an associative array.
1424           */
1425          return apply_filters( 'rest_route_data', $available, $routes );
1426      }
1427  
1428      /**
1429       * Retrieves publicly-visible data for the route.
1430       *
1431       * @since 4.4.0
1432       *
1433       * @param string $route     Route to get data for.
1434       * @param array  $callbacks Callbacks to convert to data.
1435       * @param string $context   Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
1436       * @return array|null Data for the route, or null if no publicly-visible data.
1437       */
1438  	public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
1439          $data = array(
1440              'namespace' => '',
1441              'methods'   => array(),
1442              'endpoints' => array(),
1443          );
1444  
1445          $allow_batch = false;
1446  
1447          if ( isset( $this->route_options[ $route ] ) ) {
1448              $options = $this->route_options[ $route ];
1449  
1450              if ( isset( $options['namespace'] ) ) {
1451                  $data['namespace'] = $options['namespace'];
1452              }
1453  
1454              $allow_batch = isset( $options['allow_batch'] ) ? $options['allow_batch'] : false;
1455  
1456              if ( isset( $options['schema'] ) && 'help' === $context ) {
1457                  $data['schema'] = call_user_func( $options['schema'] );
1458              }
1459          }
1460  
1461          $allowed_schema_keywords = array_flip( rest_get_allowed_schema_keywords() );
1462  
1463          $route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
1464  
1465          foreach ( $callbacks as $callback ) {
1466              // Skip to the next route if any callback is hidden.
1467              if ( empty( $callback['show_in_index'] ) ) {
1468                  continue;
1469              }
1470  
1471              $data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
1472              $endpoint_data   = array(
1473                  'methods' => array_keys( $callback['methods'] ),
1474              );
1475  
1476              $callback_batch = isset( $callback['allow_batch'] ) ? $callback['allow_batch'] : $allow_batch;
1477  
1478              if ( $callback_batch ) {
1479                  $endpoint_data['allow_batch'] = $callback_batch;
1480              }
1481  
1482              if ( isset( $callback['args'] ) ) {
1483                  $endpoint_data['args'] = array();
1484  
1485                  foreach ( $callback['args'] as $key => $opts ) {
1486                      $arg_data             = array_intersect_key( $opts, $allowed_schema_keywords );
1487                      $arg_data['required'] = ! empty( $opts['required'] );
1488  
1489                      $endpoint_data['args'][ $key ] = $arg_data;
1490                  }
1491              }
1492  
1493              $data['endpoints'][] = $endpoint_data;
1494  
1495              // For non-variable routes, generate links.
1496              if ( strpos( $route, '{' ) === false ) {
1497                  $data['_links'] = array(
1498                      'self' => array(
1499                          array(
1500                              'href' => rest_url( $route ),
1501                          ),
1502                      ),
1503                  );
1504              }
1505          }
1506  
1507          if ( empty( $data['methods'] ) ) {
1508              // No methods supported, hide the route.
1509              return null;
1510          }
1511  
1512          return $data;
1513      }
1514  
1515      /**
1516       * Gets the maximum number of requests that can be included in a batch.
1517       *
1518       * @since 5.6.0
1519       *
1520       * @return int The maximum requests.
1521       */
1522  	protected function get_max_batch_size() {
1523          /**
1524           * Filters the maximum number of REST API requests that can be included in a batch.
1525           *
1526           * @since 5.6.0
1527           *
1528           * @param int $max_size The maximum size.
1529           */
1530          return apply_filters( 'rest_get_max_batch_size', 25 );
1531      }
1532  
1533      /**
1534       * Serves the batch/v1 request.
1535       *
1536       * @since 5.6.0
1537       *
1538       * @param WP_REST_Request $batch_request The batch request object.
1539       * @return WP_REST_Response The generated response object.
1540       */
1541  	public function serve_batch_request_v1( WP_REST_Request $batch_request ) {
1542          $requests = array();
1543  
1544          foreach ( $batch_request['requests'] as $args ) {
1545              $parsed_url = wp_parse_url( $args['path'] );
1546  
1547              if ( false === $parsed_url ) {
1548                  $requests[] = new WP_Error( 'parse_path_failed', __( 'Could not parse the path.' ), array( 'status' => 400 ) );
1549  
1550                  continue;
1551              }
1552  
1553              $single_request = new WP_REST_Request( isset( $args['method'] ) ? $args['method'] : 'POST', $parsed_url['path'] );
1554  
1555              if ( ! empty( $parsed_url['query'] ) ) {
1556                  $query_args = null; // Satisfy linter.
1557                  wp_parse_str( $parsed_url['query'], $query_args );
1558                  $single_request->set_query_params( $query_args );
1559              }
1560  
1561              if ( ! empty( $args['body'] ) ) {
1562                  $single_request->set_body_params( $args['body'] );
1563              }
1564  
1565              if ( ! empty( $args['headers'] ) ) {
1566                  $single_request->set_headers( $args['headers'] );
1567              }
1568  
1569              $requests[] = $single_request;
1570          }
1571  
1572          $matches    = array();
1573          $validation = array();
1574          $has_error  = false;
1575  
1576          foreach ( $requests as $single_request ) {
1577              $match     = $this->match_request_to_handler( $single_request );
1578              $matches[] = $match;
1579              $error     = null;
1580  
1581              if ( is_wp_error( $match ) ) {
1582                  $error = $match;
1583              }
1584  
1585              if ( ! $error ) {
1586                  list( $route, $handler ) = $match;
1587  
1588                  if ( isset( $handler['allow_batch'] ) ) {
1589                      $allow_batch = $handler['allow_batch'];
1590                  } else {
1591                      $route_options = $this->get_route_options( $route );
1592                      $allow_batch   = isset( $route_options['allow_batch'] ) ? $route_options['allow_batch'] : false;
1593                  }
1594  
1595                  if ( ! is_array( $allow_batch ) || empty( $allow_batch['v1'] ) ) {
1596                      $error = new WP_Error(
1597                          'rest_batch_not_allowed',
1598                          __( 'The requested route does not support batch requests.' ),
1599                          array( 'status' => 400 )
1600                      );
1601                  }
1602              }
1603  
1604              if ( ! $error ) {
1605                  $check_required = $single_request->has_valid_params();
1606                  if ( is_wp_error( $check_required ) ) {
1607                      $error = $check_required;
1608                  }
1609              }
1610  
1611              if ( ! $error ) {
1612                  $check_sanitized = $single_request->sanitize_params();
1613                  if ( is_wp_error( $check_sanitized ) ) {
1614                      $error = $check_sanitized;
1615                  }
1616              }
1617  
1618              if ( $error ) {
1619                  $has_error    = true;
1620                  $validation[] = $error;
1621              } else {
1622                  $validation[] = true;
1623              }
1624          }
1625  
1626          $responses = array();
1627  
1628          if ( $has_error && 'require-all-validate' === $batch_request['validation'] ) {
1629              foreach ( $validation as $valid ) {
1630                  if ( is_wp_error( $valid ) ) {
1631                      $responses[] = $this->envelope_response( $this->error_to_response( $valid ), false )->get_data();
1632                  } else {
1633                      $responses[] = null;
1634                  }
1635              }
1636  
1637              return new WP_REST_Response(
1638                  array(
1639                      'failed'    => 'validation',
1640                      'responses' => $responses,
1641                  ),
1642                  WP_Http::MULTI_STATUS
1643              );
1644          }
1645  
1646          foreach ( $requests as $i => $single_request ) {
1647              $clean_request = clone $single_request;
1648              $clean_request->set_url_params( array() );
1649              $clean_request->set_attributes( array() );
1650              $clean_request->set_default_params( array() );
1651  
1652              /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
1653              $result = apply_filters( 'rest_pre_dispatch', null, $this, $clean_request );
1654  
1655              if ( empty( $result ) ) {
1656                  $match = $matches[ $i ];
1657                  $error = null;
1658  
1659                  if ( is_wp_error( $validation[ $i ] ) ) {
1660                      $error = $validation[ $i ];
1661                  }
1662  
1663                  if ( is_wp_error( $match ) ) {
1664                      $result = $this->error_to_response( $match );
1665                  } else {
1666                      list( $route, $handler ) = $match;
1667  
1668                      if ( ! $error && ! is_callable( $handler['callback'] ) ) {
1669                          $error = new WP_Error(
1670                              'rest_invalid_handler',
1671                              __( 'The handler for the route is invalid' ),
1672                              array( 'status' => 500 )
1673                          );
1674                      }
1675  
1676                      $result = $this->respond_to_request( $single_request, $route, $handler, $error );
1677                  }
1678              }
1679  
1680              /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
1681              $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $single_request );
1682  
1683              $responses[] = $this->envelope_response( $result, false )->get_data();
1684          }
1685  
1686          return new WP_REST_Response( array( 'responses' => $responses ), WP_Http::MULTI_STATUS );
1687      }
1688  
1689      /**
1690       * Sends an HTTP status code.
1691       *
1692       * @since 4.4.0
1693       *
1694       * @param int $code HTTP status.
1695       */
1696  	protected function set_status( $code ) {
1697          status_header( $code );
1698      }
1699  
1700      /**
1701       * Sends an HTTP header.
1702       *
1703       * @since 4.4.0
1704       *
1705       * @param string $key Header key.
1706       * @param string $value Header value.
1707       */
1708  	public function send_header( $key, $value ) {
1709          /*
1710           * Sanitize as per RFC2616 (Section 4.2):
1711           *
1712           * Any LWS that occurs between field-content MAY be replaced with a
1713           * single SP before interpreting the field value or forwarding the
1714           * message downstream.
1715           */
1716          $value = preg_replace( '/\s+/', ' ', $value );
1717          header( sprintf( '%s: %s', $key, $value ) );
1718      }
1719  
1720      /**
1721       * Sends multiple HTTP headers.
1722       *
1723       * @since 4.4.0
1724       *
1725       * @param array $headers Map of header name to header value.
1726       */
1727  	public function send_headers( $headers ) {
1728          foreach ( $headers as $key => $value ) {
1729              $this->send_header( $key, $value );
1730          }
1731      }
1732  
1733      /**
1734       * Removes an HTTP header from the current response.
1735       *
1736       * @since 4.8.0
1737       *
1738       * @param string $key Header key.
1739       */
1740  	public function remove_header( $key ) {
1741          header_remove( $key );
1742      }
1743  
1744      /**
1745       * Retrieves the raw request entity (body).
1746       *
1747       * @since 4.4.0
1748       *
1749       * @global string $HTTP_RAW_POST_DATA Raw post data.
1750       *
1751       * @return string Raw request data.
1752       */
1753  	public static function get_raw_data() {
1754          // phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved
1755          global $HTTP_RAW_POST_DATA;
1756  
1757          // $HTTP_RAW_POST_DATA was deprecated in PHP 5.6 and removed in PHP 7.0.
1758          if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
1759              $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
1760          }
1761  
1762          return $HTTP_RAW_POST_DATA;
1763          // phpcs:enable
1764      }
1765  
1766      /**
1767       * Extracts headers from a PHP-style $_SERVER array.
1768       *
1769       * @since 4.4.0
1770       *
1771       * @param array $server Associative array similar to `$_SERVER`.
1772       * @return array Headers extracted from the input.
1773       */
1774  	public function get_headers( $server ) {
1775          $headers = array();
1776  
1777          // CONTENT_* headers are not prefixed with HTTP_.
1778          $additional = array(
1779              'CONTENT_LENGTH' => true,
1780              'CONTENT_MD5'    => true,
1781              'CONTENT_TYPE'   => true,
1782          );
1783  
1784          foreach ( $server as $key => $value ) {
1785              if ( strpos( $key, 'HTTP_' ) === 0 ) {
1786                  $headers[ substr( $key, 5 ) ] = $value;
1787              } elseif ( 'REDIRECT_HTTP_AUTHORIZATION' === $key && empty( $server['HTTP_AUTHORIZATION'] ) ) {
1788                  /*
1789                   * In some server configurations, the authorization header is passed in this alternate location.
1790                   * Since it would not be passed in in both places we do not check for both headers and resolve.
1791                   */
1792                  $headers['AUTHORIZATION'] = $value;
1793              } elseif ( isset( $additional[ $key ] ) ) {
1794                  $headers[ $key ] = $value;
1795              }
1796          }
1797  
1798          return $headers;
1799      }
1800  }


Generated: Thu Nov 21 01:00:03 2024 Cross-referenced by PHPXref 0.7.1