Issues (23)

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/Parser/Parser.php (3 issues)

Severity

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 Neomerx\JsonApi\Parser;
4
5
/**
6
 * Copyright 2015-2020 [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 IteratorAggregate;
22
use Neomerx\JsonApi\Contracts\Factories\FactoryInterface;
23
use Neomerx\JsonApi\Contracts\Parser\DocumentDataInterface;
24
use Neomerx\JsonApi\Contracts\Parser\EditableContextInterface;
25
use Neomerx\JsonApi\Contracts\Parser\IdentifierInterface;
26
use Neomerx\JsonApi\Contracts\Parser\ParserInterface;
27
use Neomerx\JsonApi\Contracts\Parser\RelationshipInterface;
28
use Neomerx\JsonApi\Contracts\Parser\ResourceInterface;
29
use Neomerx\JsonApi\Contracts\Schema\DocumentInterface;
30
use Neomerx\JsonApi\Contracts\Schema\IdentifierInterface as SchemaIdentifierInterface;
31
use Neomerx\JsonApi\Contracts\Schema\PositionInterface;
32
use Neomerx\JsonApi\Contracts\Schema\SchemaContainerInterface;
33
use Neomerx\JsonApi\Exceptions\InvalidArgumentException;
34
use Traversable;
35
use function Neomerx\JsonApi\I18n\format as _;
36
37
/**
38
 * @package Neomerx\JsonApi
39
 *
40
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
41
 */
42
class Parser implements ParserInterface
43
{
44
    /** @var string */
45
    public const MSG_NO_SCHEMA_FOUND = 'No Schema found for top-level resource `%s`.';
46
47
    /** @var string */
48
    public const MSG_NO_DATA_IN_RELATIONSHIP =
49
        'For resource of type `%s` with ID `%s` relationship `%s` cannot be parsed because it has no data. Skipping.';
50
51
    /** @var string */
52
    public const MSG_CAN_NOT_PARSE_RELATIONSHIP =
53
        'For resource of type `%s` with ID `%s` relationship `%s` cannot be parsed because it either ' .
54
        'has `null` or identifier as data. Skipping.';
55
56
    /** @var string */
57
    public const MSG_PATHS_HAVE_NOT_BEEN_NORMALIZED_YET =
58
        'Paths have not been normalized yet. Have you called `parse` method already?';
59
60
    /**
61
     * @var SchemaContainerInterface
62
     */
63
    private $schemaContainer;
64
65
    /**
66
     * @var FactoryInterface
67
     */
68
    private $factory;
69
70
    /**
71
     * @var array
72
     */
73
    private $paths;
74
75
    /**
76
     * @var array
77
     */
78
    private $resourcesTracker;
79
80
    /**
81
     * @var EditableContextInterface
82
     */
83
    private $context;
84
85
    /**
86
     * @param FactoryInterface         $factory
87
     * @param SchemaContainerInterface $container
88
     * @param EditableContextInterface $context
89
     */
90
    public function __construct(
91
        FactoryInterface $factory,
92
        SchemaContainerInterface $container,
93
        EditableContextInterface $context
94
    ) {
95
        $this->resourcesTracker = [];
96
        $this->factory          = $factory;
97
        $this->schemaContainer  = $container;
98
        $this->context          = $context;
99
    }
100
101
    /**
102
     * @inheritdoc
103
     *
104
     * @SuppressWarnings(PHPMD.ElseExpression)
105
     */
106
    public function parse($data, array $paths = []): iterable
107
    {
108
        \assert(\is_array($data) === true || \is_object($data) === true || $data === null);
109
110
        $this->paths = $this->normalizePaths($paths);
0 ignored issues
show
$paths is of type array, but the function expects a object<Neomerx\JsonApi\Parser\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...
111
112
        $rootPosition = $this->factory->createPosition(
113
            ParserInterface::ROOT_LEVEL,
114
            ParserInterface::ROOT_PATH,
115
            null,
116
            null
117
        );
118
119
        if ($this->schemaContainer->hasSchema($data) === true) {
120
            yield $this->createDocumentDataIsResource($rootPosition);
121
            yield from $this->parseAsResource($rootPosition, $data);
122
        } elseif ($data instanceof SchemaIdentifierInterface) {
123
            yield $this->createDocumentDataIsIdentifier($rootPosition);
124
            yield $this->parseAsIdentifier($rootPosition, $data);
125
        } elseif (\is_array($data) === true) {
126
            yield $this->createDocumentDataIsCollection($rootPosition);
127
            yield from $this->parseAsResourcesOrIdentifiers($rootPosition, $data);
0 ignored issues
show
$data is of type array, but the function expects a object<Neomerx\JsonApi\Parser\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...
128
        } elseif ($data instanceof Traversable) {
129
            $data = $data instanceof IteratorAggregate ? $data->getIterator() : $data;
130
            yield $this->createDocumentDataIsCollection($rootPosition);
131
            yield from $this->parseAsResourcesOrIdentifiers($rootPosition, $data);
0 ignored issues
show
$data is of type object<Traversable>, but the function expects a object<Neomerx\JsonApi\Parser\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...
132
        } elseif ($data === null) {
133
            yield $this->createDocumentDataIsNull($rootPosition);
134
        } else {
135
            throw new InvalidArgumentException(_(static::MSG_NO_SCHEMA_FOUND, \get_class($data)));
136
        }
137
    }
138
139
    /**
140
     * @param PositionInterface $position
141
     * @param iterable          $dataOrIds
142
     *
143
     * @see ResourceInterface
144
     * @see IdentifierInterface
145
     *
146
     * @return iterable
147
     */
148
    private function parseAsResourcesOrIdentifiers(
149
        PositionInterface $position,
150
        iterable $dataOrIds
151
    ): iterable {
152
        foreach ($dataOrIds as $dataOrId) {
153
            if ($this->schemaContainer->hasSchema($dataOrId) === true) {
154
                yield from $this->parseAsResource($position, $dataOrId);
155
156
                continue;
157
            }
158
159
            \assert($dataOrId instanceof SchemaIdentifierInterface);
160
            yield $this->parseAsIdentifier($position, $dataOrId);
161
        }
162
    }
163
164
    /**
165
     * @return EditableContextInterface
166
     */
167
    protected function getContext(): EditableContextInterface
168
    {
169
        return $this->context;
170
    }
171
172
    /**
173
     * @return array
174
     */
175
    protected function getNormalizedPaths(): array
176
    {
177
        \assert($this->paths !== null, _(static::MSG_PATHS_HAVE_NOT_BEEN_NORMALIZED_YET));
178
179
        return $this->paths;
180
    }
181
182
    /**
183
     * @param string $path
184
     *
185
     * @return bool
186
     */
187
    protected function isPathRequested(string $path): bool
188
    {
189
        return isset($this->paths[$path]);
190
    }
191
192
    /**
193
     * @param PositionInterface $position
194
     * @param mixed             $data
195
     *
196
     * @see ResourceInterface
197
     *
198
     * @return iterable
199
     *
200
     */
201
    private function parseAsResource(
202
        PositionInterface $position,
203
        $data
204
    ): iterable {
205
        \assert($this->schemaContainer->hasSchema($data) === true);
206
207
        $resource = $this->factory->createParsedResource(
208
            $this->getContext(),
209
            $position,
210
            $this->schemaContainer,
211
            $data
212
        );
213
214
        yield from $this->parseResource($resource);
215
    }
216
217
    /**
218
     * @param ResourceInterface $resource
219
     *
220
     * @return iterable
221
     *
222
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
223
     */
224
    private function parseResource(ResourceInterface $resource): iterable
225
    {
226
        $seenBefore = isset($this->resourcesTracker[$resource->getId()][$resource->getType()]);
227
228
        // top level resources should be yielded in any case as it could be an array of the resources
229
        // for deeper levels it's not needed as they go to `included` section and it must have no more
230
        // than one instance of the same resource.
231
232
        if ($seenBefore === false || $resource->getPosition()->getLevel() <= ParserInterface::ROOT_LEVEL) {
233
            yield $resource;
234
        }
235
236
        // parse relationships only for resources not seen before (prevents infinite loop for circular references)
237
        if ($seenBefore === false) {
238
            // remember by id and type
239
            $this->resourcesTracker[$resource->getId()][$resource->getType()] = true;
240
241
            foreach ($resource->getRelationships() as $name => $relationship) {
242
                \assert(\is_string($name));
243
                \assert($relationship instanceof RelationshipInterface);
244
245
                $isShouldParse = $this->isPathRequested($relationship->getPosition()->getPath());
246
247
                if ($isShouldParse === true && $relationship->hasData() === true) {
248
                    $relData = $relationship->getData();
249
                    if ($relData->isResource() === true) {
250
                        yield from $this->parseResource($relData->getResource());
251
252
                        continue;
253
                    } elseif ($relData->isCollection() === true) {
254
                        foreach ($relData->getResources() as $relResource) {
255
                            \assert($relResource instanceof ResourceInterface ||
256
                                $relResource instanceof IdentifierInterface);
257
                            if ($relResource instanceof ResourceInterface) {
258
                                yield from $this->parseResource($relResource);
259
                            }
260
                        }
261
262
                        continue;
263
                    }
264
265
                    \assert($relData->isNull() || $relData->isIdentifier());
266
                }
267
            }
268
        }
269
    }
270
271
    /**
272
     * @param PositionInterface         $position
273
     * @param SchemaIdentifierInterface $identifier
274
     *
275
     * @return IdentifierInterface
276
     */
277
    private function parseAsIdentifier(
278
        PositionInterface $position,
279
        SchemaIdentifierInterface $identifier
280
    ): IdentifierInterface {
281
        return new class ($position, $identifier) implements IdentifierInterface
282
        {
283
            /**
284
             * @var PositionInterface
285
             */
286
            private $position;
287
288
            /**
289
             * @var SchemaIdentifierInterface
290
             */
291
            private $identifier;
292
293
            /**
294
             * @param PositionInterface         $position
295
             * @param SchemaIdentifierInterface $identifier
296
             */
297
            public function __construct(PositionInterface $position, SchemaIdentifierInterface $identifier)
298
            {
299
                $this->position   = $position;
300
                $this->identifier = $identifier;
301
            }
302
303
            /**
304
             * @inheritdoc
305
             */
306
            public function getPosition(): PositionInterface
307
            {
308
                return $this->position;
309
            }
310
311
            /**
312
             * @inheritdoc
313
             */
314
            public function getId(): ?string
315
            {
316
                return $this->identifier->getId();
317
            }
318
319
            /**
320
             * @inheritdoc
321
             */
322
            public function getType(): string
323
            {
324
                return $this->identifier->getType();
325
            }
326
327
            /**
328
             * @inheritdoc
329
             */
330
            public function hasIdentifierMeta(): bool
331
            {
332
                return $this->identifier->hasIdentifierMeta();
333
            }
334
335
            /**
336
             * @inheritdoc
337
             */
338
            public function getIdentifierMeta()
339
            {
340
                return $this->identifier->getIdentifierMeta();
341
            }
342
        };
343
    }
344
345
    /**
346
     * @param PositionInterface $position
347
     *
348
     * @return DocumentDataInterface
349
     */
350
    private function createDocumentDataIsCollection(PositionInterface $position): DocumentDataInterface
351
    {
352
        return $this->createParsedDocumentData($position, true, false);
353
    }
354
355
    /**
356
     * @param PositionInterface $position
357
     *
358
     * @return DocumentDataInterface
359
     */
360
    private function createDocumentDataIsNull(PositionInterface $position): DocumentDataInterface
361
    {
362
        return $this->createParsedDocumentData($position, false, true);
363
    }
364
365
    /**
366
     * @param PositionInterface $position
367
     *
368
     * @return DocumentDataInterface
369
     */
370
    private function createDocumentDataIsResource(PositionInterface $position): DocumentDataInterface
371
    {
372
        return $this->createParsedDocumentData($position, false, false);
373
    }
374
375
    /**
376
     * @param PositionInterface $position
377
     *
378
     * @return DocumentDataInterface
379
     */
380
    private function createDocumentDataIsIdentifier(PositionInterface $position): DocumentDataInterface
381
    {
382
        return $this->createParsedDocumentData($position, false, false);
383
    }
384
385
    /**
386
     * @param PositionInterface $position
387
     * @param bool              $isCollection
388
     * @param bool              $isNull
389
     *
390
     * @return DocumentDataInterface
391
     */
392 75
    private function createParsedDocumentData(
393
        PositionInterface $position,
394
        bool $isCollection,
395
        bool $isNull
396
    ): DocumentDataInterface {
397
        return new class (
398
            $position,
399
            $isCollection,
400
            $isNull
401
        ) implements DocumentDataInterface
402
        {
403
            /**
404
             * @var PositionInterface
405
             */
406
            private $position;
407
            /**
408
             * @var bool
409
             */
410
            private $isCollection;
411
412
            /**
413
             * @var bool
414
             */
415
            private $isNull;
416
417
            /**
418
             * @param PositionInterface $position
419
             * @param bool              $isCollection
420
             * @param bool              $isNull
421
             */
422
            public function __construct(
423
                PositionInterface $position,
424
                bool $isCollection,
425
                bool $isNull
426
            ) {
427 75
                $this->position     = $position;
428 75
                $this->isCollection = $isCollection;
429 75
                $this->isNull       = $isNull;
430 75
            }
431
432
            /**
433
             * @inheritdoc
434
             */
435
            public function getPosition(): PositionInterface
436
            {
437 75
                return $this->position;
438
            }
439
440
            /**
441
             * @inheritdoc
442
             */
443 75
            public function isCollection(): bool
444
            {
445 75
                return $this->isCollection;
446
            }
447
448
            /**
449
             * @inheritdoc
450
             */
451 55
            public function isNull(): bool
452
            {
453 55
                return $this->isNull;
454
            }
455
        };
456
    }
457
458
    /**
459
     * @param iterable $paths
460
     *
461
     * @return array
462
     */
463 76
    private function normalizePaths(iterable $paths): array
464
    {
465 76
        $separator = DocumentInterface::PATH_SEPARATOR;
466
467
        // convert paths like a.b.c to paths that actually should be used a, a.b, a.b.c
468 76
        $normalizedPaths = [];
469 76
        foreach ($paths as $path) {
470 27
            $curPath = '';
471 27
            foreach (\explode($separator, $path) as $pathPart) {
472 27
                $curPath                   = empty($curPath) === true ? $pathPart : $curPath . $separator . $pathPart;
473 27
                $normalizedPaths[$curPath] = true;
474
            }
475
        }
476
477 76
        return $normalizedPaths;
478
    }
479
}
480