Completed
Push — master ( 655442...cd31b0 )
by Vladimir
17s queued 14s
created

QueryComplexity::directiveExcludesField()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.8935

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 23
c 1
b 1
f 0
dl 0
loc 39
ccs 17
cts 24
cp 0.7083
rs 8.9297
cc 6
nc 6
nop 1
crap 6.8935
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 135
    public function getVisitor(ValidationContext $context)
53
    {
54 135
        $this->context = $context;
55
56 135
        $this->variableDefs     = new ArrayObject();
57 135
        $this->fieldNodeAndDefs = new ArrayObject();
58 135
        $this->complexity       = 0;
59
60 135
        return $this->invokeIfNeeded(
61 135
            $context,
62
            [
63
                NodeKind::SELECTION_SET        => function (SelectionSetNode $selectionSet) use ($context) {
64 19
                    $this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs(
65 19
                        $context,
66 19
                        $context->getParentType(),
67 19
                        $selectionSet,
68 19
                        null,
69 19
                        $this->fieldNodeAndDefs
70
                    );
71 135
                },
72
                NodeKind::VARIABLE_DEFINITION  => function ($def) {
73 7
                    $this->variableDefs[] = $def;
74
75 7
                    return Visitor::skipNode();
76 135
                },
77
                NodeKind::OPERATION_DEFINITION => [
78
                    'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) {
79 19
                        $errors = $context->getErrors();
80
81 19
                        if (! empty($errors)) {
82 1
                            return;
83
                        }
84
85 19
                        $this->complexity = $this->fieldComplexity($operationDefinition, $complexity);
86
87 18
                        if ($this->getQueryComplexity() <= $this->getMaxQueryComplexity()) {
88 18
                            return;
89
                        }
90
91 14
                        $context->reportError(
92 14
                            new Error(self::maxQueryComplexityErrorMessage(
93 14
                                $this->getMaxQueryComplexity(),
94 14
                                $this->getQueryComplexity()
95
                            ))
96
                        );
97 135
                    },
98
                ],
99
            ]
100
        );
101
    }
102
103 19
    private function fieldComplexity($node, $complexity = 0)
104
    {
105 19
        if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSetNode) {
106 19
            foreach ($node->selectionSet->selections as $childNode) {
107 19
                $complexity = $this->nodeComplexity($childNode, $complexity);
108
            }
109
        }
110
111 19
        return $complexity;
112
    }
113
114 19
    private function nodeComplexity(Node $node, $complexity = 0)
115
    {
116
        switch (true) {
117 19
            case $node instanceof FieldNode:
118
                // default values
119 19
                $args         = [];
120 19
                $complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN;
121
122
                // calculate children complexity if needed
123 19
                $childrenComplexity = 0;
124
125
                // node has children?
126 19
                if (isset($node->selectionSet)) {
127 19
                    $childrenComplexity = $this->fieldComplexity($node);
128
                }
129
130 19
                $astFieldInfo = $this->astFieldInfo($node);
131 19
                $fieldDef     = $astFieldInfo[1];
132
133 19
                if ($fieldDef instanceof FieldDefinition) {
134 19
                    if ($this->directiveExcludesField($node)) {
135 4
                        break;
136
                    }
137
138 19
                    $args = $this->buildFieldArguments($node);
139
                    //get complexity fn using fieldDef complexity
140 19
                    if (method_exists($fieldDef, 'getComplexityFn')) {
141 19
                        $complexityFn = $fieldDef->getComplexityFn();
142
                    }
143
                }
144
145 19
                $complexity += call_user_func_array($complexityFn, [$childrenComplexity, $args]);
146 19
                break;
147
148 4
            case $node instanceof InlineFragmentNode:
149
                // node has children?
150 2
                if (isset($node->selectionSet)) {
151 2
                    $complexity = $this->fieldComplexity($node, $complexity);
152
                }
153 2
                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 19
        return $complexity;
165
    }
166
167 19
    private function astFieldInfo(FieldNode $field)
168
    {
169 19
        $fieldName    = $this->getFieldName($field);
170 19
        $astFieldInfo = [null, null];
171 19
        if (isset($this->fieldNodeAndDefs[$fieldName])) {
172 19
            foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) {
173 19
                if ($astAndDef[0] === $field) {
174 19
                    $astFieldInfo = $astAndDef;
175 19
                    break;
176
                }
177
            }
178
        }
179
180 19
        return $astFieldInfo;
181
    }
182
183 19
    private function directiveExcludesField(FieldNode $node)
184
    {
185 19
        foreach ($node->directives as $directiveNode) {
186 7
            if ($directiveNode->name->value === 'deprecated') {
187
                return false;
188
            }
189 7
            [$errors, $variableValues] = Values::getVariableValues(
190 7
                $this->context->getSchema(),
191 7
                $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 7
                $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 7
            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 7
            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 5
            if ($directiveNode->name->value === Directive::SKIP_NAME) {
213 4
                $directive = Directive::skipDirective();
214
                /** @var bool $directiveArgsIf */
215 4
                $directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues)['if'];
216
217 5
                return $directiveArgsIf;
218
            }
219
        }
220
221 19
        return false;
222
    }
223
224 19
    public function getRawVariableValues()
225
    {
226 19
        return $this->rawVariableValues;
227
    }
228
229
    /**
230
     * @param mixed[]|null $rawVariableValues
231
     */
232 115
    public function setRawVariableValues(?array $rawVariableValues = null)
233
    {
234 115
        $this->rawVariableValues = $rawVariableValues ?: [];
235 115
    }
236
237 19
    private function buildFieldArguments(FieldNode $node)
238
    {
239 19
        $rawVariableValues = $this->getRawVariableValues();
240 19
        $astFieldInfo      = $this->astFieldInfo($node);
241 19
        $fieldDef          = $astFieldInfo[1];
242
243 19
        $args = [];
244
245 19
        if ($fieldDef instanceof FieldDefinition) {
246 19
            [$errors, $variableValues] = Values::getVariableValues(
247 19
                $this->context->getSchema(),
248 19
                $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

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

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