• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / http / 21832799728

09 Feb 2026 04:17PM UTC coverage: 83.62% (-0.2%) from 83.772%
21832799728

push

github

dg
Request::isSameSite() is silently deprecated

924 of 1105 relevant lines covered (83.62%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.0
/src/Http/Url.php
1
<?php
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Nette\Http;
11

12
use Nette;
13
use function array_pop, array_slice, bin2hex, chunk_split, defined, explode, function_exists, http_build_query, idn_to_utf8, implode, ini_get, ip2long, is_array, is_string, ksort, parse_str, parse_url, preg_match, preg_quote, preg_replace, preg_replace_callback, rawurldecode, rawurlencode, rtrim, str_contains, str_replace, str_starts_with, strcasecmp, strlen, strrpos, strtolower, strtoupper, substr;
14
use const IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, PHP_QUERY_RFC3986;
15

16

17
/**
18
 * Mutable representation of a URL.
19
 *
20
 * <pre>
21
 * scheme  user  password  host  port      path        query    fragment
22
 *   |      |      |        |      |        |            |         |
23
 * /--\   /--\ /------\ /-------\ /--\/------------\ /--------\ /------\
24
 * http://john:x0y17575@nette.org:8042/en/manual.php?name=param#fragment  <-- absoluteUrl
25
 * \______\__________________________/
26
 *     |               |
27
 *  hostUrl        authority
28
 * </pre>
29
 *
30
 * @property   string $scheme
31
 * @property   string $user
32
 * @property   string $password
33
 * @property   string $host
34
 * @property   int $port
35
 * @property   string $path
36
 * @property   string $query
37
 * @property   string $fragment
38
 * @property-read string $absoluteUrl
39
 * @property-read string $authority
40
 * @property-read string $hostUrl
41
 * @property-read string $basePath
42
 * @property-read string $baseUrl
43
 * @property-read string $relativeUrl
44
 * @property-read array<string,mixed> $queryParameters
45
 */
46
class Url implements \JsonSerializable
47
{
48
        use Nette\SmartObject;
49

50
        /** @var array<string, int> */
51
        public static array $defaultPorts = [
52
                'http' => 80,
53
                'https' => 443,
54
                'ftp' => 21,
55
        ];
56

57
        private string $scheme = '';
58
        private string $user = '';
59
        private string $password = '';
60
        private string $host = '';
61
        private ?int $port = null;
62
        private string $path = '';
63

64
        /** @var mixed[] */
65
        private array $query = [];
66
        private string $fragment = '';
67

68

69
        /**
70
         * @throws Nette\InvalidArgumentException if URL is malformed
71
         */
72
        public function __construct(string|self|UrlImmutable|null $url = null)
1✔
73
        {
74
                if (is_string($url)) {
1✔
75
                        $p = @parse_url($url); // @ - is escalated to exception
1✔
76
                        if ($p === false) {
1✔
77
                                throw new Nette\InvalidArgumentException("Malformed or unsupported URI '$url'.");
1✔
78
                        }
79

80
                        $this->scheme = $p['scheme'] ?? '';
1✔
81
                        $this->port = $p['port'] ?? null;
1✔
82
                        $this->host = rawurldecode($p['host'] ?? '');
1✔
83
                        $this->user = rawurldecode($p['user'] ?? '');
1✔
84
                        $this->password = rawurldecode($p['pass'] ?? '');
1✔
85
                        $this->setPath($p['path'] ?? '');
1✔
86
                        $this->setQuery($p['query'] ?? []);
1✔
87
                        $this->fragment = rawurldecode($p['fragment'] ?? '');
1✔
88

89
                } elseif ($url instanceof UrlImmutable || $url instanceof self) {
1✔
90
                        [$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment] = $url->export();
1✔
91
                }
92
        }
1✔
93

94

95
        public function setScheme(string $scheme): static
1✔
96
        {
97
                $this->scheme = $scheme;
1✔
98
                return $this;
1✔
99
        }
100

101

102
        public function getScheme(): string
103
        {
104
                return $this->scheme;
1✔
105
        }
106

107

108
        /** @deprecated */
109
        public function setUser(string $user): static
110
        {
111
                $this->user = $user;
×
112
                return $this;
×
113
        }
114

115

116
        /** @deprecated */
117
        public function getUser(): string
118
        {
119
                return $this->user;
1✔
120
        }
121

122

123
        /** @deprecated */
124
        public function setPassword(string $password): static
125
        {
126
                $this->password = $password;
×
127
                return $this;
×
128
        }
129

130

131
        /** @deprecated */
132
        public function getPassword(): string
133
        {
134
                return $this->password;
1✔
135
        }
136

137

138
        public function setHost(string $host): static
1✔
139
        {
140
                $this->host = $host;
1✔
141
                $this->setPath($this->path);
1✔
142
                return $this;
1✔
143
        }
144

145

146
        public function getHost(): string
147
        {
148
                return $this->host;
1✔
149
        }
150

151

152
        /**
153
         * Returns the part of domain.
154
         */
155
        public function getDomain(int $level = 2): string
1✔
156
        {
157
                $parts = ip2long($this->host)
1✔
158
                        ? [$this->host]
1✔
159
                        : explode('.', $this->host);
1✔
160
                $parts = $level >= 0
1✔
161
                        ? array_slice($parts, -$level)
1✔
162
                        : array_slice($parts, 0, $level);
1✔
163
                return implode('.', $parts);
1✔
164
        }
165

166

167
        public function setPort(int $port): static
1✔
168
        {
169
                $this->port = $port;
1✔
170
                return $this;
1✔
171
        }
172

173

174
        public function getPort(): ?int
175
        {
176
                return $this->port ?: $this->getDefaultPort();
1✔
177
        }
178

179

180
        public function getDefaultPort(): ?int
181
        {
182
                return self::$defaultPorts[$this->scheme] ?? null;
1✔
183
        }
184

185

186
        public function setPath(string $path): static
1✔
187
        {
188
                $this->path = $path;
1✔
189
                if ($this->host && !str_starts_with($this->path, '/')) {
1✔
190
                        $this->path = '/' . $this->path;
1✔
191
                }
192

193
                return $this;
1✔
194
        }
195

196

197
        public function getPath(): string
198
        {
199
                return $this->path;
1✔
200
        }
201

202

203
        /** @param string|mixed[] $query */
204
        public function setQuery(string|array $query): static
1✔
205
        {
206
                $this->query = is_array($query) ? $query : self::parseQuery($query);
1✔
207
                return $this;
1✔
208
        }
209

210

211
        /** @param string|mixed[] $query */
212
        public function appendQuery(string|array $query): static
1✔
213
        {
214
                $this->query = is_array($query)
1✔
215
                        ? $query + $this->query
1✔
216
                        : self::parseQuery($this->getQuery() . '&' . $query);
1✔
217
                return $this;
1✔
218
        }
219

220

221
        public function getQuery(): string
222
        {
223
                return http_build_query($this->query, '', '&', PHP_QUERY_RFC3986);
1✔
224
        }
225

226

227
        /** @return mixed[] */
228
        public function getQueryParameters(): array
229
        {
230
                return $this->query;
1✔
231
        }
232

233

234
        public function getQueryParameter(string $name): mixed
1✔
235
        {
236
                return $this->query[$name] ?? null;
1✔
237
        }
238

239

240
        public function setQueryParameter(string $name, mixed $value): static
1✔
241
        {
242
                $this->query[$name] = $value;
1✔
243
                return $this;
1✔
244
        }
245

246

247
        public function setFragment(string $fragment): static
248
        {
249
                $this->fragment = $fragment;
×
250
                return $this;
×
251
        }
252

253

254
        public function getFragment(): string
255
        {
256
                return $this->fragment;
1✔
257
        }
258

259

260
        public function getAbsoluteUrl(): string
261
        {
262
                return $this->getHostUrl() . $this->path
1✔
263
                        . (($tmp = $this->getQuery()) ? '?' . $tmp : '')
1✔
264
                        . ($this->fragment === '' ? '' : '#' . $this->fragment);
1✔
265
        }
266

267

268
        /**
269
         * Returns the [user[:pass]@]host[:port] part of URI.
270
         */
271
        public function getAuthority(): string
272
        {
273
                return $this->host === ''
1✔
274
                        ? ''
1✔
275
                        : ($this->user !== ''
1✔
276
                                ? rawurlencode($this->user) . ($this->password === '' ? '' : ':' . rawurlencode($this->password)) . '@'
1✔
277
                                : '')
1✔
278
                        . $this->host
1✔
279
                        . ($this->port && $this->port !== $this->getDefaultPort()
1✔
280
                                ? ':' . $this->port
1✔
281
                                : '');
1✔
282
        }
283

284

285
        /**
286
         * Returns the scheme and authority part of URI.
287
         */
288
        public function getHostUrl(): string
289
        {
290
                return ($this->scheme ? $this->scheme . ':' : '')
1✔
291
                        . (($authority = $this->getAuthority()) !== '' ? '//' . $authority : '');
1✔
292
        }
293

294

295
        /** @deprecated use UrlScript::getBasePath() instead */
296
        public function getBasePath(): string
297
        {
298
                $pos = strrpos($this->path, '/');
×
299
                return $pos === false ? '' : substr($this->path, 0, $pos + 1);
×
300
        }
301

302

303
        /** @deprecated use UrlScript::getBaseUrl() instead */
304
        public function getBaseUrl(): string
305
        {
306
                return $this->getHostUrl() . $this->getBasePath();
×
307
        }
308

309

310
        /** @deprecated use UrlScript::getRelativeUrl() instead */
311
        public function getRelativeUrl(): string
312
        {
313
                return substr($this->getAbsoluteUrl(), strlen($this->getBaseUrl()));
×
314
        }
315

316

317
        /**
318
         * URL comparison.
319
         */
320
        public function isEqual(string|self|UrlImmutable $url): bool
1✔
321
        {
322
                $url = new self($url);
1✔
323
                $query = $url->query;
1✔
324
                ksort($query);
1✔
325
                $query2 = $this->query;
1✔
326
                ksort($query2);
1✔
327
                $host = rtrim($url->host, '.');
1✔
328
                $host2 = rtrim($this->host, '.');
1✔
329
                return $url->scheme === $this->scheme
1✔
330
                        && (!strcasecmp($host, $host2)
1✔
331
                                || self::idnHostToUnicode($host) === self::idnHostToUnicode($host2))
1✔
332
                        && $url->getPort() === $this->getPort()
1✔
333
                        && $url->user === $this->user
1✔
334
                        && $url->password === $this->password
1✔
335
                        && self::unescape($url->path, '%/') === self::unescape($this->path, '%/')
1✔
336
                        && $query === $query2
1✔
337
                        && $url->fragment === $this->fragment;
1✔
338
        }
339

340

341
        /**
342
         * Transforms URL to canonical form.
343
         */
344
        public function canonicalize(): static
345
        {
346
                $this->path = preg_replace_callback(
1✔
347
                        '#[^!$&\'()*+,/:;=@%"]+#',
1✔
348
                        fn(array $m): string => rawurlencode($m[0]),
1✔
349
                        self::unescape($this->path, '%/'),
1✔
350
                );
351
                $this->host = rtrim($this->host, '.');
1✔
352
                $this->host = self::idnHostToUnicode(strtolower($this->host));
1✔
353
                return $this;
1✔
354
        }
355

356

357
        public function __toString(): string
358
        {
359
                return $this->getAbsoluteUrl();
1✔
360
        }
361

362

363
        public function jsonSerialize(): string
364
        {
365
                return $this->getAbsoluteUrl();
1✔
366
        }
367

368

369
        /** @internal */
370
        final public function export(): array
371
        {
372
                return [$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment];
1✔
373
        }
374

375

376
        /**
377
         * Converts IDN ASCII host to UTF-8.
378
         */
379
        private static function idnHostToUnicode(string $host): string
1✔
380
        {
381
                if (!str_contains($host, '--')) { // host does not contain IDN
1✔
382
                        return $host;
1✔
383
                }
384

385
                if (function_exists('idn_to_utf8') && defined('INTL_IDNA_VARIANT_UTS46')) {
1✔
386
                        return idn_to_utf8($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46) ?: $host;
1✔
387
                }
388

389
                trigger_error('PHP extension intl is not loaded or is too old', E_USER_WARNING);
×
390
                return $host;
×
391
        }
392

393

394
        /**
395
         * Similar to rawurldecode, but preserves reserved chars encoded.
396
         */
397
        public static function unescape(string $s, string $reserved = '%;/?:@&=+$,'): string
1✔
398
        {
399
                // reserved (@see RFC 2396) = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
400
                // within a path segment, the characters "/", ";", "=", "?" are reserved
401
                // within a query component, the characters ";", "/", "?", ":", "@", "&", "=", "+", ",", "$" are reserved.
402
                if ($reserved !== '') {
1✔
403
                        $s = preg_replace_callback(
1✔
404
                                '#%(' . substr(chunk_split(bin2hex($reserved), 2, '|'), 0, -1) . ')#i',
1✔
405
                                fn(array $m): string => '%25' . strtoupper($m[1]),
1✔
406
                                $s,
1✔
407
                        );
408
                }
409

410
                return rawurldecode($s);
1✔
411
        }
412

413

414
        /**
415
         * Parses query string. Is affected by directive arg_separator.input.
416
         * @return mixed[]
417
         */
418
        public static function parseQuery(string $s): array
1✔
419
        {
420
                $s = str_replace(['%5B', '%5b'], '[', $s);
1✔
421
                $sep = preg_quote(ini_get('arg_separator.input') ?: '&');
1✔
422
                $s = preg_replace("#([$sep])([^[$sep=]+)([^$sep]*)#", '&0[$2]$3', '&' . $s);
1✔
423
                parse_str($s, $res);
1✔
424
                return $res[0] ?? [];
1✔
425
        }
426

427

428
        /**
429
         * Determines if URL is absolute, ie if it starts with a scheme followed by colon.
430
         */
431
        public static function isAbsolute(string $url): bool
1✔
432
        {
433
                return (bool) preg_match('#^[a-z][a-z0-9+.-]*:#i', $url);
1✔
434
        }
435

436

437
        /**
438
         * Normalizes a path by handling and removing relative path references like '.', '..' and directory traversal.
439
         */
440
        public static function removeDotSegments(string $path): string
1✔
441
        {
442
                $prefix = $segment = '';
1✔
443
                if (str_starts_with($path, '/')) {
1✔
444
                        $prefix = '/';
1✔
445
                        $path = substr($path, 1);
1✔
446
                }
447
                $segments = explode('/', $path);
1✔
448
                $res = [];
1✔
449
                foreach ($segments as $segment) {
1✔
450
                        if ($segment === '..') {
1✔
451
                                array_pop($res);
1✔
452
                        } elseif ($segment !== '.') {
1✔
453
                                $res[] = $segment;
1✔
454
                        }
455
                }
456

457
                if ($segment === '.' || $segment === '..') {
1✔
458
                        $res[] = '';
1✔
459
                }
460
                return $prefix . implode('/', $res);
1✔
461
        }
462
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc