auth public function getAuthProperty() { // Use the active guard from session first, then fallback to checking all guards $activeGuard = session('active_guard', 'web'); $user = Auth::guard($activeGuard)->user(); if ($user) { return $user; } // Fallback to checking all guards if active guard doesn't have a user return Auth::guard('admin')->user() ?: Auth::guard('bank')->user() ?: Auth::guard('organization')->user() ?: Auth::guard('web')->user(); } /** * WireChat overrides Livewire's _finishUpload() to append uploads, but its copy predates * the signed upload references of Livewire 3.8: it used "token:/path" as the file path. * Those files pointed nowhere (sending in the same request as the upload failed with * "Unable to retrieve the file_size ... livewire-tmp/{token}:...") and the signature was * never checked. Verify and strip it like Livewire does, keep WireChat's append behaviour. */ public function _finishUpload($name, $tmpPath, $isMultiple, $append = false) { $this->cleanupOldUploads(); $files = collect($tmpPath)->map(function ($signedPath) { $path = TemporaryUploadedFile::extractPathFromSignedPath((string) $signedPath); abort_if($path === false, 403, 'Invalid upload reference.'); return TemporaryUploadedFile::createFromLivewire($path); })->toArray(); $this->dispatch('upload:finished', name: $name, tmpFilenames: collect($files)->map->getFilename()->toArray())->self(); // If the property is an array, APPEND the upload to the array. $currentValue = $this->getPropertyValue($name); $files = is_array($currentValue) ? array_merge($currentValue, $files) : $files[0]; app('livewire')->updateProperty($this, $name, $files); } /** * Helper to check authorization and show error toast if unauthorized */ private function checkAuthorization(): bool { $profile = getActiveProfile(); if (!$profile) { $this->dispatch('wirechat-toast', type: 'error', message: __('Unauthorized access')); return false; } try { \App\Helpers\ProfileAuthorizationHelper::authorize($profile); return true; } catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { $this->dispatch('wirechat-toast', type: 'error', message: __('Unauthorized access')); return false; } } /** * Override sendMessage to use the correct sendable_id. */ public function sendMessage() { abort_unless($this->auth(), 401); // CRITICAL SECURITY: Validate authorization before sending message $profile = getActiveProfile(); if (!$profile) { abort(403, 'No active profile'); } \App\Helpers\ProfileAuthorizationHelper::authorize($profile); // rate limit $this->rateLimit(); $attachments = array_merge($this->media, $this->files); if (empty($attachments)) { $this->validate(['body' => 'required|string']); } if (count($attachments) != 0) { $maxUploads = config('wirechat.attachments.max_uploads'); $fileMimes = implode(',', config('wirechat.attachments.file_mimes')); $fileMaxUploadSize = (int) config('wirechat.attachments.file_max_upload_size'); $mediaMimes = implode(',', config('wirechat.attachments.media_mimes')); $mediaMaxUploadSize = (int) config('wirechat.attachments.media_max_upload_size'); try { $this->validate([ 'files' => "array|max:$maxUploads|nullable", 'files.*' => "max:$fileMaxUploadSize|mimes:$fileMimes", 'media' => "array|max:$maxUploads|nullable", 'media.*' => "max:$mediaMaxUploadSize|mimes:$mediaMimes", ]); } catch (\Illuminate\Validation\ValidationException $th) { $errors = $th->errors(); foreach ($errors as $field => $messages) { $this->addError($field, $messages[0]); } return $this->dispatch('wirechat-toast', type: 'warning', message: $th->getMessage()); } $createdMessages = []; foreach ($attachments as $key => $attachment) { $path = $attachment->store( \Namu\WireChat\Facades\WireChat::storageFolder(), \Namu\WireChat\Facades\WireChat::storageDisk() ); $replyId = ($key === 0 && $this->replyMessage) ? $this->replyMessage->id : null; $message = \Namu\WireChat\Models\Message::create([ 'reply_id' => $replyId, 'conversation_id' => $this->conversation->id, 'sendable_type' => $this->auth->getMorphClass(), // Polymorphic sender type 'sendable_id' => $this->auth->id, // Polymorphic sender ID 'type' => \Namu\WireChat\Enums\MessageType::ATTACHMENT, ]); $attachment = $message->attachment()->create([ 'file_path' => $path, 'file_name' => basename($path), 'original_name' => $attachment->getClientOriginalName(), 'mime_type' => $attachment->getMimeType(), 'url' => \Storage::disk(\Namu\WireChat\Facades\WireChat::storageDisk())->url($path), ]); $createdMessages[] = $message; $this->conversation->updated_at = now(); $this->conversation->save(); $this->dispatch('refresh')->to(Chats::class); $this->dispatch('refresh')->to(\App\Http\Livewire\UnreadIndicator::class); $this->dispatchMessageCreatedEvent($message); } foreach ($createdMessages as $key => $message) { $this->pushMessage($message); } $this->dispatch('scroll-bottom'); } if ($this->body != null) { $createdMessage = \Namu\WireChat\Models\Message::create([ 'reply_id' => $this->replyMessage?->id, 'conversation_id' => $this->conversation->id, 'sendable_type' => $this->auth->getMorphClass(), // Polymorphic sender type 'sendable_id' => $this->auth->id, // Polymorphic sender ID 'body' => $this->body, 'type' => \Namu\WireChat\Enums\MessageType::TEXT, ]); $this->pushMessage($createdMessage); $this->conversation->touch(); $this->dispatchMessageCreatedEvent($createdMessage); $this->dispatch('refresh')->to(Chats::class); $this->dispatch('refreshList')->to(Chats::class); // Also dispatch to UnreadIndicator to update the red dot $this->dispatch('refresh')->to(\App\Http\Livewire\UnreadIndicator::class); } $this->reset('media', 'files', 'body'); $this->dispatch('scroll-bottom'); $this->removeReply(); } private function pushMessage(\Namu\WireChat\Models\Message $message) { $groupKey = $this->messageGroupKey($message); // Ensure loadedMessages is a Collection $this->loadedMessages = collect($this->loadedMessages); // Use tap to create a new group if it doesn’t exist, then push the message $this->loadedMessages->put($groupKey, $this->loadedMessages->get($groupKey, collect())->push($message)); } private function messageGroupKey(\Namu\WireChat\Models\Message $message): string { $messageDate = $message->created_at; $groupKey = ''; if ($messageDate->isToday()) { $groupKey = __('wirechat::chat.message_groups.today'); } elseif ($messageDate->isYesterday()) { $groupKey = __('wirechat::chat.message_groups.yesterday'); } elseif ($messageDate->greaterThanOrEqualTo(now()->subDays(7))) { $groupKey = $messageDate->format('l'); // Day name } else { $groupKey = $messageDate->format('d/m/Y'); // Older than 7 days, dd/mm/yyyy } return $groupKey; } /** * Keep a message from auto-deletion (mark as kept) */ public function keepMessage(string $id): void { if (!$this->checkAuthorization()) { return; } if (!$this->auth()) { $this->dispatch('wirechat-toast', type: 'error', message: __('Unauthorized access')); return; } $id = decrypt($id); $message = \Namu\WireChat\Models\Message::findOrFail($id); // Verify user belongs to the conversation if (!$this->auth->belongsToConversation($message->conversation)) { $this->dispatch('wirechat-toast', type: 'error', message: __('Unauthorized access')); return; } // Check if disappearing messages is enabled and allow_users_to_keep is true if (!timebank_config('wirechat.disappearing_messages.enabled', true) || !timebank_config('wirechat.disappearing_messages.allow_users_to_keep', true)) { return; } // Toggle kept status if ($message->kept_at) { // Unkeep the message $message->kept_at = null; } else { // Keep the message $message->kept_at = now(); } $message->save(); // Update the message in loadedMessages collection to trigger Livewire re-render if ($this->loadedMessages) { $this->loadedMessages = collect($this->loadedMessages)->map(function ($messageGroup) use ($message) { return $messageGroup->map(function ($loadedMessage) use ($message) { if ($loadedMessage->id === $message->id) { $loadedMessage->kept_at = $message->kept_at; } return $loadedMessage; }); }); } } /** * Delete conversation (override with authorization) */ public function deleteConversation() { if (!$this->checkAuthorization()) { return; } return parent::deleteConversation(); } /** * Clear conversation history (override with authorization) */ public function clearConversation() { if (!$this->checkAuthorization()) { return; } return parent::clearConversation(); } /** * Exit conversation (override with authorization) */ public function exitConversation() { if (!$this->checkAuthorization()) { return; } return parent::exitConversation(); } /** * Delete message for me (override with authorization) */ public function deleteForMe(string $id): void { if (!$this->checkAuthorization()) { return; } parent::deleteForMe($id); } /** * Delete message for everyone (override with authorization) */ public function deleteForEveryone(string $id): void { if (!$this->checkAuthorization()) { return; } parent::deleteForEveryone($id); } /** * Send like (override with authorization) */ public function sendLike() { if (!$this->checkAuthorization()) { return; } return parent::sendLike(); } /** * Set reply (override with authorization) */ public function setReply(string $id): void { if (!$this->checkAuthorization()) { return; } parent::setReply($id); } /** * Override to fire the app event so SendEmailNewMessage listener is triggered. * Using event() on a ShouldBroadcast event handles both WebSocket broadcasting * and listener dispatch in a single call, preventing double-firing. */ protected function dispatchMessageCreatedEvent(\Namu\WireChat\Models\Message $message): void { if ($this->conversation->isSelf()) { return; } try { event(new MessageCreated($message)); \Namu\WireChat\Jobs\NotifyParticipants::dispatch($this->conversation, $message); } catch (\Throwable $th) { \Illuminate\Support\Facades\Log::error($th->getMessage()); } } /** * Mount component (override with authorization) */ public function mount($conversation = null) { // CRITICAL SECURITY: Validate authorization on mount $profile = getActiveProfile(); if (!$profile) { abort(403, 'No active profile'); } \App\Helpers\ProfileAuthorizationHelper::authorize($profile); parent::mount($conversation); $prefill = request()->query('prefill'); if ($prefill) { $this->body = $prefill; } } public function render() { // CRITICAL SECURITY: Re-validate authorization on every render $profile = getActiveProfile(); if (!$profile) { return view('errors.unauthorized-component'); } try { \App\Helpers\ProfileAuthorizationHelper::authorize($profile); } catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { return view('errors.unauthorized-component'); } return parent::render(); } }