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


Generated : Thu May 9 08:20:02 2024 Cross-referenced by PHPXref