Skip to content

Commit 187e401

Browse files
committed
UrlImmutable, UrlScript: added resolve()
1 parent 7609720 commit 187e401

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
@@ -287,6 +287,48 @@ public function isEqual(string|Url|self $url): bool
287287
}
288288

289289

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