File "Rest_Endpoint.php"
Full Path: /home/romayxjt/public_html/wp-content/plugins/the-events-calendar/src/Tribe/Views/V2/Rest_Endpoint.php
File size: 17.82 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* The AJAX and REST API request handler.
*
* @since 4.9.2
*
* @package Tribe\Events\Views\V2
*/
namespace Tribe\Events\Views\V2;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
use WP_REST_Server as Server;
use WP_User;
/**
* Class Rest_Endpoint
*
* @since 4.9.2
*
* @package Tribe\Events\Views\V2
*/
class Rest_Endpoint {
/**
* The action for this nonce.
*
* @since 6.1.4
*
* @var string
*/
const NONCE_ACTION = '_view_rest';
/**
* The field name for the primary nonce.
*
* @since 6.1.4
* @since 6.11.1 Changed to `tvn1` from `_tec_view_rest_nonce_primary`.
*
* @var string
*/
const PRIMARY_NONCE_KEY = 'tvn1';
/**
* The field name for the secondary nonce.
*
* @since 6.1.4
* @since 6.11.1 Changed to `tvn2` from `_tec_view_rest_nonce_secondary`.
*
* @var string
*/
const SECONDARY_NONCE_KEY = 'tvn2';
/**
* Rest Endpoint namespace
*
* @since 4.9.7
*
* @var string
*/
const ROOT_NAMESPACE = 'tribe/views/v2';
/**
* AJAX action for the fallback when REST is inactive.
*
* @since 4.9.7
*
* @var string
*/
public static $ajax_action = 'tribe_events_views_v2_fallback';
/**
* A flag, set on a per-request basis, to indicate if the `rest_authentication_errors` filter fired or not.
*
* @since 4.9.12
*
* @var bool
*/
protected static $did_rest_authentication_errors;
/**
* When in a REST request, store the authenticated user ID for use later.
*
* @since 6.2.3
*
* @var null|int The authenticated user ID.
*/
protected static $user_id;
/**
* Due to our custom nonce usage on the REST auth, the _wpnonce is missing and WP core
* will fail to retain the authenticated user and removes it.
*
* This stores the user (if authenticated) for use when we check that our custom nonce(s) are valid.
*
* @since 6.2.3
* @since 6.2.7 Moved to new hook with new params in order to intercede in user auth flow for REST requests.
*
* @param array $cors_headers List of headers to be filtered.
*
* @return array List of headers.
* @see rest_cookie_check_errors()
*
*/
public static function preserve_user_for_custom_nonces( $cors_headers ) {
if ( ! is_user_logged_in() ) {
return $cors_headers;
}
$user = wp_get_current_user();
if ( ! $user instanceof WP_User || ! $user->ID ) {
return $cors_headers;
}
// Save user for our nonce checks.
self::$user_id = (int) $user->ID;
return $cors_headers;
}
/**
* Returns the user ID, if we successfully stored it during a REST request.
*
* @since 6.2.3
*
* @return int|null The user ID or null if none stored.
*/
public static function get_stored_user_id(): ?int {
return self::$user_id;
}
/**
* Allows clearing the user to handle cases we are done with an old user.
*
* @since 6.9.1
*/
public static function clear_stored_user_id() {
self::$user_id = null;
}
/**
* Ensures the nonce(s) are valid.
*
* @since 6.2.3
*
* @param Request $request
*
* @return bool
*/
public function is_valid_request( Request $request ): bool {
/*
* Since WordPress 4.7 the REST API cannot be disabled completely.
* The "disabling" happens by returning falsy or error values from the `rest_authentication_errors`
* filter.
* If false or error, we follow through and and do not authorize the callback.
* If null, the site is using alternate authentication such as SAML
*/
$auth = apply_filters( 'rest_authentication_errors', null );
if ( self::$user_id && ! is_user_logged_in() ) {
/**
* This user was set but lost, because we use custom nonces which can not be handled by WordPress auth.
*
*/
wp_set_current_user( self::$user_id );
// We have a valid user, we should not fail on the cookie check anymore.
$user_valid = static function ( $valid ) {
if ( is_null( $valid ) ) {
return true;
}
return $valid;
};
if ( ! has_filter( 'rest_authentication_errors', $user_valid ) ) {
add_filter( 'rest_authentication_errors', $user_valid );
}
}
// Did either our unauth or authed nonce pass? If neither, something is fishy.
$nonce_check = tribe_without_filters(
[ 'nonce_user_logged_out' ],
function () use ( $request ) {
return wp_verify_nonce( $request->get_param( static::PRIMARY_NONCE_KEY ), static::NONCE_ACTION )
|| wp_verify_nonce( $request->get_param( static::SECONDARY_NONCE_KEY ), static::NONCE_ACTION );
}
);
return ( $auth || is_null( $auth ) )
&& ! is_wp_error( $auth )
&& $nonce_check;
}
/**
* Get the nonces being passed to the V2 views used for our REST requests.
*
* @since 6.1.4
*
* @return array<string,string> The field => nonce array.
*/
public static function get_rest_nonces(): array {
$generated_nonces = [];
/*
* Some plugins, like WooCommerce, will modify the UID of logged out users; avoid that filtering here.
*
* @see TEC-3579
*/
$generated_nonces[ static::PRIMARY_NONCE_KEY ] = tribe_without_filters(
[ 'nonce_user_logged_out' ],
function () {
// Our current users' nonce.
return wp_create_nonce( static::NONCE_ACTION );
}
);
$generated_nonces[ static::SECONDARY_NONCE_KEY ] = tribe_without_filters(
[ 'nonce_user_logged_out' ],
function () {
// In case nonce A is a logged in user and cached and served for visitors,
// provide a valid fallback for unauthenticated visitors in B.
$uid = get_current_user_id();
// If not logged in, we already created this nonce in A.
if ( ! $uid ) {
return '';
}
// We are logged in, now generate an unauthenticated user nonce.
wp_set_current_user( 0 );
$nonce = wp_create_nonce( static::NONCE_ACTION );
wp_set_current_user( $uid );
return $nonce;
}
);
/**
* Filter the list of nonces being used on REST requests for V2 views.
*
* @since 6.1.4
*
* @param array<string,string> The field => nonce array.
*/
return (array) apply_filters( 'tec_events_views_v2_get_rest_nonces', $generated_nonces );
}
/**
* Fetches and filters the HTML tag with the encoded nonces to be output on the view markup.
*
* @since 6.2.7
*
* @param array $nonces The array of nonces that are being encoded in the HTML output.
*
* @return string The HTML for the nonces.
*/
public static function get_rest_nonce_html( array $nonces ): string {
$html = "<script data-js='tribe-events-view-nonce-data' type='application/json'>" . wp_json_encode( $nonces ) . "</script>";
/**
* This allows filtering of the nonce script tag being appended to the various views that utilize AJAX requests.
*
* @since 6.2.7
*
* @param string $html The script tag that has JSON encoded nonces.
* @param array<string,string> $nonces The associative array of nonces being generated.
*/
return (string) apply_filters( 'tec_events_views_v2_get_rest_nonce_html', $html, $nonces );
}
/**
* Returns the URL View will use to fetch their content.
*
* Depending on whether the REST API is enabled or not on the site, the URL might be a REST API one or an
* admin AJAX one.
*
* @since 4.9.2
* @since 5.2.1 Add filtering to the URL.
*
* @return string The URL of the backend endpoint Views will use to fetch their content.
*/
public function get_url() {
$rest_available = $this->is_available();
if ( ! $rest_available ) {
$url = admin_url( 'admin-ajax.php' );
$url = add_query_arg( [ 'action' => static::$ajax_action ], $url );
} else {
$url = get_rest_url( null, static::ROOT_NAMESPACE . '/html' );
}
/**
* Filters the URL Views should use to fetch their contents from the backend.
*
* @since 5.2.1
*
* @param string $url The View endpoint URL, either a REST API URL or a admin-ajax.php fallback URL if REST API
* is not available.
* @param bool $rest_available Whether the REST API endpoint URL is available on the current site or not.
*/
$url = apply_filters( 'tribe_events_views_v2_endpoint_url', $url, $rest_available );
return $url;
}
/**
* Get the arguments used to setup the HTML route for Views V2 in the REST API.
*
* @link https://developer.wordpress.org/rest-api/requests/
*
* @since 4.9.7
*
* @return array $arguments Request arguments following the WP_REST API Standards [ name => options, ... ]
*/
public function get_request_arguments() {
$arguments = [];
// URL is required, and should be a string.
$arguments['u'] = [
'required' => true,
'validate_callback' => static function ( $url ) {
return is_string( $url );
},
'sanitize_callback' => static function ( $url ) {
return filter_var( $url, FILTER_SANITIZE_URL );
},
];
// View is not required, but if it is passed, it should be a string.
$arguments['view'] = [
'required' => false,
'validate_callback' => static function ( $view ) {
return is_string( $view );
},
'sanitize_callback' => static function ( $view ) {
return tec_sanitize_string( $view );
},
];
// Primary nonce is not required, but if it is passed, it should be a string.
$arguments[ static::PRIMARY_NONCE_KEY ] = [
'required' => false,
'validate_callback' => static function ( $nonce ) {
return is_string( $nonce );
},
'sanitize_callback' => static function ( $nonce ) {
return tec_sanitize_string( $nonce );
},
];
// Secondary nonce is not required, but if it is passed, it should be a string.
$arguments[ static::SECONDARY_NONCE_KEY ] = [
'required' => false,
'validate_callback' => static function ( $nonce ) {
return is_string( $nonce );
},
'sanitize_callback' => static function ( $nonce ) {
return tec_sanitize_string( $nonce );
},
];
// View data is not required, but if it is passed, it should be an array.
$arguments['view_data'] = [
'required' => false,
'validate_callback' => static function ( $view_data ) {
return is_array( $view_data );
},
'sanitize_callback' => static function ( $view_data ) {
return is_array( $view_data ) ? $view_data : [];
},
];
// Arguments specific to AJAX requests; we add them to all requests as long as the argument is not required.
$arguments['action'] = [
'required' => false,
'validate_callback' => static function ( $action ) {
return is_string( $action );
},
'sanitize_callback' => static function ( $action ) {
return tec_sanitize_string( $action );
},
];
/**
* Filter the arguments for the HTML REST API request.
* It follows the WP_REST API standards.
*
* @link https://developer.wordpress.org/rest-api/requests/
*
* @since 4.9.7
*
* @param array $arguments Request arguments following the WP_REST API Standards [ name => options, ... ]
*/
return apply_filters( 'tribe_events_views_v2_request_arguments', $arguments );
}
/**
* Register the endpoint if available.
*
* @since 4.9.7
* @since 5.2.1 Add support for the POST method.
*
* @return boolean If we registered the endpoint.
*/
public function register() {
return register_rest_route( static::ROOT_NAMESPACE, '/html', [
// Support both GET and POST HTTP methods: we originally used GET.
'methods' => [ Server::READABLE, Server::CREATABLE ],
// @todo [BTRIA-600]: Make sure we do proper handling of caches longer then 12h.
'permission_callback' => [ $this, 'is_valid_request' ],
'callback' => [ $this, 'send_html' ],
'args' => $this->get_request_arguments(),
] );
}
/**
* Sends the HTML for the view.
*
* @since 6.11.1
*
* @param Request $request The request object.
*
* @return Response The response object.
*/
public function send_html( Request $request ) {
$request = $this->unshrink_url_components( $request );
$html = View::make_for_rest( $request )->get_html();
// Setup the response data.
$data = [
'html' => $html,
];
// Return the response, a 200 status code is set by default.
return new Response( $data );
}
/**
* Register the endpoint so it will be cached.
*
* @since 6.11.1
*
* @param array $allowed_endpoints The allowed endpoints.
*
* @return array The allowed endpoints.
*/
public function include_rest_for_caching( $allowed_endpoints ): array {
$namespace = static::ROOT_NAMESPACE;
if ( ! isset( $allowed_endpoints[ $namespace ] ) || ! in_array( 'html', $allowed_endpoints[ $namespace ] ) ) {
$allowed_endpoints[ $namespace ][] = 'html';
}
return $allowed_endpoints;
}
/**
* Unshrink the URL components.
*
* @since 6.11.1
*
* @param Request $request The request object.
*
* @return Request The request object.
*/
public function unshrink_url_components( Request $request ) {
$request->set_param( 'url', $request->get_param( 'u' ) );
$request->set_param( 'prev_url', $request->get_param( 'pu' ) );
$request->set_param( 'should_manage_url', $request->get_param( 'smu' ) );
$request->set_param( 'u', null );
$request->set_param( 'pu', null );
$request->set_param( 'smu', null );
return $request;
}
/**
* When REST is not available add AJAX fallback into the correct action.
*
* @since 4.9.7
* @since 4.9.12 Always enable this.
*
* @return void
*/
public function enable_ajax_fallback() {
$action = static::$ajax_action;
add_action( "wp_ajax_{$action}", [ $this, 'handle_ajax_request' ] );
add_action( "wp_ajax_nopriv_{$action}", [ $this, 'handle_ajax_request' ] );
}
/**
* Get the mocked rest request used for the AJAX fallback used to make sure users without
* the REST API still have the Views V2 working.
*
* @since 4.9.7
* @since 5.2.1 Changed the mock request HTTP method to POST (was GET).
*
* @param array $params Associative array with the params that will be used on this mocked request
*
* @return Request The mocked request.
*/
public function get_mocked_rest_request( array $params ) {
$request = new Request( 'POST', static::ROOT_NAMESPACE . '/html' );
$arguments = $this->get_request_arguments();
foreach ( $params as $key => $value ) {
// Quick way to prevent un-wanted params.
if ( ! isset( $arguments[ $key ] ) ) {
continue;
}
$request->set_param( $key, $value );
}
$has_valid_params = $request->has_valid_params();
if ( ! $has_valid_params || is_wp_error( $has_valid_params ) ) {
return $has_valid_params;
}
$sanitize_params = $request->sanitize_params();
if ( ! $sanitize_params || is_wp_error( $sanitize_params ) ) {
return $sanitize_params;
}
return $request;
}
/**
* AJAX fallback for when REST endpoint is disabled. We try to mock a WP_REST_Request
* and use the same method behind the scenes to make sure we have consistency.
*
* @since 4.9.7
* @since 5.2.1 Look up the POST data before the GET one to process the request.
*/
public function handle_ajax_request() {
// Use the POST method data, if set; else fallback on the GET data.
$source = isset( $_POST ) ? $_POST : $_GET;
$request = $this->get_mocked_rest_request( $source );
if ( is_wp_error( $request ) ) {
/**
* @todo Once we have a error handling on the new view we need to throw it here.
*/
return wp_send_json_error( $request );
}
View::make_for_rest( $request )->send_html();
}
/**
* Check if the REST endpoint is available.
*
* @since 4.9.7
*
* @return boolean If the REST API endpoint is available.
*/
public function is_available() {
$is_available = tribe( 'tec.rest-v1.system' )->tec_rest_api_is_enabled();
/*
* We should run this part of the check only after `rest_authentication_errors` filter ran.
* If we call `WP_REST_Server::check_authentication` before the user will be set to `0` and any following
* auth check will be altered.
*/
if ( static::$did_rest_authentication_errors ) {
/**
* There is no good way to check if rest API is really disabled since `rest_enabled` is deprecated since 4.7
*
* @link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/rest-api/class-wp-rest-server.php#L262
*/
global $wp_rest_server;
if (
! empty( $wp_rest_server )
&& $wp_rest_server instanceof Server
&& ! $wp_rest_server->check_authentication()
) {
$is_available = false;
}
}
/**
* Allows third-party deactivation of the REST Endpoint for just the view V2.
*
* @since 4.9.7
*
* @param boolean $is_available If the REST API endpoint is available.
*/
$is_available = apply_filters( 'tribe_events_views_v2_rest_endpoint_available', $is_available );
return $is_available;
}
/**
* Tracks if the `rest_authentication_errors` filter fired or not, using this filter as an action.
*
* This is a work-around fro the lack of the `did_filter` function.
*
* @since 4.9.12
*
* @param mixed $errors The authentication error, if any, unused by the method.
*
* @return mixed The authentication error.
*/
public static function did_rest_authentication_errors( $errors = null ) {
remove_filter( 'rest_authentication_errors', [ static::class, 'did_rest_authentication_errors' ] );
static::$did_rest_authentication_errors = true;
return $errors;
}
/**
* Returns the filtered HTTP method Views should use to fetch their content from the backend endpoint.
*
* @since 5.2.1
*
* @return string The filtered HTTP method Views should use to fetch their content from the back-end endpoint.
*/
public function get_method() {
/**
* Filters the HTTP method Views should use to fetch their contents calling the back-end endpoint.
*
* @since 5.2.1
* @since 6.11.1 Changed default to `GET`, for performance reasons.
*
* @param string $method The HTTP method Views will use to fetch their content. Either `POST` (default) or
* `GET`. Invalid values will be set to the default `POST`.
*/
$method = strtoupper( (string) apply_filters( 'tribe_events_views_v2_endpoint_method', 'GET' ) );
$method = in_array( $method, [ 'POST', 'GET' ], true ) ? $method : 'POST';
return $method;
}
}