Skip to content

Commit a4a09ee

Browse files
committed
Add support for mailto schemee on League\Uri\Uri
1 parent ebb8381 commit a4a09ee

File tree

5 files changed

+114
-13
lines changed

5 files changed

+114
-13
lines changed

docs/uri/7.0/rfc3986.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ $uri = Uri::new('toto://thephpleague.com/path/to?here#content');
146146
<p class="message-notice">For extra validation rules to be triggered the URI must be absolute and the
147147
scheme recognized. Otherwise, only basic RFC3986 rules are taken into account.</p>
148148

149-
### Complete Scheme Validation
149+
### Scheme Validation
150150

151151
For the following URI schemes (order alphabetically) full validation is taken into account.
152152

@@ -156,8 +156,9 @@ For the following URI schemes (order alphabetically) full validation is taken in
156156
- http(s)
157157
- ws(s)
158158
- blob **(since version 7.6)**
159+
- mailto **(since version 7.6)**
159160

160-
### URN Generic Validation
161+
#### URN Validation
161162

162163
<p class="message-notice">Since version <code>7.6.0</code></p>
163164

@@ -177,7 +178,7 @@ $uri = Uri::new('urn:isbn:foobar/baz');
177178
// possibly with hyphens only
178179
~~~
179180

180-
### Component Presence Validation
181+
### Component Validation
181182

182183
<p class="message-notice">Since version <code>7.6.0</code></p>
183184

@@ -186,17 +187,17 @@ and on the different RFCs and specifications for each URI scheme, a validation b
186187
component presence is used. For instance, the following URI will be rejected.
187188

188189
```php
189-
$uri = Uri::new('mailto:path/to?here');
190+
$uri = Uri::new('bitcoin:config/here');
190191
//will work
191192

192-
$uri = Uri::new('mailto://thephpleague.com/path/to?here');
193+
$uri = Uri::new('bitcoin://thephpleague.com');
193194
// will throw a League\Uri\Exceptions\SyntaxError
194195
```
195196

196-
In the example, because a the `mailto` URI scheme cannot contain an authority component, an
197+
In the example, because a the `bitcoin` URI scheme cannot contain an authority component, an
197198
exception is thrown. On the other hand, the path has not been checked to validate that it only
198-
contained valid email addresses. According to RFC3986, the path component is valid, but it is
199-
not according to the [`mailto` RFC](https://www.rfc-editor.org/rfc/rfc6068.html).
199+
contained a valid bitcoin address. According to RFC3986, the path component is valid, but it is
200+
not according to the [`bitcoin` specification](https://en.bitcoin.it/wiki/BIP_0021#Accessibility_(URI_scheme_name)).
200201

201202
The list of supported URI scheme can be found on the [UriScheme](https://github.com/thephpleague/uri-src/blob/master/uri/UriScheme.php) Enum
202203
available since version `7.6`.
@@ -327,7 +328,7 @@ The algorithm used is defined by the [WHATWG URL Living standard](https://url.sp
327328

328329
~~~php
329330
echo Uri::new('https://uri.thephpleague.com/uri/6.0/info/')->getOrigin(); //display 'https://uri.thephpleague.com';
330-
echo Uri::new('blob:https://mozilla.org:443')->getOrigin(); //display 'https://mozilla.org'
331+
echo Uri::new('blob:null/700475de-c453-11f0-8de9-0242ac120002')->getOrigin(); //display 'https://mozilla.org'
331332
Uri::new('file:///usr/bin/php')->getOrigin(); //returns null
332333
Uri::new('data:text/plain,Bonjour%20le%20monde%21')->getOrigin(); //returns null
333334
~~~
@@ -409,7 +410,7 @@ any leading zeros.
409410

410411
~~~php
411412
<?php
412-
Uri::new('blob:http://xn--bb-bjab.be./path')
413+
Uri::new('blob:http://xn--bb-bjab.be./700475de-c453-11f0-8de9-0242ac120002')
413414
->isCrossOrigin('http://Bébé.BE./path'); // returns false
414415

415416
Uri::new('https://example.com/123')

uri/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ All Notable changes to `League\Uri` will be documented in this file
4848
- `Uri` host encoding compliance to RFC3986 is improved by supporting RFC3986 encoded URI properly
4949
- `Uri` parsing with strings started or ended with empty string are no longer allowed
5050
- `Uri` space are rawurlencoded.
51-
- `Uri` validates URN as per RFC8141
51+
- `Uri` validates `urn` as per [RFC 8141](https://datatracker.ietf.org/doc/html/rfc8141)
52+
- `Uri` validates `mailto` scheme as per [RFC 6068](https://datatracker.ietf.org/doc/html/rfc6068)
53+
- `Uri` validates `blob` scheme as per [Blob Definition](https://w3c.github.io/FileAPI/#url)
5254
- `Uri::getPath` no longer trim the leading slashes (the `Http` class which is a PSR-7 compliant class still do!)
5355

5456
### Deprecated

uri/Uri.php

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
use function base64_encode;
4848
use function basename;
4949
use function count;
50-
use function dd;
5150
use function dirname;
5251
use function explode;
5352
use function feof;
@@ -83,6 +82,7 @@
8382
use const FILTER_FLAG_IPV6;
8483
use const FILTER_NULL_ON_FAILURE;
8584
use const FILTER_VALIDATE_BOOLEAN;
85+
use const FILTER_VALIDATE_EMAIL;
8686
use const FILTER_VALIDATE_IP;
8787

8888
/**
@@ -936,6 +936,7 @@ private function assertValidState(): void
936936
$schemeType = $scheme->type();
937937
match ($scheme) {
938938
UriScheme::Blob => $this->isValidBlob(),
939+
UriScheme::Mailto => $this->isValidMailto(),
939940
UriScheme::Data,
940941
UriScheme::About,
941942
UriScheme::Javascript => $this->isUriWithSchemeAndPathOnly(),
@@ -1004,6 +1005,53 @@ private function isValidBlob(): bool
10041005
}
10051006
}
10061007

1008+
private function isValidMailto(): bool
1009+
{
1010+
if (null !== $this->authority || null !== $this->fragment || str_contains((string) $this->query, '?')) {
1011+
return false;
1012+
}
1013+
1014+
static $mailHeaders = [
1015+
'to', 'cc', 'bcc', 'reply-to', 'from', 'sender',
1016+
'resent-to', 'resent-cc', 'resent-bcc', 'resent-from', 'resent-sender',
1017+
'return-path', 'delivery-to', 'site-owner',
1018+
];
1019+
1020+
static $headerRegexp = '/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D';
1021+
$pairs = QueryString::parseFromValue($this->query);
1022+
$hasTo = false;
1023+
foreach ($pairs as [$name, $value]) {
1024+
$headerName = strtolower($name);
1025+
if (in_array($headerName, $mailHeaders, true)) {
1026+
if (null === $value || !self::validateEmailList($value)) {
1027+
return false;
1028+
}
1029+
1030+
if (!$hasTo && 'to' === $headerName) {
1031+
$hasTo = true;
1032+
}
1033+
continue;
1034+
}
1035+
1036+
if (1 !== preg_match($headerRegexp, (string) Encoder::decodeAll($name))) {
1037+
return false;
1038+
}
1039+
}
1040+
1041+
return '' === $this->path ? $hasTo : self::validateEmailList($this->path);
1042+
}
1043+
1044+
private static function validateEmailList(string $emails): bool
1045+
{
1046+
foreach (explode(',', $emails) as $email) {
1047+
if (false === filter_var((string) Encoder::decodeAll($email), FILTER_VALIDATE_EMAIL)) {
1048+
return false;
1049+
}
1050+
}
1051+
1052+
return '' !== $emails;
1053+
}
1054+
10071055
/**
10081056
* Sets the URI origin.
10091057
*

uri/UriTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,4 +1081,54 @@ public function test_it_can_return_a_unicode_string_for_the_uri(): void
10811081
self::assertSame('https://xn--bb-bjab.be', $uri->toAsciiString());
10821082
self::assertSame('https://bébé.be', $uri->toUnicodeString());
10831083
}
1084+
1085+
#[DataProvider('provideValidMailtoUri')]
1086+
public function test_it_can_validate_mailto_uri(string $uri): void
1087+
{
1088+
self::assertInstanceOf(Uri::class, Uri::parse($uri));
1089+
}
1090+
1091+
public static function provideValidMailtoUri(): iterable
1092+
{
1093+
yield 'basic email' => ['uri' => 'mailto:me@thephpleague.com'];
1094+
yield 'basic email with subject' => ['uri' => 'mailto:me@thephpleague.com?subject=Hello'];
1095+
yield 'basic email with body' => ['uri' => 'mailto:infobot@example.com?body=send%20current-issue'];
1096+
yield 'request to subscribe to a mailing list' => ['uri' => 'mailto:majordomo@example.com?body=subscribe%20bamboo-l'];
1097+
yield 'email including a cc' => ['uri' => 'mailto:joe@example.com?cc=bob@example.com&body=hello'];
1098+
yield 'email without path but a to query string name' => ['uri' => 'mailto:?to=bob@example.com&body=hello'];
1099+
yield 'email without path but a to query string name case insensitive' => ['uri' => 'mailto:?To=bob@example.com&body=hello'];
1100+
yield 'complex email are also supported' => ['uri' => "mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org"];
1101+
}
1102+
1103+
#[DataProvider('provideInvalidMailtoUri')]
1104+
public function test_it_can_not_validate_mailto_uri(string $uri): void
1105+
{
1106+
self::assertNull(Uri::parse($uri));
1107+
}
1108+
1109+
public static function provideInvalidMailtoUri(): iterable
1110+
{
1111+
yield 'path does not contain a valid email' => ['uri' => 'mailto:joe'];
1112+
yield 'no path and no to query' => ['uri' => 'mailto:?subject=Hello'];
1113+
yield 'a valid email is missing with the to parameter' => ['uri' => 'mailto:?to=Hello'];
1114+
yield 'email query can not contains the "?" character' => ['uri' => 'mailto:joe@example.com?cc=bob@example.com?body=hello'];
1115+
}
1116+
1117+
public function test_it_can_edit_a_mailto_uri(): void
1118+
{
1119+
$uri = Uri::new('?Reply-To=me@example.com')
1120+
->withPath('you@example.com')
1121+
->withScheme('mailto')
1122+
->toString();
1123+
1124+
self::assertSame('mailto:you@example.com?Reply-To=me@example.com', $uri);
1125+
}
1126+
1127+
public function test_it_fails_to_edit_a_mailto_uri_in_the_wrong_order(): void
1128+
{
1129+
$this->expectException(SyntaxError::class);
1130+
1131+
Uri::new('?Reply-To=me@example.com')->withScheme('mailto');
1132+
1133+
}
10841134
}

uri/WsTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use PHPUnit\Framework\Attributes\TestWith;
1919
use PHPUnit\Framework\TestCase;
2020

21-
#[CoversClass(\League\Uri\Uri::class)]
21+
#[CoversClass(Uri::class)]
2222
#[Group('ws')]
2323
#[Group('uri')]
2424
class WsTest extends TestCase

0 commit comments

Comments
 (0)