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


Generated : Thu Jun 25 08:20:12 2026 Cross-referenced by PHPXref