[ Index ]

PHP Cross Reference of WordPress

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


Generated: Sat Nov 23 01:00:02 2024 Cross-referenced by PHPXref 0.7.1