~thirdplace/components

16a093a8e4571428748b7c5ddb8d994c90c904f2 — Dag 1 year, 4 months ago 1a38be7
fix: url bugs with trailing slashes
2 files changed, 39 insertions(+), 28 deletions(-)

M src/Url.php
M test/url.php
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'));