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.
789 lines (671 sloc)
22.1 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 Exception; | |
use GraphQL\Error\DebugFlag; | |
use GraphQL\Error\Error; | |
use GraphQL\GraphQL; | |
use GraphQL\Server\OperationParams; | |
use GraphQL\Server\ServerConfig; | |
use GraphQL\Server\StandardServer; | |
use WPGraphQL\Server\ValidationRules\DisableIntrospection; | |
use WP_Post; | |
use WP_Query; | |
use WPGraphQL\Server\ValidationRules\QueryDepth; | |
use WPGraphQL\Server\ValidationRules\RequireAuthentication; | |
use WPGraphQL\Server\WPHelper; | |
use WPGraphQL\Utils\DebugLog; | |
/** | |
* Class Request | |
* | |
* Proxies a request to graphql-php, applying filters and transforming request | |
* data as needed. | |
* | |
* @package WPGraphQL | |
*/ | |
class Request { | |
/** | |
* App context for this request. | |
* | |
* @var AppContext | |
*/ | |
public $app_context; | |
/** | |
* Request data. | |
* | |
* @var mixed|array|OperationParams | |
*/ | |
public $data; | |
/** | |
* Cached global post. | |
* | |
* @var ?WP_Post | |
*/ | |
public $global_post; | |
/** | |
* Cached global wp_the_query. | |
* | |
* @var ?WP_Query | |
*/ | |
private $global_wp_the_query; | |
/** | |
* GraphQL operation parameters for this request. Can also be an array of | |
* OperationParams. | |
* | |
* @var mixed|array|OperationParams|OperationParams[] | |
*/ | |
public $params; | |
/** | |
* Schema for this request. | |
* | |
* @var WPSchema | |
*/ | |
public $schema; | |
/** | |
* Debug log for WPGraphQL Requests | |
* | |
* @var DebugLog | |
*/ | |
public $debug_log; | |
/** | |
* The Type Registry the Schema is built with | |
* | |
* @var Registry\TypeRegistry | |
*/ | |
public $type_registry; | |
/** | |
* Validation rules for execution. | |
* | |
* @var array | |
*/ | |
protected $validation_rules; | |
/** | |
* The default field resolver function. Default null | |
* | |
* @var mixed|callable|null | |
*/ | |
protected $field_resolver; | |
/** | |
* The root value of the request. Default null; | |
* | |
* @var mixed | |
*/ | |
protected $root_value; | |
/** | |
* Constructor | |
* | |
* @param array $data The request data (for non-HTTP requests). | |
* | |
* @return void | |
* | |
* @throws Exception | |
*/ | |
public function __construct( array $data = [] ) { | |
/** | |
* Whether it's a GraphQL Request (http or internal) | |
* | |
* @since 0.0.5 | |
*/ | |
if ( ! defined( 'GRAPHQL_REQUEST' ) ) { | |
define( 'GRAPHQL_REQUEST', true ); | |
} | |
/** | |
* Filter "is_graphql_request" to return true | |
*/ | |
\WPGraphQL::set_is_graphql_request( true ); | |
/** | |
* Action – intentionally with no context – to indicate a GraphQL Request has started. | |
* This is a great place for plugins to hook in and modify things that should only | |
* occur in the context of a GraphQL Request. The base class hooks into this action to | |
* kick off the schema creation, so types are not set up until this action has run! | |
*/ | |
do_action( 'init_graphql_request' ); | |
// Start tracking debug log messages | |
$this->debug_log = new DebugLog(); | |
// Set request data for passed-in (non-HTTP) requests. | |
$this->data = $data; | |
// Get the Type Registry | |
$this->type_registry = \WPGraphQL::get_type_registry(); | |
// Get the Schema | |
$this->schema = \WPGraphQL::get_schema(); | |
// Get the App Context | |
$this->app_context = \WPGraphQL::get_app_context(); | |
$this->root_value = $this->get_root_value(); | |
$this->validation_rules = $this->get_validation_rules(); | |
$this->field_resolver = $this->get_field_resolver(); | |
/** | |
* Configure the app_context which gets passed down to all the resolvers. | |
* | |
* @since 0.0.4 | |
*/ | |
$app_context = new AppContext(); | |
$app_context->viewer = wp_get_current_user(); | |
$app_context->root_url = get_bloginfo( 'url' ); | |
$app_context->request = ! empty( $_REQUEST ) ? $_REQUEST : null; // phpcs:ignore | |
$app_context->type_registry = $this->type_registry; | |
$this->app_context = $app_context; | |
} | |
/** | |
* @return null | |
*/ | |
protected function get_field_resolver() { | |
return null; | |
} | |
/** | |
* Return the validation rules to use in the request | |
* | |
* @return array | |
*/ | |
protected function get_validation_rules() { | |
$validation_rules = GraphQL::getStandardValidationRules(); | |
$validation_rules['require_authentication'] = new RequireAuthentication(); | |
$validation_rules['disable_introspection'] = new DisableIntrospection(); | |
$validation_rules['query_depth'] = new QueryDepth(); | |
/** | |
* Return the validation rules to use in the request | |
* | |
* @param array $validation_rules The validation rules to use in the request | |
* @param Request $request The Request instance | |
*/ | |
return apply_filters( 'graphql_validation_rules', $validation_rules, $this ); | |
} | |
/** | |
* Returns the root value to use in the request. | |
* | |
* @return mixed|null | |
*/ | |
protected function get_root_value() { | |
/** | |
* Set the root value based on what was passed to the request | |
*/ | |
$root_value = isset( $this->data['root_value'] ) && ! empty( $this->data['root_value'] ) ? $this->data['root_value'] : null; | |
/** | |
* Return the filtered root value | |
* | |
* @param mixed $root_value The root value the Schema should use to resolve with. Default null. | |
* @param Request $request The Request instance | |
*/ | |
return apply_filters( 'graphql_root_value', $root_value, $this ); | |
} | |
/** | |
* Apply filters and do actions before GraphQL execution | |
* | |
* @return void | |
* @throws Error | |
*/ | |
private function before_execute() { | |
/** | |
* Store the global post so it can be reset after GraphQL execution | |
* | |
* This allows for a GraphQL query to be used in the middle of post content, such as in a Shortcode | |
* without disrupting the flow of the post as the global POST before and after GraphQL execution will be | |
* the same. | |
*/ | |
if ( ! empty( $GLOBALS['post'] ) ) { | |
$this->global_post = $GLOBALS['post']; | |
} | |
if ( ! empty( $GLOBALS['wp_query'] ) ) { | |
$this->global_wp_the_query = clone $GLOBALS['wp_the_query']; | |
} | |
/** | |
* If the request is a batch request it will come back as an array | |
*/ | |
if ( is_array( $this->params ) ) { | |
// If the request is a batch request, but batch requests are disabled, | |
// bail early | |
if ( ! $this->is_batch_queries_enabled() ) { | |
throw new Error( __( 'Batch Queries are not supported', 'wp-graphql' ) ); | |
} | |
$batch_limit = get_graphql_setting( 'batch_limit', 10 ); | |
$batch_limit = absint( $batch_limit ) ? absint( $batch_limit ) : 10; | |
// If batch requests are enabled, but a limit is set and the request exceeds the limit | |
// fail now | |
if ( $batch_limit < count( $this->params ) ) { | |
// translators: First placeholder is the max number of batch operations allowed in a GraphQL request. The 2nd placeholder is the number of operations requested in the current request. | |
throw new Error( sprintf( __( 'Batch requests are limited to %1$d operations. This request contained %2$d', 'wp-graphql' ), absint( $batch_limit ), count( $this->params ) ) ); | |
} | |
/** | |
* Execute batch queries | |
* | |
* @param OperationParams[] $params The operation params of the batch request | |
*/ | |
do_action( 'graphql_execute_batch_queries', $this->params ); | |
// Process the batched requests | |
array_walk( $this->params, [ $this, 'do_action' ] ); | |
} else { | |
$this->do_action( $this->params ); | |
} | |
/** | |
* This action runs before execution of a GraphQL request (regardless if it's a single or batch request) | |
* | |
* @param Request $request The instance of the Request being executed | |
*/ | |
do_action( 'graphql_before_execute', $this ); | |
} | |
/** | |
* Checks authentication errors. | |
* | |
* False will mean there are no detected errors and | |
* execution will continue. | |
* | |
* Anything else (true, WP_Error, thrown exception, etc) will prevent execution of the GraphQL | |
* request. | |
* | |
* @return boolean | |
* @throws Exception | |
*/ | |
protected function has_authentication_errors() { | |
/** | |
* Bail if this is not an HTTP request. | |
* | |
* Auth for internal requests will happen | |
* via WordPress internals. | |
*/ | |
if ( ! is_graphql_http_request() ) { | |
return false; | |
} | |
/** | |
* Access the global $wp_rest_auth_cookie | |
*/ | |
global $wp_rest_auth_cookie; | |
/** | |
* Default state of the authentication errors | |
*/ | |
$authentication_errors = false; | |
/** | |
* Is cookie authentication NOT being used? | |
* | |
* If we get an auth error, but the user is still logged in, another auth mechanism | |
* (JWT, oAuth, etc) must have been used. | |
*/ | |
if ( true !== $wp_rest_auth_cookie && is_user_logged_in() ) { | |
/** | |
* Return filtered authentication errors | |
*/ | |
return $this->filtered_authentication_errors( $authentication_errors ); | |
/** | |
* If the user is not logged in, determine if there's a nonce | |
*/ | |
} else { | |
$nonce = null; | |
if ( isset( $_REQUEST['_wpnonce'] ) ) { | |
$nonce = $_REQUEST['_wpnonce']; | |
} elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) { | |
$nonce = $_SERVER['HTTP_X_WP_NONCE']; | |
} | |
if ( null === $nonce ) { | |
// No nonce at all, so act as if it's an unauthenticated request. | |
wp_set_current_user( 0 ); | |
return $this->filtered_authentication_errors( $authentication_errors ); | |
} | |
// Check the nonce. | |
$result = wp_verify_nonce( $nonce, 'wp_rest' ); | |
if ( ! $result ) { | |
throw new Exception( __( 'Cookie nonce is invalid', 'wp-graphql' ) ); | |
} | |
} | |
/** | |
* Return the filtered authentication errors | |
*/ | |
return $this->filtered_authentication_errors( $authentication_errors ); | |
} | |
/** | |
* Filter Authentication errors. Allows plugins that authenticate to hook in and prevent | |
* execution if Authentication errors exist. | |
* | |
* @param boolean $authentication_errors Whether there are authentication errors with the | |
* request | |
* | |
* @return boolean | |
*/ | |
protected function filtered_authentication_errors( $authentication_errors = false ) { | |
/** | |
* If false, there are no authentication errors. If true, execution of the | |
* GraphQL request will be prevented and an error will be thrown. | |
* | |
* @param boolean $authentication_errors Whether there are authentication errors with the request | |
* @param Request $request Instance of the Request | |
*/ | |
return apply_filters( 'graphql_authentication_errors', $authentication_errors, $this ); | |
} | |
/** | |
* Performs actions and runs filters after execution completes | |
* | |
* @param mixed|array|object $response The response from execution. Array for batch requests, | |
* single object for individual requests | |
* | |
* @return array | |
* | |
* @throws Exception | |
*/ | |
private function after_execute( $response ) { | |
/** | |
* If there are authentication errors, prevent execution and throw an exception. | |
*/ | |
if ( false !== $this->has_authentication_errors() ) { | |
throw new Exception( __( 'Authentication Error', 'wp-graphql' ) ); | |
} | |
/** | |
* If the params and the $response are both arrays | |
* treat this as a batch request and map over the array to apply the | |
* after_execute_actions, otherwise apply them to the current response | |
*/ | |
if ( is_array( $this->params ) && is_array( $response ) ) { | |
$filtered_response = []; | |
foreach ( $response as $key => $resp ) { | |
$filtered_response[] = $this->after_execute_actions( $resp, (int) $key ); | |
} | |
} else { | |
$filtered_response = $this->after_execute_actions( $response, null ); | |
} | |
/** | |
* Reset the global post after execution | |
* | |
* This allows for a GraphQL query to be used in the middle of post content, such as in a Shortcode | |
* without disrupting the flow of the post as the global POST before and after GraphQL execution will be | |
* the same. | |
* | |
* We cannot use wp_reset_postdata here because it just resets the post from the global query which can | |
* be anything the because the resolvers themself can set it to whatever. So we just manually reset the | |
* post with setup_postdata we cached before this request. | |
*/ | |
if ( ! empty( $this->global_wp_the_query ) ) { | |
$GLOBALS['wp_the_query'] = $this->global_wp_the_query; | |
wp_reset_query(); | |
} | |
if ( ! empty( $this->global_post ) ) { | |
$GLOBALS['post'] = $this->global_post; | |
setup_postdata( $this->global_post ); | |
} | |
/** | |
* Run an action after GraphQL Execution | |
* | |
* @param array $filtered_response The response of the entire operation. Could be a single operation or a batch operation | |
* @param Request $request Instance of the Request being executed | |
*/ | |
do_action( 'graphql_after_execute', $filtered_response, $this ); | |
/** | |
* Return the filtered response | |
*/ | |
return $filtered_response; | |
} | |
/** | |
* Apply filters and do actions after GraphQL execution | |
* | |
* @param mixed|array|object $response The response for your GraphQL request | |
* @param mixed|Int|null $key The array key of the params for batch requests | |
* | |
* @return array | |
*/ | |
private function after_execute_actions( $response, $key = null ) { | |
/** | |
* Determine which params (batch or single request) to use when passing through to the actions | |
*/ | |
$query = null; | |
$operation = null; | |
$variables = null; | |
$query_id = null; | |
if ( $this->params instanceof OperationParams ) { | |
$operation = $this->params->operation; | |
$query = $this->params->query; | |
$query_id = $this->params->queryId; | |
$variables = $this->params->variables; | |
} elseif ( is_array( $this->params ) ) { | |
$operation = $this->params[ $key ]->operation ?? ''; | |
$query = $this->params[ $key ]->query ?? ''; | |
$query_id = $this->params[ $key ]->queryId ?? null; | |
$variables = $this->params[ $key ]->variables ?? null; | |
} | |
/** | |
* Run an action. This is a good place for debug tools to hook in to log things, etc. | |
* | |
* @param mixed|array $response The response your GraphQL request | |
* @param WPSchema $schema The schema object for the root request | |
* @param mixed|string|null $operation The name of the operation | |
* @param string $query The query that GraphQL executed | |
* @param array|null $variables Variables to passed to your GraphQL query | |
* @param Request $request Instance of the Request | |
* | |
* @since 0.0.4 | |
*/ | |
do_action( 'graphql_execute', $response, $this->schema, $operation, $query, $variables, $this ); | |
/** | |
* Add the debug log to the request | |
*/ | |
if ( ! empty( $response ) ) { | |
if ( is_array( $response ) ) { | |
$response['extensions']['debug'] = $this->debug_log->get_logs(); | |
} else { | |
$response->extensions['debug'] = $this->debug_log->get_logs(); | |
} | |
} | |
/** | |
* Filter the $response of the GraphQL execution. This allows for the response to be filtered | |
* before it's returned, allowing granular control over the response at the latest point. | |
* | |
* POSSIBLE USAGE EXAMPLES: | |
* This could be used to ensure that certain fields never make it to the response if they match | |
* certain criteria, etc. For example, this filter could be used to check if a current user is | |
* allowed to see certain things, and if they are not, the $response could be filtered to remove | |
* the data they should not be allowed to see. | |
* | |
* Or, perhaps some systems want the response to always include some additional piece of data in | |
* every response, regardless of the request that was sent to it, this could allow for that | |
* to be hooked in and included in the $response. | |
* | |
* @param array $response The response for your GraphQL query | |
* @param WPSchema $schema The schema object for the root query | |
* @param string $operation The name of the operation | |
* @param string $query The query that GraphQL executed | |
* @param array|null $variables Variables to passed to your GraphQL request | |
* @param Request $request Instance of the Request | |
* @param string|null $query_id The query id that GraphQL executed | |
* | |
* @since 0.0.5 | |
*/ | |
$filtered_response = apply_filters( 'graphql_request_results', $response, $this->schema, $operation, $query, $variables, $this, $query_id ); | |
/** | |
* Run an action after the response has been filtered, as the response is being returned. | |
* This is a good place for debug tools to hook in to log things, etc. | |
* | |
* @param array $filtered_response The filtered response for the GraphQL request | |
* @param array $response The response for your GraphQL request | |
* @param WPSchema $schema The schema object for the root request | |
* @param string $operation The name of the operation | |
* @param string $query The query that GraphQL executed | |
* @param array|null $variables Variables to passed to your GraphQL query | |
* @param Request $request Instance of the Request | |
* @param string|null $query_id The query id that GraphQL executed | |
*/ | |
do_action( 'graphql_return_response', $filtered_response, $response, $this->schema, $operation, $query, $variables, $this, $query_id ); | |
/** | |
* Filter "is_graphql_request" back to false. | |
*/ | |
\WPGraphQL::set_is_graphql_request( false ); | |
return $filtered_response; | |
} | |
/** | |
* Run action for a request. | |
* | |
* @param OperationParams $params OperationParams for the request. | |
* | |
* @return void | |
*/ | |
private function do_action( OperationParams $params ) { | |
/** | |
* Run an action for each request. | |
* | |
* @param string $query The GraphQL query | |
* @param string $operation The name of the operation | |
* @param ?array $variables Variables to be passed to your GraphQL request | |
* @param OperationParams $params The Operation Params. This includes any extra params, such as extenions or any other modifications to the request body | |
*/ | |
do_action( 'do_graphql_request', $params->query, $params->operation, $params->variables, $params ); | |
} | |
/** | |
* Execute an internal request (graphql() function call). | |
* | |
* @return array | |
* @throws Exception | |
*/ | |
public function execute() { | |
$helper = new WPHelper(); | |
if ( ! $this->data instanceof OperationParams ) { | |
$this->params = $helper->parseRequestParams( 'POST', $this->data, [] ); | |
} else { | |
$this->params = $this->data; | |
} | |
if ( is_array( $this->params ) ) { | |
return array_map( function ( $data ) { | |
$this->data = $data; | |
return $this->execute(); | |
}, $this->params ); | |
} elseif ( $this->params instanceof OperationParams ) { | |
/** | |
* Initialize the GraphQL Request | |
*/ | |
$this->before_execute(); | |
$response = apply_filters( 'pre_graphql_execute_request', null, $this ); | |
if ( null === $response ) { | |
/** | |
* Allow the query string to be determined by a filter. Ex, when params->queryId is present, query can be retrieved. | |
*/ | |
$query = apply_filters( | |
'graphql_execute_query_params', | |
isset( $this->params->query ) ? $this->params->query : '', | |
$this->params | |
); | |
$result = \GraphQL\GraphQL::executeQuery( | |
$this->schema, | |
$query, | |
$this->root_value, | |
$this->app_context, | |
isset( $this->params->variables ) ? $this->params->variables : null, | |
isset( $this->params->operation ) ? $this->params->operation : null, | |
$this->field_resolver, | |
$this->validation_rules | |
); | |
/** | |
* Return the result of the request | |
*/ | |
$response = $result->toArray( $this->get_debug_flag() ); | |
} | |
/** | |
* Ensure the response is returned as a proper, populated array. Otherwise add an error. | |
*/ | |
if ( empty( $response ) || ! is_array( $response ) ) { | |
$response = [ | |
'errors' => __( 'The GraphQL request returned an invalid response', 'wp-graphql' ), | |
]; | |
} | |
/** | |
* If the request is a batch request it will come back as an array | |
*/ | |
return $this->after_execute( $response ); | |
} | |
return []; | |
} | |
/** | |
* Execute an HTTP request. | |
* | |
* @return array | |
* @throws Exception | |
*/ | |
public function execute_http() { | |
/** | |
* Parse HTTP request. | |
*/ | |
$helper = new WPHelper(); | |
$this->params = $helper->parseHttpRequest(); | |
/** | |
* Initialize the GraphQL Request | |
*/ | |
$this->before_execute(); | |
/** | |
* Get the response. | |
*/ | |
$response = apply_filters( 'pre_graphql_execute_request', null, $this ); | |
/** | |
* If no cached response, execute the query | |
*/ | |
if ( null === $response ) { | |
$server = $this->get_server(); | |
$response = $server->executeRequest( $this->params ); | |
} | |
return $this->after_execute( $response ); | |
} | |
/** | |
* Get the operation params for the request. | |
* | |
* @return OperationParams|OperationParams[] | |
*/ | |
public function get_params() { | |
return $this->params; | |
} | |
/** | |
* Returns the debug flag value | |
* | |
* @return int | |
*/ | |
public function get_debug_flag() { | |
$flag = DebugFlag::INCLUDE_DEBUG_MESSAGE; | |
if ( 0 !== get_current_user_id() ) { | |
// Flag 2 shows the trace data, which should require user to be logged in to see by default | |
$flag = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE; | |
} | |
return true === \WPGraphQL::debug() ? $flag : DebugFlag::NONE; | |
} | |
/** | |
* Determines if batch queries are enabled for the server. | |
* | |
* Default is to have batch queries enabled. | |
* | |
* @return bool | |
*/ | |
private function is_batch_queries_enabled() { | |
$batch_queries_enabled = true; | |
$batch_queries_setting = get_graphql_setting( 'batch_queries_enabled', 'on' ); | |
if ( 'off' === $batch_queries_setting ) { | |
$batch_queries_enabled = false; | |
} | |
/** | |
* Filter whether batch queries are supported or not | |
* | |
* @param boolean $batch_queries_enabled Whether Batch Queries should be enabled | |
* @param OperationParams $params Request operation params | |
*/ | |
return apply_filters( 'graphql_is_batch_queries_enabled', $batch_queries_enabled, $this->params ); | |
} | |
/** | |
* Create the GraphQL server that will process the request. | |
* | |
* @return StandardServer | |
*/ | |
private function get_server() { | |
$debug_flag = $this->get_debug_flag(); | |
$config = new ServerConfig(); | |
$config | |
->setDebugFlag( $debug_flag ) | |
->setSchema( $this->schema ) | |
->setContext( $this->app_context ) | |
->setValidationRules( $this->validation_rules ) | |
->setQueryBatching( $this->is_batch_queries_enabled() ); | |
if ( ! empty( $this->root_value ) ) { | |
$config->setFieldResolver( $this->root_value ); | |
} | |
if ( ! empty( $this->field_resolver ) ) { | |
$config->setFieldResolver( $this->field_resolver ); | |
} | |
/** | |
* Run an action when the server config is created. The config can be acted | |
* upon directly to override default values or implement new features, e.g., | |
* $config->setValidationRules(). | |
* | |
* @param ServerConfig $config Server config | |
* @param OperationParams $params Request operation params | |
* | |
* @since 0.2.0 | |
*/ | |
do_action( 'graphql_server_config', $config, $this->params ); | |
return new StandardServer( $config ); | |
} | |
} |