- Bootstrap (critical): settings/create/index no longer 403 on fresh install. Site admins (admin/super_admin) can access settings when 0 groups exist. First group creation seeds default priorities (Low/Medium/High/Urgent). Index shows friendly first-run splash. Create shows warning + settings link. - Internal notes leak (high): submitters can no longer receive is_internal messages via ticket show, index detail panel, or any Inertia prop. filterMessagesForRole() strips internal notes for non-agents. - Arbitrary assignee (med/high): update() now validates assigned_to against actual agent-access users for the ticket's group server-side. - Cross-group priority/project forgery (medium): store() and update() now verify priority_id and project_id belong to the ticket's own group (or are global for priorities). - Foreign message_id on attachment upload (medium): message_id is now validated to belong to the current ticket, not just any message row.
78 lines
2.6 KiB
PHP
78 lines
2.6 KiB
PHP
<?php
|
|
|
|
namespace Dashboard\Ticketing\Http\Controllers;
|
|
|
|
use Dashboard\Ticketing\Models\Ticket;
|
|
use Dashboard\Ticketing\Models\TicketAttachment;
|
|
use Dashboard\Ticketing\Models\TicketingAgentAccess;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Routing\Controller;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class TicketAttachmentController extends Controller
|
|
{
|
|
private const MAX_SIZE_MB = 20;
|
|
|
|
private function canAccessTicket(Ticket $ticket): bool
|
|
{
|
|
$user = Auth::user();
|
|
$isAgent = TicketingAgentAccess::where('user_id', $user->id)
|
|
->where('group_id', $ticket->group_id)
|
|
->exists();
|
|
return $isAgent || $ticket->submitter_id === $user->id;
|
|
}
|
|
|
|
public function store(Request $request, Ticket $ticket)
|
|
{
|
|
if (!$this->canAccessTicket($ticket)) {
|
|
abort(403);
|
|
}
|
|
|
|
$request->validate([
|
|
'file' => 'required|file|max:' . (self::MAX_SIZE_MB * 1024),
|
|
// Bug #5: message_id must belong to THIS ticket, not any arbitrary message
|
|
'message_id' => [
|
|
'nullable',
|
|
'integer',
|
|
function ($attribute, $value, $fail) use ($ticket) {
|
|
if ($value !== null) {
|
|
$exists = \Dashboard\Ticketing\Models\TicketMessage::where('id', $value)
|
|
->where('ticket_id', $ticket->id)
|
|
->exists();
|
|
if (!$exists) {
|
|
$fail('The message does not belong to this ticket.');
|
|
}
|
|
}
|
|
},
|
|
],
|
|
]);
|
|
|
|
$file = $request->file('file');
|
|
$disk = config('ticketing.storage_disk', env('TICKETING_STORAGE_DISK', 'local'));
|
|
$path = $file->store('ticketing/attachments/' . $ticket->id, $disk);
|
|
|
|
TicketAttachment::create([
|
|
'ticket_id' => $ticket->id,
|
|
'message_id' => $request->message_id,
|
|
'filename' => $file->getClientOriginalName(),
|
|
'path' => $path,
|
|
'mime_type' => $file->getMimeType(),
|
|
'size' => $file->getSize(),
|
|
]);
|
|
|
|
return back()->with('success', 'Attachment uploaded.');
|
|
}
|
|
|
|
public function show(TicketAttachment $attachment)
|
|
{
|
|
$ticket = $attachment->ticket;
|
|
if (!$this->canAccessTicket($ticket)) {
|
|
abort(403);
|
|
}
|
|
|
|
$disk = config('ticketing.storage_disk', env('TICKETING_STORAGE_DISK', 'local'));
|
|
return Storage::disk($disk)->download($attachment->path, $attachment->filename);
|
|
}
|
|
}
|