Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
ReservedPaths
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
4 / 4
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 fromConfig
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 match
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 assertFree
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Node;
6
7use Celema\Core\Exception\HttpBadRequest;
8use Cosray\Config;
9
10/**
11 * URL prefixes that the router or the web server answers before the node
12 * catchall. A node path at or below one of them would never be reachable,
13 * so it is refused when saved instead of silently shadowed.
14 */
15final readonly class ReservedPaths
16{
17    /** @var list<string> */
18    private array $prefixes;
19
20    /** @param list<string> $prefixes */
21    public function __construct(array $prefixes = [])
22    {
23        $normalized = [];
24
25        foreach ($prefixes as $prefix) {
26            $prefix = '/' . trim($prefix, '/');
27
28            // The root prefix would reserve every path; it is not a mount.
29            if ($prefix !== '/') {
30                $normalized[] = $prefix;
31            }
32        }
33
34        $this->prefixes = array_values(array_unique($normalized));
35    }
36
37    public static function fromConfig(Config $config): self
38    {
39        return new self(array_values(array_filter([
40            $config->panel->path,
41            $config->api->path,
42            $config->path->assets,
43            $config->path->cache,
44            '/preview',
45        ])));
46    }
47
48    /** The reserved prefix the path falls under, if any. */
49    public function match(string $path): ?string
50    {
51        $path = '/' . ltrim($path, '/');
52
53        foreach ($this->prefixes as $prefix) {
54            if ($path === $prefix || str_starts_with($path, $prefix . '/')) {
55                return $prefix;
56            }
57        }
58
59        return null;
60    }
61
62    /** @param array<array-key, mixed> $paths */
63    public function assertFree(array $paths): void
64    {
65        foreach ($paths as $path) {
66            if (!is_string($path) || trim($path) === '') {
67                continue;
68            }
69
70            if ($this->match(trim($path)) !== null) {
71                throw new HttpBadRequest(payload: [
72                    'message' => __('node:reserved-path', ['path' => '/' . ltrim(trim($path), '/')]),
73                ]);
74            }
75        }
76    }
77}