mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-25 14:26:08 +03:00
Add Complexity and Depth Query Security
This commit is contained in:
parent
1bc5e0c9da
commit
545fe616a0
36
README.md
36
README.md
@ -462,6 +462,42 @@ header('Content-Type: application/json');
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
#### Query Complexity Analysis
|
||||||
|
|
||||||
|
This is a PHP port of [Query Complexity Analysis](http://sangria-graphql.org/learn/#query-complexity-analysis) in Sangria implementation.
|
||||||
|
Introspection query with description max complexity is **109**.
|
||||||
|
|
||||||
|
This document validator rule is disabled by default. Here an example to enabled it:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use GraphQL\GraphQL;
|
||||||
|
|
||||||
|
/** @var \GraphQL\Validator\Rules\QueryComplexity $queryComplexity */
|
||||||
|
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
|
||||||
|
$queryComplexity->setMaxQueryComplexity($maxQueryComplexity = 110);
|
||||||
|
|
||||||
|
GraphQL::execute(/*...*/);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Limiting Query Depth
|
||||||
|
|
||||||
|
This is a PHP port of [Limiting Query Depth](http://sangria-graphql.org/learn/#limiting-query-depth) in Sangria implementation.
|
||||||
|
Introspection query with description max depth is **7**.
|
||||||
|
|
||||||
|
This document validator rule is disabled by default. Here an example to enabled it:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use GraphQL\GraphQL;
|
||||||
|
|
||||||
|
/** @var \GraphQL\Validator\Rules\QueryDepth $queryDepth */
|
||||||
|
$queryDepth = DocumentValidator::getRule('QueryDepth');
|
||||||
|
$queryDepth->setMaxQueryDepth($maxQueryDepth = 10);
|
||||||
|
|
||||||
|
GraphQL::execute(/*...*/);
|
||||||
|
```
|
||||||
|
|
||||||
### More Examples
|
### More Examples
|
||||||
Make sure to check [tests](https://github.com/webonyx/graphql-php/tree/master/tests) for more usage examples.
|
Make sure to check [tests](https://github.com/webonyx/graphql-php/tree/master/tests) for more usage examples.
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ use GraphQL\Executor\Executor;
|
|||||||
use GraphQL\Language\Parser;
|
use GraphQL\Language\Parser;
|
||||||
use GraphQL\Language\Source;
|
use GraphQL\Language\Source;
|
||||||
use GraphQL\Validator\DocumentValidator;
|
use GraphQL\Validator\DocumentValidator;
|
||||||
|
use GraphQL\Validator\Rules\QueryComplexity;
|
||||||
|
|
||||||
class GraphQL
|
class GraphQL
|
||||||
{
|
{
|
||||||
@ -35,6 +36,11 @@ class GraphQL
|
|||||||
try {
|
try {
|
||||||
$source = new Source($requestString ?: '', 'GraphQL request');
|
$source = new Source($requestString ?: '', 'GraphQL request');
|
||||||
$documentAST = Parser::parse($source);
|
$documentAST = Parser::parse($source);
|
||||||
|
|
||||||
|
/** @var QueryComplexity $queryComplexity */
|
||||||
|
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
|
||||||
|
$queryComplexity->setRawVariableValues($variableValues);
|
||||||
|
|
||||||
$validationErrors = DocumentValidator::validate($schema, $documentAST);
|
$validationErrors = DocumentValidator::validate($schema, $documentAST);
|
||||||
|
|
||||||
if (!empty($validationErrors)) {
|
if (!empty($validationErrors)) {
|
||||||
|
@ -5,6 +5,8 @@ use GraphQL\Utils;
|
|||||||
|
|
||||||
class FieldDefinition
|
class FieldDefinition
|
||||||
{
|
{
|
||||||
|
const DEFAULT_COMPLEXITY_FN = 'GraphQL\Type\Definition\FieldDefinition::defaultComplexity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
@ -72,6 +74,7 @@ class FieldDefinition
|
|||||||
'map' => Config::CALLBACK,
|
'map' => Config::CALLBACK,
|
||||||
'description' => Config::STRING,
|
'description' => Config::STRING,
|
||||||
'deprecationReason' => Config::STRING,
|
'deprecationReason' => Config::STRING,
|
||||||
|
'complexity' => Config::CALLBACK,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,6 +116,8 @@ class FieldDefinition
|
|||||||
$this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null;
|
$this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null;
|
||||||
|
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
|
|
||||||
|
$this->complexityFn = isset($config['complexity']) ? $config['complexity'] : static::DEFAULT_COMPLEXITY_FN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -141,4 +146,17 @@ class FieldDefinition
|
|||||||
}
|
}
|
||||||
return $this->resolvedType;
|
return $this->resolvedType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return callable|\Closure
|
||||||
|
*/
|
||||||
|
public function getComplexityFn()
|
||||||
|
{
|
||||||
|
return $this->complexityFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function defaultComplexity($childrenComplexity)
|
||||||
|
{
|
||||||
|
return $childrenComplexity + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,8 @@ use GraphQL\Validator\Rules\NoUnusedVariables;
|
|||||||
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
|
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
|
||||||
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
|
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
|
||||||
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
|
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
|
||||||
|
use GraphQL\Validator\Rules\QueryComplexity;
|
||||||
|
use GraphQL\Validator\Rules\QueryDepth;
|
||||||
use GraphQL\Validator\Rules\ScalarLeafs;
|
use GraphQL\Validator\Rules\ScalarLeafs;
|
||||||
use GraphQL\Validator\Rules\VariablesAreInputTypes;
|
use GraphQL\Validator\Rules\VariablesAreInputTypes;
|
||||||
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
|
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
|
||||||
@ -82,6 +84,9 @@ class DocumentValidator
|
|||||||
'DefaultValuesOfCorrectType' => new DefaultValuesOfCorrectType(),
|
'DefaultValuesOfCorrectType' => new DefaultValuesOfCorrectType(),
|
||||||
'VariablesInAllowedPosition' => new VariablesInAllowedPosition(),
|
'VariablesInAllowedPosition' => new VariablesInAllowedPosition(),
|
||||||
'OverlappingFieldsCanBeMerged' => new OverlappingFieldsCanBeMerged(),
|
'OverlappingFieldsCanBeMerged' => new OverlappingFieldsCanBeMerged(),
|
||||||
|
// Query Security
|
||||||
|
'QueryDepth' => new QueryDepth(QueryDepth::DISABLED), // default disabled
|
||||||
|
'QueryComplexity' => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +95,9 @@ class DocumentValidator
|
|||||||
|
|
||||||
public static function getRule($name)
|
public static function getRule($name)
|
||||||
{
|
{
|
||||||
return isset(self::$rules[$name]) ? self::$rules[$name] : null ;
|
$rules = static::allRules();
|
||||||
|
|
||||||
|
return isset($rules[$name]) ? $rules[$name] : null ;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function addRule($name, callable $rule)
|
public static function addRule($name, callable $rule)
|
||||||
@ -98,11 +105,6 @@ class DocumentValidator
|
|||||||
self::$rules[$name] = $rule;
|
self::$rules[$name] = $rule;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function removeRule($name)
|
|
||||||
{
|
|
||||||
unset(self::$rules[$name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function validate(Schema $schema, Document $ast, array $rules = null)
|
public static function validate(Schema $schema, Document $ast, array $rules = null)
|
||||||
{
|
{
|
||||||
$errors = static::visitUsingRules($schema, $ast, $rules ?: static::allRules());
|
$errors = static::visitUsingRules($schema, $ast, $rules ?: static::allRules());
|
||||||
|
171
src/Validator/Rules/AbstractQuerySecurity.php
Normal file
171
src/Validator/Rules/AbstractQuerySecurity.php
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace GraphQL\Validator\Rules;
|
||||||
|
|
||||||
|
use GraphQL\Language\AST\Field;
|
||||||
|
use GraphQL\Language\AST\FragmentDefinition;
|
||||||
|
use GraphQL\Language\AST\FragmentSpread;
|
||||||
|
use GraphQL\Language\AST\InlineFragment;
|
||||||
|
use GraphQL\Language\AST\Node;
|
||||||
|
use GraphQL\Language\AST\SelectionSet;
|
||||||
|
use GraphQL\Type\Definition\Type;
|
||||||
|
use GraphQL\Type\Introspection;
|
||||||
|
use GraphQL\Utils\TypeInfo;
|
||||||
|
use GraphQL\Validator\ValidationContext;
|
||||||
|
|
||||||
|
abstract class AbstractQuerySecurity
|
||||||
|
{
|
||||||
|
const DISABLED = 0;
|
||||||
|
|
||||||
|
/** @var FragmentDefinition[] */
|
||||||
|
private $fragments = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \GraphQL\Language\AST\FragmentDefinition[]
|
||||||
|
*/
|
||||||
|
protected function getFragments()
|
||||||
|
{
|
||||||
|
return $this->fragments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if equal to 0 no check is done. Must be greater or equal to 0.
|
||||||
|
*
|
||||||
|
* @param $value
|
||||||
|
*/
|
||||||
|
protected function checkIfGreaterOrEqualToZero($name, $value)
|
||||||
|
{
|
||||||
|
if ($value < 0) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('$%s argument must be greater or equal to 0.', $name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function gatherFragmentDefinition(ValidationContext $context)
|
||||||
|
{
|
||||||
|
// Gather all the fragment definition.
|
||||||
|
// Importantly this does not include inline fragments.
|
||||||
|
$definitions = $context->getDocument()->definitions;
|
||||||
|
foreach ($definitions as $node) {
|
||||||
|
if ($node instanceof FragmentDefinition) {
|
||||||
|
$this->fragments[$node->name->value] = $node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFragment(FragmentSpread $fragmentSpread)
|
||||||
|
{
|
||||||
|
$spreadName = $fragmentSpread->name->value;
|
||||||
|
$fragments = $this->getFragments();
|
||||||
|
|
||||||
|
return isset($fragments[$spreadName]) ? $fragments[$spreadName] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function invokeIfNeeded(ValidationContext $context, array $validators)
|
||||||
|
{
|
||||||
|
// is disabled?
|
||||||
|
if (!$this->isEnabled()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->gatherFragmentDefinition($context);
|
||||||
|
|
||||||
|
return $validators;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a selectionSet, adds all of the fields in that selection to
|
||||||
|
* the passed in map of fields, and returns it at the end.
|
||||||
|
*
|
||||||
|
* Note: This is not the same as execution's collectFields because at static
|
||||||
|
* time we do not know what object type will be used, so we unconditionally
|
||||||
|
* spread in all fragments.
|
||||||
|
*
|
||||||
|
* @see GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged
|
||||||
|
*
|
||||||
|
* @param ValidationContext $context
|
||||||
|
* @param Type|null $parentType
|
||||||
|
* @param SelectionSet $selectionSet
|
||||||
|
* @param \ArrayObject $visitedFragmentNames
|
||||||
|
* @param \ArrayObject $astAndDefs
|
||||||
|
*
|
||||||
|
* @return \ArrayObject
|
||||||
|
*/
|
||||||
|
protected function collectFieldASTsAndDefs(ValidationContext $context, $parentType, SelectionSet $selectionSet, \ArrayObject $visitedFragmentNames = null, \ArrayObject $astAndDefs = null)
|
||||||
|
{
|
||||||
|
$_visitedFragmentNames = $visitedFragmentNames ?: new \ArrayObject();
|
||||||
|
$_astAndDefs = $astAndDefs ?: new \ArrayObject();
|
||||||
|
|
||||||
|
foreach ($selectionSet->selections as $selection) {
|
||||||
|
switch ($selection->kind) {
|
||||||
|
case Node::FIELD:
|
||||||
|
/* @var Field $selection */
|
||||||
|
$fieldName = $selection->name->value;
|
||||||
|
$fieldDef = null;
|
||||||
|
if ($parentType && method_exists($parentType, 'getFields')) {
|
||||||
|
$tmp = $parentType->getFields();
|
||||||
|
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
|
||||||
|
$typeMetaFieldDef = Introspection::typeMetaFieldDef();
|
||||||
|
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
|
||||||
|
|
||||||
|
if ($fieldName === $schemaMetaFieldDef->name && $context->getSchema()->getQueryType() === $parentType) {
|
||||||
|
$fieldDef = $schemaMetaFieldDef;
|
||||||
|
} elseif ($fieldName === $typeMetaFieldDef->name && $context->getSchema()->getQueryType() === $parentType) {
|
||||||
|
$fieldDef = $typeMetaFieldDef;
|
||||||
|
} elseif ($fieldName === $typeNameMetaFieldDef->name) {
|
||||||
|
$fieldDef = $typeNameMetaFieldDef;
|
||||||
|
} elseif (isset($tmp[$fieldName])) {
|
||||||
|
$fieldDef = $tmp[$fieldName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$responseName = $this->getFieldName($selection);
|
||||||
|
if (!isset($_astAndDefs[$responseName])) {
|
||||||
|
$_astAndDefs[$responseName] = new \ArrayObject();
|
||||||
|
}
|
||||||
|
// create field context
|
||||||
|
$_astAndDefs[$responseName][] = [$selection, $fieldDef];
|
||||||
|
break;
|
||||||
|
case Node::INLINE_FRAGMENT:
|
||||||
|
/* @var InlineFragment $selection */
|
||||||
|
$_astAndDefs = $this->collectFieldASTsAndDefs(
|
||||||
|
$context,
|
||||||
|
TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition),
|
||||||
|
$selection->selectionSet,
|
||||||
|
$_visitedFragmentNames,
|
||||||
|
$_astAndDefs
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case Node::FRAGMENT_SPREAD:
|
||||||
|
/* @var FragmentSpread $selection */
|
||||||
|
$fragName = $selection->name->value;
|
||||||
|
|
||||||
|
if (empty($_visitedFragmentNames[$fragName])) {
|
||||||
|
$_visitedFragmentNames[$fragName] = true;
|
||||||
|
$fragment = $context->getFragment($fragName);
|
||||||
|
|
||||||
|
if ($fragment) {
|
||||||
|
$_astAndDefs = $this->collectFieldASTsAndDefs(
|
||||||
|
$context,
|
||||||
|
TypeInfo::typeFromAST($context->getSchema(), $fragment->typeCondition),
|
||||||
|
$fragment->selectionSet,
|
||||||
|
$_visitedFragmentNames,
|
||||||
|
$_astAndDefs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $_astAndDefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFieldName(Field $node)
|
||||||
|
{
|
||||||
|
$fieldName = $node->name->value;
|
||||||
|
$responseName = $node->alias ? $node->alias->value : $fieldName;
|
||||||
|
|
||||||
|
return $responseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function isEnabled();
|
||||||
|
}
|
212
src/Validator/Rules/QueryComplexity.php
Normal file
212
src/Validator/Rules/QueryComplexity.php
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace GraphQL\Validator\Rules;
|
||||||
|
|
||||||
|
use GraphQL\Error;
|
||||||
|
use GraphQL\Executor\Values;
|
||||||
|
use GraphQL\Language\AST\Field;
|
||||||
|
use GraphQL\Language\AST\FragmentSpread;
|
||||||
|
use GraphQL\Language\AST\InlineFragment;
|
||||||
|
use GraphQL\Language\AST\Node;
|
||||||
|
use GraphQL\Language\AST\OperationDefinition;
|
||||||
|
use GraphQL\Language\AST\SelectionSet;
|
||||||
|
use GraphQL\Language\Visitor;
|
||||||
|
use GraphQL\Type\Definition\FieldDefinition;
|
||||||
|
use GraphQL\Validator\ValidationContext;
|
||||||
|
|
||||||
|
class QueryComplexity extends AbstractQuerySecurity
|
||||||
|
{
|
||||||
|
private $maxQueryComplexity;
|
||||||
|
|
||||||
|
private $rawVariableValues = [];
|
||||||
|
|
||||||
|
private $variableDefs;
|
||||||
|
|
||||||
|
private $fieldAstAndDefs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ValidationContext
|
||||||
|
*/
|
||||||
|
private $context;
|
||||||
|
|
||||||
|
public function __construct($maxQueryDepth)
|
||||||
|
{
|
||||||
|
$this->setMaxQueryComplexity($maxQueryDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function maxQueryComplexityErrorMessage($max, $count)
|
||||||
|
{
|
||||||
|
return sprintf('Max query complexity should be %d but got %d.', $max, $count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0.
|
||||||
|
*
|
||||||
|
* @param $maxQueryComplexity
|
||||||
|
*/
|
||||||
|
public function setMaxQueryComplexity($maxQueryComplexity)
|
||||||
|
{
|
||||||
|
$this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity);
|
||||||
|
|
||||||
|
$this->maxQueryComplexity = (int) $maxQueryComplexity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMaxQueryComplexity()
|
||||||
|
{
|
||||||
|
return $this->maxQueryComplexity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRawVariableValues(array $rawVariableValues = null)
|
||||||
|
{
|
||||||
|
$this->rawVariableValues = $rawVariableValues ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRawVariableValues()
|
||||||
|
{
|
||||||
|
return $this->rawVariableValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(ValidationContext $context)
|
||||||
|
{
|
||||||
|
$this->context = $context;
|
||||||
|
|
||||||
|
$this->variableDefs = new \ArrayObject();
|
||||||
|
$this->fieldAstAndDefs = new \ArrayObject();
|
||||||
|
$complexity = 0;
|
||||||
|
|
||||||
|
return $this->invokeIfNeeded(
|
||||||
|
$context,
|
||||||
|
[
|
||||||
|
// Visit FragmentDefinition after visiting FragmentSpread
|
||||||
|
'visitSpreadFragments' => true,
|
||||||
|
Node::SELECTION_SET => function (SelectionSet $selectionSet) use ($context) {
|
||||||
|
$this->fieldAstAndDefs = $this->collectFieldASTsAndDefs(
|
||||||
|
$context,
|
||||||
|
$context->getParentType(),
|
||||||
|
$selectionSet,
|
||||||
|
null,
|
||||||
|
$this->fieldAstAndDefs
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Node::VARIABLE_DEFINITION => function ($def) {
|
||||||
|
$this->variableDefs[] = $def;
|
||||||
|
|
||||||
|
return Visitor::skipNode();
|
||||||
|
},
|
||||||
|
Node::OPERATION_DEFINITION => [
|
||||||
|
'leave' => function (OperationDefinition $operationDefinition) use ($context, &$complexity) {
|
||||||
|
$complexity = $this->fieldComplexity($operationDefinition, $complexity);
|
||||||
|
|
||||||
|
if ($complexity > $this->getMaxQueryComplexity()) {
|
||||||
|
return new Error($this->maxQueryComplexityErrorMessage($this->getMaxQueryComplexity(), $complexity));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fieldComplexity($node, $complexity = 0)
|
||||||
|
{
|
||||||
|
if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSet) {
|
||||||
|
foreach ($node->selectionSet->selections as $childNode) {
|
||||||
|
$complexity = $this->nodeComplexity($childNode, $complexity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $complexity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nodeComplexity(Node $node, $complexity = 0)
|
||||||
|
{
|
||||||
|
switch ($node->kind) {
|
||||||
|
case Node::FIELD:
|
||||||
|
/* @var Field $node */
|
||||||
|
// 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) {
|
||||||
|
$args = $this->buildFieldArguments($node);
|
||||||
|
//get complexity fn using fieldDef complexity
|
||||||
|
if (method_exists($fieldDef, 'getComplexityFn')) {
|
||||||
|
$complexityFn = $fieldDef->getComplexityFn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$complexity += call_user_func_array($complexityFn, [$childrenComplexity, $args]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Node::INLINE_FRAGMENT:
|
||||||
|
/* @var InlineFragment $node */
|
||||||
|
// node has children?
|
||||||
|
if (isset($node->selectionSet)) {
|
||||||
|
$complexity = $this->fieldComplexity($node, $complexity);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Node::FRAGMENT_SPREAD:
|
||||||
|
/* @var FragmentSpread $node */
|
||||||
|
$fragment = $this->getFragment($node);
|
||||||
|
|
||||||
|
if (null !== $fragment) {
|
||||||
|
$complexity = $this->fieldComplexity($fragment, $complexity);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $complexity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function astFieldInfo(Field $field)
|
||||||
|
{
|
||||||
|
$fieldName = $this->getFieldName($field);
|
||||||
|
$astFieldInfo = [null, null];
|
||||||
|
if (isset($this->fieldAstAndDefs[$fieldName])) {
|
||||||
|
foreach ($this->fieldAstAndDefs[$fieldName] as $astAndDef) {
|
||||||
|
if ($astAndDef[0] == $field) {
|
||||||
|
$astFieldInfo = $astAndDef;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $astFieldInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildFieldArguments(Field $node)
|
||||||
|
{
|
||||||
|
$rawVariableValues = $this->getRawVariableValues();
|
||||||
|
$astFieldInfo = $this->astFieldInfo($node);
|
||||||
|
$fieldDef = $astFieldInfo[1];
|
||||||
|
|
||||||
|
$args = [];
|
||||||
|
|
||||||
|
if ($fieldDef instanceof FieldDefinition) {
|
||||||
|
$variableValues = Values::getVariableValues(
|
||||||
|
$this->context->getSchema(),
|
||||||
|
$this->variableDefs,
|
||||||
|
$rawVariableValues
|
||||||
|
);
|
||||||
|
$args = Values::getArgumentValues($fieldDef->args, $node->arguments, $variableValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isEnabled()
|
||||||
|
{
|
||||||
|
return $this->getMaxQueryComplexity() !== static::DISABLED;
|
||||||
|
}
|
||||||
|
}
|
116
src/Validator/Rules/QueryDepth.php
Normal file
116
src/Validator/Rules/QueryDepth.php
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Validator\Rules;
|
||||||
|
|
||||||
|
use GraphQL\Error;
|
||||||
|
use GraphQL\Language\AST\Field;
|
||||||
|
use GraphQL\Language\AST\FragmentSpread;
|
||||||
|
use GraphQL\Language\AST\InlineFragment;
|
||||||
|
use GraphQL\Language\AST\Node;
|
||||||
|
use GraphQL\Language\AST\OperationDefinition;
|
||||||
|
use GraphQL\Language\AST\SelectionSet;
|
||||||
|
use GraphQL\Validator\ValidationContext;
|
||||||
|
|
||||||
|
class QueryDepth extends AbstractQuerySecurity
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $maxQueryDepth;
|
||||||
|
|
||||||
|
public function __construct($maxQueryDepth)
|
||||||
|
{
|
||||||
|
$this->setMaxQueryDepth($maxQueryDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0.
|
||||||
|
*
|
||||||
|
* @param $maxQueryDepth
|
||||||
|
*/
|
||||||
|
public function setMaxQueryDepth($maxQueryDepth)
|
||||||
|
{
|
||||||
|
$this->checkIfGreaterOrEqualToZero('maxQueryDepth', $maxQueryDepth);
|
||||||
|
|
||||||
|
$this->maxQueryDepth = (int) $maxQueryDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMaxQueryDepth()
|
||||||
|
{
|
||||||
|
return $this->maxQueryDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function maxQueryDepthErrorMessage($max, $count)
|
||||||
|
{
|
||||||
|
return sprintf('Max query depth should be %d but got %d.', $max, $count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(ValidationContext $context)
|
||||||
|
{
|
||||||
|
return $this->invokeIfNeeded(
|
||||||
|
$context,
|
||||||
|
[
|
||||||
|
Node::OPERATION_DEFINITION => [
|
||||||
|
'leave' => function (OperationDefinition $operationDefinition) use ($context) {
|
||||||
|
$maxDepth = $this->fieldDepth($operationDefinition);
|
||||||
|
|
||||||
|
if ($maxDepth > $this->getMaxQueryDepth()) {
|
||||||
|
return new Error($this->maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isEnabled()
|
||||||
|
{
|
||||||
|
return $this->getMaxQueryDepth() !== static::DISABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fieldDepth($node, $depth = 0, $maxDepth = 0)
|
||||||
|
{
|
||||||
|
if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSet) {
|
||||||
|
foreach ($node->selectionSet->selections as $childNode) {
|
||||||
|
$maxDepth = $this->nodeDepth($childNode, $depth, $maxDepth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $maxDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nodeDepth(Node $node, $depth = 0, $maxDepth = 0)
|
||||||
|
{
|
||||||
|
switch ($node->kind) {
|
||||||
|
case Node::FIELD:
|
||||||
|
/* @var Field $node */
|
||||||
|
// node has children?
|
||||||
|
if (null !== $node->selectionSet) {
|
||||||
|
// update maxDepth if needed
|
||||||
|
if ($depth > $maxDepth) {
|
||||||
|
$maxDepth = $depth;
|
||||||
|
}
|
||||||
|
$maxDepth = $this->fieldDepth($node, $depth + 1, $maxDepth);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Node::INLINE_FRAGMENT:
|
||||||
|
/* @var InlineFragment $node */
|
||||||
|
// node has children?
|
||||||
|
if (null !== $node->selectionSet) {
|
||||||
|
$maxDepth = $this->fieldDepth($node, $depth, $maxDepth);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Node::FRAGMENT_SPREAD:
|
||||||
|
/* @var FragmentSpread $node */
|
||||||
|
$fragment = $this->getFragment($node);
|
||||||
|
|
||||||
|
if (null !== $fragment) {
|
||||||
|
$maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $maxDepth;
|
||||||
|
}
|
||||||
|
}
|
95
tests/Validator/AbstractQuerySecurityTest.php
Normal file
95
tests/Validator/AbstractQuerySecurityTest.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Tests\Validator;
|
||||||
|
|
||||||
|
use GraphQL\FormattedError;
|
||||||
|
use GraphQL\Language\Parser;
|
||||||
|
use GraphQL\Type\Introspection;
|
||||||
|
use GraphQL\Validator\DocumentValidator;
|
||||||
|
use GraphQL\Validator\Rules\AbstractQuerySecurity;
|
||||||
|
|
||||||
|
abstract class AbstractQuerySecurityTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param $max
|
||||||
|
*
|
||||||
|
* @return AbstractQuerySecurity
|
||||||
|
*/
|
||||||
|
abstract protected function getRule($max);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $max
|
||||||
|
* @param $count
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
abstract protected function getErrorMessage($max, $count);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \InvalidArgumentException
|
||||||
|
* @expectedExceptionMessage argument must be greater or equal to 0.
|
||||||
|
*/
|
||||||
|
public function testMaxQueryDepthMustBeGreaterOrEqualTo0()
|
||||||
|
{
|
||||||
|
$this->getRule(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createFormattedError($max, $count, $locations = [])
|
||||||
|
{
|
||||||
|
return FormattedError::create($this->getErrorMessage($max, $count), $locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertDocumentValidator($queryString, $max, array $expectedErrors = [])
|
||||||
|
{
|
||||||
|
$errors = DocumentValidator::validate(
|
||||||
|
QuerySecuritySchema::buildSchema(),
|
||||||
|
Parser::parse($queryString),
|
||||||
|
[$this->getRule($max)]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals($expectedErrors, array_map(['GraphQL\Error', 'formatError'], $errors), $queryString);
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertIntrospectionQuery($maxExpected)
|
||||||
|
{
|
||||||
|
$query = Introspection::getIntrospectionQuery(true);
|
||||||
|
|
||||||
|
$this->assertMaxValue($query, $maxExpected);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertIntrospectionTypeMetaFieldQuery($maxExpected)
|
||||||
|
{
|
||||||
|
$query = '
|
||||||
|
{
|
||||||
|
__type(name: "Human") {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
';
|
||||||
|
|
||||||
|
$this->assertMaxValue($query, $maxExpected);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertTypeNameMetaFieldQuery($maxExpected)
|
||||||
|
{
|
||||||
|
$query = '
|
||||||
|
{
|
||||||
|
human {
|
||||||
|
__typename
|
||||||
|
firstName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
';
|
||||||
|
$this->assertMaxValue($query, $maxExpected);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertMaxValue($query, $maxExpected)
|
||||||
|
{
|
||||||
|
$this->assertDocumentValidator($query, $maxExpected);
|
||||||
|
$newMax = $maxExpected - 1;
|
||||||
|
if ($newMax !== AbstractQuerySecurity::DISABLED) {
|
||||||
|
$this->assertDocumentValidator($query, $newMax, [$this->createFormattedError($newMax, $maxExpected)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
tests/Validator/QueryComplexityTest.php
Normal file
116
tests/Validator/QueryComplexityTest.php
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Tests\Validator;
|
||||||
|
|
||||||
|
use GraphQL\Validator\Rules\QueryComplexity;
|
||||||
|
|
||||||
|
class QueryComplexityTest extends AbstractQuerySecurityTest
|
||||||
|
{
|
||||||
|
/** @var QueryComplexity */
|
||||||
|
private static $rule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $max
|
||||||
|
* @param $count
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getErrorMessage($max, $count)
|
||||||
|
{
|
||||||
|
return QueryComplexity::maxQueryComplexityErrorMessage($max, $count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $maxDepth
|
||||||
|
*
|
||||||
|
* @return QueryComplexity
|
||||||
|
*/
|
||||||
|
protected function getRule($maxDepth = null)
|
||||||
|
{
|
||||||
|
if (null === self::$rule) {
|
||||||
|
self::$rule = new QueryComplexity($maxDepth);
|
||||||
|
} elseif (null !== $maxDepth) {
|
||||||
|
self::$rule->setMaxQueryComplexity($maxDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSimpleQueries()
|
||||||
|
{
|
||||||
|
$query = 'query MyQuery { human { firstName } }';
|
||||||
|
|
||||||
|
$this->assertDocumentValidators($query, 2, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInlineFragmentQueries()
|
||||||
|
{
|
||||||
|
$query = 'query MyQuery { human { ... on Human { firstName } } }';
|
||||||
|
|
||||||
|
$this->assertDocumentValidators($query, 2, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFragmentQueries()
|
||||||
|
{
|
||||||
|
$query = 'query MyQuery { human { ...F1 } } fragment F1 on Human { firstName}';
|
||||||
|
|
||||||
|
$this->assertDocumentValidators($query, 2, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAliasesQueries()
|
||||||
|
{
|
||||||
|
$query = 'query MyQuery { thomas: human(name: "Thomas") { firstName } jeremy: human(name: "Jeremy") { firstName } }';
|
||||||
|
|
||||||
|
$this->assertDocumentValidators($query, 4, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCustomComplexityQueries()
|
||||||
|
{
|
||||||
|
$query = 'query MyQuery { human { dogs { name } } }';
|
||||||
|
|
||||||
|
$this->assertDocumentValidators($query, 12, 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCustomComplexityWithArgsQueries()
|
||||||
|
{
|
||||||
|
$query = 'query MyQuery { human { dogs(name: "Root") { name } } }';
|
||||||
|
|
||||||
|
$this->assertDocumentValidators($query, 3, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCustomComplexityWithVariablesQueries()
|
||||||
|
{
|
||||||
|
$query = 'query MyQuery($dog: String!) { human { dogs(name: $dog) { name } } }';
|
||||||
|
|
||||||
|
$this->getRule()->setRawVariableValues(['dog' => 'Roots']);
|
||||||
|
|
||||||
|
$this->assertDocumentValidators($query, 3, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testComplexityIntrospectionQuery()
|
||||||
|
{
|
||||||
|
$this->assertIntrospectionQuery(109);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIntrospectionTypeMetaFieldQuery()
|
||||||
|
{
|
||||||
|
$this->assertIntrospectionTypeMetaFieldQuery(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTypeNameMetaFieldQuery()
|
||||||
|
{
|
||||||
|
$this->assertTypeNameMetaFieldQuery(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertDocumentValidators($query, $queryComplexity, $startComplexity)
|
||||||
|
{
|
||||||
|
for ($maxComplexity = $startComplexity; $maxComplexity >= 0; --$maxComplexity) {
|
||||||
|
$positions = [];
|
||||||
|
|
||||||
|
if ($maxComplexity < $queryComplexity && $maxComplexity !== QueryComplexity::DISABLED) {
|
||||||
|
$positions = [$this->createFormattedError($maxComplexity, $queryComplexity)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertDocumentValidator($query, $maxComplexity, $positions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
148
tests/Validator/QueryDepthTest.php
Normal file
148
tests/Validator/QueryDepthTest.php
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Tests\Validator;
|
||||||
|
|
||||||
|
use GraphQL\Validator\Rules\QueryDepth;
|
||||||
|
|
||||||
|
class QueryDepthTest extends AbstractQuerySecurityTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param $max
|
||||||
|
* @param $count
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getErrorMessage($max, $count)
|
||||||
|
{
|
||||||
|
return QueryDepth::maxQueryDepthErrorMessage($max, $count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $maxDepth
|
||||||
|
*
|
||||||
|
* @return QueryDepth
|
||||||
|
*/
|
||||||
|
protected function getRule($maxDepth)
|
||||||
|
{
|
||||||
|
return new QueryDepth($maxDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $queryDepth
|
||||||
|
* @param int $maxQueryDepth
|
||||||
|
* @param array $expectedErrors
|
||||||
|
* @dataProvider queryDataProvider
|
||||||
|
*/
|
||||||
|
public function testSimpleQueries($queryDepth, $maxQueryDepth = 7, $expectedErrors = [])
|
||||||
|
{
|
||||||
|
$this->assertDocumentValidator($this->buildRecursiveQuery($queryDepth), $maxQueryDepth, $expectedErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $queryDepth
|
||||||
|
* @param int $maxQueryDepth
|
||||||
|
* @param array $expectedErrors
|
||||||
|
* @dataProvider queryDataProvider
|
||||||
|
*/
|
||||||
|
public function testFragmentQueries($queryDepth, $maxQueryDepth = 7, $expectedErrors = [])
|
||||||
|
{
|
||||||
|
$this->assertDocumentValidator($this->buildRecursiveUsingFragmentQuery($queryDepth), $maxQueryDepth, $expectedErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $queryDepth
|
||||||
|
* @param int $maxQueryDepth
|
||||||
|
* @param array $expectedErrors
|
||||||
|
* @dataProvider queryDataProvider
|
||||||
|
*/
|
||||||
|
public function testInlineFragmentQueries($queryDepth, $maxQueryDepth = 7, $expectedErrors = [])
|
||||||
|
{
|
||||||
|
$this->assertDocumentValidator($this->buildRecursiveUsingInlineFragmentQuery($queryDepth), $maxQueryDepth, $expectedErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testComplexityIntrospectionQuery()
|
||||||
|
{
|
||||||
|
$this->assertIntrospectionQuery(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIntrospectionTypeMetaFieldQuery()
|
||||||
|
{
|
||||||
|
$this->assertIntrospectionTypeMetaFieldQuery(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTypeNameMetaFieldQuery()
|
||||||
|
{
|
||||||
|
$this->assertTypeNameMetaFieldQuery(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function queryDataProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[1], // Valid because depth under default limit (7)
|
||||||
|
[2],
|
||||||
|
[3],
|
||||||
|
[4],
|
||||||
|
[5],
|
||||||
|
[6],
|
||||||
|
[7],
|
||||||
|
[8, 9], // Valid because depth under new limit (9)
|
||||||
|
[10, 0], // Valid because 0 depth disable limit
|
||||||
|
[
|
||||||
|
10,
|
||||||
|
8,
|
||||||
|
[$this->createFormattedError(8, 10)],
|
||||||
|
], // failed because depth over limit (8)
|
||||||
|
[
|
||||||
|
20,
|
||||||
|
15,
|
||||||
|
[$this->createFormattedError(15, 20)],
|
||||||
|
], // failed because depth over limit (15)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRecursiveQuery($depth)
|
||||||
|
{
|
||||||
|
$query = sprintf('query MyQuery { human%s }', $this->buildRecursiveQueryPart($depth));
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRecursiveUsingFragmentQuery($depth)
|
||||||
|
{
|
||||||
|
$query = sprintf(
|
||||||
|
'query MyQuery { human { ...F1 } } fragment F1 on Human %s',
|
||||||
|
$this->buildRecursiveQueryPart($depth)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRecursiveUsingInlineFragmentQuery($depth)
|
||||||
|
{
|
||||||
|
$query = sprintf(
|
||||||
|
'query MyQuery { human { ...on Human %s } }',
|
||||||
|
$this->buildRecursiveQueryPart($depth)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRecursiveQueryPart($depth)
|
||||||
|
{
|
||||||
|
$templates = [
|
||||||
|
'human' => ' { firstName%s } ',
|
||||||
|
'dog' => ' dogs { name%s } ',
|
||||||
|
];
|
||||||
|
|
||||||
|
$part = $templates['human'];
|
||||||
|
|
||||||
|
for ($i = 1; $i <= $depth; ++$i) {
|
||||||
|
$key = ($i % 2 == 1) ? 'human' : 'dog';
|
||||||
|
$template = $templates[$key];
|
||||||
|
|
||||||
|
$part = sprintf($part, ('human' == $key ? ' owner ' : '').$template);
|
||||||
|
}
|
||||||
|
$part = str_replace('%s', '', $part);
|
||||||
|
|
||||||
|
return $part;
|
||||||
|
}
|
||||||
|
}
|
104
tests/Validator/QuerySecuritySchema.php
Normal file
104
tests/Validator/QuerySecuritySchema.php
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Tests\Validator;
|
||||||
|
|
||||||
|
use GraphQL\Schema;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\Type;
|
||||||
|
|
||||||
|
class QuerySecuritySchema
|
||||||
|
{
|
||||||
|
private static $schema;
|
||||||
|
|
||||||
|
private static $dogType;
|
||||||
|
|
||||||
|
private static $humanType;
|
||||||
|
|
||||||
|
private static $queryRootType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Schema
|
||||||
|
*/
|
||||||
|
public static function buildSchema()
|
||||||
|
{
|
||||||
|
if (null !== self::$schema) {
|
||||||
|
return self::$schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$schema = new Schema(static::buildQueryRootType());
|
||||||
|
|
||||||
|
return self::$schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buildQueryRootType()
|
||||||
|
{
|
||||||
|
if (null !== self::$queryRootType) {
|
||||||
|
return self::$queryRootType;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$queryRootType = new ObjectType([
|
||||||
|
'name' => 'QueryRoot',
|
||||||
|
'fields' => [
|
||||||
|
'human' => [
|
||||||
|
'type' => self::buildHumanType(),
|
||||||
|
'args' => ['name' => ['type' => Type::string()]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return self::$queryRootType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buildHumanType()
|
||||||
|
{
|
||||||
|
if (null !== self::$humanType) {
|
||||||
|
return self::$humanType;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$humanType = new ObjectType(
|
||||||
|
[
|
||||||
|
'name' => 'Human',
|
||||||
|
'fields' => [
|
||||||
|
'firstName' => ['type' => Type::nonNull(Type::string())],
|
||||||
|
'dogs' => [
|
||||||
|
'type' => function () {
|
||||||
|
return Type::nonNull(
|
||||||
|
Type::listOf(
|
||||||
|
Type::nonNull(self::buildDogType())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'complexity' => function ($childrenComplexity, $args) {
|
||||||
|
$complexity = isset($args['name']) ? 1 : 10;
|
||||||
|
|
||||||
|
return $childrenComplexity + $complexity;
|
||||||
|
},
|
||||||
|
'args' => ['name' => ['type' => Type::string()]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return self::$humanType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buildDogType()
|
||||||
|
{
|
||||||
|
if (null !== self::$dogType) {
|
||||||
|
return self::$dogType;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$dogType = new ObjectType(
|
||||||
|
[
|
||||||
|
'name' => 'Dog',
|
||||||
|
'fields' => [
|
||||||
|
'name' => ['type' => Type::nonNull(Type::string())],
|
||||||
|
'master' => [
|
||||||
|
'type' => self::buildHumanType(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return self::$dogType;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user