|
12 | 12 | namespace Symfony\Component\HttpFoundation;
|
13 | 13 |
|
14 | 14 | use Psr\Clock\ClockInterface;
|
| 15 | +use Symfony\Component\HttpFoundation\Exception\ExpiredSignedUriException; |
15 | 16 | use Symfony\Component\HttpFoundation\Exception\LogicException;
|
| 17 | +use Symfony\Component\HttpFoundation\Exception\SignedUriException; |
| 18 | +use Symfony\Component\HttpFoundation\Exception\UnsignedUriException; |
| 19 | +use Symfony\Component\HttpFoundation\Exception\UnverifiedSignedUriException; |
16 | 20 |
|
17 | 21 | /**
|
18 | 22 | * @author Fabien Potencier <fabien@symfony.com>
|
19 | 23 | */
|
20 | 24 | class UriSigner
|
21 | 25 | {
|
| 26 | + private const STATUS_VALID = 1; |
| 27 | + private const STATUS_INVALID = 2; |
| 28 | + private const STATUS_MISSING = 3; |
| 29 | + private const STATUS_EXPIRED = 4; |
| 30 | + |
22 | 31 | /**
|
23 | 32 | * @param string $hashParameter Query string parameter to use
|
24 | 33 | * @param string $expirationParameter Query string parameter to use for expiration
|
@@ -91,38 +100,40 @@ public function sign(string $uri/* , \DateTimeInterface|\DateInterval|int|null $
|
91 | 100 | */
|
92 | 101 | public function check(string $uri): bool
|
93 | 102 | {
|
94 |
| - $url = parse_url($uri); |
95 |
| - $params = []; |
96 |
| - |
97 |
| - if (isset($url['query'])) { |
98 |
| - parse_str($url['query'], $params); |
99 |
| - } |
| 103 | + return self::STATUS_VALID === $this->doVerify($uri); |
| 104 | + } |
100 | 105 |
|
101 |
| - if (empty($params[$this->hashParameter])) { |
102 |
| - return false; |
103 |
| - } |
| 106 | + public function checkRequest(Request $request): bool |
| 107 | + { |
| 108 | + return self::STATUS_VALID === $this->doVerify(self::normalize($request)); |
| 109 | + } |
104 | 110 |
|
105 |
| - $hash = $params[$this->hashParameter]; |
106 |
| - unset($params[$this->hashParameter]); |
| 111 | + /** |
| 112 | + * Verify a Request or string URI. |
| 113 | + * |
| 114 | + * @throws UnsignedUriException If the URI is not signed |
| 115 | + * @throws UnverifiedSignedUriException If the signature is invalid |
| 116 | + * @throws ExpiredSignedUriException If the URI has expired |
| 117 | + * @throws SignedUriException |
| 118 | + */ |
| 119 | + public function verify(Request|string $uri): void |
| 120 | + { |
| 121 | + $uri = self::normalize($uri); |
| 122 | + $status = $this->doVerify($uri); |
107 | 123 |
|
108 |
| - // In 8.0, remove support for non-url-safe tokens |
109 |
| - if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), strtr(rtrim($hash, '='), ['/' => '_', '+' => '-']))) { |
110 |
| - return false; |
| 124 | + if (self::STATUS_VALID === $status) { |
| 125 | + return; |
111 | 126 | }
|
112 | 127 |
|
113 |
| - if ($expiration = $params[$this->expirationParameter] ?? false) { |
114 |
| - return $this->now()->getTimestamp() < $expiration; |
| 128 | + if (self::STATUS_MISSING === $status) { |
| 129 | + throw new UnsignedUriException(); |
115 | 130 | }
|
116 | 131 |
|
117 |
| - return true; |
118 |
| - } |
119 |
| - |
120 |
| - public function checkRequest(Request $request): bool |
121 |
| - { |
122 |
| - $qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : ''; |
| 132 | + if (self::STATUS_INVALID === $status) { |
| 133 | + throw new UnverifiedSignedUriException(); |
| 134 | + } |
123 | 135 |
|
124 |
| - // we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering) |
125 |
| - return $this->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$qs); |
| 136 | + throw new ExpiredSignedUriException(); |
126 | 137 | }
|
127 | 138 |
|
128 | 139 | private function computeHash(string $uri): string
|
@@ -165,4 +176,48 @@ private function now(): \DateTimeImmutable
|
165 | 176 | {
|
166 | 177 | return $this->clock?->now() ?? \DateTimeImmutable::createFromFormat('U', time());
|
167 | 178 | }
|
| 179 | + |
| 180 | + /** |
| 181 | + * @return self::STATUS_* |
| 182 | + */ |
| 183 | + private function doVerify(string $uri): int |
| 184 | + { |
| 185 | + $url = parse_url($uri); |
| 186 | + $params = []; |
| 187 | + |
| 188 | + if (isset($url['query'])) { |
| 189 | + parse_str($url['query'], $params); |
| 190 | + } |
| 191 | + |
| 192 | + if (empty($params[$this->hashParameter])) { |
| 193 | + return self::STATUS_MISSING; |
| 194 | + } |
| 195 | + |
| 196 | + $hash = $params[$this->hashParameter]; |
| 197 | + unset($params[$this->hashParameter]); |
| 198 | + |
| 199 | + if (!hash_equals($this->computeHash($this->buildUrl($url, $params)), strtr(rtrim($hash, '='), ['/' => '_', '+' => '-']))) { |
| 200 | + return self::STATUS_INVALID; |
| 201 | + } |
| 202 | + |
| 203 | + if (!$expiration = $params[$this->expirationParameter] ?? false) { |
| 204 | + return self::STATUS_VALID; |
| 205 | + } |
| 206 | + |
| 207 | + if ($this->now()->getTimestamp() < $expiration) { |
| 208 | + return self::STATUS_VALID; |
| 209 | + } |
| 210 | + |
| 211 | + return self::STATUS_EXPIRED; |
| 212 | + } |
| 213 | + |
| 214 | + private static function normalize(Request|string $uri): string |
| 215 | + { |
| 216 | + if ($uri instanceof Request) { |
| 217 | + $qs = ($qs = $uri->server->get('QUERY_STRING')) ? '?'.$qs : ''; |
| 218 | + $uri = $uri->getSchemeAndHttpHost().$uri->getBaseUrl().$uri->getPathInfo().$qs; |
| 219 | + } |
| 220 | + |
| 221 | + return $uri; |
| 222 | + } |
168 | 223 | }
|
0 commit comments