Skip to content

Commit

Permalink
Bootstrap/Load: Introduce functions to check whether WordPress is ser…
Browse files Browse the repository at this point in the history
…ving a REST API request.

This changeset introduces two functions:
* `wp_is_serving_rest_request()` returns a boolean for whether WordPress is serving an actual REST API request.
* `wp_is_rest_endpoint()` returns a boolean for whether a WordPress REST API endpoint is currently being used. While this is always the case if `wp_is_serving_rest_request()` returns `true`, the function additionally covers the scenario of internal REST API requests, i.e. where WordPress calls a REST API endpoint within the same request.

Both functions should only be used after the `parse_request` action.

All relevant manual checks have been adjusted to use one of the new functions, depending on the use-case. They were all using the same constant check so far, while in fact some of them were intending to check for an actual REST API request while others were intending to check for REST endpoint usage.

A new filter `wp_is_rest_endpoint` can be used to alter the return value of the `wp_is_rest_endpoint()` function.

Props lots.0.logs, TimothyBlynJacobs, flixos90, joehoyle, peterwilsoncc, swissspidy, SergeyBiryukov, pento, mikejolley, iandunn, hellofromTonya, Cybr, petitphp.
Fixes #42061.


git-svn-id: https://develop.svn.wordpress.org/trunk@57312 602fd350-edb4-49c9-b593-d223f7449a82
  • Loading branch information
felixarntz committed Jan 19, 2024
1 parent 3287684 commit d17afcc
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/wp-includes/class-wp-query.php
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,7 @@ public function parse_query( $query = '' ) {
}

if ( ! ( $this->is_singular || $this->is_archive || $this->is_search || $this->is_feed
|| ( defined( 'REST_REQUEST' ) && REST_REQUEST && $this->is_main_query() )
|| ( wp_is_serving_rest_request() && $this->is_main_query() )
|| $this->is_trackback || $this->is_404 || $this->is_admin || $this->is_robots || $this->is_favicon ) ) {
$this->is_home = true;
}
Expand Down
2 changes: 1 addition & 1 deletion src/wp-includes/deprecated.php
Original file line number Diff line number Diff line change
Expand Up @@ -5436,7 +5436,7 @@ function _wp_theme_json_webfonts_handler() {
$settings = WP_Theme_JSON_Resolver::get_merged_data()->get_settings();

// If in the editor, add webfonts defined in variations.
if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
if ( is_admin() || wp_is_rest_endpoint() ) {
$variations = WP_Theme_JSON_Resolver::get_style_variations();
foreach ( $variations as $variation ) {
// Skip if fontFamilies are not defined in the variation.
Expand Down
21 changes: 19 additions & 2 deletions src/wp-includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -3718,7 +3718,7 @@ function wp_die( $message = '', $title = '', $args = array() ) {
* @param callable $callback Callback function name.
*/
$callback = apply_filters( 'wp_die_json_handler', '_json_wp_die_handler' );
} elseif ( defined( 'REST_REQUEST' ) && REST_REQUEST && wp_is_jsonp_request() ) {
} elseif ( wp_is_serving_rest_request() && wp_is_jsonp_request() ) {
/**
* Filters the callback for killing WordPress execution for JSONP REST requests.
*
Expand Down Expand Up @@ -4441,7 +4441,7 @@ function _wp_json_prepare_data( $value ) {
* @param int $flags Optional. Options to be passed to json_encode(). Default 0.
*/
function wp_send_json( $response, $status_code = null, $flags = 0 ) {
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
if ( wp_is_serving_rest_request() ) {
_doing_it_wrong(
__FUNCTION__,
sprintf(
Expand Down Expand Up @@ -4697,6 +4697,23 @@ function _mce_set_direction( $mce_init ) {
return $mce_init;
}

/**
* Determines whether WordPress is currently serving a REST API request.
*
* The function relies on the 'REST_REQUEST' global. As such, it only returns true when an actual REST _request_ is
* being made. It does not return true when a REST endpoint is hit as part of another request, e.g. for preloading a
* REST response. See {@see wp_is_rest_endpoint()} for that purpose.
*
* This function should not be called until the {@see 'parse_request'} action, as the constant is only defined then,
* even for an actual REST request.
*
* @since 6.5.0
*
* @return bool True if it's a WordPress REST API request, false otherwise.
*/
function wp_is_serving_rest_request() {
return defined( 'REST_REQUEST' ) && REST_REQUEST;
}

/**
* Converts smiley code to the icon graphic file equivalent.
Expand Down
4 changes: 4 additions & 0 deletions src/wp-includes/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,10 @@ function wp_debug_mode() {
error_reporting( E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_ERROR | E_WARNING | E_PARSE | E_USER_ERROR | E_USER_WARNING | E_RECOVERABLE_ERROR );
}

/*
* The 'REST_REQUEST' check here is optimistic as the constant is most
* likely not set at this point even if it is in fact a REST request.
*/
if ( defined( 'XMLRPC_REQUEST' ) || defined( 'REST_REQUEST' ) || defined( 'MS_FILES_REQUEST' )
|| ( defined( 'WP_INSTALLING' ) && WP_INSTALLING )
|| wp_doing_ajax() || wp_is_json_request()
Expand Down
37 changes: 36 additions & 1 deletion src/wp-includes/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ function rest_api_register_rewrites() {
* @since 4.4.0
*/
function rest_api_default_filters() {
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
if ( wp_is_serving_rest_request() ) {
// Deprecated reporting.
add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 );
add_filter( 'deprecated_function_trigger_error', '__return_false' );
Expand Down Expand Up @@ -3389,3 +3389,38 @@ static function ( $status, $error_data ) {

return new WP_REST_Response( $data, $status );
}

/**
* Checks whether a REST API endpoint request is currently being handled.
*
* This may be a standalone REST API request, or an internal request dispatched from within a regular page load.
*
* @since 6.5.0
*
* @global WP_REST_Server $wp_rest_server REST server instance.
*
* @return bool True if a REST endpoint request is currently being handled, false otherwise.
*/
function wp_is_rest_endpoint() {
/* @var WP_REST_Server $wp_rest_server */
global $wp_rest_server;

// Check whether this is a standalone REST request.
$is_rest_endpoint = wp_is_serving_rest_request();
if ( ! $is_rest_endpoint ) {
// Otherwise, check whether an internal REST request is currently being handled.
$is_rest_endpoint = isset( $wp_rest_server )
&& $wp_rest_server->is_dispatching();
}

/**
* Filters whether a REST endpoint request is currently being handled.
*
* This may be a standalone REST API request, or an internal request dispatched from within a regular page load.
*
* @since 6.5.0
*
* @param bool $is_request_endpoint Whether a REST endpoint request is currently being handled.
*/
return (bool) apply_filters( 'wp_is_rest_endpoint', $is_rest_endpoint );
}
32 changes: 30 additions & 2 deletions src/wp-includes/rest-api/class-wp-rest-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ class WP_REST_Server {
*/
protected $embed_cache = array();

/**
* Stores request objects that are currently being handled.
*
* @since 6.5.0
* @var array
*/
protected $dispatching_requests = array();

/**
* Instantiates the REST server.
*
Expand Down Expand Up @@ -983,6 +991,8 @@ public function get_route_options( $route ) {
* @return WP_REST_Response Response returned by the callback.
*/
public function dispatch( $request ) {
$this->dispatching_requests[] = $request;

/**
* Filters the pre-calculated result of a REST API dispatch request.
*
Expand All @@ -1008,14 +1018,17 @@ public function dispatch( $request ) {
$result = $this->error_to_response( $result );
}

array_pop( $this->dispatching_requests );
return $result;
}

$error = null;
$matched = $this->match_request_to_handler( $request );

if ( is_wp_error( $matched ) ) {
return $this->error_to_response( $matched );
$response = $this->error_to_response( $matched );
array_pop( $this->dispatching_requests );
return $response;
}

list( $route, $handler ) = $matched;
Expand All @@ -1040,7 +1053,22 @@ public function dispatch( $request ) {
}
}

return $this->respond_to_request( $request, $route, $handler, $error );
$response = $this->respond_to_request( $request, $route, $handler, $error );
array_pop( $this->dispatching_requests );
return $response;
}

/**
* Returns whether the REST server is currently dispatching / responding to a request.
*
* This may be a standalone REST API request, or an internal request dispatched from within a regular page load.
*
* @since 6.5.0
*
* @return bool Whether the REST server is currently handling a request.
*/
public function is_dispatching() {
return (bool) $this->dispatching_requests;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public function create_item( $request ) {

wp_after_insert_post( $attachment, false, null );

if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
if ( wp_is_serving_rest_request() ) {
/*
* Set a custom header with the attachment_id.
* Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
Expand Down Expand Up @@ -630,7 +630,7 @@ public function edit_media_item( $request ) {
update_post_meta( $new_attachment_id, '_wp_attachment_image_alt', wp_slash( $image_alt ) );
}

if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
if ( wp_is_serving_rest_request() ) {
/*
* Set a custom header with the attachment_id.
* Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public function get_item_schema() {
*
* @since 5.9.0
*
* @param WP_REST_REQUEST $request Full details about the request.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error The parsed details as a response object. WP_Error if there are errors.
*/
public function parse_url_details( $request ) {
Expand Down
2 changes: 1 addition & 1 deletion src/wp-includes/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -2586,7 +2586,7 @@ function wp_should_load_block_editor_scripts_and_styles() {
* @return bool Whether separate assets will be loaded.
*/
function wp_should_load_separate_core_block_assets() {
if ( is_admin() || is_feed() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
if ( is_admin() || is_feed() || wp_is_rest_endpoint() ) {
return false;
}

Expand Down
1 change: 1 addition & 0 deletions src/wp-includes/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ function wp_authenticate_application_password( $input_user, $username, $password
return $input_user;
}

// The 'REST_REQUEST' check here may happen too early for the constant to be available.
$is_api_request = ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) );

/**
Expand Down
66 changes: 66 additions & 0 deletions tests/phpunit/tests/rest-api/wpIsRestEndpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/**
* Tests for the `wp_is_rest_endpoint()` function.
*
* @group rest-api
* @covers ::wp_is_rest_endpoint
*/
class Tests_Media_Wp_Is_Rest_Endpoint extends WP_UnitTestCase {

/**
* Tests that `wp_is_rest_endpoint()` returns false by default.
*
* @ticket 42061
*/
public function test_wp_is_rest_endpoint_default() {
$this->assertFalse( wp_is_rest_endpoint() );
}

/**
* Tests that `wp_is_rest_endpoint()` relies on whether the global REST server is dispatching.
*
* @ticket 42061
*/
public function test_wp_is_rest_endpoint_via_global() {
global $wp_rest_server;

$wp_rest_server = new Spy_REST_Server();
do_action( 'rest_api_init', $wp_rest_server );

// The presence of a REST server itself won't set this to true.
$this->assertFalse( wp_is_rest_endpoint() );

// Set up filter to record value during dispatching.
$result_within_request = null;
add_filter(
'rest_pre_dispatch',
function ( $result ) use ( &$result_within_request ) {
$result_within_request = wp_is_rest_endpoint();
return $result;
}
);

/*
* Dispatch a request (doesn't matter that it's invalid).
* This already is completed after this method call.
*/
$wp_rest_server->dispatch( new WP_REST_Request() );

// Within that request, the function should have returned true.
$this->assertTrue( $result_within_request );

// After the dispatching, the function should return false again.
$this->assertFalse( wp_is_rest_endpoint() );
}

/**
* Tests that `wp_is_rest_endpoint()` returns a result enforced via filter.
*
* @ticket 42061
*/
public function test_wp_is_rest_endpoint_via_filter() {
add_filter( 'wp_is_rest_endpoint', '__return_true' );
$this->assertTrue( wp_is_rest_endpoint() );
}
}

0 comments on commit d17afcc

Please sign in to comment.