Issues (197)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Validation/JsonApi/QueryParser.php (18 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php declare (strict_types = 1);
2
3
namespace Limoncello\Flute\Validation\JsonApi;
4
5
/**
6
 * Copyright 2015-2019 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use Generator;
22
use Limoncello\Common\Reflection\ClassIsTrait;
23
use Limoncello\Contracts\L10n\FormatterFactoryInterface;
24
use Limoncello\Contracts\L10n\FormatterInterface;
25
use Limoncello\Flute\Contracts\Validation\ErrorCodes;
26
use Limoncello\Flute\Contracts\Validation\JsonApiQueryParserInterface;
27
use Limoncello\Flute\Contracts\Validation\JsonApiQueryRulesInterface;
28
use Limoncello\Flute\Contracts\Validation\JsonApiQueryRulesSerializerInterface;
29
use Limoncello\Flute\Exceptions\InvalidQueryParametersException;
30
use Limoncello\Flute\L10n\Messages;
31
use Limoncello\Flute\Validation\JsonApi\Execution\JsonApiErrorCollection;
32
use Limoncello\Validation\Contracts\Captures\CaptureAggregatorInterface;
33
use Limoncello\Validation\Contracts\Errors\ErrorAggregatorInterface;
34
use Limoncello\Validation\Contracts\Execution\ContextStorageInterface;
35
use Limoncello\Validation\Errors\Error as ValidationError;
36
use Limoncello\Validation\Execution\BlockInterpreter;
37
use Neomerx\JsonApi\Contracts\Schema\ErrorInterface;
38
use Neomerx\JsonApi\Exceptions\JsonApiException;
39
use Neomerx\JsonApi\Http\Query\BaseQueryParserTrait;
40
use Neomerx\JsonApi\Schema\Error as JsonApiError;
41
use function array_key_exists;
42
use function assert;
43
use function count;
44
use function is_array;
45
use function is_int;
46
use function is_string;
47
use function key;
48
use function reset;
49
use function strtolower;
50
use function trim;
51
52
/**
53
 * @package Limoncello\Flute
54
 *
55
 * @SuppressWarnings(PHPMD.TooManyFields)
56
 * @SuppressWarnings(PHPMD.TooManyMethods)
57
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
58
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
59
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
60
 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
61
 */
62
class QueryParser implements JsonApiQueryParserInterface
63
{
64
    use BaseQueryParserTrait {
65
        BaseQueryParserTrait::getFields as getFieldsImpl;
66
        BaseQueryParserTrait::getIncludes as getIncludesImpl;
67
        BaseQueryParserTrait::getSorts as getSortsImpl;
68
    }
69
    use ClassIsTrait;
70
71
    /** Message */
72
    public const MSG_ERR_INVALID_PARAMETER = 'Invalid Parameter.';
73
74
    /**
75
     * @var array
76
     */
77
    private $parameters;
78
79
    /**
80
     * @var string[]|null
81
     */
82
    private $messages;
83
84
    /**
85
     * @var null|string
86
     */
87
    private $identityParameter;
88
89
    /**
90
     * @var array
91
     */
92
    private $filterParameters;
93
94
    /**
95
     * @var bool
96
     */
97
    private $areFiltersWithAnd;
98
99
    /**
100
     * @var int|null
101
     */
102
    private $pagingOffset;
103
104
    /**
105
     * @var int|null
106
     */
107
    private $pagingLimit;
108
109
    /**
110
     * NOTE: Despite the type it is just a string so only static methods can be called from the interface.
111
     *
112
     * @var JsonApiQueryRulesSerializerInterface|string
113
     */
114
    private $serializerClass;
115
116
    /**
117
     * @var array
118
     */
119
    private $serializedRuleSet;
120
121
    /**
122
     * @var array
123
     */
124
    private $validationBlocks;
125
126
    /**
127
     * @var ContextStorageInterface
128
     */
129
    private $context;
130
131
    /**
132
     * @var CaptureAggregatorInterface
133
     */
134
    private $captures;
135
136
    /**
137
     * @var ErrorAggregatorInterface
138
     */
139
    private $validationErrors;
140
141
    /**
142
     * @var JsonApiErrorCollection
143
     */
144
    private $jsonErrors;
145
146
    /**
147
     * @var null|mixed
148
     */
149
    private $cachedIdentity = null;
150
151
    /**
152
     * @var null|array
153
     */
154
    private $cachedFilters = null;
155
156
    /**
157
     * @var null|array
158
     */
159
    private $cachedFields = null;
160
161
    /**
162
     * @var null|array
163
     */
164
    private $cachedSorts = null;
165
166
    /**
167
     * @var null|array
168
     */
169
    private $cachedIncludes = null;
170
171
    /**
172
     * @var FormatterFactoryInterface
173
     */
174
    private $formatterFactory;
175
176
    /**
177
     * @var FormatterInterface|null
178
     */
179 38
    private $formatter;
180
181
    /**
182
     * @param string                     $rulesClass
183
     * @param string                     $serializerClass
184
     * @param array                      $serializedData
185
     * @param ContextStorageInterface    $context
186
     * @param CaptureAggregatorInterface $captures
187
     * @param ErrorAggregatorInterface   $validationErrors
188
     * @param JsonApiErrorCollection     $jsonErrors
189
     * @param FormatterFactoryInterface  $formatterFactory
190 38
     * @param string[]|null              $messages
191 38
     */
192 38
    public function __construct(
193
        string $rulesClass,
194
        string $serializerClass,
195 38
        array $serializedData,
196 38
        ContextStorageInterface $context,
197 38
        CaptureAggregatorInterface $captures,
198 38
        ErrorAggregatorInterface $validationErrors,
199 38
        JsonApiErrorCollection $jsonErrors,
200 38
        FormatterFactoryInterface $formatterFactory,
201 38
        array $messages = null
202 38
    ) {
203
        assert(static::classImplements($rulesClass, JsonApiQueryRulesInterface::class));
204 38
        assert(static::classImplements($serializerClass, JsonApiQueryRulesSerializerInterface::class));
205 38
206 38
        $parameters = [];
207
        $this->setParameters($parameters)->setMessages($messages);
0 ignored issues
show
It seems like $messages defined by parameter $messages on line 201 can also be of type null; however, Limoncello\Flute\Validat...ryParser::setMessages() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
208 38
        $this->serializerClass  = $serializerClass;
209
        $this->context          = $context;
210
        $this->captures         = $captures;
211
        $this->validationErrors = $validationErrors;
212
        $this->jsonErrors       = $jsonErrors;
213
        $this->formatterFactory = $formatterFactory;
214 35
215
        assert($this->serializerClass::hasRules($rulesClass, $serializedData));
0 ignored issues
show
The method hasRules cannot be called on $this->serializerClass (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
216 35
        $this->serializedRuleSet = $this->serializerClass::readRules($rulesClass, $serializedData);
0 ignored issues
show
The method readRules cannot be called on $this->serializerClass (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
217
        $this->validationBlocks  = $this->serializerClass::readBlocks($serializedData);
0 ignored issues
show
The method readBlocks cannot be called on $this->serializerClass (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
218 35
219
        $this->clear();
220 35
    }
221
222 31
    /**
223
     * @inheritdoc
224
     */
225
    public function parse(?string $identity, array $parameters = []): JsonApiQueryParserInterface
226
    {
227
        $this->clear();
228 17
229
        $this->setIdentityParameter($identity)->setParameters($parameters);
230 17
231
        $this->parsePagingParameters()->parseFilterLink();
232
233
        return $this;
234
    }
235
236 1
    /**
237
     * @inheritdoc
238 1
     */
239
    public function areFiltersWithAnd(): bool
240
    {
241
        return $this->areFiltersWithAnd;
242
    }
243
244 14
    /**
245
     * @inheritdoc
246 14
     */
247
    public function hasFilters(): bool
248
    {
249
        return $this->hasParameter(static::PARAM_FILTER);
250
    }
251
252 14
    /**
253
     * @inheritdoc
254 14
     */
255
    public function hasFields(): bool
256
    {
257
        return $this->hasParameter(static::PARAM_FIELDS);
258
    }
259
260 1
    /**
261
     * @inheritdoc
262 1
     */
263
    public function hasIncludes(): bool
264
    {
265
        return $this->hasParameter(static::PARAM_INCLUDE);
266
    }
267
268 1
    /**
269
     * @inheritdoc
270 1
     */
271
    public function hasSorts(): bool
272
    {
273
        return $this->hasParameter(static::PARAM_SORT);
274
    }
275
276 5
    /**
277
     * @inheritdoc
278 5
     */
279 5
    public function hasPaging(): bool
280
    {
281
        return $this->hasParameter(static::PARAM_PAGE);
282 5
    }
283
284
    /**
285
     * @inheritdoc
286
     */
287
    public function getIdentity()
288 22
    {
289
        if ($this->cachedIdentity === null) {
290 22
            $this->cachedIdentity = $this->getValidatedIdentity();
291 22
        }
292
293
        return $this->cachedIdentity;
294 18
    }
295
296
    /**
297
     * @inheritdoc
298
     */
299
    public function getFilters(): array
300 3
    {
301
        if ($this->cachedFilters === null) {
302 3
            $this->cachedFilters = $this->iterableToArray($this->getValidatedFilters());
0 ignored issues
show
$this->getValidatedFilters() is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
303 3
        }
304 3
305
        return $this->cachedFilters;
306
    }
307 3
308
    /**
309
     * @inheritdoc
310
     */
311
    public function getFields(): array
312
    {
313 16
        if ($this->cachedFields === null) {
314
            $fields = $this->getFieldsImpl($this->getParameters(), $this->getInvalidParamMessage());
315 16
            $this->cachedFields = $this->iterableToArray($this->getValidatedFields($fields));
0 ignored issues
show
$fields is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
$this->getValidatedFields($fields) is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
316 16
        }
317 16
318
        return $this->cachedFields;
319
    }
320 16
321
    /**
322
     * @inheritdoc
323
     */
324
    public function getSorts(): array
325
    {
326 16
        if ($this->cachedSorts === null) {
327
            $sorts = $this->getSortsImpl($this->getParameters(), $this->getInvalidParamMessage());
328 16
            $this->cachedSorts = $this->iterableToArray($this->getValidatedSorts($sorts));
0 ignored issues
show
$sorts is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
$this->getValidatedSorts($sorts) is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
329 16
        }
330 16
331
        return $this->cachedSorts;
332
    }
333 16
334
    /**
335
     * @inheritdoc
336
     */
337
    public function getIncludes(): iterable
338
    {
339 17
        if ($this->cachedIncludes === null) {
340
            $includes = $this->getIncludesImpl($this->getParameters(), $this->getInvalidParamMessage());
341 17
            $this->cachedIncludes = $this->iterableToArray($this->getValidatedIncludes($includes));
0 ignored issues
show
$includes is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
$this->getValidatedIncludes($includes) is of type object<Generator>, but the function expects a object<Limoncello\Flute\...ation\JsonApi\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
342
        }
343
344
        return $this->cachedIncludes;
345
    }
346
347 17
    /**
348
     * @inheritdoc
349 17
     */
350
    public function getPagingOffset(): ?int
351
    {
352
        return $this->pagingOffset;
353
    }
354
355
    /**
356
     * @inheritdoc
357 38
     */
358
    public function getPagingLimit(): ?int
359 38
    {
360
        return $this->pagingLimit;
361 38
    }
362
363
    /**
364
     * @param array $parameters
365
     *
366
     * @return self
367 35
     */
368
    protected function setParameters(array $parameters): self
369 35
    {
370
        $this->parameters = $parameters;
371
372
        return $this;
373
    }
374
375
    /**
376
     * @return array
377 38
     */
378
    protected function getParameters(): array
379 38
    {
380
        return $this->parameters;
381 38
    }
382
383
    /**
384
     * @param array $messages
385
     *
386
     * @return self
387
     */
388
    protected function setMessages(?array $messages): self
389 25
    {
390
        $this->messages = $messages;
391 25
392
        return $this;
393 25
    }
394
395
    /**
396
     * @param string $message
397
     *
398
     * @return string
399 6
     */
400
    protected function getMessage(string $message): string
401 6
    {
402
        $hasTranslation = $this->messages !== null && array_key_exists($message, $this->messages) === false;
403
404
        return $hasTranslation === true ? $this->messages[$message] : $message;
405
    }
406
407 38
    /**
408
     * @return JsonApiErrorCollection
409 38
     */
410
    protected function getJsonErrors(): JsonApiErrorCollection
411
    {
412
        return $this->jsonErrors;
413
    }
414
415 38
    /**
416
     * @return ErrorAggregatorInterface
417 38
     */
418
    protected function getValidationErrors(): ErrorAggregatorInterface
419
    {
420
        return $this->validationErrors;
421
    }
422
423 34
    /**
424
     * @return CaptureAggregatorInterface
425 34
     */
426
    protected function getCaptures(): CaptureAggregatorInterface
427
    {
428
        return $this->captures;
429
    }
430
431 34
    /**
432
     * @return ContextStorageInterface
433 34
     */
434
    protected function getContext(): ContextStorageInterface
435
    {
436
        return $this->context;
437
    }
438
439 35
    /**
440
     * @return array
441 35
     */
442
    protected function getValidationBlocks(): array
443
    {
444
        return $this->validationBlocks;
445
    }
446
447 1
    /**
448
     * @return array
449 1
     */
450
    protected function getSerializedRuleSet(): array
451
    {
452
        return $this->serializedRuleSet;
453
    }
454
455 1
    /**
456
     * @return FormatterFactoryInterface
457 1
     */
458 1
    protected function getFormatterFactory(): FormatterFactoryInterface
459
    {
460
        return $this->formatterFactory;
461 1
    }
462
463
    /**
464
     * @return FormatterInterface
465
     */
466
    protected function getFormatter(): FormatterInterface
467
    {
468
        if ($this->formatter === null) {
469 17
            $this->formatter = $this->getFormatterFactory()->createFormatter(Messages::NAMESPACE_NAME);
470
        }
471 17
472
        return $this->formatter;
473
    }
474
475
    /**
476
     * @param string $name
477 5
     *
478
     * @return bool
479
     */
480 5
    protected function hasParameter(string $name): bool
481
    {
482 5
        return array_key_exists($name, $this->getParameters());
483 5
    }
484
485 5
    /**
486
     * @return mixed
487
     */
488 5
    private function getValidatedIdentity()
489 5
    {
490 5
        // without validation
491
        $result = $this->getIdentityParameter();
492 5
493
        $ruleIndexes = $this->serializerClass::readIdentityRuleIndexes($this->getSerializedRuleSet());
494
        if ($ruleIndexes !== null) {
495 5
            // with validation
496
            $ruleIndex = $this->serializerClass::readRuleMainIndex($ruleIndexes);
497
498
499
            $this->validationStarts(static::PARAM_IDENTITY, $ruleIndexes);
500
            $this->validateAndThrowOnError(static::PARAM_IDENTITY, $result, $ruleIndex);
501
            $this->validateEnds(static::PARAM_IDENTITY, $ruleIndexes);
502
503 22
            $result = $this->readSingleCapturedValue();
504
        }
505 22
506
        return $result;
507 22
    }
508
509 1
    /**
510 1
     * @return iterable
0 ignored issues
show
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
511
     *
512
     * @SuppressWarnings(PHPMD.ElseExpression)
513
     */
514 21
    private function getValidatedFilters(): iterable
515 21
    {
516 21
        $ruleIndexes = $this->serializerClass::readFilterRulesIndexes($this->getSerializedRuleSet());
517 13
518 13
        if ($ruleIndexes === null) {
519
            // without validation
520 1
            foreach ($this->getFilterParameters() as $field => $operationsWithArgs) {
521 1
                yield $field => $this->parseOperationsAndArguments(static::PARAM_FILTER, $operationsWithArgs);
522 1
            }
523
        } else {
524
            // with validation
525
            $mainIndexes = $this->serializerClass::readRuleMainIndexes($ruleIndexes);
526 12
            $this->validationStarts(static::PARAM_FILTER, $ruleIndexes);
527
            foreach ($this->getFilterParameters() as $field => $operationsWithArgs) {
528 2
                if (is_string($field) === false || empty($field) === true ||
529 2
                    is_array($operationsWithArgs) === false || empty($operationsWithArgs) === true
530 2
                ) {
531 2
                    throw new InvalidQueryParametersException($this->createParameterError(
532 2
                        static::PARAM_FILTER,
533 2
                        $this->getInvalidParamMessage()
534 2
                    ));
535
                }
536
537
                if (array_key_exists($field, $mainIndexes) === false) {
538
                    // unknown field set type
539 10
                    $this->getValidationErrors()->add(
540 10
                        new ValidationError(
541
                            static::PARAM_FILTER,
542 12
                            $field,
543
                            ErrorCodes::INVALID_VALUE,
544
                            Messages::INVALID_VALUE,
545 19
                            []
546
                        )
547
                    );
548
                } else {
549
                    // for field a validation rule is defined so input value will be validated
550
                    $ruleIndex = $mainIndexes[$field];
551
                    $parsed    = $this->parseOperationsAndArguments(static::PARAM_FILTER, $operationsWithArgs);
552
553
                    yield $field => $this->validateFilterArguments($ruleIndex, $parsed);
554
                }
555
            }
556
            $this->validateEnds(static::PARAM_FILTER, $ruleIndexes);
557 3
        }
558
    }
559 3
560
    /**
561 3
     * @param iterable $fieldsFromParent
562
     *
563 1
     * @return iterable
0 ignored issues
show
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
564 1
     *
565
     * @SuppressWarnings(PHPMD.StaticAccess)
566
     * @SuppressWarnings(PHPMD.ElseExpression)
567
     */
568 2
    private function getValidatedFields(iterable $fieldsFromParent): iterable
569 2
    {
570 2
        $ruleIndexes = $this->serializerClass::readFieldSetRulesIndexes($this->getSerializedRuleSet());
571 2
572 2
        if ($ruleIndexes === null) {
573
            // without validation
574
            foreach ($fieldsFromParent as $type => $fieldList) {
575 1
                yield $type => $fieldList;
576 1
            }
577 1
        } else {
578 1
            // with validation
579 1
            $mainIndexes = $this->serializerClass::readRuleMainIndexes($ruleIndexes);
580 1
            $this->validationStarts(static::PARAM_FIELDS, $ruleIndexes);
581 2
            foreach ($fieldsFromParent as $type => $fieldList) {
582
                if (array_key_exists($type, $mainIndexes) === true) {
583
                    yield $type => $this->validateValues($mainIndexes[$type], $fieldList);
584
                } else {
585
                    // unknown field set type
586 2
                    $this->getValidationErrors()->add(
587
                        new ValidationError(
588
                            static::PARAM_FIELDS,
589
                            $type,
590
                            ErrorCodes::INVALID_VALUE,
591
                            Messages::INVALID_VALUE,
592
                            []
593
                        )
594
                    );
595
                }
596
            }
597
            $this->validateEnds(static::PARAM_FIELDS, $ruleIndexes);
598 16
        }
599
    }
600 16
601
    /**
602 16
     * @param iterable $sortsFromParent
603
     *
604 1
     * @return iterable
0 ignored issues
show
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
605 1
     *
606
     * @SuppressWarnings(PHPMD.StaticAccess)
607
     * @SuppressWarnings(PHPMD.ElseExpression)
608
     */
609 15
    private function getValidatedSorts(iterable $sortsFromParent): iterable
610 15
    {
611 15
        $ruleIndexes = $this->serializerClass::readSortsRuleIndexes($this->getSerializedRuleSet());
612 5
613 5
        if ($ruleIndexes === null) {
614 5
            // without validation
615 5
            foreach ($sortsFromParent as $field => $isAsc) {
616
                yield $field => $isAsc;
617
            }
618 15
        } else {
619
            // with validation
620
            $ruleIndex = $this->serializerClass::readRuleMainIndex($ruleIndexes);
621
            $this->validationStarts(static::PARAM_SORT, $ruleIndexes);
622
            foreach ($sortsFromParent as $field => $isAsc) {
623
                $this->getCaptures()->clear();
624
                $this->validateAndAccumulateError($field, $ruleIndex);
625
                if ($this->getCaptures()->count() > 0) {
626
                    yield $this->readSingleCapturedValue() => $isAsc;
627
                }
628
            }
629
            $this->validateEnds(static::PARAM_SORT, $ruleIndexes);
630 16
        }
631
    }
632 16
633
    /**
634 16
     * @param iterable $includesFromParent
635
     *
636 1
     * @return iterable
0 ignored issues
show
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
637 1
     *
638
     * @SuppressWarnings(PHPMD.StaticAccess)
639
     * @SuppressWarnings(PHPMD.ElseExpression)
640
     */
641 15
    private function getValidatedIncludes(iterable $includesFromParent): iterable
642 15
    {
643 15
        $ruleIndexes = $this->serializerClass::readIncludesRuleIndexes($this->getSerializedRuleSet());
644 4
645 4
        if ($ruleIndexes === null) {
646 4
            // without validation
647 4
            foreach ($includesFromParent as $path => $split) {
648
                yield $path => $split;
649
            }
650 15
        } else {
651
            // with validation
652
            $ruleIndex = $this->serializerClass::readRuleMainIndex($ruleIndexes);
653
            $this->validationStarts(static::PARAM_INCLUDE, $ruleIndexes);
654
            foreach ($includesFromParent as $path => $split) {
655
                $this->getCaptures()->clear();
656
                $this->validateAndAccumulateError($path, $ruleIndex);
657
                if ($this->getCaptures()->count() > 0) {
658
                    yield $this->readSingleCapturedValue() => $split;
659 25
                }
660
            }
661 25
            $this->validateEnds(static::PARAM_INCLUDE, $ruleIndexes);
662
        }
663 25
    }
664 19
665
    /**
666
     * @param iterable $iterable
667 21
     *
668
     * @return array
669
     */
670
    private function iterableToArray(iterable $iterable): array
671
    {
672
        $result = [];
673
674
        foreach ($iterable as $key => $value) {
675 35
            $result[$key] = $value instanceof Generator ? $this->iterableToArray($value) : $value;
676
        }
677 35
678 35
        return $result;
679
    }
680 35
681
    /**
682
     * @param mixed $value
683
     *
684
     * @return int
685
     */
686
    private function validatePageOffset($value): int
687
    {
688 35
        $ruleIndexes    = $this->serializerClass::readPageOffsetRuleIndexes($this->getSerializedRuleSet());
689
        $validatedValue = $this->validatePaginationValue($value, $ruleIndexes);
690 35
691 35
        return $validatedValue;
692
    }
693 35
694
    /**
695
     * @param mixed $value
696
     *
697
     * @return int
698
     */
699
    private function validatePageLimit($value): int
700
    {
701
        $ruleIndexes    = $this->serializerClass::readPageLimitRuleIndexes($this->getSerializedRuleSet());
702 35
        $validatedValue = $this->validatePaginationValue($value, $ruleIndexes);
703
704
        return $validatedValue;
705 35
    }
706 1
707
    /**
708
     * @param mixed $value
709 34
     * @param array $ruleIndexes
710
     *
711 34
     * @return int
712 34
     */
713 34
    private function validatePaginationValue($value, ?array $ruleIndexes): int
714
    {
715 34
        // no validation rule means we should accept any input value
716
        if ($ruleIndexes === null) {
717 34
            return is_numeric($value) === true ? (int)$value : 0;
718
        }
719
720
        $ruleIndex = $this->serializerClass::readRuleMainIndex($ruleIndexes);
721
722
        $this->validationStarts(static::PARAM_PAGE, $ruleIndexes);
723
        $this->validateAndThrowOnError(static::PARAM_PAGE, $value, $ruleIndex);
724
        $this->validateEnds(static::PARAM_PAGE, $ruleIndexes);
725
726
        $validatedValue = $this->readSingleCapturedValue();
727
728 34
        return (int)$validatedValue;
729
    }
730 34
731 34
    /**
732
     * @param string $paramName
733 34
     * @param array  $ruleIndexes
734 34
     *
735 34
     * @return void
736 34
     *
737 34
     * @SuppressWarnings(PHPMD.StaticAccess)
738
     */
739 34
    private function validationStarts(string $paramName, array $ruleIndexes): void
740
    {
741
        $this->getCaptures()->clear();
742
        $this->getValidationErrors()->clear();
743
744
        BlockInterpreter::executeStarts(
745
            $this->serializerClass::readRuleStartIndexes($ruleIndexes),
746
            $this->getValidationBlocks(),
747
            $this->getContext(),
748
            $this->getValidationErrors()
749
        );
750
        $this->checkValidationQueueErrors($paramName);
751 34
    }
752
753 34
    /**
754 34
     * @param string $paramName
755 34
     * @param mixed  $value
756 34
     * @param int    $ruleIndex
757 34
     *
758 34
     * @return void
759 34
     *
760
     * @SuppressWarnings(PHPMD.StaticAccess)
761 34
     */
762
    private function validateAndThrowOnError(string $paramName, $value, int $ruleIndex): void
763
    {
764
        BlockInterpreter::executeBlock(
765
            $value,
766
            $ruleIndex,
767
            $this->getValidationBlocks(),
768
            $this->getContext(),
769
            $this->getCaptures(),
770
            $this->getValidationErrors()
771
        );
772 16
        $this->checkValidationQueueErrors($paramName);
773
    }
774 16
775 16
    /**
776 16
     * @param mixed $value
777 16
     * @param int   $ruleIndex
778 16
     *
779 16
     * @return bool
780 16
     *
781
     * @SuppressWarnings(PHPMD.StaticAccess)
782
     */
783
    private function validateAndAccumulateError($value, int $ruleIndex): bool
784
    {
785
        return BlockInterpreter::executeBlock(
786
            $value,
787
            $ruleIndex,
788
            $this->getValidationBlocks(),
789
            $this->getContext(),
790
            $this->getCaptures(),
791
            $this->getValidationErrors()
792 34
        );
793
    }
794 34
795 34
    /**
796 34
     * @param string $paramName
797 34
     * @param array  $ruleIndexes
798 34
     *
799
     * @return void
800 34
     *
801
     * @SuppressWarnings(PHPMD.StaticAccess)
802
     */
803
    private function validateEnds(string $paramName, array $ruleIndexes): void
804
    {
805
        BlockInterpreter::executeEnds(
806 34
            $this->serializerClass::readRuleEndIndexes($ruleIndexes),
807
            $this->getValidationBlocks(),
808 34
            $this->getContext(),
809 34
            $this->getValidationErrors()
810
        );
811 34
        $this->checkValidationQueueErrors($paramName);
812
    }
813
814
    /**
815
     * @return mixed
816
     */
817
    private function readSingleCapturedValue()
818
    {
819
        assert(count($this->getCaptures()->get()) === 1, 'Expected that only one value would be captured.');
820 10
        $value = current($this->getCaptures()->get());
821
822 10
        return $value;
823 9
    }
824 9
825 9
    /**
826 9
     * @param int      $ruleIndex
827
     * @param iterable $values
828
     *
829
     * @return iterable
0 ignored issues
show
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
830
     */
831
    private function validateValues(int $ruleIndex, iterable $values): iterable
832
    {
833
        foreach ($values as $key => $value) {
834
            $this->getCaptures()->clear();
835
            $this->validateAndAccumulateError($value, $ruleIndex);
836
            if ($this->getCaptures()->count() > 0) {
837 10
                yield $key => $this->readSingleCapturedValue();
838
            }
839 10
        }
840 9
    }
841
842
    /**
843
     * @param int      $ruleIndex
844
     * @param iterable $opsAndArgs
845
     *
846
     * @return iterable
0 ignored issues
show
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
847 35
     */
848
    private function validateFilterArguments(int $ruleIndex, iterable $opsAndArgs): iterable
849 35
    {
850 35
        foreach ($opsAndArgs as $operation => $arguments) {
851 35
            yield $operation => $this->validateValues($ruleIndex, $arguments);
852
        }
853 35
    }
854 35
855
    /**
856 35
     * @return self
857 35
     */
858
    private function parsePagingParameters(): self
859 35
    {
860
        $parameters    = $this->getParameters();
861
        $mightBeOffset = $parameters[static::PARAM_PAGE][static::PARAM_PAGING_OFFSET] ?? null;
862
        $mightBeLimit  = $parameters[static::PARAM_PAGE][static::PARAM_PAGING_LIMIT] ?? null;
863
864
        $this->pagingOffset = $this->validatePageOffset($mightBeOffset);
865
        $this->pagingLimit  = $this->validatePageLimit($mightBeLimit);
866
867
        assert(is_int($this->pagingOffset) === true && $this->pagingOffset >= 0);
868
        assert(is_int($this->pagingLimit) === true && $this->pagingLimit > 0);
869 34
870
        return $this;
871 34
    }
872 6
873 6
    /**
874
     * @param string $paramName
875
     *
876 6
     * @return void
877
     *
878
     * @throws JsonApiException
879
     */
880
    private function checkValidationQueueErrors(string $paramName): void
881
    {
882
        if ($this->getValidationErrors()->count() > 0) {
883
            foreach ($this->getValidationErrors()->get() as $error) {
884
                $this->getJsonErrors()->addValidationQueryError($paramName, $error);
885 35
            }
886
887 35
            throw new JsonApiException($this->getJsonErrors());
888
        }
889 35
    }
890
891
    /**
892
     * @param string|null $value
893
     *
894
     * @return self
895 5
     */
896
    private function setIdentityParameter(?string $value): self
897 5
    {
898
        $this->identityParameter = $value;
899
900
        return $this;
901
    }
902
903
    /**
904
     * @return null|string
905 31
     */
906
    private function getIdentityParameter(): ?string
907 31
    {
908
        return $this->identityParameter;
909 31
    }
910
911
    /**
912
     * @param array $values
913
     *
914
     * @return self
915 22
     */
916
    private function setFilterParameters(array $values): self
917 22
    {
918
        $this->filterParameters = $values;
919
920
        return $this;
921
    }
922
923 29
    /**
924
     * @return array
925 29
     */
926
    private function getFilterParameters(): array
927 29
    {
928
        return $this->filterParameters;
929
    }
930
931
    /**
932
     * @return self
933 2
     */
934
    private function setFiltersWithAnd(): self
935 2
    {
936
        $this->areFiltersWithAnd = true;
937 2
938
        return $this;
939
    }
940
941
    /**
942
     * @return self
943 38
     */
944
    private function setFiltersWithOr(): self
945 38
    {
946 38
        $this->areFiltersWithAnd = false;
947 38
948 38
        return $this;
949 38
    }
950
951 38
    /**
952 38
     * @return self
953 38
     */
954 38
    private function clear(): self
955 38
    {
956
        $this->identityParameter = null;
957 38
        $this->filterParameters  = [];
958 38
        $this->areFiltersWithAnd = true;
959
        $this->pagingOffset      = null;
960 38
        $this->pagingLimit       = null;
961
962
        $this->cachedIdentity = null;
963
        $this->cachedFilters  = null;
964
        $this->cachedFields   = null;
965
        $this->cachedIncludes = null;
966
        $this->cachedSorts    = null;
967
968
        $this->getCaptures()->clear();
969
        $this->getValidationErrors()->clear();
970
971 35
        return $this;
972
    }
973 35
974 17
    /**
975
     * Pre-parsing for filter parameters.
976 17
     *
977
     * @return self
978
     *
979 18
     * @SuppressWarnings(PHPMD.ElseExpression)
980 18
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
981 2
     */
982 2
    private function parseFilterLink(): self
983 2
    {
984
        if (array_key_exists(static::PARAM_FILTER, $this->getParameters()) === false) {
985
            $this->setFiltersWithAnd()->setFilterParameters([]);
986
987 16
            return $this;
988 16
        }
989
990
        $filterSection = $this->getParameters()[static::PARAM_FILTER];
991 16
        if (is_array($filterSection) === false || empty($filterSection) === true) {
992 16
            throw new InvalidQueryParametersException($this->createParameterError(
993 16
                static::PARAM_FILTER,
994 4
                $this->getInvalidParamMessage()
995 4
            ));
996 4
        }
997
998 2
        $isWithAnd = true;
999 2
        reset($filterSection);
1000 2
1001
        // check if top level element is `AND` or `OR`
1002
        $firstKey   = key($filterSection);
1003 2
        $firstLcKey = strtolower(trim($firstKey));
1004 2
        if (($hasOr = ($firstLcKey === 'or')) || $firstLcKey === 'and') {
1005 2
            if (count($filterSection) > 1 ||
1006
                empty($filterSection = $filterSection[$firstKey]) === true ||
1007
                is_array($filterSection) === false
1008
            ) {
1009 12
                throw new InvalidQueryParametersException($this->createParameterError(
1010
                    static::PARAM_FILTER,
1011
                    $this->getInvalidParamMessage()
1012 14
                ));
1013
            } else {
1014 14
                $this->setFilterParameters($filterSection);
1015
                if ($hasOr === true) {
1016
                    $isWithAnd = false;
1017
                }
1018
            }
1019
        } else {
1020
            $this->setFilterParameters($filterSection);
1021
        }
1022
1023
        $isWithAnd === true ? $this->setFiltersWithAnd() : $this->setFiltersWithOr();
1024
1025 11
        return $this;
1026
    }
1027
1028 11
    /**
1029 11
     * @param string $parameterName
1030 11
     * @param array  $value
1031
     *
1032 1
     * @return iterable
0 ignored issues
show
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1033 1
     *
1034 1
     * @SuppressWarnings(PHPMD.ElseExpression)
1035
     */
1036
    private function parseOperationsAndArguments(string $parameterName, array $value): iterable
1037 10
    {
1038 1
        // in this case we interpret it as an [operation => 'comma separated argument(s)']
1039
        foreach ($value as $operationName => $arguments) {
1040 9
            if (is_string($operationName) === false || empty($operationName) === true ||
1041 9
                is_string($arguments) === false
1042 9
            ) {
1043 10
                $title  = $this->getFormatter()->formatMessage(Messages::INVALID_OPERATION_ARGUMENTS);
1044
                $error  = $this->createQueryError($parameterName, $title);
1045
                throw new InvalidQueryParametersException($error);
1046
            }
1047
1048
            if ($arguments === '') {
1049
                yield $operationName => [];
1050
            } else {
1051
                yield $operationName => $this->splitCommaSeparatedStringAndCheckNoEmpties(
1052
                    $parameterName,
1053
                    $arguments,
1054
                    $this->getInvalidParamMessage()
1055 1
                );
1056
            }
1057 1
        }
1058 1
    }
1059
1060 1
    /**
1061
     * @param string $paramName
1062
     * @param string $errorTitle
1063
     *
1064
     * @return ErrorInterface
1065
     */
1066
    private function createQueryError(string $paramName, string $errorTitle): ErrorInterface
1067 25
    {
1068
        $source = [ErrorInterface::SOURCE_PARAMETER => $paramName];
1069 25
        $error  = new JsonApiError(null, null, null, null, null, $errorTitle, null, $source);
1070
1071
        return $error;
1072
    }
1073
1074
    /**
1075
     *
1076
     * @return string
1077
     */
1078
    private function getInvalidParamMessage(): string
1079
    {
1080
        return $this->getMessage(static::MSG_ERR_INVALID_PARAMETER);
1081
    }
1082
}
1083