[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

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


Generated : Tue May 5 08:20:14 2026 Cross-referenced by PHPXref