Skip to content

Commit 1cb3f00

Browse files
committed
Fix signed URls with transformations
1 parent bf79676 commit 1cb3f00

File tree

3 files changed

+37
-34
lines changed

3 files changed

+37
-34
lines changed

psalm-baseline.xml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,44 @@
44
<DocblockTypeContradiction>
55
<code><![CDATA[is_resource($contents)]]></code>
66
</DocblockTypeContradiction>
7+
<InvalidArgument>
8+
<code><![CDATA[[...$options, 'expiresIn' => max(0, (int) now()->diffInSeconds($expiration))]]]></code>
9+
</InvalidArgument>
710
<MixedArgument>
8-
<code><![CDATA[$url_options]]></code>
9-
<code><![CDATA[$url_options]]></code>
1011
<code><![CDATA[$itemMetadata]]></code>
1112
<code><![CDATA[$itemMetadata]]></code>
1213
<code><![CDATA[$itemMetadata['mimetype'] ?? null]]></code>
1314
<code><![CDATA[$itemMetadata['size'] ?? null]]></code>
1415
<code><![CDATA[$mimeType]]></code>
15-
<code><![CDATA[$options['transform']]]></code>
1616
<code><![CDATA[$response->json('signedURL')]]></code>
1717
<code><![CDATA[$response->json()]]></code>
1818
<code><![CDATA[$url]]></code>
1919
<code><![CDATA[$url]]></code>
2020
<code><![CDATA[Arr::get($item, 'metadata.size')]]></code>
2121
</MixedArgument>
22+
<MixedArgumentTypeCoercion>
23+
<code><![CDATA[$urlOptions]]></code>
24+
</MixedArgumentTypeCoercion>
2225
<MixedArrayAccess>
2326
<code><![CDATA[$itemMetadata['lastModified']]]></code>
2427
<code><![CDATA[$itemMetadata['mimetype']]]></code>
2528
<code><![CDATA[$itemMetadata['size']]]></code>
2629
</MixedArrayAccess>
2730
<MixedAssignment>
28-
<code><![CDATA[$defaultUrlGeneration]]></code>
29-
<code><![CDATA[$url_options]]></code>
31+
<code><![CDATA[$download]]></code>
3032
<code><![CDATA[$endpoint]]></code>
3133
<code><![CDATA[$itemMetadata]]></code>
3234
<code><![CDATA[$lastModified]]></code>
3335
<code><![CDATA[$lastModifiedRaw]]></code>
3436
<code><![CDATA[$mimeType]]></code>
35-
<code><![CDATA[$options['expiresIn']]]></code>
3637
<code><![CDATA[$this->bucket]]></code>
3738
<code><![CDATA[$this->key]]></code>
3839
<code><![CDATA[$url]]></code>
3940
<code><![CDATA[$url]]></code>
4041
</MixedAssignment>
4142
<MixedOperand>
42-
<code><![CDATA[$defaultUrlGeneration]]></code>
4343
<code><![CDATA[$endpoint]]></code>
44+
<code><![CDATA[$this->config->get('public', true)]]></code>
4445
<code><![CDATA[$this->key]]></code>
4546
</MixedOperand>
4647
<MixedReturnTypeCoercion>

src/SupabaseAdapter.php

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,21 @@
2727

2828
/**
2929
* @see https://supabase.com/docs/guides/storage/s3/compatibility
30+
* @see https://github.com/supabase/storage-js/blob/main/src/lib/types.ts
3031
*
3132
* Image Transformations:
3233
* @see https://supabase.com/docs/guides/storage/serving/image-transformations#transformation-options
33-
* @psalm-type ImageTransformationOptions = array{
34+
* @psalm-type TransformOptions = array{
3435
* width?: positive-int,
3536
* height?: positive-int,
3637
* resize?: 'contain'|'cover'|'fill',
37-
* format?: 'origin'|'avif',
3838
* quality?: int<20, 100>,
39+
* format?: 'origin'|'avif',
40+
* }
41+
* @psalm-type UrlParameters = array{
42+
* download?: string|bool,
43+
* transform?: TransformOptions,
44+
* expiresIn?: int,
3945
* }
4046
*/
4147
final class SupabaseAdapter implements FilesystemAdapter
@@ -110,7 +116,7 @@ public function write(string $path, string $contents, Config $config): void
110116
throw UnableToWriteFile::atLocation($path, $res->body());
111117
}
112118

113-
// Delete empty placeholder file if not specified directly
119+
// Delete an empty placeholder file if not specified directly
114120
$filename = pathinfo($path, PATHINFO_BASENAME);
115121
if ($filename !== self::EMPTY_FOLDER_PLACEHOLDER_NAME) {
116122
$dirname = pathinfo($path, PATHINFO_DIRNAME);
@@ -431,46 +437,41 @@ public function getUrl(string $path): string
431437

432438
/**
433439
* @internal
434-
* @param array{expiresIn?: int, transform?: ImageTransformationOptions, download?: bool|string, ...} $options
440+
* @see https://supabase.com/docs/guides/storage/serving/image-transformations?queryGroups=language&language=js#signing-urls-with-transformation-options
441+
* @psalm-param UrlParameters $options
435442
* @throws UnableToGenerateTemporaryUrl
436443
*/
437444
public function getSignedUrl(string $path, array $options = []): string
438445
{
439-
$options['expiresIn'] ??= $this->config->get('signed_url_ttl,', 3_600);
440-
$_queryString = '';
446+
$url = $this->config->get('url', $this->endpoint);
441447

442-
$transformOptions = [];
443-
if (isset($options['transform'])) {
444-
$transformOptions = array_merge($transformOptions, $options['transform']);
445-
unset($options['transform']);
446-
}
448+
$apiBody = [
449+
'expiresIn' => $options['expiresIn'] ?? $this->config->get('signed_url_ttl,', 3_600),
450+
];
447451

448-
if (Arr::get($options, 'download')) {
449-
$_queryString = '&download';
450-
unset($options['download']);
452+
if (isset($options['transform'])) {
453+
$apiBody['transform'] = $options['transform'];
451454
}
452455

453-
$response = $this->httpClient->post(sprintf('/object/sign/%s/%s', $this->bucket, $path), $options);
456+
/** @see https://supabase.github.io/storage/#/object/post_object_sign__bucketName___wildcard_ */
457+
$response = $this->httpClient->post(sprintf('/object/sign/%s/%s', $this->bucket, $path), $apiBody);
454458
if (!$response->successful() || $response->json('signedURL') === null) {
455459
throw new UnableToGenerateTemporaryUrl($response->body(), $path);
456460
}
457461

458-
$url = $this->config->get('url', $this->endpoint);
459462
$signedUrl = $this->joinPaths($url, $response->json('signedURL'));
460463

461-
$transformJson = json_encode($transformOptions);
462-
if ($transformJson === false) {
463-
throw new UnableToGenerateTemporaryUrl('Failed to encode transform options', $path);
464+
if ($download = Arr::get($options, 'download')) {
465+
$signedUrl .= (str_contains($signedUrl, '?') ? '&' : '?').'download'.(is_string($download) ? "=$download" : '');
464466
}
465467

466-
$signedUrl .= (str_contains($signedUrl, '?') ? '&' : '?').'transform='.$transformJson;
467-
468-
return urldecode($signedUrl.$_queryString);
468+
return urldecode($signedUrl);
469469
}
470470

471471
/**
472472
* @internal
473-
* @param array{transform?: ImageTransformationOptions, download?: bool|string, ...} $options
473+
* @see https://supabase.com/docs/guides/storage/serving/image-transformations?queryGroups=language&language=js#get-a-public-url-for-a-transformed-image
474+
* @pslam-param UrlParameters $options
474475
* @throws \RuntimeException
475476
*/
476477
public function getPublicUrl(string $path, array $options = []): string
@@ -506,7 +507,7 @@ public function getPublicUrl(string $path, array $options = []): string
506507
/**
507508
* Laravel's magic method.
508509
* Used by {@see \Illuminate\Filesystem\FilesystemAdapter::temporaryUrl}
509-
* @param array<mixed> $options
510+
* @param UrlParameters $options
510511
* @api
511512
*/
512513
public function getTemporaryUrl(string $path, \DateTimeInterface $expiration, array $options): string

tests/Feature/UrlGenerationFeatureTest.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public function it_gets_signed_url(): void
7979
$result = $adapter->getSignedUrl('test-file.txt');
8080

8181
// Assert the result
82-
$this->assertSame('https://example.com/signed-url?transform={"format":"origin"}', $result);
82+
$this->assertSame('https://example.com/signed-url', $result);
8383

8484
// Assert the HTTP request was made
8585
Http::assertSent(static fn(Request $request): bool => $request->method() === 'POST' &&
@@ -104,10 +104,11 @@ public function it_gets_signed_url_with_options(): void
104104
$result = $adapter->getSignedUrl('test-file.txt', [
105105
'expiresIn' => 1800,
106106
'download' => true,
107+
'transform' => ['width' => 100],
107108
]);
108109

109110
// Assert the result
110-
$this->assertSame('https://example.com/signed-url?transform={"format":"origin"}&download', $result);
111+
$this->assertSame('https://example.com/signed-url?download', $result);
111112

112113
// Assert the HTTP request was made
113114
Http::assertSent(static fn(Request $request): bool => $request->method() === 'POST' &&
@@ -150,7 +151,7 @@ public function it_gets_temporary_url(): void
150151
$result = $adapter->getTemporaryUrl('test-file.txt', now()->addHour(), []);
151152

152153
// Assert the result
153-
$this->assertSame('https://example.com/signed-url?transform={"format":"origin"}', $result);
154+
$this->assertSame('https://example.com/signed-url', $result);
154155

155156
// Assert the HTTP request was made
156157
Http::assertSent(static fn(Request $request): bool => $request->method() === 'POST' &&

0 commit comments

Comments
 (0)