Completed
Push — master ( a01b08...b72ba3 )
by Vladimir
16s queued 14s
created

QueryComplexity   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 258
Duplicated Lines 0 %

Test Coverage

Coverage 89.92%

Importance

Changes 0
Metric Value
wmc 37
eloc 119
dl 0
loc 258
ccs 116
cts 129
cp 0.8992
rs 9.44
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getVisitor() 0 43 3
A __construct() 0 3 1
A fieldComplexity() 0 9 4
B nodeComplexity() 0 51 10
A astFieldInfo() 0 14 4
A directiveExcludesField() 0 33 5
A getMaxQueryComplexity() 0 3 1
A setRawVariableValues() 0 3 2
A setMaxQueryComplexity() 0 5 1
A maxQueryComplexityErrorMessage() 0 3 1
A buildFieldArguments() 0 31 3
A getRawVariableValues() 0 3 1
A isEnabled() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Validator\Rules;
6
7
use ArrayObject;
8
use GraphQL\Error\Error;
9
use GraphQL\Executor\Values;
10
use GraphQL\Language\AST\FieldNode;
11
use GraphQL\Language\AST\FragmentSpreadNode;
12
use GraphQL\Language\AST\InlineFragmentNode;
13
use GraphQL\Language\AST\Node;
14
use GraphQL\Language\AST\NodeKind;
15
use GraphQL\Language\AST\OperationDefinitionNode;
16
use GraphQL\Language\AST\SelectionSetNode;
17
use GraphQL\Language\Visitor;
18
use GraphQL\Type\Definition\Directive;
19
use GraphQL\Type\Definition\FieldDefinition;
20
use GraphQL\Validator\ValidationContext;
21
use function array_map;
22
use function call_user_func_array;
23
use function implode;
24
use function method_exists;
25
use function sprintf;
26
27
class QueryComplexity extends QuerySecurityRule
28
{
29
    /** @var int */
30
    private $maxQueryComplexity;
31
32
    /** @var mixed[]|null */
33
    private $rawVariableValues = [];
34
35
    /** @var ArrayObject */
36
    private $variableDefs;
37
38
    /** @var ArrayObject */
39
    private $fieldNodeAndDefs;
40
41
    /** @var ValidationContext */
42
    private $context;
43
44 2
    public function __construct($maxQueryComplexity)
45
    {
46 2
        $this->setMaxQueryComplexity($maxQueryComplexity);
47 2
    }
48
49 130
    public function getVisitor(ValidationContext $context)
50
    {
51 130
        $this->context = $context;
52
53 130
        $this->variableDefs     = new ArrayObject();
54 130
        $this->fieldNodeAndDefs = new ArrayObject();
55 130
        $complexity             = 0;
56
57 130
        return $this->invokeIfNeeded(
58 130
            $context,
59
            [
60
                NodeKind::SELECTION_SET        => function (SelectionSetNode $selectionSet) use ($context) {
61 16
                    $this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs(
62 16
                        $context,
63 16
                        $context->getParentType(),
64 16
                        $selectionSet,
65 16
                        null,
66 16
                        $this->fieldNodeAndDefs
67
                    );
68 130
                },
69
                NodeKind::VARIABLE_DEFINITION  => function ($def) {
70 6
                    $this->variableDefs[] = $def;
71
72 6
                    return Visitor::skipNode();
73 130
                },
74
                NodeKind::OPERATION_DEFINITION => [
75
                    'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) {
76 16
                        $errors = $context->getErrors();
77
78 16
                        if (! empty($errors)) {
79 1
                            return;
80
                        }
81
82 16
                        $complexity = $this->fieldComplexity($operationDefinition, $complexity);
83
84 15
                        if ($complexity <= $this->getMaxQueryComplexity()) {
85 15
                            return;
86
                        }
87
88 13
                        $context->reportError(
89 13
                            new Error(self::maxQueryComplexityErrorMessage(
90 13
                                $this->getMaxQueryComplexity(),
91 13
                                $complexity
92
                            ))
93
                        );
94 130
                    },
95
                ],
96
            ]
97
        );
98
    }
99
100 16
    private function fieldComplexity($node, $complexity = 0)
101
    {
102 16
        if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSetNode) {
103 16
            foreach ($node->selectionSet->selections as $childNode) {
104 16
                $complexity = $this->nodeComplexity($childNode, $complexity);
105
            }
106
        }
107
108 16
        return $complexity;
109
    }
110
111 16
    private function nodeComplexity(Node $node, $complexity = 0)
112
    {
113
        switch (true) {
114 16
            case $node instanceof FieldNode:
115
                // default values
116 16
                $args         = [];
117 16
                $complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN;
118
119
                // calculate children complexity if needed
120 16
                $childrenComplexity = 0;
121
122
                // node has children?
123 16
                if (isset($node->selectionSet)) {
124 16
                    $childrenComplexity = $this->fieldComplexity($node);
125
                }
126
127 16
                $astFieldInfo = $this->astFieldInfo($node);
128 16
                $fieldDef     = $astFieldInfo[1];
129
130 16
                if ($fieldDef instanceof FieldDefinition) {
131 16
                    if ($this->directiveExcludesField($node)) {
132 3
                        break;
133
                    }
134
135 16
                    $args = $this->buildFieldArguments($node);
136
                    //get complexity fn using fieldDef complexity
137 16
                    if (method_exists($fieldDef, 'getComplexityFn')) {
138 16
                        $complexityFn = $fieldDef->getComplexityFn();
139
                    }
140
                }
141
142 16
                $complexity += call_user_func_array($complexityFn, [$childrenComplexity, $args]);
143 16
                break;
144
145 3
            case $node instanceof InlineFragmentNode:
146
                // node has children?
147 1
                if (isset($node->selectionSet)) {
148 1
                    $complexity = $this->fieldComplexity($node, $complexity);
149
                }
150 1
                break;
151
152 2
            case $node instanceof FragmentSpreadNode:
153 2
                $fragment = $this->getFragment($node);
154
155 2
                if ($fragment !== null) {
156 2
                    $complexity = $this->fieldComplexity($fragment, $complexity);
157
                }
158 2
                break;
159
        }
160
161 16
        return $complexity;
162
    }
163
164 16
    private function astFieldInfo(FieldNode $field)
165
    {
166 16
        $fieldName    = $this->getFieldName($field);
167 16
        $astFieldInfo = [null, null];
168 16
        if (isset($this->fieldNodeAndDefs[$fieldName])) {
169 16
            foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) {
170 16
                if ($astAndDef[0] === $field) {
171 16
                    $astFieldInfo = $astAndDef;
172 16
                    break;
173
                }
174
            }
175
        }
176
177 16
        return $astFieldInfo;
178
    }
179
180 16
    private function directiveExcludesField(FieldNode $node)
181
    {
182 16
        foreach ($node->directives as $directiveNode) {
183 5
            if ($directiveNode->name->value === 'deprecated') {
184
                return false;
185
            }
186 5
            [$errors, $variableValues] = Values::getVariableValues(
187 5
                $this->context->getSchema(),
188 5
                $this->variableDefs,
0 ignored issues
show
Bug introduced by
$this->variableDefs of type ArrayObject is incompatible with the type GraphQL\Language\AST\VariableDefinitionNode[] expected by parameter $varDefNodes of GraphQL\Executor\Values::getVariableValues(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

188
                /** @scrutinizer ignore-type */ $this->variableDefs,
Loading history...
189 5
                $this->getRawVariableValues()
0 ignored issues
show
Bug introduced by
It seems like $this->getRawVariableValues() can also be of type null; however, parameter $inputs of GraphQL\Executor\Values::getVariableValues() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

189
                /** @scrutinizer ignore-type */ $this->getRawVariableValues()
Loading history...
190
            );
191 5
            if (! empty($errors)) {
192
                throw new Error(implode(
193
                    "\n\n",
194
                    array_map(
195
                        static function ($error) {
196
                            return $error->getMessage();
197
                        },
198
                        $errors
199
                    )
200
                ));
201
            }
202 5
            if ($directiveNode->name->value === 'include') {
203 3
                $directive = Directive::includeDirective();
204
                /** @var bool $directiveArgsIf */
205 3
                $directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues)['if'];
206
207 3
                return ! $directiveArgsIf;
208
            }
209 3
            $directive       = Directive::skipDirective();
210 3
            $directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues);
211
212 3
            return $directiveArgsIf['if'];
213
        }
214 16
    }
215
216 16
    public function getRawVariableValues()
217
    {
218 16
        return $this->rawVariableValues;
219
    }
220
221
    /**
222
     * @param mixed[]|null $rawVariableValues
223
     */
224 112
    public function setRawVariableValues(?array $rawVariableValues = null)
225
    {
226 112
        $this->rawVariableValues = $rawVariableValues ?: [];
227 112
    }
228
229 16
    private function buildFieldArguments(FieldNode $node)
230
    {
231 16
        $rawVariableValues = $this->getRawVariableValues();
232 16
        $astFieldInfo      = $this->astFieldInfo($node);
233 16
        $fieldDef          = $astFieldInfo[1];
234
235 16
        $args = [];
236
237 16
        if ($fieldDef instanceof FieldDefinition) {
238 16
            [$errors, $variableValues] = Values::getVariableValues(
239 16
                $this->context->getSchema(),
240 16
                $this->variableDefs,
0 ignored issues
show
Bug introduced by
$this->variableDefs of type ArrayObject is incompatible with the type GraphQL\Language\AST\VariableDefinitionNode[] expected by parameter $varDefNodes of GraphQL\Executor\Values::getVariableValues(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

240
                /** @scrutinizer ignore-type */ $this->variableDefs,
Loading history...
241 16
                $rawVariableValues
0 ignored issues
show
Bug introduced by
It seems like $rawVariableValues can also be of type null; however, parameter $inputs of GraphQL\Executor\Values::getVariableValues() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

241
                /** @scrutinizer ignore-type */ $rawVariableValues
Loading history...
242
            );
243
244 16
            if (! empty($errors)) {
245
                throw new Error(implode(
246
                    "\n\n",
247
                    array_map(
248
                        static function ($error) {
249
                            return $error->getMessage();
250
                        },
251
                        $errors
252
                    )
253
                ));
254
            }
255
256 16
            $args = Values::getArgumentValues($fieldDef, $node, $variableValues);
257
        }
258
259 16
        return $args;
260
    }
261
262 130
    public function getMaxQueryComplexity()
263
    {
264 130
        return $this->maxQueryComplexity;
265
    }
266
267
    /**
268
     * Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0.
269
     */
270 18
    public function setMaxQueryComplexity($maxQueryComplexity)
271
    {
272 18
        $this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity);
273
274 17
        $this->maxQueryComplexity = (int) $maxQueryComplexity;
275 17
    }
276
277 13
    public static function maxQueryComplexityErrorMessage($max, $count)
278
    {
279 13
        return sprintf('Max query complexity should be %d but got %d.', $max, $count);
280
    }
281
282 130
    protected function isEnabled()
283
    {
284 130
        return $this->getMaxQueryComplexity() !== self::DISABLED;
285
    }
286
}
287