Passed
Pull Request — master (#314)
by Jakub
08:46 queued 02:41
created

QueryComplexity::setRawVariableValues()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
eloc 1
nc 1
nop 1
crap 2
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 123
    public function getVisitor(ValidationContext $context)
50
    {
51 123
        $this->context = $context;
52
53 123
        $this->variableDefs     = new ArrayObject();
54 123
        $this->fieldNodeAndDefs = new ArrayObject();
55 123
        $complexity             = 0;
56
57 123
        return $this->invokeIfNeeded(
58 123
            $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 123
                },
69
                NodeKind::VARIABLE_DEFINITION  => function ($def) {
70 6
                    $this->variableDefs[] = $def;
71
72 6
                    return Visitor::skipNode();
73 123
                },
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 123
                    },
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 16
        switch ($node->kind) {
114 16
            case NodeKind::FIELD:
115
                /** @var FieldNode $node */
116
                // default values
117 16
                $args         = [];
118 16
                $complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN;
119
120
                // calculate children complexity if needed
121 16
                $childrenComplexity = 0;
122
123
                // node has children?
124 16
                if (isset($node->selectionSet)) {
125 16
                    $childrenComplexity = $this->fieldComplexity($node);
126
                }
127
128 16
                $astFieldInfo = $this->astFieldInfo($node);
129 16
                $fieldDef     = $astFieldInfo[1];
130
131 16
                if ($fieldDef instanceof FieldDefinition) {
132 16
                    if ($this->directiveExcludesField($node)) {
133 3
                        break;
134
                    }
135
136 16
                    $args = $this->buildFieldArguments($node);
137
                    //get complexity fn using fieldDef complexity
138 16
                    if (method_exists($fieldDef, 'getComplexityFn')) {
139 16
                        $complexityFn = $fieldDef->getComplexityFn();
140
                    }
141
                }
142
143 16
                $complexity += call_user_func_array($complexityFn, [$childrenComplexity, $args]);
144 16
                break;
145
146 3
            case NodeKind::INLINE_FRAGMENT:
147
                /** @var InlineFragmentNode $node */
148
                // node has children?
149 1
                if (isset($node->selectionSet)) {
150 1
                    $complexity = $this->fieldComplexity($node, $complexity);
151
                }
152 1
                break;
153
154 2
            case NodeKind::FRAGMENT_SPREAD:
155
                /** @var FragmentSpreadNode $node */
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 16
        return $complexity;
165
    }
166
167 16
    private function astFieldInfo(FieldNode $field)
168
    {
169 16
        $fieldName    = $this->getFieldName($field);
170 16
        $astFieldInfo = [null, null];
171 16
        if (isset($this->fieldNodeAndDefs[$fieldName])) {
172 16
            foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) {
173 16
                if ($astAndDef[0] === $field) {
174 16
                    $astFieldInfo = $astAndDef;
175 16
                    break;
176
                }
177
            }
178
        }
179
180 16
        return $astFieldInfo;
181
    }
182
183 16
    private function directiveExcludesField(FieldNode $node)
184
    {
185 16
        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 3
                return ! $directiveArgsIf;
210
            }
211 3
            $directive       = Directive::skipDirective();
212 3
            $directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues);
213 3
            return $directiveArgsIf['if'];
214
        }
215 16
    }
216
217 16
    public function getRawVariableValues()
218
    {
219 16
        return $this->rawVariableValues;
220
    }
221
222
    /**
223
     * @param mixed[]|null $rawVariableValues
224
     */
225 105
    public function setRawVariableValues(?array $rawVariableValues = null)
226
    {
227 105
        $this->rawVariableValues = $rawVariableValues ?: [];
228 105
    }
229
230 16
    private function buildFieldArguments(FieldNode $node)
231
    {
232 16
        $rawVariableValues = $this->getRawVariableValues();
233 16
        $astFieldInfo      = $this->astFieldInfo($node);
234 16
        $fieldDef          = $astFieldInfo[1];
235
236 16
        $args = [];
237
238 16
        if ($fieldDef instanceof FieldDefinition) {
239 16
            [$errors, $variableValues] = Values::getVariableValues(
240 16
                $this->context->getSchema(),
241 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

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

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