[ 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   * @since 5.5.0 Added a `_doing_it_wrong()` notice when the required `permission_callback` argument is not set.
  25   *
  26   * @param string $namespace The first URL segment after core prefix. Should be unique to your package/plugin.
  27   * @param string $route     The base URL for route you are adding.
  28   * @param array  $args      Optional. Either an array of options for the endpoint, or an array of arrays for
  29   *                          multiple methods. Default empty array.
  30   * @param bool   $override  Optional. If the route already exists, should we override it? True overrides,
  31   *                          false merges (with newer overriding if duplicate keys exist). Default false.
  32   * @return bool True on success, false on error.
  33   */
  34  function register_rest_route( $namespace, $route, $args = array(), $override = false ) {
  35      if ( empty( $namespace ) ) {
  36          /*
  37           * Non-namespaced routes are not allowed, with the exception of the main
  38           * and namespace indexes. If you really need to register a
  39           * non-namespaced route, call `WP_REST_Server::register_route` directly.
  40           */
  41          _doing_it_wrong( 'register_rest_route', __( 'Routes must be namespaced with plugin or theme name and version.' ), '4.4.0' );
  42          return false;
  43      } elseif ( empty( $route ) ) {
  44          _doing_it_wrong( 'register_rest_route', __( 'Route must be specified.' ), '4.4.0' );
  45          return false;
  46      }
  47  
  48      $clean_namespace = trim( $namespace, '/' );
  49  
  50      if ( $clean_namespace !== $namespace ) {
  51          _doing_it_wrong( __FUNCTION__, __( 'Namespace must not start or end with a slash.' ), '5.4.2' );
  52      }
  53  
  54      if ( ! did_action( 'rest_api_init' ) ) {
  55          _doing_it_wrong(
  56              'register_rest_route',
  57              sprintf(
  58                  /* translators: %s: rest_api_init */
  59                  __( 'REST API routes must be registered on the %s action.' ),
  60                  '<code>rest_api_init</code>'
  61              ),
  62              '5.1.0'
  63          );
  64      }
  65  
  66      if ( isset( $args['args'] ) ) {
  67          $common_args = $args['args'];
  68          unset( $args['args'] );
  69      } else {
  70          $common_args = array();
  71      }
  72  
  73      if ( isset( $args['callback'] ) ) {
  74          // Upgrade a single set to multiple.
  75          $args = array( $args );
  76      }
  77  
  78      $defaults = array(
  79          'methods'  => 'GET',
  80          'callback' => null,
  81          'args'     => array(),
  82      );
  83  
  84      foreach ( $args as $key => &$arg_group ) {
  85          if ( ! is_numeric( $key ) ) {
  86              // Route option, skip here.
  87              continue;
  88          }
  89  
  90          $arg_group         = array_merge( $defaults, $arg_group );
  91          $arg_group['args'] = array_merge( $common_args, $arg_group['args'] );
  92  
  93          if ( ! isset( $arg_group['permission_callback'] ) ) {
  94              _doing_it_wrong(
  95                  __FUNCTION__,
  96                  sprintf(
  97                      /* translators: 1: The REST API route being registered, 2: The argument name, 3: The suggested function name. */
  98                      __( '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.' ),
  99                      '<code>' . $clean_namespace . '/' . trim( $route, '/' ) . '</code>',
 100                      '<code>permission_callback</code>',
 101                      '<code>__return_true</code>'
 102                  ),
 103                  '5.5.0'
 104              );
 105          }
 106      }
 107  
 108      $full_route = '/' . $clean_namespace . '/' . trim( $route, '/' );
 109      rest_get_server()->register_route( $clean_namespace, $full_route, $args, $override );
 110      return true;
 111  }
 112  
 113  /**
 114   * Registers a new field on an existing WordPress object type.
 115   *
 116   * @since 4.7.0
 117   *
 118   * @global array $wp_rest_additional_fields Holds registered fields, organized
 119   *                                          by object type.
 120   *
 121   * @param string|array $object_type Object(s) the field is being registered
 122   *                                  to, "post"|"term"|"comment" etc.
 123   * @param string       $attribute   The attribute name.
 124   * @param array        $args {
 125   *     Optional. An array of arguments used to handle the registered field.
 126   *
 127   *     @type callable|null $get_callback    Optional. The callback function used to retrieve the field value. Default is
 128   *                                          'null', the field will not be returned in the response. The function will
 129   *                                          be passed the prepared object data.
 130   *     @type callable|null $update_callback Optional. The callback function used to set and update the field value. Default
 131   *                                          is 'null', the value cannot be set or updated. The function will be passed
 132   *                                          the model object, like WP_Post.
 133   *     @type array|null $schema             Optional. The schema for this field.
 134   *                                          Default is 'null', no schema entry will be returned.
 135   * }
 136   */
 137  function register_rest_field( $object_type, $attribute, $args = array() ) {
 138      $defaults = array(
 139          'get_callback'    => null,
 140          'update_callback' => null,
 141          'schema'          => null,
 142      );
 143  
 144      $args = wp_parse_args( $args, $defaults );
 145  
 146      global $wp_rest_additional_fields;
 147  
 148      $object_types = (array) $object_type;
 149  
 150      foreach ( $object_types as $object_type ) {
 151          $wp_rest_additional_fields[ $object_type ][ $attribute ] = $args;
 152      }
 153  }
 154  
 155  /**
 156   * Registers rewrite rules for the REST API.
 157   *
 158   * @since 4.4.0
 159   *
 160   * @see rest_api_register_rewrites()
 161   * @global WP $wp Current WordPress environment instance.
 162   */
 163  function rest_api_init() {
 164      rest_api_register_rewrites();
 165  
 166      global $wp;
 167      $wp->add_query_var( 'rest_route' );
 168  }
 169  
 170  /**
 171   * Adds REST rewrite rules.
 172   *
 173   * @since 4.4.0
 174   *
 175   * @see add_rewrite_rule()
 176   * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
 177   */
 178  function rest_api_register_rewrites() {
 179      global $wp_rewrite;
 180  
 181      add_rewrite_rule( '^' . rest_get_url_prefix() . '/?$', 'index.php?rest_route=/', 'top' );
 182      add_rewrite_rule( '^' . rest_get_url_prefix() . '/(.*)?', 'index.php?rest_route=/$matches[1]', 'top' );
 183      add_rewrite_rule( '^' . $wp_rewrite->index . '/' . rest_get_url_prefix() . '/?$', 'index.php?rest_route=/', 'top' );
 184      add_rewrite_rule( '^' . $wp_rewrite->index . '/' . rest_get_url_prefix() . '/(.*)?', 'index.php?rest_route=/$matches[1]', 'top' );
 185  }
 186  
 187  /**
 188   * Registers the default REST API filters.
 189   *
 190   * Attached to the {@see 'rest_api_init'} action
 191   * to make testing and disabling these filters easier.
 192   *
 193   * @since 4.4.0
 194   */
 195  function rest_api_default_filters() {
 196      if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
 197          // Deprecated reporting.
 198          add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 );
 199          add_filter( 'deprecated_function_trigger_error', '__return_false' );
 200          add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 );
 201          add_filter( 'deprecated_argument_trigger_error', '__return_false' );
 202          add_action( 'doing_it_wrong_run', 'rest_handle_doing_it_wrong', 10, 3 );
 203          add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
 204      }
 205  
 206      // Default serving.
 207      add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
 208      add_filter( 'rest_post_dispatch', 'rest_send_allow_header', 10, 3 );
 209      add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 );
 210  
 211      add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 );
 212      add_filter( 'rest_index', 'rest_add_application_passwords_to_index' );
 213  }
 214  
 215  /**
 216   * Registers default REST API routes.
 217   *
 218   * @since 4.7.0
 219   */
 220  function create_initial_rest_routes() {
 221      foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
 222          $controller = $post_type->get_rest_controller();
 223  
 224          if ( ! $controller ) {
 225              continue;
 226          }
 227  
 228          $controller->register_routes();
 229  
 230          if ( post_type_supports( $post_type->name, 'revisions' ) ) {
 231              $revisions_controller = new WP_REST_Revisions_Controller( $post_type->name );
 232              $revisions_controller->register_routes();
 233          }
 234  
 235          if ( 'attachment' !== $post_type->name ) {
 236              $autosaves_controller = new WP_REST_Autosaves_Controller( $post_type->name );
 237              $autosaves_controller->register_routes();
 238          }
 239      }
 240  
 241      // Post types.
 242      $controller = new WP_REST_Post_Types_Controller;
 243      $controller->register_routes();
 244  
 245      // Post statuses.
 246      $controller = new WP_REST_Post_Statuses_Controller;
 247      $controller->register_routes();
 248  
 249      // Taxonomies.
 250      $controller = new WP_REST_Taxonomies_Controller;
 251      $controller->register_routes();
 252  
 253      // Terms.
 254      foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
 255          $controller = $taxonomy->get_rest_controller();
 256  
 257          if ( ! $controller ) {
 258              continue;
 259          }
 260  
 261          $controller->register_routes();
 262      }
 263  
 264      // Users.
 265      $controller = new WP_REST_Users_Controller;
 266      $controller->register_routes();
 267  
 268      // Application Passwords
 269      $controller = new WP_REST_Application_Passwords_Controller();
 270      $controller->register_routes();
 271  
 272      // Comments.
 273      $controller = new WP_REST_Comments_Controller;
 274      $controller->register_routes();
 275  
 276      $search_handlers = array(
 277          new WP_REST_Post_Search_Handler(),
 278          new WP_REST_Term_Search_Handler(),
 279          new WP_REST_Post_Format_Search_Handler(),
 280      );
 281  
 282      /**
 283       * Filters the search handlers to use in the REST search controller.
 284       *
 285       * @since 5.0.0
 286       *
 287       * @param array $search_handlers List of search handlers to use in the controller. Each search
 288       *                               handler instance must extend the `WP_REST_Search_Handler` class.
 289       *                               Default is only a handler for posts.
 290       */
 291      $search_handlers = apply_filters( 'wp_rest_search_handlers', $search_handlers );
 292  
 293      $controller = new WP_REST_Search_Controller( $search_handlers );
 294      $controller->register_routes();
 295  
 296      // Block Renderer.
 297      $controller = new WP_REST_Block_Renderer_Controller;
 298      $controller->register_routes();
 299  
 300      // Block Types.
 301      $controller = new WP_REST_Block_Types_Controller();
 302      $controller->register_routes();
 303  
 304      // Settings.
 305      $controller = new WP_REST_Settings_Controller;
 306      $controller->register_routes();
 307  
 308      // Themes.
 309      $controller = new WP_REST_Themes_Controller;
 310      $controller->register_routes();
 311  
 312      // Plugins.
 313      $controller = new WP_REST_Plugins_Controller();
 314      $controller->register_routes();
 315  
 316      // Block Directory.
 317      $controller = new WP_REST_Block_Directory_Controller();
 318      $controller->register_routes();
 319  
 320      // Site Health.
 321      $site_health = WP_Site_Health::get_instance();
 322      $controller  = new WP_REST_Site_Health_Controller( $site_health );
 323      $controller->register_routes();
 324  }
 325  
 326  /**
 327   * Loads the REST API.
 328   *
 329   * @since 4.4.0
 330   *
 331   * @global WP $wp Current WordPress environment instance.
 332   */
 333  function rest_api_loaded() {
 334      if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
 335          return;
 336      }
 337  
 338      /**
 339       * Whether this is a REST Request.
 340       *
 341       * @since 4.4.0
 342       * @var bool
 343       */
 344      define( 'REST_REQUEST', true );
 345  
 346      // Initialize the server.
 347      $server = rest_get_server();
 348  
 349      // Fire off the request.
 350      $route = untrailingslashit( $GLOBALS['wp']->query_vars['rest_route'] );
 351      if ( empty( $route ) ) {
 352          $route = '/';
 353      }
 354      $server->serve_request( $route );
 355  
 356      // We're done.
 357      die();
 358  }
 359  
 360  /**
 361   * Retrieves the URL prefix for any API resource.
 362   *
 363   * @since 4.4.0
 364   *
 365   * @return string Prefix.
 366   */
 367  function rest_get_url_prefix() {
 368      /**
 369       * Filters the REST URL prefix.
 370       *
 371       * @since 4.4.0
 372       *
 373       * @param string $prefix URL prefix. Default 'wp-json'.
 374       */
 375      return apply_filters( 'rest_url_prefix', 'wp-json' );
 376  }
 377  
 378  /**
 379   * Retrieves the URL to a REST endpoint on a site.
 380   *
 381   * Note: The returned URL is NOT escaped.
 382   *
 383   * @since 4.4.0
 384   *
 385   * @todo Check if this is even necessary
 386   * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
 387   *
 388   * @param int|null $blog_id Optional. Blog ID. Default of null returns URL for current blog.
 389   * @param string   $path    Optional. REST route. Default '/'.
 390   * @param string   $scheme  Optional. Sanitization scheme. Default 'rest'.
 391   * @return string Full URL to the endpoint.
 392   */
 393  function get_rest_url( $blog_id = null, $path = '/', $scheme = 'rest' ) {
 394      if ( empty( $path ) ) {
 395          $path = '/';
 396      }
 397  
 398      $path = '/' . ltrim( $path, '/' );
 399  
 400      if ( is_multisite() && get_blog_option( $blog_id, 'permalink_structure' ) || get_option( 'permalink_structure' ) ) {
 401          global $wp_rewrite;
 402  
 403          if ( $wp_rewrite->using_index_permalinks() ) {
 404              $url = get_home_url( $blog_id, $wp_rewrite->index . '/' . rest_get_url_prefix(), $scheme );
 405          } else {
 406              $url = get_home_url( $blog_id, rest_get_url_prefix(), $scheme );
 407          }
 408  
 409          $url .= $path;
 410      } else {
 411          $url = trailingslashit( get_home_url( $blog_id, '', $scheme ) );
 412          // nginx only allows HTTP/1.0 methods when redirecting from / to /index.php.
 413          // To work around this, we manually add index.php to the URL, avoiding the redirect.
 414          if ( 'index.php' !== substr( $url, 9 ) ) {
 415              $url .= 'index.php';
 416          }
 417  
 418          $url = add_query_arg( 'rest_route', $path, $url );
 419      }
 420  
 421      if ( is_ssl() && isset( $_SERVER['SERVER_NAME'] ) ) {
 422          // If the current host is the same as the REST URL host, force the REST URL scheme to HTTPS.
 423          if ( parse_url( get_home_url( $blog_id ), PHP_URL_HOST ) === $_SERVER['SERVER_NAME'] ) {
 424              $url = set_url_scheme( $url, 'https' );
 425          }
 426      }
 427  
 428      if ( is_admin() && force_ssl_admin() ) {
 429          /*
 430           * In this situation the home URL may be http:, and `is_ssl()` may be false,
 431           * but the admin is served over https: (one way or another), so REST API usage
 432           * will be blocked by browsers unless it is also served over HTTPS.
 433           */
 434          $url = set_url_scheme( $url, 'https' );
 435      }
 436  
 437      /**
 438       * Filters the REST URL.
 439       *
 440       * Use this filter to adjust the url returned by the get_rest_url() function.
 441       *
 442       * @since 4.4.0
 443       *
 444       * @param string   $url     REST URL.
 445       * @param string   $path    REST route.
 446       * @param int|null $blog_id Blog ID.
 447       * @param string   $scheme  Sanitization scheme.
 448       */
 449      return apply_filters( 'rest_url', $url, $path, $blog_id, $scheme );
 450  }
 451  
 452  /**
 453   * Retrieves the URL to a REST endpoint.
 454   *
 455   * Note: The returned URL is NOT escaped.
 456   *
 457   * @since 4.4.0
 458   *
 459   * @param string $path   Optional. REST route. Default empty.
 460   * @param string $scheme Optional. Sanitization scheme. Default 'rest'.
 461   * @return string Full URL to the endpoint.
 462   */
 463  function rest_url( $path = '', $scheme = 'rest' ) {
 464      return get_rest_url( null, $path, $scheme );
 465  }
 466  
 467  /**
 468   * Do a REST request.
 469   *
 470   * Used primarily to route internal requests through WP_REST_Server.
 471   *
 472   * @since 4.4.0
 473   *
 474   * @param WP_REST_Request|string $request Request.
 475   * @return WP_REST_Response REST response.
 476   */
 477  function rest_do_request( $request ) {
 478      $request = rest_ensure_request( $request );
 479      return rest_get_server()->dispatch( $request );
 480  }
 481  
 482  /**
 483   * Retrieves the current REST server instance.
 484   *
 485   * Instantiates a new instance if none exists already.
 486   *
 487   * @since 4.5.0
 488   *
 489   * @global WP_REST_Server $wp_rest_server REST server instance.
 490   *
 491   * @return WP_REST_Server REST server instance.
 492   */
 493  function rest_get_server() {
 494      /* @var WP_REST_Server $wp_rest_server */
 495      global $wp_rest_server;
 496  
 497      if ( empty( $wp_rest_server ) ) {
 498          /**
 499           * Filters the REST Server Class.
 500           *
 501           * This filter allows you to adjust the server class used by the REST API, using a
 502           * different class to handle requests.
 503           *
 504           * @since 4.4.0
 505           *
 506           * @param string $class_name The name of the server class. Default 'WP_REST_Server'.
 507           */
 508          $wp_rest_server_class = apply_filters( 'wp_rest_server_class', 'WP_REST_Server' );
 509          $wp_rest_server       = new $wp_rest_server_class;
 510  
 511          /**
 512           * Fires when preparing to serve a REST API request.
 513           *
 514           * Endpoint objects should be created and register their hooks on this action rather
 515           * than another action to ensure they're only loaded when needed.
 516           *
 517           * @since 4.4.0
 518           *
 519           * @param WP_REST_Server $wp_rest_server Server object.
 520           */
 521          do_action( 'rest_api_init', $wp_rest_server );
 522      }
 523  
 524      return $wp_rest_server;
 525  }
 526  
 527  /**
 528   * Ensures request arguments are a request object (for consistency).
 529   *
 530   * @since 4.4.0
 531   * @since 5.3.0 Accept string argument for the request path.
 532   *
 533   * @param array|string|WP_REST_Request $request Request to check.
 534   * @return WP_REST_Request REST request instance.
 535   */
 536  function rest_ensure_request( $request ) {
 537      if ( $request instanceof WP_REST_Request ) {
 538          return $request;
 539      }
 540  
 541      if ( is_string( $request ) ) {
 542          return new WP_REST_Request( 'GET', $request );
 543      }
 544  
 545      return new WP_REST_Request( 'GET', '', $request );
 546  }
 547  
 548  /**
 549   * Ensures a REST response is a response object (for consistency).
 550   *
 551   * This implements WP_REST_Response, allowing usage of `set_status`/`header`/etc
 552   * without needing to double-check the object. Will also allow WP_Error to indicate error
 553   * responses, so users should immediately check for this value.
 554   *
 555   * @since 4.4.0
 556   *
 557   * @param WP_REST_Response|WP_Error|WP_HTTP_Response|mixed $response Response to check.
 558   * @return WP_REST_Response|WP_Error If response generated an error, WP_Error, if response
 559   *                                   is already an instance, WP_REST_Response, otherwise
 560   *                                   returns a new WP_REST_Response instance.
 561   */
 562  function rest_ensure_response( $response ) {
 563      if ( is_wp_error( $response ) ) {
 564          return $response;
 565      }
 566  
 567      if ( $response instanceof WP_REST_Response ) {
 568          return $response;
 569      }
 570  
 571      // While WP_HTTP_Response is the base class of WP_REST_Response, it doesn't provide
 572      // all the required methods used in WP_REST_Server::dispatch().
 573      if ( $response instanceof WP_HTTP_Response ) {
 574          return new WP_REST_Response(
 575              $response->get_data(),
 576              $response->get_status(),
 577              $response->get_headers()
 578          );
 579      }
 580  
 581      return new WP_REST_Response( $response );
 582  }
 583  
 584  /**
 585   * Handles _deprecated_function() errors.
 586   *
 587   * @since 4.4.0
 588   *
 589   * @param string $function    The function that was called.
 590   * @param string $replacement The function that should have been called.
 591   * @param string $version     Version.
 592   */
 593  function rest_handle_deprecated_function( $function, $replacement, $version ) {
 594      if ( ! WP_DEBUG || headers_sent() ) {
 595          return;
 596      }
 597      if ( ! empty( $replacement ) ) {
 598          /* translators: 1: Function name, 2: WordPress version number, 3: New function name. */
 599          $string = sprintf( __( '%1$s (since %2$s; use %3$s instead)' ), $function, $version, $replacement );
 600      } else {
 601          /* translators: 1: Function name, 2: WordPress version number. */
 602          $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
 603      }
 604  
 605      header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) );
 606  }
 607  
 608  /**
 609   * Handles _deprecated_argument() errors.
 610   *
 611   * @since 4.4.0
 612   *
 613   * @param string $function    The function that was called.
 614   * @param string $message     A message regarding the change.
 615   * @param string $version     Version.
 616   */
 617  function rest_handle_deprecated_argument( $function, $message, $version ) {
 618      if ( ! WP_DEBUG || headers_sent() ) {
 619          return;
 620      }
 621      if ( $message ) {
 622          /* translators: 1: Function name, 2: WordPress version number, 3: Error message. */
 623          $string = sprintf( __( '%1$s (since %2$s; %3$s)' ), $function, $version, $message );
 624      } else {
 625          /* translators: 1: Function name, 2: WordPress version number. */
 626          $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
 627      }
 628  
 629      header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) );
 630  }
 631  
 632  /**
 633   * Handles _doing_it_wrong errors.
 634   *
 635   * @since 5.5.0
 636   *
 637   * @param string      $function The function that was called.
 638   * @param string      $message  A message explaining what has been done incorrectly.
 639   * @param string|null $version  The version of WordPress where the message was added.
 640   */
 641  function rest_handle_doing_it_wrong( $function, $message, $version ) {
 642      if ( ! WP_DEBUG || headers_sent() ) {
 643          return;
 644      }
 645  
 646      if ( $version ) {
 647          /* translators: Developer debugging message. 1: PHP function name, 2: WordPress version number, 3: Explanatory message. */
 648          $string = __( '%1$s (since %2$s; %3$s)' );
 649          $string = sprintf( $string, $function, $version, $message );
 650      } else {
 651          /* translators: Developer debugging message. 1: PHP function name, 2: Explanatory message. */
 652          $string = __( '%1$s (%2$s)' );
 653          $string = sprintf( $string, $function, $message );
 654      }
 655  
 656      header( sprintf( 'X-WP-DoingItWrong: %s', $string ) );
 657  }
 658  
 659  /**
 660   * Sends Cross-Origin Resource Sharing headers with API requests.
 661   *
 662   * @since 4.4.0
 663   *
 664   * @param mixed $value Response data.
 665   * @return mixed Response data.
 666   */
 667  function rest_send_cors_headers( $value ) {
 668      $origin = get_http_origin();
 669  
 670      if ( $origin ) {
 671          // Requests from file:// and data: URLs send "Origin: null".
 672          if ( 'null' !== $origin ) {
 673              $origin = esc_url_raw( $origin );
 674          }
 675          header( 'Access-Control-Allow-Origin: ' . $origin );
 676          header( 'Access-Control-Allow-Methods: OPTIONS, GET, POST, PUT, PATCH, DELETE' );
 677          header( 'Access-Control-Allow-Credentials: true' );
 678          header( 'Vary: Origin', false );
 679      } elseif ( ! headers_sent() && 'GET' === $_SERVER['REQUEST_METHOD'] && ! is_user_logged_in() ) {
 680          header( 'Vary: Origin', false );
 681      }
 682  
 683      return $value;
 684  }
 685  
 686  /**
 687   * Handles OPTIONS requests for the server.
 688   *
 689   * This is handled outside of the server code, as it doesn't obey normal route
 690   * mapping.
 691   *
 692   * @since 4.4.0
 693   *
 694   * @param mixed           $response Current response, either response or `null` to indicate pass-through.
 695   * @param WP_REST_Server  $handler  ResponseHandler instance (usually WP_REST_Server).
 696   * @param WP_REST_Request $request  The request that was used to make current response.
 697   * @return WP_REST_Response Modified response, either response or `null` to indicate pass-through.
 698   */
 699  function rest_handle_options_request( $response, $handler, $request ) {
 700      if ( ! empty( $response ) || $request->get_method() !== 'OPTIONS' ) {
 701          return $response;
 702      }
 703  
 704      $response = new WP_REST_Response();
 705      $data     = array();
 706  
 707      foreach ( $handler->get_routes() as $route => $endpoints ) {
 708          $match = preg_match( '@^' . $route . '$@i', $request->get_route(), $matches );
 709  
 710          if ( ! $match ) {
 711              continue;
 712          }
 713  
 714          $args = array();
 715          foreach ( $matches as $param => $value ) {
 716              if ( ! is_int( $param ) ) {
 717                  $args[ $param ] = $value;
 718              }
 719          }
 720  
 721          foreach ( $endpoints as $endpoint ) {
 722              // Remove the redundant preg_match() argument.
 723              unset( $args[0] );
 724  
 725              $request->set_url_params( $args );
 726              $request->set_attributes( $endpoint );
 727          }
 728  
 729          $data = $handler->get_data_for_route( $route, $endpoints, 'help' );
 730          $response->set_matched_route( $route );
 731          break;
 732      }
 733  
 734      $response->set_data( $data );
 735      return $response;
 736  }
 737  
 738  /**
 739   * Sends the "Allow" header to state all methods that can be sent to the current route.
 740   *
 741   * @since 4.4.0
 742   *
 743   * @param WP_REST_Response $response Current response being served.
 744   * @param WP_REST_Server   $server   ResponseHandler instance (usually WP_REST_Server).
 745   * @param WP_REST_Request  $request  The request that was used to make current response.
 746   * @return WP_REST_Response Response to be served, with "Allow" header if route has allowed methods.
 747   */
 748  function rest_send_allow_header( $response, $server, $request ) {
 749      $matched_route = $response->get_matched_route();
 750  
 751      if ( ! $matched_route ) {
 752          return $response;
 753      }
 754  
 755      $routes = $server->get_routes();
 756  
 757      $allowed_methods = array();
 758  
 759      // Get the allowed methods across the routes.
 760      foreach ( $routes[ $matched_route ] as $_handler ) {
 761          foreach ( $_handler['methods'] as $handler_method => $value ) {
 762  
 763              if ( ! empty( $_handler['permission_callback'] ) ) {
 764  
 765                  $permission = call_user_func( $_handler['permission_callback'], $request );
 766  
 767                  $allowed_methods[ $handler_method ] = true === $permission;
 768              } else {
 769                  $allowed_methods[ $handler_method ] = true;
 770              }
 771          }
 772      }
 773  
 774      // Strip out all the methods that are not allowed (false values).
 775      $allowed_methods = array_filter( $allowed_methods );
 776  
 777      if ( $allowed_methods ) {
 778          $response->header( 'Allow', implode( ', ', array_map( 'strtoupper', array_keys( $allowed_methods ) ) ) );
 779      }
 780  
 781      return $response;
 782  }
 783  
 784  /**
 785   * Recursively computes the intersection of arrays using keys for comparison.
 786   *
 787   * @since 5.3.0
 788   *
 789   * @param array $array1 The array with master keys to check.
 790   * @param array $array2 An array to compare keys against.
 791   * @return array An associative array containing all the entries of array1 which have keys
 792   *               that are present in all arguments.
 793   */
 794  function _rest_array_intersect_key_recursive( $array1, $array2 ) {
 795      $array1 = array_intersect_key( $array1, $array2 );
 796      foreach ( $array1 as $key => $value ) {
 797          if ( is_array( $value ) && is_array( $array2[ $key ] ) ) {
 798              $array1[ $key ] = _rest_array_intersect_key_recursive( $value, $array2[ $key ] );
 799          }
 800      }
 801      return $array1;
 802  }
 803  
 804  /**
 805   * Filters the REST API response to include only a white-listed set of response object fields.
 806   *
 807   * @since 4.8.0
 808   *
 809   * @param WP_REST_Response $response Current response being served.
 810   * @param WP_REST_Server   $server   ResponseHandler instance (usually WP_REST_Server).
 811   * @param WP_REST_Request  $request  The request that was used to make current response.
 812   * @return WP_REST_Response Response to be served, trimmed down to contain a subset of fields.
 813   */
 814  function rest_filter_response_fields( $response, $server, $request ) {
 815      if ( ! isset( $request['_fields'] ) || $response->is_error() ) {
 816          return $response;
 817      }
 818  
 819      $data = $response->get_data();
 820  
 821      $fields = wp_parse_list( $request['_fields'] );
 822  
 823      if ( 0 === count( $fields ) ) {
 824          return $response;
 825      }
 826  
 827      // Trim off outside whitespace from the comma delimited list.
 828      $fields = array_map( 'trim', $fields );
 829  
 830      // Create nested array of accepted field hierarchy.
 831      $fields_as_keyed = array();
 832      foreach ( $fields as $field ) {
 833          $parts = explode( '.', $field );
 834          $ref   = &$fields_as_keyed;
 835          while ( count( $parts ) > 1 ) {
 836              $next = array_shift( $parts );
 837              if ( isset( $ref[ $next ] ) && true === $ref[ $next ] ) {
 838                  // Skip any sub-properties if their parent prop is already marked for inclusion.
 839                  break 2;
 840              }
 841              $ref[ $next ] = isset( $ref[ $next ] ) ? $ref[ $next ] : array();
 842              $ref          = &$ref[ $next ];
 843          }
 844          $last         = array_shift( $parts );
 845          $ref[ $last ] = true;
 846      }
 847  
 848      if ( wp_is_numeric_array( $data ) ) {
 849          $new_data = array();
 850          foreach ( $data as $item ) {
 851              $new_data[] = _rest_array_intersect_key_recursive( $item, $fields_as_keyed );
 852          }
 853      } else {
 854          $new_data = _rest_array_intersect_key_recursive( $data, $fields_as_keyed );
 855      }
 856  
 857      $response->set_data( $new_data );
 858  
 859      return $response;
 860  }
 861  
 862  /**
 863   * Given an array of fields to include in a response, some of which may be
 864   * `nested.fields`, determine whether the provided field should be included
 865   * in the response body.
 866   *
 867   * If a parent field is passed in, the presence of any nested field within
 868   * that parent will cause the method to return `true`. For example "title"
 869   * will return true if any of `title`, `title.raw` or `title.rendered` is
 870   * provided.
 871   *
 872   * @since 5.3.0
 873   *
 874   * @param string $field  A field to test for inclusion in the response body.
 875   * @param array  $fields An array of string fields supported by the endpoint.
 876   * @return bool Whether to include the field or not.
 877   */
 878  function rest_is_field_included( $field, $fields ) {
 879      if ( in_array( $field, $fields, true ) ) {
 880          return true;
 881      }
 882  
 883      foreach ( $fields as $accepted_field ) {
 884          // Check to see if $field is the parent of any item in $fields.
 885          // A field "parent" should be accepted if "parent.child" is accepted.
 886          if ( strpos( $accepted_field, "$field." ) === 0 ) {
 887              return true;
 888          }
 889          // Conversely, if "parent" is accepted, all "parent.child" fields
 890          // should also be accepted.
 891          if ( strpos( $field, "$accepted_field." ) === 0 ) {
 892              return true;
 893          }
 894      }
 895  
 896      return false;
 897  }
 898  
 899  /**
 900   * Adds the REST API URL to the WP RSD endpoint.
 901   *
 902   * @since 4.4.0
 903   *
 904   * @see get_rest_url()
 905   */
 906  function rest_output_rsd() {
 907      $api_root = get_rest_url();
 908  
 909      if ( empty( $api_root ) ) {
 910          return;
 911      }
 912      ?>
 913      <api name="WP-API" blogID="1" preferred="false" apiLink="<?php echo esc_url( $api_root ); ?>" />
 914      <?php
 915  }
 916  
 917  /**
 918   * Outputs the REST API link tag into page header.
 919   *
 920   * @since 4.4.0
 921   *
 922   * @see get_rest_url()
 923   */
 924  function rest_output_link_wp_head() {
 925      $api_root = get_rest_url();
 926  
 927      if ( empty( $api_root ) ) {
 928          return;
 929      }
 930  
 931      printf( '<link rel="https://api.w.org/" href="%s" />', esc_url( $api_root ) );
 932  
 933      $resource = rest_get_queried_resource_route();
 934  
 935      if ( $resource ) {
 936          printf( '<link rel="alternate" type="application/json" href="%s" />', esc_url( rest_url( $resource ) ) );
 937      }
 938  }
 939  
 940  /**
 941   * Sends a Link header for the REST API.
 942   *
 943   * @since 4.4.0
 944   */
 945  function rest_output_link_header() {
 946      if ( headers_sent() ) {
 947          return;
 948      }
 949  
 950      $api_root = get_rest_url();
 951  
 952      if ( empty( $api_root ) ) {
 953          return;
 954      }
 955  
 956      header( sprintf( 'Link: <%s>; rel="https://api.w.org/"', esc_url_raw( $api_root ) ), false );
 957  
 958      $resource = rest_get_queried_resource_route();
 959  
 960      if ( $resource ) {
 961          header( sprintf( 'Link: <%s>; rel="alternate"; type="application/json"', esc_url_raw( rest_url( $resource ) ) ), false );
 962      }
 963  }
 964  
 965  /**
 966   * Checks for errors when using cookie-based authentication.
 967   *
 968   * WordPress' built-in cookie authentication is always active
 969   * for logged in users. However, the API has to check nonces
 970   * for each request to ensure users are not vulnerable to CSRF.
 971   *
 972   * @since 4.4.0
 973   *
 974   * @global mixed          $wp_rest_auth_cookie
 975   *
 976   * @param WP_Error|mixed $result Error from another authentication handler,
 977   *                               null if we should handle it, or another value if not.
 978   * @return WP_Error|mixed|bool WP_Error if the cookie is invalid, the $result, otherwise true.
 979   */
 980  function rest_cookie_check_errors( $result ) {
 981      if ( ! empty( $result ) ) {
 982          return $result;
 983      }
 984  
 985      global $wp_rest_auth_cookie;
 986  
 987      /*
 988       * Is cookie authentication being used? (If we get an auth
 989       * error, but we're still logged in, another authentication
 990       * must have been used).
 991       */
 992      if ( true !== $wp_rest_auth_cookie && is_user_logged_in() ) {
 993          return $result;
 994      }
 995  
 996      // Determine if there is a nonce.
 997      $nonce = null;
 998  
 999      if ( isset( $_REQUEST['_wpnonce'] ) ) {
1000          $nonce = $_REQUEST['_wpnonce'];
1001      } elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) {
1002          $nonce = $_SERVER['HTTP_X_WP_NONCE'];
1003      }
1004  
1005      if ( null === $nonce ) {
1006          // No nonce at all, so act as if it's an unauthenticated request.
1007          wp_set_current_user( 0 );
1008          return true;
1009      }
1010  
1011      // Check the nonce.
1012      $result = wp_verify_nonce( $nonce, 'wp_rest' );
1013  
1014      if ( ! $result ) {
1015          return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) );
1016      }
1017  
1018      // Send a refreshed nonce in header.
1019      rest_get_server()->send_header( 'X-WP-Nonce', wp_create_nonce( 'wp_rest' ) );
1020  
1021      return true;
1022  }
1023  
1024  /**
1025   * Collects cookie authentication status.
1026   *
1027   * Collects errors from wp_validate_auth_cookie for use by rest_cookie_check_errors.
1028   *
1029   * @since 4.4.0
1030   *
1031   * @see current_action()
1032   * @global mixed $wp_rest_auth_cookie
1033   */
1034  function rest_cookie_collect_status() {
1035      global $wp_rest_auth_cookie;
1036  
1037      $status_type = current_action();
1038  
1039      if ( 'auth_cookie_valid' !== $status_type ) {
1040          $wp_rest_auth_cookie = substr( $status_type, 12 );
1041          return;
1042      }
1043  
1044      $wp_rest_auth_cookie = true;
1045  }
1046  
1047  /**
1048   * Collects the status of authenticating with an application password.
1049   *
1050   * @since 5.6.0
1051   * @since 5.7.0 Added the `$app_password` parameter.
1052   *
1053   * @global WP_User|WP_Error|null $wp_rest_application_password_status
1054   * @global string|null $wp_rest_application_password_uuid
1055   *
1056   * @param WP_Error $user_or_error The authenticated user or error instance.
1057   * @param array    $app_password  The Application Password used to authenticate.
1058   */
1059  function rest_application_password_collect_status( $user_or_error, $app_password = array() ) {
1060      global $wp_rest_application_password_status, $wp_rest_application_password_uuid;
1061  
1062      $wp_rest_application_password_status = $user_or_error;
1063  
1064      if ( empty( $app_password['uuid'] ) ) {
1065          $wp_rest_application_password_uuid = null;
1066      } else {
1067          $wp_rest_application_password_uuid = $app_password['uuid'];
1068      }
1069  }
1070  
1071  /**
1072   * Gets the Application Password used for authenticating the request.
1073   *
1074   * @since 5.7.0
1075   *
1076   * @global string|null $wp_rest_application_password_uuid
1077   *
1078   * @return string|null The App Password UUID, or null if Application Passwords was not used.
1079   */
1080  function rest_get_authenticated_app_password() {
1081      global $wp_rest_application_password_uuid;
1082  
1083      return $wp_rest_application_password_uuid;
1084  }
1085  
1086  /**
1087   * Checks for errors when using application password-based authentication.
1088   *
1089   * @since 5.6.0
1090   *
1091   * @global WP_User|WP_Error|null $wp_rest_application_password_status
1092   *
1093   * @param WP_Error|null|true $result Error from another authentication handler,
1094   *                                   null if we should handle it, or another value if not.
1095   * @return WP_Error|null|true WP_Error if the application password is invalid, the $result, otherwise true.
1096   */
1097  function rest_application_password_check_errors( $result ) {
1098      global $wp_rest_application_password_status;
1099  
1100      if ( ! empty( $result ) ) {
1101          return $result;
1102      }
1103  
1104      if ( is_wp_error( $wp_rest_application_password_status ) ) {
1105          $data = $wp_rest_application_password_status->get_error_data();
1106  
1107          if ( ! isset( $data['status'] ) ) {
1108              $data['status'] = 401;
1109          }
1110  
1111          $wp_rest_application_password_status->add_data( $data );
1112  
1113          return $wp_rest_application_password_status;
1114      }
1115  
1116      if ( $wp_rest_application_password_status instanceof WP_User ) {
1117          return true;
1118      }
1119  
1120      return $result;
1121  }
1122  
1123  /**
1124   * Adds Application Passwords info to the REST API index.
1125   *
1126   * @since 5.6.0
1127   *
1128   * @param WP_REST_Response $response The index response object.
1129   * @return WP_REST_Response
1130   */
1131  function rest_add_application_passwords_to_index( $response ) {
1132      if ( ! wp_is_application_passwords_available() ) {
1133          return $response;
1134      }
1135  
1136      $response->data['authentication']['application-passwords'] = array(
1137          'endpoints' => array(
1138              'authorization' => admin_url( 'authorize-application.php' ),
1139          ),
1140      );
1141  
1142      return $response;
1143  }
1144  
1145  /**
1146   * Retrieves the avatar urls in various sizes.
1147   *
1148   * @since 4.7.0
1149   *
1150   * @see get_avatar_url()
1151   *
1152   * @param mixed $id_or_email The Gravatar to retrieve a URL for. Accepts a user_id, gravatar md5 hash,
1153   *                           user email, WP_User object, WP_Post object, or WP_Comment object.
1154   * @return array Avatar URLs keyed by size. Each value can be a URL string or boolean false.
1155   */
1156  function rest_get_avatar_urls( $id_or_email ) {
1157      $avatar_sizes = rest_get_avatar_sizes();
1158  
1159      $urls = array();
1160      foreach ( $avatar_sizes as $size ) {
1161          $urls[ $size ] = get_avatar_url( $id_or_email, array( 'size' => $size ) );
1162      }
1163  
1164      return $urls;
1165  }
1166  
1167  /**
1168   * Retrieves the pixel sizes for avatars.
1169   *
1170   * @since 4.7.0
1171   *
1172   * @return int[] List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`.
1173   */
1174  function rest_get_avatar_sizes() {
1175      /**
1176       * Filters the REST avatar sizes.
1177       *
1178       * Use this filter to adjust the array of sizes returned by the
1179       * `rest_get_avatar_sizes` function.
1180       *
1181       * @since 4.4.0
1182       *
1183       * @param int[] $sizes An array of int values that are the pixel sizes for avatars.
1184       *                     Default `[ 24, 48, 96 ]`.
1185       */
1186      return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
1187  }
1188  
1189  /**
1190   * Parses an RFC3339 time into a Unix timestamp.
1191   *
1192   * @since 4.4.0
1193   *
1194   * @param string $date      RFC3339 timestamp.
1195   * @param bool   $force_utc Optional. Whether to force UTC timezone instead of using
1196   *                          the timestamp's timezone. Default false.
1197   * @return int Unix timestamp.
1198   */
1199  function rest_parse_date( $date, $force_utc = false ) {
1200      if ( $force_utc ) {
1201          $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date );
1202      }
1203  
1204      $regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#';
1205  
1206      if ( ! preg_match( $regex, $date, $matches ) ) {
1207          return false;
1208      }
1209  
1210      return strtotime( $date );
1211  }
1212  
1213  /**
1214   * Parses a 3 or 6 digit hex color (with #).
1215   *
1216   * @since 5.4.0
1217   *
1218   * @param string $color 3 or 6 digit hex color (with #).
1219   * @return string|false
1220   */
1221  function rest_parse_hex_color( $color ) {
1222      $regex = '|^#([A-Fa-f0-9]{3}){1,2}$|';
1223      if ( ! preg_match( $regex, $color, $matches ) ) {
1224          return false;
1225      }
1226  
1227      return $color;
1228  }
1229  
1230  /**
1231   * Parses a date into both its local and UTC equivalent, in MySQL datetime format.
1232   *
1233   * @since 4.4.0
1234   *
1235   * @see rest_parse_date()
1236   *
1237   * @param string $date   RFC3339 timestamp.
1238   * @param bool   $is_utc Whether the provided date should be interpreted as UTC. Default false.
1239   * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s),
1240   *                    null on failure.
1241   */
1242  function rest_get_date_with_gmt( $date, $is_utc = false ) {
1243      /*
1244       * Whether or not the original date actually has a timezone string
1245       * changes the way we need to do timezone conversion.
1246       * Store this info before parsing the date, and use it later.
1247       */
1248      $has_timezone = preg_match( '#(Z|[+-]\d{2}(:\d{2})?)$#', $date );
1249  
1250      $date = rest_parse_date( $date );
1251  
1252      if ( empty( $date ) ) {
1253          return null;
1254      }
1255  
1256      /*
1257       * At this point $date could either be a local date (if we were passed
1258       * a *local* date without a timezone offset) or a UTC date (otherwise).
1259       * Timezone conversion needs to be handled differently between these two cases.
1260       */
1261      if ( ! $is_utc && ! $has_timezone ) {
1262          $local = gmdate( 'Y-m-d H:i:s', $date );
1263          $utc   = get_gmt_from_date( $local );
1264      } else {
1265          $utc   = gmdate( 'Y-m-d H:i:s', $date );
1266          $local = get_date_from_gmt( $utc );
1267      }
1268  
1269      return array( $local, $utc );
1270  }
1271  
1272  /**
1273   * Returns a contextual HTTP error code for authorization failure.
1274   *
1275   * @since 4.7.0
1276   *
1277   * @return int 401 if the user is not logged in, 403 if the user is logged in.
1278   */
1279  function rest_authorization_required_code() {
1280      return is_user_logged_in() ? 403 : 401;
1281  }
1282  
1283  /**
1284   * Validate a request argument based on details registered to the route.
1285   *
1286   * @since 4.7.0
1287   *
1288   * @param mixed           $value
1289   * @param WP_REST_Request $request
1290   * @param string          $param
1291   * @return true|WP_Error
1292   */
1293  function rest_validate_request_arg( $value, $request, $param ) {
1294      $attributes = $request->get_attributes();
1295      if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
1296          return true;
1297      }
1298      $args = $attributes['args'][ $param ];
1299  
1300      return rest_validate_value_from_schema( $value, $args, $param );
1301  }
1302  
1303  /**
1304   * Sanitize a request argument based on details registered to the route.
1305   *
1306   * @since 4.7.0
1307   *
1308   * @param mixed           $value
1309   * @param WP_REST_Request $request
1310   * @param string          $param
1311   * @return mixed
1312   */
1313  function rest_sanitize_request_arg( $value, $request, $param ) {
1314      $attributes = $request->get_attributes();
1315      if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
1316          return $value;
1317      }
1318      $args = $attributes['args'][ $param ];
1319  
1320      return rest_sanitize_value_from_schema( $value, $args, $param );
1321  }
1322  
1323  /**
1324   * Parse a request argument based on details registered to the route.
1325   *
1326   * Runs a validation check and sanitizes the value, primarily to be used via
1327   * the `sanitize_callback` arguments in the endpoint args registration.
1328   *
1329   * @since 4.7.0
1330   *
1331   * @param mixed           $value
1332   * @param WP_REST_Request $request
1333   * @param string          $param
1334   * @return mixed
1335   */
1336  function rest_parse_request_arg( $value, $request, $param ) {
1337      $is_valid = rest_validate_request_arg( $value, $request, $param );
1338  
1339      if ( is_wp_error( $is_valid ) ) {
1340          return $is_valid;
1341      }
1342  
1343      $value = rest_sanitize_request_arg( $value, $request, $param );
1344  
1345      return $value;
1346  }
1347  
1348  /**
1349   * Determines if an IP address is valid.
1350   *
1351   * Handles both IPv4 and IPv6 addresses.
1352   *
1353   * @since 4.7.0
1354   *
1355   * @param string $ip IP address.
1356   * @return string|false The valid IP address, otherwise false.
1357   */
1358  function rest_is_ip_address( $ip ) {
1359      $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]?)$/';
1360  
1361      if ( ! preg_match( $ipv4_pattern, $ip ) && ! Requests_IPv6::check_ipv6( $ip ) ) {
1362          return false;
1363      }
1364  
1365      return $ip;
1366  }
1367  
1368  /**
1369   * Changes a boolean-like value into the proper boolean value.
1370   *
1371   * @since 4.7.0
1372   *
1373   * @param bool|string|int $value The value being evaluated.
1374   * @return bool Returns the proper associated boolean value.
1375   */
1376  function rest_sanitize_boolean( $value ) {
1377      // String values are translated to `true`; make sure 'false' is false.
1378      if ( is_string( $value ) ) {
1379          $value = strtolower( $value );
1380          if ( in_array( $value, array( 'false', '0' ), true ) ) {
1381              $value = false;
1382          }
1383      }
1384  
1385      // Everything else will map nicely to boolean.
1386      return (bool) $value;
1387  }
1388  
1389  /**
1390   * Determines if a given value is boolean-like.
1391   *
1392   * @since 4.7.0
1393   *
1394   * @param bool|string $maybe_bool The value being evaluated.
1395   * @return bool True if a boolean, otherwise false.
1396   */
1397  function rest_is_boolean( $maybe_bool ) {
1398      if ( is_bool( $maybe_bool ) ) {
1399          return true;
1400      }
1401  
1402      if ( is_string( $maybe_bool ) ) {
1403          $maybe_bool = strtolower( $maybe_bool );
1404  
1405          $valid_boolean_values = array(
1406              'false',
1407              'true',
1408              '0',
1409              '1',
1410          );
1411  
1412          return in_array( $maybe_bool, $valid_boolean_values, true );
1413      }
1414  
1415      if ( is_int( $maybe_bool ) ) {
1416          return in_array( $maybe_bool, array( 0, 1 ), true );
1417      }
1418  
1419      return false;
1420  }
1421  
1422  /**
1423   * Determines if a given value is integer-like.
1424   *
1425   * @since 5.5.0
1426   *
1427   * @param mixed $maybe_integer The value being evaluated.
1428   * @return bool True if an integer, otherwise false.
1429   */
1430  function rest_is_integer( $maybe_integer ) {
1431      return is_numeric( $maybe_integer ) && round( (float) $maybe_integer ) === (float) $maybe_integer;
1432  }
1433  
1434  /**
1435   * Determines if a given value is array-like.
1436   *
1437   * @since 5.5.0
1438   *
1439   * @param mixed $maybe_array The value being evaluated.
1440   * @return bool
1441   */
1442  function rest_is_array( $maybe_array ) {
1443      if ( is_scalar( $maybe_array ) ) {
1444          $maybe_array = wp_parse_list( $maybe_array );
1445      }
1446  
1447      return wp_is_numeric_array( $maybe_array );
1448  }
1449  
1450  /**
1451   * Converts an array-like value to an array.
1452   *
1453   * @since 5.5.0
1454   *
1455   * @param mixed $maybe_array The value being evaluated.
1456   * @return array Returns the array extracted from the value.
1457   */
1458  function rest_sanitize_array( $maybe_array ) {
1459      if ( is_scalar( $maybe_array ) ) {
1460          return wp_parse_list( $maybe_array );
1461      }
1462  
1463      if ( ! is_array( $maybe_array ) ) {
1464          return array();
1465      }
1466  
1467      // Normalize to numeric array so nothing unexpected is in the keys.
1468      return array_values( $maybe_array );
1469  }
1470  
1471  /**
1472   * Determines if a given value is object-like.
1473   *
1474   * @since 5.5.0
1475   *
1476   * @param mixed $maybe_object The value being evaluated.
1477   * @return bool True if object like, otherwise false.
1478   */
1479  function rest_is_object( $maybe_object ) {
1480      if ( '' === $maybe_object ) {
1481          return true;
1482      }
1483  
1484      if ( $maybe_object instanceof stdClass ) {
1485          return true;
1486      }
1487  
1488      if ( $maybe_object instanceof JsonSerializable ) {
1489          $maybe_object = $maybe_object->jsonSerialize();
1490      }
1491  
1492      return is_array( $maybe_object );
1493  }
1494  
1495  /**
1496   * Converts an object-like value to an object.
1497   *
1498   * @since 5.5.0
1499   *
1500   * @param mixed $maybe_object The value being evaluated.
1501   * @return array Returns the object extracted from the value.
1502   */
1503  function rest_sanitize_object( $maybe_object ) {
1504      if ( '' === $maybe_object ) {
1505          return array();
1506      }
1507  
1508      if ( $maybe_object instanceof stdClass ) {
1509          return (array) $maybe_object;
1510      }
1511  
1512      if ( $maybe_object instanceof JsonSerializable ) {
1513          $maybe_object = $maybe_object->jsonSerialize();
1514      }
1515  
1516      if ( ! is_array( $maybe_object ) ) {
1517          return array();
1518      }
1519  
1520      return $maybe_object;
1521  }
1522  
1523  /**
1524   * Gets the best type for a value.
1525   *
1526   * @since 5.5.0
1527   *
1528   * @param mixed $value The value to check.
1529   * @param array $types The list of possible types.
1530   * @return string The best matching type, an empty string if no types match.
1531   */
1532  function rest_get_best_type_for_value( $value, $types ) {
1533      static $checks = array(
1534          'array'   => 'rest_is_array',
1535          'object'  => 'rest_is_object',
1536          'integer' => 'rest_is_integer',
1537          'number'  => 'is_numeric',
1538          'boolean' => 'rest_is_boolean',
1539          'string'  => 'is_string',
1540          'null'    => 'is_null',
1541      );
1542  
1543      // Both arrays and objects allow empty strings to be converted to their types.
1544      // But the best answer for this type is a string.
1545      if ( '' === $value && in_array( 'string', $types, true ) ) {
1546          return 'string';
1547      }
1548  
1549      foreach ( $types as $type ) {
1550          if ( isset( $checks[ $type ] ) && $checks[ $type ]( $value ) ) {
1551              return $type;
1552          }
1553      }
1554  
1555      return '';
1556  }
1557  
1558  /**
1559   * Handles getting the best type for a multi-type schema.
1560   *
1561   * This is a wrapper for {@see rest_get_best_type_for_value()} that handles
1562   * backward compatibility for schemas that use invalid types.
1563   *
1564   * @since 5.5.0
1565   *
1566   * @param mixed  $value The value to check.
1567   * @param array  $args  The schema array to use.
1568   * @param string $param The parameter name, used in error messages.
1569   * @return string
1570   */
1571  function rest_handle_multi_type_schema( $value, $args, $param = '' ) {
1572      $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
1573      $invalid_types = array_diff( $args['type'], $allowed_types );
1574  
1575      if ( $invalid_types ) {
1576          _doing_it_wrong(
1577              __FUNCTION__,
1578              /* translators: 1: Parameter, 2: List of allowed types. */
1579              wp_sprintf( __( 'The "type" schema keyword for %1$s can only contain the built-in types: %2$l.' ), $param, $allowed_types ),
1580              '5.5.0'
1581          );
1582      }
1583  
1584      $best_type = rest_get_best_type_for_value( $value, $args['type'] );
1585  
1586      if ( ! $best_type ) {
1587          if ( ! $invalid_types ) {
1588              return '';
1589          }
1590  
1591          // Backward compatibility for previous behavior which allowed the value if there was an invalid type used.
1592          $best_type = reset( $invalid_types );
1593      }
1594  
1595      return $best_type;
1596  }
1597  
1598  /**
1599   * Checks if an array is made up of unique items.
1600   *
1601   * @since 5.5.0
1602   *
1603   * @param array $array The array to check.
1604   * @return bool True if the array contains unique items, false otherwise.
1605   */
1606  function rest_validate_array_contains_unique_items( $array ) {
1607      $seen = array();
1608  
1609      foreach ( $array as $item ) {
1610          $stabilized = rest_stabilize_value( $item );
1611          $key        = serialize( $stabilized );
1612  
1613          if ( ! isset( $seen[ $key ] ) ) {
1614              $seen[ $key ] = true;
1615  
1616              continue;
1617          }
1618  
1619          return false;
1620      }
1621  
1622      return true;
1623  }
1624  
1625  /**
1626   * Stabilizes a value following JSON Schema semantics.
1627   *
1628   * For lists, order is preserved. For objects, properties are reordered alphabetically.
1629   *
1630   * @since 5.5.0
1631   *
1632   * @param mixed $value The value to stabilize. Must already be sanitized. Objects should have been converted to arrays.
1633   * @return mixed The stabilized value.
1634   */
1635  function rest_stabilize_value( $value ) {
1636      if ( is_scalar( $value ) || is_null( $value ) ) {
1637          return $value;
1638      }
1639  
1640      if ( is_object( $value ) ) {
1641          _doing_it_wrong( __FUNCTION__, __( 'Cannot stabilize objects. Convert the object to an array first.' ), '5.5.0' );
1642  
1643          return $value;
1644      }
1645  
1646      ksort( $value );
1647  
1648      foreach ( $value as $k => $v ) {
1649          $value[ $k ] = rest_stabilize_value( $v );
1650      }
1651  
1652      return $value;
1653  }
1654  
1655  /**
1656   * Validates if the JSON Schema pattern matches a value.
1657   *
1658   * @since 5.6.0
1659   *
1660   * @param string $pattern The pattern to match against.
1661   * @param string $value   The value to check.
1662   * @return bool           True if the pattern matches the given value, false otherwise.
1663   */
1664  function rest_validate_json_schema_pattern( $pattern, $value ) {
1665      $escaped_pattern = str_replace( '#', '\\#', $pattern );
1666  
1667      return 1 === preg_match( '#' . $escaped_pattern . '#u', $value );
1668  }
1669  
1670  /**
1671   * Finds the schema for a property using the patternProperties keyword.
1672   *
1673   * @since 5.6.0
1674   *
1675   * @param string $property The property name to check.
1676   * @param array  $args     The schema array to use.
1677   * @return array|null      The schema of matching pattern property, or null if no patterns match.
1678   */
1679  function rest_find_matching_pattern_property_schema( $property, $args ) {
1680      if ( isset( $args['patternProperties'] ) ) {
1681          foreach ( $args['patternProperties'] as $pattern => $child_schema ) {
1682              if ( rest_validate_json_schema_pattern( $pattern, $property ) ) {
1683                  return $child_schema;
1684              }
1685          }
1686      }
1687  
1688      return null;
1689  }
1690  
1691  /**
1692   * Formats a combining operation error into a WP_Error object.
1693   *
1694   * @since 5.6.0
1695   *
1696   * @param string $param The parameter name.
1697   * @param array $error  The error details.
1698   * @return WP_Error
1699   */
1700  function rest_format_combining_operation_error( $param, $error ) {
1701      $position = $error['index'];
1702      $reason   = $error['error_object']->get_error_message();
1703  
1704      if ( isset( $error['schema']['title'] ) ) {
1705          $title = $error['schema']['title'];
1706  
1707          return new WP_Error(
1708              'rest_no_matching_schema',
1709              /* translators: 1: Parameter, 2: Schema title, 3: Reason. */
1710              sprintf( __( '%1$s is not a valid %2$s. Reason: %3$s' ), $param, $title, $reason ),
1711              array( 'position' => $position )
1712          );
1713      }
1714  
1715      return new WP_Error(
1716          'rest_no_matching_schema',
1717          /* translators: 1: Parameter, 2: Reason. */
1718          sprintf( __( '%1$s does not match the expected format. Reason: %2$s' ), $param, $reason ),
1719          array( 'position' => $position )
1720      );
1721  }
1722  
1723  /**
1724   * Gets the error of combining operation.
1725   *
1726   * @since 5.6.0
1727   *
1728   * @param array  $value  The value to validate.
1729   * @param string $param  The parameter name, used in error messages.
1730   * @param array  $errors The errors array, to search for possible error.
1731   * @return WP_Error      The combining operation error.
1732   */
1733  function rest_get_combining_operation_error( $value, $param, $errors ) {
1734      // If there is only one error, simply return it.
1735      if ( 1 === count( $errors ) ) {
1736          return rest_format_combining_operation_error( $param, $errors[0] );
1737      }
1738  
1739      // Filter out all errors related to type validation.
1740      $filtered_errors = array();
1741      foreach ( $errors as $error ) {
1742          $error_code = $error['error_object']->get_error_code();
1743          $error_data = $error['error_object']->get_error_data();
1744  
1745          if ( 'rest_invalid_type' !== $error_code || ( isset( $error_data['param'] ) && $param !== $error_data['param'] ) ) {
1746              $filtered_errors[] = $error;
1747          }
1748      }
1749  
1750      // If there is only one error left, simply return it.
1751      if ( 1 === count( $filtered_errors ) ) {
1752          return rest_format_combining_operation_error( $param, $filtered_errors[0] );
1753      }
1754  
1755      // If there are only errors related to object validation, try choosing the most appropriate one.
1756      if ( count( $filtered_errors ) > 1 && 'object' === $filtered_errors[0]['schema']['type'] ) {
1757          $result = null;
1758          $number = 0;
1759  
1760          foreach ( $filtered_errors as $error ) {
1761              if ( isset( $error['schema']['properties'] ) ) {
1762                  $n = count( array_intersect_key( $error['schema']['properties'], $value ) );
1763                  if ( $n > $number ) {
1764                      $result = $error;
1765                      $number = $n;
1766                  }
1767              }
1768          }
1769  
1770          if ( null !== $result ) {
1771              return rest_format_combining_operation_error( $param, $result );
1772          }
1773      }
1774  
1775      // If each schema has a title, include those titles in the error message.
1776      $schema_titles = array();
1777      foreach ( $errors as $error ) {
1778          if ( isset( $error['schema']['title'] ) ) {
1779              $schema_titles[] = $error['schema']['title'];
1780          }
1781      }
1782  
1783      if ( count( $schema_titles ) === count( $errors ) ) {
1784          /* translators: 1: Parameter, 2: Schema titles. */
1785          return new WP_Error( 'rest_no_matching_schema', wp_sprintf( __( '%1$s is not a valid %2$l.' ), $param, $schema_titles ) );
1786      }
1787  
1788      /* translators: %s: Parameter. */
1789      return new WP_Error( 'rest_no_matching_schema', sprintf( __( '%s does not match any of the expected formats.' ), $param ) );
1790  }
1791  
1792  /**
1793   * Finds the matching schema among the "anyOf" schemas.
1794   *
1795   * @since 5.6.0
1796   *
1797   * @param mixed  $value   The value to validate.
1798   * @param array  $args    The schema array to use.
1799   * @param string $param   The parameter name, used in error messages.
1800   * @return array|WP_Error The matching schema or WP_Error instance if all schemas do not match.
1801   */
1802  function rest_find_any_matching_schema( $value, $args, $param ) {
1803      $errors = array();
1804  
1805      foreach ( $args['anyOf'] as $index => $schema ) {
1806          if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) {
1807              $schema['type'] = $args['type'];
1808          }
1809  
1810          $is_valid = rest_validate_value_from_schema( $value, $schema, $param );
1811          if ( ! is_wp_error( $is_valid ) ) {
1812              return $schema;
1813          }
1814  
1815          $errors[] = array(
1816              'error_object' => $is_valid,
1817              'schema'       => $schema,
1818              'index'        => $index,
1819          );
1820      }
1821  
1822      return rest_get_combining_operation_error( $value, $param, $errors );
1823  }
1824  
1825  /**
1826   * Finds the matching schema among the "oneOf" schemas.
1827   *
1828   * @since 5.6.0
1829   *
1830   * @param mixed  $value                  The value to validate.
1831   * @param array  $args                   The schema array to use.
1832   * @param string $param                  The parameter name, used in error messages.
1833   * @param bool   $stop_after_first_match Optional. Whether the process should stop after the first successful match.
1834   * @return array|WP_Error                The matching schema or WP_Error instance if the number of matching schemas is not equal to one.
1835   */
1836  function rest_find_one_matching_schema( $value, $args, $param, $stop_after_first_match = false ) {
1837      $matching_schemas = array();
1838      $errors           = array();
1839  
1840      foreach ( $args['oneOf'] as $index => $schema ) {
1841          if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) {
1842              $schema['type'] = $args['type'];
1843          }
1844  
1845          $is_valid = rest_validate_value_from_schema( $value, $schema, $param );
1846          if ( ! is_wp_error( $is_valid ) ) {
1847              if ( $stop_after_first_match ) {
1848                  return $schema;
1849              }
1850  
1851              $matching_schemas[] = array(
1852                  'schema_object' => $schema,
1853                  'index'         => $index,
1854              );
1855          } else {
1856              $errors[] = array(
1857                  'error_object' => $is_valid,
1858                  'schema'       => $schema,
1859                  'index'        => $index,
1860              );
1861          }
1862      }
1863  
1864      if ( ! $matching_schemas ) {
1865          return rest_get_combining_operation_error( $value, $param, $errors );
1866      }
1867  
1868      if ( count( $matching_schemas ) > 1 ) {
1869          $schema_positions = array();
1870          $schema_titles    = array();
1871  
1872          foreach ( $matching_schemas as $schema ) {
1873              $schema_positions[] = $schema['index'];
1874  
1875              if ( isset( $schema['schema_object']['title'] ) ) {
1876                  $schema_titles[] = $schema['schema_object']['title'];
1877              }
1878          }
1879  
1880          // If each schema has a title, include those titles in the error message.
1881          if ( count( $schema_titles ) === count( $matching_schemas ) ) {
1882              return new WP_Error(
1883                  'rest_one_of_multiple_matches',
1884                  /* translators: 1: Parameter, 2: Schema titles. */
1885                  wp_sprintf( __( '%1$s matches %2$l, but should match only one.' ), $param, $schema_titles ),
1886                  array( 'positions' => $schema_positions )
1887              );
1888          }
1889  
1890          return new WP_Error(
1891              'rest_one_of_multiple_matches',
1892              /* translators: %s: Parameter. */
1893              sprintf( __( '%s matches more than one of the expected formats.' ), $param ),
1894              array( 'positions' => $schema_positions )
1895          );
1896      }
1897  
1898      return $matching_schemas[0]['schema_object'];
1899  }
1900  
1901  /**
1902   * Checks the equality of two values, following JSON Schema semantics.
1903   *
1904   * Property order is ignored for objects.
1905   *
1906   * Values must have been previously sanitized/coerced to their native types.
1907   *
1908   * @since 5.7.0
1909   *
1910   * @param mixed $value1 The first value to check.
1911   * @param mixed $value2 The second value to check.
1912   * @return bool True if the values are equal or false otherwise.
1913   */
1914  function rest_are_values_equal( $value1, $value2 ) {
1915      if ( is_array( $value1 ) && is_array( $value2 ) ) {
1916          if ( count( $value1 ) !== count( $value2 ) ) {
1917              return false;
1918          }
1919  
1920          foreach ( $value1 as $index => $value ) {
1921              if ( ! array_key_exists( $index, $value2 ) || ! rest_are_values_equal( $value, $value2[ $index ] ) ) {
1922                  return false;
1923              }
1924          }
1925  
1926          return true;
1927      }
1928  
1929      if ( is_int( $value1 ) && is_float( $value2 )
1930          || is_float( $value1 ) && is_int( $value2 )
1931      ) {
1932          return (float) $value1 === (float) $value2;
1933      }
1934  
1935      return $value1 === $value2;
1936  }
1937  
1938  /**
1939   * Validates that the given value is a member of the JSON Schema "enum".
1940   *
1941   * @since 5.7.0
1942   *
1943   * @param mixed  $value  The value to validate.
1944   * @param array  $args   The schema array to use.
1945   * @param string $param  The parameter name, used in error messages.
1946   * @return true|WP_Error True if the "enum" contains the value or a WP_Error instance otherwise.
1947   */
1948  function rest_validate_enum( $value, $args, $param ) {
1949      $sanitized_value = rest_sanitize_value_from_schema( $value, $args, $param );
1950      if ( is_wp_error( $sanitized_value ) ) {
1951          return $sanitized_value;
1952      }
1953  
1954      foreach ( $args['enum'] as $enum_value ) {
1955          if ( rest_are_values_equal( $sanitized_value, $enum_value ) ) {
1956              return true;
1957          }
1958      }
1959  
1960      $encoded_enum_values = array();
1961      foreach ( $args['enum'] as $enum_value ) {
1962          $encoded_enum_values[] = is_scalar( $enum_value ) ? $enum_value : wp_json_encode( $enum_value );
1963      }
1964  
1965      if ( count( $encoded_enum_values ) === 1 ) {
1966          /* translators: 1: Parameter, 2: Valid values. */
1967          return new WP_Error( 'rest_not_in_enum', wp_sprintf( __( '%1$s is not %2$s.' ), $param, $encoded_enum_values[0] ) );
1968      }
1969  
1970      /* translators: 1: Parameter, 2: List of valid values. */
1971      return new WP_Error( 'rest_not_in_enum', wp_sprintf( __( '%1$s is not one of %2$l.' ), $param, $encoded_enum_values ) );
1972  }
1973  
1974  /**
1975   * Get all valid JSON schema properties.
1976   *
1977   * @since 5.6.0
1978   *
1979   * @return string[] All valid JSON schema properties.
1980   */
1981  function rest_get_allowed_schema_keywords() {
1982      return array(
1983          'title',
1984          'description',
1985          'default',
1986          'type',
1987          'format',
1988          'enum',
1989          'items',
1990          'properties',
1991          'additionalProperties',
1992          'patternProperties',
1993          'minProperties',
1994          'maxProperties',
1995          'minimum',
1996          'maximum',
1997          'exclusiveMinimum',
1998          'exclusiveMaximum',
1999          'multipleOf',
2000          'minLength',
2001          'maxLength',
2002          'pattern',
2003          'minItems',
2004          'maxItems',
2005          'uniqueItems',
2006          'anyOf',
2007          'oneOf',
2008      );
2009  }
2010  
2011  /**
2012   * Validate a value based on a schema.
2013   *
2014   * @since 4.7.0
2015   * @since 4.9.0 Support the "object" type.
2016   * @since 5.2.0 Support validating "additionalProperties" against a schema.
2017   * @since 5.3.0 Support multiple types.
2018   * @since 5.4.0 Convert an empty string to an empty object.
2019   * @since 5.5.0 Add the "uuid" and "hex-color" formats.
2020   *              Support the "minLength", "maxLength" and "pattern" keywords for strings.
2021   *              Support the "minItems", "maxItems" and "uniqueItems" keywords for arrays.
2022   *              Validate required properties.
2023   * @since 5.6.0 Support the "minProperties" and "maxProperties" keywords for objects.
2024   *              Support the "multipleOf" keyword for numbers and integers.
2025   *              Support the "patternProperties" keyword for objects.
2026   *              Support the "anyOf" and "oneOf" keywords.
2027   *
2028   * @param mixed  $value The value to validate.
2029   * @param array  $args  Schema array to use for validation.
2030   * @param string $param The parameter name, used in error messages.
2031   * @return true|WP_Error
2032   */
2033  function rest_validate_value_from_schema( $value, $args, $param = '' ) {
2034      if ( isset( $args['anyOf'] ) ) {
2035          $matching_schema = rest_find_any_matching_schema( $value, $args, $param );
2036          if ( is_wp_error( $matching_schema ) ) {
2037              return $matching_schema;
2038          }
2039  
2040          if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) {
2041              $args['type'] = $matching_schema['type'];
2042          }
2043      }
2044  
2045      if ( isset( $args['oneOf'] ) ) {
2046          $matching_schema = rest_find_one_matching_schema( $value, $args, $param );
2047          if ( is_wp_error( $matching_schema ) ) {
2048              return $matching_schema;
2049          }
2050  
2051          if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) {
2052              $args['type'] = $matching_schema['type'];
2053          }
2054      }
2055  
2056      $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
2057  
2058      if ( ! isset( $args['type'] ) ) {
2059          /* translators: %s: Parameter. */
2060          _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' );
2061      }
2062  
2063      if ( is_array( $args['type'] ) ) {
2064          $best_type = rest_handle_multi_type_schema( $value, $args, $param );
2065  
2066          if ( ! $best_type ) {
2067              return new WP_Error(
2068                  'rest_invalid_type',
2069                  /* translators: 1: Parameter, 2: List of types. */
2070                  sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ),
2071                  array( 'param' => $param )
2072              );
2073          }
2074  
2075          $args['type'] = $best_type;
2076      }
2077  
2078      if ( ! in_array( $args['type'], $allowed_types, true ) ) {
2079          _doing_it_wrong(
2080              __FUNCTION__,
2081              /* translators: 1: Parameter, 2: The list of allowed types. */
2082              wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ),
2083              '5.5.0'
2084          );
2085      }
2086  
2087      switch ( $args['type'] ) {
2088          case 'null':
2089              $is_valid = rest_validate_null_value_from_schema( $value, $param );
2090              break;
2091          case 'boolean':
2092              $is_valid = rest_validate_boolean_value_from_schema( $value, $param );
2093              break;
2094          case 'object':
2095              $is_valid = rest_validate_object_value_from_schema( $value, $args, $param );
2096              break;
2097          case 'array':
2098              $is_valid = rest_validate_array_value_from_schema( $value, $args, $param );
2099              break;
2100          case 'number':
2101              $is_valid = rest_validate_number_value_from_schema( $value, $args, $param );
2102              break;
2103          case 'string':
2104              $is_valid = rest_validate_string_value_from_schema( $value, $args, $param );
2105              break;
2106          case 'integer':
2107              $is_valid = rest_validate_integer_value_from_schema( $value, $args, $param );
2108              break;
2109          default:
2110              $is_valid = true;
2111              break;
2112      }
2113  
2114      if ( is_wp_error( $is_valid ) ) {
2115          return $is_valid;
2116      }
2117  
2118      if ( ! empty( $args['enum'] ) ) {
2119          $enum_contains_value = rest_validate_enum( $value, $args, $param );
2120          if ( is_wp_error( $enum_contains_value ) ) {
2121              return $enum_contains_value;
2122          }
2123      }
2124  
2125      // The "format" keyword should only be applied to strings. However, for backward compatibility,
2126      // we allow the "format" keyword if the type keyword was not specified, or was set to an invalid value.
2127      if ( isset( $args['format'] )
2128          && ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) )
2129      ) {
2130          switch ( $args['format'] ) {
2131              case 'hex-color':
2132                  if ( ! rest_parse_hex_color( $value ) ) {
2133                      return new WP_Error( 'rest_invalid_hex_color', __( 'Invalid hex color.' ) );
2134                  }
2135                  break;
2136  
2137              case 'date-time':
2138                  if ( ! rest_parse_date( $value ) ) {
2139                      return new WP_Error( 'rest_invalid_date', __( 'Invalid date.' ) );
2140                  }
2141                  break;
2142  
2143              case 'email':
2144                  if ( ! is_email( $value ) ) {
2145                      return new WP_Error( 'rest_invalid_email', __( 'Invalid email address.' ) );
2146                  }
2147                  break;
2148              case 'ip':
2149                  if ( ! rest_is_ip_address( $value ) ) {
2150                      /* translators: %s: IP address. */
2151                      return new WP_Error( 'rest_invalid_ip', sprintf( __( '%s is not a valid IP address.' ), $param ) );
2152                  }
2153                  break;
2154              case 'uuid':
2155                  if ( ! wp_is_uuid( $value ) ) {
2156                      /* translators: %s: The name of a JSON field expecting a valid UUID. */
2157                      return new WP_Error( 'rest_invalid_uuid', sprintf( __( '%s is not a valid UUID.' ), $param ) );
2158                  }
2159                  break;
2160          }
2161      }
2162  
2163      return true;
2164  }
2165  
2166  /**
2167   * Validates a null value based on a schema.
2168   *
2169   * @since 5.7.0
2170   *
2171   * @param mixed  $value The value to validate.
2172   * @param string $param The parameter name, used in error messages.
2173   * @return true|WP_Error
2174   */
2175  function rest_validate_null_value_from_schema( $value, $param ) {
2176      if ( null !== $value ) {
2177          return new WP_Error(
2178              'rest_invalid_type',
2179              /* translators: 1: Parameter, 2: Type name. */
2180              sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ),
2181              array( 'param' => $param )
2182          );
2183      }
2184  
2185      return true;
2186  }
2187  
2188  /**
2189   * Validates a boolean value based on a schema.
2190   *
2191   * @since 5.7.0
2192   *
2193   * @param mixed  $value The value to validate.
2194   * @param string $param The parameter name, used in error messages.
2195   * @return true|WP_Error
2196   */
2197  function rest_validate_boolean_value_from_schema( $value, $param ) {
2198      if ( ! rest_is_boolean( $value ) ) {
2199          return new WP_Error(
2200              'rest_invalid_type',
2201              /* translators: 1: Parameter, 2: Type name. */
2202              sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ),
2203              array( 'param' => $param )
2204          );
2205      }
2206  
2207      return true;
2208  }
2209  
2210  /**
2211   * Validates an object value based on a schema.
2212   *
2213   * @since 5.7.0
2214   *
2215   * @param mixed  $value The value to validate.
2216   * @param array  $args  Schema array to use for validation.
2217   * @param string $param The parameter name, used in error messages.
2218   * @return true|WP_Error
2219   */
2220  function rest_validate_object_value_from_schema( $value, $args, $param ) {
2221      if ( ! rest_is_object( $value ) ) {
2222          return new WP_Error(
2223              'rest_invalid_type',
2224              /* translators: 1: Parameter, 2: Type name. */
2225              sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ),
2226              array( 'param' => $param )
2227          );
2228      }
2229  
2230      $value = rest_sanitize_object( $value );
2231  
2232      if ( isset( $args['required'] ) && is_array( $args['required'] ) ) { // schema version 4
2233          foreach ( $args['required'] as $name ) {
2234              if ( ! array_key_exists( $name, $value ) ) {
2235                  return new WP_Error(
2236                      'rest_property_required',
2237                      /* translators: 1: Property of an object, 2: Parameter. */
2238                      sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param )
2239                  );
2240              }
2241          }
2242      } elseif ( isset( $args['properties'] ) ) { // schema version 3
2243          foreach ( $args['properties'] as $name => $property ) {
2244              if ( isset( $property['required'] ) && true === $property['required'] && ! array_key_exists( $name, $value ) ) {
2245                  return new WP_Error(
2246                      'rest_property_required',
2247                      /* translators: 1: Property of an object, 2: Parameter. */
2248                      sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param )
2249                  );
2250              }
2251          }
2252      }
2253  
2254      foreach ( $value as $property => $v ) {
2255          if ( isset( $args['properties'][ $property ] ) ) {
2256              $is_valid = rest_validate_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' );
2257              if ( is_wp_error( $is_valid ) ) {
2258                  return $is_valid;
2259              }
2260              continue;
2261          }
2262  
2263          $pattern_property_schema = rest_find_matching_pattern_property_schema( $property, $args );
2264          if ( null !== $pattern_property_schema ) {
2265              $is_valid = rest_validate_value_from_schema( $v, $pattern_property_schema, $param . '[' . $property . ']' );
2266              if ( is_wp_error( $is_valid ) ) {
2267                  return $is_valid;
2268              }
2269              continue;
2270          }
2271  
2272          if ( isset( $args['additionalProperties'] ) ) {
2273              if ( false === $args['additionalProperties'] ) {
2274                  return new WP_Error(
2275                      'rest_additional_properties_forbidden',
2276                      /* translators: %s: Property of an object. */
2277                      sprintf( __( '%1$s is not a valid property of Object.' ), $property )
2278                  );
2279              }
2280  
2281              if ( is_array( $args['additionalProperties'] ) ) {
2282                  $is_valid = rest_validate_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' );
2283                  if ( is_wp_error( $is_valid ) ) {
2284                      return $is_valid;
2285                  }
2286              }
2287          }
2288      }
2289  
2290      if ( isset( $args['minProperties'] ) && count( $value ) < $args['minProperties'] ) {
2291          return new WP_Error(
2292              'rest_too_few_properties',
2293              sprintf(
2294                  /* translators: 1: Parameter, 2: Number. */
2295                  _n(
2296                      '%1$s must contain at least %2$s property.',
2297                      '%1$s must contain at least %2$s properties.',
2298                      $args['minProperties']
2299                  ),
2300                  $param,
2301                  number_format_i18n( $args['minProperties'] )
2302              )
2303          );
2304      }
2305  
2306      if ( isset( $args['maxProperties'] ) && count( $value ) > $args['maxProperties'] ) {
2307          return new WP_Error(
2308              'rest_too_many_properties',
2309              sprintf(
2310                  /* translators: 1: Parameter, 2: Number. */
2311                  _n(
2312                      '%1$s must contain at most %2$s property.',
2313                      '%1$s must contain at most %2$s properties.',
2314                      $args['maxProperties']
2315                  ),
2316                  $param,
2317                  number_format_i18n( $args['maxProperties'] )
2318              )
2319          );
2320      }
2321  
2322      return true;
2323  }
2324  
2325  /**
2326   * Validates an array value based on a schema.
2327   *
2328   * @since 5.7.0
2329   *
2330   * @param mixed  $value The value to validate.
2331   * @param array  $args  Schema array to use for validation.
2332   * @param string $param The parameter name, used in error messages.
2333   * @return true|WP_Error
2334   */
2335  function rest_validate_array_value_from_schema( $value, $args, $param ) {
2336      if ( ! rest_is_array( $value ) ) {
2337          return new WP_Error(
2338              'rest_invalid_type',
2339              /* translators: 1: Parameter, 2: Type name. */
2340              sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ),
2341              array( 'param' => $param )
2342          );
2343      }
2344  
2345      $value = rest_sanitize_array( $value );
2346  
2347      if ( isset( $args['items'] ) ) {
2348          foreach ( $value as $index => $v ) {
2349              $is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
2350              if ( is_wp_error( $is_valid ) ) {
2351                  return $is_valid;
2352              }
2353          }
2354      }
2355  
2356      if ( isset( $args['minItems'] ) && count( $value ) < $args['minItems'] ) {
2357          return new WP_Error(
2358              'rest_too_few_items',
2359              sprintf(
2360                  /* translators: 1: Parameter, 2: Number. */
2361                  _n(
2362                      '%1$s must contain at least %2$s item.',
2363                      '%1$s must contain at least %2$s items.',
2364                      $args['minItems']
2365                  ),
2366                  $param,
2367                  number_format_i18n( $args['minItems'] )
2368              )
2369          );
2370      }
2371  
2372      if ( isset( $args['maxItems'] ) && count( $value ) > $args['maxItems'] ) {
2373          return new WP_Error(
2374              'rest_too_many_items',
2375              sprintf(
2376                  /* translators: 1: Parameter, 2: Number. */
2377                  _n(
2378                      '%1$s must contain at most %2$s item.',
2379                      '%1$s must contain at most %2$s items.',
2380                      $args['maxItems']
2381                  ),
2382                  $param,
2383                  number_format_i18n( $args['maxItems'] )
2384              )
2385          );
2386      }
2387  
2388      if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) {
2389          /* translators: %s: Parameter. */
2390          return new WP_Error( 'rest_duplicate_items', sprintf( __( '%s has duplicate items.' ), $param ) );
2391      }
2392  
2393      return true;
2394  }
2395  
2396  /**
2397   * Validates a number value based on a schema.
2398   *
2399   * @since 5.7.0
2400   *
2401   * @param mixed  $value The value to validate.
2402   * @param array  $args  Schema array to use for validation.
2403   * @param string $param The parameter name, used in error messages.
2404   * @return true|WP_Error
2405   */
2406  function rest_validate_number_value_from_schema( $value, $args, $param ) {
2407      if ( ! is_numeric( $value ) ) {
2408          return new WP_Error(
2409              'rest_invalid_type',
2410              /* translators: 1: Parameter, 2: Type name. */
2411              sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ),
2412              array( 'param' => $param )
2413          );
2414      }
2415  
2416      if ( isset( $args['multipleOf'] ) && fmod( $value, $args['multipleOf'] ) !== 0.0 ) {
2417          return new WP_Error(
2418              'rest_invalid_multiple',
2419              /* translators: 1: Parameter, 2: Multiplier. */
2420              sprintf( __( '%1$s must be a multiple of %2$s.' ), $param, $args['multipleOf'] )
2421          );
2422      }
2423  
2424      if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) {
2425          if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) {
2426              return new WP_Error(
2427                  'rest_out_of_bounds',
2428                  /* translators: 1: Parameter, 2: Minimum number. */
2429                  sprintf( __( '%1$s must be greater than %2$d' ), $param, $args['minimum'] )
2430              );
2431          }
2432  
2433          if ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) {
2434              return new WP_Error(
2435                  'rest_out_of_bounds',
2436                  /* translators: 1: Parameter, 2: Minimum number. */
2437                  sprintf( __( '%1$s must be greater than or equal to %2$d' ), $param, $args['minimum'] )
2438              );
2439          }
2440      }
2441  
2442      if ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) {
2443          if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) {
2444              return new WP_Error(
2445                  'rest_out_of_bounds',
2446                  /* translators: 1: Parameter, 2: Maximum number. */
2447                  sprintf( __( '%1$s must be less than %2$d' ), $param, $args['maximum'] )
2448              );
2449          }
2450  
2451          if ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) {
2452              return new WP_Error(
2453                  'rest_out_of_bounds',
2454                  /* translators: 1: Parameter, 2: Maximum number. */
2455                  sprintf( __( '%1$s must be less than or equal to %2$d' ), $param, $args['maximum'] )
2456              );
2457          }
2458      }
2459  
2460      if ( isset( $args['minimum'], $args['maximum'] ) ) {
2461          if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
2462              if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) {
2463                  return new WP_Error(
2464                      'rest_out_of_bounds',
2465                      sprintf(
2466                          /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
2467                          __( '%1$s must be between %2$d (exclusive) and %3$d (exclusive)' ),
2468                          $param,
2469                          $args['minimum'],
2470                          $args['maximum']
2471                      )
2472                  );
2473              }
2474          }
2475  
2476          if ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
2477              if ( $value > $args['maximum'] || $value <= $args['minimum'] ) {
2478                  return new WP_Error(
2479                      'rest_out_of_bounds',
2480                      sprintf(
2481                          /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
2482                          __( '%1$s must be between %2$d (exclusive) and %3$d (inclusive)' ),
2483                          $param,
2484                          $args['minimum'],
2485                          $args['maximum']
2486                      )
2487                  );
2488              }
2489          }
2490  
2491          if ( ! empty( $args['exclusiveMaximum'] ) && empty( $args['exclusiveMinimum'] ) ) {
2492              if ( $value >= $args['maximum'] || $value < $args['minimum'] ) {
2493                  return new WP_Error(
2494                      'rest_out_of_bounds',
2495                      sprintf(
2496                          /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
2497                          __( '%1$s must be between %2$d (inclusive) and %3$d (exclusive)' ),
2498                          $param,
2499                          $args['minimum'],
2500                          $args['maximum']
2501                      )
2502                  );
2503              }
2504          }
2505  
2506          if ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
2507              if ( $value > $args['maximum'] || $value < $args['minimum'] ) {
2508                  return new WP_Error(
2509                      'rest_out_of_bounds',
2510                      sprintf(
2511                          /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
2512                          __( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ),
2513                          $param,
2514                          $args['minimum'],
2515                          $args['maximum']
2516                      )
2517                  );
2518              }
2519          }
2520      }
2521  
2522      return true;
2523  }
2524  
2525  /**
2526   * Validates a string value based on a schema.
2527   *
2528   * @since 5.7.0
2529   *
2530   * @param mixed  $value The value to validate.
2531   * @param array  $args  Schema array to use for validation.
2532   * @param string $param The parameter name, used in error messages.
2533   * @return true|WP_Error
2534   */
2535  function rest_validate_string_value_from_schema( $value, $args, $param ) {
2536      if ( ! is_string( $value ) ) {
2537          return new WP_Error(
2538              'rest_invalid_type',
2539              /* translators: 1: Parameter, 2: Type name. */
2540              sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ),
2541              array( 'param' => $param )
2542          );
2543      }
2544  
2545      if ( isset( $args['minLength'] ) && mb_strlen( $value ) < $args['minLength'] ) {
2546          return new WP_Error(
2547              'rest_too_short',
2548              sprintf(
2549                  /* translators: 1: Parameter, 2: Number of characters. */
2550                  _n(
2551                      '%1$s must be at least %2$s character long.',
2552                      '%1$s must be at least %2$s characters long.',
2553                      $args['minLength']
2554                  ),
2555                  $param,
2556                  number_format_i18n( $args['minLength'] )
2557              )
2558          );
2559      }
2560  
2561      if ( isset( $args['maxLength'] ) && mb_strlen( $value ) > $args['maxLength'] ) {
2562          return new WP_Error(
2563              'rest_too_long',
2564              sprintf(
2565                  /* translators: 1: Parameter, 2: Number of characters. */
2566                  _n(
2567                      '%1$s must be at most %2$s character long.',
2568                      '%1$s must be at most %2$s characters long.',
2569                      $args['maxLength']
2570                  ),
2571                  $param,
2572                  number_format_i18n( $args['maxLength'] )
2573              )
2574          );
2575      }
2576  
2577      if ( isset( $args['pattern'] ) && ! rest_validate_json_schema_pattern( $args['pattern'], $value ) ) {
2578          return new WP_Error(
2579              'rest_invalid_pattern',
2580              /* translators: 1: Parameter, 2: Pattern. */
2581              sprintf( __( '%1$s does not match pattern %2$s.' ), $param, $args['pattern'] )
2582          );
2583      }
2584  
2585      return true;
2586  }
2587  
2588  /**
2589   * Validates an integer value based on a schema.
2590   *
2591   * @since 5.7.0
2592   *
2593   * @param mixed  $value The value to validate.
2594   * @param array  $args  Schema array to use for validation.
2595   * @param string $param The parameter name, used in error messages.
2596   * @return true|WP_Error
2597   */
2598  function rest_validate_integer_value_from_schema( $value, $args, $param ) {
2599      $is_valid_number = rest_validate_number_value_from_schema( $value, $args, $param );
2600      if ( is_wp_error( $is_valid_number ) ) {
2601          return $is_valid_number;
2602      }
2603  
2604      if ( ! rest_is_integer( $value ) ) {
2605          return new WP_Error(
2606              'rest_invalid_type',
2607              /* translators: 1: Parameter, 2: Type name. */
2608              sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ),
2609              array( 'param' => $param )
2610          );
2611      }
2612  
2613      return true;
2614  }
2615  
2616  /**
2617   * Sanitize a value based on a schema.
2618   *
2619   * @since 4.7.0
2620   * @since 5.5.0 Added the `$param` parameter.
2621   * @since 5.6.0 Support the "anyOf" and "oneOf" keywords.
2622   *
2623   * @param mixed  $value The value to sanitize.
2624   * @param array  $args  Schema array to use for sanitization.
2625   * @param string $param The parameter name, used in error messages.
2626   * @return mixed|WP_Error The sanitized value or a WP_Error instance if the value cannot be safely sanitized.
2627   */
2628  function rest_sanitize_value_from_schema( $value, $args, $param = '' ) {
2629      if ( isset( $args['anyOf'] ) ) {
2630          $matching_schema = rest_find_any_matching_schema( $value, $args, $param );
2631          if ( is_wp_error( $matching_schema ) ) {
2632              return $matching_schema;
2633          }
2634  
2635          if ( ! isset( $args['type'] ) ) {
2636              $args['type'] = $matching_schema['type'];
2637          }
2638  
2639          $value = rest_sanitize_value_from_schema( $value, $matching_schema, $param );
2640      }
2641  
2642      if ( isset( $args['oneOf'] ) ) {
2643          $matching_schema = rest_find_one_matching_schema( $value, $args, $param );
2644          if ( is_wp_error( $matching_schema ) ) {
2645              return $matching_schema;
2646          }
2647  
2648          if ( ! isset( $args['type'] ) ) {
2649              $args['type'] = $matching_schema['type'];
2650          }
2651  
2652          $value = rest_sanitize_value_from_schema( $value, $matching_schema, $param );
2653      }
2654  
2655      $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
2656  
2657      if ( ! isset( $args['type'] ) ) {
2658          /* translators: %s: Parameter. */
2659          _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' );
2660      }
2661  
2662      if ( is_array( $args['type'] ) ) {
2663          $best_type = rest_handle_multi_type_schema( $value, $args, $param );
2664  
2665          if ( ! $best_type ) {
2666              return null;
2667          }
2668  
2669          $args['type'] = $best_type;
2670      }
2671  
2672      if ( ! in_array( $args['type'], $allowed_types, true ) ) {
2673          _doing_it_wrong(
2674              __FUNCTION__,
2675              /* translators: 1: Parameter, 2: The list of allowed types. */
2676              wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ),
2677              '5.5.0'
2678          );
2679      }
2680  
2681      if ( 'array' === $args['type'] ) {
2682          $value = rest_sanitize_array( $value );
2683  
2684          if ( ! empty( $args['items'] ) ) {
2685              foreach ( $value as $index => $v ) {
2686                  $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
2687              }
2688          }
2689  
2690          if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) {
2691              /* translators: %s: Parameter. */
2692              return new WP_Error( 'rest_duplicate_items', sprintf( __( '%s has duplicate items.' ), $param ) );
2693          }
2694  
2695          return $value;
2696      }
2697  
2698      if ( 'object' === $args['type'] ) {
2699          $value = rest_sanitize_object( $value );
2700  
2701          foreach ( $value as $property => $v ) {
2702              if ( isset( $args['properties'][ $property ] ) ) {
2703                  $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' );
2704                  continue;
2705              }
2706  
2707              $pattern_property_schema = rest_find_matching_pattern_property_schema( $property, $args );
2708              if ( null !== $pattern_property_schema ) {
2709                  $value[ $property ] = rest_sanitize_value_from_schema( $v, $pattern_property_schema, $param . '[' . $property . ']' );
2710                  continue;
2711              }
2712  
2713              if ( isset( $args['additionalProperties'] ) ) {
2714                  if ( false === $args['additionalProperties'] ) {
2715                      unset( $value[ $property ] );
2716                  } elseif ( is_array( $args['additionalProperties'] ) ) {
2717                      $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' );
2718                  }
2719              }
2720          }
2721  
2722          return $value;
2723      }
2724  
2725      if ( 'null' === $args['type'] ) {
2726          return null;
2727      }
2728  
2729      if ( 'integer' === $args['type'] ) {
2730          return (int) $value;
2731      }
2732  
2733      if ( 'number' === $args['type'] ) {
2734          return (float) $value;
2735      }
2736  
2737      if ( 'boolean' === $args['type'] ) {
2738          return rest_sanitize_boolean( $value );
2739      }
2740  
2741      // This behavior matches rest_validate_value_from_schema().
2742      if ( isset( $args['format'] )
2743          && ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) )
2744      ) {
2745          switch ( $args['format'] ) {
2746              case 'hex-color':
2747                  return (string) sanitize_hex_color( $value );
2748  
2749              case 'date-time':
2750                  return sanitize_text_field( $value );
2751  
2752              case 'email':
2753                  // sanitize_email() validates, which would be unexpected.
2754                  return sanitize_text_field( $value );
2755  
2756              case 'uri':
2757                  return esc_url_raw( $value );
2758  
2759              case 'ip':
2760                  return sanitize_text_field( $value );
2761  
2762              case 'uuid':
2763                  return sanitize_text_field( $value );
2764          }
2765      }
2766  
2767      if ( 'string' === $args['type'] ) {
2768          return (string) $value;
2769      }
2770  
2771      return $value;
2772  }
2773  
2774  /**
2775   * Append result of internal request to REST API for purpose of preloading data to be attached to a page.
2776   * Expected to be called in the context of `array_reduce`.
2777   *
2778   * @since 5.0.0
2779   *
2780   * @param array  $memo Reduce accumulator.
2781   * @param string $path REST API path to preload.
2782   * @return array Modified reduce accumulator.
2783   */
2784  function rest_preload_api_request( $memo, $path ) {
2785      // array_reduce() doesn't support passing an array in PHP 5.2,
2786      // so we need to make sure we start with one.
2787      if ( ! is_array( $memo ) ) {
2788          $memo = array();
2789      }
2790  
2791      if ( empty( $path ) ) {
2792          return $memo;
2793      }
2794  
2795      $method = 'GET';
2796      if ( is_array( $path ) && 2 === count( $path ) ) {
2797          $method = end( $path );
2798          $path   = reset( $path );
2799  
2800          if ( ! in_array( $method, array( 'GET', 'OPTIONS' ), true ) ) {
2801              $method = 'GET';
2802          }
2803      }
2804  
2805      $path_parts = parse_url( $path );
2806      if ( false === $path_parts ) {
2807          return $memo;
2808      }
2809  
2810      $request = new WP_REST_Request( $method, $path_parts['path'] );
2811      if ( ! empty( $path_parts['query'] ) ) {
2812          parse_str( $path_parts['query'], $query_params );
2813          $request->set_query_params( $query_params );
2814      }
2815  
2816      $response = rest_do_request( $request );
2817      if ( 200 === $response->status ) {
2818          $server = rest_get_server();
2819          $embed  = $request->has_param( '_embed' ) ? rest_parse_embed_param( $request['_embed'] ) : false;
2820          $data   = (array) $server->response_to_data( $response, $embed );
2821  
2822          if ( 'OPTIONS' === $method ) {
2823              $response = rest_send_allow_header( $response, $server, $request );
2824  
2825              $memo[ $method ][ $path ] = array(
2826                  'body'    => $data,
2827                  'headers' => $response->headers,
2828              );
2829          } else {
2830              $memo[ $path ] = array(
2831                  'body'    => $data,
2832                  'headers' => $response->headers,
2833              );
2834          }
2835      }
2836  
2837      return $memo;
2838  }
2839  
2840  /**
2841   * Parses the "_embed" parameter into the list of resources to embed.
2842   *
2843   * @since 5.4.0
2844   *
2845   * @param string|array $embed Raw "_embed" parameter value.
2846   * @return true|string[] Either true to embed all embeds, or a list of relations to embed.
2847   */
2848  function rest_parse_embed_param( $embed ) {
2849      if ( ! $embed || 'true' === $embed || '1' === $embed ) {
2850          return true;
2851      }
2852  
2853      $rels = wp_parse_list( $embed );
2854  
2855      if ( ! $rels ) {
2856          return true;
2857      }
2858  
2859      return $rels;
2860  }
2861  
2862  /**
2863   * Filters the response to remove any fields not available in the given context.
2864   *
2865   * @since 5.5.0
2866   * @since 5.6.0 Support the "patternProperties" keyword for objects.
2867   *              Support the "anyOf" and "oneOf" keywords.
2868   *
2869   * @param array|object $data    The response data to modify.
2870   * @param array        $schema  The schema for the endpoint used to filter the response.
2871   * @param string       $context The requested context.
2872   * @return array|object The filtered response data.
2873   */
2874  function rest_filter_response_by_context( $data, $schema, $context ) {
2875      if ( isset( $schema['anyOf'] ) ) {
2876          $matching_schema = rest_find_any_matching_schema( $data, $schema, '' );
2877          if ( ! is_wp_error( $matching_schema ) ) {
2878              if ( ! isset( $schema['type'] ) ) {
2879                  $schema['type'] = $matching_schema['type'];
2880              }
2881  
2882              $data = rest_filter_response_by_context( $data, $matching_schema, $context );
2883          }
2884      }
2885  
2886      if ( isset( $schema['oneOf'] ) ) {
2887          $matching_schema = rest_find_one_matching_schema( $data, $schema, '', true );
2888          if ( ! is_wp_error( $matching_schema ) ) {
2889              if ( ! isset( $schema['type'] ) ) {
2890                  $schema['type'] = $matching_schema['type'];
2891              }
2892  
2893              $data = rest_filter_response_by_context( $data, $matching_schema, $context );
2894          }
2895      }
2896  
2897      if ( ! is_array( $data ) && ! is_object( $data ) ) {
2898          return $data;
2899      }
2900  
2901      if ( isset( $schema['type'] ) ) {
2902          $type = $schema['type'];
2903      } elseif ( isset( $schema['properties'] ) ) {
2904          $type = 'object'; // Back compat if a developer accidentally omitted the type.
2905      } else {
2906          return $data;
2907      }
2908  
2909      $is_array_type  = 'array' === $type || ( is_array( $type ) && in_array( 'array', $type, true ) );
2910      $is_object_type = 'object' === $type || ( is_array( $type ) && in_array( 'object', $type, true ) );
2911  
2912      if ( $is_array_type && $is_object_type ) {
2913          if ( rest_is_array( $data ) ) {
2914              $is_object_type = false;
2915          } else {
2916              $is_array_type = false;
2917          }
2918      }
2919  
2920      $has_additional_properties = $is_object_type && isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] );
2921  
2922      foreach ( $data as $key => $value ) {
2923          $check = array();
2924  
2925          if ( $is_array_type ) {
2926              $check = isset( $schema['items'] ) ? $schema['items'] : array();
2927          } elseif ( $is_object_type ) {
2928              if ( isset( $schema['properties'][ $key ] ) ) {
2929                  $check = $schema['properties'][ $key ];
2930              } else {
2931                  $pattern_property_schema = rest_find_matching_pattern_property_schema( $key, $schema );
2932                  if ( null !== $pattern_property_schema ) {
2933                      $check = $pattern_property_schema;
2934                  } elseif ( $has_additional_properties ) {
2935                      $check = $schema['additionalProperties'];
2936                  }
2937              }
2938          }
2939  
2940          if ( ! isset( $check['context'] ) ) {
2941              continue;
2942          }
2943  
2944          if ( ! in_array( $context, $check['context'], true ) ) {
2945              if ( $is_array_type ) {
2946                  // All array items share schema, so there's no need to check each one.
2947                  $data = array();
2948                  break;
2949              }
2950  
2951              if ( is_object( $data ) ) {
2952                  unset( $data->$key );
2953              } else {
2954                  unset( $data[ $key ] );
2955              }
2956          } elseif ( is_array( $value ) || is_object( $value ) ) {
2957              $new_value = rest_filter_response_by_context( $value, $check, $context );
2958  
2959              if ( is_object( $data ) ) {
2960                  $data->$key = $new_value;
2961              } else {
2962                  $data[ $key ] = $new_value;
2963              }
2964          }
2965      }
2966  
2967      return $data;
2968  }
2969  
2970  /**
2971   * Sets the "additionalProperties" to false by default for all object definitions in the schema.
2972   *
2973   * @since 5.5.0
2974   * @since 5.6.0 Support the "patternProperties" keyword.
2975   *
2976   * @param array $schema The schema to modify.
2977   * @return array The modified schema.
2978   */
2979  function rest_default_additional_properties_to_false( $schema ) {
2980      $type = (array) $schema['type'];
2981  
2982      if ( in_array( 'object', $type, true ) ) {
2983          if ( isset( $schema['properties'] ) ) {
2984              foreach ( $schema['properties'] as $key => $child_schema ) {
2985                  $schema['properties'][ $key ] = rest_default_additional_properties_to_false( $child_schema );
2986              }
2987          }
2988  
2989          if ( isset( $schema['patternProperties'] ) ) {
2990              foreach ( $schema['patternProperties'] as $key => $child_schema ) {
2991                  $schema['patternProperties'][ $key ] = rest_default_additional_properties_to_false( $child_schema );
2992              }
2993          }
2994  
2995          if ( ! isset( $schema['additionalProperties'] ) ) {
2996              $schema['additionalProperties'] = false;
2997          }
2998      }
2999  
3000      if ( in_array( 'array', $type, true ) ) {
3001          if ( isset( $schema['items'] ) ) {
3002              $schema['items'] = rest_default_additional_properties_to_false( $schema['items'] );
3003          }
3004      }
3005  
3006      return $schema;
3007  }
3008  
3009  /**
3010   * Gets the REST API route for a post.
3011   *
3012   * @since 5.5.0
3013   *
3014   * @param int|WP_Post $post Post ID or post object.
3015   * @return string The route path with a leading slash for the given post, or an empty string if there is not a route.
3016   */
3017  function rest_get_route_for_post( $post ) {
3018      $post = get_post( $post );
3019  
3020      if ( ! $post instanceof WP_Post ) {
3021          return '';
3022      }
3023  
3024      $post_type = get_post_type_object( $post->post_type );
3025      if ( ! $post_type ) {
3026          return '';
3027      }
3028  
3029      $controller = $post_type->get_rest_controller();
3030      if ( ! $controller ) {
3031          return '';
3032      }
3033  
3034      $route = '';
3035  
3036      // The only two controllers that we can detect are the Attachments and Posts controllers.
3037      if ( in_array( get_class( $controller ), array( 'WP_REST_Attachments_Controller', 'WP_REST_Posts_Controller' ), true ) ) {
3038          $namespace = 'wp/v2';
3039          $rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
3040          $route     = sprintf( '/%s/%s/%d', $namespace, $rest_base, $post->ID );
3041      }
3042  
3043      /**
3044       * Filters the REST API route for a post.
3045       *
3046       * @since 5.5.0
3047       *
3048       * @param string  $route The route path.
3049       * @param WP_Post $post  The post object.
3050       */
3051      return apply_filters( 'rest_route_for_post', $route, $post );
3052  }
3053  
3054  /**
3055   * Gets the REST API route for a term.
3056   *
3057   * @since 5.5.0
3058   *
3059   * @param int|WP_Term $term Term ID or term object.
3060   * @return string The route path with a leading slash for the given term, or an empty string if there is not a route.
3061   */
3062  function rest_get_route_for_term( $term ) {
3063      $term = get_term( $term );
3064  
3065      if ( ! $term instanceof WP_Term ) {
3066          return '';
3067      }
3068  
3069      $taxonomy = get_taxonomy( $term->taxonomy );
3070      if ( ! $taxonomy ) {
3071          return '';
3072      }
3073  
3074      $controller = $taxonomy->get_rest_controller();
3075      if ( ! $controller ) {
3076          return '';
3077      }
3078  
3079      $route = '';
3080  
3081      // The only controller that works is the Terms controller.
3082      if ( $controller instanceof WP_REST_Terms_Controller ) {
3083          $namespace = 'wp/v2';
3084          $rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
3085          $route     = sprintf( '/%s/%s/%d', $namespace, $rest_base, $term->term_id );
3086      }
3087  
3088      /**
3089       * Filters the REST API route for a term.
3090       *
3091       * @since 5.5.0
3092       *
3093       * @param string  $route The route path.
3094       * @param WP_Term $term  The term object.
3095       */
3096      return apply_filters( 'rest_route_for_term', $route, $term );
3097  }
3098  
3099  /**
3100   * Gets the REST route for the currently queried object.
3101   *
3102   * @since 5.5.0
3103   *
3104   * @return string The REST route of the resource, or an empty string if no resource identified.
3105   */
3106  function rest_get_queried_resource_route() {
3107      if ( is_singular() ) {
3108          $route = rest_get_route_for_post( get_queried_object() );
3109      } elseif ( is_category() || is_tag() || is_tax() ) {
3110          $route = rest_get_route_for_term( get_queried_object() );
3111      } elseif ( is_author() ) {
3112          $route = '/wp/v2/users/' . get_queried_object_id();
3113      } else {
3114          $route = '';
3115      }
3116  
3117      /**
3118       * Filters the REST route for the currently queried object.
3119       *
3120       * @since 5.5.0
3121       *
3122       * @param string $link The route with a leading slash, or an empty string.
3123       */
3124      return apply_filters( 'rest_queried_resource_route', $route );
3125  }
3126  
3127  /**
3128   * Retrieves an array of endpoint arguments from the item schema and endpoint method.
3129   *
3130   * @since 5.6.0
3131   *
3132   * @param array  $schema The full JSON schema for the endpoint.
3133   * @param string $method Optional. HTTP method of the endpoint. The arguments for `CREATABLE` endpoints are
3134   *                       checked for required values and may fall-back to a given default, this is not done
3135   *                       on `EDITABLE` endpoints. Default WP_REST_Server::CREATABLE.
3136   * @return array The endpoint arguments.
3137   */
3138  function rest_get_endpoint_args_for_schema( $schema, $method = WP_REST_Server::CREATABLE ) {
3139  
3140      $schema_properties       = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
3141      $endpoint_args           = array();
3142      $valid_schema_properties = rest_get_allowed_schema_keywords();
3143      $valid_schema_properties = array_diff( $valid_schema_properties, array( 'default', 'required' ) );
3144  
3145      foreach ( $schema_properties as $field_id => $params ) {
3146  
3147          // Arguments specified as `readonly` are not allowed to be set.
3148          if ( ! empty( $params['readonly'] ) ) {
3149              continue;
3150          }
3151  
3152          $endpoint_args[ $field_id ] = array(
3153              'validate_callback' => 'rest_validate_request_arg',
3154              'sanitize_callback' => 'rest_sanitize_request_arg',
3155          );
3156  
3157          if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) {
3158              $endpoint_args[ $field_id ]['default'] = $params['default'];
3159          }
3160  
3161          if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) {
3162              $endpoint_args[ $field_id ]['required'] = true;
3163          }
3164  
3165          foreach ( $valid_schema_properties as $schema_prop ) {
3166              if ( isset( $params[ $schema_prop ] ) ) {
3167                  $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
3168              }
3169          }
3170  
3171          // Merge in any options provided by the schema property.
3172          if ( isset( $params['arg_options'] ) ) {
3173  
3174              // Only use required / default from arg_options on CREATABLE endpoints.
3175              if ( WP_REST_Server::CREATABLE !== $method ) {
3176                  $params['arg_options'] = array_diff_key(
3177                      $params['arg_options'],
3178                      array(
3179                          'required' => '',
3180                          'default'  => '',
3181                      )
3182                  );
3183              }
3184  
3185              $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
3186          }
3187      }
3188  
3189      return $endpoint_args;
3190  }
3191  
3192  
3193  /**
3194   * Converts an error to a response object.
3195   *
3196   * This iterates over all error codes and messages to change it into a flat
3197   * array. This enables simpler client behaviour, as it is represented as a
3198   * list in JSON rather than an object/map.
3199   *
3200   * @since 5.7.0
3201   *
3202   * @param WP_Error $error WP_Error instance.
3203   *
3204   * @return WP_REST_Response List of associative arrays with code and message keys.
3205   */
3206  function rest_convert_error_to_response( $error ) {
3207      $status = array_reduce(
3208          $error->get_all_error_data(),
3209          function ( $status, $error_data ) {
3210              return is_array( $error_data ) && isset( $error_data['status'] ) ? $error_data['status'] : $status;
3211          },
3212          500
3213      );
3214  
3215      $errors = array();
3216  
3217      foreach ( (array) $error->errors as $code => $messages ) {
3218          $all_data  = $error->get_all_error_data( $code );
3219          $last_data = array_pop( $all_data );
3220  
3221          foreach ( (array) $messages as $message ) {
3222              $formatted = array(
3223                  'code'    => $code,
3224                  'message' => $message,
3225                  'data'    => $last_data,
3226              );
3227  
3228              if ( $all_data ) {
3229                  $formatted['additional_data'] = $all_data;
3230              }
3231  
3232              $errors[] = $formatted;
3233          }
3234      }
3235  
3236      $data = $errors[0];
3237      if ( count( $errors ) > 1 ) {
3238          // Remove the primary error.
3239          array_shift( $errors );
3240          $data['additional_errors'] = $errors;
3241      }
3242  
3243      return new WP_REST_Response( $data, $status );
3244  }


Generated: Fri Apr 23 01:00:05 2021 Cross-referenced by PHPXref 0.7.1