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/DataParser.php (1 issue)

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 Limoncello\Common\Reflection\ClassIsTrait;
22
use Limoncello\Contracts\L10n\FormatterFactoryInterface;
23
use Limoncello\Contracts\L10n\FormatterInterface;
24
use Limoncello\Flute\Contracts\Validation\JsonApiDataParserInterface;
25
use Limoncello\Flute\Contracts\Validation\JsonApiDataRulesSerializerInterface;
26
use Limoncello\Flute\Http\JsonApiResponse;
27
use Limoncello\Flute\L10n\Messages;
28
use Limoncello\Flute\Validation\JsonApi\Execution\JsonApiErrorCollection;
29
use Limoncello\Flute\Validation\Rules\RelationshipRulesTrait;
30
use Limoncello\Validation\Captures\CaptureAggregator;
31
use Limoncello\Validation\Contracts\Captures\CaptureAggregatorInterface;
32
use Limoncello\Validation\Contracts\Errors\ErrorAggregatorInterface;
33
use Limoncello\Validation\Contracts\Errors\ErrorInterface;
34
use Limoncello\Validation\Contracts\Execution\ContextStorageInterface;
35
use Limoncello\Validation\Errors\ErrorAggregator;
36
use Limoncello\Validation\Execution\BlockInterpreter;
37
use Neomerx\JsonApi\Contracts\Schema\DocumentInterface as DI;
38
use Neomerx\JsonApi\Exceptions\JsonApiException;
39
use function array_key_exists;
40
use function array_merge;
41
use function assert;
42
use function count;
43
use function is_array;
44
use function is_int;
45
use function is_scalar;
46
47
/**
48
 * @package Limoncello\Flute
49
 *
50
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
51
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
52
 */
53
class DataParser implements JsonApiDataParserInterface
54
{
55
    use RelationshipRulesTrait, ClassIsTrait;
56
57
    /** Rule description index */
58
    const RULE_INDEX = 0;
59
60
    /** Rule description index */
61
    const RULE_ATTRIBUTES = self::RULE_INDEX + 1;
62
63
    /** Rule description index */
64
    const RULE_TO_ONE = self::RULE_ATTRIBUTES + 1;
65
66
    /** Rule description index */
67
    const RULE_TO_MANY = self::RULE_TO_ONE + 1;
68
69
    /** Rule description index */
70
    const RULE_UNLISTED_ATTRIBUTE = self::RULE_TO_MANY + 1;
71
72
    /** Rule description index */
73
    const RULE_UNLISTED_RELATIONSHIP = self::RULE_UNLISTED_ATTRIBUTE + 1;
74
75
    /**
76
     * NOTE: Despite the type it is just a string so only static methods can be called from the interface.
77
     *
78
     * @var JsonApiDataRulesSerializerInterface|string
79
     */
80
    private $serializerClass;
81
82
    /**
83
     * @var int|null
84
     */
85
    private $errorStatus;
86
87
    /**
88
     * @var ContextStorageInterface
89
     */
90
    private $context;
91
92
    /**
93
     * @var JsonApiErrorCollection
94
     */
95
    private $jsonApiErrors;
96
97
    /**
98
     * @var array
99
     */
100
    private $blocks;
101
102
    /**
103
     * @var array
104
     */
105
    private $idRule;
106
107
    /**
108
     * @var array
109
     */
110
    private $typeRule;
111
112
    /**
113
     * @var int[]
114
     */
115
    private $attributeRules;
116
117
    /**
118
     * @var int[]
119
     */
120
    private $toOneRules;
121
122
    /**
123
     * @var int[]
124
     */
125
    private $toManyRules;
126
127
    /**
128
     * @var bool
129
     */
130
    private $isIgnoreUnknowns;
131
132
    /**
133
     * @var FormatterInterface|null
134
     */
135
    private $formatter;
136
137
    /**
138
     * @var FormatterFactoryInterface
139
     */
140
    private $formatterFactory;
141
142
    /**
143
     * @var ErrorAggregatorInterface
144
     */
145
    private $errorAggregator;
146
147
    /**
148
     * @var CaptureAggregatorInterface
149
     */
150
    private $captureAggregator;
151
152
    /**
153
     * @param string                    $rulesClass
154 30
     * @param string                    $serializerClass
155
     * @param array                     $serializedData
156
     * @param ContextStorageInterface   $context
157
     * @param JsonApiErrorCollection    $jsonErrors
158
     * @param FormatterFactoryInterface $formatterFactory
159
     *
160
     * @SuppressWarnings(PHPMD.StaticAccess)
161
     */
162
    public function __construct(
163 30
        string $rulesClass,
164 30
        string $serializerClass,
165 30
        array $serializedData,
166 30
        ContextStorageInterface $context,
167
        JsonApiErrorCollection $jsonErrors,
168 30
        FormatterFactoryInterface $formatterFactory
169 30
    ) {
170 30
        $this
171 30
            ->setSerializerClass($serializerClass)
172 30
            ->setContext($context)
173
            ->setJsonApiErrors($jsonErrors)
174
            ->setFormatterFactory($formatterFactory);
175 30
176 30
        $this->blocks      = $this->getSerializer()::readBlocks($serializedData);
177 30
        $ruleSet           = $this->getSerializer()::readRules($rulesClass, $serializedData);
178 30
        $this->idRule      = $this->getSerializer()::readIdRuleIndexes($ruleSet);
179
        $this->typeRule    = $this->getSerializer()::readTypeRuleIndexes($ruleSet);
180 30
        $this->errorStatus = null;
181 30
182
        $this
183
            ->setAttributeRules($this->getSerializer()::readAttributeRulesIndexes($ruleSet))
184
            ->setToOneIndexes($this->getSerializer()::readToOneRulesIndexes($ruleSet))
185
            ->setToManyIndexes($this->getSerializer()::readToManyRulesIndexes($ruleSet))
186
            ->disableIgnoreUnknowns();
187 13
188
        $this->errorAggregator   = new ErrorAggregator();
189 13
        $this->captureAggregator = new CaptureAggregator();
190 5
    }
191
192
    /**
193 8
     * @inheritdoc
194
     */
195
    public function assert(array $jsonData): JsonApiDataParserInterface
196
    {
197
        if ($this->parse($jsonData) === false) {
198
            throw new JsonApiException($this->getJsonApiErrorCollection(), $this->getErrorStatus());
199
        }
200
201 17
        return $this;
202
    }
203 17
204
    /**
205
     * @inheritdoc
206 17
     *
207 17
     * @SuppressWarnings(PHPMD.ElseExpression)
208 17
     */
209 17
    public function parse(array $input): bool
210
    {
211 17
        $this->resetAggregators();
212
213 17
        $this
214
            ->validateType($input)
215
            ->validateId($input)
216
            ->validateAttributes($input)
217
            ->validateRelationships($input);
218
219
        $hasNoErrors = $this->getJsonApiErrorCollection()->count() <= 0;
220
221 9
        return $hasNoErrors;
222
    }
223 9
224
    /**
225 9
     * @inheritdoc
226 9
     *
227 9
     * @SuppressWarnings(PHPMD.ElseExpression)
228
     */
229 9
    public function parseRelationship(string $index, string $name, array $jsonData): bool
230 1
    {
231 1
        $this->resetAggregators();
232 1
233 1
        $isFoundInToOne  = array_key_exists($name, $this->getSerializer()::readRulesIndexes($this->getToOneRules()));
234 1
        $isFoundInToMany = $isFoundInToOne === false &&
235
            array_key_exists($name, $this->getSerializer()::readRulesIndexes($this->getToManyRules()));
236 8
237 8
        if ($isFoundInToOne === false && $isFoundInToMany === false) {
238 8
            $title   = $this->formatMessage(Messages::INVALID_VALUE);
239 8
            $details = $this->formatMessage(Messages::UNKNOWN_RELATIONSHIP);
240
            $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
241
            $this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, (string)$status);
242
            $this->addErrorStatus($status);
243 8
        } else {
244 8
            assert($isFoundInToOne xor $isFoundInToMany);
245 8
            $ruleIndexes = $this->getSerializer()::readSingleRuleIndexes(
246 3
                $isFoundInToOne === true ? $this->getToOneRules() : $this->getToManyRules(),
247 5
                $name
248 8
            );
249
250 8
            // now execute validation rules
251 3
            $this->executeStarts($this->getSerializer()::readRuleStartIndexes($ruleIndexes));
252 3
            $ruleIndex = $this->getSerializer()::readRuleIndex($ruleIndexes);
253 3
            $isFoundInToOne === true ?
254 3
                $this->validateAsToOneRelationship($ruleIndex, $name, $jsonData) :
255
                $this->validateAsToManyRelationship($ruleIndex, $name, $jsonData);
256 3
            $this->executeEnds($this->getSerializer()::readRuleEndIndexes($ruleIndexes));
257
258
            if (count($this->getErrorAggregator()) > 0) {
259
                $status  = JsonApiResponse::HTTP_CONFLICT;
260 9
                foreach ($this->getErrorAggregator()->get() as $error) {
261
                    $this->getJsonApiErrorCollection()->addValidationRelationshipError($error, $status);
262 9
                    $this->addErrorStatus($status);
263
                }
264
                $this->getErrorAggregator()->clear();
265
            }
266
        }
267
268 9
        $hasNoErrors = count($this->getJsonApiErrorCollection()) <= 0;
269
270
        return $hasNoErrors;
271
    }
272
273 9
    /**
274 4
     * @inheritdoc
275 4
     */
276
    public function assertRelationship(
277
        string $index,
278 5
        string $name,
279
        array $jsonData
280
    ): JsonApiDataParserInterface {
281
        if ($this->parseRelationship($index, $name, $jsonData) === false) {
282
            $status = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
283
            throw new JsonApiException($this->getJsonApiErrorCollection(), $status);
284 7
        }
285
286 7
        return $this;
287
    }
288
289
    /**
290
     * @inheritdoc
291
     */
292 16
    public function getErrors(): array
293
    {
294 16
        return $this->getJsonApiErrorCollection()->getArrayCopy();
295
    }
296
297
    /**
298
     * @inheritdoc
299
     */
300 26
    public function getCaptures(): array
301
    {
302 26
        return $this->getCaptureAggregator()->get();
303 26
    }
304 26
305
    /**
306 26
     * @return self
307
     */
308
    protected function resetAggregators(): self
309
    {
310
        $this->getCaptureAggregator()->clear();
311
        $this->getErrorAggregator()->clear();
312
        $this->getContext()->clear();
313
314 30
        return $this;
315
    }
316 30
317 30
    /**
318 30
     * @param string $serializerClass
319
     *
320
     * @return self
321 30
     */
322
    protected function setSerializerClass(string $serializerClass): self
323 30
    {
324
        assert(static::classImplements($serializerClass, JsonApiDataRulesSerializerInterface::class));
325
326
        $this->serializerClass = $serializerClass;
327
328
        return $this;
329 30
    }
330
331 30
    /**
332
     * @return JsonApiDataRulesSerializerInterface|string
333
     */
334
    protected function getSerializer()
335
    {
336
        return $this->serializerClass;
337
    }
338
339
    /**
340
     * @param array $jsonData
341
     *
342 17
     * @return self
343
     *
344
     * @SuppressWarnings(PHPMD.StaticAccess)
345 17
     * @SuppressWarnings(PHPMD.ElseExpression)
346
     */
347 17
    private function validateType(array $jsonData): self
348 17
    {
349
        // execute start(s)
350
        $this->executeStarts($this->getSerializer()::readRuleStartIndexes($this->getTypeRule()));
351 16
352 16
        if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true &&
353
            array_key_exists(DI::KEYWORD_TYPE, $data = $jsonData[DI::KEYWORD_DATA]) === true
354 1
        ) {
355 1
            // execute main validation block(s)
356 1
            $index = $this->getSerializer()::readRuleIndex($this->getTypeRule());
357 1
            $this->executeBlock($data[DI::KEYWORD_TYPE], $index);
358 1
        } else {
359
            $title   = $this->formatMessage(Messages::INVALID_VALUE);
360
            $details = $this->formatMessage(Messages::TYPE_MISSING);
361
            $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
362 17
            $this->getJsonApiErrorCollection()->addDataTypeError($title, $details, (string)$status);
363
            $this->addErrorStatus($status);
364 17
        }
365 1
366 1
        // execute end(s)
367 1
        $this->executeEnds($this->getSerializer()::readRuleEndIndexes($this->getTypeRule()));
368 1
369 1
        if (count($this->getErrorAggregator()) > 0) {
370
            $title  = $this->formatMessage(Messages::INVALID_VALUE);
371 1
            $status = JsonApiResponse::HTTP_CONFLICT;
372 1
            foreach ($this->getErrorAggregator()->get() as $error) {
373
                $details = $this->getMessage($error);
374
                $this->getJsonApiErrorCollection()->addDataTypeError($title, $details, (string)$status);
375 17
            }
376
            $this->addErrorStatus($status);
377
            $this->getErrorAggregator()->clear();
378
        }
379
380
        return $this;
381
    }
382
383
    /**
384
     * @param array $jsonData
385 17
     *
386
     * @return self
387
     *
388 17
     * @SuppressWarnings(PHPMD.StaticAccess)
389
     */
390
    private function validateId(array $jsonData): self
391 17
    {
392 17
        // execute start(s)
393
        $this->executeStarts($this->getSerializer()::readRuleStartIndexes($this->getIdRule()));
394 14
395 14
        // execute main validation block(s)
396
        if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true &&
397
            array_key_exists(DI::KEYWORD_ID, $data = $jsonData[DI::KEYWORD_DATA]) === true
398
        ) {
399 17
            $index = $this->getSerializer()::readRuleIndex($this->getIdRule());
400
            $this->executeBlock($data[DI::KEYWORD_ID], $index);
401 17
        }
402 3
403 3
        // execute end(s)
404 3
        $this->executeEnds($this->getSerializer()::readRuleEndIndexes($this->getIdRule()));
405 3
406 3
        if (count($this->getErrorAggregator()) > 0) {
407
            $title  = $this->formatMessage(Messages::INVALID_VALUE);
408 3
            $status = JsonApiResponse::HTTP_CONFLICT;
409 3
            foreach ($this->getErrorAggregator()->get() as $error) {
410
                $details = $this->getMessage($error);
411
                $this->getJsonApiErrorCollection()->addDataIdError($title, $details, (string)$status);
412 17
            }
413
            $this->addErrorStatus($status);
414
            $this->getErrorAggregator()->clear();
415
        }
416
417
        return $this;
418
    }
419
420
    /**
421
     * @param array $jsonData
422
     *
423 17
     * @return self
424
     *
425
     * @SuppressWarnings(PHPMD.StaticAccess)
426 17
     * @SuppressWarnings(PHPMD.ElseExpression)
427
     */
428 17
    private function validateAttributes(array $jsonData): self
429 17
    {
430
        // execute start(s)
431 15
        $this->executeStarts($this->getSerializer()::readRulesStartIndexes($this->getAttributeRules()));
432 2
433 2
        if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true &&
434 2
            array_key_exists(DI::KEYWORD_ATTRIBUTES, $data = $jsonData[DI::KEYWORD_DATA]) === true
435 2
        ) {
436 2
            if (is_array($attributes = $data[DI::KEYWORD_ATTRIBUTES]) === false) {
437
                $title   = $this->formatMessage(Messages::INVALID_VALUE);
438
                $details = $this->formatMessage(Messages::INVALID_ATTRIBUTES);
439 13
                $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
440 10
                $this->getJsonApiErrorCollection()->addAttributesError($title, $details, (string)$status);
441 9
                $this->addErrorStatus($status);
442 1
            } else {
443 1
                // execute main validation block(s)
444 1
                foreach ($attributes as $name => $value) {
445 1
                    if (($index = $this->getAttributeIndex($name)) !== null) {
446 1
                        $this->executeBlock($value, $index);
447 1
                    } elseif ($this->isIgnoreUnknowns() === false) {
448 10
                        $title   = $this->formatMessage(Messages::INVALID_VALUE);
449
                        $details = $this->formatMessage(Messages::UNKNOWN_ATTRIBUTE);
450
                        $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
451
                        $this->getJsonApiErrorCollection()
452
                            ->addDataAttributeError($name, $title, $details, (string)$status);
453
                        $this->addErrorStatus($status);
454
                    }
455 17
                }
456
            }
457 17
        }
458 2
459 2
        // execute end(s)
460 2
        $this->executeEnds($this->getSerializer()::readRulesEndIndexes($this->getAttributeRules()));
461
462 2
        if (count($this->getErrorAggregator()) > 0) {
463 2
            $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
464
            foreach ($this->getErrorAggregator()->get() as $error) {
465
                $this->getJsonApiErrorCollection()->addValidationAttributeError($error, $status);
466 17
            }
467
            $this->addErrorStatus($status);
468
            $this->getErrorAggregator()->clear();
469
        }
470
471
        return $this;
472
    }
473
474
    /**
475
     * @param array $jsonData
476
     *
477 17
     * @return self
478
     *
479
     * @SuppressWarnings(PHPMD.StaticAccess)
480 17
     * @SuppressWarnings(PHPMD.ElseExpression)
481 17
     */
482 17
    private function validateRelationships(array $jsonData): self
483
    {
484
        // execute start(s)
485 17
        $this->executeStarts(array_merge(
486 17
            $this->getSerializer()::readRulesStartIndexes($this->getToOneRules()),
487
            $this->getSerializer()::readRulesStartIndexes($this->getToManyRules())
488 10
        ));
489 1
490 1
        if (array_key_exists(DI::KEYWORD_DATA, $jsonData) === true &&
491 1
            array_key_exists(DI::KEYWORD_RELATIONSHIPS, $data = $jsonData[DI::KEYWORD_DATA]) === true
492 1
        ) {
493 1
            if (is_array($relationships = $data[DI::KEYWORD_RELATIONSHIPS]) === false) {
494
                $title   = $this->formatMessage(Messages::INVALID_VALUE);
495
                $details = $this->formatMessage(Messages::INVALID_RELATIONSHIP_TYPE);
496 9
                $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
497 9
                $this->getJsonApiErrorCollection()->addRelationshipsError($title, $details, (string)$status);
498
                $this->addErrorStatus($status);
499 9
            } else {
500 9
                // ok we got to something that could be null or a valid relationship
501
                $toOneIndexes  = $this->getSerializer()::readRulesIndexes($this->getToOneRules());
502 8
                $toManyIndexes = $this->getSerializer()::readRulesIndexes($this->getToManyRules());
503 9
504
                foreach ($relationships as $name => $relationship) {
505 8
                    if (array_key_exists($name, $toOneIndexes) === true) {
506
                        // it might be to1 relationship
507
                        $this->validateAsToOneRelationship($toOneIndexes[$name], $name, $relationship);
508 1
                    } elseif (array_key_exists($name, $toManyIndexes) === true) {
509 1
                        // it might be toMany relationship
510 1
                        $this->validateAsToManyRelationship($toManyIndexes[$name], $name, $relationship);
511 1
                    } else {
512 1
                        // unknown relationship
513 9
                        $title   = $this->formatMessage(Messages::INVALID_VALUE);
514
                        $details = $this->formatMessage(Messages::UNKNOWN_RELATIONSHIP);
515
                        $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
516
                        $this->getJsonApiErrorCollection()
517
                            ->addRelationshipError($name, $title, $details, (string)$status);
518
                        $this->addErrorStatus($status);
519
                    }
520 17
                }
521 17
            }
522 17
        }
523
524
        // execute end(s)
525 17
        $this->executeEnds(array_merge(
526 3
            $this->getSerializer()::readRulesEndIndexes($this->getToOneRules()),
527 3
            $this->getSerializer()::readRulesEndIndexes($this->getToManyRules())
528 3
        ));
529
530 3
        if (count($this->getErrorAggregator()) > 0) {
531 3
            $status  = JsonApiResponse::HTTP_CONFLICT;
532
            foreach ($this->getErrorAggregator()->get() as $error) {
533
                $this->getJsonApiErrorCollection()->addValidationRelationshipError($error, $status);
534 17
            }
535
            $this->addErrorStatus($status);
536
            $this->getErrorAggregator()->clear();
537
        }
538
539
        return $this;
540
    }
541
542
    /**
543
     * @param int    $index
544
     * @param string $name
545
     * @param mixed  $mightBeRelationship
546 11
     *
547
     * @return void
548 11
     *
549 11
     * @SuppressWarnings(PHPMD.ElseExpression)
550 11
     */
551
    private function validateAsToOneRelationship(int $index, string $name, $mightBeRelationship): void
552
    {
553 9
        if (is_array($mightBeRelationship) === true &&
554
            array_key_exists(DI::KEYWORD_DATA, $mightBeRelationship) === true &&
555 2
            ($parsed = $this->parseSingleRelationship($mightBeRelationship[DI::KEYWORD_DATA])) !== false
556 2
        ) {
557 2
            // All right we got something. Now pass it to a validation rule.
558 2
            $this->executeBlock($parsed, $index);
559 2
        } else {
560
            $title   = $this->formatMessage(Messages::INVALID_VALUE);
561
            $details = $this->formatMessage(Messages::INVALID_RELATIONSHIP);
562
            $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
563
            $this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, (string)$status);
564
            $this->addErrorStatus($status);
565
        }
566
    }
567
568
    /**
569
     * @param int    $index
570
     * @param string $name
571
     * @param mixed  $mightBeRelationship
572 13
     *
573
     * @return void
574 13
     *
575 13
     * @SuppressWarnings(PHPMD.ElseExpression)
576 13
     */
577 13
    private function validateAsToManyRelationship(int $index, string $name, $mightBeRelationship): void
578 13
    {
579
        $isParsed       = true;
580 11
        $collectedPairs = [];
581
        if (is_array($mightBeRelationship) === true &&
582 10
            array_key_exists(DI::KEYWORD_DATA, $mightBeRelationship) === true &&
583 9
            is_array($data = $mightBeRelationship[DI::KEYWORD_DATA]) === true
584
        ) {
585 1
            foreach ($data as $mightTypeAndId) {
586 11
                // we accept only pairs of type and id (no `null`s are accepted).
587
                if (is_array($parsed = $this->parseSingleRelationship($mightTypeAndId)) === true) {
588
                    $collectedPairs[] = $parsed;
589
                } else {
590 2
                    $isParsed = false;
591
                    break;
592
                }
593 13
            }
594
        } else {
595 10
            $isParsed = false;
596
        }
597 3
598 3
        if ($isParsed === true) {
599 3
            // All right we got something. Now pass it to a validation rule.
600 3
            $this->executeBlock($collectedPairs, $index);
601 3
        } else {
602
            $title   = $this->formatMessage(Messages::INVALID_VALUE);
603
            $details = $this->formatMessage(Messages::INVALID_RELATIONSHIP);
604
            $status  = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY;
605
            $this->getJsonApiErrorCollection()->addRelationshipError($name, $title, $details, (string)$status);
606
            $this->addErrorStatus($status);
607
        }
608
    }
609
610
    /**
611
     * @param mixed $data
612 15
     *
613
     * @return array|null|false Either `array` ($type => $id), or `null`, or `false` on error.
0 ignored issues
show
Consider making the return type a bit more specific; maybe use array<*,integer|double|string|boolean>|false|null.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
614 15
     *
615 2
     * @SuppressWarnings(PHPMD.ElseExpression)
616 13
     */
617 13
    private function parseSingleRelationship($data)
618 13
    {
619 13
        if ($data === null) {
620 13
            $result = null;
621
        } elseif (is_array($data) === true &&
622 12
            array_key_exists(DI::KEYWORD_TYPE, $data) === true &&
623
            array_key_exists(DI::KEYWORD_ID, $data) === true &&
624 1
            is_scalar($type = $data[DI::KEYWORD_TYPE]) === true &&
625
            is_scalar($index = $data[DI::KEYWORD_ID]) === true
626
        ) {
627 15
            $result = [$type => $index];
628
        } else {
629
            $result = false;
630
        }
631
632
        return $result;
633
    }
634
635
    /**
636
     * @param mixed $input
637
     * @param int   $index
638 24
     *
639
     * @return void
640 24
     *
641 24
     * @SuppressWarnings(PHPMD.StaticAccess)
642 24
     */
643 24
    private function executeBlock($input, int $index): void
644 24
    {
645 24
        BlockInterpreter::executeBlock(
646 24
            $input,
647
            $index,
648
            $this->getBlocks(),
649
            $this->getContext(),
650
            $this->getCaptureAggregator(),
651
            $this->getErrorAggregator()
652
        );
653
    }
654
655
    /**
656
     * @param array $indexes
657 25
     *
658
     * @return void
659 25
     *
660 1
     * @SuppressWarnings(PHPMD.StaticAccess)
661 1
     */
662 1
    private function executeStarts(array $indexes): void
663 1
    {
664 1
        if (empty($indexes) === false) {
665
            BlockInterpreter::executeStarts(
666
                $indexes,
667
                $this->getBlocks(),
668
                $this->getContext(),
669
                $this->getErrorAggregator()
670
            );
671
        }
672
    }
673
674
    /**
675
     * @param array $indexes
676 25
     *
677
     * @return void
678 25
     *
679 5
     * @SuppressWarnings(PHPMD.StaticAccess)
680 5
     */
681 5
    private function executeEnds(array $indexes): void
682 5
    {
683 5
        if (empty($indexes) === false) {
684
            BlockInterpreter::executeEnds(
685
                $indexes,
686
                $this->getBlocks(),
687
                $this->getContext(),
688
                $this->getErrorAggregator()
689
            );
690
        }
691
    }
692
693 3
    /**
694
     * @param ErrorInterface $error
695 3
     *
696
     * @return string
697 3
     */
698
    private function getMessage(ErrorInterface $error): string
699
    {
700
        $message = $this->formatMessage($error->getMessageTemplate(), $error->getMessageParameters());
701
702
        return $message;
703 17
    }
704
705 17
    /**
706
     * @return array
707
     */
708
    protected function getIdRule(): array
709
    {
710
        return $this->idRule;
711 17
    }
712
713 17
    /**
714
     * @return array
715
     */
716
    protected function getTypeRule(): array
717
    {
718
        return $this->typeRule;
719 26
    }
720
721 26
    /**
722
     * @return ContextStorageInterface
723
     */
724
    protected function getContext(): ContextStorageInterface
725
    {
726
        return $this->context;
727
    }
728
729 30
    /**
730
     * @param ContextStorageInterface $context
731 30
     *
732
     * @return self
733 30
     */
734
    protected function setContext(ContextStorageInterface $context): self
735
    {
736
        $this->context = $context;
737
738
        return $this;
739 26
    }
740
741 26
    /**
742
     * @return JsonApiErrorCollection
743
     */
744
    protected function getJsonApiErrorCollection(): JsonApiErrorCollection
745
    {
746
        return $this->jsonApiErrors;
747
    }
748
749 30
    /**
750
     * @param JsonApiErrorCollection $errors
751 30
     *
752
     * @return self
753 30
     */
754
    protected function setJsonApiErrors(JsonApiErrorCollection $errors): self
755
    {
756
        $this->jsonApiErrors = $errors;
757
758
        return $this;
759 1
    }
760
761 1
    /**
762
     * @return bool
763
     */
764
    protected function isIgnoreUnknowns(): bool
765
    {
766
        return $this->isIgnoreUnknowns;
767 1
    }
768
769 1
    /**
770
     * @return self
771 1
     */
772
    protected function enableIgnoreUnknowns(): self
773
    {
774
        $this->isIgnoreUnknowns = true;
775
776
        return $this;
777 30
    }
778
779 30
    /**
780
     * @return self
781 30
     */
782
    protected function disableIgnoreUnknowns(): self
783
    {
784
        $this->isIgnoreUnknowns = false;
785
786
        return $this;
787 5
    }
788
789 5
    /**
790
     * @return int
791 5
     */
792
    private function getErrorStatus(): int
793
    {
794
        assert($this->errorStatus !== null, 'Check error code was set');
795
796
        return $this->errorStatus;
797 13
    }
798
799
    /**
800
     * @param int $status
801
     */
802
    private function addErrorStatus(int $status): void
803
    {
804
        // Currently (at the moment of writing) the spec is vague about how error status should be set.
805
        // On the one side it says, for example, 'A server MUST return 409 Conflict when processing a POST
806
        // request to create a resource with a client-generated ID that already exists.'
807
        // So you might think 'simple, that should be HTTP status, right?'
808
        // But on the other
809
        // - 'it [server] MAY continue processing and encounter multiple problems.'
810
        // - 'When a server encounters multiple problems for a single request, the most generally applicable
811
        //    HTTP error code SHOULD be used in the response. For instance, 400 Bad Request might be appropriate
812
        //    for multiple 4xx errors'
813 13
814 13
        // So, as we might return multiple errors, we have to figure out what is the best status for response.
815 6
816 3
        // The strategy is the following: for the first error its error code becomes the Response's status.
817
        // If any following error code do not match the previous the status becomes generic 400.
818
        if ($this->errorStatus === null) {
819
            $this->errorStatus = $status;
820
        } elseif ($this->errorStatus !== JsonApiResponse::HTTP_BAD_REQUEST && $this->errorStatus !== $status) {
821
            $this->errorStatus = JsonApiResponse::HTTP_BAD_REQUEST;
822
        }
823
    }
824
825 30
    /**
826
     * @param array $rules
827 30
     *
828
     * @return self
829 30
     */
830
    private function setAttributeRules(array $rules): self
831 30
    {
832
        assert($this->debugCheckIndexesExist($rules));
833
834
        $this->attributeRules = $rules;
835
836
        return $this;
837
    }
838
839 30
    /**
840
     * @param array $rules
841 30
     *
842
     * @return self
843 30
     */
844
    private function setToOneIndexes(array $rules): self
845 30
    {
846
        assert($this->debugCheckIndexesExist($rules));
847
848
        $this->toOneRules = $rules;
849
850
        return $this;
851
    }
852
853 30
    /**
854
     * @param array $rules
855 30
     *
856
     * @return self
857 30
     */
858
    private function setToManyIndexes(array $rules): self
859 30
    {
860
        assert($this->debugCheckIndexesExist($rules));
861
862
        $this->toManyRules = $rules;
863
864
        return $this;
865 17
    }
866
867 17
    /**
868
     * @return int[]
869
     */
870
    protected function getAttributeRules(): array
871
    {
872
        return $this->attributeRules;
873 26
    }
874
875 26
    /**
876
     * @return int[]
877
     */
878
    protected function getToOneRules(): array
879
    {
880
        return $this->toOneRules;
881 23
    }
882
883 23
    /**
884
     * @return int[]
885
     */
886
    protected function getToManyRules(): array
887
    {
888
        return $this->toManyRules;
889 28
    }
890
891 28
    /**
892
     * @return array
893
     */
894
    private function getBlocks(): array
895
    {
896
        return $this->blocks;
897 9
    }
898
899 9
    /**
900 9
     * @return FormatterInterface
901
     */
902
    protected function getFormatter(): FormatterInterface
903 9
    {
904
        if ($this->formatter === null) {
905
            $this->formatter = $this->formatterFactory->createFormatter(Messages::NAMESPACE_NAME);
906
        }
907
908
        return $this->formatter;
909
    }
910
911 30
    /**
912
     * @param FormatterFactoryInterface $formatterFactory
913 30
     *
914
     * @return self
915 30
     */
916
    protected function setFormatterFactory(FormatterFactoryInterface $formatterFactory): self
917
    {
918
        $this->formatterFactory = $formatterFactory;
919
920
        return $this;
921
    }
922
923
    /**
924
     * @param string $name
925 10
     *
926
     * @return int|null
927 10
     *
928 10
     * @SuppressWarnings(PHPMD.StaticAccess)
929
     */
930 10
    private function getAttributeIndex(string $name): ?int
931
    {
932
        $indexes = $this->getSerializer()::readRulesIndexes($this->getAttributeRules());
933
        $index   = $indexes[$name] ?? null;
934
935
        return $index;
936
    }
937
938
    /**
939 9
     * @param string $defaultMessage
940
     * @param array  $args
941 9
     *
942
     * @return string
943 9
     */
944
    private function formatMessage(string $defaultMessage, array $args = []): string
945
    {
946
        $message = $this->getFormatter()->formatMessage($defaultMessage, $args);
947
948
        return $message;
949 26
    }
950
951 26
    /**
952
     * @return ErrorAggregatorInterface
953
     */
954
    private function getErrorAggregator(): ErrorAggregatorInterface
955
    {
956
        return $this->errorAggregator;
957 26
    }
958
959 26
    /**
960
     * @return CaptureAggregatorInterface
961
     */
962
    private function getCaptureAggregator(): CaptureAggregatorInterface
963
    {
964
        return $this->captureAggregator;
965
    }
966
967
    /**
968
     * @param array $rules
969 30
     *
970
     * @return bool
971 30
     *
972
     * @SuppressWarnings(PHPMD.StaticAccess)
973 30
     */
974 30
    private function debugCheckIndexesExist(array $rules): bool
975 30
    {
976 30
        $allOk = true;
977
978
        $indexes = array_merge(
979 30
            $this->getSerializer()::readRulesIndexes($rules),
980 25
            $this->getSerializer()::readRulesStartIndexes($rules),
981
            $this->getSerializer()::readRulesEndIndexes($rules)
982
        );
983 30
984
        foreach ($indexes as $index) {
985
            $allOk = $allOk && is_int($index) && $this->getSerializer()::hasRule($index, $this->getBlocks());
986
        }
987
988
        return $allOk;
989
    }
990
}
991