Issues (25)

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/AbstractJsonRepository.php (5 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
2
3
/*
4
 * This file is part of the puli/repository package.
5
 *
6
 * (c) Bernhard Schussek <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Puli\Repository;
13
14
use Psr\Log\LoggerAwareInterface;
15
use Psr\Log\LoggerInterface;
16
use Psr\Log\LogLevel;
17
use Puli\Repository\Api\ChangeStream\ChangeStream;
18
use Puli\Repository\Api\Resource\FilesystemResource;
19
use Puli\Repository\Api\Resource\PuliResource;
20
use Puli\Repository\Api\ResourceCollection;
21
use Puli\Repository\Api\ResourceNotFoundException;
22
use Puli\Repository\Api\UnsupportedResourceException;
23
use Puli\Repository\Resource\Collection\ArrayResourceCollection;
24
use Puli\Repository\Resource\DirectoryResource;
25
use Puli\Repository\Resource\FileResource;
26
use Puli\Repository\Resource\GenericResource;
27
use Puli\Repository\Resource\LinkResource;
28
use RuntimeException;
29
use Webmozart\Assert\Assert;
30
use Webmozart\Json\JsonDecoder;
31
use Webmozart\Json\JsonEncoder;
32
use Webmozart\PathUtil\Path;
33
34
/**
35
 * Base class for repositories backed by a JSON file.
36
 *
37
 * The generated JSON file is described by res/schema/repository-schema-1.0.json.
38
 *
39
 * @since  1.0
40
 *
41
 * @author Bernhard Schussek <[email protected]>
42
 * @author Titouan Galopin <[email protected]>
43
 */
44
abstract class AbstractJsonRepository extends AbstractEditableRepository implements LoggerAwareInterface
45
{
46
    /**
47
     * Flag: Whether to stop after the first result.
48
     *
49
     * @internal
50
     */
51
    const STOP_ON_FIRST = 1;
52
53
    /**
54
     * @var array
55
     */
56
    protected $json;
57
58
    /**
59
     * @var string
60
     */
61
    protected $baseDirectory;
62
63
    /**
64
     * @var string
65
     */
66
    private $path;
67
68
    /**
69
     * @var string
70
     */
71
    private $schemaPath;
72
73
    /**
74
     * @var JsonEncoder
75
     */
76
    private $encoder;
77
78
    /**
79
     * @var LoggerInterface
80
     */
81
    private $logger;
82
83
    /**
84
     * Creates a new repository.
85
     *
86
     * @param string            $path          The path to the JSON file. If
87
     *                                         relative, it must be relative to
88
     *                                         the base directory.
89
     * @param string            $baseDirectory The base directory of the store.
90
     *                                         Paths inside that directory are
91
     *                                         stored as relative paths. Paths
92
     *                                         outside that directory are stored
93
     *                                         as absolute paths.
94
     * @param bool              $validateJson  Whether to validate the JSON file
95
     *                                         against the schema. Slow but
96
     *                                         spots problems.
97
     * @param ChangeStream|null $changeStream  If provided, the repository will
98
     *                                         append resource changes to this
99
     *                                         change stream.
100
     */
101 392
    public function __construct($path, $baseDirectory, $validateJson = false, ChangeStream $changeStream = null)
102
    {
103 392
        parent::__construct($changeStream);
104
105 392
        $this->baseDirectory = $baseDirectory;
106 392
        $this->path = Path::makeAbsolute($path, $baseDirectory);
107 392
        $this->encoder = new JsonEncoder();
108 392
        $this->encoder->setPrettyPrinting(true);
109 392
        $this->encoder->setEscapeSlash(false);
110
111 392
        if ($validateJson) {
112 392
            $this->schemaPath = Path::canonicalize(__DIR__.'/../res/schema/path-mappings-schema-1.0.json');
113
        }
114 392
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 22
    public function setLogger(LoggerInterface $logger = null)
120
    {
121 22
        $this->logger = $logger;
122 22
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 352
    public function add($path, $resource)
128
    {
129 352
        if (null === $this->json) {
130 352
            $this->load();
131
        }
132
133 352
        $path = $this->sanitizePath($path);
134
135 340 View Code Duplication
        if ($resource instanceof ResourceCollection) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
136 4
            $this->ensureDirectoryExists($path);
137
138 4
            foreach ($resource as $child) {
139 4
                $this->addResource($path.'/'.$child->getName(), $child);
140
            }
141
142 4
            $this->flush();
143
144 4
            return;
145
        }
146
147 336
        $this->ensureDirectoryExists(Path::getDirectory($path));
148
149 336
        $this->addResource($path, $resource);
0 ignored issues
show
$resource is of type object<Puli\Repository\Api\Resource\PuliResource>, but the function expects a object<Puli\Repository\A...\Resource\LinkResource>.

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...
150
151 332
        $this->flush();
152 332
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 164
    public function get($path)
158
    {
159 164
        if (null === $this->json) {
160 28
            $this->load();
161
        }
162
163 164
        $path = $this->sanitizePath($path);
164 152
        $references = $this->getReferencesForPath($path);
165
166
        // Might be null, don't use isset()
167 152
        if (array_key_exists($path, $references)) {
168 142
            return $this->createResource($path, $references[$path]);
169
        }
170
171 10
        throw ResourceNotFoundException::forPath($path);
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 48
    public function find($query, $language = 'glob')
178
    {
179 48
        if (null === $this->json) {
180 6
            $this->load();
181
        }
182
183 48
        $this->failUnlessGlob($language);
184 44
        $query = $this->sanitizePath($query);
185 32
        $results = $this->createResources($this->getReferencesForGlob($query));
186
187 32
        ksort($results);
188
189 32
        return new ArrayResourceCollection(array_values($results));
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 68
    public function contains($query, $language = 'glob')
196
    {
197 68
        if (null === $this->json) {
198 22
            $this->load();
199
        }
200
201 68
        $this->failUnlessGlob($language);
202 64
        $query = $this->sanitizePath($query);
203
204 52
        $results = $this->getReferencesForGlob($query, self::STOP_ON_FIRST);
205
206 52
        return !empty($results);
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212 66
    public function remove($query, $language = 'glob')
213
    {
214 66
        if (null === $this->json) {
215 24
            $this->load();
216
        }
217
218 66
        $this->failUnlessGlob($language);
219 62
        $query = $this->sanitizePath($query);
220
221 50
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
222
223 42
        $removed = $this->removeReferences($query);
224
225 40
        $this->flush();
226
227 40
        return $removed;
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233 6
    public function clear()
234
    {
235 6
        if (null === $this->json) {
236 2
            $this->load();
237
        }
238
239
        // Subtract root which is not deleted
240 6
        $removed = count($this->getReferencesForRegex('/', '~.~')) - 1;
241
242 6
        $this->json = array();
243
244 6
        $this->flush();
245
246 6
        return $removed;
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252 48
    public function listChildren($path)
253
    {
254 48
        if (null === $this->json) {
255 2
            $this->load();
256
        }
257
258 48
        $path = $this->sanitizePath($path);
259 36
        $results = $this->createResources($this->getReferencesInDirectory($path));
260
261 36
        if (empty($results)) {
262 16
            $pathResults = $this->getReferencesForPath($path);
263
264 16
            if (empty($pathResults)) {
265 4
                throw ResourceNotFoundException::forPath($path);
266
            }
267
        }
268
269 32
        ksort($results);
270
271 32
        return new ArrayResourceCollection(array_values($results));
272
    }
273
274
    /**
275
     * {@inheritdoc}
276
     */
277 32
    public function hasChildren($path)
278
    {
279 32
        if (null === $this->json) {
280 2
            $this->load();
281
        }
282
283 32
        $path = $this->sanitizePath($path);
284
285 20
        $results = $this->getReferencesInDirectory($path, self::STOP_ON_FIRST);
286
287 20
        if (empty($results)) {
288 12
            $pathResults = $this->getReferencesForPath($path);
289
290 12
            if (empty($pathResults)) {
291 4
                throw ResourceNotFoundException::forPath($path);
292
            }
293
294 8
            return false;
295
        }
296
297 12
        return true;
298
    }
299
300
    /**
301
     * Inserts a path reference into the JSON file.
302
     *
303
     * The path reference can be:
304
     *
305
     *  * a link starting with `@`
306
     *  * an absolute filesystem path
307
     *
308
     * @param string      $path      The Puli path.
309
     * @param string|null $reference The path reference.
310
     */
311
    abstract protected function insertReference($path, $reference);
312
313
    /**
314
     * Removes all path references matching the given glob from the JSON file.
315
     *
316
     * @param string $glob The glob for a list of Puli paths.
317
     */
318
    abstract protected function removeReferences($glob);
319
320
    /**
321
     * Returns the references for a given Puli path.
322
     *
323
     * Each reference returned by this method can be:
324
     *
325
     *  * `null`
326
     *  * a link starting with `@`
327
     *  * an absolute filesystem path
328
     *
329
     * The result has either one entry or none, if no path was found. The key
330
     * of the single entry is the path passed to this method.
331
     *
332
     * @param string $path The Puli path.
333
     *
334
     * @return string[]|null[] A one-level array of references with Puli paths
335
     *                         as keys. The array has at most one entry.
336
     */
337
    abstract protected function getReferencesForPath($path);
338
339
    /**
340
     * Returns the references matching a given Puli path glob.
341
     *
342
     * Each reference returned by this method can be:
343
     *
344
     *  * `null`
345
     *  * a link starting with `@`
346
     *  * an absolute filesystem path
347
     *
348
     * The keys of the returned array are Puli paths. Their order is undefined.
349
     *
350
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
351
     *
352
     * @param string $glob  The glob.
353
     * @param int    $flags A bitwise combination of the flag constants in this
354
     *                      class.
355
     *
356
     * @return string[]|null[] A one-level array of references with Puli paths
357
     *                         as keys.
358
     */
359
    abstract protected function getReferencesForGlob($glob, $flags = 0);
360
361
    /**
362
     * Returns the references matching a given Puli path regular expression.
363
     *
364
     * Each reference returned by this method can be:
365
     *
366
     *  * `null`
367
     *  * a link starting with `@`
368
     *  * an absolute filesystem path
369
     *
370
     * The keys of the returned array are Puli paths. Their order is undefined.
371
     *
372
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
373
     *
374
     * @param string $staticPrefix The static prefix of all Puli paths matching
375
     *                             the regular expression.
376
     * @param string $regex        The regular expression.
377
     * @param int    $flags        A bitwise combination of the flag constants
378
     *                             in this class.
379
     *
380
     * @return string[]|null[] A one-level array of references with Puli paths
381
     *                         as keys.
382
     */
383
    abstract protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0);
384
385
    /**
386
     * Returns the references in a given Puli path.
387
     *
388
     * Each reference returned by this method can be:
389
     *
390
     *  * `null`
391
     *  * a link starting with `@`
392
     *  * an absolute filesystem path
393
     *
394
     * The keys of the returned array are Puli paths. Their order is undefined.
395
     *
396
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
397
     *
398
     * @param string $path  The Puli path.
399
     * @param int    $flags A bitwise combination of the flag constants in this
400
     *                      class.
401
     *
402
     * @return string[]|null[] A one-level array of references with Puli paths
403
     *                         as keys.
404
     */
405
    abstract protected function getReferencesInDirectory($path, $flags = 0);
406
407
    /**
408
     * Logs a message.
409
     *
410
     * @param mixed  $level   One of the level constants in {@link LogLevel}.
411
     * @param string $message The message.
412
     */
413 22
    protected function log($level, $message)
414
    {
415 22
        if (null !== $this->logger) {
416 22
            $this->logger->log($level, $message);
417
        }
418 22
    }
419
420
    /**
421
     * Logs a warning that a reference could not be found.
422
     *
423
     * @param string $path              The Puli path of a path mapping.
424
     * @param string $reference         The reference that was not found.
425
     * @param string $absoluteReference The absolute filesystem path of the
426
     *                                  reference.
427
     */
428 22
    protected function logReferenceNotFound($path, $reference, $absoluteReference)
429
    {
430 22
        $this->log(LogLevel::WARNING, sprintf(
431 22
            'The reference "%s"%s mapped by the path %s could not be found.',
432
            $reference,
433 22
            $reference !== $absoluteReference ? ' ('.$absoluteReference.')' : '',
434
            $path
435
        ));
436 22
    }
437
438
    /**
439
     * Adds a filesystem resource to the JSON file.
440
     *
441
     * @param string             $path     The Puli path.
442
     * @param FilesystemResource $resource The resource to add.
443
     */
444 336
    protected function addFilesystemResource($path, FilesystemResource $resource)
445
    {
446 336
        $resource = clone $resource;
447 336
        $resource->attachTo($this, $path);
448
449 336
        $relativePath = Path::makeRelative($resource->getFilesystemPath(), $this->baseDirectory);
450
451 336
        $this->insertReference($path, $relativePath);
452
453 336
        $this->storeVersion($resource);
454 336
    }
455
456
    /**
457
     * Loads the JSON file.
458
     */
459 392
    protected function load()
460
    {
461 392
        $decoder = new JsonDecoder();
462
463 392
        $this->json = file_exists($this->path)
464 69
            ? (array) $decoder->decodeFile($this->path, $this->schemaPath)
465 392
            : array();
466
467 392
        if (isset($this->json['_order'])) {
468 5
            $this->json['_order'] = (array) $this->json['_order'];
469
470 5
            foreach ($this->json['_order'] as $path => $entries) {
471 5
                foreach ($entries as $key => $entry) {
472 5
                    $this->json['_order'][$path][$key] = (array) $entry;
473
                }
474
            }
475
        }
476
477
        // The root node always exists
478 392
        if (!isset($this->json['/'])) {
479 392
            $this->json['/'] = null;
480
        }
481
482
        // Make sure the JSON is sorted in reverse order
483 392
        krsort($this->json);
484 392
    }
485
486
    /**
487
     * Writes the JSON file.
488
     */
489 340
    protected function flush()
490
    {
491
        // The root node always exists
492 340
        if (!isset($this->json['/'])) {
493 144
            $this->json['/'] = null;
494
        }
495
496
        // Always save in reverse order
497 340
        krsort($this->json);
498
499
        // Comply to schema
500 340
        $json = (object) $this->json;
501
502 340
        if (isset($json->{'_order'})) {
503 10
            $order = $json->{'_order'};
504
505 10
            foreach ($order as $path => $entries) {
506 10
                foreach ($entries as $key => $entry) {
507 10
                    $order[$path][$key] = (object) $entry;
508
                }
509
            }
510
511 10
            $json->{'_order'} = (object) $order;
512
        }
513
514 340
        $this->encoder->encodeFile($json, $this->path, $this->schemaPath);
515 340
    }
516
517
    /**
518
     * Returns whether a reference contains a link.
519
     *
520
     * @param string $reference The reference.
521
     *
522
     * @return bool Whether the reference contains a link.
523
     */
524 284
    protected function isLinkReference($reference)
525
    {
526 284
        return isset($reference{0}) && '@' === $reference{0};
527
    }
528
529
    /**
530
     * Returns whether a reference contains an absolute or relative filesystem
531
     * path.
532
     *
533
     * @param string $reference The reference.
534
     *
535
     * @return bool Whether the reference contains a filesystem path.
536
     */
537 290
    protected function isFilesystemReference($reference)
538
    {
539 290
        return null !== $reference && !$this->isLinkReference($reference);
540
    }
541
542
    /**
543
     * Turns a reference into a resource.
544
     *
545
     * @param string      $path      The Puli path.
546
     * @param string|null $reference The reference.
547
     *
548
     * @return PuliResource The resource.
0 ignored issues
show
Consider making the return type a bit more specific; maybe use GenericResource.

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...
549
     */
550 160
    protected function createResource($path, $reference)
551
    {
552 160
        if (null === $reference) {
553 12
            $resource = new GenericResource();
554 148
        } elseif (isset($reference{0}) && '@' === $reference{0}) {
555 8
            $resource = new LinkResource(substr($reference, 1));
556 148
        } elseif (is_dir($reference)) {
557 84
            $resource = new DirectoryResource($reference);
558 100
        } elseif (is_file($reference)) {
559 100
            $resource = new FileResource($reference);
560
        } else {
561
            throw new RuntimeException(sprintf(
562
                'Trying to create a FilesystemResource on a non-existing file or directory "%s"',
563
                $reference
564
            ));
565
        }
566
567 160
        $resource->attachTo($this, $path);
568
569 160
        return $resource;
570
    }
571
572
    /**
573
     * Turns a list of references into a list of resources.
574
     *
575
     * The references are expected to be in the format returned by
576
     * {@link getReferencesForPath()}, {@link getReferencesForGlob()} and
577
     * {@link getReferencesInDirectory()}.
578
     *
579
     * The result contains Puli paths as keys and {@link PuliResource}
580
     * implementations as values. The order of the results is undefined.
581
     *
582
     * @param string[]|null[] $references The references indexed by Puli paths.
583
     *
584
     * @return array
0 ignored issues
show
Consider making the return type a bit more specific; maybe use array<string|null|GenericResource>.

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...
585
     */
586 68
    private function createResources(array $references)
587
    {
588 68
        foreach ($references as $path => $reference) {
589 44
            $references[$path] = $this->createResource($path, $reference);
0 ignored issues
show
It seems like $reference defined by $reference on line 588 can also be of type object<Puli\Repository\Resource\GenericResource>; however, Puli\Repository\Abstract...itory::createResource() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
590
        }
591
592 68
        return $references;
593
    }
594
595
    /**
596
     * Adds all ancestor directories of a path to the repository.
597
     *
598
     * @param string $path A Puli path.
599
     */
600 340
    private function ensureDirectoryExists($path)
601
    {
602 340
        if (array_key_exists($path, $this->json)) {
603 340
            return;
604
        }
605
606
        // Recursively initialize parent directories
607 104
        if ('/' !== $path) {
608 104
            $this->ensureDirectoryExists(Path::getDirectory($path));
609
        }
610
611 104
        $this->json[$path] = null;
612 104
    }
613
614
    /**
615
     * Adds a resource to the repository.
616
     *
617
     * @param string                          $path     The Puli path to add the
618
     *                                                  resource at.
619
     * @param FilesystemResource|LinkResource $resource The resource to add.
620
     */
621 340
    private function addResource($path, $resource)
622
    {
623 340
        if (!$resource instanceof FilesystemResource && !$resource instanceof LinkResource) {
624 4
            throw new UnsupportedResourceException(sprintf(
625
                'The %s only supports adding FilesystemResource and '.
626 4
                'LinkedResource instances. Got: %s',
627
                // Get the short class name
628 4
                $this->getShortClassName(get_class($this)),
629 4
                $this->getShortClassName(get_class($resource))
630
            ));
631
        }
632
633 336
        if ($resource instanceof LinkResource) {
634 8
            $resource = clone $resource;
635 8
            $resource->attachTo($this, $path);
636
637 8
            $this->insertReference($path, '@'.$resource->getTargetPath());
638
639 8
            $this->storeVersion($resource);
640
        } else {
641
            // Extension point for the optimized repository
642 336
            $this->addFilesystemResource($path, $resource);
643
        }
644 336
    }
645
646
    /**
647
     * Returns the short name of a fully-qualified class name.
648
     *
649
     * @param string $className The fully-qualified class name.
650
     *
651
     * @return string The short class name.
652
     */
653 4
    private function getShortClassName($className)
654
    {
655 4
        if (false !== ($pos = strrpos($className, '\\'))) {
656 4
            return substr($className, $pos + 1);
657
        }
658
659 4
        return $className;
660
    }
661
}
662