[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> rest-api.php (source)

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


Generated : Thu Oct 1 08:20:30 2026 Cross-referenced by PHPXref