Skip to content

Commit e34cf05

Browse files
committed
UrlImmutable, UrlScript: added resolve()
1 parent d57f6a0 commit e34cf05

File tree

4 files changed

+185
-0
lines changed

4 files changed

+185
-0
lines changed

src/Http/UrlImmutable.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,48 @@ public function isEqual(string|Url|self $url): bool
282282
}
283283

284284

285+
/**
286+
* Resolves relative URLs in the same way as browser. If path is relative, it is resolved against
287+
* base URL, if begins with /, it is resolved against the host root.
288+
*/
289+
public function resolve(string $reference): self
290+
{
291+
$ref = new self($reference);
292+
if ($ref->scheme !== '') {
293+
$ref->path = Url::removeDotSegments($ref->path);
294+
return $ref;
295+
}
296+
297+
$ref->scheme = $this->scheme;
298+
299+
if ($ref->host !== '') {
300+
$ref->path = Url::removeDotSegments($ref->path);
301+
return $ref;
302+
}
303+
304+
$ref->host = $this->host;
305+
$ref->port = $this->port;
306+
307+
if ($ref->path === '') {
308+
$ref->path = $this->path;
309+
$ref->query = $ref->query ?: $this->query;
310+
} elseif (str_starts_with($ref->path, '/')) {
311+
$ref->path = Url::removeDotSegments($ref->path);
312+
} else {
313+
$ref->path = Url::removeDotSegments($this->mergePath($ref->path));
314+
}
315+
return $ref;
316+
}
317+
318+
319+
/** @internal */
320+
protected function mergePath(string $path): string
321+
{
322+
$pos = strrpos($this->path, '/');
323+
return $pos === false ? $path : substr($this->path, 0, $pos + 1) . $path;
324+
}
325+
326+
285327
public function jsonSerialize(): string
286328
{
287329
return $this->getAbsoluteUrl();

src/Http/UrlScript.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,9 @@ public function getPathInfo(): string
106106
}
107107

108108

109+
/** @internal */
110+
protected function mergePath(string $path): string
111+
{
112+
return $this->basePath . $path;
113+
}
109114
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Http\UrlImmutable;
6+
use Tester\Assert;
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
$tests = [
12+
'https://example.com/path/' => [
13+
// absolute URLs with various schemes
14+
'a:' => 'a:',
15+
'a:b' => 'a:b',
16+
'http://other.com/test' => 'http://other.com/test',
17+
'https://other.com/test' => 'https://other.com/test',
18+
'ftp://other.com/test' => 'ftp://other.com/test',
19+
20+
// protocol-relative URLs - keep current scheme
21+
'//other.com/test' => 'https://other.com/test',
22+
23+
// root-relative paths
24+
'/test' => 'https://example.com/test',
25+
'/test/' => 'https://example.com/test/',
26+
27+
// relative paths
28+
'sibling' => 'https://example.com/path/sibling',
29+
'../parent' => 'https://example.com/parent',
30+
'child/' => 'https://example.com/path/child/',
31+
],
32+
33+
// base dir with query string and fragment
34+
'https://example.com/path/?q=123#frag' => [
35+
'' => 'https://example.com/path/?q=123',
36+
'file' => 'https://example.com/path/file',
37+
'./file' => 'https://example.com/path/file',
38+
'/root' => 'https://example.com/root',
39+
'subdir/?q=456' => 'https://example.com/path/subdir/?q=456',
40+
'subdir/#frag' => 'https://example.com/path/subdir/#frag',
41+
'../file' => 'https://example.com/file',
42+
'/../file' => 'https://example.com/file',
43+
'file?newq=/..#newfrag/..' => 'https://example.com/path/file?newq=%2F..#newfrag/..',
44+
'?newq=/..' => 'https://example.com/path/?newq=%2F..',
45+
'#newfrag/..' => 'https://example.com/path/?q=123#newfrag/..',
46+
],
47+
48+
// base file with query string and fragment
49+
'https://example.com/path/file?q=123#frag' => [
50+
'' => 'https://example.com/path/file?q=123',
51+
'file' => 'https://example.com/path/file',
52+
'./file' => 'https://example.com/path/file',
53+
'/root' => 'https://example.com/root',
54+
'subdir/file?q=123' => 'https://example.com/path/subdir/file?q=123',
55+
'subdir/file#frag' => 'https://example.com/path/subdir/file#frag',
56+
'../file' => 'https://example.com/file',
57+
'/../file' => 'https://example.com/file',
58+
'?newq=/..' => 'https://example.com/path/file?newq=%2F..',
59+
'#newfrag/..' => 'https://example.com/path/file?q=123#newfrag/..',
60+
],
61+
];
62+
63+
64+
foreach ($tests as $base => $paths) {
65+
$url = new UrlImmutable($base);
66+
foreach ($paths as $path => $expected) {
67+
Assert::same($expected, (string) $url->resolve($path), "Base: $base, Reference: $path");
68+
}
69+
}

tests/Http/UrlScript.resolve.phpt

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Http\UrlScript;
6+
use Tester\Assert;
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
$tests = [
12+
'https://example.com/path/' => [
13+
// absolute URLs with various schemes
14+
'a:' => 'a:',
15+
'a:b' => 'a:b',
16+
'http://other.com/test' => 'http://other.com/test',
17+
'https://other.com/test' => 'https://other.com/test',
18+
'ftp://other.com/test' => 'ftp://other.com/test',
19+
20+
// protocol-relative URLs - keep current scheme
21+
'//other.com/test' => 'https://other.com/test',
22+
23+
// root-relative paths
24+
'/test' => 'https://example.com/test',
25+
'/test/' => 'https://example.com/test/',
26+
27+
// relative paths
28+
'sibling' => 'https://example.com/sibling',
29+
'../parent' => 'https://example.com/parent',
30+
'child/' => 'https://example.com/child/',
31+
],
32+
33+
// base dir with query string and fragment
34+
'https://example.com/path/?q=123#frag' => [
35+
'' => 'https://example.com/path/?q=123',
36+
'file' => 'https://example.com/file',
37+
'./file' => 'https://example.com/file',
38+
'/root' => 'https://example.com/root',
39+
'subdir/?q=456' => 'https://example.com/subdir/?q=456',
40+
'subdir/#frag' => 'https://example.com/subdir/#frag',
41+
'../file' => 'https://example.com/file',
42+
'/../file' => 'https://example.com/file',
43+
'file?newq=/..#newfrag/..' => 'https://example.com/file?newq=%2F..#newfrag/..',
44+
'?newq=/..' => 'https://example.com/path/?newq=%2F..',
45+
'#newfrag/..' => 'https://example.com/path/?q=123#newfrag/..',
46+
],
47+
48+
// base file with query string and fragment
49+
'https://example.com/path/file?q=123#frag' => [
50+
'' => 'https://example.com/path/file?q=123',
51+
'file' => 'https://example.com/file',
52+
'./file' => 'https://example.com/file',
53+
'/root' => 'https://example.com/root',
54+
'subdir/file?q=123' => 'https://example.com/subdir/file?q=123',
55+
'subdir/file#frag' => 'https://example.com/subdir/file#frag',
56+
'../file' => 'https://example.com/file',
57+
'/../file' => 'https://example.com/file',
58+
'?newq=/..' => 'https://example.com/path/file?newq=%2F..',
59+
'#newfrag/..' => 'https://example.com/path/file?q=123#newfrag/..',
60+
],
61+
];
62+
63+
64+
foreach ($tests as $base => $paths) {
65+
$url = new UrlScript($base, '/');
66+
foreach ($paths as $path => $expected) {
67+
Assert::same($expected, (string) $url->resolve($path), "Base: $base, Reference: $path");
68+
}
69+
}

0 commit comments

Comments
 (0)