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


Generated : Thu Nov 12 08:20:02 2026 Cross-referenced by PHPXref