diff --git a/.env.example b/.env.example index c0660ea..7e2d925 100644 --- a/.env.example +++ b/.env.example @@ -37,6 +37,10 @@ BROADCAST_CONNECTION=log FILESYSTEM_DISK=local QUEUE_CONNECTION=database +CHAT_ATTACHMENT_MAX_FILES=5 +CHAT_ATTACHMENT_MAX_FILE_SIZE_KB=2097152 +CHAT_LIVEWIRE_MAX_UPLOAD_TIME=60 + CACHE_STORE=database # CACHE_PREFIX= diff --git a/app/Livewire/Chat/MessageComposer.php b/app/Livewire/Chat/MessageComposer.php index b6df454..8078d39 100644 --- a/app/Livewire/Chat/MessageComposer.php +++ b/app/Livewire/Chat/MessageComposer.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Number; use Livewire\Attributes\Locked; use Livewire\Component; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; @@ -41,16 +42,19 @@ class MessageComposer extends Component { $this->body = trim($this->body); $hasAttachments = $this->hasAttachments(); + $maxAttachmentFiles = $this->maxAttachmentFiles(); + $maxAttachmentFileSizeKilobytes = $this->maxAttachmentFileSizeKilobytes(); + $maxAttachmentFileSizeLabel = $this->maxAttachmentFileSizeLabel(); $validated = $this->validate([ 'body' => [$hasAttachments ? 'nullable' : 'required', 'string', 'max:4000'], - 'attachments' => ['array', 'max:5'], - 'attachments.*' => ['file', 'max:10240'], + 'attachments' => ['array', 'max:'.$maxAttachmentFiles], + 'attachments.*' => ['file', 'max:'.$maxAttachmentFileSizeKilobytes], ], [ 'body.required' => __('Write a message or attach a file before sending.'), - 'attachments.max' => __('Attach up to :max files at a time.'), + 'attachments.max' => __('Attach up to :max files at a time.', ['max' => $maxAttachmentFiles]), 'attachments.*.file' => __('Each attachment must be a valid file.'), - 'attachments.*.max' => __('Each attachment must be 10 MB or smaller.'), + 'attachments.*.max' => __('Each attachment must be :size or smaller.', ['size' => $maxAttachmentFileSizeLabel]), ]); $conversation = $this->conversation(); @@ -172,6 +176,14 @@ class MessageComposer extends Component ]; } + public function attachmentLimitSummary(): string + { + return __('Up to :count files, :size each', [ + 'count' => $this->maxAttachmentFiles(), + 'size' => $this->maxAttachmentFileSizeLabel(), + ]); + } + private function conversation(): Conversation { $conversation = Conversation::query() @@ -190,6 +202,21 @@ class MessageComposer extends Component ->isNotEmpty(); } + private function maxAttachmentFiles(): int + { + return (int) config('chat.attachments.max_files', 5); + } + + private function maxAttachmentFileSizeKilobytes(): int + { + return (int) config('chat.attachments.max_file_size_kilobytes', 2 * 1024 * 1024); + } + + private function maxAttachmentFileSizeLabel(): string + { + return Number::fileSize($this->maxAttachmentFileSizeKilobytes() * 1024); + } + private function emojiForCode(string $code): ?string { $code = strtoupper($code); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f1525e9..45a8de7 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -15,7 +15,7 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->configureFileUploads(); } /** @@ -47,4 +47,14 @@ class AppServiceProvider extends ServiceProvider : null, ); } + + protected function configureFileUploads(): void + { + $maxFileSizeKilobytes = (int) config('chat.attachments.max_file_size_kilobytes'); + + config([ + 'livewire.temporary_file_upload.rules' => ['required', 'file', 'max:'.$maxFileSizeKilobytes], + 'livewire.temporary_file_upload.max_upload_time' => (int) config('chat.attachments.livewire_max_upload_time'), + ]); + } } diff --git a/config/chat.php b/config/chat.php new file mode 100644 index 0000000..fbcd8c0 --- /dev/null +++ b/config/chat.php @@ -0,0 +1,9 @@ + [ + 'max_files' => (int) env('CHAT_ATTACHMENT_MAX_FILES', 5), + 'max_file_size_kilobytes' => (int) env('CHAT_ATTACHMENT_MAX_FILE_SIZE_KB', 2 * 1024 * 1024), + 'livewire_max_upload_time' => (int) env('CHAT_LIVEWIRE_MAX_UPLOAD_TIME', 60), + ], +]; diff --git a/public/.user.ini b/public/.user.ini new file mode 100644 index 0000000..71c0173 --- /dev/null +++ b/public/.user.ini @@ -0,0 +1,4 @@ +upload_max_filesize=2048M +post_max_size=2050M +max_input_time=3600 +max_execution_time=3600 diff --git a/resources/views/livewire/chat/message-composer.blade.php b/resources/views/livewire/chat/message-composer.blade.php index 2fbf98a..e46d018 100644 --- a/resources/views/livewire/chat/message-composer.blade.php +++ b/resources/views/livewire/chat/message-composer.blade.php @@ -21,7 +21,7 @@
{{ __('Attachments') }}
-
{{ __('Up to 5 files, 10 MB each') }}
+
{{ $this->attachmentLimitSummary() }}
assertExists($message->attachmentPath()); }); +test('chat file uploads are configured for two gigabyte attachments', function () { + expect(config('chat.attachments.max_file_size_kilobytes'))->toBe(2 * 1024 * 1024) + ->and(config('livewire.temporary_file_upload.rules'))->toContain('max:2097152') + ->and(config('livewire.temporary_file_upload.max_upload_time'))->toBe(60); +}); + +test('participants can send files larger than the previous ten megabyte limit', function () { + Storage::fake('local'); + + $user = User::factory()->create(); + $conversation = Conversation::factory() + ->direct() + ->for($user, 'creator') + ->create(); + + ConversationParticipant::factory()->for($conversation)->for($user)->create(); + + $this->actingAs($user); + + Livewire::test(MessageComposer::class, ['conversationId' => $conversation->id]) + ->set('attachments', [ + UploadedFile::fake()->create('large-export.zip', 13 * 1024, 'application/zip'), + ]) + ->call('sendMessage') + ->assertHasNoErrors(); + + $message = Message::query() + ->where('conversation_id', $conversation->id) + ->where('user_id', $user->id) + ->where('type', Message::TypeFile) + ->first(); + + expect($message)->not->toBeNull(); + expect($message->attachmentName())->toBe('large-export.zip'); +}); + test('file metadata is captured before temporary uploads are moved', function () { Storage::fake('local');