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


Generated: Wed Jan 26 01:00:03 2022 Cross-referenced by PHPXref 0.7.1