Failed Conditions
Push — master ( e31947...cc39b3 )
by Šimon
11s
created

QueryComplexity::buildFieldArguments()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3.243

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 32
ccs 14
cts 20
cp 0.7
rs 9.6333
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3.243
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Validator\Rules;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Executor\Values;
9
use GraphQL\Language\AST\FieldNode;
10
use GraphQL\Language\AST\FragmentSpreadNode;
11
use GraphQL\Language\AST\InlineFragmentNode;
12
use GraphQL\Language\AST\Node;
13
use GraphQL\Language\AST\NodeKind;
14
use GraphQL\Language\AST\OperationDefinitionNode;
15
use GraphQL\Language\AST\SelectionSetNode;
16
use GraphQL\Language\Visitor;
17
use GraphQL\Type\Definition\Directive;
18
use GraphQL\Type\Definition\FieldDefinition;
19
use GraphQL\Validator\ValidationContext;
20
use function array_map;
21
use function call_user_func_array;
22
use function implode;
23
use function method_exists;
24
use function sprintf;
25
26
class QueryComplexity extends QuerySecurityRule
27
{
28
    /** @var int */
29
    private $maxQueryComplexity;
30
31
    /** @var mixed[]|null  */
32
    private $rawVariableValues = [];
33
34
    /** @var \ArrayObject */
35
    private $variableDefs;
36
37
    /** @var \ArrayObject */
38
    private $fieldNodeAndDefs;
39
40
    /** @var ValidationContext */
41
    private $context;
42
43 2
    public function __construct($maxQueryComplexity)
44
    {
45 2
        $this->setMaxQueryComplexity($maxQueryComplexity);
46 2
    }
47
48 118
    public function getVisitor(ValidationContext $context)
49
    {
50 118
        $this->context = $context;
51
52 118
        $this->variableDefs     = new \ArrayObject();
53 118
        $this->fieldNodeAndDefs = new \ArrayObject();
54 118
        $complexity             = 0;
55
56 118
        return $this->invokeIfNeeded(
57 118
            $context,
58
            [
59
                NodeKind::SELECTION_SET        => function (SelectionSetNode $selectionSet) use ($context) {
60 16
                    $this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs(
61 16
                        $context,
62 16
                        $context->getParentType(),
63 16
                        $selectionSet,
64 16
                        null,
65 16
                        $this->fieldNodeAndDefs
66
                    );
67 118
                },
68
                NodeKind::VARIABLE_DEFINITION  => function ($def) {
69 6
                    $this->variableDefs[] = $def;
70
71 6
                    return Visitor::skipNode();
72 118
                },
73
                NodeKind::OPERATION_DEFINITION => [
74
                    'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) {
75 16
                        $errors = $context->getErrors();
76
77 16
                        if (! empty($errors)) {
78 1
                            return;
79
                        }
80
81 16
                        $complexity = $this->fieldComplexity($operationDefinition, $complexity);
82
83 15
                        if ($complexity <= $this->getMaxQueryComplexity()) {
84 15
                            return;
85
                        }
86
87 13
                        $context->reportError(
88 13
                            new Error($this->maxQueryComplexityErrorMessage(
89 13
                                $this->getMaxQueryComplexity(),
90 13
                                $complexity
91
                            ))
92
                        );
93 118
                    },
94
                ],
95
            ]
96
        );
97
    }
98
99 16
    private function fieldComplexity($node, $complexity = 0)
100
    {
101 16
        if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSetNode) {
102 16
            foreach ($node->selectionSet->selections as $childNode) {
103 16
                $complexity = $this->nodeComplexity($childNode, $complexity);
104
            }
105
        }
106
107 16
        return $complexity;
108
    }
109
110 16
    private function nodeComplexity(Node $node, $complexity = 0)
111
    {
112 16
        switch ($node->kind) {
113 16
            case NodeKind::FIELD:
114
                /** @var FieldNode $node */
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 NodeKind::INLINE_FRAGMENT:
146
                /** @var InlineFragmentNode $node */
147
                // node has children?
148 1
                if (isset($node->selectionSet)) {
149 1
                    $complexity = $this->fieldComplexity($node, $complexity);
150
                }
151 1
                break;
152
153 2
            case NodeKind::FRAGMENT_SPREAD:
154
                /** @var FragmentSpreadNode $node */
155 2
                $fragment = $this->getFragment($node);
156
157 2
                if ($fragment !== null) {
158 2
                    $complexity = $this->fieldComplexity($fragment, $complexity);
159
                }
160 2
                break;
161
        }
162
163 16
        return $complexity;
164
    }
165
166 16
    private function astFieldInfo(FieldNode $field)
167
    {
168 16
        $fieldName    = $this->getFieldName($field);
169 16
        $astFieldInfo = [null, null];
170 16
        if (isset($this->fieldNodeAndDefs[$fieldName])) {
171 16
            foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) {
172 16
                if ($astAndDef[0] === $field) {
173 16
                    $astFieldInfo = $astAndDef;
174 16
                    break;
175
                }
176
            }
177
        }
178
179 16
        return $astFieldInfo;
180
    }
181
182 16
    private function directiveExcludesField(FieldNode $node)
183
    {
184 16
        foreach ($node->directives as $directiveNode) {
185 5
            if ($directiveNode->name->value === 'deprecated') {
186
                return false;
187
            }
188
189 5
            $variableValuesResult = 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()
193
            );
194
195 5
            if ($variableValuesResult['errors']) {
196
                throw new Error(implode(
197
                    "\n\n",
198
                    array_map(
199
                        function ($error) {
200
                            return $error->getMessage();
201
                        },
202
                        $variableValuesResult['errors']
203
                    )
204
                ));
205
            }
206 5
            $variableValues = $variableValuesResult['coerced'];
207
208 5
            if ($directiveNode->name->value === 'include') {
209 3
                $directive     = Directive::includeDirective();
210 3
                $directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);
211
212 3
                return ! $directiveArgs['if'];
213
            }
214
215 3
            $directive     = Directive::skipDirective();
216 3
            $directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);
217
218 3
            return $directiveArgs['if'];
219
        }
220 16
    }
221
222 16
    public function getRawVariableValues()
223
    {
224 16
        return $this->rawVariableValues;
225
    }
226
227
    /**
228
     * @param mixed[]|null $rawVariableValues
229
     */
230 100
    public function setRawVariableValues(?array $rawVariableValues = null)
231
    {
232 100
        $this->rawVariableValues = $rawVariableValues ?: [];
233 100
    }
234
235 16
    private function buildFieldArguments(FieldNode $node)
236
    {
237 16
        $rawVariableValues = $this->getRawVariableValues();
238 16
        $astFieldInfo      = $this->astFieldInfo($node);
239 16
        $fieldDef          = $astFieldInfo[1];
240
241 16
        $args = [];
242
243 16
        if ($fieldDef instanceof FieldDefinition) {
244 16
            $variableValuesResult = Values::getVariableValues(
245 16
                $this->context->getSchema(),
246 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

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