Skip to content

Commit d57f6a0

Browse files
committed
Url: added isAbsolute() & removeDotSegments()
1 parent d74dc45 commit d57f6a0

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

src/Http/Url.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,4 +409,40 @@ public static function parseQuery(string $s): array
409409
parse_str($s, $res);
410410
return $res[0] ?? [];
411411
}
412+
413+
414+
/**
415+
* Determines if URL is absolute, ie if it starts with a scheme followed by colon.
416+
*/
417+
public static function isAbsolute(string $url): bool
418+
{
419+
return (bool) preg_match('#^[a-z][a-z0-9+.-]*:#i', $url);
420+
}
421+
422+
423+
/**
424+
* Normalizes a path by handling and removing relative path references like '.', '..' and directory traversal.
425+
*/
426+
public static function removeDotSegments(string $path): string
427+
{
428+
$prefix = $segment = '';
429+
if (str_starts_with($path, '/')) {
430+
$prefix = '/';
431+
$path = substr($path, 1);
432+
}
433+
$segments = explode('/', $path);
434+
$res = [];
435+
foreach ($segments as $segment) {
436+
if ($segment === '..') {
437+
array_pop($res);
438+
} elseif ($segment !== '.') {
439+
$res[] = $segment;
440+
}
441+
}
442+
443+
if ($segment === '.' || $segment === '..') {
444+
$res[] = '';
445+
}
446+
return $prefix . implode('/', $res);
447+
}
412448
}

tests/Http/Url.isAbsolute.phpt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Http\Url;
6+
use Tester\Assert;
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
Assert::false(Url::isAbsolute('/'));
12+
Assert::false(Url::isAbsolute('//'));
13+
Assert::false(Url::isAbsolute(''));
14+
Assert::false(Url::isAbsolute('https'));
15+
Assert::true(Url::isAbsolute('https:'));
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Http\Url;
6+
use Tester\Assert;
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
test('begins with /', function () {
12+
Assert::same('/', Url::removeDotSegments('/'));
13+
Assert::same('/file', Url::removeDotSegments('/file'));
14+
Assert::same('/file/', Url::removeDotSegments('/file/'));
15+
16+
Assert::same('/', Url::removeDotSegments('/.'));
17+
Assert::same('/', Url::removeDotSegments('/./'));
18+
19+
Assert::same('/file/', Url::removeDotSegments('/file/.'));
20+
Assert::same('/', Url::removeDotSegments('/file/..'));
21+
Assert::same('/', Url::removeDotSegments('/file/../'));
22+
Assert::same('/file', Url::removeDotSegments('/./file'));
23+
});
24+
25+
26+
test('not begins with /', function () {
27+
Assert::same('', Url::removeDotSegments(''));
28+
Assert::same('file', Url::removeDotSegments('file'));
29+
Assert::same('file/', Url::removeDotSegments('file/'));
30+
31+
Assert::same('', Url::removeDotSegments('.'));
32+
Assert::same('', Url::removeDotSegments('./'));
33+
34+
Assert::same('file/', Url::removeDotSegments('file/.'));
35+
Assert::same('', Url::removeDotSegments('file/..'));
36+
Assert::same('', Url::removeDotSegments('file/../'));
37+
Assert::same('file', Url::removeDotSegments('./file'));
38+
});
39+
40+
41+
test('incorrect ..', function () {
42+
Assert::same('/', Url::removeDotSegments('/file/../..'));
43+
Assert::same('/', Url::removeDotSegments('/file/../../'));
44+
Assert::same('/bar', Url::removeDotSegments('/file/../../bar'));
45+
Assert::same('/file', Url::removeDotSegments('/../file'));
46+
Assert::same('/bar', Url::removeDotSegments('/file/./.././.././bar'));
47+
Assert::same('/bar/', Url::removeDotSegments('/file/./.././.././bar/'));
48+
});
49+
50+
51+
test('double slash', function () {
52+
Assert::same('//', Url::removeDotSegments('//'));
53+
Assert::same('//foo//', Url::removeDotSegments('//foo//'));
54+
Assert::same('//foo//', Url::removeDotSegments('//foo//..//'));
55+
});

0 commit comments

Comments
 (0)