301 lines
9.3 KiB
PHP
301 lines
9.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace GraphQL\Validator\Rules;
|
|
|
|
use ArrayObject;
|
|
use GraphQL\Error\Error;
|
|
use GraphQL\Executor\Values;
|
|
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\Language\Visitor;
|
|
use GraphQL\Language\VisitorOperation;
|
|
use GraphQL\Type\Definition\Directive;
|
|
use GraphQL\Type\Definition\FieldDefinition;
|
|
use GraphQL\Validator\ValidationContext;
|
|
use function array_map;
|
|
use function count;
|
|
use function implode;
|
|
use function method_exists;
|
|
use function sprintf;
|
|
|
|
class QueryComplexity extends QuerySecurityRule
|
|
{
|
|
/** @var int */
|
|
private $maxQueryComplexity;
|
|
|
|
/** @var mixed[]|null */
|
|
private $rawVariableValues = [];
|
|
|
|
/** @var ArrayObject */
|
|
private $variableDefs;
|
|
|
|
/** @var ArrayObject */
|
|
private $fieldNodeAndDefs;
|
|
|
|
/** @var ValidationContext */
|
|
private $context;
|
|
|
|
/** @var int */
|
|
private $complexity;
|
|
|
|
public function __construct($maxQueryComplexity)
|
|
{
|
|
$this->setMaxQueryComplexity($maxQueryComplexity);
|
|
}
|
|
|
|
public function getVisitor(ValidationContext $context)
|
|
{
|
|
$this->context = $context;
|
|
|
|
$this->variableDefs = new ArrayObject();
|
|
$this->fieldNodeAndDefs = new ArrayObject();
|
|
$this->complexity = 0;
|
|
|
|
return $this->invokeIfNeeded(
|
|
$context,
|
|
[
|
|
NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) : void {
|
|
$this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs(
|
|
$context,
|
|
$context->getParentType(),
|
|
$selectionSet,
|
|
null,
|
|
$this->fieldNodeAndDefs
|
|
);
|
|
},
|
|
NodeKind::VARIABLE_DEFINITION => function ($def) : VisitorOperation {
|
|
$this->variableDefs[] = $def;
|
|
|
|
return Visitor::skipNode();
|
|
},
|
|
NodeKind::OPERATION_DEFINITION => [
|
|
'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) : void {
|
|
$errors = $context->getErrors();
|
|
|
|
if (count($errors) > 0) {
|
|
return;
|
|
}
|
|
|
|
$this->complexity = $this->fieldComplexity($operationDefinition, $complexity);
|
|
|
|
if ($this->getQueryComplexity() <= $this->getMaxQueryComplexity()) {
|
|
return;
|
|
}
|
|
|
|
$context->reportError(
|
|
new Error(self::maxQueryComplexityErrorMessage(
|
|
$this->getMaxQueryComplexity(),
|
|
$this->getQueryComplexity()
|
|
))
|
|
);
|
|
},
|
|
],
|
|
]
|
|
);
|
|
}
|
|
|
|
private function fieldComplexity($node, $complexity = 0)
|
|
{
|
|
if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSetNode) {
|
|
foreach ($node->selectionSet->selections as $childNode) {
|
|
$complexity = $this->nodeComplexity($childNode, $complexity);
|
|
}
|
|
}
|
|
|
|
return $complexity;
|
|
}
|
|
|
|
private function nodeComplexity(Node $node, $complexity = 0)
|
|
{
|
|
switch (true) {
|
|
case $node instanceof FieldNode:
|
|
// default values
|
|
$args = [];
|
|
$complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN;
|
|
|
|
// calculate children complexity if needed
|
|
$childrenComplexity = 0;
|
|
|
|
// node has children?
|
|
if (isset($node->selectionSet)) {
|
|
$childrenComplexity = $this->fieldComplexity($node);
|
|
}
|
|
|
|
$astFieldInfo = $this->astFieldInfo($node);
|
|
$fieldDef = $astFieldInfo[1];
|
|
|
|
if ($fieldDef instanceof FieldDefinition) {
|
|
if ($this->directiveExcludesField($node)) {
|
|
break;
|
|
}
|
|
|
|
$args = $this->buildFieldArguments($node);
|
|
//get complexity fn using fieldDef complexity
|
|
if (method_exists($fieldDef, 'getComplexityFn')) {
|
|
$complexityFn = $fieldDef->getComplexityFn();
|
|
}
|
|
}
|
|
|
|
$complexity += $complexityFn($childrenComplexity, $args);
|
|
break;
|
|
|
|
case $node instanceof InlineFragmentNode:
|
|
// node has children?
|
|
if (isset($node->selectionSet)) {
|
|
$complexity = $this->fieldComplexity($node, $complexity);
|
|
}
|
|
break;
|
|
|
|
case $node instanceof FragmentSpreadNode:
|
|
$fragment = $this->getFragment($node);
|
|
|
|
if ($fragment !== null) {
|
|
$complexity = $this->fieldComplexity($fragment, $complexity);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $complexity;
|
|
}
|
|
|
|
private function astFieldInfo(FieldNode $field)
|
|
{
|
|
$fieldName = $this->getFieldName($field);
|
|
$astFieldInfo = [null, null];
|
|
if (isset($this->fieldNodeAndDefs[$fieldName])) {
|
|
foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) {
|
|
if ($astAndDef[0] === $field) {
|
|
$astFieldInfo = $astAndDef;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $astFieldInfo;
|
|
}
|
|
|
|
private function directiveExcludesField(FieldNode $node)
|
|
{
|
|
foreach ($node->directives as $directiveNode) {
|
|
if ($directiveNode->name->value === 'deprecated') {
|
|
return false;
|
|
}
|
|
[$errors, $variableValues] = Values::getVariableValues(
|
|
$this->context->getSchema(),
|
|
$this->variableDefs,
|
|
$this->getRawVariableValues()
|
|
);
|
|
if (count($errors ?? []) > 0) {
|
|
throw new Error(implode(
|
|
"\n\n",
|
|
array_map(
|
|
static function ($error) {
|
|
return $error->getMessage();
|
|
},
|
|
$errors
|
|
)
|
|
));
|
|
}
|
|
if ($directiveNode->name->value === 'include') {
|
|
$directive = Directive::includeDirective();
|
|
/** @var bool $directiveArgsIf */
|
|
$directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues)['if'];
|
|
|
|
return ! $directiveArgsIf;
|
|
}
|
|
if ($directiveNode->name->value === Directive::SKIP_NAME) {
|
|
$directive = Directive::skipDirective();
|
|
/** @var bool $directiveArgsIf */
|
|
$directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues)['if'];
|
|
|
|
return $directiveArgsIf;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function getRawVariableValues()
|
|
{
|
|
return $this->rawVariableValues;
|
|
}
|
|
|
|
/**
|
|
* @param mixed[]|null $rawVariableValues
|
|
*/
|
|
public function setRawVariableValues(?array $rawVariableValues = null)
|
|
{
|
|
$this->rawVariableValues = $rawVariableValues ?? [];
|
|
}
|
|
|
|
private function buildFieldArguments(FieldNode $node)
|
|
{
|
|
$rawVariableValues = $this->getRawVariableValues();
|
|
$astFieldInfo = $this->astFieldInfo($node);
|
|
$fieldDef = $astFieldInfo[1];
|
|
|
|
$args = [];
|
|
|
|
if ($fieldDef instanceof FieldDefinition) {
|
|
[$errors, $variableValues] = Values::getVariableValues(
|
|
$this->context->getSchema(),
|
|
$this->variableDefs,
|
|
$rawVariableValues
|
|
);
|
|
|
|
if (count($errors ?? []) > 0) {
|
|
throw new Error(implode(
|
|
"\n\n",
|
|
array_map(
|
|
static function ($error) {
|
|
return $error->getMessage();
|
|
},
|
|
$errors
|
|
)
|
|
));
|
|
}
|
|
|
|
$args = Values::getArgumentValues($fieldDef, $node, $variableValues);
|
|
}
|
|
|
|
return $args;
|
|
}
|
|
|
|
public function getQueryComplexity()
|
|
{
|
|
return $this->complexity;
|
|
}
|
|
|
|
public function getMaxQueryComplexity()
|
|
{
|
|
return $this->maxQueryComplexity;
|
|
}
|
|
|
|
/**
|
|
* Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0.
|
|
*/
|
|
public function setMaxQueryComplexity($maxQueryComplexity)
|
|
{
|
|
$this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity);
|
|
|
|
$this->maxQueryComplexity = (int) $maxQueryComplexity;
|
|
}
|
|
|
|
public static function maxQueryComplexityErrorMessage($max, $count)
|
|
{
|
|
return sprintf('Max query complexity should be %d but got %d.', $max, $count);
|
|
}
|
|
|
|
protected function isEnabled()
|
|
{
|
|
return $this->getMaxQueryComplexity() !== self::DISABLED;
|
|
}
|
|
}
|