Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add configurable and validatable schemes. #67

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 58 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,19 @@

A simple package to deal with URLs in your applications.

Retrieve parts of the URL:
## Installation

You can install the package via composer:

```bash
composer require spatie/url
```

## Usage

### Parse and transform a URL

Retrieve any part of the URL:

```php
use Spatie\Url\Url;
Expand All @@ -18,7 +30,10 @@ echo $url->getHost(); // 'spatie.be'
echo $url->getPath(); // '/opensource'
```

Transform any part of the URL (the `Url` class is immutable):
Transform any part of the URL:

> **Note**
> the `Url` class is immutable.

```php
$url = Url::fromString('https://spatie.be/opensource');
Expand All @@ -27,6 +42,37 @@ echo $url->withHost('github.com')->withPath('spatie');
// 'https://github.com/spatie'
```

### Scheme

Transform the URL scheme.
```php
$url = Url::fromString('http://spatie.be/opensource');

echo $url->withScheme('https'); // 'https://spatie.be/opensource'
```

Use a list of allowed schemes.

> **Note**
> each scheme in the list will be sanitized

```php
$url = Url::fromString('https://spatie.be/opensource');

echo $url->withAllowedSchemes(['wss'])->withScheme('wss'); // 'wss://spatie.be/opensource'
```

or pass the list directly to `fromString` as the URL's scheme will be sanitized and validated immediately:

```php
$url = Url::fromString('https://spatie.be/opensource', [...SchemeValidator::VALID_SCHEMES, 'wss']);

echo $url->withScheme('wss'); // 'wss://spatie.be/opensource'
```


### Query parameters

Retrieve and transform query parameters:

```php
Expand All @@ -46,6 +92,8 @@ echo $url->withoutQueryParameter('utm_campaign'); // 'https://spatie.be/opensour
echo $url->withQueryParameters(['utm_campaign' => 'packages']); // 'https://spatie.be/opensource?utm_source=github&utm_campaign=packages'
```

### Path segments

Retrieve path segments:

```php
Expand All @@ -55,6 +103,8 @@ echo $url->getSegment(1); // 'opensource'
echo $url->getSegment(2); // 'laravel'
```

### PSR-7 `UriInterface`

Implements PSR-7's `UriInterface` interface:

```php
Expand All @@ -65,35 +115,19 @@ The [`league/uri`](https://github.com/thephpleague/uri) is a more powerful packa

Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource).

## Support us

[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/url.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/url)

We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).

We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).

## Installation

You can install the package via composer:
## Testing

```bash
composer require spatie/url
composer test
```

## Usage

Usage is pretty straightforward. Check out the code examples at the top of this readme.

## Changelog
## Support us

Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/url.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/url)

## Testing
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).

```bash
composer test
```
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).

## Changelog

Expand Down
9 changes: 9 additions & 0 deletions src/BaseValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Spatie\Url;

use Spatie\Url\Contracts\Validator;

abstract class BaseValidator implements Validator
{
}
8 changes: 8 additions & 0 deletions src/Contracts/Validator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Spatie\Url\Contracts;

interface Validator
{
public function validate(): void;
}
6 changes: 4 additions & 2 deletions src/Exceptions/InvalidArgument.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

class InvalidArgument extends InvalidArgumentException
{
public static function invalidScheme(string $url): static
public static function invalidScheme(string $scheme, array $allowedSchemes): static
{
return new static("The scheme `{$url}` isn't valid. It should be either `http`, `https`, `mailto` or `tel`.");
$schemes = implode(', ', array_map(fn($scheme) => "`{$scheme}`", $allowedSchemes));

return new static("The scheme `{$scheme}` isn't valid. It should be either {$schemes}.");
}

public static function invalidUrl(string $url): static
Expand Down
65 changes: 65 additions & 0 deletions src/Scheme.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Spatie\Url;

use Spatie\Macroable\Macroable;
use Stringable;

class Scheme implements Stringable
{
use Macroable;

protected string $scheme;

protected SchemeValidator $validator;

public function __construct(
string $scheme = '',
array|null $allowedSchemes = null,
) {
$this->validator = new SchemeValidator($allowedSchemes);

$this->setScheme($scheme);
}

protected function validate(string $scheme): void
{
$this->validator->setScheme($scheme);

$this->validator->validate();
}

public function getScheme(): string
{
return $this->scheme;
}

public function setScheme(string $scheme): void
{
$sanitizedScheme = $this->validator::sanitizeScheme($scheme);

$this->validate($sanitizedScheme);

$this->scheme = $sanitizedScheme;
}

public function getAllowedSchemes(): array
{
return $this->validator->getAllowedSchemes();
}

public function setAllowedSchemes(array $allowedSchemes): void
{
$this->validator->setAllowedSchemes($allowedSchemes);
}

public function __toString(): string
{
return $this->getScheme();
}

public function __clone()
{
$this->validator = clone $this->validator;
}
}
58 changes: 58 additions & 0 deletions src/SchemeValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Spatie\Url;

use Spatie\Url\Exceptions\InvalidArgument;

class SchemeValidator extends BaseValidator
{
public const VALID_SCHEMES = ['http', 'https', 'mailto', 'tel'];

private string|null $scheme;

public function __construct(
private array|null $allowedSchemes = null
) {
$this->scheme = null;
$this->allowedSchemes = $allowedSchemes ?? self::VALID_SCHEMES;
}

public function validate(): void
{
// '' aka "no scheme" must always be valid
$alwaysAllowedSchemes = [''];

if (! in_array($this->scheme, [...$this->allowedSchemes, ...$alwaysAllowedSchemes])) {
throw InvalidArgument::invalidScheme($this->scheme, $this->allowedSchemes);
}
}

public static function sanitizeScheme(string $scheme): string
{
// TODO: regex to allow correct format according to https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
return strtolower($scheme);
}

public function getScheme(): string|null
{
return $this->scheme;
}

public function setScheme(string $scheme): void
{
$this->scheme = $scheme;
}

public function getAllowedSchemes(): array|null
{
return $this->allowedSchemes;
}

public function setAllowedSchemes(array $allowedSchemes): void
{
$this->allowedSchemes = array_map(
fn($scheme) => static::sanitizeScheme($scheme),
$allowedSchemes
);
}
}
54 changes: 31 additions & 23 deletions src/Url.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Url implements UriInterface, Stringable
{
use Macroable;

protected string $scheme = '';
protected Scheme $scheme;

protected string $host = '';

Expand All @@ -27,10 +27,9 @@ class Url implements UriInterface, Stringable

protected string $fragment = '';

public const VALID_SCHEMES = ['http', 'https', 'mailto', 'tel'];

public function __construct()
{
$this->scheme = new Scheme();
$this->query = new QueryParameterBag();
}

Expand All @@ -39,23 +38,33 @@ public static function create(): static
return new static();
}

public static function fromString(string $url): static
public static function fromString(string $url, array|null $allowedSchemes = null): static
{
if (! $parts = parse_url($url)) {
throw InvalidArgument::invalidUrl($url);
$toUrl = new static();

if($allowedSchemes !== null) {
$toUrl = $toUrl->withAllowedSchemes($allowedSchemes);
}

$url = new static();
$url->scheme = isset($parts['scheme']) ? $url->sanitizeScheme($parts['scheme']) : '';
$url->host = $parts['host'] ?? '';
$url->port = $parts['port'] ?? null;
$url->user = $parts['user'] ?? '';
$url->password = $parts['pass'] ?? null;
$url->path = $parts['path'] ?? '/';
$url->query = QueryParameterBag::fromString($parts['query'] ?? '');
$url->fragment = $parts['fragment'] ?? '';
return static::make($url, $toUrl);
}

return $url;
protected static function make(string $fromUrl, self $toUrl): static
{
if (! $parts = parse_url($fromUrl)) {
throw InvalidArgument::invalidUrl($fromUrl);
}

$toUrl->scheme->setScheme(isset($parts['scheme']) ? $parts['scheme'] : '');
$toUrl->host = $parts['host'] ?? '';
$toUrl->port = $parts['port'] ?? null;
$toUrl->user = $parts['user'] ?? '';
$toUrl->password = $parts['pass'] ?? null;
$toUrl->path = $parts['path'] ?? '/';
$toUrl->query = QueryParameterBag::fromString($parts['query'] ?? '');
$toUrl->fragment = $parts['fragment'] ?? '';

return $toUrl;
}

public function getScheme(): string
Expand Down Expand Up @@ -217,20 +226,18 @@ public function withScheme($scheme): static
{
$url = clone $this;

$url->scheme = $this->sanitizeScheme($scheme);
$url->scheme->setScheme($scheme);

return $url;
}

protected function sanitizeScheme(string $scheme): string
public function withAllowedSchemes(array $schemes): static
{
$scheme = strtolower($scheme);
$url = clone $this;

if (! in_array($scheme, static::VALID_SCHEMES)) {
throw InvalidArgument::invalidScheme($scheme);
}
$url->scheme->setAllowedSchemes($schemes);

return $scheme;
return $url;
}

public function withUserInfo($user, $password = null): static
Expand Down Expand Up @@ -361,5 +368,6 @@ public function __toString(): string
public function __clone()
{
$this->query = clone $this->query;
$this->scheme = clone $this->scheme;
}
}
Loading
Loading