From 332513e360760b0735a545f09f4218a6fdd5341d Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Mon, 4 May 2026 14:59:57 -0300 Subject: [PATCH] refactor: replace regex/preg_match with sscanf + ends_with in chunked upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regex pieces in StoreChunkedAssetRequest, both replaced by more fitting tools: 1. Parsing 'Content-Range' header — preg_match with capturing groups and $matches[1..3] indexing replaced by sscanf, which is purpose-built for parsing structured strings with a known format: $parsed = sscanf($header, 'bytes %d-%d/%d') ?: []; $start = $parsed[0] ?? null; No regex, no $matches array, no manual int casts (sscanf returns ints for %d). 2. Validating filename suffix — regex:/\.(jpe?g|png|gif|webp|mp4)$/i replaced by Laravel's built-in 'ends_with' rule, with extensions derived from the MediaType enum: $allowedSuffixes = collect([Image, Video]) ->flatMap(fn (MediaType $t) => $t->extensions()) ->map(fn ($ext) => '.'.$ext) ->all(); For the case-insensitive part, normalize the filename to lowercase in prepareForValidation so 'IMG_1234.JPG' matches '.jpg' naturally. Added MediaType::extensions() — single source of truth for filename suffixes (jpg/jpeg/png/gif/webp + mp4/mov/webm). Mirrors allowedMimeTypes() for callers that validate by name instead of MIME. --- app/Enums/Media/Type.php | 15 +++++++++++ .../App/Asset/StoreChunkedAssetRequest.php | 25 +++++++++++-------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/app/Enums/Media/Type.php b/app/Enums/Media/Type.php index a06e420a..c916501e 100644 --- a/app/Enums/Media/Type.php +++ b/app/Enums/Media/Type.php @@ -28,6 +28,21 @@ public function allowedMimeTypes(): array }; } + /** + * Filename extensions that match this type. Used by callers that + * validate by name (chunked upload, URL fetch fallback) instead of + * by MIME. + * + * @return array + */ + public function extensions(): array + { + return match ($this) { + self::Image => ['jpg', 'jpeg', 'png', 'gif', 'webp'], + self::Video => ['mp4', 'mov', 'webm'], + }; + } + public function maxSizeInMb(): int { return (int) config("trypost.media.max_size_mb.{$this->value}"); diff --git a/app/Http/Requests/App/Asset/StoreChunkedAssetRequest.php b/app/Http/Requests/App/Asset/StoreChunkedAssetRequest.php index 7a0975ab..84ebf16f 100644 --- a/app/Http/Requests/App/Asset/StoreChunkedAssetRequest.php +++ b/app/Http/Requests/App/Asset/StoreChunkedAssetRequest.php @@ -22,17 +22,15 @@ public function authorize(): bool protected function prepareForValidation(): void { - preg_match( - '/bytes (\d+)-(\d+)\/(\d+)/', - (string) $this->header('Content-Range'), - $matches, - ); + $parsed = sscanf((string) $this->header('Content-Range'), 'bytes %d-%d/%d') ?: []; $this->merge([ - 'range_start' => isset($matches[1]) ? (int) $matches[1] : null, - 'range_end' => isset($matches[2]) ? (int) $matches[2] : null, - 'total_size' => isset($matches[3]) ? (int) $matches[3] : null, - 'file_name' => $this->header('X-File-Name', 'upload'), + 'range_start' => $parsed[0] ?? null, + 'range_end' => $parsed[1] ?? null, + 'total_size' => $parsed[2] ?? null, + // Lowercase the name so `ends_with` validation is effectively + // case-insensitive (IMG_1234.JPG vs img_1234.jpg). + 'file_name' => strtolower((string) $this->header('X-File-Name', 'upload')), ]); } @@ -41,11 +39,16 @@ protected function prepareForValidation(): void */ public function rules(): array { + $allowedSuffixes = collect([MediaType::Image, MediaType::Video]) + ->flatMap(fn (MediaType $type) => $type->extensions()) + ->map(fn (string $ext) => '.'.$ext) + ->all(); + return [ 'range_start' => ['required', 'integer', 'min:0'], 'range_end' => ['required', 'integer', 'gte:range_start'], 'total_size' => ['required', 'integer', 'min:1', 'max:'.MediaType::Video->maxSizeInBytes()], - 'file_name' => ['required', 'string', 'regex:/\.(jpe?g|png|gif|webp|mp4)$/i'], + 'file_name' => ['required', 'string', 'ends_with:'.implode(',', $allowedSuffixes)], ]; } @@ -59,7 +62,7 @@ public function messages(): array 'range_end.required' => 'Invalid Content-Range header', 'total_size.required' => 'Invalid Content-Range header', 'total_size.max' => 'File size exceeds the maximum allowed ('.MediaType::Video->maxSizeInMb().' MB).', - 'file_name.regex' => 'File type not supported.', + 'file_name.ends_with' => 'File type not supported.', ]; } }