[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

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

   1  <?php
   2  /**
   3   * REST API functions.
   4   *
   5   * @package WordPress
   6   * @subpackage REST_API
   7   * @since 4.4.0
   8   */
   9  
  10  /**
  11   * Version number for our API.
  12   *
  13   * @var string
  14   */
  15  define( 'REST_API_VERSION', '2.0' );
  16  
  17  /**
  18   * Registers a REST API route.
  19   *
  20   * Note: Do not use before the {@see 'rest_api_init'} hook.
  21   *
  22   * @since 4.4.0
  23   * @since 5.1.0 Added a _doing_it_wrong() notice when not called on or after the rest_api_init hook.
  24   *
  25   * @param string $namespace The first URL segment after core prefix. Should be unique to your package/plugin.
  26   * @param string $route     The base URL for route you are adding.
  27   * @param array  $args      Optional. Either an array of options for the endpoint, or an array of arrays for
  28   *                          multiple methods. Default empty array.
  29   * @param bool   $override  Optional. If the route already exists, should we override it? True overrides,
  30   *                          false merges (with newer overriding if duplicate keys exist). Default false.
  31   * @return bool True on success, false on error.
  32   */
  33  function register_rest_route( $namespace, $route, $args = array(), $override = false ) {
  34      if ( empty( $namespace ) ) {
  35          /*
  36           * Non-namespaced routes are not allowed, with the exception of the main
  37           * and namespace indexes. If you really need to register a
  38           * non-namespaced route, call `WP_REST_Server::register_route` directly.
  39           */
  40          _doing_it_wrong( 'register_rest_route', __( 'Routes must be namespaced with plugin or theme name and version.' ), '4.4.0' );
  41          return false;
  42      } elseif ( empty( $route ) ) {
  43          _doing_it_wrong( 'register_rest_route', __( 'Route must be specified.' ), '4.4.0' );
  44          return false;
  45      }
  46  
  47      $clean_namespace = trim( $namespace, '/' );
  48  
  49      if ( $clean_namespace !== $namespace ) {
  50          _doing_it_wrong( __FUNCTION__, __( 'Namespace must not start or end with a slash.' ), '5.4.2' );
  51      }
  52  
  53      if ( ! did_action( 'rest_api_init' ) ) {
  54          _doing_it_wrong(
  55              'register_rest_route',
  56              sprintf(
  57                  /* translators: %s: rest_api_init */
  58                  __( 'REST API routes must be registered on the %s action.' ),
  59                  '<code>rest_api_init</code>'
  60              ),
  61              '5.1.0'
  62          );
  63      }
  64  
  65      if ( isset( $args['args'] ) ) {
  66          $common_args = $args['args'];
  67          unset( $args['args'] );
  68      } else {
  69          $common_args = array();
  70      }
  71  
  72      if ( isset( $args['callback'] ) ) {
  73          // Upgrade a single set to multiple.
  74          $args = array( $args );
  75      }
  76  
  77      $defaults = array(
  78          'methods'  => 'GET',
  79          'callback' => null,
  80          'args'     => array(),
  81      );
  82  
  83      foreach ( $args as $key => &$arg_group ) {
  84          if ( ! is_numeric( $key ) ) {
  85              // Route option, skip here.
  86              continue;
  87          }
  88  
  89          $arg_group         = array_merge( $defaults, $arg_group );
  90          $arg_group['args'] = array_merge( $common_args, $arg_group['args'] );
  91  
  92          if ( ! isset( $arg_group['permission_callback'] ) ) {
  93              _doing_it_wrong(
  94                  __FUNCTION__,
  95                  sprintf(
  96                      /* translators: 1: The REST API route being registered, 2: The argument name, 3: The suggested function name. */
  97                      __( 'The REST API route definition for %1$s is missing the required %2$s argument. For REST API routes that are intended to be public, use %3$s as the permission callback.' ),
  98                      '<code>' . $clean_namespace . '/' . trim( $route, '/' ) . '</code>',
  99                      '<code>permission_callback</code>',
 100                      '<code>__return_true</code>'
 101                  ),
 102                  '5.5.0'
 103              );
 104          }
 105      }
 106  
 107      $full_route = '/' . $clean_namespace . '/' . trim( $route, '/' );
 108      rest_get_server()->register_route( $clean_namespace, $full_route, $args, $override );
 109      return true;
 110  }
 111  
 112  /**
 113   * Registers a new field on an existing WordPress object type.
 114   *
 115   * @since 4.7.0
 116   *
 117   * @global array $wp_rest_additional_fields Holds registered fields, organized
 118   *                                          by object type.
 119   *
 120   * @param string|array $object_type Object(s) the field is being registered
 121   *                                  to, "post"|"term"|"comment" etc.
 122   * @param string       $attribute   The attribute name.
 123   * @param array        $args {
 124   *     Optional. An array of arguments used to handle the registered field.
 125   *
 126   *     @type callable|null $get_callback    Optional. The callback function used to retrieve the field value. Default is
 127   *                                          'null', the field will not be returned in the response. The function will
 128   *                                          be passed the prepared object data.
 129   *     @type callable|null $update_callback Optional. The callback function used to set and update the field value. Default
 130   *                                          is 'null', the value cannot be set or updated. The function will be passed
 131   *                                          the model object, like WP_Post.
 132   *     @type array|null $schema             Optional. The callback function used to create the schema for this field.
 133   *                                          Default is 'null', no schema entry will be returned.
 134   * }
 135   */
 136  function register_rest_field( $object_type, $attribute, $args = array() ) {
 137      $defaults = array(
 138          'get_callback'    => null,
 139          'update_callback' => null,
 140          'schema'          => null,
 141      );
 142  
 143      $args = wp_parse_args( $args, $defaults );
 144  
 145      global $wp_rest_additional_fields;
 146  
 147      $object_types = (array) $object_type;
 148  
 149      foreach ( $object_types as $object_type ) {
 150          $wp_rest_additional_fields[ $object_type ][ $attribute ] = $args;
 151      }
 152  }
 153  
 154  /**
 155   * Registers rewrite rules for the API.
 156   *
 157   * @since 4.4.0
 158   *
 159   * @see rest_api_register_rewrites()
 160   * @global WP $wp Current WordPress environment instance.
 161   */
 162  function rest_api_init() {
 163      rest_api_register_rewrites();
 164  
 165      global $wp;
 166      $wp->add_query_var( 'rest_route' );
 167  }
 168  
 169  /**
 170   * Adds REST rewrite rules.
 171   *
 172   * @since 4.4.0
 173   *
 174   * @see add_rewrite_rule()
 175   * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
 176   */
 177  function rest_api_register_rewrites() {
 178      global $wp_rewrite;
 179  
 180      add_rewrite_rule( '^' . rest_get_url_prefix() . '/?$', 'index.php?rest_route=/', 'top' );
 181      add_rewrite_rule( '^' . rest_get_url_prefix() . '/(.*)?', 'index.php?rest_route=/$matches[1]', 'top' );
 182      add_rewrite_rule( '^' . $wp_rewrite->index . '/' . rest_get_url_prefix() . '/?$', 'index.php?rest_route=/', 'top' );
 183      add_rewrite_rule( '^' . $wp_rewrite->index . '/' . rest_get_url_prefix() . '/(.*)?', 'index.php?rest_route=/$matches[1]', 'top' );
 184  }
 185  
 186  /**
 187   * Registers the default REST API filters.
 188   *
 189   * Attached to the {@see 'rest_api_init'} action
 190   * to make testing and disabling these filters easier.
 191   *
 192   * @since 4.4.0
 193   */
 194  function rest_api_default_filters() {
 195      if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
 196          // Deprecated reporting.
 197          add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 );
 198          add_filter( 'deprecated_function_trigger_error', '__return_false' );
 199          add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 );
 200          add_filter( 'deprecated_argument_trigger_error', '__return_false' );
 201          add_action( 'doing_it_wrong_run', 'rest_handle_doing_it_wrong', 10, 3 );
 202          add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
 203      }
 204  
 205      // Default serving.
 206      add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
 207      add_filter( 'rest_post_dispatch', 'rest_send_allow_header', 10, 3 );
 208      add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 );
 209  
 210      add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 );
 211  }
 212  
 213  /**
 214   * Registers default REST API routes.
 215   *
 216   * @since 4.7.0
 217   */
 218  function create_initial_rest_routes() {
 219      foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
 220          $controller = $post_type->get_rest_controller();
 221  
 222          if ( ! $controller ) {
 223              continue;
 224          }
 225  
 226          $controller->register_routes();
 227  
 228          if ( post_type_supports( $post_type->name, 'revisions' ) ) {
 229              $revisions_controller = new WP_REST_Revisions_Controller( $post_type->name );
 230              $revisions_controller->register_routes();
 231          }
 232  
 233          if ( 'attachment' !== $post_type->name ) {
 234              $autosaves_controller = new WP_REST_Autosaves_Controller( $post_type->name );
 235              $autosaves_controller->register_routes();
 236          }
 237      }
 238  
 239      // Post types.
 240      $controller = new WP_REST_Post_Types_Controller;
 241      $controller->register_routes();
 242  
 243      // Post statuses.
 244      $controller = new WP_REST_Post_Statuses_Controller;
 245      $controller->register_routes();
 246  
 247      // Taxonomies.
 248      $controller = new WP_REST_Taxonomies_Controller;
 249      $controller->register_routes();
 250  
 251      // Terms.
 252      foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
 253          $controller = $taxonomy->get_rest_controller();
 254  
 255          if ( ! $controller ) {
 256              continue;
 257          }
 258  
 259          $controller->register_routes();
 260      }
 261  
 262      // Users.
 263      $controller = new WP_REST_Users_Controller;
 264      $controller->register_routes();
 265  
 266      // Comments.
 267      $controller = new WP_REST_Comments_Controller;
 268      $controller->register_routes();
 269  
 270      /**
 271       * Filters the search handlers to use in the REST search controller.
 272       *
 273       * @since 5.0.0
 274       *
 275       * @param array $search_handlers List of search handlers to use in the controller. Each search
 276       *                               handler instance must extend the `WP_REST_Search_Handler` class.
 277       *                               Default is only a handler for posts.
 278       */
 279      $search_handlers = apply_filters( 'wp_rest_search_handlers', array( new WP_REST_Post_Search_Handler() ) );
 280  
 281      $controller = new WP_REST_Search_Controller( $search_handlers );
 282      $controller->register_routes();
 283  
 284      // Block Renderer.
 285      $controller = new WP_REST_Block_Renderer_Controller;
 286      $controller->register_routes();
 287  
 288      // Block Types.
 289      $controller = new WP_REST_Block_Types_Controller();
 290      $controller->register_routes();
 291  
 292      // Settings.
 293      $controller = new WP_REST_Settings_Controller;
 294      $controller->register_routes();
 295  
 296      // Themes.
 297      $controller = new WP_REST_Themes_Controller;
 298      $controller->register_routes();
 299  
 300      // Plugins.
 301      $controller = new WP_REST_Plugins_Controller();
 302      $controller->register_routes();
 303  
 304      // Block Directory.
 305      $controller = new WP_REST_Block_Directory_Controller();
 306      $controller->register_routes();
 307  
 308  }
 309  
 310  /**
 311   * Loads the REST API.
 312   *
 313   * @since 4.4.0
 314   *
 315   * @global WP $wp Current WordPress environment instance.
 316   */
 317  function rest_api_loaded() {
 318      if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
 319          return;
 320      }
 321  
 322      /**
 323       * Whether this is a REST Request.
 324       *
 325       * @since 4.4.0
 326       * @var bool
 327       */
 328      define( 'REST_REQUEST', true );
 329  
 330      // Initialize the server.
 331      $server = rest_get_server();
 332  
 333      // Fire off the request.
 334      $route = untrailingslashit( $GLOBALS['wp']->query_vars['rest_route'] );
 335      if ( empty( $route ) ) {
 336          $route = '/';
 337      }
 338      $server->serve_request( $route );
 339  
 340      // We're done.
 341      die();
 342  }
 343  
 344  /**
 345   * Retrieves the URL prefix for any API resource.
 346   *
 347   * @since 4.4.0
 348   *
 349   * @return string Prefix.
 350   */
 351  function rest_get_url_prefix() {
 352      /**
 353       * Filters the REST URL prefix.
 354       *
 355       * @since 4.4.0
 356       *
 357       * @param string $prefix URL prefix. Default 'wp-json'.
 358       */
 359      return apply_filters( 'rest_url_prefix', 'wp-json' );
 360  }
 361  
 362  /**
 363   * Retrieves the URL to a REST endpoint on a site.
 364   *
 365   * Note: The returned URL is NOT escaped.
 366   *
 367   * @since 4.4.0
 368   *
 369   * @todo Check if this is even necessary
 370   * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
 371   *
 372   * @param int    $blog_id Optional. Blog ID. Default of null returns URL for current blog.
 373   * @param string $path    Optional. REST route. Default '/'.
 374   * @param string $scheme  Optional. Sanitization scheme. Default 'rest'.
 375   * @return string Full URL to the endpoint.
 376   */
 377  function get_rest_url( $blog_id = null, $path = '/', $scheme = 'rest' ) {
 378      if ( empty( $path ) ) {
 379          $path = '/';
 380      }
 381  
 382      $path = '/' . ltrim( $path, '/' );
 383  
 384      if ( is_multisite() && get_blog_option( $blog_id, 'permalink_structure' ) || get_option( 'permalink_structure' ) ) {
 385          global $wp_rewrite;
 386  
 387          if ( $wp_rewrite->using_index_permalinks() ) {
 388              $url = get_home_url( $blog_id, $wp_rewrite->index . '/' . rest_get_url_prefix(), $scheme );
 389          } else {
 390              $url = get_home_url( $blog_id, rest_get_url_prefix(), $scheme );
 391          }
 392  
 393          $url .= $path;
 394      } else {
 395          $url = trailingslashit( get_home_url( $blog_id, '', $scheme ) );
 396          // nginx only allows HTTP/1.0 methods when redirecting from / to /index.php.
 397          // To work around this, we manually add index.php to the URL, avoiding the redirect.
 398          if ( 'index.php' !== substr( $url, 9 ) ) {
 399              $url .= 'index.php';
 400          }
 401  
 402          $url = add_query_arg( 'rest_route', $path, $url );
 403      }
 404  
 405      if ( is_ssl() && isset( $_SERVER['SERVER_NAME'] ) ) {
 406          // If the current host is the same as the REST URL host, force the REST URL scheme to HTTPS.
 407          if ( parse_url( get_home_url( $blog_id ), PHP_URL_HOST ) === $_SERVER['SERVER_NAME'] ) {
 408              $url = set_url_scheme( $url, 'https' );
 409          }
 410      }
 411  
 412      if ( is_admin() && force_ssl_admin() ) {
 413          /*
 414           * In this situation the home URL may be http:, and `is_ssl()` may be false,
 415           * but the admin is served over https: (one way or another), so REST API usage
 416           * will be blocked by browsers unless it is also served over HTTPS.
 417           */
 418          $url = set_url_scheme( $url, 'https' );
 419      }
 420  
 421      /**
 422       * Filters the REST URL.
 423       *
 424       * Use this filter to adjust the url returned by the get_rest_url() function.
 425       *
 426       * @since 4.4.0
 427       *
 428       * @param string $url     REST URL.
 429       * @param string $path    REST route.
 430       * @param int    $blog_id Blog ID.
 431       * @param string $scheme  Sanitization scheme.
 432       */
 433      return apply_filters( 'rest_url', $url, $path, $blog_id, $scheme );
 434  }
 435  
 436  /**
 437   * Retrieves the URL to a REST endpoint.
 438   *
 439   * Note: The returned URL is NOT escaped.
 440   *
 441   * @since 4.4.0
 442   *
 443   * @param string $path   Optional. REST route. Default empty.
 444   * @param string $scheme Optional. Sanitization scheme. Default 'rest'.
 445   * @return string Full URL to the endpoint.
 446   */
 447  function rest_url( $path = '', $scheme = 'rest' ) {
 448      return get_rest_url( null, $path, $scheme );
 449  }
 450  
 451  /**
 452   * Do a REST request.
 453   *
 454   * Used primarily to route internal requests through WP_REST_Server.
 455   *
 456   * @since 4.4.0
 457   *
 458   * @param WP_REST_Request|string $request Request.
 459   * @return WP_REST_Response REST response.
 460   */
 461  function rest_do_request( $request ) {
 462      $request = rest_ensure_request( $request );
 463      return rest_get_server()->dispatch( $request );
 464  }
 465  
 466  /**
 467   * Retrieves the current REST server instance.
 468   *
 469   * Instantiates a new instance if none exists already.
 470   *
 471   * @since 4.5.0
 472   *
 473   * @global WP_REST_Server $wp_rest_server REST server instance.
 474   *
 475   * @return WP_REST_Server REST server instance.
 476   */
 477  function rest_get_server() {
 478      /* @var WP_REST_Server $wp_rest_server */
 479      global $wp_rest_server;
 480  
 481      if ( empty( $wp_rest_server ) ) {
 482          /**
 483           * Filters the REST Server Class.
 484           *
 485           * This filter allows you to adjust the server class used by the API, using a
 486           * different class to handle requests.
 487           *
 488           * @since 4.4.0
 489           *
 490           * @param string $class_name The name of the server class. Default 'WP_REST_Server'.
 491           */
 492          $wp_rest_server_class = apply_filters( 'wp_rest_server_class', 'WP_REST_Server' );
 493          $wp_rest_server       = new $wp_rest_server_class;
 494  
 495          /**
 496           * Fires when preparing to serve an API request.
 497           *
 498           * Endpoint objects should be created and register their hooks on this action rather
 499           * than another action to ensure they're only loaded when needed.
 500           *
 501           * @since 4.4.0
 502           *
 503           * @param WP_REST_Server $wp_rest_server Server object.
 504           */
 505          do_action( 'rest_api_init', $wp_rest_server );
 506      }
 507  
 508      return $wp_rest_server;
 509  }
 510  
 511  /**
 512   * Ensures request arguments are a request object (for consistency).
 513   *
 514   * @since 4.4.0
 515   * @since 5.3.0 Accept string argument for the request path.
 516   *
 517   * @param array|string|WP_REST_Request $request Request to check.
 518   * @return WP_REST_Request REST request instance.
 519   */
 520  function rest_ensure_request( $request ) {
 521      if ( $request instanceof WP_REST_Request ) {
 522          return $request;
 523      }
 524  
 525      if ( is_string( $request ) ) {
 526          return new WP_REST_Request( 'GET', $request );
 527      }
 528  
 529      return new WP_REST_Request( 'GET', '', $request );
 530  }
 531  
 532  /**
 533   * Ensures a REST response is a response object (for consistency).
 534   *
 535   * This implements WP_REST_Response, allowing usage of `set_status`/`header`/etc
 536   * without needing to double-check the object. Will also allow WP_Error to indicate error
 537   * responses, so users should immediately check for this value.
 538   *
 539   * @since 4.4.0
 540   *
 541   * @param WP_REST_Response|WP_Error|WP_HTTP_Response|mixed $response Response to check.
 542   * @return WP_REST_Response|WP_Error If response generated an error, WP_Error, if response
 543   *                                   is already an instance, WP_REST_Response, otherwise
 544   *                                   returns a new WP_REST_Response instance.
 545   */
 546  function rest_ensure_response( $response ) {
 547      if ( is_wp_error( $response ) ) {
 548          return $response;
 549      }
 550  
 551      if ( $response instanceof WP_REST_Response ) {
 552          return $response;
 553      }
 554  
 555      // While WP_HTTP_Response is the base class of WP_REST_Response, it doesn't provide
 556      // all the required methods used in WP_REST_Server::dispatch().
 557      if ( $response instanceof WP_HTTP_Response ) {
 558          return new WP_REST_Response(
 559              $response->get_data(),
 560              $response->get_status(),
 561              $response->get_headers()
 562          );
 563      }
 564  
 565      return new WP_REST_Response( $response );
 566  }
 567  
 568  /**
 569   * Handles _deprecated_function() errors.
 570   *
 571   * @since 4.4.0
 572   *
 573   * @param string $function    The function that was called.
 574   * @param string $replacement The function that should have been called.
 575   * @param string $version     Version.
 576   */
 577  function rest_handle_deprecated_function( $function, $replacement, $version ) {
 578      if ( ! WP_DEBUG || headers_sent() ) {
 579          return;
 580      }
 581      if ( ! empty( $replacement ) ) {
 582          /* translators: 1: Function name, 2: WordPress version number, 3: New function name. */
 583          $string = sprintf( __( '%1$s (since %2$s; use %3$s instead)' ), $function, $version, $replacement );
 584      } else {
 585          /* translators: 1: Function name, 2: WordPress version number. */
 586          $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
 587      }
 588  
 589      header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) );
 590  }
 591  
 592  /**
 593   * Handles _deprecated_argument() errors.
 594   *
 595   * @since 4.4.0
 596   *
 597   * @param string $function    The function that was called.
 598   * @param string $message     A message regarding the change.
 599   * @param string $version     Version.
 600   */
 601  function rest_handle_deprecated_argument( $function, $message, $version ) {
 602      if ( ! WP_DEBUG || headers_sent() ) {
 603          return;
 604      }
 605      if ( $message ) {
 606          /* translators: 1: Function name, 2: WordPress version number, 3: Error message. */
 607          $string = sprintf( __( '%1$s (since %2$s; %3$s)' ), $function, $version, $message );
 608      } else {
 609          /* translators: 1: Function name, 2: WordPress version number. */
 610          $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
 611      }
 612  
 613      header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) );
 614  }
 615  
 616  /**
 617   * Handles _doing_it_wrong errors.
 618   *
 619   * @since 5.5.0
 620   *
 621   * @param string      $function The function that was called.
 622   * @param string      $message  A message explaining what has been done incorrectly.
 623   * @param string|null $version  The version of WordPress where the message was added.
 624   */
 625  function rest_handle_doing_it_wrong( $function, $message, $version ) {
 626      if ( ! WP_DEBUG || headers_sent() ) {
 627          return;
 628      }
 629  
 630      if ( $version ) {
 631          /* translators: Developer debugging message. 1: PHP function name, 2: WordPress version number, 3: Explanatory message. */
 632          $string = __( '%1$s (since %2$s; %3$s)' );
 633          $string = sprintf( $string, $function, $version, $message );
 634      } else {
 635          /* translators: Developer debugging message. 1: PHP function name, 2: Explanatory message. */
 636          $string = __( '%1$s (%2$s)' );
 637          $string = sprintf( $string, $function, $message );
 638      }
 639  
 640      header( sprintf( 'X-WP-DoingItWrong: %s', $string ) );
 641  }
 642  
 643  /**
 644   * Sends Cross-Origin Resource Sharing headers with API requests.
 645   *
 646   * @since 4.4.0
 647   *
 648   * @param mixed $value Response data.
 649   * @return mixed Response data.
 650   */
 651  function rest_send_cors_headers( $value ) {
 652      $origin = get_http_origin();
 653  
 654      if ( $origin ) {
 655          // Requests from file:// and data: URLs send "Origin: null".
 656          if ( 'null' !== $origin ) {
 657              $origin = esc_url_raw( $origin );
 658          }
 659          header( 'Access-Control-Allow-Origin: ' . $origin );
 660          header( 'Access-Control-Allow-Methods: OPTIONS, GET, POST, PUT, PATCH, DELETE' );
 661          header( 'Access-Control-Allow-Credentials: true' );
 662          header( 'Vary: Origin', false );
 663      } elseif ( ! headers_sent() && 'GET' === $_SERVER['REQUEST_METHOD'] && ! is_user_logged_in() ) {
 664          header( 'Vary: Origin', false );
 665      }
 666  
 667      return $value;
 668  }
 669  
 670  /**
 671   * Handles OPTIONS requests for the server.
 672   *
 673   * This is handled outside of the server code, as it doesn't obey normal route
 674   * mapping.
 675   *
 676   * @since 4.4.0
 677   *
 678   * @param mixed           $response Current response, either response or `null` to indicate pass-through.
 679   * @param WP_REST_Server  $handler  ResponseHandler instance (usually WP_REST_Server).
 680   * @param WP_REST_Request $request  The request that was used to make current response.
 681   * @return WP_REST_Response Modified response, either response or `null` to indicate pass-through.
 682   */
 683  function rest_handle_options_request( $response, $handler, $request ) {
 684      if ( ! empty( $response ) || $request->get_method() !== 'OPTIONS' ) {
 685          return $response;
 686      }
 687  
 688      $response = new WP_REST_Response();
 689      $data     = array();
 690  
 691      foreach ( $handler->get_routes() as $route => $endpoints ) {
 692          $match = preg_match( '@^' . $route . '$@i', $request->get_route(), $matches );
 693  
 694          if ( ! $match ) {
 695              continue;
 696          }
 697  
 698          $args = array();
 699          foreach ( $matches as $param => $value ) {
 700              if ( ! is_int( $param ) ) {
 701                  $args[ $param ] = $value;
 702              }
 703          }
 704  
 705          foreach ( $endpoints as $endpoint ) {
 706              // Remove the redundant preg_match() argument.
 707              unset( $args[0] );
 708  
 709              $request->set_url_params( $args );
 710              $request->set_attributes( $endpoint );
 711          }
 712  
 713          $data = $handler->get_data_for_route( $route, $endpoints, 'help' );
 714          $response->set_matched_route( $route );
 715          break;
 716      }
 717  
 718      $response->set_data( $data );
 719      return $response;
 720  }
 721  
 722  /**
 723   * Sends the "Allow" header to state all methods that can be sent to the current route.
 724   *
 725   * @since 4.4.0
 726   *
 727   * @param WP_REST_Response $response Current response being served.
 728   * @param WP_REST_Server   $server   ResponseHandler instance (usually WP_REST_Server).
 729   * @param WP_REST_Request  $request  The request that was used to make current response.
 730   * @return WP_REST_Response Response to be served, with "Allow" header if route has allowed methods.
 731   */
 732  function rest_send_allow_header( $response, $server, $request ) {
 733      $matched_route = $response->get_matched_route();
 734  
 735      if ( ! $matched_route ) {
 736          return $response;
 737      }
 738  
 739      $routes = $server->get_routes();
 740  
 741      $allowed_methods = array();
 742  
 743      // Get the allowed methods across the routes.
 744      foreach ( $routes[ $matched_route ] as $_handler ) {
 745          foreach ( $_handler['methods'] as $handler_method => $value ) {
 746  
 747              if ( ! empty( $_handler['permission_callback'] ) ) {
 748  
 749                  $permission = call_user_func( $_handler['permission_callback'], $request );
 750  
 751                  $allowed_methods[ $handler_method ] = true === $permission;
 752              } else {
 753                  $allowed_methods[ $handler_method ] = true;
 754              }
 755          }
 756      }
 757  
 758      // Strip out all the methods that are not allowed (false values).
 759      $allowed_methods = array_filter( $allowed_methods );
 760  
 761      if ( $allowed_methods ) {
 762          $response->header( 'Allow', implode( ', ', array_map( 'strtoupper', array_keys( $allowed_methods ) ) ) );
 763      }
 764  
 765      return $response;
 766  }
 767  
 768  /**
 769   * Recursively computes the intersection of arrays using keys for comparison.
 770   *
 771   * @since 5.3.0
 772   *
 773   * @param array $array1 The array with master keys to check.
 774   * @param array $array2 An array to compare keys against.
 775   * @return array An associative array containing all the entries of array1 which have keys
 776   *               that are present in all arguments.
 777   */
 778  function _rest_array_intersect_key_recursive( $array1, $array2 ) {
 779      $array1 = array_intersect_key( $array1, $array2 );
 780      foreach ( $array1 as $key => $value ) {
 781          if ( is_array( $value ) && is_array( $array2[ $key ] ) ) {
 782              $array1[ $key ] = _rest_array_intersect_key_recursive( $value, $array2[ $key ] );
 783          }
 784      }
 785      return $array1;
 786  }
 787  
 788  /**
 789   * Filters the API response to include only a white-listed set of response object fields.
 790   *
 791   * @since 4.8.0
 792   *
 793   * @param WP_REST_Response $response Current response being served.
 794   * @param WP_REST_Server   $server   ResponseHandler instance (usually WP_REST_Server).
 795   * @param WP_REST_Request  $request  The request that was used to make current response.
 796   * @return WP_REST_Response Response to be served, trimmed down to contain a subset of fields.
 797   */
 798  function rest_filter_response_fields( $response, $server, $request ) {
 799      if ( ! isset( $request['_fields'] ) || $response->is_error() ) {
 800          return $response;
 801      }
 802  
 803      $data = $response->get_data();
 804  
 805      $fields = wp_parse_list( $request['_fields'] );
 806  
 807      if ( 0 === count( $fields ) ) {
 808          return $response;
 809      }
 810  
 811      // Trim off outside whitespace from the comma delimited list.
 812      $fields = array_map( 'trim', $fields );
 813  
 814      // Create nested array of accepted field hierarchy.
 815      $fields_as_keyed = array();
 816      foreach ( $fields as $field ) {
 817          $parts = explode( '.', $field );
 818          $ref   = &$fields_as_keyed;
 819          while ( count( $parts ) > 1 ) {
 820              $next = array_shift( $parts );
 821              if ( isset( $ref[ $next ] ) && true === $ref[ $next ] ) {
 822                  // Skip any sub-properties if their parent prop is already marked for inclusion.
 823                  break 2;
 824              }
 825              $ref[ $next ] = isset( $ref[ $next ] ) ? $ref[ $next ] : array();
 826              $ref          = &$ref[ $next ];
 827          }
 828          $last         = array_shift( $parts );
 829          $ref[ $last ] = true;
 830      }
 831  
 832      if ( wp_is_numeric_array( $data ) ) {
 833          $new_data = array();
 834          foreach ( $data as $item ) {
 835              $new_data[] = _rest_array_intersect_key_recursive( $item, $fields_as_keyed );
 836          }
 837      } else {
 838          $new_data = _rest_array_intersect_key_recursive( $data, $fields_as_keyed );
 839      }
 840  
 841      $response->set_data( $new_data );
 842  
 843      return $response;
 844  }
 845  
 846  /**
 847   * Given an array of fields to include in a response, some of which may be
 848   * `nested.fields`, determine whether the provided field should be included
 849   * in the response body.
 850   *
 851   * If a parent field is passed in, the presence of any nested field within
 852   * that parent will cause the method to return `true`. For example "title"
 853   * will return true if any of `title`, `title.raw` or `title.rendered` is
 854   * provided.
 855   *
 856   * @since 5.3.0
 857   *
 858   * @param string $field  A field to test for inclusion in the response body.
 859   * @param array  $fields An array of string fields supported by the endpoint.
 860   * @return bool Whether to include the field or not.
 861   */
 862  function rest_is_field_included( $field, $fields ) {
 863      if ( in_array( $field, $fields, true ) ) {
 864          return true;
 865      }
 866  
 867      foreach ( $fields as $accepted_field ) {
 868          // Check to see if $field is the parent of any item in $fields.
 869          // A field "parent" should be accepted if "parent.child" is accepted.
 870          if ( strpos( $accepted_field, "$field." ) === 0 ) {
 871              return true;
 872          }
 873          // Conversely, if "parent" is accepted, all "parent.child" fields
 874          // should also be accepted.
 875          if ( strpos( $field, "$accepted_field." ) === 0 ) {
 876              return true;
 877          }
 878      }
 879  
 880      return false;
 881  }
 882  
 883  /**
 884   * Adds the REST API URL to the WP RSD endpoint.
 885   *
 886   * @since 4.4.0
 887   *
 888   * @see get_rest_url()
 889   */
 890  function rest_output_rsd() {
 891      $api_root = get_rest_url();
 892  
 893      if ( empty( $api_root ) ) {
 894          return;
 895      }
 896      ?>
 897      <api name="WP-API" blogID="1" preferred="false" apiLink="<?php echo esc_url( $api_root ); ?>" />
 898      <?php
 899  }
 900  
 901  /**
 902   * Outputs the REST API link tag into page header.
 903   *
 904   * @since 4.4.0
 905   *
 906   * @see get_rest_url()
 907   */
 908  function rest_output_link_wp_head() {
 909      $api_root = get_rest_url();
 910  
 911      if ( empty( $api_root ) ) {
 912          return;
 913      }
 914  
 915      printf( '<link rel="https://api.w.org/" href="%s" />', esc_url( $api_root ) );
 916  
 917      $resource = rest_get_queried_resource_route();
 918  
 919      if ( $resource ) {
 920          printf( '<link rel="alternate" type="application/json" href="%s" />', esc_url( rest_url( $resource ) ) );
 921      }
 922  }
 923  
 924  /**
 925   * Sends a Link header for the REST API.
 926   *
 927   * @since 4.4.0
 928   */
 929  function rest_output_link_header() {
 930      if ( headers_sent() ) {
 931          return;
 932      }
 933  
 934      $api_root = get_rest_url();
 935  
 936      if ( empty( $api_root ) ) {
 937          return;
 938      }
 939  
 940      header( sprintf( 'Link: <%s>; rel="https://api.w.org/"', esc_url_raw( $api_root ) ), false );
 941  
 942      $resource = rest_get_queried_resource_route();
 943  
 944      if ( $resource ) {
 945          header( sprintf( 'Link: <%s>; rel="alternate"; type="application/json"', esc_url_raw( rest_url( $resource ) ) ), false );
 946      }
 947  }
 948  
 949  /**
 950   * Checks for errors when using cookie-based authentication.
 951   *
 952   * WordPress' built-in cookie authentication is always active
 953   * for logged in users. However, the API has to check nonces
 954   * for each request to ensure users are not vulnerable to CSRF.
 955   *
 956   * @since 4.4.0
 957   *
 958   * @global mixed          $wp_rest_auth_cookie
 959   *
 960   * @param WP_Error|mixed $result Error from another authentication handler,
 961   *                               null if we should handle it, or another value if not.
 962   * @return WP_Error|mixed|bool WP_Error if the cookie is invalid, the $result, otherwise true.
 963   */
 964  function rest_cookie_check_errors( $result ) {
 965      if ( ! empty( $result ) ) {
 966          return $result;
 967      }
 968  
 969      global $wp_rest_auth_cookie;
 970  
 971      /*
 972       * Is cookie authentication being used? (If we get an auth
 973       * error, but we're still logged in, another authentication
 974       * must have been used).
 975       */
 976      if ( true !== $wp_rest_auth_cookie && is_user_logged_in() ) {
 977          return $result;
 978      }
 979  
 980      // Determine if there is a nonce.
 981      $nonce = null;
 982  
 983      if ( isset( $_REQUEST['_wpnonce'] ) ) {
 984          $nonce = $_REQUEST['_wpnonce'];
 985      } elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) {
 986          $nonce = $_SERVER['HTTP_X_WP_NONCE'];
 987      }
 988  
 989      if ( null === $nonce ) {
 990          // No nonce at all, so act as if it's an unauthenticated request.
 991          wp_set_current_user( 0 );
 992          return true;
 993      }
 994  
 995      // Check the nonce.
 996      $result = wp_verify_nonce( $nonce, 'wp_rest' );
 997  
 998      if ( ! $result ) {
 999          return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) );
1000      }
1001  
1002      // Send a refreshed nonce in header.
1003      rest_get_server()->send_header( 'X-WP-Nonce', wp_create_nonce( 'wp_rest' ) );
1004  
1005      return true;
1006  }
1007  
1008  /**
1009   * Collects cookie authentication status.
1010   *
1011   * Collects errors from wp_validate_auth_cookie for use by rest_cookie_check_errors.
1012   *
1013   * @since 4.4.0
1014   *
1015   * @see current_action()
1016   * @global mixed $wp_rest_auth_cookie
1017   */
1018  function rest_cookie_collect_status() {
1019      global $wp_rest_auth_cookie;
1020  
1021      $status_type = current_action();
1022  
1023      if ( 'auth_cookie_valid' !== $status_type ) {
1024          $wp_rest_auth_cookie = substr( $status_type, 12 );
1025          return;
1026      }
1027  
1028      $wp_rest_auth_cookie = true;
1029  }
1030  
1031  /**
1032   * Retrieves the avatar urls in various sizes.
1033   *
1034   * @since 4.7.0
1035   *
1036   * @see get_avatar_url()
1037   *
1038   * @param mixed $id_or_email The Gravatar to retrieve a URL for. Accepts a user_id, gravatar md5 hash,
1039   *                           user email, WP_User object, WP_Post object, or WP_Comment object.
1040   * @return array Avatar URLs keyed by size. Each value can be a URL string or boolean false.
1041   */
1042  function rest_get_avatar_urls( $id_or_email ) {
1043      $avatar_sizes = rest_get_avatar_sizes();
1044  
1045      $urls = array();
1046      foreach ( $avatar_sizes as $size ) {
1047          $urls[ $size ] = get_avatar_url( $id_or_email, array( 'size' => $size ) );
1048      }
1049  
1050      return $urls;
1051  }
1052  
1053  /**
1054   * Retrieves the pixel sizes for avatars.
1055   *
1056   * @since 4.7.0
1057   *
1058   * @return int[] List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`.
1059   */
1060  function rest_get_avatar_sizes() {
1061      /**
1062       * Filters the REST avatar sizes.
1063       *
1064       * Use this filter to adjust the array of sizes returned by the
1065       * `rest_get_avatar_sizes` function.
1066       *
1067       * @since 4.4.0
1068       *
1069       * @param int[] $sizes An array of int values that are the pixel sizes for avatars.
1070       *                     Default `[ 24, 48, 96 ]`.
1071       */
1072      return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
1073  }
1074  
1075  /**
1076   * Parses an RFC3339 time into a Unix timestamp.
1077   *
1078   * @since 4.4.0
1079   *
1080   * @param string $date      RFC3339 timestamp.
1081   * @param bool   $force_utc Optional. Whether to force UTC timezone instead of using
1082   *                          the timestamp's timezone. Default false.
1083   * @return int Unix timestamp.
1084   */
1085  function rest_parse_date( $date, $force_utc = false ) {
1086      if ( $force_utc ) {
1087          $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date );
1088      }
1089  
1090      $regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#';
1091  
1092      if ( ! preg_match( $regex, $date, $matches ) ) {
1093          return false;
1094      }
1095  
1096      return strtotime( $date );
1097  }
1098  
1099  /**
1100   * Parses a 3 or 6 digit hex color (with #).
1101   *
1102   * @since 5.4.0
1103   *
1104   * @param string $color 3 or 6 digit hex color (with #).
1105   * @return string|false
1106   */
1107  function rest_parse_hex_color( $color ) {
1108      $regex = '|^#([A-Fa-f0-9]{3}){1,2}$|';
1109      if ( ! preg_match( $regex, $color, $matches ) ) {
1110          return false;
1111      }
1112  
1113      return $color;
1114  }
1115  
1116  /**
1117   * Parses a date into both its local and UTC equivalent, in MySQL datetime format.
1118   *
1119   * @since 4.4.0
1120   *
1121   * @see rest_parse_date()
1122   *
1123   * @param string $date   RFC3339 timestamp.
1124   * @param bool   $is_utc Whether the provided date should be interpreted as UTC. Default false.
1125   * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s),
1126   *                    null on failure.
1127   */
1128  function rest_get_date_with_gmt( $date, $is_utc = false ) {
1129      /*
1130       * Whether or not the original date actually has a timezone string
1131       * changes the way we need to do timezone conversion.
1132       * Store this info before parsing the date, and use it later.
1133       */
1134      $has_timezone = preg_match( '#(Z|[+-]\d{2}(:\d{2})?)$#', $date );
1135  
1136      $date = rest_parse_date( $date );
1137  
1138      if ( empty( $date ) ) {
1139          return null;
1140      }
1141  
1142      /*
1143       * At this point $date could either be a local date (if we were passed
1144       * a *local* date without a timezone offset) or a UTC date (otherwise).
1145       * Timezone conversion needs to be handled differently between these two cases.
1146       */
1147      if ( ! $is_utc && ! $has_timezone ) {
1148          $local = gmdate( 'Y-m-d H:i:s', $date );
1149          $utc   = get_gmt_from_date( $local );
1150      } else {
1151          $utc   = gmdate( 'Y-m-d H:i:s', $date );
1152          $local = get_date_from_gmt( $utc );
1153      }
1154  
1155      return array( $local, $utc );
1156  }
1157  
1158  /**
1159   * Returns a contextual HTTP error code for authorization failure.
1160   *
1161   * @since 4.7.0
1162   *
1163   * @return integer 401 if the user is not logged in, 403 if the user is logged in.
1164   */
1165  function rest_authorization_required_code() {
1166      return is_user_logged_in() ? 403 : 401;
1167  }
1168  
1169  /**
1170   * Validate a request argument based on details registered to the route.
1171   *
1172   * @since 4.7.0
1173   *
1174   * @param mixed           $value
1175   * @param WP_REST_Request $request
1176   * @param string          $param
1177   * @return true|WP_Error
1178   */
1179  function rest_validate_request_arg( $value, $request, $param ) {
1180      $attributes = $request->get_attributes();
1181      if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
1182          return true;
1183      }
1184      $args = $attributes['args'][ $param ];
1185  
1186      return rest_validate_value_from_schema( $value, $args, $param );
1187  }
1188  
1189  /**
1190   * Sanitize a request argument based on details registered to the route.
1191   *
1192   * @since 4.7.0
1193   *
1194   * @param mixed           $value
1195   * @param WP_REST_Request $request
1196   * @param string          $param
1197   * @return mixed
1198   */
1199  function rest_sanitize_request_arg( $value, $request, $param ) {
1200      $attributes = $request->get_attributes();
1201      if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
1202          return $value;
1203      }
1204      $args = $attributes['args'][ $param ];
1205  
1206      return rest_sanitize_value_from_schema( $value, $args, $param );
1207  }
1208  
1209  /**
1210   * Parse a request argument based on details registered to the route.
1211   *
1212   * Runs a validation check and sanitizes the value, primarily to be used via
1213   * the `sanitize_callback` arguments in the endpoint args registration.
1214   *
1215   * @since 4.7.0
1216   *
1217   * @param mixed           $value
1218   * @param WP_REST_Request $request
1219   * @param string          $param
1220   * @return mixed
1221   */
1222  function rest_parse_request_arg( $value, $request, $param ) {
1223      $is_valid = rest_validate_request_arg( $value, $request, $param );
1224  
1225      if ( is_wp_error( $is_valid ) ) {
1226          return $is_valid;
1227      }
1228  
1229      $value = rest_sanitize_request_arg( $value, $request, $param );
1230  
1231      return $value;
1232  }
1233  
1234  /**
1235   * Determines if an IP address is valid.
1236   *
1237   * Handles both IPv4 and IPv6 addresses.
1238   *
1239   * @since 4.7.0
1240   *
1241   * @param string $ip IP address.
1242   * @return string|false The valid IP address, otherwise false.
1243   */
1244  function rest_is_ip_address( $ip ) {
1245      $ipv4_pattern = '/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/';
1246  
1247      if ( ! preg_match( $ipv4_pattern, $ip ) && ! Requests_IPv6::check_ipv6( $ip ) ) {
1248          return false;
1249      }
1250  
1251      return $ip;
1252  }
1253  
1254  /**
1255   * Changes a boolean-like value into the proper boolean value.
1256   *
1257   * @since 4.7.0
1258   *
1259   * @param bool|string|int $value The value being evaluated.
1260   * @return boolean Returns the proper associated boolean value.
1261   */
1262  function rest_sanitize_boolean( $value ) {
1263      // String values are translated to `true`; make sure 'false' is false.
1264      if ( is_string( $value ) ) {
1265          $value = strtolower( $value );
1266          if ( in_array( $value, array( 'false', '0' ), true ) ) {
1267              $value = false;
1268          }
1269      }
1270  
1271      // Everything else will map nicely to boolean.
1272      return (bool) $value;
1273  }
1274  
1275  /**
1276   * Determines if a given value is boolean-like.
1277   *
1278   * @since 4.7.0
1279   *
1280   * @param bool|string $maybe_bool The value being evaluated.
1281   * @return boolean True if a boolean, otherwise false.
1282   */
1283  function rest_is_boolean( $maybe_bool ) {
1284      if ( is_bool( $maybe_bool ) ) {
1285          return true;
1286      }
1287  
1288      if ( is_string( $maybe_bool ) ) {
1289          $maybe_bool = strtolower( $maybe_bool );
1290  
1291          $valid_boolean_values = array(
1292              'false',
1293              'true',
1294              '0',
1295              '1',
1296          );
1297  
1298          return in_array( $maybe_bool, $valid_boolean_values, true );
1299      }
1300  
1301      if ( is_int( $maybe_bool ) ) {
1302          return in_array( $maybe_bool, array( 0, 1 ), true );
1303      }
1304  
1305      return false;
1306  }
1307  
1308  /**
1309   * Determines if a given value is integer-like.
1310   *
1311   * @since 5.5.0
1312   *
1313   * @param mixed $maybe_integer The value being evaluated.
1314   * @return bool True if an integer, otherwise false.
1315   */
1316  function rest_is_integer( $maybe_integer ) {
1317      return round( floatval( $maybe_integer ) ) === floatval( $maybe_integer );
1318  }
1319  
1320  /**
1321   * Determines if a given value is array-like.
1322   *
1323   * @since 5.5.0
1324   *
1325   * @param mixed $maybe_array The value being evaluated.
1326   * @return bool
1327   */
1328  function rest_is_array( $maybe_array ) {
1329      if ( is_scalar( $maybe_array ) ) {
1330          $maybe_array = wp_parse_list( $maybe_array );
1331      }
1332  
1333      return wp_is_numeric_array( $maybe_array );
1334  }
1335  
1336  /**
1337   * Converts an array-like value to an array.
1338   *
1339   * @since 5.5.0
1340   *
1341   * @param mixed $maybe_array The value being evaluated.
1342   * @return array Returns the array extracted from the value.
1343   */
1344  function rest_sanitize_array( $maybe_array ) {
1345      if ( is_scalar( $maybe_array ) ) {
1346          return wp_parse_list( $maybe_array );
1347      }
1348  
1349      if ( ! is_array( $maybe_array ) ) {
1350          return array();
1351      }
1352  
1353      // Normalize to numeric array so nothing unexpected is in the keys.
1354      return array_values( $maybe_array );
1355  }
1356  
1357  /**
1358   * Determines if a given value is object-like.
1359   *
1360   * @since 5.5.0
1361   *
1362   * @param mixed $maybe_object The value being evaluated.
1363   * @return bool True if object like, otherwise false.
1364   */
1365  function rest_is_object( $maybe_object ) {
1366      if ( '' === $maybe_object ) {
1367          return true;
1368      }
1369  
1370      if ( $maybe_object instanceof stdClass ) {
1371          return true;
1372      }
1373  
1374      if ( $maybe_object instanceof JsonSerializable ) {
1375          $maybe_object = $maybe_object->jsonSerialize();
1376      }
1377  
1378      return is_array( $maybe_object );
1379  }
1380  
1381  /**
1382   * Converts an object-like value to an object.
1383   *
1384   * @since 5.5.0
1385   *
1386   * @param mixed $maybe_object The value being evaluated.
1387   * @return array Returns the object extracted from the value.
1388   */
1389  function rest_sanitize_object( $maybe_object ) {
1390      if ( '' === $maybe_object ) {
1391          return array();
1392      }
1393  
1394      if ( $maybe_object instanceof stdClass ) {
1395          return (array) $maybe_object;
1396      }
1397  
1398      if ( $maybe_object instanceof JsonSerializable ) {
1399          $maybe_object = $maybe_object->jsonSerialize();
1400      }
1401  
1402      if ( ! is_array( $maybe_object ) ) {
1403          return array();
1404      }
1405  
1406      return $maybe_object;
1407  }
1408  
1409  /**
1410   * Gets the best type for a value.
1411   *
1412   * @since 5.5.0
1413   *
1414   * @param mixed $value The value to check.
1415   * @param array $types The list of possible types.
1416   * @return string The best matching type, an empty string if no types match.
1417   */
1418  function rest_get_best_type_for_value( $value, $types ) {
1419      static $checks = array(
1420          'array'   => 'rest_is_array',
1421          'object'  => 'rest_is_object',
1422          'integer' => 'rest_is_integer',
1423          'number'  => 'is_numeric',
1424          'boolean' => 'rest_is_boolean',
1425          'string'  => 'is_string',
1426          'null'    => 'is_null',
1427      );
1428  
1429      // Both arrays and objects allow empty strings to be converted to their types.
1430      // But the best answer for this type is a string.
1431      if ( '' === $value && in_array( 'string', $types, true ) ) {
1432          return 'string';
1433      }
1434  
1435      foreach ( $types as $type ) {
1436          if ( isset( $checks[ $type ] ) && $checks[ $type ]( $value ) ) {
1437              return $type;
1438          }
1439      }
1440  
1441      return '';
1442  }
1443  
1444  /**
1445   * Handles getting the best type for a multi-type schema.
1446   *
1447   * This is a wrapper for {@see rest_get_best_type_for_value()} that handles
1448   * backward compatibility for schemas that use invalid types.
1449   *
1450   * @since 5.5.0
1451   *
1452   * @param mixed  $value The value to check.
1453   * @param array  $args  The schema array to use.
1454   * @param string $param The parameter name, used in error messages.
1455   * @return string
1456   */
1457  function rest_handle_multi_type_schema( $value, $args, $param = '' ) {
1458      $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
1459      $invalid_types = array_diff( $args['type'], $allowed_types );
1460  
1461      if ( $invalid_types ) {
1462          _doing_it_wrong(
1463              __FUNCTION__,
1464              /* translators: 1: Parameter, 2: List of allowed types. */
1465              wp_sprintf( __( 'The "type" schema keyword for %1$s can only contain the built-in types: %2$l.' ), $param, $allowed_types ),
1466              '5.5.0'
1467          );
1468      }
1469  
1470      $best_type = rest_get_best_type_for_value( $value, $args['type'] );
1471  
1472      if ( ! $best_type ) {
1473          if ( ! $invalid_types ) {
1474              return '';
1475          }
1476  
1477          // Backward compatibility for previous behavior which allowed the value if there was an invalid type used.
1478          $best_type = reset( $invalid_types );
1479      }
1480  
1481      return $best_type;
1482  }
1483  
1484  /**
1485   * Checks if an array is made up of unique items.
1486   *
1487   * @since 5.5.0
1488   *
1489   * @param array $array The array to check.
1490   * @return bool True if the array contains unique items, false otherwise.
1491   */
1492  function rest_validate_array_contains_unique_items( $array ) {
1493      $seen = array();
1494  
1495      foreach ( $array as $item ) {
1496          $stabilized = rest_stabilize_value( $item );
1497          $key        = serialize( $stabilized );
1498  
1499          if ( ! isset( $seen[ $key ] ) ) {
1500              $seen[ $key ] = true;
1501  
1502              continue;
1503          }
1504  
1505          return false;
1506      }
1507  
1508      return true;
1509  }
1510  
1511  /**
1512   * Stabilizes a value following JSON Schema semantics.
1513   *
1514   * For lists, order is preserved. For objects, properties are reordered alphabetically.
1515   *
1516   * @since 5.5.0
1517   *
1518   * @param mixed $value The value to stabilize. Must already be sanitized. Objects should have been converted to arrays.
1519   * @return mixed The stabilized value.
1520   */
1521  function rest_stabilize_value( $value ) {
1522      if ( is_scalar( $value ) || is_null( $value ) ) {
1523          return $value;
1524      }
1525  
1526      if ( is_object( $value ) ) {
1527          _doing_it_wrong( __FUNCTION__, __( 'Cannot stabilize objects. Convert the object to an array first.' ), '5.5.0' );
1528  
1529          return $value;
1530      }
1531  
1532      ksort( $value );
1533  
1534      foreach ( $value as $k => $v ) {
1535          $value[ $k ] = rest_stabilize_value( $v );
1536      }
1537  
1538      return $value;
1539  }
1540  
1541  /**
1542   * Validate a value based on a schema.
1543   *
1544   * @since 4.7.0
1545   * @since 4.9.0 Support the "object" type.
1546   * @since 5.2.0 Support validating "additionalProperties" against a schema.
1547   * @since 5.3.0 Support multiple types.
1548   * @since 5.4.0 Convert an empty string to an empty object.
1549   * @since 5.5.0 Add the "uuid" and "hex-color" formats.
1550   *              Support the "minLength", "maxLength" and "pattern" keywords for strings.
1551   *              Support the "minItems", "maxItems" and "uniqueItems" keywords for arrays.
1552   *              Validate required properties.
1553   *
1554   * @param mixed  $value The value to validate.
1555   * @param array  $args  Schema array to use for validation.
1556   * @param string $param The parameter name, used in error messages.
1557   * @return true|WP_Error
1558   */
1559  function rest_validate_value_from_schema( $value, $args, $param = '' ) {
1560      $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
1561  
1562      if ( ! isset( $args['type'] ) ) {
1563          /* translators: %s: Parameter. */
1564          _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' );
1565      }
1566  
1567      if ( is_array( $args['type'] ) ) {
1568          $best_type = rest_handle_multi_type_schema( $value, $args, $param );
1569  
1570          if ( ! $best_type ) {
1571              /* translators: 1: Parameter, 2: List of types. */
1572              return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ) );
1573          }
1574  
1575          $args['type'] = $best_type;
1576      }
1577  
1578      if ( ! in_array( $args['type'], $allowed_types, true ) ) {
1579          _doing_it_wrong(
1580              __FUNCTION__,
1581              /* translators: 1: Parameter, 2: The list of allowed types. */
1582              wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ),
1583              '5.5.0'
1584          );
1585      }
1586  
1587      if ( 'array' === $args['type'] ) {
1588          if ( ! rest_is_array( $value ) ) {
1589              /* translators: 1: Parameter, 2: Type name. */
1590              return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ) );
1591          }
1592  
1593          $value = rest_sanitize_array( $value );
1594  
1595          if ( isset( $args['items'] ) ) {
1596              foreach ( $value as $index => $v ) {
1597                  $is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
1598                  if ( is_wp_error( $is_valid ) ) {
1599                      return $is_valid;
1600                  }
1601              }
1602          }
1603  
1604          if ( isset( $args['minItems'] ) && count( $value ) < $args['minItems'] ) {
1605              /* translators: 1: Parameter, 2: Number. */
1606              return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must contain at least %2$s items.' ), $param, number_format_i18n( $args['minItems'] ) ) );
1607          }
1608  
1609          if ( isset( $args['maxItems'] ) && count( $value ) > $args['maxItems'] ) {
1610              /* translators: 1: Parameter, 2: Number. */
1611              return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must contain at most %2$s items.' ), $param, number_format_i18n( $args['maxItems'] ) ) );
1612          }
1613  
1614          if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) {
1615              /* translators: 1: Parameter. */
1616              return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s has duplicate items.' ), $param ) );
1617          }
1618      }
1619  
1620      if ( 'object' === $args['type'] ) {
1621          if ( ! rest_is_object( $value ) ) {
1622              /* translators: 1: Parameter, 2: Type name. */
1623              return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ) );
1624          }
1625  
1626          $value = rest_sanitize_object( $value );
1627  
1628          if ( isset( $args['required'] ) && is_array( $args['required'] ) ) { // schema version 4
1629              foreach ( $args['required'] as $name ) {
1630                  if ( ! array_key_exists( $name, $value ) ) {
1631                      /* translators: 1: Property of an object, 2: Parameter. */
1632                      return new WP_Error( 'rest_property_required', sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param ) );
1633                  }
1634              }
1635          } elseif ( isset( $args['properties'] ) ) { // schema version 3
1636              foreach ( $args['properties'] as $name => $property ) {
1637                  if ( isset( $property['required'] ) && true === $property['required'] && ! array_key_exists( $name, $value ) ) {
1638                      /* translators: 1: Property of an object, 2: Parameter. */
1639                      return new WP_Error( 'rest_property_required', sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param ) );
1640                  }
1641              }
1642          }
1643  
1644          foreach ( $value as $property => $v ) {
1645              if ( isset( $args['properties'][ $property ] ) ) {
1646                  $is_valid = rest_validate_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' );
1647                  if ( is_wp_error( $is_valid ) ) {
1648                      return $is_valid;
1649                  }
1650              } elseif ( isset( $args['additionalProperties'] ) ) {
1651                  if ( false === $args['additionalProperties'] ) {
1652                      /* translators: %s: Property of an object. */
1653                      return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not a valid property of Object.' ), $property ) );
1654                  }
1655  
1656                  if ( is_array( $args['additionalProperties'] ) ) {
1657                      $is_valid = rest_validate_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' );
1658                      if ( is_wp_error( $is_valid ) ) {
1659                          return $is_valid;
1660                      }
1661                  }
1662              }
1663          }
1664      }
1665  
1666      if ( 'null' === $args['type'] ) {
1667          if ( null !== $value ) {
1668              /* translators: 1: Parameter, 2: Type name. */
1669              return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ) );
1670          }
1671  
1672          return true;
1673      }
1674  
1675      if ( ! empty( $args['enum'] ) ) {
1676          if ( ! in_array( $value, $args['enum'], true ) ) {
1677              /* translators: 1: Parameter, 2: List of valid values. */
1678              return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) );
1679          }
1680      }
1681  
1682      if ( in_array( $args['type'], array( 'integer', 'number' ), true ) && ! is_numeric( $value ) ) {
1683          /* translators: 1: Parameter, 2: Type name. */
1684          return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) );
1685      }
1686  
1687      if ( 'integer' === $args['type'] && ! rest_is_integer( $value ) ) {
1688          /* translators: 1: Parameter, 2: Type name. */
1689          return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ) );
1690      }
1691  
1692      if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) {
1693          /* translators: 1: Parameter, 2: Type name. */
1694          return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ) );
1695      }
1696  
1697      if ( 'string' === $args['type'] ) {
1698          if ( ! is_string( $value ) ) {
1699              /* translators: 1: Parameter, 2: Type name. */
1700              return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ) );
1701          }
1702  
1703          if ( isset( $args['minLength'] ) && mb_strlen( $value ) < $args['minLength'] ) {
1704              return new WP_Error(
1705                  'rest_invalid_param',
1706                  sprintf(
1707                      /* translators: 1: Parameter, 2: Number of characters. */
1708                      _n( '%1$s must be at least %2$s character long.', '%1$s must be at least %2$s characters long.', $args['minLength'] ),
1709                      $param,
1710                      number_format_i18n( $args['minLength'] )
1711                  )
1712              );
1713          }
1714  
1715          if ( isset( $args['maxLength'] ) && mb_strlen( $value ) > $args['maxLength'] ) {
1716              return new WP_Error(
1717                  'rest_invalid_param',
1718                  sprintf(
1719                      /* translators: 1: Parameter, 2: Number of characters. */
1720                      _n( '%1$s must be at most %2$s character long.', '%1$s must be at most %2$s characters long.', $args['maxLength'] ),
1721                      $param,
1722                      number_format_i18n( $args['maxLength'] )
1723                  )
1724              );
1725          }
1726  
1727          if ( isset( $args['pattern'] ) ) {
1728              $pattern = str_replace( '#', '\\#', $args['pattern'] );
1729              if ( ! preg_match( '#' . $pattern . '#u', $value ) ) {
1730                  /* translators: 1: Parameter, 2: Pattern. */
1731                  return new WP_Error( 'rest_invalid_pattern', sprintf( __( '%1$s does not match pattern %2$s.' ), $param, $args['pattern'] ) );
1732              }
1733          }
1734      }
1735  
1736      // The "format" keyword should only be applied to strings. However, for backward compatibility,
1737      // we allow the "format" keyword if the type keyword was not specified, or was set to an invalid value.
1738      if ( isset( $args['format'] )
1739          && ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) )
1740      ) {
1741          switch ( $args['format'] ) {
1742              case 'hex-color':
1743                  if ( ! rest_parse_hex_color( $value ) ) {
1744                      return new WP_Error( 'rest_invalid_hex_color', __( 'Invalid hex color.' ) );
1745                  }
1746                  break;
1747  
1748              case 'date-time':
1749                  if ( ! rest_parse_date( $value ) ) {
1750                      return new WP_Error( 'rest_invalid_date', __( 'Invalid date.' ) );
1751                  }
1752                  break;
1753  
1754              case 'email':
1755                  if ( ! is_email( $value ) ) {
1756                      return new WP_Error( 'rest_invalid_email', __( 'Invalid email address.' ) );
1757                  }
1758                  break;
1759              case 'ip':
1760                  if ( ! rest_is_ip_address( $value ) ) {
1761                      /* translators: %s: IP address. */
1762                      return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not a valid IP address.' ), $param ) );
1763                  }
1764                  break;
1765              case 'uuid':
1766                  if ( ! wp_is_uuid( $value ) ) {
1767                      /* translators: %s: The name of a JSON field expecting a valid UUID. */
1768                      return new WP_Error( 'rest_invalid_uuid', sprintf( __( '%s is not a valid UUID.' ), $param ) );
1769                  }
1770                  break;
1771          }
1772      }
1773  
1774      if ( in_array( $args['type'], array( 'number', 'integer' ), true ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) {
1775          if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) {
1776              if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) {
1777                  /* translators: 1: Parameter, 2: Minimum number. */
1778                  return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d' ), $param, $args['minimum'] ) );
1779              } elseif ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) {
1780                  /* translators: 1: Parameter, 2: Minimum number. */
1781                  return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than or equal to %2$d' ), $param, $args['minimum'] ) );
1782              }
1783          } elseif ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) {
1784              if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) {
1785                  /* translators: 1: Parameter, 2: Maximum number. */
1786                  return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d' ), $param, $args['maximum'] ) );
1787              } elseif ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) {
1788                  /* translators: 1: Parameter, 2: Maximum number. */
1789                  return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than or equal to %2$d' ), $param, $args['maximum'] ) );
1790              }
1791          } elseif ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) {
1792              if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
1793                  if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) {
1794                      /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
1795                      return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (exclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
1796                  }
1797              } elseif ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
1798                  if ( $value >= $args['maximum'] || $value < $args['minimum'] ) {
1799                      /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
1800                      return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (inclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
1801                  }
1802              } elseif ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
1803                  if ( $value > $args['maximum'] || $value <= $args['minimum'] ) {
1804                      /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
1805                      return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (exclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
1806                  }
1807              } elseif ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
1808                  if ( $value > $args['maximum'] || $value < $args['minimum'] ) {
1809                      /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
1810                      return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
1811                  }
1812              }
1813          }
1814      }
1815  
1816      return true;
1817  }
1818  
1819  /**
1820   * Sanitize a value based on a schema.
1821   *
1822   * @since 4.7.0
1823   * @since 5.5.0 Added the `$param` parameter.
1824   *
1825   * @param mixed  $value The value to sanitize.
1826   * @param array  $args  Schema array to use for sanitization.
1827   * @param string $param The parameter name, used in error messages.
1828   * @return mixed|WP_Error The sanitized value or a WP_Error instance if the value cannot be safely sanitized.
1829   */
1830  function rest_sanitize_value_from_schema( $value, $args, $param = '' ) {
1831      $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
1832  
1833      if ( ! isset( $args['type'] ) ) {
1834          /* translators: %s: Parameter. */
1835          _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' );
1836      }
1837  
1838      if ( is_array( $args['type'] ) ) {
1839          $best_type = rest_handle_multi_type_schema( $value, $args, $param );
1840  
1841          if ( ! $best_type ) {
1842              return null;
1843          }
1844  
1845          $args['type'] = $best_type;
1846      }
1847  
1848      if ( ! in_array( $args['type'], $allowed_types, true ) ) {
1849          _doing_it_wrong(
1850              __FUNCTION__,
1851              /* translators: 1: Parameter, 2: The list of allowed types. */
1852              wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ),
1853              '5.5.0'
1854          );
1855      }
1856  
1857      if ( 'array' === $args['type'] ) {
1858          $value = rest_sanitize_array( $value );
1859  
1860          if ( ! empty( $args['items'] ) ) {
1861              foreach ( $value as $index => $v ) {
1862                  $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
1863              }
1864          }
1865  
1866          if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) {
1867              /* translators: 1: Parameter. */
1868              return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s has duplicate items.' ), $param ) );
1869          }
1870  
1871          return $value;
1872      }
1873  
1874      if ( 'object' === $args['type'] ) {
1875          $value = rest_sanitize_object( $value );
1876  
1877          foreach ( $value as $property => $v ) {
1878              if ( isset( $args['properties'][ $property ] ) ) {
1879                  $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' );
1880              } elseif ( isset( $args['additionalProperties'] ) ) {
1881                  if ( false === $args['additionalProperties'] ) {
1882                      unset( $value[ $property ] );
1883                  } elseif ( is_array( $args['additionalProperties'] ) ) {
1884                      $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' );
1885                  }
1886              }
1887          }
1888  
1889          return $value;
1890      }
1891  
1892      if ( 'null' === $args['type'] ) {
1893          return null;
1894      }
1895  
1896      if ( 'integer' === $args['type'] ) {
1897          return (int) $value;
1898      }
1899  
1900      if ( 'number' === $args['type'] ) {
1901          return (float) $value;
1902      }
1903  
1904      if ( 'boolean' === $args['type'] ) {
1905          return rest_sanitize_boolean( $value );
1906      }
1907  
1908      // This behavior matches rest_validate_value_from_schema().
1909      if ( isset( $args['format'] )
1910          && ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) )
1911      ) {
1912          switch ( $args['format'] ) {
1913              case 'hex-color':
1914                  return (string) sanitize_hex_color( $value );
1915  
1916              case 'date-time':
1917                  return sanitize_text_field( $value );
1918  
1919              case 'email':
1920                  // sanitize_email() validates, which would be unexpected.
1921                  return sanitize_text_field( $value );
1922  
1923              case 'uri':
1924                  return esc_url_raw( $value );
1925  
1926              case 'ip':
1927                  return sanitize_text_field( $value );
1928  
1929              case 'uuid':
1930                  return sanitize_text_field( $value );
1931          }
1932      }
1933  
1934      if ( 'string' === $args['type'] ) {
1935          return strval( $value );
1936      }
1937  
1938      return $value;
1939  }
1940  
1941  /**
1942   * Append result of internal request to REST API for purpose of preloading data to be attached to a page.
1943   * Expected to be called in the context of `array_reduce`.
1944   *
1945   * @since 5.0.0
1946   *
1947   * @param array  $memo Reduce accumulator.
1948   * @param string $path REST API path to preload.
1949   * @return array Modified reduce accumulator.
1950   */
1951  function rest_preload_api_request( $memo, $path ) {
1952      // array_reduce() doesn't support passing an array in PHP 5.2,
1953      // so we need to make sure we start with one.
1954      if ( ! is_array( $memo ) ) {
1955          $memo = array();
1956      }
1957  
1958      if ( empty( $path ) ) {
1959          return $memo;
1960      }
1961  
1962      $method = 'GET';
1963      if ( is_array( $path ) && 2 === count( $path ) ) {
1964          $method = end( $path );
1965          $path   = reset( $path );
1966  
1967          if ( ! in_array( $method, array( 'GET', 'OPTIONS' ), true ) ) {
1968              $method = 'GET';
1969          }
1970      }
1971  
1972      $path_parts = parse_url( $path );
1973      if ( false === $path_parts ) {
1974          return $memo;
1975      }
1976  
1977      $request = new WP_REST_Request( $method, $path_parts['path'] );
1978      if ( ! empty( $path_parts['query'] ) ) {
1979          parse_str( $path_parts['query'], $query_params );
1980          $request->set_query_params( $query_params );
1981      }
1982  
1983      $response = rest_do_request( $request );
1984      if ( 200 === $response->status ) {
1985          $server = rest_get_server();
1986          $data   = (array) $response->get_data();
1987          $links  = $server::get_compact_response_links( $response );
1988          if ( ! empty( $links ) ) {
1989              $data['_links'] = $links;
1990          }
1991  
1992          if ( 'OPTIONS' === $method ) {
1993              $response = rest_send_allow_header( $response, $server, $request );
1994  
1995              $memo[ $method ][ $path ] = array(
1996                  'body'    => $data,
1997                  'headers' => $response->headers,
1998              );
1999          } else {
2000              $memo[ $path ] = array(
2001                  'body'    => $data,
2002                  'headers' => $response->headers,
2003              );
2004          }
2005      }
2006  
2007      return $memo;
2008  }
2009  
2010  /**
2011   * Parses the "_embed" parameter into the list of resources to embed.
2012   *
2013   * @since 5.4.0
2014   *
2015   * @param string|array $embed Raw "_embed" parameter value.
2016   * @return true|string[] Either true to embed all embeds, or a list of relations to embed.
2017   */
2018  function rest_parse_embed_param( $embed ) {
2019      if ( ! $embed || 'true' === $embed || '1' === $embed ) {
2020          return true;
2021      }
2022  
2023      $rels = wp_parse_list( $embed );
2024  
2025      if ( ! $rels ) {
2026          return true;
2027      }
2028  
2029      return $rels;
2030  }
2031  
2032  /**
2033   * Filters the response to remove any fields not available in the given context.
2034   *
2035   * @since 5.5.0
2036   *
2037   * @param array|object $data    The response data to modify.
2038   * @param array        $schema  The schema for the endpoint used to filter the response.
2039   * @param string       $context The requested context.
2040   * @return array|object The filtered response data.
2041   */
2042  function rest_filter_response_by_context( $data, $schema, $context ) {
2043      if ( ! is_array( $data ) && ! is_object( $data ) ) {
2044          return $data;
2045      }
2046  
2047      if ( isset( $schema['type'] ) ) {
2048          $type = $schema['type'];
2049      } elseif ( isset( $schema['properties'] ) ) {
2050          $type = 'object'; // Back compat if a developer accidentally omitted the type.
2051      } else {
2052          return $data;
2053      }
2054  
2055      $is_array_type  = 'array' === $type || ( is_array( $type ) && in_array( 'array', $type, true ) );
2056      $is_object_type = 'object' === $type || ( is_array( $type ) && in_array( 'object', $type, true ) );
2057  
2058      if ( $is_array_type && $is_object_type ) {
2059          if ( rest_is_array( $data ) ) {
2060              $is_object_type = false;
2061          } else {
2062              $is_array_type = false;
2063          }
2064      }
2065  
2066      $has_additional_properties = $is_object_type && isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] );
2067  
2068      foreach ( $data as $key => $value ) {
2069          $check = array();
2070  
2071          if ( $is_array_type ) {
2072              $check = isset( $schema['items'] ) ? $schema['items'] : array();
2073          } elseif ( $is_object_type ) {
2074              if ( isset( $schema['properties'][ $key ] ) ) {
2075                  $check = $schema['properties'][ $key ];
2076              } elseif ( $has_additional_properties ) {
2077                  $check = $schema['additionalProperties'];
2078              }
2079          }
2080  
2081          if ( ! isset( $check['context'] ) ) {
2082              continue;
2083          }
2084  
2085          if ( ! in_array( $context, $check['context'], true ) ) {
2086              if ( $is_array_type ) {
2087                  // All array items share schema, so there's no need to check each one.
2088                  $data = array();
2089                  break;
2090              }
2091  
2092              if ( is_object( $data ) ) {
2093                  unset( $data->$key );
2094              } else {
2095                  unset( $data[ $key ] );
2096              }
2097          } elseif ( is_array( $value ) || is_object( $value ) ) {
2098              $new_value = rest_filter_response_by_context( $value, $check, $context );
2099  
2100              if ( is_object( $data ) ) {
2101                  $data->$key = $new_value;
2102              } else {
2103                  $data[ $key ] = $new_value;
2104              }
2105          }
2106      }
2107  
2108      return $data;
2109  }
2110  
2111  /**
2112   * Sets the "additionalProperties" to false by default for all object definitions in the schema.
2113   *
2114   * @since 5.5.0
2115   *
2116   * @param array $schema The schema to modify.
2117   * @return array The modified schema.
2118   */
2119  function rest_default_additional_properties_to_false( $schema ) {
2120      $type = (array) $schema['type'];
2121  
2122      if ( in_array( 'object', $type, true ) ) {
2123          if ( isset( $schema['properties'] ) ) {
2124              foreach ( $schema['properties'] as $key => $child_schema ) {
2125                  $schema['properties'][ $key ] = rest_default_additional_properties_to_false( $child_schema );
2126              }
2127          }
2128  
2129          if ( ! isset( $schema['additionalProperties'] ) ) {
2130              $schema['additionalProperties'] = false;
2131          }
2132      }
2133  
2134      if ( in_array( 'array', $type, true ) ) {
2135          if ( isset( $schema['items'] ) ) {
2136              $schema['items'] = rest_default_additional_properties_to_false( $schema['items'] );
2137          }
2138      }
2139  
2140      return $schema;
2141  }
2142  
2143  /**
2144   * Gets the REST API route for a post.
2145   *
2146   * @since 5.5.0
2147   *
2148   * @param int|WP_Post $post Post ID or post object.
2149   * @return string The route path with a leading slash for the given post, or an empty string if there is not a route.
2150   */
2151  function rest_get_route_for_post( $post ) {
2152      $post = get_post( $post );
2153  
2154      if ( ! $post instanceof WP_Post ) {
2155          return '';
2156      }
2157  
2158      $post_type = get_post_type_object( $post->post_type );
2159      if ( ! $post_type ) {
2160          return '';
2161      }
2162  
2163      $controller = $post_type->get_rest_controller();
2164      if ( ! $controller ) {
2165          return '';
2166      }
2167  
2168      $route = '';
2169  
2170      // The only two controllers that we can detect are the Attachments and Posts controllers.
2171      if ( in_array( get_class( $controller ), array( 'WP_REST_Attachments_Controller', 'WP_REST_Posts_Controller' ), true ) ) {
2172          $namespace = 'wp/v2';
2173          $rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
2174          $route     = sprintf( '/%s/%s/%d', $namespace, $rest_base, $post->ID );
2175      }
2176  
2177      /**
2178       * Filters the REST API route for a post.
2179       *
2180       * @since 5.5.0
2181       *
2182       * @param string  $route The route path.
2183       * @param WP_Post $post  The post object.
2184       */
2185      return apply_filters( 'rest_route_for_post', $route, $post );
2186  }
2187  
2188  /**
2189   * Gets the REST API route for a term.
2190   *
2191   * @since 5.5.0
2192   *
2193   * @param int|WP_Term $term Term ID or term object.
2194   * @return string The route path with a leading slash for the given term, or an empty string if there is not a route.
2195   */
2196  function rest_get_route_for_term( $term ) {
2197      $term = get_term( $term );
2198  
2199      if ( ! $term instanceof WP_Term ) {
2200          return '';
2201      }
2202  
2203      $taxonomy = get_taxonomy( $term->taxonomy );
2204      if ( ! $taxonomy ) {
2205          return '';
2206      }
2207  
2208      $controller = $taxonomy->get_rest_controller();
2209      if ( ! $controller ) {
2210          return '';
2211      }
2212  
2213      $route = '';
2214  
2215      // The only controller that works is the Terms controller.
2216      if ( 'WP_REST_Terms_Controller' === get_class( $controller ) ) {
2217          $namespace = 'wp/v2';
2218          $rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
2219          $route     = sprintf( '/%s/%s/%d', $namespace, $rest_base, $term->term_id );
2220      }
2221  
2222      /**
2223       * Filters the REST API route for a term.
2224       *
2225       * @since 5.5.0
2226       *
2227       * @param string  $route The route path.
2228       * @param WP_Term $term  The term object.
2229       */
2230      return apply_filters( 'rest_route_for_term', $route, $term );
2231  }
2232  
2233  /**
2234   * Gets the REST route for the currently queried object.
2235   *
2236   * @since 5.5.0
2237   *
2238   * @return string The REST route of the resource, or an empty string if no resource identified.
2239   */
2240  function rest_get_queried_resource_route() {
2241      if ( is_singular() ) {
2242          $route = rest_get_route_for_post( get_queried_object() );
2243      } elseif ( is_category() || is_tag() || is_tax() ) {
2244          $route = rest_get_route_for_term( get_queried_object() );
2245      } elseif ( is_author() ) {
2246          $route = '/wp/v2/users/' . get_queried_object_id();
2247      } else {
2248          $route = '';
2249      }
2250  
2251      /**
2252       * Filters the REST route for the currently queried object.
2253       *
2254       * @since 5.5.0
2255       *
2256       * @param string $link The route with a leading slash, or an empty string.
2257       */
2258      return apply_filters( 'rest_queried_resource_route', $route );
2259  }


Generated: Wed Aug 12 01:00:03 2020 Cross-referenced by PHPXref 0.7.1