[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

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

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


Generated: Fri Oct 30 01:00:03 2020 Cross-referenced by PHPXref 0.7.1