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


Generated : Fri Oct 30 08:20:34 2026 Cross-referenced by PHPXref