Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
562 lines (478 sloc)
15.6 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace WPGraphQL; | |
use GraphQL\Error\FormattedError; | |
use GraphQL\Executor\ExecutionResult; | |
use WP_User; | |
/** | |
* Class Router | |
* This sets up the /graphql endpoint | |
* | |
* @package WPGraphQL | |
* @since 0.0.1 | |
*/ | |
class Router { | |
/** | |
* Sets the route to use as the endpoint | |
* | |
* @var string $route | |
*/ | |
public static $route = 'graphql'; | |
/** | |
* Holds the Global Post for later resetting | |
* | |
* @var string | |
*/ | |
protected static $global_post = ''; | |
/** | |
* Set the default status code to 200. | |
* | |
* @var int | |
*/ | |
public static $http_status_code = 200; | |
/** | |
* Router constructor. | |
* | |
* @since 0.0.1 | |
*/ | |
public function __construct() { | |
/** | |
* Pass the route through a filter in case the endpoint /graphql should need to be changed | |
* | |
* @return string | |
* @since 0.0.1 | |
*/ | |
$filtered_endpoint = apply_filters( 'graphql_endpoint', null ); | |
$endpoint = $filtered_endpoint ? $filtered_endpoint : get_graphql_setting( 'graphql_endpoint', 'graphql' ); | |
self::$route = $endpoint; | |
/** | |
* Create the rewrite rule for the route | |
* | |
* @since 0.0.1 | |
*/ | |
add_action( 'init', [ $this, 'add_rewrite_rule' ], 10 ); | |
/** | |
* Add the query var for the route | |
* | |
* @since 0.0.1 | |
*/ | |
add_filter( 'query_vars', [ $this, 'add_query_var' ], 1, 1 ); | |
/** | |
* Redirects the route to the graphql processor | |
* | |
* @since 0.0.1 | |
*/ | |
add_action( 'parse_request', [ $this, 'resolve_http_request' ], 10 ); | |
/** | |
* Adds support for application passwords | |
*/ | |
add_filter( 'application_password_is_api_request', [ $this, 'is_api_request' ] ); | |
} | |
/** | |
* Adds rewrite rule for the route endpoint | |
* | |
* @return void | |
* @since 0.0.1 | |
* @uses add_rewrite_rule() | |
*/ | |
public static function add_rewrite_rule() { | |
add_rewrite_rule( | |
self::$route . '/?$', | |
'index.php?' . self::$route . '=true', | |
'top' | |
); | |
} | |
/** | |
* Determines whether the request is an API request to play nice with | |
* application passwords and potential other WordPress core functionality | |
* for APIs | |
* | |
* @param bool $is_api_request Whether the request is an API request | |
* | |
* @return bool | |
*/ | |
public function is_api_request( $is_api_request ) { | |
return true === is_graphql_http_request() ? true : $is_api_request; | |
} | |
/** | |
* Adds the query_var for the route | |
* | |
* @param array $query_vars The array of whitelisted query variables. | |
* | |
* @return array | |
* @since 0.0.1 | |
*/ | |
public static function add_query_var( $query_vars ) { | |
$query_vars[] = self::$route; | |
return $query_vars; | |
} | |
/** | |
* Returns true when the current request is a GraphQL request coming from the HTTP | |
* | |
* NOTE: This will only indicate whether the GraphQL Request is an HTTP request. Many features | |
* need to affect _all_ GraphQL requests, including internal requests using the `graphql()` | |
* function, so be careful how you use this to check your conditions. | |
* | |
* @return boolean | |
*/ | |
public static function is_graphql_http_request() { | |
/** | |
* Filter whether the request is a GraphQL HTTP Request. Default is null, as the majority | |
* of WordPress requests are NOT GraphQL requests (at least today that's true 😆). | |
* | |
* If this filter returns anything other than null, the function will return now and skip the | |
* default checks. | |
* | |
* @param ?bool $is_graphql_http_request Whether the request is a GraphQL HTTP Request. Default false. | |
*/ | |
$pre_is_graphql_http_request = apply_filters( 'graphql_pre_is_graphql_http_request', null ); | |
/** | |
* If the filter has been applied, return now before executing default checks | |
*/ | |
if ( null !== $pre_is_graphql_http_request ) { | |
return (bool) $pre_is_graphql_http_request; | |
} | |
// Default is false | |
$is_graphql_http_request = false; | |
// Support wp-graphiql style request to /index.php?graphql. | |
if ( isset( $_GET[ self::$route ] ) ) { | |
$is_graphql_http_request = true; | |
} else { | |
// Check the server to determine if the GraphQL endpoint is being requested | |
if ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) { | |
$host = wp_unslash( $_SERVER['HTTP_HOST'] ); | |
$uri = wp_unslash( $_SERVER['REQUEST_URI'] ); | |
if ( ! is_string( $host ) ) { | |
return false; | |
} | |
if ( ! is_string( $uri ) ) { | |
return false; | |
} | |
$parsed_site_url = parse_url( site_url( self::$route ), PHP_URL_PATH ); | |
$graphql_url = ! empty( $parsed_site_url ) ? wp_unslash( $parsed_site_url ) : self::$route; | |
$parsed_request_url = parse_url( $uri, PHP_URL_PATH ); | |
$request_url = ! empty( $parsed_request_url ) ? wp_unslash( $parsed_request_url ) : ''; | |
// Determine if the route is indeed a graphql request | |
$is_graphql_http_request = str_replace( '/', '', $request_url ) === str_replace( '/', '', $graphql_url ); | |
} | |
} | |
/** | |
* Filter whether the request is a GraphQL HTTP Request. Default is false, as the majority | |
* of WordPress requests are NOT GraphQL requests (at least today that's true 😆). | |
* | |
* The request has to "prove" that it is indeed an HTTP request via HTTP for | |
* this to be true. | |
* | |
* Different servers _might_ have different needs to determine whether a request | |
* is a GraphQL request. | |
* | |
* @param boolean $is_graphql_http_request Whether the request is a GraphQL HTTP Request. Default false. | |
*/ | |
return apply_filters( 'graphql_is_graphql_http_request', $is_graphql_http_request ); | |
} | |
/** | |
* DEPRECATED: Returns whether a request is a GraphQL Request. Deprecated | |
* because it's name is a bit misleading. This will only return if the request | |
* is a GraphQL request coming from the HTTP endpoint. Internal GraphQL requests | |
* won't be able to use this to properly determine if the request is a GraphQL request | |
* or not. | |
* | |
* @return boolean | |
* @deprecated 0.4.1 Use Router::is_graphql_http_request instead. This now resolves to it | |
*/ | |
public static function is_graphql_request() { | |
return self::is_graphql_http_request(); | |
} | |
/** | |
* This resolves the http request and ensures that WordPress can respond with the appropriate | |
* JSON response instead of responding with a template from the standard WordPress Template | |
* Loading process | |
* | |
* @return void | |
* @throws \Exception Throws exception. | |
* @throws \Throwable Throws exception. | |
* @since 0.0.1 | |
*/ | |
public static function resolve_http_request() { | |
/** | |
* Access the $wp_query object | |
*/ | |
global $wp_query; | |
/** | |
* Ensure we're on the registered route for graphql route | |
*/ | |
if ( ! self::is_graphql_http_request() || is_graphql_request() ) { | |
return; | |
} | |
/** | |
* Set is_home to false | |
*/ | |
$wp_query->is_home = false; | |
/** | |
* Whether it's a GraphQL HTTP Request | |
* | |
* @since 0.0.5 | |
*/ | |
if ( ! defined( 'GRAPHQL_HTTP_REQUEST' ) ) { | |
define( 'GRAPHQL_HTTP_REQUEST', true ); | |
} | |
/** | |
* Process the GraphQL query Request | |
*/ | |
self::process_http_request(); | |
} | |
/** | |
* Sends an HTTP header. | |
* | |
* @param string $key Header key. | |
* @param string $value Header value. | |
* | |
* @return void | |
* @since 0.0.5 | |
*/ | |
public static function send_header( $key, $value ) { | |
/** | |
* Sanitize as per RFC2616 (Section 4.2): | |
* | |
* Any LWS that occurs between field-content MAY be replaced with a | |
* single SP before interpreting the field value or forwarding the | |
* message downstream. | |
*/ | |
$value = preg_replace( '/\s+/', ' ', $value ); | |
header( apply_filters( 'graphql_send_header', sprintf( '%s: %s', $key, $value ), $key, $value ) ); | |
} | |
/** | |
* Sends an HTTP status code. | |
* | |
* @return void | |
*/ | |
protected static function set_status() { | |
status_header( self::$http_status_code ); | |
} | |
/** | |
* Returns an array of headers to send with the HTTP response | |
* | |
* @return array | |
*/ | |
protected static function get_response_headers() { | |
/** | |
* Filtered list of access control headers. | |
* | |
* @param array $access_control_headers Array of headers to allow. | |
*/ | |
$access_control_allow_headers = apply_filters( | |
'graphql_access_control_allow_headers', | |
[ | |
'Authorization', | |
'Content-Type', | |
] | |
); | |
$headers = [ | |
'Access-Control-Allow-Origin' => '*', | |
'Access-Control-Allow-Headers' => implode( ', ', $access_control_allow_headers ), | |
'Access-Control-Max-Age' => 600, | |
// cache the result of preflight requests (600 is the upper limit for Chromium). | |
'Content-Type' => 'application/json ; charset=' . get_option( 'blog_charset' ), | |
'X-Robots-Tag' => 'noindex', | |
'X-Content-Type-Options' => 'nosniff', | |
]; | |
if ( true === \WPGraphQL::debug() ) { | |
$headers['X-hacker'] = __( 'If you\'re reading this, you should visit github.com/wp-graphql/wp-graphql and contribute!', 'wp-graphql' ); | |
} | |
/** | |
* Send nocache headers on authenticated requests. | |
* | |
* @param bool $rest_send_nocache_headers Whether to send no-cache headers. | |
* | |
* @since 0.0.5 | |
*/ | |
$send_no_cache_headers = apply_filters( 'graphql_send_nocache_headers', is_user_logged_in() ); | |
if ( $send_no_cache_headers ) { | |
foreach ( wp_get_nocache_headers() as $no_cache_header_key => $no_cache_header_value ) { | |
$headers[ $no_cache_header_key ] = $no_cache_header_value; | |
} | |
} | |
/** | |
* Filter the $headers to send | |
*/ | |
return apply_filters( 'graphql_response_headers_to_send', $headers ); | |
} | |
/** | |
* Set the response headers | |
* | |
* @return void | |
* @since 0.0.1 | |
*/ | |
public static function set_headers() { | |
if ( false === headers_sent() ) { | |
/** | |
* Set the HTTP response status | |
*/ | |
self::set_status(); | |
/** | |
* Get the response headers | |
*/ | |
$headers = self::get_response_headers(); | |
/** | |
* If there are headers, set them for the response | |
*/ | |
if ( ! empty( $headers ) && is_array( $headers ) ) { | |
foreach ( $headers as $key => $value ) { | |
self::send_header( $key, $value ); | |
} | |
} | |
/** | |
* Fire an action when the headers are set | |
* | |
* @param array $headers The headers sent in the response | |
*/ | |
do_action( 'graphql_response_set_headers', $headers ); | |
} | |
} | |
/** | |
* Retrieves the raw request entity (body). | |
* | |
* @return string Raw request data. | |
* @global string php://input Raw post data. | |
* @since 0.0.5 | |
*/ | |
public static function get_raw_data() { | |
$input = file_get_contents( 'php://input' ); | |
return ! empty( $input ) ? $input : ''; | |
} | |
/** | |
* This processes the graphql requests that come into the /graphql endpoint via an HTTP request | |
* | |
* @return mixed | |
* @throws \Exception Throws Exception. | |
* @throws \Throwable Throws Exception. | |
* @global WP_User $current_user The currently authenticated user. | |
* @since 0.0.1 | |
*/ | |
public static function process_http_request() { | |
/* @var WP_User|null $current_user */ | |
global $current_user; | |
if ( $current_user instanceof WP_User && ! $current_user->exists() ) { | |
/* | |
* If there is no current user authenticated via other means, clear | |
* the cached lack of user, so that an authenticate check can set it | |
* properly. | |
* | |
* This is done because for authentications such as Application | |
* Passwords, we don't want it to be accepted unless the current HTTP | |
* request is a GraphQL API request, which can't always be identified early | |
* enough in evaluation. | |
* | |
* See serve_request in wp-includes/rest-api/class-wp-rest-server.php. | |
*/ | |
$current_user = null; | |
} | |
/** | |
* This action can be hooked to to enable various debug tools, | |
* such as enableValidation from the GraphQL Config. | |
* | |
* @since 0.0.4 | |
*/ | |
do_action( 'graphql_process_http_request' ); | |
/** | |
* Respond to pre-flight requests. | |
* | |
* @see: https://apollographql.slack.com/archives/C10HTKHPC/p1507649812000123 | |
* @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests | |
*/ | |
if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'OPTIONS' === $_SERVER['REQUEST_METHOD'] ) { | |
self::$http_status_code = 200; | |
self::set_headers(); | |
exit; | |
} | |
$query = ''; | |
$operation_name = ''; | |
$variables = []; | |
$request = new Request(); | |
try { | |
$response = $request->execute_http(); | |
// Get the operation params from the request. | |
$params = $request->get_params(); | |
$query = isset( $params->query ) ? $params->query : ''; | |
$operation_name = isset( $params->operation ) ? $params->operation : ''; | |
$variables = isset( $params->variables ) ? $params->variables : null; | |
} catch ( \Exception $error ) { | |
/** | |
* If there are errors, set the status to 500 | |
* and format the captured errors to be output properly | |
* | |
* @since 0.0.4 | |
*/ | |
self::$http_status_code = 500; | |
/** | |
* Filter thrown GraphQL errors | |
* | |
* @param array $errors Formatted errors object. | |
* @param \Exception $error Thrown error. | |
* @param \WPGraphQL\Request $request WPGraphQL Request object. | |
*/ | |
$response['errors'] = apply_filters( | |
'graphql_http_request_response_errors', | |
[ FormattedError::createFromException( $error, $request->get_debug_flag() ) ], | |
$error, | |
$request | |
); | |
} // End try(). | |
// Previously there was a small distinction between the response and the result, but | |
// now that we are delegating to Request, just send the response for both. | |
$result = $response; | |
if ( false === headers_sent() ) { | |
self::prepare_headers( $response, $result, $query, $operation_name, $variables ); | |
} | |
/** | |
* Run an action after the HTTP Response is ready to be sent back. This might be a good place for tools | |
* to hook in to track metrics, such as how long the process took from `graphql_process_http_request` | |
* to here, etc. | |
* | |
* @param array $response The GraphQL response | |
* @param array $result The result of the GraphQL Query | |
* @param string $operation_name The name of the operation | |
* @param string $query The request that GraphQL executed | |
* @param ?array $variables Variables to passed to your GraphQL query | |
* @param mixed $status_code The status code for the response | |
* | |
* @since 0.0.5 | |
*/ | |
do_action( 'graphql_process_http_request_response', $response, $result, $operation_name, $query, $variables, self::$http_status_code ); | |
/** | |
* Send the response | |
*/ | |
wp_send_json( $response ); | |
} | |
/** | |
* Prepare headers for response | |
* | |
* @param mixed|array|ExecutionResult $response The response of the GraphQL Request. | |
* @param mixed|array|ExecutionResult $graphql_results The results of the GraphQL execution. | |
* @param string $query The GraphQL query. | |
* @param string $operation_name The operation name of the GraphQL | |
* Request. | |
* @param mixed|array|null $variables The variables applied to the GraphQL | |
* Request. | |
* @param mixed|WP_User|null $user The current user object. | |
* | |
* @return void | |
*/ | |
protected static function prepare_headers( $response, $graphql_results, string $query, string $operation_name, $variables, $user = null ) { | |
/** | |
* Filter the $status_code before setting the headers | |
* | |
* @param int $status_code The status code to apply to the headers | |
* @param array $response The response of the GraphQL Request | |
* @param array $graphql_results The results of the GraphQL execution | |
* @param string $query The GraphQL query | |
* @param string $operation_name The operation name of the GraphQL Request | |
* @param array $variables The variables applied to the GraphQL Request | |
* @param WP_User $user The current user object | |
*/ | |
self::$http_status_code = apply_filters( 'graphql_response_status_code', self::$http_status_code, $response, $graphql_results, $query, $operation_name, $variables, $user ); | |
/** | |
* Set the response headers | |
*/ | |
self::set_headers(); | |
} | |
} |