Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e6a9d9c

Browse filesBrowse files
committed
[HttpFoundation] Add UriSigner::signAndWrap() and UriSigner::verify()
1 parent 79fa5f2 commit e6a9d9c
Copy full SHA for e6a9d9c

File tree

Expand file treeCollapse file tree

5 files changed

+186
-26
lines changed
Filter options
Expand file treeCollapse file tree

5 files changed

+186
-26
lines changed
+26Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Exception;
13+
14+
/**
15+
* @author Kevin Bond <kevinbond@gmail.com>
16+
*/
17+
final class ExpiredSignedUriException extends SignedUriException
18+
{
19+
public function __construct(
20+
public readonly \DateTimeImmutable $expiredAt,
21+
string $uri,
22+
string $message,
23+
) {
24+
parent::__construct($uri, $message);
25+
}
26+
}
+25Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Exception;
13+
14+
/**
15+
* @author Kevin Bond <kevinbond@gmail.com>
16+
*/
17+
abstract class SignedUriException extends \RuntimeException implements ExceptionInterface
18+
{
19+
public function __construct(
20+
public readonly string $uri,
21+
string $message,
22+
) {
23+
parent::__construct($message);
24+
}
25+
}
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Exception;
13+
14+
/**
15+
* @author Kevin Bond <kevinbond@gmail.com>
16+
*/
17+
final class UnverifiedSignedUriException extends SignedUriException
18+
{
19+
}
+34Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation;
13+
14+
/**
15+
* @author Kevin Bond <kevinbond@gmail.com>
16+
*/
17+
final class SignedUri implements \Stringable
18+
{
19+
public function __construct(
20+
public readonly string $value,
21+
private ?string $expiration = null,
22+
) {
23+
}
24+
25+
public function __toString(): string
26+
{
27+
return $this->value;
28+
}
29+
30+
public function expiresAt(): ?\DateTimeImmutable
31+
{
32+
return null === $this->expiration ? null : \DateTimeImmutable::createFromFormat('U', $this->expiration);
33+
}
34+
}

‎src/Symfony/Component/HttpFoundation/UriSigner.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/UriSigner.php
+82-26Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111

1212
namespace Symfony\Component\HttpFoundation;
1313

14+
use Symfony\Component\HttpFoundation\Exception\ExpiredSignedUriException;
1415
use Symfony\Component\HttpFoundation\Exception\LogicException;
16+
use Symfony\Component\HttpFoundation\Exception\SignedUriException;
17+
use Symfony\Component\HttpFoundation\Exception\UnverifiedSignedUriException;
1518

1619
/**
1720
* @author Fabien Potencier <fabien@symfony.com>
@@ -33,7 +36,7 @@ public function __construct(
3336
}
3437

3538
/**
36-
* Signs a URI.
39+
* Signs a URI and returns a SignedUri object.
3740
*
3841
* The given URI is signed by adding the query string parameter
3942
* which value depends on the URI and the secret.
@@ -46,18 +49,8 @@ public function __construct(
4649
*
4750
* The expiration is added as a query string parameter.
4851
*/
49-
public function sign(string $uri/* , \DateTimeInterface|\DateInterval|int|null $expiration = null */): string
52+
public function signAndWrap(string $uri, \DateTimeInterface|\DateInterval|int|null $expiration = null): SignedUri
5053
{
51-
$expiration = null;
52-
53-
if (1 < \func_num_args()) {
54-
$expiration = func_get_arg(1);
55-
}
56-
57-
if (null !== $expiration && !$expiration instanceof \DateTimeInterface && !$expiration instanceof \DateInterval && !\is_int($expiration)) {
58-
throw new \TypeError(\sprintf('The second argument of "%s()" must be an instance of "%s" or "%s", an integer or null (%s given).', __METHOD__, \DateTimeInterface::class, \DateInterval::class, get_debug_type($expiration)));
59-
}
60-
6154
$url = parse_url($uri);
6255
$params = [];
6356

@@ -74,13 +67,42 @@ public function sign(string $uri/* , \DateTimeInterface|\DateInterval|int|null $
7467
}
7568

7669
if (null !== $expiration) {
77-
$params[$this->expirationParameter] = $this->getExpirationTime($expiration);
70+
$params[$this->expirationParameter] = $expiration = $this->getExpirationTime($expiration);
7871
}
7972

8073
$uri = $this->buildUrl($url, $params);
8174
$params[$this->hashParameter] = $this->computeHash($uri);
8275

83-
return $this->buildUrl($url, $params);
76+
return new SignedUri($this->buildUrl($url, $params), $expiration);
77+
}
78+
79+
/**
80+
* Signs a URI.
81+
*
82+
* The given URI is signed by adding the query string parameter
83+
* which value depends on the URI and the secret.
84+
*
85+
* @param \DateTimeInterface|\DateInterval|int|null $expiration The expiration for the given URI.
86+
* If $expiration is a \DateTimeInterface, it's expected to be the exact date + time.
87+
* If $expiration is a \DateInterval, the interval is added to "now" to get the date + time.
88+
* If $expiration is an int, it's expected to be a timestamp in seconds of the exact date + time.
89+
* If $expiration is null, no expiration.
90+
*
91+
* The expiration is added as a query string parameter.
92+
*/
93+
public function sign(string $uri/* , \DateTimeInterface|\DateInterval|int|null $expiration = null */): string
94+
{
95+
$expiration = null;
96+
97+
if (1 < \func_num_args()) {
98+
$expiration = func_get_arg(1);
99+
}
100+
101+
if (null !== $expiration && !$expiration instanceof \DateTimeInterface && !$expiration instanceof \DateInterval && !\is_int($expiration)) {
102+
throw new \TypeError(\sprintf('The second argument of "%s()" must be an instance of "%s" or "%s", an integer or null (%s given).', __METHOD__, \DateTimeInterface::class, \DateInterval::class, get_debug_type($expiration)));
103+
}
104+
105+
return (string) $this->signAndWrap($uri, $expiration);
84106
}
85107

86108
/**
@@ -89,6 +111,40 @@ public function sign(string $uri/* , \DateTimeInterface|\DateInterval|int|null $
89111
*/
90112
public function check(string $uri): bool
91113
{
114+
try {
115+
$this->verify($uri);
116+
} catch (SignedUriException) {
117+
return false;
118+
}
119+
120+
return true;
121+
}
122+
123+
public function checkRequest(Request $request): bool
124+
{
125+
try {
126+
$this->verify($request);
127+
} catch (SignedUriException) {
128+
return false;
129+
}
130+
131+
return true;
132+
}
133+
134+
/**
135+
* Verify a Request/string URI and return a SignedUri object.
136+
*
137+
* @throws UnverifiedSignedUriException If the URI is not signed or the signature is invalid
138+
* @throws ExpiredSignedUriException If the URI has expired
139+
* @throws SignedUriException
140+
*/
141+
public function verify(Request|string $uri): SignedUri
142+
{
143+
if ($uri instanceof Request) {
144+
$qs = ($qs = $uri->server->get('QUERY_STRING')) ? '?'.$qs : '';
145+
$uri = $uri->getSchemeAndHttpHost().$uri->getBaseUrl().$uri->getPathInfo().$qs;
146+
}
147+
92148
$url = parse_url($uri);
93149
$params = [];
94150

@@ -97,30 +153,30 @@ public function check(string $uri): bool
97153
}
98154

99155
if (empty($params[$this->hashParameter])) {
100-
return false;
156+
throw new UnverifiedSignedUriException($uri, 'The URI is not signed.');
101157
}
102158

103159
$hash = $params[$this->hashParameter];
104160
unset($params[$this->hashParameter]);
105161

106162
// In 8.0, remove support for non-url-safe tokens
107163
if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), strtr(rtrim($hash, '='), ['/' => '_', '+' => '-']))) {
108-
return false;
164+
throw new UnverifiedSignedUriException($uri, 'The URI signature is invalid.');
109165
}
110166

111-
if ($expiration = $params[$this->expirationParameter] ?? false) {
112-
return time() < $expiration;
167+
if (!$expiration = $params[$this->expirationParameter] ?? false) {
168+
return new SignedUri($uri);
113169
}
114170

115-
return true;
116-
}
117-
118-
public function checkRequest(Request $request): bool
119-
{
120-
$qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : '';
171+
if (time() < $expiration) {
172+
return new SignedUri($uri, $expiration);
173+
}
121174

122-
// we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering)
123-
return $this->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$qs);
175+
throw new ExpiredSignedUriException(
176+
\DateTimeImmutable::createFromFormat('U', $expiration),
177+
$uri,
178+
'The URI has expired.',
179+
);
124180
}
125181

126182
private function computeHash(string $uri): string

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.