← All changes
|
app/Services/FileSystem/Drivers/S3/S3FileUploader.php
+21
-6
1.6.2
→
1.6.5
View file →
| @@ -3,9 +3,8 @@ | ||
| 3 | 3 | namespace FluentCart\App\Services\FileSystem\Drivers\S3; |
| 4 | 4 | |
| 5 | 5 | use Exception; |
| 6 | 6 | use FluentCart\App\Modules\StorageDrivers\S3\S3; |
| 7 | -use FluentCart\Framework\Support\Str; | |
| 8 | 7 | use WP_Error; |
| 9 | 8 | |
| 10 | 9 | class S3FileUploader |
| 11 | 10 | { |
| @@ -48,15 +47,16 @@ | ||
| 48 | 47 | // ✅ Correct Regional Endpoint |
| 49 | 48 | // $this->requestUrl = "https://{$this->bucket}.s3.{$this->region}.amazonaws.com/{$this->s3FilePath}"; |
| 50 | 49 | |
| 51 | 50 | $hasDot = strpos($this->bucket, '.') !== false; |
| 51 | + $encodedFilePath = $this->encodeS3ObjectKey($this->s3FilePath); | |
| 52 | 52 | |
| 53 | 53 | if ($hasDot) { |
| 54 | 54 | // Path-style URL |
| 55 | - $this->requestUrl = "https://s3.{$this->region}.amazonaws.com/{$this->bucket}/{$this->s3FilePath}"; | |
| 55 | + $this->requestUrl = "https://s3.{$this->region}.amazonaws.com/{$this->bucket}/{$encodedFilePath}"; | |
| 56 | 56 | } else { |
| 57 | 57 | // Virtual-hosted style |
| 58 | - $this->requestUrl = "https://{$this->bucket}.s3.{$this->region}.amazonaws.com/{$this->s3FilePath}"; | |
| 58 | + $this->requestUrl = "https://{$this->bucket}.s3.{$this->region}.amazonaws.com/{$encodedFilePath}"; | |
| 59 | 59 | } |
| 60 | 60 | |
| 61 | 61 | // Opt-in: when enabled the upload is refused instead of replacing an |
| 62 | 62 | // existing object. Resolved before signing because the header is part |
| @@ -151,16 +151,31 @@ | ||
| 151 | 151 | return hash($this->hashAlgorithm, file_get_contents($this->localFilePath)); |
| 152 | 152 | } |
| 153 | 153 | |
| 154 | 154 | /** |
| 155 | + * Percent-encodes each "/"-separated segment of an S3 object key using | |
| 156 | + * AWS's UriEncode rules (rawurlencode leaves "/" alone). This keeps the | |
| 157 | + * canonical signing path and the actual request URL identical to what | |
| 158 | + * S3 receives on the wire, so multi-byte UTF-8 characters, spaces, and | |
| 159 | + * reserved characters ("+", "#", "?", literal "%") all round-trip to | |
| 160 | + * the exact key that was requested instead of a different object. | |
| 161 | + * | |
| 162 | + * Deliberately does not ltrim() leading slashes: "foo", "/foo", and | |
| 163 | + * "//foo" are three distinct S3 keys, and stripping the slash made a | |
| 164 | + * delete/upload targeting "/foo" silently operate on "foo" instead. | |
| 165 | + */ | |
| 166 | + private function encodeS3ObjectKey(string $path): string | |
| 167 | + { | |
| 168 | + return implode('/', array_map('rawurlencode', explode('/', $path))); | |
| 169 | + } | |
| 170 | + | |
| 171 | + /** | |
| 155 | 172 | * @throws Exception |
| 156 | 173 | */ |
| 157 | 174 | private function createCanonicalUrl(): string |
| 158 | 175 | { |
| 159 | 176 | // Ensure file path begins with / |
| 160 | - $s3FilePath = Str::startsWith($this->s3FilePath, '/') | |
| 161 | - ? $this->s3FilePath | |
| 162 | - : "/{$this->s3FilePath}"; | |
| 177 | + $s3FilePath = '/' . $this->encodeS3ObjectKey($this->s3FilePath); | |
| 163 | 178 | |
| 164 | 179 | $contentHash = $this->getContentHash(); |
| 165 | 180 | |
| 166 | 181 | // If bucket has dot, use path-style URL in canonical request |