Version 4.6.0

This commit is contained in:
Neo committed 2025-12-19 05:55:31 -08:00
1 parent f79dcf067a
commit 517a5e1f70
2036 files changed
+110041 -26162

No files matched your search

+192 -173
View File
@@ -1,52 +1,56 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator;
use Exception;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\LeafType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Utils;
use GraphQL\Type\Schema;
use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\Rules\AbstractValidationRule;
use GraphQL\Validator\Rules\ArgumentsOfCorrectType;
use GraphQL\Validator\Rules\DefaultValuesOfCorrectType;
use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\Rules\ExecutableDefinitions;
use GraphQL\Validator\Rules\FieldsOnCorrectType;
use GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
use GraphQL\Validator\Rules\KnownArgumentNames;
use GraphQL\Validator\Rules\KnownArgumentNamesOnDirectives;
use GraphQL\Validator\Rules\KnownDirectives;
use GraphQL\Validator\Rules\KnownFragmentNames;
use GraphQL\Validator\Rules\KnownTypeNames;
use GraphQL\Validator\Rules\LoneAnonymousOperation;
use GraphQL\Validator\Rules\LoneSchemaDefinition;
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;
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
use GraphQL\Validator\Rules\ProvidedRequiredArguments;
use GraphQL\Validator\Rules\ProvidedRequiredArgumentsOnDirectives;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\QuerySecurityRule;
use GraphQL\Validator\Rules\ScalarLeafs;
use GraphQL\Validator\Rules\SingleFieldSubscription;
use GraphQL\Validator\Rules\UniqueArgumentNames;
use GraphQL\Validator\Rules\UniqueDirectivesPerLocation;
use GraphQL\Validator\Rules\UniqueFragmentNames;
use GraphQL\Validator\Rules\UniqueInputFieldNames;
use GraphQL\Validator\Rules\UniqueOperationNames;
use GraphQL\Validator\Rules\UniqueVariableNames;
use GraphQL\Validator\Rules\ValidationRule;
use GraphQL\Validator\Rules\ValuesOfCorrectType;
use GraphQL\Validator\Rules\VariablesAreInputTypes;
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
use Throwable;
use function array_filter;
use function array_merge;
use function count;
use function is_array;
use function sprintf;
/**
* Implements the "Validation" section of the spec.
@@ -57,7 +61,7 @@ use GraphQL\Validator\Rules\VariablesInAllowedPosition;
* A list of specific validation rules may be provided. If not provided, the
* default list of rules defined by the GraphQL specification will be used.
*
* Each validation rule is an instance of GraphQL\Validator\Rules\AbstractValidationRule
* Each validation rule is an instance of GraphQL\Validator\Rules\ValidationRule
* which returns a visitor (see the [GraphQL\Language\Visitor API](reference.md#graphqllanguagevisitor)).
*
* Visitor methods are expected to return an instance of [GraphQL\Error\Error](reference.md#graphqlerrorerror),
@@ -68,50 +72,61 @@ use GraphQL\Validator\Rules\VariablesInAllowedPosition;
*/
class DocumentValidator
{
/** @var ValidationRule[] */
private static $rules = [];
/** @var ValidationRule[]|null */
private static $defaultRules;
/** @var QuerySecurityRule[]|null */
private static $securityRules;
/** @var ValidationRule[]|null */
private static $sdlRules;
/** @var bool */
private static $initRules = false;
/**
* Primary method for query validation. See class description for details.
*
* @api
* @param Schema $schema
* @param DocumentNode $ast
* @param AbstractValidationRule[]|null $rules
* @param TypeInfo|null $typeInfo
* @param ValidationRule[]|null $rules
*
* @return Error[]
*
* @api
*/
public static function validate(
Schema $schema,
DocumentNode $ast,
array $rules = null,
TypeInfo $typeInfo = null
)
{
if (null === $rules) {
?array $rules = null,
?TypeInfo $typeInfo = null
) {
if ($rules === null) {
$rules = static::allRules();
}
$typeInfo = $typeInfo ?: new TypeInfo($schema);
$errors = static::visitUsingRules($schema, $typeInfo, $ast, $rules);
return $errors;
}
if (is_array($rules) === true && count($rules) === 0) {
// Skip validation if there are no rules
return [];
}
$typeInfo = $typeInfo ?? new TypeInfo($schema);
return static::visitUsingRules($schema, $typeInfo, $ast, $rules);
}
/**
* Returns all global validation rules.
*
* @return ValidationRule[]
*
* @api
* @return AbstractValidationRule[]
*/
public static function allRules()
{
if (!self::$initRules) {
static::$rules = array_merge(static::defaultRules(), self::securityRules(), self::$rules);
if (! self::$initRules) {
static::$rules = array_merge(static::defaultRules(), self::securityRules(), self::$rules);
static::$initRules = true;
}
@@ -120,33 +135,34 @@ class DocumentValidator
public static function defaultRules()
{
if (null === self::$defaultRules) {
if (self::$defaultRules === null) {
self::$defaultRules = [
UniqueOperationNames::class => new UniqueOperationNames(),
LoneAnonymousOperation::class => new LoneAnonymousOperation(),
KnownTypeNames::class => new KnownTypeNames(),
FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(),
VariablesAreInputTypes::class => new VariablesAreInputTypes(),
ScalarLeafs::class => new ScalarLeafs(),
FieldsOnCorrectType::class => new FieldsOnCorrectType(),
UniqueFragmentNames::class => new UniqueFragmentNames(),
KnownFragmentNames::class => new KnownFragmentNames(),
NoUnusedFragments::class => new NoUnusedFragments(),
PossibleFragmentSpreads::class => new PossibleFragmentSpreads(),
NoFragmentCycles::class => new NoFragmentCycles(),
UniqueVariableNames::class => new UniqueVariableNames(),
NoUndefinedVariables::class => new NoUndefinedVariables(),
NoUnusedVariables::class => new NoUnusedVariables(),
KnownDirectives::class => new KnownDirectives(),
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
KnownArgumentNames::class => new KnownArgumentNames(),
UniqueArgumentNames::class => new UniqueArgumentNames(),
ArgumentsOfCorrectType::class => new ArgumentsOfCorrectType(),
ProvidedNonNullArguments::class => new ProvidedNonNullArguments(),
DefaultValuesOfCorrectType::class => new DefaultValuesOfCorrectType(),
VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
ExecutableDefinitions::class => new ExecutableDefinitions(),
UniqueOperationNames::class => new UniqueOperationNames(),
LoneAnonymousOperation::class => new LoneAnonymousOperation(),
SingleFieldSubscription::class => new SingleFieldSubscription(),
KnownTypeNames::class => new KnownTypeNames(),
FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(),
VariablesAreInputTypes::class => new VariablesAreInputTypes(),
ScalarLeafs::class => new ScalarLeafs(),
FieldsOnCorrectType::class => new FieldsOnCorrectType(),
UniqueFragmentNames::class => new UniqueFragmentNames(),
KnownFragmentNames::class => new KnownFragmentNames(),
NoUnusedFragments::class => new NoUnusedFragments(),
PossibleFragmentSpreads::class => new PossibleFragmentSpreads(),
NoFragmentCycles::class => new NoFragmentCycles(),
UniqueVariableNames::class => new UniqueVariableNames(),
NoUndefinedVariables::class => new NoUndefinedVariables(),
NoUnusedVariables::class => new NoUnusedVariables(),
KnownDirectives::class => new KnownDirectives(),
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
KnownArgumentNames::class => new KnownArgumentNames(),
UniqueArgumentNames::class => new UniqueArgumentNames(),
ValuesOfCorrectType::class => new ValuesOfCorrectType(),
ProvidedRequiredArguments::class => new ProvidedRequiredArguments(),
VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(),
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
];
}
@@ -154,7 +170,7 @@ class DocumentValidator
}
/**
* @return array
* @return QuerySecurityRule[]
*/
public static function securityRules()
{
@@ -162,25 +178,65 @@ class DocumentValidator
// When custom security rule is required - it should be just added via DocumentValidator::addRule();
// TODO: deprecate this
if (null === self::$securityRules) {
if (self::$securityRules === null) {
self::$securityRules = [
DisableIntrospection::class => new DisableIntrospection(DisableIntrospection::DISABLED), // DEFAULT DISABLED
QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), // default disabled
QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), // default disabled
QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
];
}
return self::$securityRules;
}
public static function sdlRules()
{
if (self::$sdlRules === null) {
self::$sdlRules = [
LoneSchemaDefinition::class => new LoneSchemaDefinition(),
KnownDirectives::class => new KnownDirectives(),
KnownArgumentNamesOnDirectives::class => new KnownArgumentNamesOnDirectives(),
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
UniqueArgumentNames::class => new UniqueArgumentNames(),
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
ProvidedRequiredArgumentsOnDirectives::class => new ProvidedRequiredArgumentsOnDirectives(),
];
}
return self::$sdlRules;
}
/**
* This uses a specialized visitor which runs multiple visitors in parallel,
* while maintaining the visitor skip and break API.
*
* @param ValidationRule[] $rules
*
* @return Error[]
*/
public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, DocumentNode $documentNode, array $rules)
{
$context = new ValidationContext($schema, $documentNode, $typeInfo);
$visitors = [];
foreach ($rules as $rule) {
$visitors[] = $rule->getVisitor($context);
}
Visitor::visit($documentNode, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors)));
return $context->getErrors();
}
/**
* Returns global validation rule by name. Standard rules are named by class name, so
* example usage for such rules:
*
* $rule = DocumentValidator::getRule(GraphQL\Validator\Rules\QueryComplexity::class);
*
* @api
* @param string $name
* @return AbstractValidationRule
*
* @return ValidationRule
*
* @api
*/
public static function getRule($name)
{
@@ -190,17 +246,17 @@ class DocumentValidator
return $rules[$name];
}
$name = "GraphQL\\Validator\\Rules\\$name";
return isset($rules[$name]) ? $rules[$name] : null ;
$name = sprintf('GraphQL\\Validator\\Rules\\%s', $name);
return $rules[$name] ?? null;
}
/**
* Add rule to list of global validation rules
*
* @api
* @param AbstractValidationRule $rule
*/
public static function addRule(AbstractValidationRule $rule)
public static function addRule(ValidationRule $rule)
{
self::$rules[$rule->getName()] = $rule;
}
@@ -208,8 +264,13 @@ class DocumentValidator
public static function isError($value)
{
return is_array($value)
? count(array_filter($value, function($item) { return $item instanceof \Exception || $item instanceof \Throwable;})) === count($value)
: ($value instanceof \Exception || $value instanceof \Throwable);
? count(array_filter(
$value,
static function ($item) : bool {
return $item instanceof Throwable;
}
)) === count($value)
: $value instanceof Throwable;
}
public static function append(&$arr, $items)
@@ -219,124 +280,82 @@ class DocumentValidator
} else {
$arr[] = $items;
}
return $arr;
}
/**
* Utility for validators which determines if a value literal AST is valid given
* an input type.
* Utility which determines if a value literal node is valid for an input type.
*
* Note that this only validates literal values, variables are assumed to
* provide values of the correct type.
* Deprecated. Rely on validation for documents co
* ntaining literal values.
*
* @return array
* @deprecated
*
* @return Error[]
*/
public static function isValidLiteralValue(Type $type, $valueNode)
{
// A value must be provided if the type is non-null.
if ($type instanceof NonNull) {
if (!$valueNode || $valueNode instanceof NullValueNode) {
return [ 'Expected "' . Utils::printSafe($type) . '", found null.' ];
}
return static::isValidLiteralValue($type->getWrappedType(), $valueNode);
}
$emptySchema = new Schema([]);
$emptyDoc = new DocumentNode(['definitions' => []]);
$typeInfo = new TypeInfo($emptySchema, $type);
$context = new ValidationContext($emptySchema, $emptyDoc, $typeInfo);
$validator = new ValuesOfCorrectType();
$visitor = $validator->getVisitor($context);
Visitor::visit($valueNode, Visitor::visitWithTypeInfo($typeInfo, $visitor));
if (!$valueNode || $valueNode instanceof NullValueNode) {
return [];
}
// This function only tests literals, and assumes variables will provide
// values of the correct type.
if ($valueNode instanceof VariableNode) {
return [];
}
// Lists accept a non-list value as a list of one.
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if ($valueNode instanceof ListValueNode) {
$errors = [];
foreach($valueNode->values as $index => $itemNode) {
$tmp = static::isValidLiteralValue($itemType, $itemNode);
if ($tmp) {
$errors = array_merge($errors, Utils::map($tmp, function($error) use ($index) {
return "In element #$index: $error";
}));
}
}
return $errors;
} else {
return static::isValidLiteralValue($itemType, $valueNode);
}
}
// Input objects check each defined field and look for undefined fields.
if ($type instanceof InputObjectType) {
if ($valueNode->kind !== NodeKind::OBJECT) {
return [ "Expected \"{$type->name}\", found not an object." ];
}
$fields = $type->getFields();
$errors = [];
// Ensure every provided field is defined.
$fieldNodes = $valueNode->fields;
foreach ($fieldNodes as $providedFieldNode) {
if (empty($fields[$providedFieldNode->name->value])) {
$errors[] = "In field \"{$providedFieldNode->name->value}\": Unknown field.";
}
}
// Ensure every defined field is valid.
$fieldNodeMap = Utils::keyMap($fieldNodes, function($fieldNode) {return $fieldNode->name->value;});
foreach ($fields as $fieldName => $field) {
$result = static::isValidLiteralValue(
$field->getType(),
isset($fieldNodeMap[$fieldName]) ? $fieldNodeMap[$fieldName]->value : null
);
if ($result) {
$errors = array_merge($errors, Utils::map($result, function($error) use ($fieldName) {
return "In field \"$fieldName\": $error";
}));
}
}
return $errors;
}
if ($type instanceof LeafType) {
// Scalars must parse to a non-null value
if (!$type->isValidLiteral($valueNode)) {
$printed = Printer::doPrint($valueNode);
return [ "Expected type \"{$type->name}\", found $printed." ];
}
return [];
}
throw new InvariantViolation('Must be input type');
return $context->getErrors();
}
/**
* This uses a specialized visitor which runs multiple visitors in parallel,
* while maintaining the visitor skip and break API.
* @param ValidationRule[]|null $rules
*
* @param Schema $schema
* @param TypeInfo $typeInfo
* @param DocumentNode $documentNode
* @param AbstractValidationRule[] $rules
* @return array
* @return Error[]
*
* @throws Exception
*/
public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, DocumentNode $documentNode, array $rules)
{
$context = new ValidationContext($schema, $documentNode, $typeInfo);
$visitors = [];
foreach ($rules as $rule) {
$visitors[] = $rule->getVisitor($context);
public static function validateSDL(
DocumentNode $documentAST,
?Schema $schemaToExtend = null,
?array $rules = null
) {
$usedRules = $rules ?? self::sdlRules();
$context = new SDLValidationContext($documentAST, $schemaToExtend);
$visitors = [];
foreach ($usedRules as $rule) {
$visitors[] = $rule->getSDLVisitor($context);
}
Visitor::visit($documentNode, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors)));
Visitor::visit($documentAST, Visitor::visitInParallel($visitors));
return $context->getErrors();
}
public static function assertValidSDL(DocumentNode $documentAST)
{
$errors = self::validateSDL($documentAST);
if (count($errors) > 0) {
throw new Error(self::combineErrorMessages($errors));
}
}
public static function assertValidSDLExtension(DocumentNode $documentAST, Schema $schema)
{
$errors = self::validateSDL($documentAST, $schema);
if (count($errors) > 0) {
throw new Error(self::combineErrorMessages($errors));
}
}
/**
* @param Error[] $errors
*/
private static function combineErrorMessages(array $errors) : string
{
$str = '';
foreach ($errors as $error) {
$str .= ($error->getMessage() . "\n\n");
}
return $str;
}
}