2015-07-15 23:05:46 +06:00
|
|
|
<?php
|
|
|
|
namespace GraphQL\Validator;
|
|
|
|
|
|
|
|
use GraphQL\Error;
|
2015-08-17 20:01:55 +06:00
|
|
|
use GraphQL\Language\AST\ListValue;
|
2015-07-15 23:05:46 +06:00
|
|
|
use GraphQL\Language\AST\Document;
|
|
|
|
use GraphQL\Language\AST\FragmentSpread;
|
|
|
|
use GraphQL\Language\AST\Node;
|
|
|
|
use GraphQL\Language\AST\Value;
|
|
|
|
use GraphQL\Language\AST\Variable;
|
2016-04-25 03:57:09 +06:00
|
|
|
use GraphQL\Language\Printer;
|
2015-07-15 23:05:46 +06:00
|
|
|
use GraphQL\Language\Visitor;
|
|
|
|
use GraphQL\Language\VisitorOperation;
|
|
|
|
use GraphQL\Schema;
|
|
|
|
use GraphQL\Type\Definition\EnumType;
|
|
|
|
use GraphQL\Type\Definition\InputObjectType;
|
2016-04-25 03:57:09 +06:00
|
|
|
use GraphQL\Type\Definition\InputType;
|
2015-07-15 23:05:46 +06:00
|
|
|
use GraphQL\Type\Definition\ListOfType;
|
|
|
|
use GraphQL\Type\Definition\NonNull;
|
|
|
|
use GraphQL\Type\Definition\ScalarType;
|
|
|
|
use GraphQL\Type\Definition\Type;
|
|
|
|
use GraphQL\Utils;
|
|
|
|
use GraphQL\Utils\TypeInfo;
|
|
|
|
use GraphQL\Validator\Rules\ArgumentsOfCorrectType;
|
|
|
|
use GraphQL\Validator\Rules\DefaultValuesOfCorrectType;
|
|
|
|
use GraphQL\Validator\Rules\FieldsOnCorrectType;
|
|
|
|
use GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
|
|
|
|
use GraphQL\Validator\Rules\KnownArgumentNames;
|
|
|
|
use GraphQL\Validator\Rules\KnownDirectives;
|
|
|
|
use GraphQL\Validator\Rules\KnownFragmentNames;
|
|
|
|
use GraphQL\Validator\Rules\KnownTypeNames;
|
2016-04-25 03:57:09 +06:00
|
|
|
use GraphQL\Validator\Rules\LoneAnonymousOperation;
|
2015-07-15 23:05:46 +06:00
|
|
|
use GraphQL\Validator\Rules\NoFragmentCycles;
|
|
|
|
use GraphQL\Validator\Rules\NoUndefinedVariables;
|
|
|
|
use GraphQL\Validator\Rules\NoUnusedFragments;
|
|
|
|
use GraphQL\Validator\Rules\NoUnusedVariables;
|
|
|
|
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
|
|
|
|
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
|
2015-08-17 20:01:55 +06:00
|
|
|
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
|
2016-04-09 10:04:14 +02:00
|
|
|
use GraphQL\Validator\Rules\QueryComplexity;
|
|
|
|
use GraphQL\Validator\Rules\QueryDepth;
|
2015-07-15 23:05:46 +06:00
|
|
|
use GraphQL\Validator\Rules\ScalarLeafs;
|
|
|
|
use GraphQL\Validator\Rules\VariablesAreInputTypes;
|
|
|
|
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
|
|
|
|
|
|
|
|
class DocumentValidator
|
|
|
|
{
|
2016-04-09 08:44:57 +02:00
|
|
|
private static $rules = [];
|
2015-07-15 23:05:46 +06:00
|
|
|
|
2016-04-09 08:44:57 +02:00
|
|
|
private static $defaultRules;
|
|
|
|
|
|
|
|
private static $initRules = false;
|
|
|
|
|
|
|
|
public static function allRules()
|
2015-07-15 23:05:46 +06:00
|
|
|
{
|
2016-04-09 08:44:57 +02:00
|
|
|
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 = [
|
2016-04-25 03:57:09 +06:00
|
|
|
// 'UniqueOperationNames' => new UniqueOperationNames(),
|
|
|
|
'LoneAnonymousOperation' => new LoneAnonymousOperation(),
|
2016-04-09 08:44:57 +02:00
|
|
|
'KnownTypeNames' => new KnownTypeNames(),
|
|
|
|
'FragmentsOnCompositeTypes' => new FragmentsOnCompositeTypes(),
|
|
|
|
'VariablesAreInputTypes' => new VariablesAreInputTypes(),
|
|
|
|
'ScalarLeafs' => new ScalarLeafs(),
|
|
|
|
'FieldsOnCorrectType' => new FieldsOnCorrectType(),
|
2016-04-25 03:57:09 +06:00
|
|
|
// 'UniqueFragmentNames' => new UniqueFragmentNames(),
|
2016-04-09 08:44:57 +02:00
|
|
|
'KnownFragmentNames' => new KnownFragmentNames(),
|
|
|
|
'NoUnusedFragments' => new NoUnusedFragments(),
|
|
|
|
'PossibleFragmentSpreads' => new PossibleFragmentSpreads(),
|
|
|
|
'NoFragmentCycles' => new NoFragmentCycles(),
|
2016-04-25 03:57:09 +06:00
|
|
|
// 'UniqueVariableNames' => new UniqueVariableNames(),
|
2016-04-09 08:44:57 +02:00
|
|
|
'NoUndefinedVariables' => new NoUndefinedVariables(),
|
|
|
|
'NoUnusedVariables' => new NoUnusedVariables(),
|
|
|
|
'KnownDirectives' => new KnownDirectives(),
|
|
|
|
'KnownArgumentNames' => new KnownArgumentNames(),
|
2016-04-25 03:57:09 +06:00
|
|
|
// 'UniqueArgumentNames' => new UniqueArgumentNames(),
|
2016-04-09 08:44:57 +02:00
|
|
|
'ArgumentsOfCorrectType' => new ArgumentsOfCorrectType(),
|
|
|
|
'ProvidedNonNullArguments' => new ProvidedNonNullArguments(),
|
|
|
|
'DefaultValuesOfCorrectType' => new DefaultValuesOfCorrectType(),
|
|
|
|
'VariablesInAllowedPosition' => new VariablesInAllowedPosition(),
|
|
|
|
'OverlappingFieldsCanBeMerged' => new OverlappingFieldsCanBeMerged(),
|
2016-04-25 03:57:09 +06:00
|
|
|
// 'UniqueInputFieldNames' => new UniqueInputFieldNames(),
|
|
|
|
|
2016-04-09 10:04:14 +02:00
|
|
|
// Query Security
|
|
|
|
'QueryDepth' => new QueryDepth(QueryDepth::DISABLED), // default disabled
|
|
|
|
'QueryComplexity' => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
|
2015-07-15 23:05:46 +06:00
|
|
|
];
|
|
|
|
}
|
2016-04-09 08:44:57 +02:00
|
|
|
|
|
|
|
return self::$defaultRules;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function getRule($name)
|
|
|
|
{
|
2016-04-09 10:04:14 +02:00
|
|
|
$rules = static::allRules();
|
|
|
|
|
|
|
|
return isset($rules[$name]) ? $rules[$name] : null ;
|
2016-04-09 08:44:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public static function addRule($name, callable $rule)
|
|
|
|
{
|
|
|
|
self::$rules[$name] = $rule;
|
|
|
|
}
|
|
|
|
|
2015-07-15 23:05:46 +06:00
|
|
|
public static function validate(Schema $schema, Document $ast, array $rules = null)
|
|
|
|
{
|
2016-04-25 03:57:09 +06:00
|
|
|
$typeInfo = new TypeInfo($schema);
|
|
|
|
$errors = static::visitUsingRules($schema, $typeInfo, $ast, $rules ?: static::allRules());
|
2015-08-17 20:01:55 +06:00
|
|
|
return $errors;
|
2015-07-15 23:05:46 +06:00
|
|
|
}
|
|
|
|
|
2016-04-09 08:44:57 +02:00
|
|
|
public static function isError($value)
|
2015-07-15 23:05:46 +06:00
|
|
|
{
|
|
|
|
return is_array($value)
|
|
|
|
? count(array_filter($value, function($item) { return $item instanceof \Exception;})) === count($value)
|
|
|
|
: $value instanceof \Exception;
|
|
|
|
}
|
|
|
|
|
2016-04-09 08:44:57 +02:00
|
|
|
public static function append(&$arr, $items)
|
2015-07-15 23:05:46 +06:00
|
|
|
{
|
|
|
|
if (is_array($items)) {
|
|
|
|
$arr = array_merge($arr, $items);
|
|
|
|
} else {
|
|
|
|
$arr[] = $items;
|
|
|
|
}
|
|
|
|
return $arr;
|
|
|
|
}
|
|
|
|
|
2016-04-25 03:57:09 +06:00
|
|
|
/**
|
|
|
|
* Utility for validators which determines if a value literal AST is valid given
|
|
|
|
* an input type.
|
|
|
|
*
|
|
|
|
* Note that this only validates literal values, variables are assumed to
|
|
|
|
* provide values of the correct type.
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public static function isValidLiteralValue(Type $type, $valueAST)
|
2015-07-15 23:05:46 +06:00
|
|
|
{
|
2016-04-25 03:57:09 +06:00
|
|
|
// A value must be provided if the type is non-null.
|
|
|
|
if ($type instanceof NonNull) {
|
|
|
|
$wrappedType = $type->getWrappedType();
|
|
|
|
if (!$valueAST) {
|
|
|
|
if ($wrappedType->name) {
|
|
|
|
return [ "Expected \"{$wrappedType->name}!\", found null." ];
|
|
|
|
}
|
|
|
|
return ['Expected non-null value, found null.'];
|
|
|
|
}
|
|
|
|
return static::isValidLiteralValue($wrappedType, $valueAST);
|
2015-07-15 23:05:46 +06:00
|
|
|
}
|
|
|
|
|
2016-04-25 03:57:09 +06:00
|
|
|
if (!$valueAST) {
|
|
|
|
return [];
|
2015-07-15 23:05:46 +06:00
|
|
|
}
|
|
|
|
|
|
|
|
// This function only tests literals, and assumes variables will provide
|
|
|
|
// values of the correct type.
|
|
|
|
if ($valueAST instanceof Variable) {
|
2016-04-25 03:57:09 +06:00
|
|
|
return [];
|
2015-07-15 23:05:46 +06:00
|
|
|
}
|
|
|
|
|
|
|
|
// Lists accept a non-list value as a list of one.
|
|
|
|
if ($type instanceof ListOfType) {
|
|
|
|
$itemType = $type->getWrappedType();
|
2015-08-17 20:01:55 +06:00
|
|
|
if ($valueAST instanceof ListValue) {
|
2016-04-25 03:57:09 +06:00
|
|
|
$errors = [];
|
|
|
|
foreach($valueAST->values as $index => $itemAST) {
|
|
|
|
$tmp = static::isValidLiteralValue($itemType, $itemAST);
|
|
|
|
|
|
|
|
if ($tmp) {
|
|
|
|
$errors = array_merge($errors, Utils::map($tmp, function($error) use ($index) {
|
|
|
|
return "In element #$index: $error";
|
|
|
|
}));
|
2015-07-15 23:05:46 +06:00
|
|
|
}
|
|
|
|
}
|
2016-04-25 03:57:09 +06:00
|
|
|
return $errors;
|
2015-07-15 23:05:46 +06:00
|
|
|
} else {
|
2016-04-25 03:57:09 +06:00
|
|
|
return static::isValidLiteralValue($itemType, $valueAST);
|
2015-07-15 23:05:46 +06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-25 03:57:09 +06:00
|
|
|
// Input objects check each defined field and look for undefined fields.
|
2015-07-15 23:05:46 +06:00
|
|
|
if ($type instanceof InputObjectType) {
|
|
|
|
if ($valueAST->kind !== Node::OBJECT) {
|
2016-04-25 03:57:09 +06:00
|
|
|
return [ "Expected \"{$type->name}\", found not an object." ];
|
2015-07-15 23:05:46 +06:00
|
|
|
}
|
2016-04-25 03:57:09 +06:00
|
|
|
|
|
|
|
$fields = $type->getFields();
|
|
|
|
$errors = [];
|
|
|
|
|
|
|
|
// Ensure every provided field is defined.
|
2015-07-15 23:05:46 +06:00
|
|
|
$fieldASTs = $valueAST->fields;
|
|
|
|
|
2016-04-25 03:57:09 +06:00
|
|
|
foreach ($fieldASTs as $providedFieldAST) {
|
|
|
|
if (empty($fields[$providedFieldAST->name->value])) {
|
|
|
|
$errors[] = "In field \"{$providedFieldAST->name->value}\": Unknown field.";
|
2015-07-15 23:05:46 +06:00
|
|
|
}
|
|
|
|
}
|
2016-04-25 03:57:09 +06:00
|
|
|
|
|
|
|
// Ensure every defined field is valid.
|
|
|
|
$fieldASTMap = Utils::keyMap($fieldASTs, function($fieldAST) {return $fieldAST->name->value;});
|
|
|
|
foreach ($fields as $fieldName => $field) {
|
|
|
|
$result = static::isValidLiteralValue(
|
|
|
|
$field->getType(),
|
|
|
|
isset($fieldASTMap[$fieldName]) ? $fieldASTMap[$fieldName]->value : null
|
|
|
|
);
|
|
|
|
if ($result) {
|
|
|
|
$errors = array_merge($errors, Utils::map($result, function($error) use ($fieldName) {
|
|
|
|
return "In field \"$fieldName\": $error";
|
|
|
|
}));
|
2015-07-15 23:05:46 +06:00
|
|
|
}
|
|
|
|
}
|
2016-04-25 03:57:09 +06:00
|
|
|
|
|
|
|
return $errors;
|
2015-07-15 23:05:46 +06:00
|
|
|
}
|
|
|
|
|
2016-04-25 03:57:09 +06:00
|
|
|
Utils::invariant(
|
|
|
|
$type instanceof ScalarType || $type instanceof EnumType,
|
|
|
|
'Must be input type'
|
|
|
|
);
|
|
|
|
|
|
|
|
// Scalar/Enum input checks to ensure the type can parse the value to
|
|
|
|
// a non-null value.
|
|
|
|
$parseResult = $type->parseLiteral($valueAST);
|
|
|
|
|
|
|
|
if (null === $parseResult) {
|
|
|
|
$printed = Printer::doPrint($valueAST);
|
|
|
|
return [ "Expected type \"{$type->name}\", found $printed." ];
|
|
|
|
}
|
|
|
|
|
|
|
|
return [];
|
2015-07-15 23:05:46 +06:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This uses a specialized visitor which runs multiple visitors in parallel,
|
|
|
|
* while maintaining the visitor skip and break API.
|
|
|
|
*
|
|
|
|
* @param Schema $schema
|
2016-04-25 03:57:09 +06:00
|
|
|
* @param TypeInfo $typeInfo
|
2015-07-15 23:05:46 +06:00
|
|
|
* @param Document $documentAST
|
|
|
|
* @param array $rules
|
|
|
|
* @return array
|
|
|
|
*/
|
2016-04-25 03:57:09 +06:00
|
|
|
public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, Document $documentAST, array $rules)
|
2015-07-15 23:05:46 +06:00
|
|
|
{
|
|
|
|
$context = new ValidationContext($schema, $documentAST, $typeInfo);
|
2016-04-25 03:57:09 +06:00
|
|
|
$visitors = [];
|
|
|
|
foreach ($rules as $rule) {
|
|
|
|
$visitors[] = $rule($context);
|
|
|
|
}
|
|
|
|
Visitor::visit($documentAST, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors)));
|
|
|
|
return $context->getErrors();
|
|
|
|
|
|
|
|
|
|
|
|
|
2015-07-15 23:05:46 +06:00
|
|
|
$errors = [];
|
|
|
|
|
|
|
|
// TODO: convert to class
|
|
|
|
$visitInstances = function($ast, $instances) use ($typeInfo, $context, &$errors, &$visitInstances) {
|
|
|
|
$skipUntil = new \SplFixedArray(count($instances));
|
|
|
|
$skipCount = 0;
|
|
|
|
|
|
|
|
Visitor::visit($ast, [
|
|
|
|
'enter' => function ($node, $key) use ($typeInfo, $instances, $skipUntil, &$skipCount, &$errors, $context, $visitInstances) {
|
|
|
|
$typeInfo->enter($node);
|
|
|
|
for ($i = 0; $i < count($instances); $i++) {
|
|
|
|
// Do not visit this instance if it returned false for a previous node
|
|
|
|
if ($skipUntil[$i]) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$result = null;
|
|
|
|
|
|
|
|
// Do not visit top level fragment definitions if this instance will
|
|
|
|
// visit those fragments inline because it
|
|
|
|
// provided `visitSpreadFragments`.
|
|
|
|
if ($node->kind === Node::FRAGMENT_DEFINITION && $key !== null && !empty($instances[$i]['visitSpreadFragments'])) {
|
|
|
|
$result = Visitor::skipNode();
|
|
|
|
} else {
|
2016-04-25 03:57:09 +06:00
|
|
|
$enter = Visitor::getVisitFn($instances[$i], $node->kind, false);
|
2015-07-15 23:05:46 +06:00
|
|
|
if ($enter instanceof \Closure) {
|
|
|
|
// $enter = $enter->bindTo($instances[$i]);
|
|
|
|
$result = call_user_func_array($enter, func_get_args());
|
|
|
|
} else {
|
|
|
|
$result = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($result instanceof VisitorOperation) {
|
|
|
|
if ($result->doContinue) {
|
|
|
|
$skipUntil[$i] = $node;
|
|
|
|
$skipCount++;
|
|
|
|
// If all instances are being skipped over, skip deeper traversal
|
|
|
|
if ($skipCount === count($instances)) {
|
|
|
|
for ($k = 0; $k < count($instances); $k++) {
|
|
|
|
if ($skipUntil[$k] === $node) {
|
|
|
|
$skipUntil[$k] = null;
|
|
|
|
$skipCount--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Visitor::skipNode();
|
|
|
|
}
|
|
|
|
} else if ($result->doBreak) {
|
|
|
|
$instances[$i] = null;
|
|
|
|
}
|
2016-04-09 08:44:57 +02:00
|
|
|
} else if ($result && static::isError($result)) {
|
|
|
|
static::append($errors, $result);
|
2015-07-15 23:05:46 +06:00
|
|
|
for ($j = $i - 1; $j >= 0; $j--) {
|
2016-04-25 03:57:09 +06:00
|
|
|
$leaveFn = Visitor::getVisitFn($instances[$j], $node->kind, true);
|
2015-07-15 23:05:46 +06:00
|
|
|
if ($leaveFn) {
|
|
|
|
// $leaveFn = $leaveFn->bindTo($instances[$j])
|
|
|
|
$result = call_user_func_array($leaveFn, func_get_args());
|
|
|
|
|
|
|
|
if ($result instanceof VisitorOperation) {
|
|
|
|
if ($result->doBreak) {
|
|
|
|
$instances[$j] = null;
|
|
|
|
}
|
2016-04-09 08:44:57 +02:00
|
|
|
} else if (static::isError($result)) {
|
|
|
|
static::append($errors, $result);
|
2015-07-15 23:05:46 +06:00
|
|
|
} else if ($result !== null) {
|
|
|
|
throw new \Exception("Config cannot edit document.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$typeInfo->leave($node);
|
|
|
|
return Visitor::skipNode();
|
|
|
|
} else if ($result !== null) {
|
|
|
|
throw new \Exception("Config cannot edit document.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If any validation instances provide the flag `visitSpreadFragments`
|
|
|
|
// and this node is a fragment spread, validate the fragment from
|
|
|
|
// this point.
|
|
|
|
if ($node instanceof FragmentSpread) {
|
|
|
|
$fragment = $context->getFragment($node->name->value);
|
|
|
|
if ($fragment) {
|
|
|
|
$fragVisitingInstances = [];
|
|
|
|
foreach ($instances as $idx => $inst) {
|
|
|
|
if (!empty($inst['visitSpreadFragments']) && !$skipUntil[$idx]) {
|
|
|
|
$fragVisitingInstances[] = $inst;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!empty($fragVisitingInstances)) {
|
|
|
|
$visitInstances($fragment, $fragVisitingInstances);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
'leave' => function ($node) use ($instances, $typeInfo, $skipUntil, &$skipCount, &$errors) {
|
|
|
|
for ($i = count($instances) - 1; $i >= 0; $i--) {
|
|
|
|
if ($skipUntil[$i]) {
|
|
|
|
if ($skipUntil[$i] === $node) {
|
|
|
|
$skipUntil[$i] = null;
|
|
|
|
$skipCount--;
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
2016-04-25 03:57:09 +06:00
|
|
|
$leaveFn = Visitor::getVisitFn($instances[$i], $node->kind, true);
|
2015-07-15 23:05:46 +06:00
|
|
|
|
|
|
|
if ($leaveFn) {
|
|
|
|
// $leaveFn = $leaveFn.bindTo($instances[$i]);
|
|
|
|
$result = call_user_func_array($leaveFn, func_get_args());
|
|
|
|
|
|
|
|
if ($result instanceof VisitorOperation) {
|
|
|
|
if ($result->doBreak) {
|
|
|
|
$instances[$i] = null;
|
|
|
|
}
|
2016-04-09 08:44:57 +02:00
|
|
|
} else if (static::isError($result)) {
|
|
|
|
static::append($errors, $result);
|
2015-07-15 23:05:46 +06:00
|
|
|
} else if ($result !== null) {
|
|
|
|
throw new \Exception("Config cannot edit document.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$typeInfo->leave($node);
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Visit the whole document with instances of all provided rules.
|
|
|
|
$allRuleInstances = [];
|
|
|
|
foreach ($rules as $rule) {
|
2016-04-09 08:44:57 +02:00
|
|
|
$allRuleInstances[] = call_user_func_array($rule, [$context]);
|
2015-07-15 23:05:46 +06:00
|
|
|
}
|
|
|
|
$visitInstances($documentAST, $allRuleInstances);
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
}
|
|
|
|
}
|