Skip to content

Commit bc9b5c0

Browse files
authored
add signedUrl method
1 parent 7bb0f55 commit bc9b5c0

File tree

3 files changed

+140
-0
lines changed

3 files changed

+140
-0
lines changed

src/Illuminate/Contracts/Routing/UrlGenerator.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ public function route($name, $parameters = [], $absolute = true);
7272
*/
7373
public function signedRoute($name, $parameters = [], $expiration = null, $absolute = true);
7474

75+
/**
76+
* Create a signed URL for a raw url.
77+
*
78+
* @param string $url
79+
* @param \DateTimeInterface|\DateInterval|int|null $expiration
80+
* @return string
81+
*
82+
* @throws \InvalidArgumentException
83+
*/
84+
public function signedUrl($url, $expiration = null);
85+
7586
/**
7687
* Create a temporary signed route URL for a named route.
7788
*

src/Illuminate/Routing/UrlGenerator.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Illuminate\Support\InteractsWithTime;
1414
use Illuminate\Support\Str;
1515
use Illuminate\Support\Traits\Macroable;
16+
use Illuminate\Support\Uri;
1617
use InvalidArgumentException;
1718
use Symfony\Component\Routing\Exception\RouteNotFoundException;
1819

@@ -378,6 +379,40 @@ public function signedRoute($name, $parameters = [], $expiration = null, $absolu
378379
], $absolute);
379380
}
380381

382+
/**
383+
* Create a signed URL for a raw url.
384+
*
385+
* @param string $url
386+
* @param \DateTimeInterface|\DateInterval|int|null $expiration
387+
* @return string
388+
*
389+
* @throws \InvalidArgumentException
390+
*/
391+
public function signedUrl($url, $expiration = null)
392+
{
393+
$uri = Uri::of($url);
394+
395+
$parameters = $uri->query()->all();
396+
397+
$this->ensureSignedRouteParametersAreNotReserved($parameters);
398+
399+
if ($expiration) {
400+
$parameters = $parameters + ['expires' => $this->availableAt($expiration)];
401+
}
402+
403+
ksort($parameters);
404+
405+
$key = call_user_func($this->keyResolver);
406+
407+
return $uri->replaceQuery($parameters + [
408+
'signature' => hash_hmac(
409+
'sha256',
410+
$uri->replaceQuery($parameters),
411+
is_array($key) ? $key[0] : $key
412+
),
413+
])->value();
414+
}
415+
381416
/**
382417
* Ensure the given signed route parameters are not reserved.
383418
*

tests/Routing/RoutingUrlGeneratorTest.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Illuminate\Routing\Route;
1010
use Illuminate\Routing\RouteCollection;
1111
use Illuminate\Routing\UrlGenerator;
12+
use Illuminate\Support\Carbon;
1213
use InvalidArgumentException;
1314
use PHPUnit\Framework\Attributes\DataProvider;
1415
use PHPUnit\Framework\TestCase;
@@ -938,6 +939,99 @@ public function testSignedUrlWithKeyResolver()
938939
$this->assertTrue($url3->hasValidSignature($secondRequest));
939940
}
940941

942+
public function testSignedUrlWithRawUrl()
943+
{
944+
$url = new UrlGenerator(
945+
new RouteCollection,
946+
Request::create('http://www.foo.com/')
947+
);
948+
$url->setKeyResolver(function () {
949+
return 'secret';
950+
});
951+
952+
$signedUrl = $url->signedUrl('http://www.foo.com/test');
953+
$this->assertStringContainsString('signature=', $signedUrl);
954+
955+
$request = Request::create($signedUrl);
956+
$this->assertTrue($url->hasValidSignature($request));
957+
958+
$request = Request::create($signedUrl.'&tampered=true');
959+
$this->assertFalse($url->hasValidSignature($request));
960+
}
961+
962+
public function testSignedUrlWithExistingQueryParameters()
963+
{
964+
$url = new UrlGenerator(
965+
new RouteCollection,
966+
Request::create('http://www.foo.com/')
967+
);
968+
$url->setKeyResolver(function () {
969+
return 'secret';
970+
});
971+
972+
$signedUrl = $url->signedUrl('http://www.foo.com/test?param1=value1&param2=value2');
973+
$this->assertStringContainsString('signature=', $signedUrl);
974+
$this->assertStringContainsString('param1=value1', $signedUrl);
975+
$this->assertStringContainsString('param2=value2', $signedUrl);
976+
977+
$request = Request::create($signedUrl);
978+
$this->assertTrue($url->hasValidSignature($request));
979+
}
980+
981+
public function testSignedUrlWithExpiration()
982+
{
983+
$url = new UrlGenerator(
984+
new RouteCollection,
985+
Request::create('http://www.foo.com/')
986+
);
987+
$url->setKeyResolver(function () {
988+
return 'secret';
989+
});
990+
991+
Carbon::setTestNow(Carbon::create(2025, 1, 1));
992+
$signedUrl = $url->signedUrl('http://www.foo.com/test', now()->addMinutes(5));
993+
$this->assertStringContainsString('signature=', $signedUrl);
994+
$this->assertStringContainsString('expires=', $signedUrl);
995+
996+
$request = Request::create($signedUrl);
997+
$this->assertTrue($url->hasValidSignature($request));
998+
999+
Carbon::setTestNow(Carbon::create(2025, 1, 1)->addMinutes(10));
1000+
$this->assertFalse($url->hasValidSignature($request));
1001+
}
1002+
1003+
public function testSignedUrlWithReservedSignatureParameter()
1004+
{
1005+
$url = new UrlGenerator(
1006+
new RouteCollection,
1007+
Request::create('http://www.foo.com/')
1008+
);
1009+
$url->setKeyResolver(function () {
1010+
return 'secret';
1011+
});
1012+
1013+
$this->expectException(InvalidArgumentException::class);
1014+
$this->expectExceptionMessage('reserved');
1015+
1016+
$url->signedUrl('http://www.foo.com/test?signature=tampered');
1017+
}
1018+
1019+
public function testSignedUrlWithReservedExpiresParameter()
1020+
{
1021+
$url = new UrlGenerator(
1022+
new RouteCollection,
1023+
Request::create('http://www.foo.com/')
1024+
);
1025+
$url->setKeyResolver(function () {
1026+
return 'secret';
1027+
});
1028+
1029+
$this->expectException(InvalidArgumentException::class);
1030+
$this->expectExceptionMessage('reserved');
1031+
1032+
$url->signedUrl('http://www.foo.com/test?expires=123456', now()->addMinutes(5));
1033+
}
1034+
9411035
public function testMissingNamedRouteResolution()
9421036
{
9431037
$url = new UrlGenerator(

0 commit comments

Comments
 (0)