Files
timebank-cc-public/app/Http/Livewire/WireChat/Chat/Chat.php
T

417 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Livewire\WireChat\Chat;
use App\Http\Livewire\WireChat\Chats\Chats;
use Illuminate\Support\Facades\Auth;
use Namu\WireChat\Events\MessageCreated;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Namu\WireChat\Livewire\Chat\Chat as BaseChat;
class Chat extends BaseChat
{
// This magic accessor will be called for $this->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();
}
}