File: /var/www/html/wp-content/plugins/wp-graphql/src/Server/ValidationRules/QueryDepth.php
<?php
namespace WPGraphQL\Server\ValidationRules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Validator\Rules\QuerySecurityRule;
/**
* Class QueryDepth
*
* @package WPGraphQL\Server\ValidationRules
*/
class QueryDepth extends QuerySecurityRule {
/**
* The max query depth allowed.
*/
private int $maxQueryDepth;
/**
* QueryDepth constructor.
*/
public function __construct() {
$max_query_depth = get_graphql_setting( 'query_depth_max', 10 );
$max_query_depth = absint( $max_query_depth ) ?? 10;
$this->setMaxQueryDepth( $max_query_depth );
}
/**
* {@inheritDoc}
*/
public function getVisitor( \GraphQL\Validator\QueryValidationContext $context ): array {
return $this->invokeIfNeeded(
$context,
[
NodeKind::OPERATION_DEFINITION => [
'leave' => function ( Node $node ) use ( $context ): void {
if ( ! $node instanceof OperationDefinitionNode ) {
return;
}
$maxDepth = $this->fieldDepth( $node );
if ( $maxDepth <= $this->getMaxQueryDepth() ) {
return;
}
$context->reportError(
new Error( $this->errorMessage( $this->getMaxQueryDepth(), $maxDepth ) )
);
},
],
]
);
}
/**
* Determine field depth
*
* @param \GraphQL\Language\AST\Node $node The node being analyzed
* @param int $depth The depth of the field. Default is 0
* @param int $maxDepth The max depth allowed. Default is 0
*/
private function fieldDepth( Node $node, int $depth = 0, int $maxDepth = 0 ): int {
if ( isset( $node->selectionSet ) && $node->selectionSet instanceof SelectionSetNode ) {
foreach ( $node->selectionSet->selections as $childNode ) {
$maxDepth = $this->nodeDepth( $childNode, $depth, $maxDepth );
}
}
return $maxDepth;
}
/**
* Determine node depth
*
* @param \GraphQL\Language\AST\Node $node The node being analyzed in the operation
* @param int $depth The depth of the operation
* @param int $maxDepth The Max Depth of the operation
*/
private function nodeDepth( Node $node, int $depth, int $maxDepth ): int {
switch ( true ) {
case $node instanceof FieldNode:
// node has children?
if ( isset( $node->selectionSet ) ) {
// update maxDepth if needed
if ( $depth > $maxDepth ) {
$maxDepth = $depth;
}
$maxDepth = $this->fieldDepth( $node, $depth + 1, $maxDepth );
}
break;
case $node instanceof InlineFragmentNode:
// node has children?
$maxDepth = $this->fieldDepth( $node, $depth, $maxDepth );
break;
case $node instanceof FragmentSpreadNode:
$fragment = $this->getFragment( $node );
if ( null !== $fragment ) {
$maxDepth = $this->fieldDepth( $fragment, $depth, $maxDepth );
}
break;
}
return $maxDepth;
}
/**
* Return the maxQueryDepth allowed
*
* @return int
*/
public function getMaxQueryDepth() {
return $this->maxQueryDepth;
}
/**
* Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0.
*
* @param int $maxQueryDepth The max query depth to allow for GraphQL operations
*
* @return void
*/
public function setMaxQueryDepth( int $maxQueryDepth ) {
$this->checkIfGreaterOrEqualToZero( 'maxQueryDepth', $maxQueryDepth );
$this->maxQueryDepth = $maxQueryDepth;
}
/**
* Return the max query depth error message
*
* @param int $max The max number of levels to allow in GraphQL operation
* @param int $count The number of levels in the current operation
*
* @return string
*/
public function errorMessage( $max, $count ) {
return sprintf( 'The server administrator has limited the max query depth to %d, but the requested query has %d levels.', $max, $count );
}
/**
* Determine whether the rule should be enabled
*/
protected function isEnabled(): bool {
$is_enabled = false;
$enabled = get_graphql_setting( 'query_depth_enabled', 'off' );
if ( 'on' === $enabled && absint( $this->getMaxQueryDepth() ) && 1 <= $this->getMaxQueryDepth() ) {
$is_enabled = true;
}
return $is_enabled;
}
}