new KnownTypeNames(), 'FragmentsOnCompositeTypes' => new FragmentsOnCompositeTypes(), 'VariablesAreInputTypes' => new VariablesAreInputTypes(), 'ScalarLeafs' => new ScalarLeafs(), 'FieldsOnCorrectType' => new FieldsOnCorrectType(), // new UniqueFragmentNames, '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, '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::$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 = static::visitUsingRules($schema, $ast, $rules ?: static::allRules()); return $errors; } public static function isError($value) { return is_array($value) ? count(array_filter($value, function($item) { return $item instanceof \Exception;})) === count($value) : $value instanceof \Exception; } public static function append(&$arr, $items) { if (is_array($items)) { $arr = array_merge($arr, $items); } else { $arr[] = $items; } return $arr; } public static function isValidLiteralValue($valueAST, Type $type) { // A value can only be not provided if the type is nullable. if (!$valueAST) { return !($type instanceof NonNull); } // Unwrap non-null. if ($type instanceof NonNull) { return static::isValidLiteralValue($valueAST, $type->getWrappedType()); } // This function only tests literals, and assumes variables will provide // values of the correct type. if ($valueAST instanceof Variable) { return true; } if (!$valueAST instanceof Value) { return false; } // Lists accept a non-list value as a list of one. if ($type instanceof ListOfType) { $itemType = $type->getWrappedType(); if ($valueAST instanceof ListValue) { foreach($valueAST->values as $itemAST) { if (!static::isValidLiteralValue($itemAST, $itemType)) { return false; } } return true; } else { return static::isValidLiteralValue($valueAST, $itemType); } } // Scalar/Enum input checks to ensure the type can serialize the value to // a non-null value. if ($type instanceof ScalarType || $type instanceof EnumType) { return $type->parseLiteral($valueAST) !== null; } // Input objects check each defined field, ensuring it is of the correct // type and provided if non-nullable. if ($type instanceof InputObjectType) { $fields = $type->getFields(); if ($valueAST->kind !== Node::OBJECT) { return false; } $fieldASTs = $valueAST->fields; $fieldASTMap = Utils::keyMap($fieldASTs, function($field) {return $field->name->value;}); foreach ($fields as $fieldKey => $field) { $fieldName = $field->name ?: $fieldKey; if (!isset($fieldASTMap[$fieldName]) && $field->getType() instanceof NonNull) { // Required fields missing return false; } } foreach ($fieldASTs as $fieldAST) { if (empty($fields[$fieldAST->name->value]) || !static::isValidLiteralValue($fieldAST->value, $fields[$fieldAST->name->value]->getType())) { return false; } } return true; } // Any other kind of type is not an input type, and a literal cannot be used. return false; } /** * This uses a specialized visitor which runs multiple visitors in parallel, * while maintaining the visitor skip and break API. * * @param Schema $schema * @param Document $documentAST * @param array $rules * @return array */ public static function visitUsingRules(Schema $schema, Document $documentAST, array $rules) { $typeInfo = new TypeInfo($schema); $context = new ValidationContext($schema, $documentAST, $typeInfo); $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 { $enter = Visitor::getVisitFn($instances[$i], false, $node->kind); 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; } } 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) { // $leaveFn = $leaveFn->bindTo($instances[$j]) $result = call_user_func_array($leaveFn, func_get_args()); if ($result instanceof VisitorOperation) { if ($result->doBreak) { $instances[$j] = null; } } else if (static::isError($result)) { static::append($errors, $result); } 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; } $leaveFn = Visitor::getVisitFn($instances[$i], true, $node->kind); 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; } } else if (static::isError($result)) { static::append($errors, $result); } 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) { $allRuleInstances[] = call_user_func_array($rule, [$context]); } $visitInstances($documentAST, $allRuleInstances); return $errors; } }