Snapshot of main at 9f39a6e1e (2026-11-02)

This commit is contained in:
Ronald Huynen committed 2026-11-02 10:41:47 +01:00
commit 6ccf1792d7
2580 files changed
+1040202

No files matched your search

+416
View File
@@ -0,0 +1,416 @@
<?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();
}
}