PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
← All changes | app/Services/FileSystem/Drivers/S3/S3Driver.php +74 -3 1.4.1 → 1.6.5 View file →
@@ -143,12 +143,14 @@
143 143 $credentialScope = "{$dateStamp}/{$this->region}/s3/aws4_request";
144 144
145 145 $hasDot = strpos($this->bucket, '.') !== false;
146 146
147 - // Canonical request components
147 + // Canonical request components. Deliberately does not ltrim() leading
148 + // slashes: "foo", "/foo", and "//foo" are distinct S3 keys, and
149 + // stripping the slash would redirect the customer to the wrong object.
148 150 $encodedFilePath = '/' . implode('/', array_map(
149 151 'rawurlencode',
150 - explode('/', ltrim($filePath, '/'))
152 + explode('/', $filePath)
151 153 ));
152 154
153 155 // For dotted buckets, use path-style: include bucket in canonical URI
154 156 if ($hasDot) {
@@ -167,9 +169,9 @@
167 169 'X-Amz-SignedHeaders' => 'host',
168 170 ];
169 171
170 172 if (!empty($fileName)) {
171 - $canonicalQueryString['response-content-disposition'] = 'attachment; filename="' . $fileName . '"';
173 + $canonicalQueryString['response-content-disposition'] = $this->buildContentDispositionValue($fileName);
172 174 }
173 175
174 176 // Sort query parameters
175 177 ksort($canonicalQueryString);
@@ -201,8 +203,77 @@
201 203 $url = "https://{$host}{$canonicalUri}?{$canonicalQueryStringStr}&X-Amz-Signature={$signature}";
202 204
203 205 return $url;
204 206 }
207 +
208 + /**
209 + * Builds an RFC 6266/RFC 5987-compliant Content-Disposition value.
210 + * HTTP header values must be ISO-8859-1, so a raw Unicode filename (e.g.
211 + * Bengali) in a plain filename="..." parameter makes S3 reject the
212 + * request with "Header value cannot be represented using ISO-8859-1."
213 + * This supplies both an ASCII-safe filename="..." fallback for clients
214 + * that don't understand filename*, and a filename*=UTF-8''... parameter
215 + * carrying the exact intended Unicode name that modern browsers use.
216 + *
217 + * The returned raw value is assigned into $canonicalQueryString as-is;
218 + * the existing per-parameter rawurlencode() loop further down encodes
219 + * it exactly once for the query string, so this method must not
220 + * pre-encode the value itself (only the filename* attr-value, which is
221 + * a distinct RFC 5987 encoding layer, not query-string encoding).
222 + */
223 + private function buildContentDispositionValue(string $fileName): string
224 + {
225 + // Strip control/CR-LF characters so neither the raw header value nor
226 + // the filename* payload can inject extra header content.
227 + $sanitized = preg_replace('/[\x00-\x1F\x7F]/', '', $fileName);
228 + if ($sanitized === null) {
229 + $sanitized = $fileName;
230 + }
231 +
232 + $asciiFileName = $this->buildAsciiFallbackFileName($sanitized);
233 + // RFC 6266 quoted-string: backslash-escape backslashes and quotes.
234 + $asciiFileName = addcslashes($asciiFileName, '\\"');
235 +
236 + $encodedFileName = rawurlencode($sanitized);
237 +
238 + return 'attachment; filename="' . $asciiFileName . '"; filename*=UTF-8\'\'' . $encodedFileName;
239 + }
240 +
241 + private function buildAsciiFallbackFileName(string $fileName): string
242 + {
243 + $extension = preg_replace('/[^\x20-\x7E]/', '', pathinfo($fileName, PATHINFO_EXTENSION));
244 +
245 + // Empty or dot-only names ("", ".", "..", "...") carry no meaningful
246 + // identity to preserve; keep the generic fallback for them.
247 + if (preg_match('/^\.*$/', $fileName)) {
248 + return $extension !== '' ? "download.{$extension}" : 'download';
249 + }
250 +
251 + // Already fully ASCII: preserve it exactly, including leading dots,
252 + // underscores, hyphens, and extension punctuation (e.g. ".env",
253 + // "_report_.txt", "report.a-b") - nothing here needs to change for
254 + // a client that ignores filename*.
255 + if (preg_replace('/[^\x20-\x7E]/', '', $fileName) === $fileName) {
256 + return $fileName;
257 + }
258 +
259 + // Otherwise the basename has non-ASCII content to strip. Anything
260 + // dropped here (Bengali, Japanese, Arabic, emoji…) is fully
261 + // preserved via filename*; legacy clients ignoring filename* never
262 + // see this fallback. Trim the separator characters (dots, dashes,
263 + // underscores, spaces) that content leaves dangling at the edges
264 + // once it's gone.
265 + $basename = pathinfo($fileName, PATHINFO_FILENAME);
266 + $basename = preg_replace('/[^\x20-\x7E]/', '', $basename);
267 + $basename = trim($basename, " \t\n\r\0\x0B-_.");
268 +
269 + if ($basename === '') {
270 + return $extension !== '' ? "download.{$extension}" : 'download';
271 + }
272 +
273 + return $extension !== '' ? "{$basename}.{$extension}" : $basename;
274 + }
275 +
205 276 protected function retrieveFileForDownload(string $downloadableFilePath, $bucket = null)
206 277 {
207 278 $this->bucket = $bucket;
208 279 $this->region = S3::getBucketRegion($this->bucket);