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


Generated : Sat Aug 1 08:20:18 2026 Cross-referenced by PHPXref