[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Sun Oct 1 08:20:01 2023 Cross-referenced by PHPXref