M src/Url.php => src/Url.php +20 -12
@@ 3,7 3,9 @@ declare(strict_types=1);
namespace Thirdplace;
-final class UrlException extends \Exception {}
+final class UrlException extends \Exception
+{
+}
function url(string $url): Url
{
@@ 19,7 21,9 @@ final class Url implements \JsonSerializable
public array $query;
private ?string $fragment;
- private function __construct() {}
+ private function __construct()
+ {
+ }
public static function fromString(string $url): self
{
@@ 47,7 51,7 @@ final class Url implements \JsonSerializable
return clone $this;
}
if (substr($href, 0, 2) === '//') {
- return url($this->scheme . '://' . substr($href, 2));
+ return url($this->scheme . '://' . substr($href, 2));
}
if ($href[0] === '/') {
return $this->withPath($href);
@@ 62,15 66,15 @@ final class Url implements \JsonSerializable
public static function validate(string $url): bool
{
- if(strlen($url) > 1500) {
+ if (strlen($url) > 1500) {
return false;
}
$pattern = '#^https?://' // scheme
- . '([a-z0-9-]+\.?)+' // one or more domain names
- . '(\.[a-z]{1,24})?' // optional global tld
- . '(:\d+)?' // optional port
- . '($|/|\?)#i' // end of string or slash or question mark
+ . '([a-z0-9-]+\.?)+' // one or more domain names
+ . '(\.[a-z]{1,24})?' // optional global tld
+ . '(:\d+)?' // optional port
+ . '($|/|\?)#i' // end of string or slash or question mark
;
return preg_match($pattern, $url) === 1;
}
@@ 147,7 151,7 @@ final class Url implements \JsonSerializable
public function jsonSerialize(): string
{
- return (string) $this;
+ return (string)$this;
}
public function __toString()
@@ 157,6 161,8 @@ final class Url implements \JsonSerializable
private function normalize(): string
{
+ $hasTrailingSlash = $this->path[-1] === '/';
+
$explode = explode('/', $this->path);
foreach ($explode as $i => $part) {
if (in_array($part, ['', '.'])) {
@@ 164,18 170,20 @@ final class Url implements \JsonSerializable
continue;
}
if ($part === '..') {
- unset($explode[$i-1]);
+ unset($explode[$i - 1]);
unset($explode[$i]);
}
}
$path = implode('/', $explode);
+ if ($hasTrailingSlash) {
+ $path .= '/';
+ }
$path = '/' . ltrim($path, '/');
if (!$this->query && !$this->fragment && $path === '/') {
- $path = rtrim($path, '/') . '/';
+ $path = '';
}
- // todo: remove trailing / if the path, query and fragment is empty
return sprintf(
'%s://%s%s%s%s%s',
$this->scheme,
M test/url.php => test/url.php +19 -16
@@ 4,19 4,22 @@ declare(strict_types=1);
namespace Thirdplace;
assertEquals('https://example.com:81/foo?bar=baz#kek', (string)url('https://example.com:81/foo?bar=baz#kek'));
-assertEquals('https://example.com/', url('https://example.com'));
-assertEquals('https://example.com/', url('https://example.com/'));
-assertEquals('https://example.com/', url('https://example.com//'));
-assertEquals('https://example.com/', url('https://example.com/./'));
-assertEquals('https://example.com/', url('https://example.com//./'));
-assertEquals('https://example.com/', url('https://example.com/.///'));
-assertEquals('https://example.com/', url('https://example.com/../'));
-assertEquals('https://example.com/', url('https://example.com/foo/..'));
-assertEquals('https://example.com/foo', url('https://example.com/foo/bar/..'));
-assertEquals('https://example.com/foo?bar=', url('https://example.com/foo?bar'));
-assertEquals('https://example.com/?baz=', url('https://example.com/foo/..?baz='));
-assertEquals('https://example.com/foo/bar', url('https://example.com/foo/bar/baz/..'));
-assertEquals('https://example.com/foo', url('https://example.com/foo/bar/../baz/..'));
-//assertEquals('https://example.com/', url('https://example.com/foo/bar/../..'));
-assertEquals('https://example.com/', (string)url('https://example.com/../..'));
-assertEquals('https://example.com/foo?foo=bar', (string) url('https://example.com/foo/?foo=bar'));
+assertEquals('https://example.com', (string)url('https://example.com'));
+assertEquals('https://example.com/foo', (string)url('https://example.com/foo'));
+assertEquals('https://example.com/foo/', (string)url('https://example.com/foo/'));
+assertEquals('https://example.com', (string)url('https://example.com/'));
+assertEquals('https://example.com', (string)url('https://example.com//'));
+assertEquals('https://example.com', (string)url('https://example.com/./'));
+assertEquals('https://example.com', (string)url('https://example.com//./'));
+assertEquals('https://example.com', (string)url('https://example.com/.///'));
+assertEquals('https://example.com', (string)url('https://example.com/../'));
+assertEquals('https://example.com', (string)url('https://example.com/foo/..'));
+assertEquals('https://example.com/foo', (string)url('https://example.com/foo/bar/..'));
+assertEquals('https://example.com/foo?bar=', (string)url('https://example.com/foo?bar'));
+assertEquals('https://example.com/?baz=', (string)url('https://example.com/foo/..?baz='));
+assertEquals('https://example.com/foo/bar', (string)url('https://example.com/foo/bar/baz/..'));
+assertEquals('https://example.com/foo', (string)url('https://example.com/foo/bar/../baz/..'));
+//assertEquals('https://example.com', (string)url('https://example.com/foo/bar/../..'));
+assertEquals('https://example.com', (string)url('https://example.com/../..'));
+assertEquals('https://example.com/foo?foo=bar', (string) url('https://example.com/foo?foo=bar'));
+assertEquals('https://blog.zulip.com/2022/05/05/public-access-option', (string) url('https://blog.zulip.com/2022/05/05/public-access-option'));