Merge pull request #32 from mcg-web/add_query_security_rules

Add query security document validation rules
This commit is contained in:
Vladimir Razuvaev 2016-04-15 16:48:58 +06:00
commit 36a845499c
56 changed files with 1211 additions and 95 deletions

View File

@ -462,6 +462,42 @@ header('Content-Type: application/json');
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
Make sure to check [tests](https://github.com/webonyx/graphql-php/tree/master/tests) for more usage examples.

View File

@ -18,15 +18,13 @@
"bin-dir": "bin"
},
"autoload": {
"classmap": [
"src/"
]
"psr-4": {
"GraphQL\\": "src/"
}
},
"autoload-dev": {
"classmap": [
"tests/"
],
"files": [
]
"psr-4": {
"GraphQL\\Tests\\": "tests/"
}
}
}

34
phpunit.xml.dist Normal file
View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="webonyx/graphql-php Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
<php>
<ini name="error_reporting" value="E_ALL"/>
</php>
</phpunit>

View File

@ -6,6 +6,7 @@ use GraphQL\Executor\Executor;
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\QueryComplexity;
class GraphQL
{
@ -35,6 +36,11 @@ class GraphQL
try {
$source = new Source($requestString ?: '', 'GraphQL request');
$documentAST = Parser::parse($source);
/** @var QueryComplexity $queryComplexity */
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
$queryComplexity->setRawVariableValues($variableValues);
$validationErrors = DocumentValidator::validate($schema, $documentAST);
if (!empty($validationErrors)) {

View File

@ -5,6 +5,8 @@ use GraphQL\Utils;
class FieldDefinition
{
const DEFAULT_COMPLEXITY_FN = 'GraphQL\Type\Definition\FieldDefinition::defaultComplexity';
/**
* @var string
*/
@ -72,6 +74,7 @@ class FieldDefinition
'map' => Config::CALLBACK,
'description' => Config::STRING,
'deprecationReason' => Config::STRING,
'complexity' => Config::CALLBACK,
]);
}
@ -113,6 +116,8 @@ class FieldDefinition
$this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null;
$this->config = $config;
$this->complexityFn = isset($config['complexity']) ? $config['complexity'] : static::DEFAULT_COMPLEXITY_FN;
}
/**
@ -141,4 +146,17 @@ class FieldDefinition
}
return $this->resolvedType;
}
/**
* @return callable|\Closure
*/
public function getComplexityFn()
{
return $this->complexityFn;
}
public static function defaultComplexity($childrenComplexity)
{
return $childrenComplexity + 1;
}
}

View File

@ -34,59 +34,91 @@ use GraphQL\Validator\Rules\NoUnusedVariables;
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\ScalarLeafs;
use GraphQL\Validator\Rules\VariablesAreInputTypes;
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
class DocumentValidator
{
private static $allRules;
private static $rules = [];
static function allRules()
private static $defaultRules;
private static $initRules = false;
public static function allRules()
{
if (null === self::$allRules) {
self::$allRules = [
if (!self::$initRules) {
self::$rules = array_merge(static::defaultRules(), self::$rules);
self::$initRules = true;
}
return self::$rules;
}
public static function defaultRules()
{
if (null === self::$defaultRules) {
self::$defaultRules = [
// new UniqueOperationNames,
// new LoneAnonymousOperation,
new KnownTypeNames,
new FragmentsOnCompositeTypes,
new VariablesAreInputTypes,
new ScalarLeafs,
new FieldsOnCorrectType,
'KnownTypeNames' => new KnownTypeNames(),
'FragmentsOnCompositeTypes' => new FragmentsOnCompositeTypes(),
'VariablesAreInputTypes' => new VariablesAreInputTypes(),
'ScalarLeafs' => new ScalarLeafs(),
'FieldsOnCorrectType' => new FieldsOnCorrectType(),
// new UniqueFragmentNames,
new KnownFragmentNames,
new NoUnusedFragments,
new PossibleFragmentSpreads,
new NoFragmentCycles,
new NoUndefinedVariables,
new NoUnusedVariables,
new KnownDirectives,
new KnownArgumentNames,
'KnownFragmentNames' => new KnownFragmentNames(),
'NoUnusedFragments' => new NoUnusedFragments(),
'PossibleFragmentSpreads' => new PossibleFragmentSpreads(),
'NoFragmentCycles' => new NoFragmentCycles(),
'NoUndefinedVariables' => new NoUndefinedVariables(),
'NoUnusedVariables' => new NoUnusedVariables(),
'KnownDirectives' => new KnownDirectives(),
'KnownArgumentNames' => new KnownArgumentNames(),
// new UniqueArgumentNames,
new ArgumentsOfCorrectType,
new ProvidedNonNullArguments,
new DefaultValuesOfCorrectType,
new VariablesInAllowedPosition,
new OverlappingFieldsCanBeMerged,
'ArgumentsOfCorrectType' => new ArgumentsOfCorrectType(),
'ProvidedNonNullArguments' => new ProvidedNonNullArguments(),
'DefaultValuesOfCorrectType' => new DefaultValuesOfCorrectType(),
'VariablesInAllowedPosition' => new VariablesInAllowedPosition(),
'OverlappingFieldsCanBeMerged' => new OverlappingFieldsCanBeMerged(),
// Query Security
'QueryDepth' => new QueryDepth(QueryDepth::DISABLED), // default disabled
'QueryComplexity' => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
];
}
return self::$allRules;
return self::$defaultRules;
}
public static function getRule($name)
{
$rules = static::allRules();
return isset($rules[$name]) ? $rules[$name] : null ;
}
public static function addRule($name, callable $rule)
{
self::$rules[$name] = $rule;
}
public static function validate(Schema $schema, Document $ast, array $rules = null)
{
$errors = self::visitUsingRules($schema, $ast, $rules ?: self::allRules());
$errors = static::visitUsingRules($schema, $ast, $rules ?: static::allRules());
return $errors;
}
static function isError($value)
public static function isError($value)
{
return is_array($value)
? count(array_filter($value, function($item) { return $item instanceof \Exception;})) === count($value)
: $value instanceof \Exception;
}
static function append(&$arr, $items)
public static function append(&$arr, $items)
{
if (is_array($items)) {
$arr = array_merge($arr, $items);
@ -96,7 +128,7 @@ class DocumentValidator
return $arr;
}
static function isValidLiteralValue($valueAST, Type $type)
public static function isValidLiteralValue($valueAST, Type $type)
{
// A value can only be not provided if the type is nullable.
if (!$valueAST) {
@ -105,7 +137,7 @@ class DocumentValidator
// Unwrap non-null.
if ($type instanceof NonNull) {
return self::isValidLiteralValue($valueAST, $type->getWrappedType());
return static::isValidLiteralValue($valueAST, $type->getWrappedType());
}
// This function only tests literals, and assumes variables will provide
@ -123,13 +155,13 @@ class DocumentValidator
$itemType = $type->getWrappedType();
if ($valueAST instanceof ListValue) {
foreach($valueAST->values as $itemAST) {
if (!self::isValidLiteralValue($itemAST, $itemType)) {
if (!static::isValidLiteralValue($itemAST, $itemType)) {
return false;
}
}
return true;
} else {
return self::isValidLiteralValue($valueAST, $itemType);
return static::isValidLiteralValue($valueAST, $itemType);
}
}
@ -157,7 +189,7 @@ class DocumentValidator
}
}
foreach ($fieldASTs as $fieldAST) {
if (empty($fields[$fieldAST->name->value]) || !self::isValidLiteralValue($fieldAST->value, $fields[$fieldAST->name->value]->getType())) {
if (empty($fields[$fieldAST->name->value]) || !static::isValidLiteralValue($fieldAST->value, $fields[$fieldAST->name->value]->getType())) {
return false;
}
}
@ -231,8 +263,8 @@ class DocumentValidator
} else if ($result->doBreak) {
$instances[$i] = null;
}
} else if ($result && self::isError($result)) {
self::append($errors, $result);
} else if ($result && static::isError($result)) {
static::append($errors, $result);
for ($j = $i - 1; $j >= 0; $j--) {
$leaveFn = Visitor::getVisitFn($instances[$j], true, $node->kind);
if ($leaveFn) {
@ -243,8 +275,8 @@ class DocumentValidator
if ($result->doBreak) {
$instances[$j] = null;
}
} else if (self::isError($result)) {
self::append($errors, $result);
} else if (static::isError($result)) {
static::append($errors, $result);
} else if ($result !== null) {
throw new \Exception("Config cannot edit document.");
}
@ -294,8 +326,8 @@ class DocumentValidator
if ($result->doBreak) {
$instances[$i] = null;
}
} else if (self::isError($result)) {
self::append($errors, $result);
} else if (static::isError($result)) {
static::append($errors, $result);
} else if ($result !== null) {
throw new \Exception("Config cannot edit document.");
}
@ -309,7 +341,7 @@ class DocumentValidator
// Visit the whole document with instances of all provided rules.
$allRuleInstances = [];
foreach ($rules as $rule) {
$allRuleInstances[] = $rule($context);
$allRuleInstances[] = call_user_func_array($rule, [$context]);
}
$visitInstances($documentAST, $allRuleInstances);

View 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();
}

View 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;
}
}

View 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;
}
}

View File

@ -1,8 +1,8 @@
<?php
namespace GraphQL\Executor;
require_once __DIR__ . '/TestClasses.php';
namespace GraphQL\Tests\Executor;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Executor;
use GraphQL\Language\Parser;
use GraphQL\Schema;
use GraphQL\Type\Definition\InterfaceType;

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Executor;
namespace GraphQL\Tests\Executor;
use GraphQL\Executor\Executor;
use GraphQL\Language\Parser;
use GraphQL\Schema;
use GraphQL\Type\Definition\ObjectType;

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Executor;
namespace GraphQL\Tests\Executor;
use GraphQL\Executor\Executor;
use GraphQL\Language\Parser;
use GraphQL\Schema;
use GraphQL\Type\Definition\Config;

View File

@ -1,7 +1,8 @@
<?php
namespace GraphQL\Executor;
namespace GraphQL\Tests\Executor;
use GraphQL\Error;
use GraphQL\Executor\Executor;
use GraphQL\FormattedError;
use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;

View File

@ -1,7 +1,8 @@
<?php
namespace GraphQL\Executor;
namespace GraphQL\Tests\Executor;
use GraphQL\Error;
use GraphQL\Executor\Executor;
use GraphQL\FormattedError;
use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Executor;
namespace GraphQL\Tests\Executor;
use GraphQL\Executor\Executor;
use GraphQL\FormattedError;
use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;

View File

@ -1,7 +1,8 @@
<?php
namespace GraphQL\Executor;
namespace GraphQL\Tests\Executor;
use GraphQL\Error;
use GraphQL\Executor\Executor;
use GraphQL\FormattedError;
use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Executor;
namespace GraphQL\Tests\Executor;
use GraphQL\Type\Definition\ScalarType;

View File

@ -1,8 +1,9 @@
<?php
namespace GraphQL\Executor;
namespace GraphQL\Tests\Executor;
require_once __DIR__ . '/TestClasses.php';
use GraphQL\Executor\Executor;
use GraphQL\Language\Parser;
use GraphQL\Schema;
use GraphQL\Type\Definition\Config;

View File

@ -1,9 +1,10 @@
<?php
namespace GraphQL\Executor;
namespace GraphQL\Tests\Executor;
require_once __DIR__ . '/TestClasses.php';
use GraphQL\Error;
use GraphQL\Executor\Executor;
use GraphQL\FormattedError;
use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;

View File

@ -1,6 +1,9 @@
<?php
namespace GraphQL\Language;
namespace GraphQL\Tests\Language;
use GraphQL\Language\Lexer;
use GraphQL\Language\Source;
use GraphQL\Language\Token;
use GraphQL\SyntaxError;
class LexerTest extends \PHPUnit_Framework_TestCase

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Language;
namespace GraphQL\Tests\Language;
use GraphQL\Error;
use GraphQL\Language\AST\Argument;
@ -10,6 +10,9 @@ use GraphQL\Language\AST\Location;
use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Language\AST\SelectionSet;
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
use GraphQL\SyntaxError;
class ParserTest extends \PHPUnit_Framework_TestCase

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Language;
namespace GraphQL\Tests\Language;
use GraphQL\Language\AST\Document;
use GraphQL\Language\AST\EnumValue;
@ -10,6 +10,8 @@ use GraphQL\Language\AST\SelectionSet;
use GraphQL\Language\AST\StringValue;
use GraphQL\Language\AST\Variable;
use GraphQL\Language\AST\VariableDefinition;
use GraphQL\Language\Parser;
use GraphQL\Language\Printer;
class PrinterTest extends \PHPUnit_Framework_TestCase
{

View File

@ -1,10 +1,12 @@
<?php
namespace GraphQL\Language;
namespace GraphQL\Tests\Language;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\SelectionSet;
use GraphQL\Language\Parser;
use GraphQL\Language\Visitor;
class VisitorTest extends \PHPUnit_Framework_TestCase
{

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL;
namespace GraphQL\Tests;
class StarWarsData
{

View File

@ -1,7 +1,9 @@
<?php
namespace GraphQL;
namespace GraphQL\Tests;
use GraphQL\GraphQL;
class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
{
// Star Wars Introspection Tests

View File

@ -1,7 +1,9 @@
<?php
namespace GraphQL;
namespace GraphQL\Tests;
use GraphQL\GraphQL;
class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
{
// Star Wars Query Tests

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL;
namespace GraphQL\Tests;
/**
* This is designed to be an end-to-end test, demonstrating
@ -11,6 +11,7 @@ namespace GraphQL;
* NOTE: This may contain spoilers for the original Star
* Wars trilogy.
*/
use GraphQL\Schema;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\NonNull;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL;
namespace GraphQL\Tests;
use GraphQL\Language\Parser;
use GraphQL\Validator\DocumentValidator;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Type;
namespace GraphQL\Tests\Type;
use GraphQL\Schema;
use GraphQL\Type\Definition\Config;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Type;
namespace GraphQL\Tests\Type;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;
@ -9,6 +9,7 @@ use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Introspection;
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
class IntrospectionTest extends \PHPUnit_Framework_TestCase

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Type;
namespace GraphQL\Tests\Type;
use GraphQL\GraphQL;
use GraphQL\Schema;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Type;
namespace GraphQL\Tests\Type;
use GraphQL\Type\Definition\Type;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Type;
namespace GraphQL\Tests\Type;
use GraphQL\Schema;
use GraphQL\Type\Definition\InputObjectType;
@ -9,6 +9,8 @@ use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Type\SchemaValidator;
class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
{

View 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)]);
}
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View File

@ -1,8 +1,9 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;
use GraphQL\Validator\Messages;
use GraphQL\Validator\Rules\DefaultValuesOfCorrectType;
class DefaultValuesOfCorrectTypeTest extends TestCase

View File

@ -1,8 +1,9 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;
use GraphQL\Validator\Messages;
use GraphQL\Validator\Rules\FieldsOnCorrectType;
class FieldsOnCorrectTypeTest extends TestCase

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\Validator\Rules\KnownFragmentNames;
use GraphQL\FormattedError;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\Source;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View 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);
}
}
}

View 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;
}
}

View 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;
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\Language\Parser;
use GraphQL\Schema;
@ -10,6 +10,7 @@ use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Validator\DocumentValidator;
abstract class TestCase extends \PHPUnit_Framework_TestCase
{

View File

@ -1,5 +1,5 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;

View File

@ -1,8 +1,9 @@
<?php
namespace GraphQL\Validator;
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;
use GraphQL\Validator\Messages;
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
class VariablesInAllowedPositionTest extends TestCase