Completed
Push — master ( 39bfaa...9e787e )
by Vladimir
15s queued 14s
created

QueryComplexity::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
crap 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
    /** @var int */
45
    private $complexity;
46
47 2
    public function __construct($maxQueryComplexity)
48
    {
49 2
        $this->setMaxQueryComplexity($maxQueryComplexity);
50 2
    }
51
52 132
    public function getVisitor(ValidationContext $context)
53
    {
54 132
        $this->context = $context;
55
56 132
        $this->variableDefs     = new ArrayObject();
57 132
        $this->fieldNodeAndDefs = new ArrayObject();
58 132
        $this->complexity       = 0;
59
60 132
        return $this->invokeIfNeeded(
61 132
            $context,
62
            [
63
                NodeKind::SELECTION_SET        => function (SelectionSetNode $selectionSet) use ($context) {
64 17
                    $this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs(
65 17
                        $context,
66 17
                        $context->getParentType(),
67 17
                        $selectionSet,
68 17
                        null,
69 17
                        $this->fieldNodeAndDefs
70
                    );
71 132
                },
72
                NodeKind::VARIABLE_DEFINITION  => function ($def) {
73 6
                    $this->variableDefs[] = $def;
74
75 6
                    return Visitor::skipNode();
76 132
                },
77
                NodeKind::OPERATION_DEFINITION => [
78
                    'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) {
79 17
                        $errors = $context->getErrors();
80
81 17
                        if (! empty($errors)) {
82 1
                            return;
83
                        }
84
85 17
                        $this->complexity = $this->fieldComplexity($operationDefinition, $complexity);
86
87 16
                        if ($this->getQueryComplexity() <= $this->getMaxQueryComplexity()) {
88 16
                            return;
89
                        }
90
91 13
                        $context->reportError(
92 13
                            new Error(self::maxQueryComplexityErrorMessage(
93 13
                                $this->getMaxQueryComplexity(),
94 13
                                $this->getQueryComplexity()
95
                            ))
96
                        );
97 132
                    },
98
                ],
99
            ]
100
        );
101
    }
102
103 17
    private function fieldComplexity($node, $complexity = 0)
104
    {
105 17
        if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSetNode) {
106 17
            foreach ($node->selectionSet->selections as $childNode) {
107 17
                $complexity = $this->nodeComplexity($childNode, $complexity);
108
            }
109
        }
110
111 17
        return $complexity;
112
    }
113
114 17
    private function nodeComplexity(Node $node, $complexity = 0)
115
    {
116
        switch (true) {
117 17
            case $node instanceof FieldNode:
118
                // default values
119 17
                $args         = [];
120 17
                $complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN;
121
122
                // calculate children complexity if needed
123 17
                $childrenComplexity = 0;
124
125
                // node has children?
126 17
                if (isset($node->selectionSet)) {
127 17
                    $childrenComplexity = $this->fieldComplexity($node);
128
                }
129
130 17
                $astFieldInfo = $this->astFieldInfo($node);
131 17
                $fieldDef     = $astFieldInfo[1];
132
133 17
                if ($fieldDef instanceof FieldDefinition) {
134 17
                    if ($this->directiveExcludesField($node)) {
135 3
                        break;
136
                    }
137
138 17
                    $args = $this->buildFieldArguments($node);
139
                    //get complexity fn using fieldDef complexity
140 17
                    if (method_exists($fieldDef, 'getComplexityFn')) {
141 17
                        $complexityFn = $fieldDef->getComplexityFn();
142
                    }
143
                }
144
145 17
                $complexity += call_user_func_array($complexityFn, [$childrenComplexity, $args]);
146 17
                break;
147
148 3
            case $node instanceof InlineFragmentNode:
149
                // node has children?
150 1
                if (isset($node->selectionSet)) {
151 1
                    $complexity = $this->fieldComplexity($node, $complexity);
152
                }
153 1
                break;
154
155 2
            case $node instanceof FragmentSpreadNode:
156 2
                $fragment = $this->getFragment($node);
157
158 2
                if ($fragment !== null) {
159 2
                    $complexity = $this->fieldComplexity($fragment, $complexity);
160
                }
161 2
                break;
162
        }
163
164 17
        return $complexity;
165
    }
166
167 17
    private function astFieldInfo(FieldNode $field)
168
    {
169 17
        $fieldName    = $this->getFieldName($field);
170 17
        $astFieldInfo = [null, null];
171 17
        if (isset($this->fieldNodeAndDefs[$fieldName])) {
172 17
            foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) {
173 17
                if ($astAndDef[0] === $field) {
174 17
                    $astFieldInfo = $astAndDef;
175 17
                    break;
176
                }
177
            }
178
        }
179
180 17
        return $astFieldInfo;
181
    }
182
183 17
    private function directiveExcludesField(FieldNode $node)
184
    {
185 17
        foreach ($node->directives as $directiveNode) {
186 5
            if ($directiveNode->name->value === 'deprecated') {
187
                return false;
188
            }
189 5
            [$errors, $variableValues] = Values::getVariableValues(
190 5
                $this->context->getSchema(),
191 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

191
                /** @scrutinizer ignore-type */ $this->variableDefs,
Loading history...
192 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

192
                /** @scrutinizer ignore-type */ $this->getRawVariableValues()
Loading history...
193
            );
194 5
            if (! empty($errors)) {
195
                throw new Error(implode(
196
                    "\n\n",
197
                    array_map(
198
                        static function ($error) {
199
                            return $error->getMessage();
200
                        },
201
                        $errors
202
                    )
203
                ));
204
            }
205 5
            if ($directiveNode->name->value === 'include') {
206 3
                $directive = Directive::includeDirective();
207
                /** @var bool $directiveArgsIf */
208 3
                $directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues)['if'];
209
210 3
                return ! $directiveArgsIf;
211
            }
212 3
            $directive       = Directive::skipDirective();
213 3
            $directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues);
214
215 3
            return $directiveArgsIf['if'];
216
        }
217 17
    }
218
219 17
    public function getRawVariableValues()
220
    {
221 17
        return $this->rawVariableValues;
222
    }
223
224
    /**
225
     * @param mixed[]|null $rawVariableValues
226
     */
227 113
    public function setRawVariableValues(?array $rawVariableValues = null)
228
    {
229 113
        $this->rawVariableValues = $rawVariableValues ?: [];
230 113
    }
231
232 17
    private function buildFieldArguments(FieldNode $node)
233
    {
234 17
        $rawVariableValues = $this->getRawVariableValues();
235 17
        $astFieldInfo      = $this->astFieldInfo($node);
236 17
        $fieldDef          = $astFieldInfo[1];
237
238 17
        $args = [];
239
240 17
        if ($fieldDef instanceof FieldDefinition) {
241 17
            [$errors, $variableValues] = Values::getVariableValues(
242 17
                $this->context->getSchema(),
243 17
                $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

243
                /** @scrutinizer ignore-type */ $this->variableDefs,
Loading history...
244 17
                $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

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