Feature allow agents to attach files to tickets in the app

This commit is contained in:
johnnyq
2026-07-29 17:58:04 -04:00
parent e74307eaea
commit 7c8c93c01c
5 changed files with 153 additions and 40 deletions

View File

@@ -20,7 +20,7 @@ ob_start();
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<!-- Hidden/System fields -->
<?php if ($client_id) { ?>
@@ -203,6 +203,11 @@ ob_start();
</div>
<?php } ?>
<div class="form-group">
<label><i class="fa fa-fw fa-paperclip mr-1"></i>Attachments</label>
<input type="file" class="form-control-file" name="attachments[]" multiple accept=".jpg, .jpeg, .gif, .png, .webp, .pdf, .txt, .md, .doc, .docx, .odt, .csv, .xls, .xlsx, .ods, .pptx, .odp, .zip, .tar, .gz, .xml, .msg, .json, .wav, .mp3, .ogg, .mov, .mp4, .av1, .ovpn">
</div>
</div>
<div class="tab-pane fade" id="pills-add-tasks">

View File

@@ -89,6 +89,9 @@ if (isset($_POST['add_ticket'])) {
addTasksFromTicketTemplate($ticket_id, $ticket_template_id);
}
// Store any attached files against the ticket itself
saveTicketAttachments($ticket_id, null);
// Add Watchers
if (isset($_POST['watchers'])) {
foreach ($_POST['watchers'] as $watcher) {
@@ -1957,12 +1960,62 @@ if (isset($_POST['add_ticket_reply'])) {
flashAlert("Ticket updated");
}
// Store any attached files. They hang off the reply when there is one, and off
// the ticket itself when a file was uploaded without any accompanying text.
saveTicketAttachments($ticket_id, $ticket_reply_id ?: null);
logAudit("Ticket", "Reply", "$session_name replied to ticket $ticket_prefix$ticket_number - $ticket_subject and was a $ticket_reply_type reply", $client_id, $ticket_id);
redirect();
}
if (isset($_GET['delete_ticket_attachment'])) {
validateCSRFToken();
enforceUserPermission('module_support', 3);
$attachment_id = intval($_GET['delete_ticket_attachment']);
$sql = mysqli_query($mysqli, "SELECT ticket_attachment_name, ticket_attachment_reference_name, ticket_attachment_ticket_id FROM ticket_attachments WHERE ticket_attachment_id = $attachment_id LIMIT 1");
if (mysqli_num_rows($sql) !== 1) {
flashAlert("Attachment not found", 'error');
redirect();
}
$row = mysqli_fetch_assoc($sql);
$attachment_name = escapeSql($row['ticket_attachment_name']);
$attachment_reference_name = $row['ticket_attachment_reference_name'];
$ticket_id = intval($row['ticket_attachment_ticket_id']);
$client_id = intval(getFieldById('tickets', $ticket_id, 'ticket_client_id'));
// Don't Enforce Client Access if Ticket doesn't have an assigned client
if ($client_id) {
enforceClientAccess();
}
// Resolve the path and confirm it is still inside uploads before unlinking,
// the same guard the download endpoint applies
$uploads_base = realpath(__DIR__ . "/../../uploads");
$file_path = realpath(__DIR__ . "/../../uploads/tickets/$ticket_id/$attachment_reference_name");
if ($file_path !== false && $uploads_base !== false && strpos($file_path, $uploads_base) === 0) {
unlink($file_path);
}
mysqli_query($mysqli, "DELETE FROM ticket_attachments WHERE ticket_attachment_id = $attachment_id");
logAudit("Ticket", "Delete", "$session_name deleted ticket attachment $attachment_name", $client_id, $ticket_id);
flashAlert("Attachment <strong>$attachment_name</strong> deleted", 'error');
redirect();
}
if (isset($_POST['edit_ticket_reply'])) {
validateCSRFToken();

View File

@@ -596,7 +596,7 @@ if (isset($_GET['ticket_id'])) {
<tr>
<td><i class='fas fa-fw fa-paperclip text-secondary mr-1'></i><?= $name ?></td>
<td>
<a target='_blank' class='mr-1 ml-1' href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>&action=view'>[View]</a><a href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>'>[Download]</a>
<a target='_blank' class='mr-1 ml-1' href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>&action=view'>[View]</a><a href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>'>[Download]</a><?php if (lookupUserPermission("module_support") >= 3) { ?><a class='confirm-link ml-1 text-danger' href='post.php?delete_ticket_attachment=<?= $ticket_attachment_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>'>[Delete]</a><?php } ?>
</td>
</tr>
@@ -611,7 +611,7 @@ if (isset($_GET['ticket_id'])) {
<!-- Only show ticket reply modal if status is not closed -->
<?php if (lookupUserPermission("module_support") >= 2 && empty($ticket_resolved_at) && empty($ticket_closed_at)) { ?>
<form action="post.php" method="post" autocomplete="off">
<form action="post.php" method="post" enctype="multipart/form-data" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="ticket_id" id="ticket_id" value="<?= $ticket_id ?>">
@@ -642,6 +642,12 @@ if (isset($_GET['ticket_id'])) {
</textarea>
</div>
<div class="form-group">
<label class="text-muted small mb-1"><i class="fa fa-fw fa-paperclip mr-1"></i>Attachments</label>
<input type="file" class="form-control-file" name="attachments[]" multiple accept=".jpg, .jpeg, .gif, .png, .webp, .pdf, .txt, .md, .doc, .docx, .odt, .csv, .xls, .xlsx, .ods, .pptx, .odp, .zip, .tar, .gz, .xml, .msg, .json, .wav, .mp3, .ogg, .mov, .mp4, .av1, .ovpn">
<small class="form-text text-muted">Stored against this reply, or against the ticket itself if you submit without any text. Attachments are not included in the emailed copy of a public reply.</small>
</div>
<div class="form-row">
<div class="col-md-3">
<div class="form-group">
@@ -830,7 +836,7 @@ if (isset($_GET['ticket_id'])) {
<tr>
<td><i class='fas fa-fw fa-paperclip text-secondary mr-1'></i><?= $name ?></td>
<td>
<a target='_blank' class='mr-1 ml-1' href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>&action=view'>[View]</a><a href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>'>[Download]</a>
<a target='_blank' class='mr-1 ml-1' href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>&action=view'>[View]</a><a href='ticket_attachment.php?attachment_id=<?= $ticket_attachment_id; ?>'>[Download]</a><?php if (lookupUserPermission("module_support") >= 3) { ?><a class='confirm-link ml-1 text-danger' href='post.php?delete_ticket_attachment=<?= $ticket_attachment_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>'>[Delete]</a><?php } ?>
</td>
</tr>