Code

< 40 %
40-60 %
> 60 %
1
<?php
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 *
11
 */
12
13
namespace Koded\Http;
14
15
use InvalidArgumentException;
16
use JsonSerializable;
17
use Koded\Http\Interfaces\HttpStatus;
18
use Psr\Http\Message\UriInterface;
19
use Throwable;
20
21
class Uri implements UriInterface, JsonSerializable
22
{
23
    const STANDARD_PORTS = [80, 443, 21, 23, 70, 110, 119, 143, 389];
24
25
    private string $scheme = '';
26
    private string $host = '';
27
    private ?int $port = 80;
28
    private string $path = '';
29
    private string $user = '';
30
    private string $pass = '';
31
    private string $fragment = '';
32
    private string $query = '';
33
34
    public function __construct(string $uri)
35
    {
36
        $uri && $this->parse($uri);
37
    }
38
39
    public function __toString()
40
    {
41
        return \sprintf('%s%s%s%s%s',
42
            $this->scheme ? ($this->getScheme() . '://') : '',
43
            $this->getAuthority() ?: $this->getHostWithPort(),
44
            $this->getPath(),
45
            \strlen($this->query) ? ('?' . $this->query) : '',
46 174
            \strlen($this->fragment) ? ('#' . $this->fragment) : ''
47
        );
48 174
    }
49 173
50
    public function getScheme(): string
51 26
    {
52
        return \strtolower($this->scheme);
53 26
    }
54 26
55 26
    public function getAuthority(): string
56 26
    {
57 26
        $userInfo = $this->getUserInfo();
58 26
        if (0 === \strlen($userInfo)) {
59
            return '';
60
        }
61
        return $userInfo . '@' . $this->getHostWithPort();
62 26
    }
63
64 26
    public function getUserInfo(): string
65
    {
66
        if (0 === \strlen($this->user)) {
67 30
            return '';
68
        }
69 30
        return \trim($this->user . ':' . $this->pass, ':');
70
    }
71 30
72 26
    public function getHost(): string
73
    {
74
        return \strtolower($this->host);
75 6
    }
76
77
    public function getPort(): ?int
78 34
    {
79
        if (!$this->scheme && !$this->port) {
80 34
            return null;
81 30
        }
82
        return $this->port;
83
    }
84 8
85
    public function getPath(): string
86
    {
87 135
        return $this->reduceSlashes($this->path);
88
    }
89 135
90
    public function getQuery(): string
91
    {
92 9
        return $this->query;
93
    }
94 9
95 1
    public function getFragment(): string
96
    {
97
        return $this->fragment;
98 8
    }
99
100
    public function withScheme($scheme): UriInterface
101 46
    {
102
        if (null !== $scheme && false === \is_string($scheme)) {
103 46
            throw new InvalidArgumentException('Invalid URI scheme', 400);
104
        }
105
106 9
        $instance         = clone $this;
107
        $instance->scheme = (string)$scheme;
108 9
        return $instance;
109
    }
110
111 8
    public function withUserInfo($user, $password = null): UriInterface
112
    {
113 8
        $instance       = clone $this;
114
        $instance->user = (string)$user;
115
        $instance->pass = (string)$password;
116 8
117
        // If the path is rootless and an authority is present,
118 8
        // the path MUST be prefixed with "/"
119 4
        if ('/' !== ($instance->path[0] ?? '')) {
120
            $instance->path = '/' . $instance->path;
121
        }
122 4
        return $instance;
123 4
    }
124
125 4
    public function withHost($host): UriInterface
126
    {
127
        $instance       = clone $this;
128 4
        $instance->host = (string)$host;
129
        return $instance;
130 4
    }
131 4
132 4
    public function withPort($port): UriInterface
133
    {
134
        $instance = clone $this;
135
        if (null === $port) {
136 4
            $instance->port = null;
137 2
            return $instance;
138
        }
139
        if (false === \is_int($port) || $port < 1) {
140 4
            throw new InvalidArgumentException('Invalid port');
141
        }
142
        $instance->port = $port;
143 7
        return $instance;
144
    }
145 7
146 7
    public function withPath($path): UriInterface
147
    {
148 7
        $instance       = clone $this;
149
        $instance->path = $this->fixPath((string)$path);
150
        return $instance;
151 6
    }
152
153 6
    public function withQuery($query): UriInterface
154
    {
155 6
        try {
156 2
            $query = \rawurldecode($query);
157
        } catch (Throwable) {
158 2
            throw new InvalidArgumentException('The provided query string is invalid');
159
        }
160
        $instance        = clone $this;
161 4
        $instance->query = (string)$query;
162 1
        return $instance;
163
    }
164
165 3
    public function withFragment($fragment): UriInterface
166
    {
167 3
        $instance           = clone $this;
168
        $instance->fragment = \str_replace(['#', '%23'], '', $fragment);
169
        return $instance;
170 4
    }
171
172 4
    private function parse(string $uri)
173 4
    {
174
        if (false === $parts = \parse_url($uri)) {
175 4
            throw new InvalidArgumentException('Please provide a valid URI', HttpStatus::BAD_REQUEST);
176
        }
177
        foreach ($parts as $k => $v) {
178 3
            $this->$k = $v;
179
        }
180
        $this->path = $this->fixPath($parts['path'] ?? '');
181 3
        if ($this->isStandardPort()) {
182 1
            $this->port = null;
183 1
        }
184
    }
185
186 2
    private function fixPath(string $path): string
187 2
    {
188
        if (empty($path)) {
189 2
            return $path;
190
        }
191
        // Percent encode the path
192 3
        $path = \explode('/', $path);
193
        foreach ($path as $k => $part) {
194 3
            $path[$k] = \str_contains($part, '%') ? $part : \rawurlencode($part);
195 3
        }
196
        // TODO remove the entry script from the path?
197 3
        $path = \str_replace('/index.php', '', \join('/', $path));
198
        return $path;
199
    }
200 138
201
    private function reduceSlashes(string $path): string
202 138
    {
203 3
        if ('/' === ($path[0] ?? '') && 0 === \strlen($this->user)) {
204
            return \preg_replace('/\/+/', '/', $path);
205
        }
206 137
        return $path;
207 137
    }
208
209
    private function getHostWithPort(): string
210 137
    {
211
        if ($this->port) {
212 137
            return $this->host . ($this->isStandardPort() ? '' : ':' . $this->port);
213 99
        }
214
        return $this->host;
215 137
    }
216
217 138
    private function isStandardPort(): bool
218
    {
219 138
        return \in_array($this->port, static::STANDARD_PORTS);
220 79
    }
221
222
    public function jsonSerialize()
223
    {
224 65
        return \array_filter([
225 65
            'scheme' => $this->getScheme(),
226 65
            'host' => $this->getHost(),
227
            'port' => $this->getPort(),
228
            'path' => $this->getPath(),
229
            'user' => $this->user,
230 65
            'pass' => $this->pass,
231
            'fragment' => $this->fragment,
232 65
            'query' => $this->query,
233
        ]);
234
    }
235
}
236